Compare commits

..

164 Commits

Author SHA1 Message Date
Patrick Goldinger
37bb4cea43 Release v0.3.13-beta04 2021-06-08 00:05:36 +02:00
Patrick Goldinger
79d608feea Update translations from Crowdin 2021-06-07 23:53:37 +02:00
Patrick Goldinger
54573de3e3 Merge pull request #1006 from Luensche/move_clipboard_item_to_begin
Move new clipboard items with the same content to the beginning
2021-06-07 23:51:35 +02:00
Björn Engel
2fba2d3b4a Do not compare images 2021-06-07 15:49:05 +02:00
Björn Engel
fd0cbbdcb1 Move new clipboard items with the same content to the beginning, closes #991 2021-06-07 14:08:39 +02:00
Patrick Goldinger
b6e3deedf4 Add default system subtype for proper display in system settings 2021-06-04 19:39:02 +02:00
Patrick Goldinger
4c74bf1b4a Fix glide typing not working for caps/caps-lock 2021-06-04 19:19:53 +02:00
Patrick Goldinger
2a4e3c8c58 Merge pull request #982 from dessalines/halmak
Add the Halmak keyboard layout
2021-06-04 18:56:48 +02:00
Patrick Goldinger
e34e5b4260 Merge pull request #992 from florisboard/rework-textkeyboard-rendering
Rework TextKeyboard rendering
2021-06-04 18:53:42 +02:00
Patrick Goldinger
ae2df7dfe4 Fix Smartbar incorrectly not updating selection-specific keys 2021-06-04 18:49:48 +02:00
Patrick Goldinger
1b3d0a5cf2 Fix touch logic incorrect pointer and capacity issues 2021-06-04 18:31:05 +02:00
Patrick Goldinger
4c94329071 Fix glide typing not correctly initialized at startup 2021-06-04 17:13:16 +02:00
Patrick Goldinger
6ffcf2f865 Fix keyboard preview in Settings 2021-06-04 05:43:42 +02:00
Patrick Goldinger
e2c9a66880 Fix further state bugs 2021-06-04 05:12:38 +02:00
Patrick Goldinger
e9bc25ebc7 Improve extended popup rendering performance 2021-06-04 03:53:03 +02:00
Patrick Goldinger
6379e63669 Rework TextKeyboard rendering 2021-06-04 03:31:46 +02:00
Patrick Goldinger
70a0763e7f Merge pull request #981 from florisboard/fix-keyboard-state-bug
Fix keyboard state bug for the active mode
2021-06-04 03:29:47 +02:00
Dessalines
863080e6ce Remove slash from bottom row. 2021-06-03 14:46:55 -04:00
Patrick Goldinger
3ef454b8bd Fix Smartbar not showing sometimes (#987) 2021-06-03 17:43:23 +02:00
Patrick Goldinger
2bbdfc71d0 Rework UI initialization and reduce duplicate state changes 2021-06-03 15:42:28 +02:00
Patrick Goldinger
d1c783dde1 Fix keyboard state bug for the active mode 2021-06-02 17:51:18 +02:00
Dessalines
644da67601 Add the Halmak keyboard layout 2021-06-01 21:39:26 -04:00
Patrick Goldinger
b8d99efd29 Merge pull request #977 from GoRaN909/patch-5
Update kurdish.json
2021-06-01 01:15:58 +02:00
GoRaN
4067d92a44 Update kurdish.json
Added stretched button (Kashida) to support all Kurdish layouts.
2021-06-01 01:06:00 +03:00
Patrick Goldinger
13a17f3a6b Merge pull request #974 from GoRaN909/patch-2
Update ckb.json
2021-05-31 23:50:01 +02:00
Patrick Goldinger
57c679e500 Merge pull request #975 from GoRaN909/patch-3
Update kurdish_standard.json
2021-05-31 23:41:42 +02:00
Patrick Goldinger
f70f45dab6 Merge pull request #973 from GoRaN909/patch-1
Update kurdish.json
2021-05-31 23:37:41 +02:00
GoRaN
8d8f723d66 Update kurdish_standard.json
popup characters added
2021-06-01 00:29:23 +03:00
GoRaN
7c3c6a7ad7 Update ckb.json
Added popup characters for letter (ح)
2021-06-01 00:24:18 +03:00
GoRaN
d7a1c9377a Update kurdish.json
Some changes of words position and corrections codes
2021-06-01 00:19:19 +03:00
Patrick Goldinger
2a317372b2 Release v0.3.13-beta03 2021-05-31 20:18:43 +02:00
Patrick Goldinger
402f7bd267 Update translations from Crowdin 2021-05-31 20:02:33 +02:00
Patrick Goldinger
e8eb6e3068 Fix emoticon layout missing (#950) 2021-05-31 19:17:38 +02:00
Patrick Goldinger
3dd9c45777 Fix crash when using delete left swipe in raw editors (#967) 2021-05-31 18:30:24 +02:00
Patrick Goldinger
7255229361 Merge pull request #966 from florisboard/major-input-logic-overhaul
Major input logic overhaul
2021-05-31 17:52:19 +02:00
Patrick Goldinger
4d2fa29886 Fix IME checking utility not using new ID 2021-05-31 12:46:14 +02:00
Patrick Goldinger
ef90faf98b Merge pull request #963 from Hayleia/composingFix
Fix getting composer from name
2021-05-31 06:11:38 +02:00
Patrick Goldinger
82caa8365e Fix glide trail stuck after initial touch down 2021-05-31 05:16:20 +02:00
Patrick Goldinger
391257e9e9 Re-add simple key shadows 2021-05-31 05:04:02 +02:00
Patrick Goldinger
b082253167 Fix keys not registered correctly (#953) 2021-05-31 03:59:31 +02:00
Patrick Goldinger
8df701e3fe Adapt input view to new keyboard state register 2021-05-31 03:56:08 +02:00
Patrick Goldinger
9f232f5dbf Add new keyboard state register 2021-05-31 03:55:05 +02:00
Hayleia
7017726dcb Fix getting composer from name
also use an available constant when possible rather than a hardcoded string
2021-05-30 11:05:28 +02:00
Patrick Goldinger
b48ca8fd1e Restructure the package structure 2021-05-28 21:04:27 +02:00
Patrick Goldinger
88d5e15a5e Introduce TextKeyboardState 2021-05-28 03:36:54 +02:00
Patrick Goldinger
e9537cbd1d Merge pull request #947 from yashpalgoyal1304/devanagari-fix
Fixed Devanagari Codes
2021-05-26 23:32:10 +02:00
yashpalgoyal1304
8e216bf3ac Fixed Devanagari Codes 2021-05-27 02:37:14 +05:30
Patrick Goldinger
63352cc615 Improve logic and rendering performance a bit 2021-05-26 17:12:28 +02:00
Patrick Goldinger
e9e2563739 Release v0.3.13-beta02 2021-05-26 01:26:33 +02:00
Patrick Goldinger
87bb098445 Fix batch level preventing cached input from updating 2021-05-26 01:26:17 +02:00
Patrick Goldinger
da1944bedf Temporarily remove key shadow support (#943) 2021-05-26 01:09:50 +02:00
Patrick Goldinger
d4a92e0d46 Merge pull request #942 from florisboard/new-touch-logic
Introduce new touch logic to TextKeyboardView
2021-05-26 00:46:31 +02:00
yashpalgoyal1304
0fa6c1f235 Added Indic Numerals (#940)
* Indic Devanagari Numeric

* Fixed name and label

* Fixed file name

* Added indic scripts numerals
2021-05-26 00:43:21 +02:00
Patrick Goldinger
260b1ba5ca Improve touch logic 2021-05-26 00:19:35 +02:00
Patrick Goldinger
f0799a6a0e Rework text keyboard view touch logic 2021-05-25 20:48:17 +02:00
Patrick Goldinger
155238946a Merge pull request #866 from Hayleia/composing1
Composing input method (and Korean as the first subject)
2021-05-24 15:30:06 +02:00
Patrick Goldinger
45f91cf40c Merge pull request #928 from ostrya/fix-hint-merge
fix hint merge logic (#872)
2021-05-23 16:27:22 +02:00
Patrick Goldinger
94f5b56b6a Possibly fix key shadow performance 2021-05-23 16:19:28 +02:00
Kai Helbig
46db467073 fix hint merge logic (#872)
The merge of the hints depends on the underlying main key. Especially,
hints should only be shown for character keys, and if the hint is
identical to the main key, it should not be added at all. Since the
actual main key is only evaluated on demand with TextKey#compute, all
corresponding hint merge logic needs to be moved there too.
2021-05-23 12:16:33 +02:00
Patrick Goldinger
17dde536d9 Fix one-handed panel not correctly measuring sometimes (#896) 2021-05-23 03:50:17 +02:00
Patrick Goldinger
be67bf4b84 Fix Smartbar number row bugs in password fields (#905) 2021-05-23 03:19:17 +02:00
Patrick Goldinger
8f142548fe Merge pull request #920 from tsiflimagas/default-popup-fix-greek
Fix the default popup for some letters
2021-05-23 02:49:28 +02:00
Kostas Giapis
a68f439f39 Enforce the main popup character 2021-05-22 23:01:04 +03:00
Patrick Goldinger
7a0892bb36 Fix space bar text too large (#862) 2021-05-22 20:16:55 +02:00
Patrick Goldinger
8457390156 Fix keys not showing a shadow (#901, #921) 2021-05-22 19:54:12 +02:00
Hayleia
72be3898c1 move local function out, and fix firefox url bar? 2021-05-22 19:47:30 +02:00
Kostas Giapis
d35bf5af63 Fix the default popup for some letters 2021-05-22 16:23:13 +03:00
Patrick Goldinger
04d3af6484 Merge pull request #908 from Luensche/copy-versionstring
Copy version string to clipboard on click on the version
2021-05-22 12:59:46 +02:00
Björn Engel
26920e4a98 Move the toast outside of if 2021-05-20 14:44:23 +02:00
Björn Engel
7419966b51 Create ripple for click on head_area 2021-05-20 14:37:17 +02:00
Björn Engel
58b832c6c3 Add new area for long pressing and change to onLongClickListener 2021-05-20 10:20:49 +02:00
Hayleia
99f2ec1879 deprecated methods 2021-05-19 11:47:28 +02:00
Hayleia
4249f9ef86 add author 2021-05-19 11:39:13 +02:00
Hayleia
60107ae299 useless "public" keyword 2021-05-19 09:11:07 +02:00
Hayleia
6a95a865fa one spinner per linear layout 2021-05-19 09:09:14 +02:00
Hayleia
9e32589af5 style: space before colon 2021-05-19 09:04:30 +02:00
Hayleia
6133e225e1 add author 2021-05-19 09:03:34 +02:00
Hayleia
348c143d92 use case_selector to specify shift/non-shift characters 2021-05-19 08:59:52 +02:00
Hayleia
ce00785ffe Revert "support specifying uppercase and lowercase separately in json"
This reverts commit 1715e5ddfa.

Conflicts:
	app/src/main/java/dev/patrickgold/florisboard/ime/extension/AssetManager.kt
2021-05-19 08:24:51 +02:00
Hayleia
78cdce750d style in json 2021-05-19 08:22:25 +02:00
Patrick Goldinger
f3f95ae282 Fix crash loops from occurring after a crash (#910) 2021-05-19 01:33:53 +02:00
Björn Engel
018885eb30 Copy version string to clipboard on click on the version 2021-05-18 15:18:01 +02:00
Patrick Goldinger
c6c8a76dd6 Fix user dictionary max size (#898) 2021-05-18 01:51:49 +02:00
Patrick Goldinger
3cae8b7230 Release v0.3.13-beta01 2021-05-17 20:40:39 +02:00
Patrick Goldinger
814c8de0c2 Update translations from Crowdin 2021-05-17 20:30:37 +02:00
Patrick Goldinger
32fe175b48 Small code base improvements 2021-05-17 20:27:32 +02:00
Patrick Goldinger
b901f6de8d Fix space bar gestures for non-repeating actions (#886) 2021-05-17 20:13:50 +02:00
Patrick Goldinger
fe9ba3246c Merge pull request #884 from debnone/patch-1
Fix hebrew characters
2021-05-17 19:52:32 +02:00
Patrick Goldinger
71a39f0fc1 Merge pull request #876 from florisboard/android11-autofill-api
Add support for Android 11's Autofill API
2021-05-17 10:56:31 +02:00
Patrick Goldinger
f7556898e1 Document inline suggestions code / Fix some inconsistencies 2021-05-17 03:01:46 +02:00
Patrick Goldinger
578539f5d0 Add inline suggestions theme support 2021-05-17 02:04:52 +02:00
debnone
7c28c7fbea Fix hebrew characters
fixed bottom half layout its was reversed and incorrect.
2021-05-15 23:17:28 +03:00
Patrick Goldinger
88bcadff81 Fix inline suggestions state bugs and improve logic 2021-05-15 04:50:49 +02:00
Patrick Goldinger
25e25dfbf0 Add support for Android 11's Autofill API 2021-05-15 03:23:51 +02:00
Patrick Goldinger
ba3dc0178d Merge pull request #875 from X-yl/glide-number-row
Reinitialize pruner when layout changes
2021-05-15 03:20:23 +02:00
x-yl
91e7f424bb Reinitialize pruner when layout changes
Closes #854
2021-05-14 22:16:10 +04:00
Hayleia
b89f791eb0 rename south korean layout 2021-05-14 07:51:51 +02:00
Hayleia
ad3a0425ab fix config.json after merge 2021-05-14 07:51:40 +02:00
Hayleia
7cf52ecf3e Merge branch 'master' of https://github.com/florisboard/florisboard into composing1 2021-05-14 07:35:56 +02:00
Patrick Goldinger
b1ef18f4fd Improve C++ code base 2021-05-14 00:30:19 +02:00
Hayleia
b74af5bbe9 manage old subtype configurations 2021-05-13 20:48:00 +02:00
Hayleia
b8aa4bbfc4 fix subtype equals and hashcode (and javadoc) 2021-05-13 20:16:50 +02:00
Hayleia
e024ac9272 fix default subtype crash with no subtype declared 2021-05-13 20:03:47 +02:00
Hayleia
c5fa027a8e move composer dropdown in add/edit subtype dialog 2021-05-13 16:39:32 +02:00
Hayleia
b6ec2b25be Merge branch 'master' of https://github.com/florisboard/florisboard into composing1 2021-05-13 16:25:13 +02:00
Patrick Goldinger
a756b59c60 Merge pull request #606 from ostrya/improved-hints
Merge hints more flexibly
2021-05-13 14:04:08 +02:00
Patrick Goldinger
8687ce55ed Merge pull request #527 from ostrya/neo2-layout
Neo2 layout
2021-05-13 14:04:01 +02:00
ostrya
1ac6985dd0 Allow merging popups of hints #618
A new configuration was introduced to allow showing the popup keys of
the hint keys of a given character key in addition the character key's
normal popup keys.

The previous change allowed both number and symbol hint to be merged at
the same time, with the number hint being shown as popup only.
Therefore, when allowing the popups of the hint key to be shown as
popups, both hint keys need to be taken into account.

To ensure this and also take into account the separate key hint
settings for number and symbol hints, the MutablePopupSet was extended
to contain both hint keys as well as both lists of popup keys in
addition to the existing main key and relevant list. The logic that
chooses the key prioritization when rendering the popup has now also
been moved from the PopupManager to the PopupSet.

For performance, the prioritized collection of popup keys is generated
once and then cached for a given configuration in a new PopupKeys
object. This class now has the collection semantics previously present
in the PopupSet class. Different from before, the PopupKeys object now
explicitly contains the prioritized keys (those that should be shown
directly above the original key for easier reach) in order of priority.

The PopupManager now only needs to take the number of prioritized keys
(maximum 3: main key, number hint, symbol hint) when calculating the
key positions in the popup.
2021-05-13 11:52:53 +02:00
Patrick Goldinger
986b4a878f Merge pull request #858 from florisboard/java-jni-basics
Set up base for Kotlin/C++ interoperability
2021-05-13 00:33:10 +02:00
Patrick Goldinger
1ef38fe7f3 Fix GitHub workflows not setting up cmake 2021-05-12 20:31:34 +02:00
Patrick Goldinger
bcad0af35e Finalize base implementation for SuggestionList 2021-05-12 19:29:21 +02:00
Patrick Goldinger
b5b89fde4f Add native instance wrapper interface / Clean up code 2021-05-12 02:25:41 +02:00
Patrick Goldinger
be1fc710ed Set up base for Kotlin/C++ interoperability 2021-05-12 00:40:53 +02:00
Kai Helbig
aa55fd3070 Directly merge numeric and symbolic hints
Co-authored-by: Patrick Goldinger <patrick.goldinger@pm.me>
2021-05-11 23:58:31 +02:00
ostrya
a132462466 Merge hints more flexibly
To allow symbol layouts with the same or more rows as the character
layout to be hinted more consistently, the hinting of the numeric row
is split from the rest of the symbol layout.

If enabled, the numeric row hinting is always done in the first row.
If an actual numeric row is enabled as well, no additional numeric
hints will be shown (as they are only added to CHARACTER type keys).

The symbol hinting is now bottom-aligned: hints from the last symbol
row are shown in the last character row.

If the symbol layout (excluding numeric row) has at least the same
number of rows as the character layout, the numerical row is disabled
and numerical hinting is enabled, the symbol keys take precedence. The
numeric hints are instead added as additional popup characters.
2021-05-11 23:58:25 +02:00
Hayleia
df393ff607 composers can be specified in config.json
no compatibility with previous settings, need to update the regex
2021-05-11 19:03:30 +02:00
Patrick Goldinger
64040f0407 Release v0.3.12 2021-05-10 15:47:05 +02:00
Patrick Goldinger
0c1abdd507 Merge pull request #850 from X-yl/master
Stop glide suggestions disappearing and remove the redundant first suggestion
2021-05-10 15:28:21 +02:00
Patrick Goldinger
53594e3343 Fix glide logic not triggering when shift/caps is active (#847) 2021-05-10 15:22:45 +02:00
X-yl
c6c06b87c5 Stop glide suggestions disappearing and remove redundant first option 2021-05-10 16:52:50 +04:00
Patrick Goldinger
ae6eb5d72d Release v0.3.11 2021-05-10 00:06:07 +02:00
Patrick Goldinger
bbce53fdf4 Update README and open-source licenses 2021-05-09 20:45:33 +02:00
Patrick Goldinger
fdd7e60c1d Release v0.3.11-beta06 2021-05-09 16:49:10 +02:00
Patrick Goldinger
3b9a489d5c Update translations from Crowdin 2021-05-09 16:43:35 +02:00
Patrick Goldinger
de40ccb759 Fix KeyboardView null issue and rename Preferences (#785) 2021-05-09 16:30:52 +02:00
Patrick Goldinger
a04d584402 Remove unused dimension updating in glide typing manager 2021-05-09 15:09:51 +02:00
Patrick Goldinger
a14a6a798b Merge pull request #843 from X-yl/main-thread-clip
Set up clipboard history view on main thread
2021-05-09 14:57:42 +02:00
Patrick Goldinger
636d329dba Merge pull request #842 from X-yl/quick-glide-fix
Make normalizeBoxSide have a non-zero minimum longestSide
2021-05-09 14:56:16 +02:00
X-yl
d1e97dac57 Setup clipboard history on main thread.
No idea why it was on a different thread before? Think I just copied it
like that from MediaInputManager. Oops.
2021-05-09 16:36:40 +04:00
X-yl
41fbca8f65 Make normalizeBoxSide have a minimum longestSide
If it is zero, as sometimes happens because the dictionary contains
"words" like "yyy" and "ggg", it causes NaN issues.
2021-05-09 16:22:51 +04:00
Patrick Goldinger
535b48e5b4 Re-implement glide typing for new keyboard view 2021-05-09 13:53:27 +02:00
Patrick Goldinger
d3e8d35e5d Release v0.3.11-beta05 2021-05-08 15:40:58 +02:00
Patrick Goldinger
da8073141e Fix dynamic text size infinite loop bug (#825) 2021-05-08 15:35:16 +02:00
Patrick Goldinger
030665732a Merge pull request #817 from florisboard/user-dictionary
System / internal user dictionary
2021-05-08 11:22:32 +02:00
Patrick Goldinger
cc042dd77c Add input validation for user dictionary add/edit dialogs 2021-05-07 20:52:30 +02:00
Patrick Goldinger
773624769d Add shortcut support for user dictionary 2021-05-07 19:21:15 +02:00
Patrick Goldinger
0b1d0c74fe Fix query syntax issues for the system user dictionary 2021-05-07 04:22:17 +02:00
Patrick Goldinger
760d307478 Improve user dictionary UI 2021-05-07 04:01:47 +02:00
Patrick Goldinger
084c2abfc2 Add user dictionary manager UI for system and internal 2021-05-07 03:51:40 +02:00
Patrick Goldinger
df6b08024f Fix SQL user input causing crash 2021-05-06 19:04:36 +02:00
Patrick Goldinger
25498695ef Add basic UI wrapper for managing user dictionaries 2021-05-06 18:16:38 +02:00
Patrick Goldinger
5c81179d60 Add import/export backend logic for user dictionaries 2021-05-06 18:16:01 +02:00
Patrick Goldinger
58d150bb03 Update Kotlin to 1.5.0 2021-05-06 01:05:45 +02:00
Patrick Goldinger
2b1951ea5f Add internal and system user dictionary 2021-05-05 21:07:59 +02:00
Patrick Goldinger
5a5089c413 Fix AppCompat theme crash for Huawei devices (#799, #809) 2021-05-05 20:34:16 +02:00
Patrick Goldinger
dcd20e4b73 Add user dictionary preferences 2021-05-05 18:32:20 +02:00
Hayleia
88a6f436ef Merge branch 'master' of https://github.com/florisboard/florisboard into composing1 2021-05-05 10:02:17 +02:00
ostrya
ee8f44d816 Use new currency set mechanism 2021-05-04 20:52:53 +02:00
ostrya
0308ec355f Adapt to new layout rework 2021-05-04 20:44:57 +02:00
Hayleia
3ac14f8a2a remove pointless reflection (going to use serialization anyways) 2021-05-04 20:16:23 +02:00
Hayleia
2b087b76dc korean double consonants and two vowels on shift key 2021-05-04 20:12:03 +02:00
Hayleia
1715e5ddfa support specifying uppercase and lowercase separately in json 2021-05-04 20:11:27 +02:00
Hayleia
6cc17161a5 factor stuff 2021-05-03 21:00:04 +02:00
Hayleia
5d1c20617b Merge branch 'master' of https://github.com/florisboard/florisboard into composing1 2021-05-03 19:22:23 +02:00
Hayleia
d9efa48c9c copy pasted code to compose texte with suggestions enabled too 2021-05-03 19:15:03 +02:00
ostrya
dedd4cb7f0 Use custom modifier for symbol layer
To make the switch from character to symbol layer more consistent,
a neo specific symbol modifier layout was added. This also allows
overriding the comma and full stop with their layer 3 equivalents.
2021-05-02 17:06:07 +02:00
ostrya
42b147b656 Add neo/bone locale variant for better compatibility
The default de locale already defines a lot of extended popups which
do not match the Neo2 / Bone layout logic. Adding a locale variant
allows overriding those defaults.

As the Locale class does not support arbitrary country keys, the new
locale was chosen as a variant of de_DE with variant name "neobone".
There is no deep meaning in the name, it is only the concatenation of
neo and bone, and according to the Javadoc of Locale, a valid variant
must have either 5 to 8 characters or start with a number.
2021-05-02 17:06:06 +02:00
ostrya
47ce490d6c Initial attempt at Neo2 / Bone layout (#498)
* For now, only layers 1, 2 and 3 are supported.
* Layer 2 is reachable via caps, apart from number row, comma and full
  stop (which I think are easier to use if not affected by caps).
  Instead, the relevant characters are added as popups.
* Layer 3 is set up as a separate neo2 symbol / number row layer

The overall layout is kept as much as possible, with the following
exceptions:
* The number row contains only numbers and minus sign, while circumflex
  and grave accents are not included.
* To not overcrowd the layout and have the same number of keys for
  first and second row, the acute accent is not included as separate
  key but can be reached as additional popup to sharp s.
* Comma and full stop are not put between m and j (or z and k
  respectively), because the backspace takes up too much space for both
  keys to be put in this row.
* Also, having comma and full stop on the same height with the space
  key makes the layout more consistent with the existing layouts and
  the special usage as ~left and ~right keys.
2021-05-02 17:05:59 +02:00
Hayleia
5563a1cadd merge compatibility 2021-05-01 20:30:24 +02:00
Hayleia
7beb2e5ef6 Merge branch 'master' of https://github.com/florisboard/florisboard into composing1 2021-05-01 19:22:19 +02:00
Hayleia
f00da13cba less kotlin warnings and slightly more usable code
still hardcoded korean composer for all layouts
but at least it's not instanciated at every keypress
2021-05-01 09:34:40 +02:00
Hayleia
bfed1747f7 better korean jsons 2021-05-01 09:19:59 +02:00
Hayleia
abb4b104fa fix input being ignored sometimes? 2021-04-30 13:12:28 +02:00
Hayleia
b69b1caa72 Test Korean composition
currency is wrong
code is plugged at the wrong place
input is ignored sometimes
there is reflection for what seems to be no reason
I know, this is just a test and this will either be done again (properly) on another branch or discarded altogether
2021-04-30 07:31:32 +02:00
201 changed files with 9070 additions and 3149 deletions

View File

@@ -9,7 +9,7 @@ insert_final_newline = true
max_line_length = 120
trim_trailing_whitespace = true
[{*.har,*.json}]
[{*.har,*.json,*yml}]
indent_size = 2
[*.kt]

View File

@@ -16,6 +16,8 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Setup CMake and Ninja
uses: lukka/get-cmake@v3.20.1
- uses: actions/cache@v2
with:
path: |
@@ -25,7 +27,7 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Build with Gradle
run: ./gradlew clean assemble
run: ./gradlew clean assembleDebug
- uses: actions/upload-artifact@v2
with:
name: app-debug.apk

8
.gitignore vendored
View File

@@ -39,4 +39,10 @@ captures/
# Keystore files
*.jks
crowdin.properties
crowdin.properties
# C++
.cxx/
# AndroidX Room schema JSONs
/app/schemas/

View File

@@ -45,7 +45,11 @@ _A. IzzySoft's repo for F-Droid_:
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" height="64" alt="IzzySoft repo badge">](https://apt.izzysoft.de/fdroid/index/apk/dev.patrickgold.florisboard.beta)
_B. Use the APK provided in the release section of this repo_
_B. Google Play_:
Follow the same steps as for the stable track, the app can then be accessed [here](https://play.google.com/store/apps/details?id=dev.patrickgold.florisboard.beta).
_C. Use the APK provided in the release section of this repo_
### Giving feedback
If you want to give feedback to FlorisBoard, there are several ways to
@@ -74,8 +78,8 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
### Layouts
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish, Norwegian, Swedish/Finnish, Icelandic, Danish,
Hungarian, Croatian, Polish, Romanian, Colemak, Dvorak, Turkish-Q, Turkish-F, ...)
* [x] Non-latin character layouts (Arabic, Persian, Kurdish, Greek, Russian (JCUKEN))
Hungarian, Croatian, Polish, Romanian, Colemak, Dvorak, Turkish-Q, Turkish-F, and more...)
* [x] Non-latin character layouts (Arabic, Persian, Kurdish, Greek, Russian (JCUKEN), and more...)
* [x] Adapt to situation in app (password, url, text, etc. )
* [x] Special character layout(s)
* [x] Numeric layout
@@ -93,13 +97,17 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
* [x] Subtype selection (language/layout)
* [x] Keyboard behaviour preferences
* [x] Gesture preferences
* [x] User dictionary manager (system and internal)
### Other useful features
* [x] Support for Android 11+ inline autofill API
* [x] One-handed mode
* [x] Clipboard/cursor tools
* [x] Clipboard manager/history
* [x] Integrated number row / symbols in character layouts
* [x] Gesture support
* [x] Full support for the system user dictionary (shared dictionary
between all keyboards) and a private, internal user dictionary
* [x] Full integration in IME service list of Android (xml/method)
(integration is internal-only, because Android's default subtype
implementation not really allows for dynamic language/layout
@@ -131,13 +139,14 @@ close as possible.
- Next-word suggestions by training language models. Data collected here is stored locally and never leaves
the user's device.
- Module C: Extension packs (base implementation with [#162])
- Module C: Extension packs (Implemented with [#162], reworked several times and still not stable)
- Ability to load dictionaries (and later potentially other cool
features too) only if needed to keep the core APK size small
- Currently unclear how exactly this will work, but this is definitely
a must-have feature
- A full implementation may come only in v0.5.0
- Module D: Glide typing
- Module D: Glide typing (Implemented with [#544])
- Swiping over the characters will automatically convert this to a word
- Possibly also add improvements based on the Flow keyboard
@@ -151,9 +160,11 @@ close as possible.
- Theme import/export
### [v0.5.0](https://github.com/florisboard/florisboard/milestone/5)
There's no exact roadmap yet but it is planned that the media part of
FlorisBoard (emojis, emoticons, kaomoji) gets a rework. Also as an extension
(requires v0.4.0/Module C) GIF support is planned.
There's no exact roadmap yet, but these are the most important points:
- Full layout customization in runtime
- Extensive rework and customization of the media input (emojis, emoticons, kaomoji)
- Better Smartbar customization
- As an extension GIF support
### > v0.5.0
This is completely open as of now and will gather planned features as time
@@ -166,6 +177,7 @@ Backlog (currently not assigned to any milestone):
[#91]: https://github.com/florisboard/florisboard/pull/91
[#162]: https://github.com/florisboard/florisboard/pull/162
[#329]: https://github.com/florisboard/florisboard/pull/329
[#544]: https://github.com/florisboard/florisboard/pull/544
## Contributing
Wanna contribute to FlorisBoard? That's great to hear! There are lots of
@@ -183,8 +195,8 @@ to get more information on this topic.
by [google](https://github.com/google)
* [Google Material icons](https://github.com/google/material-design-icons) by
[google](https://github.com/google)
* [Moshi JSON library](https://github.com/square/moshi) by
[square](https://github.com/square)
* [KotlinX serialization library](https://github.com/Kotlin/kotlinx.serialization) by
[Kotlin](https://github.com/Kotlin)
* [ColorPicker preference](https://github.com/jaredrummler/ColorPicker) by
[Jared Rummler](https://github.com/jaredrummler)
* [Timber](https://github.com/JakeWharton/timber) by
@@ -194,7 +206,7 @@ to get more information on this topic.
## Usage notes for included binary dictionary files
All binary dictionaries included within this project in
(this)[app/src/main/assets/ime/dict) asset folder are built from various
(this)[app/src/main/assets/ime/dict] asset folder are built from various
sources, as stated below.
### Source 1: [wordfreq library by LuminosoInsight](https://github.com/LuminosoInsight/wordfreq):

View File

@@ -1,9 +1,9 @@
plugins {
id("com.android.application") version "4.1.3"
kotlin("android") version "1.5.0-RC"
kotlin("kapt") version "1.5.0-RC"
kotlin("plugin.serialization") version "1.5.0-RC"
id("com.android.application") version "4.2.1"
kotlin("android") version "1.5.0"
kotlin("kapt") version "1.5.0"
kotlin("plugin.serialization") version "1.5.0"
}
android {
@@ -24,21 +24,47 @@ android {
applicationId = "dev.patrickgold.florisboard"
minSdkVersion(23)
targetSdkVersion(30)
versionCode(39)
versionName("0.3.11")
versionCode(47)
versionName("0.3.13")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments += mapOf(
Pair("room.schemaLocation", "$projectDir/schemas"),
Pair("room.incremental", "true"),
Pair("room.expandProjection", "true")
)
}
}
externalNativeBuild {
cmake {
cppFlags("-std=c++17", "-fexceptions", "-frtti")
arguments("-DANDROID_STL=c++_static")
}
}
}
buildFeatures {
viewBinding = true
}
externalNativeBuild {
cmake {
path("src/main/cpp/CMakeLists.txt")
}
}
buildTypes {
named("debug").configure {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
isDebuggable = true
isJniDebuggable = true
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_debug")
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_debug_round")
resValue("string", "floris_app_name", "FlorisBoard Debug")
@@ -79,12 +105,13 @@ android {
dependencies {
implementation("androidx.activity", "activity-ktx", "1.2.1")
implementation("androidx.appcompat", "appcompat", "1.2.0")
implementation("androidx.autofill", "autofill", "1.1.0")
implementation("androidx.core", "core-ktx", "1.3.2")
implementation("androidx.fragment", "fragment-ktx", "1.3.0")
implementation("androidx.preference", "preference-ktx", "1.1.1")
implementation("androidx.constraintlayout", "constraintlayout", "2.0.4")
implementation("androidx.lifecycle", "lifecycle-service", "2.2.0")
implementation("com.google.android", "flexbox", "2.0.1") // requires jcenter as of version 2.0.1
implementation("com.google.android", "flexbox", "2.0.1")
implementation("com.google.android.material", "material", "1.3.0")
implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-android", "1.4.2")
implementation("org.jetbrains.kotlinx", "kotlinx-serialization-json", "1.1.0")

View File

@@ -21,7 +21,7 @@
<uses-permission android:name="android.permission.VIBRATE"/>
<application
android:name=".ime.core.FlorisApplication"
android:name="dev.patrickgold.florisboard.FlorisApplication"
android:allowBackup="false"
android:icon="@mipmap/floris_app_icon"
android:label="@string/floris_app_name"
@@ -31,16 +31,13 @@
<!-- IME service -->
<service
android:name="dev.patrickgold.florisboard.ime.core.FlorisBoard"
android:name="dev.patrickgold.florisboard.FlorisImeService"
android:label="@string/floris_app_name"
android:permission="android.permission.BIND_INPUT_METHOD">
<meta-data
android:name="android.view.im"
android:resource="@xml/method"/>
<intent-filter>
<action android:name="android.view.InputMethod"/>
</intent-filter>
<meta-data android:name="android.view.im" android:resource="@xml/method"/>
</service>
<!-- Settings Activity -->
@@ -66,6 +63,13 @@
</intent-filter>
</activity-alias>
<!-- User Dictionary Manager Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.UdmActivity"
android:icon="@mipmap/floris_app_icon"
android:label="@string/settings__title"
android:theme="@style/SettingsTheme"/>
<!-- Theme Selector Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.ThemeManagerActivity"

View File

@@ -1,5 +1,9 @@
{
"package": "dev.patrickgold.florisboard",
"composers": [
{ "$": "appender" },
{ "$": "hangul-unicode" }
],
"currencySets": [
{
"name": "azerbaijani_manat",
@@ -246,6 +250,7 @@
{
"id": 101,
"languageTag": "en-US",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "qwerty"
@@ -254,6 +259,7 @@
{
"id": 102,
"languageTag": "en-UK",
"composer": "appender",
"currencySet": "pound",
"preferred": {
"characters": "qwerty"
@@ -262,6 +268,7 @@
{
"id": 103,
"languageTag": "en-CA",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "qwerty"
@@ -270,6 +277,7 @@
{
"id": 104,
"languageTag": "en-AU",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "qwerty"
@@ -278,6 +286,7 @@
{
"id": 201,
"languageTag": "de-DE",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwertz"
@@ -286,6 +295,7 @@
{
"id": 202,
"languageTag": "de-AT",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwertz"
@@ -294,14 +304,27 @@
{
"id": 203,
"languageTag": "de-CH",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "swiss_german"
}
},
{
"id": 204,
"languageTag": "de-DE-neobone",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "neo2",
"symbols": "neo2",
"numericRow": "neo2"
}
},
{
"id": 301,
"languageTag": "fr-FR",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "azerty"
@@ -310,6 +333,7 @@
{
"id": 302,
"languageTag": "fr-CA",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "canadian_french"
@@ -318,6 +342,7 @@
{
"id": 303,
"languageTag": "fr-CH",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "swiss_french"
@@ -326,6 +351,7 @@
{
"id": 401,
"languageTag": "it-IT",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
@@ -334,6 +360,7 @@
{
"id": 402,
"languageTag": "it-CH",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "swiss_italian"
@@ -342,6 +369,7 @@
{
"id": 501,
"languageTag": "es-ES",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "spanish"
@@ -350,6 +378,7 @@
{
"id": 502,
"languageTag": "es-US",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "spanish"
@@ -358,6 +387,7 @@
{
"id": 503,
"languageTag": "es-419",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "spanish"
@@ -366,6 +396,7 @@
{
"id": 601,
"languageTag": "pt-PT",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
@@ -374,6 +405,7 @@
{
"id": 602,
"languageTag": "pt-BR",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "qwerty"
@@ -382,6 +414,7 @@
{
"id": 701,
"languageTag": "nb-NO",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "norwegian"
@@ -390,6 +423,7 @@
{
"id": 702,
"languageTag": "nn-NO",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "norwegian"
@@ -398,6 +432,7 @@
{
"id": 711,
"languageTag": "sv-SE",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "swedish_finnish"
@@ -406,6 +441,7 @@
{
"id": 721,
"languageTag": "fi-FI",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "swedish_finnish"
@@ -414,6 +450,7 @@
{
"id": 731,
"languageTag": "da-DK",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "danish"
@@ -422,6 +459,7 @@
{
"id": 741,
"languageTag": "is-IS",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "icelandic"
@@ -430,6 +468,7 @@
{
"id": 751,
"languageTag": "fo",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "faroese"
@@ -438,6 +477,7 @@
{
"id": 801,
"languageTag": "fa-FA",
"composer": "appender",
"currencySet": "iranian_rial",
"preferred": {
"characters": "persian",
@@ -449,6 +489,7 @@
{
"id": 901,
"languageTag": "ar",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "arabic",
@@ -460,6 +501,7 @@
{
"id": 1001,
"languageTag": "hu",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "hungarian"
@@ -468,6 +510,7 @@
{
"id": 1101,
"languageTag": "eo",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "esperanto"
@@ -476,6 +519,7 @@
{
"id": 1201,
"languageTag": "hr",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwertz"
@@ -484,6 +528,7 @@
{
"id": 1301,
"languageTag": "ru",
"composer": "appender",
"currencySet": "russian_ruble",
"preferred": {
"characters": "jcuken_russian"
@@ -492,6 +537,7 @@
{
"id": 1351,
"languageTag": "uk",
"composer": "appender",
"currencySet": "ukrainian_hryvnia",
"preferred": {
"characters": "jcuken_ukrainian"
@@ -500,6 +546,7 @@
{
"id": 1401,
"languageTag": "el",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "greek"
@@ -508,6 +555,7 @@
{
"id": 1501,
"languageTag": "ro",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
@@ -516,6 +564,7 @@
{
"id": 1601,
"languageTag": "pl",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
@@ -524,6 +573,7 @@
{
"id": 1701,
"languageTag": "bg-bg",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "bulgarian_phonetic"
@@ -532,6 +582,7 @@
{
"id": 1801,
"languageTag": "tr",
"composer": "appender",
"currencySet": "turkish_lira",
"preferred": {
"characters": "qwerty"
@@ -540,6 +591,7 @@
{
"id": 1901,
"languageTag": "iw-IL",
"composer": "appender",
"currencySet": "israeli_new_shekel",
"preferred": {
"characters": "hebrew"
@@ -548,6 +600,7 @@
{
"id": 2001,
"languageTag": "ckb",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "kurdish",
@@ -559,6 +612,7 @@
{
"id": 2101,
"languageTag": "sr-RS",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "serbian_cyrillic"
@@ -567,6 +621,7 @@
{
"id": 2201,
"languageTag": "lv-LV",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
@@ -575,6 +630,7 @@
{
"id": 2301,
"languageTag": "ku",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "kurdish_kurmanci"
@@ -583,6 +639,7 @@
{
"id": 2501,
"languageTag": "ca",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "catalan"
@@ -591,6 +648,7 @@
{
"id": 2601,
"languageTag": "IPA-IPA",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "ipa",
@@ -601,6 +659,7 @@
{
"id": 2701,
"languageTag": "sk",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwertz"
@@ -609,10 +668,20 @@
{
"id": 2801,
"languageTag": "cs",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwertz"
}
},
{
"id": 2900,
"languageTag": "ko",
"composer": "hangul-unicode",
"currencySet": "south_korean_won",
"preferred": {
"characters": "korean"
}
}
]
}

View File

@@ -0,0 +1,61 @@
{
"type": "characters",
"name": "bone",
"label": "Bone",
"authors": [ "ostrya" ],
"direction": "ltr",
"modifier": "neo2",
"arrangement": [
[
{ "$": "auto_text_key", "code": 106, "label": "j" },
{ "$": "auto_text_key", "code": 100, "label": "d" },
{ "$": "auto_text_key", "code": 117, "label": "u" },
{ "$": "auto_text_key", "code": 97, "label": "a" },
{ "$": "auto_text_key", "code": 120, "label": "x" },
{ "$": "auto_text_key", "code": 112, "label": "p" },
{ "$": "auto_text_key", "code": 104, "label": "h" },
{ "$": "auto_text_key", "code": 108, "label": "l" },
{ "$": "auto_text_key", "code": 109, "label": "m" },
{ "$": "auto_text_key", "code": 119, "label": "w" },
{ "$": "case_selector",
"lower": {
"code": 223, "label": "ß", "popup": {
"relevant": [
{ "code": 180, "label": "´" }
]
}
},
"upper": {
"code": 7838, "label": "ẞ", "popup": {
"relevant": [
{ "code": 180, "label": "´" }
]
}
}
}
],
[
{ "$": "auto_text_key", "code": 99, "label": "c" },
{ "$": "auto_text_key", "code": 116, "label": "t" },
{ "$": "auto_text_key", "code": 105, "label": "i" },
{ "$": "auto_text_key", "code": 101, "label": "e" },
{ "$": "auto_text_key", "code": 111, "label": "o" },
{ "$": "auto_text_key", "code": 98, "label": "b" },
{ "$": "auto_text_key", "code": 110, "label": "n" },
{ "$": "auto_text_key", "code": 114, "label": "r" },
{ "$": "auto_text_key", "code": 115, "label": "s" },
{ "$": "auto_text_key", "code": 103, "label": "g" },
{ "$": "auto_text_key", "code": 113, "label": "q" }
],
[
{ "$": "auto_text_key", "code": 102, "label": "f" },
{ "$": "auto_text_key", "code": 118, "label": "v" },
{ "$": "auto_text_key", "code": 252, "label": "ü" },
{ "$": "auto_text_key", "code": 228, "label": "ä" },
{ "$": "auto_text_key", "code": 246, "label": "ö" },
{ "$": "auto_text_key", "code": 121, "label": "y" },
{ "$": "auto_text_key", "code": 122, "label": "z" },
{ "$": "auto_text_key", "code": 107, "label": "k" }
]
]
}

View File

@@ -4,28 +4,22 @@
"authors": [ "GoRaN" ],
"mapping": {
"all": {
"": {
"relevant": [
{ "code": 1577, "label": "ة" },
{ "code": 1729, "label": "ـہ" }
]
},
"ر": {
"relevant": [
{ "code": 1685, "label": "ڕ" },
{ "code": 1682, "label": "ڒ" }
]
},
"ی": {
"relevant": [
{ "code": 1746, "label": "ے" },
{ "code": 1610, "label": "ي" },
{ "code": 1744, "label": "ې" },
{ "code": 1741, "label": "ۍ" },
{ "code": 1742, "label": "ێ" },
{ "code": 1744, "label": "ې" },
{ "code": 1610, "label": "ي" },
{ "code": 1597, "label": "ؽ" }
]
},
@@ -34,10 +28,15 @@
"ﺋ": {
"relevant": [
{ "code": 65163, "label": "ﺋ" },
{ "code": 1569, "label": "ء" },
{ "code": 65139, "label": "ﹳ" }
]
},
"ح": {
"relevant": [
{ "code": 65010, "label": "ﷲ" },
{ "code": 65019, "label": "ﷻ" }
]
},
"ع": {
"relevant": [
@@ -56,12 +55,9 @@
]
},
"ف": {
"relevant": [
{ "code": 1701, "label": "ڥ" },
{ "code": 1700, "label": "ڤ" },
{ "code": 1698, "label": "ڢ" },
{ "code": 1697, "label": "ڡ" }
]
@@ -70,7 +66,6 @@
"د": {
"relevant": [
{ "code": 1676, "label": "ڌ" },
{ "code": 1584, "label": "ذ" },
{ "code": 64390, "label": "ﮆ" },
{ "code": 1774, "label": "ۮ" }
]
@@ -93,9 +88,7 @@
},
"ب": {
"relevant": [
{ "code": 65010, "label": "" },
{ "code": 65021, "label": "﷽" },
{ "code": 65019, "label": "ﷻ" }
{ "code": 65021, "label": "" }
]
},
"م": {
@@ -108,7 +101,6 @@
"relevant": [
{ "code": 1718, "label": "ڶ" },
{ "code": 1719, "label": "ڷ" },
{ "code": 1717, "label": "ڵ" },
{ "code": 1720, "label": "ڸ" }
]
},

View File

@@ -0,0 +1,19 @@
{
"type": "characters/extended_popups",
"name": "de-DE-neobone",
"authors": [ "ostrya" ],
"mapping": {
"uri": {
"~right": {
"main": { "code": -255, "label": ".com" },
"relevant": [
{ "code": -255, "label": ".ch" },
{ "code": -255, "label": ".de" },
{ "code": -255, "label": ".at" },
{ "code": -255, "label": ".net" }
]
}
}
}
}

View File

@@ -20,10 +20,10 @@
]
},
"ι": {
"main": { "$": "auto_text_key", "code": 943, "label": "ί" },
"relevant": [
{ "$": "auto_text_key", "code": 912, "label": "ΐ" },
{ "$": "auto_text_key", "code": 970, "label": "ϊ" },
{ "$": "auto_text_key", "code": 943, "label": "ί" }
{ "$": "auto_text_key", "code": 970, "label": "ϊ" }
]
},
"ο": {
@@ -32,10 +32,10 @@
]
},
"υ": {
"main": { "$": "auto_text_key", "code": 973, "label": "ύ" },
"relevant": [
{ "$": "auto_text_key", "code": 944, "label": "ΰ" },
{ "$": "auto_text_key", "code": 971, "label": "ϋ" },
{ "$": "auto_text_key", "code": 973, "label": "ύ" }
{ "$": "auto_text_key", "code": 971, "label": "ϋ" }
]
},
"ω": {

View File

@@ -0,0 +1,75 @@
{
"type": "characters/extended_popups",
"name": "ko",
"authors": [ "patrickgold", "Hayleia" ],
"mapping": {
"all": {
"ㅂ": {
"relevant": [
{ "$": "auto_text_key", "code": 12611, "label": "ㅃ" }
]
},
"ㅈ": {
"relevant": [
{ "$": "auto_text_key", "code": 12617, "label": "ㅉ" }
]
},
"ㄷ": {
"relevant": [
{ "$": "auto_text_key", "code": 12600, "label": "ㄸ" }
]
},
"ㄱ": {
"relevant": [
{ "$": "auto_text_key", "code": 12594, "label": "ㄲ" }
]
},
"ㅅ": {
"relevant": [
{ "$": "auto_text_key", "code": 12614, "label": "ㅆ" }
]
},
"ㅐ": {
"relevant": [
{ "$": "auto_text_key", "code": 12626, "label": "ㅒ" }
]
},
"ㅔ": {
"relevant": [
{ "$": "auto_text_key", "code": 12630, "label": "ㅖ" }
]
},
"~right": {
"main": { "code": 44, "label": "," },
"relevant": [
{ "code": 38, "label": "&" },
{ "code": 37, "label": "%" },
{ "code": 43, "label": "+" },
{ "code": 34, "label": "\"" },
{ "code": 45, "label": "-" },
{ "code": 58, "label": ":" },
{ "code": 39, "label": "'" },
{ "code": 64, "label": "@" },
{ "code": 59, "label": ";" },
{ "code": 47, "label": "/" },
{ "code": 40, "label": "(" },
{ "code": 41, "label": ")" },
{ "code": 35, "label": "#" },
{ "code": 33, "label": "!" },
{ "code": 63, "label": "?" }
]
}
},
"uri": {
"~right": {
"main": { "code": -255, "label": ".com" },
"relevant": [
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}
}
}
}

View File

@@ -0,0 +1,77 @@
{
"type": "characters",
"name": "halmak",
"label": "Halmak",
"authors": [ "dessalines" ],
"direction": "ltr",
"arrangement": [
[
{ "$": "auto_text_key", "code": 119, "label": "w" },
{ "$": "auto_text_key", "code": 108, "label": "l" },
{ "$": "auto_text_key", "code": 114, "label": "r" },
{ "$": "auto_text_key", "code": 98, "label": "b" },
{ "$": "auto_text_key", "code": 122, "label": "z" },
{ "$": "case_selector",
"lower": { "code": 59, "label": ";", "popup": {
"relevant": [
{ "code": 58, "label": ":" }
]
} },
"upper": { "code": 58, "label": ":", "popup": {
"relevant": [
{ "code": 59, "label": ";" }
]
} }
},
{ "$": "auto_text_key", "code": 113, "label": "q" },
{ "$": "auto_text_key", "code": 117, "label": "u" },
{ "$": "auto_text_key", "code": 100, "label": "d" },
{ "$": "auto_text_key", "code": 106, "label": "j" }
],
[
{ "$": "auto_text_key", "code": 115, "label": "s" },
{ "$": "auto_text_key", "code": 104, "label": "h" },
{ "$": "auto_text_key", "code": 110, "label": "n" },
{ "$": "auto_text_key", "code": 116, "label": "t" },
{ "$": "case_selector",
"lower": { "code": 44, "label": ",", "popup": {
"relevant": [
{ "code": 40, "label": "(" }
]
} },
"upper": { "code": 40, "label": "(", "popup": {
"relevant": [
{ "code": 44, "label": "," }
]
} }
},
{ "$": "case_selector",
"lower": { "code": 46, "label": ".", "popup": {
"relevant": [
{ "code": 41, "label": ")" }
]
} },
"upper": { "code": 41, "label": ")", "popup": {
"relevant": [
{ "code": 46, "label": "." }
]
} }
},
{ "$": "auto_text_key", "code": 97, "label": "a" },
{ "$": "auto_text_key", "code": 101, "label": "e" },
{ "$": "auto_text_key", "code": 111, "label": "o" },
{ "$": "auto_text_key", "code": 105, "label": "i" }
],
[
{ "$": "auto_text_key", "code": 102, "label": "f" },
{ "$": "auto_text_key", "code": 109, "label": "m" },
{ "$": "auto_text_key", "code": 118, "label": "v" },
{ "$": "auto_text_key", "code": 99, "label": "c" },
{ "$": "auto_text_key", "code": 103, "label": "g" },
{ "$": "auto_text_key", "code": 112, "label": "p" },
{ "$": "auto_text_key", "code": 120, "label": "x" },
{ "$": "auto_text_key", "code": 107, "label": "k" },
{ "$": "auto_text_key", "code": 121, "label": "y" }
]
]
}

View File

@@ -28,27 +28,27 @@
{ "code": 1508, "label": "פ" }
],
[
{ "code": 1513, "label": "ף" },
{ "code": 1491, "label": "ך" },
{ "code": 1490, "label": "ל" },
{ "code": 1499, "label": "ח" },
{ "code": 1506, "label": "י" },
{ "code": 1497, "label": "ע" },
{ "code": 1495, "label": "כ" },
{ "code": 1500, "label": "ג" },
{ "code": 1498, "label": "ד" },
{ "code": 1507, "label": "ש" }
{ "code": 1513, "label": "ש" },
{ "code": 1491, "label": "ד" },
{ "code": 1490, "label": "ג" },
{ "code": 1499, "label": "כ" },
{ "code": 1506, "label": "ע" },
{ "code": 1497, "label": "י" },
{ "code": 1495, "label": "ח" },
{ "code": 1500, "label": "ל" },
{ "code": 1498, "label": "ך" },
{ "code": 1507, "label": "ף" }
],
[
{ "code": 1494, "label": "ץ" },
{ "code": 1505, "label": "ת" },
{ "code": 1489, "label": "צ" },
{ "code": 1492, "label": "מ" },
{ "code": 1494, "label": "ז" },
{ "code": 1505, "label": "ס" },
{ "code": 1489, "label": "ב" },
{ "code": 1492, "label": "ה" },
{ "code": 1504, "label": "נ" },
{ "code": 1502, "label": "ה" },
{ "code": 1510, "label": "ב" },
{ "code": 1514, "label": "ס" },
{ "code": 1509, "label": "ז" }
{ "code": 1502, "label": "מ" },
{ "code": 1510, "label": "צ" },
{ "code": 1514, "label": "ת" },
{ "code": 1509, "label": "ץ" }
]
]
}

View File

@@ -0,0 +1,62 @@
{
"type": "characters",
"name": "korean",
"label": "South Korean standard",
"authors": [ "patrickgold", "Hayleia" ],
"direction": "ltr",
"arrangement": [
[
{ "$": "case_selector",
"lower": { "code": 12610, "label": "ㅂ" },
"upper": { "code": 12611, "label": "ㅃ" }
},
{ "$": "case_selector",
"lower": { "code": 12616, "label": "ㅈ" },
"upper": { "code": 12617, "label": "ㅉ" }
},
{ "$": "case_selector",
"lower": { "code": 12599, "label": "ㄷ" },
"upper": { "code": 12600, "label": "ㄸ" }
},
{ "$": "case_selector",
"lower": { "code": 12593, "label": "ㄱ" },
"upper": { "code": 12594, "label": "ㄲ" }
},
{ "$": "case_selector",
"lower": { "code": 12613, "label": "ㅅ" },
"upper": { "code": 12614, "label": "ㅆ" }
},
{ "$": "auto_text_key", "code": 12635, "label": "ㅛ"},
{ "$": "auto_text_key", "code": 12629, "label": "ㅕ"},
{ "$": "auto_text_key", "code": 12625, "label": "ㅑ"},
{ "$": "case_selector",
"lower": { "code": 12624, "label": "ㅐ" },
"upper": { "code": 12626, "label": "ㅒ" }
},
{ "$": "case_selector",
"lower": { "code": 12628, "label": "ㅔ" },
"upper": { "code": 12630, "label": "ㅖ" }
}
],
[
{ "$": "auto_text_key", "code": 12609, "label": "ㅁ"},
{ "$": "auto_text_key", "code": 12596, "label": "ㄴ"},
{ "$": "auto_text_key", "code": 12615, "label": "ㅇ"},
{ "$": "auto_text_key", "code": 12601, "label": "ㄹ"},
{ "$": "auto_text_key", "code": 12622, "label": "ㅎ"},
{ "$": "auto_text_key", "code": 12631, "label": "ㅗ"},
{ "$": "auto_text_key", "code": 12627, "label": "ㅓ"},
{ "$": "auto_text_key", "code": 12623, "label": "ㅏ"},
{ "$": "auto_text_key", "code": 12643, "label": "ㅣ"}
],
[
{ "$": "auto_text_key", "code": 12619, "label": "ㅋ"},
{ "$": "auto_text_key", "code": 12620, "label": "ㅌ"},
{ "$": "auto_text_key", "code": 12618, "label": "ㅊ"},
{ "$": "auto_text_key", "code": 12621, "label": "ㅍ"},
{ "$": "auto_text_key", "code": 12640, "label": "ㅠ"},
{ "$": "auto_text_key", "code": 12636, "label": "ㅜ"},
{ "$": "auto_text_key", "code": 12641, "label": "ㅡ"}
]
]
}

View File

@@ -1,7 +1,7 @@
{
"type": "characters",
"name": "kurdish",
"label": "کوردی",
"label": "کوردی (قوەرتی نوێ)",
"authors": [ "GoRaN" ],
"direction": "rtl",
"modifier": "kurdish",
@@ -13,34 +13,46 @@
{ "code": 1608, "label": "و", "popup": {
"main": { "code": -255, "label": "وو" }
} },
{ "code": 1749, "label": "" },
{ "code": 1585, "label": "ر" },
{ "code": 1749, "label": "", "popup": {
"main": { "code": 1577, "label": "ة" }
} },
{ "code": 1585, "label": "ر", "popup": {
"main": { "code": 1685, "label": "ڕ" }
} },
{ "code": 1578, "label": "ت", "popup": {
"main": { "code": 1591, "label": "ط" }
} },
{ "code": 1740, "label": "ی" },
{ "code": 1574, "label": "ﺋ"},
{ "code": 1740, "label": "ی", "popup": {
"main": { "code": 1742, "label": "ێ" }
} },
{ "code": 1574, "label": "ﺋ", "popup": {
"main": { "code": 1569, "label": "ء" }
} },
{ "code": 1593, "label": "ع", "popup": {
"main": { "code": 1594, "label": "غ" }
} },
{ "code": 1734, "label": "ۆ" },
{ "code": 1662, "label": "پ", "popup": {
"main": { "code": 1579, "label": "ث" }
} }
],
[
{ "code": 1575, "label": "ا" },
{"code": 1575, "label": "ا"},
{ "code": 1587, "label": "س" },
{ "code": 1588, "label": "ش" },
{ "code": 1583, "label": "د" },
{ "code": 1601, "label": "ف" },
{ "code": 1583, "label": "د", "popup": {
"main": {"code": 1584, "label": "ذ" }
} },
{ "code": 1601, "label": "ف" , "popup": {
"main": {"code": 1700, "label": "ڤ" }
} },
{ "code": 1607, "label": "ھ" },
{ "code": 1688, "label": "ژ" },
{ "code": 1604, "label": "ل" },
{ "code": 1688, "label": "ژ", "popup": {
"main": { "code": 1600, "label": "" }
} },
{ "code": 1604, "label": "ل", "popup": {
"main": { "code": 1717, "label": "ڵ" }
} },
{ "code": 1705, "label": "ک" },
{ "code": 1711, "label": "گ" }
],

View File

@@ -1,7 +1,7 @@
{
"type": "characters",
"name": "kurdish_standard",
"label": "کوردی - ستاندارد",
"label": "کوردی (ق‌ڤ‌ف‌غ)",
"authors": [ "GoRaN" ],
"direction": "rtl",
"modifier": "kurdish",
@@ -10,16 +10,14 @@
{ "code": 1602, "label": "ق", "popup": {
"main": { "code": 1647, "label": "ٯ" }
} },
{ "code": 1700, "label": "ڤ", "popup": {
"main": { "code": 1701, "label": "ڥ" }
} },
{ "code": 1601, "label": "ف", "popup": {
"main": { "code": 1698, "label": "ڢ" }
} },
{ "code": 1700, "label": "ڤ" },
{ "code": 1601, "label": "ف" },
{ "code": 1594, "label": "غ" },
{ "code": 1593, "label": "ع"},
{ "code": 1607, "label": "ھ" },
{ "code": 1749, "label": "" },
{ "code": 1749, "label": "", "popup": {
"main": { "code": 1577, "label": "ة" }
} },
{ "code": 1578, "label": "ت", "popup": {
"main": { "code": 1591, "label": "ط" }
@@ -46,7 +44,9 @@
} },
{ "code": 1585, "label": "ر" },
{ "code": 1685, "label": "ڕ" },
{ "code": 1583, "label": "د" },
{ "code": 1583, "label": "د", "popup": {
"main": {"code": 1584, "label": "ذ" }
} },
{ "code": -255, "label": "وو" },
{ "code": 1608, "label": "و" },
{ "code": 1734, "label": "ۆ" },
@@ -55,8 +55,10 @@
],
[
{ "code": 1600, "label": "kashida", "variation": "normal" },
{ "code": 1574, "label": "ﺋ"},
{ "code": 1574, "label": "ﺋ", "popup": {
"main": { "code": 1569, "label": "ء" }
} },
{ "code": 1662, "label": "پ", "popup": {
"main": { "code": 1579, "label": "ث" }

View File

@@ -6,14 +6,17 @@
"direction": "rtl",
"arrangement": [
[
{ "code": 1600, "label": "kashida", "popup":
{ "main": { "code": 8204, "label": "half_space" }
} },
{ "code": 0, "type": "placeholder" },
{ "code": -5, "label": "delete", "type": "enter_editing" }
],
[
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
{ "$": "variation_selector",
"default": { "code": 1567, "label": "؟", "groupId": 1 },
"password": { "code": 1548, "label": "،", "groupId": 1 },
"default": { "code": 1548, "label": "،", "groupId": 1 },
"password": { "code": 35, "label": "#", "groupId": 1 },
"email": { "code": 64, "label": "@", "groupId": 1 },
"uri": { "code": 47, "label": "/", "groupId": 1 }
},

View File

@@ -0,0 +1,53 @@
{
"type": "characters/mod",
"name": "neo2",
"label": "Neo2",
"authors": [ "ostrya" ],
"direction": "ltr",
"arrangement": [
[
{ "code": -1, "label": "shift", "type": "modifier" },
{ "code": 0, "type": "placeholder" },
{ "code": -5, "label": "delete", "type": "enter_editing" }
],
[
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
{ "code": -210, "label": "language_switch", "type": "system_gui" },
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
{ "code": 32, "label": "space" },
{ "$": "variation_selector",
"default": { "code": 44, "label": ",", "groupId": 1,
"popup": {
"main": { "code": 34, "label": "\"" },
"relevant": [
{ "code": 8211, "label": "" }
]
} },
"email": { "code": 64, "label": "@", "groupId": 1,
"popup": {
"relevant": [
{ "code": 44, "label": "," }
]
} },
"uri": { "code": 47, "label": "/", "groupId": 1,
"popup": {
"relevant": [
{ "code": 44, "label": "," }
]
} }
},
{ "$": "variation_selector",
"default": { "code": 46, "label": ".", "groupId": 2,
"popup": {
"relevant": [
{ "code": 183, "label": "·" },
{ "code": 39, "label": "'" }
]
} },
"email": { "code": 46, "label": ".", "groupId": 2 },
"uri": { "code": 46, "label": ".", "groupId": 2 }
},
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
]
]
}

View File

@@ -0,0 +1,61 @@
{
"type": "characters",
"name": "neo2",
"label": "Neo2",
"authors": [ "ostrya" ],
"direction": "ltr",
"modifier": "neo2",
"arrangement": [
[
{ "$": "auto_text_key", "code": 120, "label": "x" },
{ "$": "auto_text_key", "code": 118, "label": "v" },
{ "$": "auto_text_key", "code": 108, "label": "l" },
{ "$": "auto_text_key", "code": 99, "label": "c" },
{ "$": "auto_text_key", "code": 119, "label": "w" },
{ "$": "auto_text_key", "code": 107, "label": "k" },
{ "$": "auto_text_key", "code": 104, "label": "h" },
{ "$": "auto_text_key", "code": 103, "label": "g" },
{ "$": "auto_text_key", "code": 102, "label": "f" },
{ "$": "auto_text_key", "code": 113, "label": "q" },
{ "$": "case_selector",
"lower": {
"code": 223, "label": "ß", "popup": {
"relevant": [
{ "code": 180, "label": "´" }
]
}
},
"upper": {
"code": 7838, "label": "ẞ", "popup": {
"relevant": [
{ "code": 180, "label": "´" }
]
}
}
}
],
[
{ "$": "auto_text_key", "code": 117, "label": "u" },
{ "$": "auto_text_key", "code": 105, "label": "i" },
{ "$": "auto_text_key", "code": 97, "label": "a" },
{ "$": "auto_text_key", "code": 101, "label": "e" },
{ "$": "auto_text_key", "code": 111, "label": "o" },
{ "$": "auto_text_key", "code": 115, "label": "s" },
{ "$": "auto_text_key", "code": 110, "label": "n" },
{ "$": "auto_text_key", "code": 114, "label": "r" },
{ "$": "auto_text_key", "code": 116, "label": "t" },
{ "$": "auto_text_key", "code": 100, "label": "d" },
{ "$": "auto_text_key", "code": 121, "label": "y" }
],
[
{ "$": "auto_text_key", "code": 252, "label": "ü" },
{ "$": "auto_text_key", "code": 246, "label": "ö" },
{ "$": "auto_text_key", "code": 228, "label": "ä" },
{ "$": "auto_text_key", "code": 112, "label": "p" },
{ "$": "auto_text_key", "code": 122, "label": "z" },
{ "$": "auto_text_key", "code": 98, "label": "b" },
{ "$": "auto_text_key", "code": 109, "label": "m" },
{ "$": "auto_text_key", "code": 106, "label": "j" }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "bengali",
"label": "Bengali",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 2535, "label": "১", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 2536, "label": "২", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 2537, "label": "৩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 2538, "label": "", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 2539, "label": "৫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 2540, "label": "৬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 2541, "label": "", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 2542, "label": "৮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 2543, "label": "৯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 2534, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "devanagari",
"label": "Devanagari",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 2407, "label": "१", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 2408, "label": "२", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 2409, "label": "३", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 2410, "label": "४", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 2411, "label": "५", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 2412, "label": "६", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 2413, "label": "७", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 2414, "label": "८", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 2415, "label": "९", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 2406, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "gujarati",
"label": "Gujarati",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 2791, "label": "૧", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 2792, "label": "૨", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 2793, "label": "૩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 2794, "label": "૪", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 2795, "label": "૫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 2796, "label": "૬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 2797, "label": "૭", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 2798, "label": "૮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 2799, "label": "૯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 2790, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "gurmukhi",
"label": "Gurmukhi",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 2663, "label": "", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 2664, "label": "੨", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 2665, "label": "੩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 2666, "label": "", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 2667, "label": "੫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 2668, "label": "੬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 2669, "label": "੭", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 2670, "label": "੮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 2671, "label": "੯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 2662, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "kannada",
"label": "Kannada",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 3303, "label": "೧", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 3304, "label": "೨", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 3305, "label": "೩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 3306, "label": "೪", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 3307, "label": "೫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 3308, "label": "೬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 3309, "label": "೭", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 3310, "label": "೮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 3311, "label": "೯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 3302, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "malayalam",
"label": "Malayalam",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 3431, "label": "൧", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 3432, "label": "൨", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 3433, "label": "൩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 3434, "label": "൪", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 3435, "label": "൫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 3436, "label": "൬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 3437, "label": "", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 3438, "label": "൮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 3439, "label": "൯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 3430, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,80 @@
{
"type": "numeric_row",
"name": "neo2",
"label": "Neo2",
"authors": [ "ostrya" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 49, "label": "1", "type": "numeric", "popup": {
"relevant": [
{ "code": 176, "label": "°" },
{ "code": 185, "label": "¹" }
]
} },
{ "code": 50, "label": "2", "type": "numeric", "popup": {
"relevant": [
{ "code": 167, "label": "§" },
{ "code": 178, "label": "²" }
]
} },
{ "code": 51, "label": "3", "type": "numeric", "popup": {
"relevant": [
{ "code": 8467, "label": "" },
{ "code": 179, "label": "³" }
]
} },
{ "code": 52, "label": "4", "type": "numeric", "popup": {
"relevant": [
{ "code": 187, "label": "»" },
{ "code": 8250, "label": "" }
]
} },
{ "code": 53, "label": "5", "type": "numeric", "popup": {
"relevant": [
{ "code": 171, "label": "«" },
{ "code": 8249, "label": "" }
]
} },
{ "code": 54, "label": "6", "type": "numeric", "popup": {
"relevant": [
{ "code": 36, "label": "$" },
{ "code": 162, "label": "¢" }
]
} },
{ "code": 55, "label": "7", "type": "numeric", "popup": {
"main": { "code": -801, "label": "currency_slot_1" },
"relevant": [
{ "code": -802, "label": "currency_slot_2" },
{ "code": -803, "label": "currency_slot_3" },
{ "code": -804, "label": "currency_slot_4" },
{ "code": -805, "label": "currency_slot_5" },
{ "code": -806, "label": "currency_slot_6" }
]
} },
{ "code": 56, "label": "8", "type": "numeric", "popup": {
"relevant": [
{ "code": 8222, "label": "„" },
{ "code": 8218, "label": "" }
]
} },
{ "code": 57, "label": "9", "type": "numeric", "popup": {
"relevant": [
{ "code": 8220, "label": "“" },
{ "code": 8216, "label": "" }
]
} },
{ "code": 48, "label": "0", "type": "numeric", "popup": {
"relevant": [
{ "code": 8221, "label": "”" },
{ "code": 8217, "label": "" }
]
} },
{ "code": 45, "label": "-", "type": "numeric", "popup": {
"relevant": [
{ "code": 8212, "label": "—" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "oriya",
"label": "Odia",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 2919, "label": "୧", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 2920, "label": "", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 2921, "label": "୩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 2922, "label": "୪", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 2923, "label": "୫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 2924, "label": "୬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 2925, "label": "୭", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 2926, "label": "୮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 2927, "label": "୯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 2918, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "tamil",
"label": "Tamil",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 3047, "label": "௧", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 3048, "label": "௨", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 3049, "label": "௩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 3050, "label": "௪", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 3051, "label": "௫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 3052, "label": "௬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 3053, "label": "௭", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 3054, "label": "௮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 3055, "label": "௯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 3046, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "telugu",
"label": "Telugu",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 3175, "label": "౧", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 3176, "label": "౨", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 3177, "label": "౩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 3178, "label": "౪", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 3179, "label": "౫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 3180, "label": "౬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 3181, "label": "౭", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 3182, "label": "౮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 3183, "label": "౯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 3174, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,22 @@
{
"type": "symbols/mod",
"name": "neo2",
"label": "Neo2",
"authors": [ "ostrya" ],
"direction": "ltr",
"arrangement": [
[
{ "code": -203, "label": "view_symbols2", "type": "system_gui" },
{ "code": 0, "type": "placeholder" },
{ "code": -5, "label": "delete", "type": "enter_editing" }
],
[
{ "code": -201, "label": "view_characters", "type": "system_gui" },
{ "code": -205, "label": "view_numeric_advanced", "type": "system_gui" },
{ "code": 32, "label": "space" },
{ "code": 34, "label": "\"" },
{ "code": 39, "label": "'" },
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
]
]
}

View File

@@ -0,0 +1,46 @@
{
"type": "symbols",
"name": "neo2",
"label": "Neo2",
"authors": [ "ostrya" ],
"direction": "ltr",
"modifier": "neo2",
"arrangement": [
[
{ "code": 8230, "label": "…" },
{ "code": 95, "label": "_" },
{ "code": 91, "label": "[" },
{ "code": 93, "label": "]" },
{ "code": 94, "label": "^" },
{ "code": 33, "label": "!" },
{ "code": 60, "label": "<" },
{ "code": 62, "label": ">" },
{ "code": 61, "label": "=" },
{ "code": 38, "label": "&" },
{ "code": 383, "label": "ſ" }
],
[
{ "code": 92, "label": "\\" },
{ "code": 47, "label": "/" },
{ "code": 123, "label": "{" },
{ "code": 125, "label": "}" },
{ "code": 42, "label": "*" },
{ "code": 63, "label": "?" },
{ "code": 40, "label": "(" },
{ "code": 41, "label": ")" },
{ "code": 45, "label": "-" },
{ "code": 58, "label": ":" },
{ "code": 64, "label": "@" }
],
[
{ "code": 35, "label": "#" },
{ "code": 36, "label": "$" },
{ "code": 124, "label": "|" },
{ "code": 126, "label": "~" },
{ "code": 96, "label": "`" },
{ "code": 43, "label": "+" },
{ "code": 37, "label": "%" },
{ "code": 59, "label": ";" }
]
]
}

View File

@@ -503,24 +503,6 @@ SOFTWARE.
<hr>
<h3>kotlin-result</h3>
<span>Copyright (c) 2017-2020 Michael Bull (https://www.michael-bull.com)</span>
<pre>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
</pre>
<hr>
<h3>Material Icons</h3>
<span>Copyright 2018 Google LLC</span>
<pre>
@@ -729,24 +711,6 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
<hr>
<h3>Moshi</h3>
<span>Copyright 2015 Square, Inc.</span>
<pre>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
</pre>
<hr>
<h3>Timber</h3>
<span>Copyright 2013 Jake Wharton</span>
<pre>

View File

@@ -0,0 +1,38 @@
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
cmake_minimum_required(VERSION 3.10.2)
project("florisboard")
set(CMAKE_CXX_STANDARD 17)
add_subdirectory(ime/nlp)
add_library(
# Name
florisboard-native
# Type
SHARED
# Sources
dev_patrickgold_florisboard_ime_nlp_SuggestionList.cpp
)
find_library(
# Save to var
log-lib
# Original name
log
)
target_link_libraries(
# Destination
florisboard-native
# Sources
${log-lib}
ime-nlp
)

View File

@@ -0,0 +1,123 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <jni.h>
#include "ime/nlp/suggestion_list.h"
#pragma ide diagnostic ignored "UnusedLocalVariable"
using namespace ime::nlp;
extern "C"
JNIEXPORT jlong JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeInitialize(
JNIEnv *env,
jobject thiz,
jint max_size) {
auto *suggestionList = new SuggestionList(max_size);
return reinterpret_cast<jlong>(suggestionList);
}
extern "C"
JNIEXPORT void JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeDispose(
JNIEnv *env,
jobject thiz,
jlong native_ptr) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
suggestionList->clear();
delete suggestionList;
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeAdd(
JNIEnv *env,
jobject thiz,
jlong native_ptr,
jstring word,
jint freq) {
const char *cWord = env->GetStringUTFChars(word, nullptr);
word_t stdWord = word_t(cWord);
env->ReleaseStringUTFChars(word, cWord);
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
return suggestionList->add(std::move(stdWord), freq);
}
extern "C"
JNIEXPORT void JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeClear(
JNIEnv *env,
jobject thiz,
jlong native_ptr) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
suggestionList->clear();
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeContains(
JNIEnv *env,
jobject thiz,
jlong native_ptr,
jstring element) {
const char *cWord = env->GetStringUTFChars(element, nullptr);
const word_t stdWord = word_t(cWord);
env->ReleaseStringUTFChars(element, cWord);
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
return suggestionList->containsWord(stdWord);
}
extern "C"
JNIEXPORT jstring JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeGetOrNull(
JNIEnv *env,
jobject thiz,
jlong native_ptr,
jint index) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
auto weightedToken = suggestionList->get(index);
if (weightedToken == nullptr) {
return nullptr;
}
return env->NewStringUTF(weightedToken->data.c_str());
}
extern "C"
JNIEXPORT jint JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeSize(
JNIEnv *env,
jobject thiz,
jlong native_ptr) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
return suggestionList->size();
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeGetIsPrimaryTokenAutoInsert(
JNIEnv *env, jobject thiz, jlong native_ptr) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
return suggestionList->isPrimaryTokenAutoInsert;
}
extern "C"
JNIEXPORT void JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeSetIsPrimaryTokenAutoInsert(
JNIEnv *env, jobject thiz, jlong native_ptr, jboolean v) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
suggestionList->isPrimaryTokenAutoInsert = v;
}

View File

View File

@@ -0,0 +1,13 @@
add_library(
# Name
ime-nlp
# Headers
nlp.h
token.h
suggestion_list.h
# Sources
token.cpp
suggestion_list.cpp
)

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef FLORISBOARD_NLP_H
#define FLORISBOARD_NLP_H
#include <string>
namespace ime::nlp {
typedef std::string word_t;
typedef uint16_t freq_t;
static const freq_t FREQ_VALUE_MASK = 0xFF;
static const freq_t FREQ_POSSIBLY_OFFENSIVE = 0x01;
} // namespace ime::nlp
#endif // FLORISBOARD_NLP_H

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "suggestion_list.h"
#include <utility>
using namespace ime::nlp;
SuggestionList::SuggestionList(size_t _maxSize) :
maxSize(_maxSize), internalSize(0), tokens(_maxSize), isPrimaryTokenAutoInsert(false)
{ }
SuggestionList::~SuggestionList() = default;
bool SuggestionList::add(word_t &&word, freq_t &&freq) {
auto entryIndex = indexOfWord(word);
if (entryIndex.has_value()) {
// Word exists already
auto entry = tokens[entryIndex.value()];
if (entry.freq < freq) {
// Need to update freq
entry.freq = freq;
} else {
return false;
}
} else {
if (internalSize < maxSize) {
tokens[internalSize++] = WeightedToken(std::move(word), freq);
} else {
auto last = tokens[internalSize - 1];
if (last.freq < freq) {
tokens[internalSize - 1] = WeightedToken(std::move(word), freq);
} else {
return false;
}
}
}
std::sort(tokens.begin(), tokens.begin() + internalSize, std::greater<>());
return true;
}
void SuggestionList::clear() {
internalSize = 0;
isPrimaryTokenAutoInsert = false;
}
bool SuggestionList::contains(const WeightedToken &element) const {
return indexOf(element).has_value();
}
bool SuggestionList::containsWord(const word_t &word) const {
return indexOfWord(word).has_value();
}
const WeightedToken *SuggestionList::get(size_t index) const {
if (index < 0 || index >= internalSize) return nullptr;
return &tokens[index];
}
std::optional<size_t> SuggestionList::indexOf(const WeightedToken &element) const {
for (size_t n = 0; n < internalSize; n++) {
if (element == tokens[n]) {
return n;
}
}
return std::nullopt;
}
std::optional<size_t> SuggestionList::indexOfWord(const word_t &word) const {
for (size_t n = 0; n < internalSize; n++) {
if (word == tokens[n].data) {
return n;
}
}
return std::nullopt;
}
bool SuggestionList::isEmpty() const {
return internalSize == 0;
}
size_t SuggestionList::size() const {
return internalSize;
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef FLORISBOARD_SUGGESTION_LIST_H
#define FLORISBOARD_SUGGESTION_LIST_H
#include <optional>
#include <vector>
#include "token.h"
namespace ime::nlp {
class SuggestionList {
public:
SuggestionList(size_t _maxSize);
~SuggestionList();
bool add(word_t &&word, freq_t &&freq);
void clear();
bool contains(const WeightedToken &element) const;
bool containsWord(const word_t &word) const;
const WeightedToken *get(size_t index) const;
std::optional<size_t> indexOf(const WeightedToken &element) const;
std::optional<size_t> indexOfWord(const word_t &word) const;
bool isEmpty() const;
size_t size() const;
bool isPrimaryTokenAutoInsert;
private:
std::vector<WeightedToken> tokens;
size_t internalSize;
size_t maxSize;
};
} // namespace ime::nlp
#endif // FLORISBOARD_SUGGESTION_LIST_H

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "token.h"
#include <utility>
namespace ime::nlp {
Token::Token() : data() {}
Token::Token(word_t &&_data) : data(std::move(_data)) {}
bool operator==(const Token &t1, const Token &t2) {
return t1.data == t2.data;
}
bool operator!=(const Token &t1, const Token &t2) {
return !(t1 == t2);
}
WeightedToken::WeightedToken() : Token(), freq(0) {}
WeightedToken::WeightedToken(word_t &&_data, freq_t _freq) : Token(std::move(_data)), freq(_freq) {}
bool operator==(const WeightedToken &t1, const WeightedToken &t2) {
return t1.data == t2.data && t1.freq == t2.freq;
}
bool operator!=(const WeightedToken &t1, const WeightedToken &t2) {
return !(t1 == t2);
}
bool operator<(const WeightedToken &t1, const WeightedToken &t2) {
return t1.freq < t2.freq;
}
bool operator<=(const WeightedToken &t1, const WeightedToken &t2) {
return t1.freq <= t2.freq;
}
bool operator>(const WeightedToken &t1, const WeightedToken &t2) {
return t1.freq > t2.freq;
}
bool operator>=(const WeightedToken &t1, const WeightedToken &t2) {
return t1.freq >= t2.freq;
}
} // namespace ime::nlp

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef FLORISBOARD_TOKEN_H
#define FLORISBOARD_TOKEN_H
#include "nlp.h"
#include <string>
namespace ime::nlp {
class Token {
public:
word_t data;
Token();
Token(word_t &&_data);
friend bool operator==(const Token &t1, const Token &t2);
friend bool operator!=(const Token &t1, const Token &t2);
};
class WeightedToken : public Token {
public:
freq_t freq;
WeightedToken();
WeightedToken(word_t &&_data, freq_t _freq);
friend bool operator==(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator!=(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator<(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator<=(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator>(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator>=(const WeightedToken &t1, const WeightedToken &t2);
};
} // namespace ime::nlp
#endif // FLORISBOARD_TOKEN_H

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Patrick Goldinger
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,22 +14,21 @@
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.core
package dev.patrickgold.florisboard
import android.app.Application
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.crashutility.CrashUtility
import dev.patrickgold.florisboard.debug.Flog
import dev.patrickgold.florisboard.debug.LogTopic
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.ime.core.SubtypeManager
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
import dev.patrickgold.florisboard.ime.extension.AssetManager
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import timber.log.Timber
@Suppress("unused")
class FlorisApplication : Application(), CoroutineScope by MainScope() {
class FlorisApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
@@ -43,11 +42,11 @@ class FlorisApplication : Application(), CoroutineScope by MainScope() {
flogOutputs = Flog.OUTPUT_CONSOLE
)
CrashUtility.install(this)
val prefHelper = PrefHelper.getDefaultInstance(this)
val prefs = Preferences.initDefault(this)
val assetManager = AssetManager.init(this)
SubtypeManager.init(this, prefHelper)
SubtypeManager.init(this)
DictionaryManager.init(this)
ThemeManager.init(this, assetManager, prefHelper)
prefHelper.initDefaultPreferences()
ThemeManager.init(this, assetManager)
prefs.initDefaultPreferences()
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard
import dev.patrickgold.florisboard.ime.core.FlorisBoard
/**
* This class only exists to prevent accidental IME deactivation after an update
* of FlorisBoard to a new version when the location of the FlorisBoard class has
* changed. The Android Framework uses the service class path as the IME id,
* using this extension here makes sure it won't change ever again for the system.
*
* Important: DO NOT PUT ANY LOGIC INTO THIS CLASS. Make the necessary changes
* within the FlorisBoard class instead.
*/
class FlorisImeService : FlorisBoard()

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.core
package dev.patrickgold.florisboard.common
import android.content.ClipData
import android.content.ClipboardManager
@@ -26,6 +26,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding
import com.google.android.material.snackbar.Snackbar
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
@@ -34,10 +35,7 @@ abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity(), CoroutineS
private var _binding: V? = null
protected val binding: V
get() = _binding!!
private var _prefs: PrefHelper? = null
protected val prefs: PrefHelper
get() = _prefs!!
protected val prefs: Preferences get() = Preferences.default()
private var errorDialog: AlertDialog? = null
private var errorSnackbar: Snackbar? = null
@@ -48,7 +46,6 @@ abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity(), CoroutineS
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_prefs = PrefHelper.getDefaultInstance(applicationContext)
onCreateBinding().let {
_binding = it
setContentView(it.root)
@@ -59,7 +56,6 @@ abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity(), CoroutineS
super.onDestroy()
cancel()
_binding = null
_prefs = null
errorDialog?.dismiss()
errorDialog = null
errorSnackbar?.dismiss()
@@ -102,7 +98,7 @@ abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity(), CoroutineS
errorDialog?.dismiss()
errorDialog = AlertDialog.Builder(this@FlorisActivity).run {
setTitle(R.string.assets__error__details)
setMessage(errorThrowable.toString())
setMessage(errorThrowable?.stackTraceToString())
setPositiveButton(android.R.string.ok, null)
setNeutralButton(R.string.crash_dialog__copy_to_clipboard) { _, _ ->
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE)

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.core
package dev.patrickgold.florisboard.common
import android.content.Context
import android.util.AttributeSet

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.common
/**
* Type alias for a native pointer.
*/
typealias NativePtr = Long
/**
* Constant value for a native null pointer.
*/
const val NATIVE_NULLPTR: NativePtr = 0
/**
* Generic interface for a native instance object. Defines the basic
* methods which each native instance wrapper should define and be able
* to handle to.
*/
interface NativeInstanceWrapper {
/**
* Returns the native pointer of this instance. The returned pointer
* is only valid if [dispose] has not been previously called.
*
* @return The native null pointer for this instance.
*/
fun nativePtr(): NativePtr
/**
* Deletes the native object and frees allocated resources. After
* invoking this method one MUST NOT touch this instance ever again.
*/
fun dispose()
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.common
import androidx.annotation.RestrictTo
/**
* A simple helper object managing touch pointer objects. This class is designed to hold
* at max [capacity] at once. It tries to reduce the need to recreate objects and to resize
* arrays by creating a fixed-size list and by reusing pointers. This map supports iterating
* over all active pointers.
*
* @property capacity The capacity of this map, determining the maximum number of pointers this
* map can hold at once. This value must be greater than or equal to one. Should a smaller capacity
* be passed, automatically the minimum capacity `1` is assumed.
* @param init The initializer for each pointer. Note that [Pointer.reset] is called before
* storing the new object, to ensure that this pointer is not initialized with some pointer data.
*/
class PointerMap<P : Pointer>(val capacity: Int = 4, init: (Int) -> P) : Iterable<P> {
/**
* The internal list of pointers, is not intended for public access.
*/
private val pointers: List<P> = List(capacity.coerceAtLeast(1)) { i ->
init(i).also { pointer -> pointer.reset() }
}
/**
* Adds a new pointer with given [id] and [index] and returns it. If this map is already at max
* capacity, null is returned and the pointer could not be added.
*
* @param id The id of the pointer to add.
* @param index The index of the pointer to add.
*
* @return The newly added pointer or null if the map is already full.
*/
fun add(id: Int, index: Int): P? {
for (pointer in pointers) {
if (pointer.isNotUsed) {
pointer.id = id
pointer.index = index
return pointer
}
}
return null
}
/**
* Clears this map and resets all pointers.
*/
fun clear() {
for (pointer in pointers) {
pointer.reset()
}
}
/**
* Finds a pointer by given [id].
*
* @param id The id of the pointer which should be found.
*
* @return The pointer with given [id] or null.
*/
fun findById(id: Int): P? {
for (pointer in pointers) {
if (pointer.id == id) {
return pointer
}
}
return null
}
/**
* Gets a pointer from the internal array based on the internal array index. This method
* is intended to be used only by the [PointerIterator].
*
* @param index
*
* @return The pointer for given index or null, excluding unused pointers.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
fun get(index: Int): P? {
val pointer = pointers.getOrNull(index)
if (pointer != null && pointer.isUsed) {
return pointer
}
return null
}
override fun iterator(): Iterator<P> {
return PointerIterator(this)
}
/**
* Removes a pointer with given [id] and returns a boolean result.
*
* @param id The id of the pointer to remove. If the id is not existent, noting happens.
*
* @return True if a pointer was removed, false otherwise.
*/
fun removeById(id: Int): Boolean {
for (pointer in pointers) {
if (pointer.id == id) {
pointer.reset()
return true
}
}
return false
}
/**
* Returns the size of this map (only counting active pointers). This value is anywhere
* between 0 and [capacity].
*/
val size: Int
get() = pointers.count { it.isUsed }
}
class PointerIterator<P : Pointer>(private val pointerMap: PointerMap<P>) : Iterator<P> {
private var index: Int = 0
override fun hasNext(): Boolean {
do {
if (pointerMap.get(index) != null) {
return true
}
} while (++index < pointerMap.capacity)
return false
}
override fun next(): P {
return pointerMap.get(index++)!!
}
}
/**
* Abstract touch pointer definition.
*/
abstract class Pointer {
companion object {
const val UNUSED_P: Int = -1
}
/**
* The id of this pointer, corresponds to the motion event this pointer originated.
*/
var id: Int = UNUSED_P
/**
* The index of this pointer, corresponds to the motion event this pointer originated.
*/
var index: Int = UNUSED_P
/**
* True if this pointer is used and active, false otherwise.
*/
val isUsed: Boolean
get() = id >= 0
/**
* False if this pointer is used and active, true otherwise.
*/
val isNotUsed: Boolean
get() = !isUsed
/**
* Resets this pointer to be used again.
*/
open fun reset() {
id = UNUSED_P
index = UNUSED_P
}
}

View File

@@ -1,5 +1,6 @@
/*
* Copyright (C) 2011 The Android Open Source Project
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,16 +15,15 @@
* limitations under the License.
*/
package dev.patrickgold.florisboard.util
package dev.patrickgold.florisboard.common
import android.content.Context
import android.content.res.Resources
import android.util.DisplayMetrics
import android.view.View
import android.view.Window
import android.widget.FrameLayout
import android.widget.LinearLayout
/**
* This file has been taken from the Android LatinIME project. Following modifications were done to
* the original source code:
@@ -38,7 +38,7 @@ import android.widget.LinearLayout
* The original source code can be found at the following location:
* https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+/refs/heads/master/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java
*/
object ViewLayoutUtils {
object ViewUtils {
fun updateLayoutHeightOf(window: Window, layoutHeight: Int) {
val params = window.attributes
if (params != null && params.height != layoutHeight) {
@@ -81,11 +81,10 @@ object ViewLayoutUtils {
* Source: https://stackoverflow.com/a/9563438/6801193 (by Muhammad Nabeel Arif)
*
* @param dp A value in dp (density independent pixels) unit. Which we need to convert into pixels
* @param context Context to get resources and device specific display metrics
* @return A float value to represent px equivalent to dp depending on device density
*/
fun convertDpToPixel(dp: Float, context: Context): Float {
return dp * (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
fun dp2px(dp: Float): Float {
return dp * (Resources.getSystem().displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
}
/**
@@ -94,10 +93,9 @@ object ViewLayoutUtils {
* Source: https://stackoverflow.com/a/9563438/6801193 (by Muhammad Nabeel Arif)
*
* @param px A value in px (pixels) unit. Which we need to convert into db
* @param context Context to get resources and device specific display metrics
* @return A float value to represent dp equivalent to px value
*/
fun convertPixelsToDp(px: Float, context: Context): Float {
return px / (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
fun px2dp(px: Float): Float {
return px / (Resources.getSystem().displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
}
}

View File

@@ -26,14 +26,13 @@ import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.CrashDialogBinding
import dev.patrickgold.florisboard.debug.*
import dev.patrickgold.florisboard.ime.core.PrefHelper
import java.util.*
import dev.patrickgold.florisboard.ime.core.Preferences
class CrashDialogActivity : AppCompatActivity() {
private lateinit var binding: CrashDialogBinding
private var stacktraces: List<CrashUtility.Stacktrace> = listOf()
private var errorReport: StringBuilder = StringBuilder()
private var prefs: PrefHelper? = null
private var prefs: Preferences? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -43,7 +42,7 @@ class CrashDialogActivity : AppCompatActivity() {
// We secure the PrefHelper usage here because the PrefHelper could potentially be the
// source of the crash, thus making the crash dialog unusable if not wrapped.
try {
prefs = PrefHelper.getDefaultInstance(this)
prefs = Preferences.default()
} catch (_: Exception) {
}
@@ -136,10 +135,10 @@ class CrashDialogActivity : AppCompatActivity() {
private fun getDeviceName(): String {
val manufacturer = Build.MANUFACTURER
val model = Build.MODEL
return if (model.toLowerCase(Locale.ENGLISH).startsWith(manufacturer.toLowerCase(Locale.ENGLISH))) {
model.capitalize(Locale.ENGLISH)
return if (model.lowercase().startsWith(manufacturer.lowercase())) {
model.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
} else {
"${manufacturer.capitalize(Locale.ENGLISH)} $model"
"${manufacturer.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }} $model"
}
}

View File

@@ -53,6 +53,7 @@ abstract class CrashUtility private constructor() {
private const val UNHANDLED_STACKTRACE_FILE_EXT = "stacktrace"
private var lastActivityCreated: WeakReference<Activity?> = WeakReference(null)
private var stagedException: Throwable? = null
/**
* Installs the CrashUtility crash handler for the given package [context]. Also registers
@@ -148,6 +149,22 @@ abstract class CrashUtility private constructor() {
return true
}
fun stageException(e: Throwable?) {
if (stagedException == null) {
stagedException = e
}
}
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
fun handleStagedButUnhandledExceptions() {
val e = stagedException ?: return
val handler = Thread.getDefaultUncaughtExceptionHandler()
if (handler is UncaughtExceptionHandler) {
stagedException = null
handler.uncaughtException(null, e)
}
}
/**
* Reads and returns all unhandled stacktrace files.
*
@@ -362,7 +379,6 @@ abstract class CrashUtility private constructor() {
flogInfo(LogTopic.CRASH_UTILITY) {
"Detected application crash, executing custom crash handler."
}
thread ?: return
throwable ?: return
val timestamp = System.currentTimeMillis()
val stacktrace = Log.getStackTraceString(throwable)

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
@file:OptIn(ExperimentalContracts::class, ExperimentalUnsignedTypes::class)
@file:OptIn(ExperimentalContracts::class)
package dev.patrickgold.florisboard.debug

View File

@@ -14,8 +14,6 @@
* limitations under the License.
*/
@file:OptIn(ExperimentalUnsignedTypes::class)
package dev.patrickgold.florisboard.debug
/**
@@ -38,6 +36,7 @@ object LogTopic {
const val LAYOUT_MANAGER: FlogTopic = 8u
const val TEXT_KEYBOARD_VIEW: FlogTopic = 16u
const val GESTURES: FlogTopic = 32u
const val SMARTBAR: FlogTopic = 64u
const val GLIDE: FlogTopic = 512u
const val CLIPBOARD: FlogTopic = 1024u

View File

@@ -64,7 +64,7 @@ class ClipboardHistoryView : LinearLayout, FlorisBoard.EventListener,
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val height = florisboard?.inputView?.desiredMediaKeyboardViewHeight ?: 0.0f
val height = florisboard?.uiBinding?.inputView?.desiredMediaKeyboardViewHeight ?: 0.0f
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height.roundToInt(), MeasureSpec.EXACTLY))
}

View File

@@ -3,15 +3,14 @@ package dev.patrickgold.florisboard.ime.clip
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.View
import android.widget.*
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.FlorisboardBinding
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
import dev.patrickgold.florisboard.ime.core.InputView
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.keyboard.BasicTextKeyData
import kotlinx.coroutines.*
@@ -43,29 +42,23 @@ class ClipboardInputManager private constructor() : CoroutineScope by MainScope(
florisboard.addEventListener(this)
}
/**
* Called when a new input view has been registered. Used to initialize all media-relevant
* views and layouts.
*/
@SuppressLint("ClickableViewAccessibility")
override fun onRegisterInputView(inputView: InputView) {
launch(Dispatchers.Default) {
inputView.findViewById<ImageButton>(R.id.back_to_keyboard_button)
.setOnTouchListener { view, event -> onButtonPressEvent(view, event) }
inputView.findViewById<ImageButton>(R.id.clear_clipboard_history)
.setOnTouchListener { view, event -> onButtonPressEvent(view, event) }
recyclerView = inputView.findViewById(R.id.clipboard_history_items)
override fun onInitializeInputUi(uiBinding: FlorisboardBinding) {
uiBinding.clipboard.backToKeyboardButton
.setOnTouchListener { view, event -> onButtonPressEvent(view, event) }
uiBinding.clipboard.clearClipboardHistory
.setOnTouchListener { view, event -> onButtonPressEvent(view, event) }
recyclerView = uiBinding.clipboard.clipboardHistoryItems.also {
if (BuildConfig.DEBUG && adapter == null) {
error("initClipboard() not called")
}
recyclerView!!.adapter = adapter
it.adapter = adapter
val manager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
recyclerView!!.layoutManager = manager
it.layoutManager = manager
}
}
/**
@@ -79,8 +72,8 @@ class ClipboardInputManager private constructor() : CoroutineScope by MainScope(
/**
* Returns a reference to the [ClipboardHistoryView]
*/
fun getClipboardHistoryView(): ClipboardHistoryView{
return FlorisBoard.getInstance().inputView?.mainViewFlipper?.getChildAt(2) as ClipboardHistoryView
fun getClipboardHistoryView(): ClipboardHistoryView {
return florisboard.uiBinding?.mainViewFlipper?.getChildAt(2) as ClipboardHistoryView
}
/**

View File

@@ -10,7 +10,7 @@ import android.widget.LinearLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import dev.patrickgold.florisboard.common.ViewUtils
class ClipboardPopupView: LinearLayout, ThemeManager.OnThemeUpdatedListener {
@@ -23,7 +23,7 @@ class ClipboardPopupView: LinearLayout, ThemeManager.OnThemeUpdatedListener {
)
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
setCornerRadius(ViewUtils.dp2px(6.0f))
}
private val themeManager: ThemeManager = ThemeManager.default()

View File

@@ -7,7 +7,7 @@ import android.os.Handler
import android.os.Looper
import dev.patrickgold.florisboard.ime.clip.provider.*
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.util.cancelAll
import dev.patrickgold.florisboard.util.postAtScheduledRate
import timber.log.Timber
@@ -49,7 +49,7 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
private var onPrimaryClipChangedListeners: ArrayList<OnPrimaryClipChangedListener> = arrayListOf()
private lateinit var systemClipboardManager: ClipboardManager
private lateinit var handler: Handler
private lateinit var prefHelper: PrefHelper
private val prefs get() = Preferences.default()
data class TimedClipData(val data: ClipboardItem, val timeUTC: Long)
@@ -105,24 +105,47 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
* Adds a new item to the clipboard history (if enabled).
*/
fun updateHistory(newData: ClipboardItem) {
val clipboardPrefs = prefHelper.clipboard
val clipboardPrefs = prefs.clipboard
if (clipboardPrefs.enableHistory) {
if (clipboardPrefs.limitHistorySize) {
var numRemoved = 0
while (history.size >= clipboardPrefs.maxHistorySize) {
numRemoved += 1
history.removeLast().data.close()
}
ClipboardInputManager.getInstance().notifyItemRangeRemoved(history.size, numRemoved)
}
val clipboardInputManager = ClipboardInputManager.getInstance()
val timed = TimedClipData(newData, System.currentTimeMillis())
history.addFirst(timed)
ClipboardInputManager.getInstance().notifyItemInserted(pins.size)
val historyElement = history.firstOrNull { it.data.type == ItemType.TEXT && it.data.text == newData.text }
if (historyElement != null) {
moveToTheBeginning(historyElement, newData, clipboardInputManager)
} else {
if (clipboardPrefs.limitHistorySize) {
var numRemoved = 0
while (history.size >= clipboardPrefs.maxHistorySize) {
numRemoved += 1
history.removeLast().data.close()
}
clipboardInputManager.notifyItemRangeRemoved(history.size, numRemoved)
}
createAndAddNewTimedClipData(newData)
clipboardInputManager.notifyItemInserted(pins.size)
}
}
}
/**
* Moves a ClipboardItem to the beginning of the history by removing the old one and creating a new one
*/
private fun moveToTheBeginning(
historyElement: TimedClipData,
newData: ClipboardItem,
clipboardInputManager: ClipboardInputManager
) {
val elementsPosition = history.indexOf(historyElement)
history.remove(historyElement)
createAndAddNewTimedClipData(newData)
clipboardInputManager.notifyItemMoved(elementsPosition, 0)
clipboardInputManager.notifyItemChanged(0)
}
/**
* Used so that [onPrimaryClipChanged] knows whether it was called by [changeCurrent] (and hence shouldn't update
* history)
@@ -133,14 +156,14 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
* Changes current clipboard item. WITHOUT updating the history.
*/
fun changeCurrent(newData: ClipboardItem, closePrevious: Boolean) {
if (prefHelper.clipboard.enableInternal) {
if (prefs.clipboard.enableInternal) {
if (closePrevious) current?.close()
current = newData
val isEqual = when (newData.type) {
ItemType.TEXT -> newData.text == systemClipboardManager.primaryClip?.getItemAt(0)?.text
ItemType.IMAGE -> newData.uri == systemClipboardManager.primaryClip?.getItemAt(0)?.uri
}
if (prefHelper.clipboard.syncToSystem && !isEqual)
if (prefs.clipboard.syncToSystem && !isEqual)
systemClipboardManager.setPrimaryClip(newData.toClipData())
} else {
shouldUpdateHistory = false
@@ -156,7 +179,7 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
fun addNewClip(newData: ClipboardItem) {
updateHistory(newData)
// If history is disabled, this new item will replace the old one and hence should be closed.
changeCurrent(newData, !prefHelper.clipboard.enableHistory)
changeCurrent(newData, !prefs.clipboard.enableHistory)
}
/**
@@ -168,7 +191,7 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
}
val primaryClip: ClipboardItem?
get() = if (prefHelper.clipboard.enableInternal) {
get() = if (prefs.clipboard.enableInternal) {
current
} else {
systemClipboardManager.primaryClip?.let { ClipboardItem.fromClipData(it, false) }
@@ -204,13 +227,13 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
ItemType.IMAGE -> internalPrimaryClip.uri == systemPrimaryClip.getItemAt(0)?.uri
else -> false
}
if (prefHelper.clipboard.enableInternal) {
if (prefs.clipboard.enableInternal) {
// In the event that the internal clipboard is enabled, sync to internal clipboard is enabled
// and the item is not already in internal clipboard, add it.
if (prefHelper.clipboard.syncToFloris && !isEqual) {
if (prefs.clipboard.syncToFloris && !isEqual) {
addNewClip(ClipboardItem.fromClipData(systemPrimaryClip, true))
}
} else if (prefHelper.clipboard.enableHistory) {
} else if (prefs.clipboard.enableHistory) {
// in the event history is enabled, and it should be updated it is updated
if (shouldUpdateHistory) {
updateHistory(ClipboardItem.fromClipData(systemPrimaryClip, false))
@@ -247,16 +270,14 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
systemClipboardManager = (context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager)
systemClipboardManager.addPrimaryClipChangedListener(this)
prefHelper = PrefHelper.getDefaultInstance(context)
val cleanUpClipboard = Runnable {
if (!prefHelper.clipboard.cleanUpOld) {
if (!prefs.clipboard.cleanUpOld) {
return@Runnable
}
val currentTime = System.currentTimeMillis()
var numToPop = 0
val expiryTime = prefHelper.clipboard.cleanUpAfter * 60 * 1000
val expiryTime = prefs.clipboard.cleanUpAfter * 60 * 1000
for (item in history.asReversed()) {
if (item.timeUTC + expiryTime < currentTime) {
numToPop += 1
@@ -271,7 +292,7 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
}
FlorisBoard.getInstance().clipInputManager.initClipboard(this.history, this.pins)
handler = Handler(Looper.getMainLooper())
prefHelper
prefs
handler.postAtScheduledRate(0, INTERVAL, cleanUpClipboard)
executor = FlorisBoard.getInstance().asyncExecutor
executor.execute {
@@ -333,7 +354,7 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
val clipInputManager = FlorisBoard.getInstance().clipInputManager
val item = pins.removeAt(adapterPos)
val clipboardPrefs = prefHelper.clipboard
val clipboardPrefs = prefs.clipboard
if (clipboardPrefs.limitHistorySize) {
var numRemoved = 0
while (history.size >= clipboardPrefs.maxHistorySize) {
@@ -343,8 +364,7 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
ClipboardInputManager.getInstance().notifyItemRangeRemoved(history.size, numRemoved)
}
val timed = TimedClipData(item, System.currentTimeMillis())
history.addFirst(timed)
createAndAddNewTimedClipData(item)
clipInputManager.notifyItemMoved(adapterPos, pins.size)
clipInputManager.notifyItemChanged(pins.size)
@@ -354,6 +374,14 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
}
}
/**
* Creates a new TimedClipData and adds it to the history
*/
private fun createAndAddNewTimedClipData(newData: ClipboardItem) {
val timed = TimedClipData(newData, System.currentTimeMillis())
history.addFirst(timed)
}
fun removeClip(pos: Int) {
when {
pos < pins.size -> {

View File

@@ -21,32 +21,33 @@ import android.content.Intent
import android.inputmethodservice.InputMethodService
import android.os.Build
import android.os.SystemClock
import android.text.InputType
import android.view.InputDevice
import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import androidx.annotation.RequiresApi
import androidx.core.text.isDigitsOnly
import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.view.inputmethod.InputContentInfoCompat
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.clip.provider.ItemType
import dev.patrickgold.florisboard.ime.keyboard.ImeOptions
import dev.patrickgold.florisboard.ime.keyboard.InputAttributes
import dev.patrickgold.florisboard.ime.keyboard.KeyboardState
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.composing.Composer
import timber.log.Timber
/**
* Class which holds information relevant to an editor instance like the [cachedInput], [selection],
* [inputAttributes], [imeOptions], etc. This class is thought to be an improved [EditorInfo]
* Class which holds information relevant to an editor instance like the [cachedInput],
* [selection] etc. This class is thought to be an improved [EditorInfo]
* object which also holds the state of the currently focused input editor.
*/
class EditorInstance private constructor(
private val ims: InputMethodService?,
val imeOptions: ImeOptions,
val inputAttributes: InputAttributes,
val packageName: String,
val activeState: KeyboardState,
private val editorInfo: EditorInfo
) {
val cachedInput: CachedInput = CachedInput(this)
@@ -56,25 +57,12 @@ class EditorInstance private constructor(
get() {
val ic = inputConnection ?: return InputAttributes.CapsMode.NONE
return InputAttributes.CapsMode.fromFlags(
ic.getCursorCapsMode(inputAttributes.capsMode.toFlags())
ic.getCursorCapsMode(activeState.inputAttributes.capsMode.toFlags())
)
}
val inputConnection: InputConnection?
get() = ims?.currentInputConnection
var isComposingEnabled: Boolean = false
set(v) {
field = v
cachedInput.reevaluate()
if (v && !isRawInputEditor) {
markComposingRegion(cachedInput.currentWord)
} else {
markComposingRegion(null)
}
}
var shouldReevaluateComposingSuggestions: Boolean = false
var isPrivateMode: Boolean = false
val isRawInputEditor: Boolean
get() = inputAttributes.type == InputAttributes.Type.NULL
var selection: Selection = Selection(this)
private set
var isPhantomSpaceActive: Boolean = false
@@ -85,22 +73,20 @@ class EditorInstance private constructor(
fun default(): EditorInstance {
return EditorInstance(
ims = null,
imeOptions = ImeOptions.fromImeOptionsInt(EditorInfo.IME_NULL),
inputAttributes = InputAttributes.fromInputTypeInt(InputType.TYPE_NULL),
packageName = "undefined",
activeState = KeyboardState.new(),
editorInfo = EditorInfo()
)
}
fun from(editorInfo: EditorInfo?, ims: InputMethodService?): EditorInstance {
fun from(editorInfo: EditorInfo?, ims: InputMethodService?, state: KeyboardState): EditorInstance {
return if (editorInfo == null) {
default()
} else {
EditorInstance(
ims = ims,
imeOptions = ImeOptions.fromImeOptionsInt(editorInfo.imeOptions),
inputAttributes = InputAttributes.fromInputTypeInt(editorInfo.inputType),
packageName = editorInfo.packageName,
activeState = state,
editorInfo = editorInfo
).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
@@ -124,6 +110,9 @@ class EditorInstance private constructor(
newSelStart: Int, newSelEnd: Int,
candidatesStart: Int, candidatesEnd: Int
) {
if (newSelStart == oldSelStart && newSelEnd == oldSelEnd) {
return
}
// The Android Framework allows that start can be greater than end in some cases. To prevent bugs in the Floris
// input logic, we swap start and end here if this should really be the case.
if (newSelEnd < newSelStart) {
@@ -136,13 +125,30 @@ class EditorInstance private constructor(
} else if (isPhantomSpaceActive && !wasPhantomSpaceActiveLastUpdate) {
wasPhantomSpaceActiveLastUpdate = true
}
cachedInput.update()
if (isComposingEnabled && candidatesStart >= 0 && candidatesEnd >= 0) {
shouldReevaluateComposingSuggestions = true
if (selection.isCursorMode) {
cachedInput.update()
if (activeState.isComposingEnabled) {
if (candidatesStart >= 0 && candidatesEnd >= 0) {
shouldReevaluateComposingSuggestions = true
}
if (activeState.isRichInputEditor && !isPhantomSpaceActive) {
markComposingRegion(cachedInput.currentWord)
} else if (newSelStart >= 0) {
markComposingRegion(null)
}
}
} else {
if (candidatesStart >= 0 || candidatesEnd >= 0) {
markComposingRegion(null)
}
}
if (selection.isCursorMode && isComposingEnabled && !isRawInputEditor && !isPhantomSpaceActive) {
}
fun composingEnabledChanged() {
cachedInput.reevaluate()
if (activeState.isComposingEnabled && activeState.isRichInputEditor) {
markComposingRegion(cachedInput.currentWord)
} else if (newSelStart >= 0) {
} else {
markComposingRegion(null)
}
}
@@ -156,7 +162,7 @@ class EditorInstance private constructor(
*/
fun commitCompletion(text: String): Boolean {
val ic = inputConnection ?: return false
return if (isRawInputEditor) {
return if (activeState.isRawInputEditor) {
false
} else {
ic.beginBatchEdit()
@@ -172,6 +178,26 @@ class EditorInstance private constructor(
}
}
/**
* Internal helper, replacing a call to inputConnection.commitText with text composition in mind.
*/
fun doCommitText(text: String): Pair<Boolean, String> {
val ic = inputConnection ?: return Pair(false, "")
val composer: Composer = FlorisBoard.getInstance().composer
return if (text.length != 1) {
Pair(ic.commitText(text, 1), text)
} else {
ic.beginBatchEdit()
ic.finishComposingText()
val previous = getTextBeforeCursor(composer.toRead)
val (rm, finalText) = composer.getActions(previous, text[0])
if (rm != 0) ic.deleteSurroundingText(rm, 0)
ic.commitText(finalText, 1)
ic.endBatchEdit()
Pair(true, finalText)
}
}
/**
* Commits the given [text] to this editor instance and adjusts both the cursor position and
* composing region, if any.
@@ -185,8 +211,8 @@ class EditorInstance private constructor(
*/
fun commitText(text: String): Boolean {
val ic = inputConnection ?: return false
return if (isRawInputEditor || selection.isSelectionMode || !isComposingEnabled) {
ic.commitText(text, 1)
return if (activeState.isRawInputEditor || selection.isSelectionMode || !activeState.isComposingEnabled) {
doCommitText(text).first
} else {
ic.beginBatchEdit()
val isWordComponent = CachedInput.isWordComponent(text)
@@ -199,8 +225,8 @@ class EditorInstance private constructor(
}
!isPhantomSpace && isWordComponent -> {
ic.finishComposingText()
ic.commitText(text, 1)
ic.setComposingRegion(cachedInput.currentWord.start, cachedInput.currentWord.end + text.length)
val finalText = doCommitText(text).second
ic.setComposingRegion(cachedInput.currentWord.start, cachedInput.currentWord.end + finalText.length)
}
else -> {
ic.finishComposingText()
@@ -219,7 +245,7 @@ class EditorInstance private constructor(
*/
fun commitGesture(text: String): Boolean {
val ic = inputConnection ?: return false
return if (isRawInputEditor) {
return if (activeState.isRawInputEditor) {
false
} else {
ic.beginBatchEdit()
@@ -302,7 +328,7 @@ class EditorInstance private constructor(
val ic = inputConnection ?: return false
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
return if (n < 1 || isRawInputEditor || !selection.isValid || !selection.isCursorMode) {
return if (n < 1 || activeState.isRawInputEditor || !selection.isValid || !selection.isCursorMode) {
false
} else {
ic.beginBatchEdit()
@@ -340,7 +366,7 @@ class EditorInstance private constructor(
*/
fun getTextAfterCursor(n: Int): String {
val ic = inputConnection
if (ic == null || !selection.isValid || n < 1 || isRawInputEditor) {
if (ic == null || !selection.isValid || n < 1 || activeState.isRawInputEditor) {
return ""
}
return ic.getTextAfterCursor(n, 0)?.toString() ?: ""
@@ -356,7 +382,7 @@ class EditorInstance private constructor(
*/
fun getTextBeforeCursor(n: Int): String {
val ic = inputConnection
if (ic == null || !selection.isValid || n < 1 || isRawInputEditor) {
if (ic == null || !selection.isValid || n < 1 || activeState.isRawInputEditor) {
return ""
}
return ic.getTextBeforeCursor(n.coerceAtMost(selection.start), 0)?.toString() ?: ""
@@ -425,7 +451,7 @@ class EditorInstance private constructor(
*
* @return True on success, false if an error occurred or the input connection is invalid.
*/
private fun markComposingRegion(region: Region?): Boolean {
fun markComposingRegion(region: Region?): Boolean {
val ic = inputConnection ?: return false
return if (region == null || !region.isValid) {
ic.finishComposingText()
@@ -486,7 +512,7 @@ class EditorInstance private constructor(
wasPhantomSpaceActiveLastUpdate = false
markComposingRegion(null)
val ic = inputConnection ?: return false
if (isRawInputEditor) {
if (activeState.isRawInputEditor) {
sendDownUpKeyEvent(KeyEvent.KEYCODE_A, meta(ctrl = true))
} else {
ic.performContextMenuAction(android.R.id.selectAll)
@@ -502,7 +528,7 @@ class EditorInstance private constructor(
fun performEnter(): Boolean {
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
return if (isRawInputEditor) {
return if (activeState.isRawInputEditor) {
sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER)
} else {
commitText("\n")
@@ -516,7 +542,7 @@ class EditorInstance private constructor(
*
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun performEnterAction(action: ImeOptions.Action): Boolean {
fun performEnterAction(action: ImeOptions.EnterAction): Boolean {
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
val ic = inputConnection ?: return false
@@ -625,26 +651,26 @@ class EditorInstance private constructor(
val ic = inputConnection ?: return false
ic.beginBatchEdit()
val eventTime = SystemClock.uptimeMillis()
if (metaState and KeyEvent.META_CTRL_ON > 0) {
if (metaState and KeyEvent.META_CTRL_ON != 0) {
sendDownKeyEvent(eventTime, KeyEvent.KEYCODE_CTRL_LEFT, 0)
}
if (metaState and KeyEvent.META_ALT_ON > 0) {
if (metaState and KeyEvent.META_ALT_ON != 0) {
sendDownKeyEvent(eventTime, KeyEvent.KEYCODE_ALT_LEFT, 0)
}
if (metaState and KeyEvent.META_SHIFT_ON > 0) {
if (metaState and KeyEvent.META_SHIFT_ON != 0) {
sendDownKeyEvent(eventTime, KeyEvent.KEYCODE_SHIFT_LEFT, 0)
}
for (n in 0 until count) {
sendDownKeyEvent(eventTime, keyEventCode, metaState)
sendUpKeyEvent(eventTime, keyEventCode, metaState)
}
if (metaState and KeyEvent.META_SHIFT_ON > 0) {
if (metaState and KeyEvent.META_SHIFT_ON != 0) {
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_SHIFT_LEFT, 0)
}
if (metaState and KeyEvent.META_ALT_ON > 0) {
if (metaState and KeyEvent.META_ALT_ON != 0) {
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_ALT_LEFT, 0)
}
if (metaState and KeyEvent.META_CTRL_ON > 0) {
if (metaState and KeyEvent.META_CTRL_ON != 0) {
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_CTRL_LEFT, 0)
}
ic.endBatchEdit()
@@ -652,227 +678,6 @@ class EditorInstance private constructor(
}
}
/**
* Class which holds the same information as an [EditorInfo.imeOptions] int but more accessible and
* readable.
*/
class ImeOptions private constructor(imeOptions: Int) {
val action: Action = Action.fromInt(imeOptions)
val flagForceAscii: Boolean = imeOptions and EditorInfo.IME_FLAG_FORCE_ASCII > 0
val flagNavigateNext: Boolean = imeOptions and EditorInfo.IME_FLAG_NAVIGATE_NEXT > 0
val flagNavigatePrevious: Boolean = imeOptions and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS > 0
val flagNoAccessoryAction: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION > 0
val flagNoEnterAction: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0
val flagNoExtractUi: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_EXTRACT_UI > 0
val flagNoFullscreen: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_FULLSCREEN > 0
@RequiresApi(Build.VERSION_CODES.O)
val flagNoPersonalizedLearning: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING > 0
companion object {
fun default(): ImeOptions {
return fromImeOptionsInt(EditorInfo.IME_NULL)
}
fun fromImeOptionsInt(imeOptions: Int): ImeOptions {
return ImeOptions(imeOptions)
}
}
enum class Action {
DONE,
GO,
NEXT,
NONE,
PREVIOUS,
SEARCH,
SEND,
UNSPECIFIED;
companion object {
fun fromInt(raw: Int): Action {
return when (raw and EditorInfo.IME_MASK_ACTION) {
EditorInfo.IME_ACTION_DONE -> DONE
EditorInfo.IME_ACTION_GO -> GO
EditorInfo.IME_ACTION_NEXT -> NEXT
EditorInfo.IME_ACTION_NONE -> NONE
EditorInfo.IME_ACTION_PREVIOUS -> PREVIOUS
EditorInfo.IME_ACTION_SEARCH -> SEARCH
EditorInfo.IME_ACTION_SEND -> SEND
EditorInfo.IME_ACTION_UNSPECIFIED -> UNSPECIFIED
else -> NONE
}
}
}
fun toInt(): Int {
return when (this) {
DONE -> EditorInfo.IME_ACTION_DONE
GO -> EditorInfo.IME_ACTION_GO
NEXT -> EditorInfo.IME_ACTION_NEXT
NONE -> EditorInfo.IME_ACTION_NONE
PREVIOUS -> EditorInfo.IME_ACTION_PREVIOUS
SEARCH -> EditorInfo.IME_ACTION_SEARCH
SEND -> EditorInfo.IME_ACTION_SEND
UNSPECIFIED -> EditorInfo.IME_ACTION_UNSPECIFIED
}
}
}
}
/**
* Class which holds the same information as an [EditorInfo.inputType] int but more accessible and
* readable.
*/
class InputAttributes private constructor(inputType: Int) {
val type: Type
val variation: Variation
val capsMode: CapsMode
var flagNumberDecimal: Boolean = false
private set
var flagNumberSigned: Boolean = false
private set
var flagTextAutoComplete: Boolean = false
private set
var flagTextAutoCorrect: Boolean = false
private set
var flagTextImeMultiLine: Boolean = false
private set
var flagTextMultiLine: Boolean = false
private set
var flagTextNoSuggestions: Boolean = false
private set
init {
when (inputType and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_NULL -> {
type = Type.NULL
variation = Variation.NORMAL
capsMode = CapsMode.NONE
}
InputType.TYPE_CLASS_DATETIME -> {
type = Type.DATETIME
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_DATETIME_VARIATION_DATE -> Variation.DATE
InputType.TYPE_DATETIME_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_DATETIME_VARIATION_TIME -> Variation.TIME
else -> Variation.NORMAL
}
capsMode = CapsMode.NONE
}
InputType.TYPE_CLASS_NUMBER -> {
type = Type.NUMBER
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_NUMBER_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> Variation.PASSWORD
else -> Variation.NORMAL
}
capsMode = CapsMode.NONE
flagNumberDecimal = inputType and InputType.TYPE_NUMBER_FLAG_DECIMAL > 0
flagNumberSigned = inputType and InputType.TYPE_NUMBER_FLAG_SIGNED > 0
}
InputType.TYPE_CLASS_PHONE -> {
type = Type.PHONE
variation = Variation.NORMAL
capsMode = CapsMode.NONE
}
InputType.TYPE_CLASS_TEXT -> {
type = Type.TEXT
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> Variation.EMAIL_ADDRESS
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> Variation.EMAIL_SUBJECT
InputType.TYPE_TEXT_VARIATION_FILTER -> Variation.FILTER
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> Variation.LONG_MESSAGE
InputType.TYPE_TEXT_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_TEXT_VARIATION_PASSWORD -> Variation.PASSWORD
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> Variation.PERSON_NAME
InputType.TYPE_TEXT_VARIATION_PHONETIC -> Variation.PHONETIC
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> Variation.POSTAL_ADDRESS
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> Variation.SHORT_MESSAGE
InputType.TYPE_TEXT_VARIATION_URI -> Variation.URI
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> Variation.VISIBLE_PASSWORD
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> Variation.WEB_EDIT_TEXT
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> Variation.WEB_EMAIL_ADDRESS
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> Variation.WEB_PASSWORD
else -> Variation.NORMAL
}
capsMode = CapsMode.fromFlags(inputType)
flagTextAutoComplete = inputType and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE > 0
flagTextAutoCorrect = inputType and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT > 0
flagTextImeMultiLine = inputType and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE > 0
flagTextMultiLine = inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE > 0
flagTextNoSuggestions = inputType and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS > 0
}
else -> {
type = Type.TEXT
variation = Variation.NORMAL
capsMode = CapsMode.NONE
}
}
}
companion object {
fun fromInputTypeInt(inputType: Int): InputAttributes {
return InputAttributes(inputType)
}
}
enum class Type {
DATETIME,
NULL,
NUMBER,
PHONE,
TEXT;
}
enum class Variation {
DATE,
EMAIL_ADDRESS,
EMAIL_SUBJECT,
FILTER,
LONG_MESSAGE,
NORMAL,
PASSWORD,
PERSON_NAME,
PHONETIC,
POSTAL_ADDRESS,
SHORT_MESSAGE,
TIME,
URI,
VISIBLE_PASSWORD,
WEB_EDIT_TEXT,
WEB_EMAIL_ADDRESS,
WEB_PASSWORD;
}
enum class CapsMode {
ALL,
NONE,
SENTENCES,
WORDS;
companion object {
fun fromFlags(flags: Int): CapsMode {
return when {
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0 -> ALL
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0 -> SENTENCES
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0 -> WORDS
else -> NONE
}
}
}
fun toFlags(): Int {
return when (this) {
ALL -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
SENTENCES -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
WORDS -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
else -> 0
}
}
}
}
/**
* Class which marks a region of [CachedInput.rawText] and which provides length and
* validation fields, as well as providing an easy way to get a [text] for this region.
@@ -961,7 +766,7 @@ class Selection(private val editorInstance: EditorInstance) : Region(editorInsta
* selection change.
*/
fun updateAndNotify(newStart: Int, newEnd: Int): Boolean {
return super.update(newStart, newEnd) && if (!editorInstance.isRawInputEditor) {
return super.update(newStart, newEnd) && if (editorInstance.activeState.isRichInputEditor) {
editorInstance.inputConnection?.setSelection(newStart, newEnd) ?: false
} else {
false
@@ -1001,8 +806,8 @@ class CachedInput(private val editorInstance: EditorInstance) {
private set
companion object {
private const val CACHED_TEXT_N_CHARS_BEFORE_CURSOR: Int = 192
private const val CACHED_TEXT_N_CHARS_AFTER_CURSOR: Int = 64
private const val CACHED_TEXT_N_CHARS_BEFORE_CURSOR: Int = 128
private const val CACHED_TEXT_N_CHARS_AFTER_CURSOR: Int = 48
private val WORD_EVAL_REGEX = """[^\p{L}\']""".toRegex()
private val WORD_SPLIT_REGEX_EN = """((?<=$WORD_EVAL_REGEX)|(?=$WORD_EVAL_REGEX))""".toRegex()
@@ -1051,7 +856,7 @@ class CachedInput(private val editorInstance: EditorInstance) {
*/
fun update() = editorInstance.run {
val ic = inputConnection
if (ic == null) {
if (ic == null || selection.isSelectionMode) {
offset = 0
rawText.clear()
expectedMaxLength = 0

View File

@@ -25,21 +25,35 @@ import android.inputmethodservice.ExtractEditText
import android.inputmethodservice.InputMethodService
import android.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.VibrationEffect
import android.os.Vibrator
import android.provider.Settings
import android.view.*
import android.util.Size
import android.view.ContextThemeWrapper
import android.view.Gravity
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InlineSuggestionsRequest
import android.view.inputmethod.InlineSuggestionsResponse
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.FrameLayout
import android.widget.inline.InlinePresentationSpec
import androidx.annotation.RequiresApi
import androidx.annotation.StyleRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.lifecycle.*
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.crashutility.CrashUtility
import dev.patrickgold.florisboard.debug.*
import dev.patrickgold.florisboard.ime.clip.ClipboardInputManager
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
@@ -49,15 +63,29 @@ import dev.patrickgold.florisboard.ime.media.MediaInputManager
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.popup.PopupLayerView
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.composing.Appender
import dev.patrickgold.florisboard.ime.text.composing.Composer
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.*
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.text.key.CurrencySet
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.setup.SetupActivity
import dev.patrickgold.florisboard.util.*
import kotlinx.coroutines.*
import dev.patrickgold.florisboard.util.AppVersionUtils
import dev.patrickgold.florisboard.common.ViewUtils
import dev.patrickgold.florisboard.databinding.FlorisboardBinding
import dev.patrickgold.florisboard.ime.keyboard.KeyboardState
import dev.patrickgold.florisboard.ime.keyboard.updateKeyboardState
import dev.patrickgold.florisboard.util.debugSummarize
import dev.patrickgold.florisboard.util.findViewWithType
import dev.patrickgold.florisboard.util.refreshLayoutOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@@ -77,8 +105,11 @@ private var florisboardInstance: FlorisBoard? = null
/**
* Core class responsible to link together both the text and media input managers as well as
* managing the one-handed UI.
*
* All inline suggestion code has been added based on this demo autofill IME provided by Android directly:
* https://cs.android.com/android/platform/superproject/+/master:development/samples/AutofillKeyboard/src/com/example/android/autofillkeyboard/AutofillImeService.java
*/
class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager.OnPrimaryClipChangedListener,
open class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager.OnPrimaryClipChangedListener,
ThemeManager.OnThemeUpdatedListener {
private val serviceLifecycleDispatcher: ServiceLifecycleDispatcher = ServiceLifecycleDispatcher(this)
@@ -91,16 +122,15 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
* service class should be used directly.
*/
private var _themeContext: Context? = null
private val themeContext: Context
val themeContext: Context
get() = _themeContext ?: this
lateinit var prefs: PrefHelper
private set
private val prefs: Preferences get() = Preferences.default()
val activeState: KeyboardState = KeyboardState.new()
private var extractEditLayout: WeakReference<ViewGroup?> = WeakReference(null)
var inputView: InputView? = null
var uiBinding: FlorisboardBinding? = null
private set
private var inputWindowView: InputWindowView? = null
private var extractEditLayout: WeakReference<ViewGroup?> = WeakReference(null)
var popupLayerView: PopupLayerView? = null
private set
private var eventListeners: CopyOnWriteArrayList<EventListener> = CopyOnWriteArrayList()
@@ -125,30 +155,40 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
var activeEditorInstance: EditorInstance = EditorInstance.default()
val subtypeManager: SubtypeManager get() = SubtypeManager.default()
val composer: Composer get() = subtypeManager.imeConfig.composerFromName.getValue(activeSubtype.composerName)
lateinit var activeSubtype: Subtype
private var currentThemeIsNight: Boolean = false
private var currentThemeResId: Int = 0
private var isNumberRowVisible: Boolean = false
private var isWindowShown: Boolean = false
val textInputManager: TextInputManager
val mediaInputManager: MediaInputManager
val clipInputManager: ClipboardInputManager
private var responseState = ResponseState.RESET
private var pendingResponse: Runnable? = null
private val handler: Handler = Handler(Looper.getMainLooper())
lateinit var textInputManager: TextInputManager
lateinit var mediaInputManager: MediaInputManager
lateinit var clipInputManager: ClipboardInputManager
var isClipboardContextMenuShown = false
init {
florisboardInstance = this
// MUST WRAP all code within Service init in try..catch to prevent any crash loops
try {
florisboardInstance = this
textInputManager = TextInputManager.getInstance()
mediaInputManager = MediaInputManager.getInstance()
clipInputManager = ClipboardInputManager.getInstance()
textInputManager = TextInputManager.getInstance()
mediaInputManager = MediaInputManager.getInstance()
clipInputManager = ClipboardInputManager.getInstance()
System.loadLibrary("florisboard-native")
} catch (e: Exception) {
CrashUtility.stageException(e)
}
}
lateinit var asyncExecutor: ExecutorService
companion object {
@Synchronized
fun getInstance(): FlorisBoard {
return florisboardInstance!!
@@ -176,67 +216,76 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
}
override fun onCreate() {
/*if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog()
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.penaltyDeath()
.build()
)
}*/
flogInfo(LogTopic.IMS_EVENTS)
serviceLifecycleDispatcher.onServicePreSuperOnCreate()
// MUST WRAP all code within Service onCreate() in try..catch to prevent any crash loops
try {
// Additional try..catch wrapper as the event listeners chain or the super.onCreate() method could crash
// and lead to a crash loop
try {
// "Main" try..catch block
flogInfo(LogTopic.IMS_EVENTS)
serviceLifecycleDispatcher.onServicePreSuperOnCreate()
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
audioManager = getSystemService(Context.AUDIO_SERVICE) as? AudioManager
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
prefs = PrefHelper.getDefaultInstance(this)
prefs.initDefaultPreferences()
prefs.sync()
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
audioManager = getSystemService(Context.AUDIO_SERVICE) as? AudioManager
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
prefs.sync()
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
currentThemeIsNight = themeManager.activeTheme.isNightTheme
currentThemeResId = getDayNightBaseThemeId(currentThemeIsNight)
isNumberRowVisible = prefs.keyboard.numberRow
setTheme(currentThemeResId)
themeManager.registerOnThemeUpdatedListener(this)
currentThemeIsNight = themeManager.activeTheme.isNightTheme
currentThemeResId = getDayNightBaseThemeId(currentThemeIsNight)
setTheme(currentThemeResId)
themeManager.registerOnThemeUpdatedListener(this)
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
asyncExecutor = Executors.newSingleThreadExecutor()
florisClipboardManager = FlorisClipboardManager.getInstance().also {
it.initialize(this)
it.addPrimaryClipChangedListener(this)
asyncExecutor = Executors.newSingleThreadExecutor()
florisClipboardManager = FlorisClipboardManager.getInstance().also {
it.initialize(this)
it.addPrimaryClipChangedListener(this)
}
} catch (e: Exception) {
super.onCreate() // MUST CALL even if exception thrown or crash loop is imminent
CrashUtility.stageException(e)
return
}
// Code executed here indicates no crashes occurred, so we execute the onCreate() event as normal
super.onCreate()
eventListeners.toList().forEach { it?.onCreate() }
} catch (e: Exception) {
CrashUtility.stageException(e)
}
super.onCreate()
eventListeners.toList().forEach { it?.onCreate() }
}
@SuppressLint("InflateParams")
override fun onCreateInputView(): View? {
flogInfo(LogTopic.IMS_EVENTS)
CrashUtility.handleStagedButUnhandledExceptions()
updateThemeContext(currentThemeResId)
popupLayerView = PopupLayerView(themeContext)
window?.window?.findViewById<View>(android.R.id.content)?.let { content ->
if (content is ViewGroup) {
content.addView(popupLayerView)
}
}
inputWindowView = LayoutInflater.from(themeContext).inflate(R.layout.florisboard, null) as? InputWindowView
inputWindowView?.isHapticFeedbackEnabled = true
uiBinding = FlorisboardBinding.inflate(LayoutInflater.from(themeContext))
eventListeners.toList().forEach { it?.onCreateInputView() }
eventListeners.toList().forEach { it?.onInitializeInputUi(uiBinding!!) }
return inputWindowView
return uiBinding!!.inputWindowView
}
fun initWindow() {
flogInfo(LogTopic.IMS_EVENTS)
updateSoftInputWindowLayoutParameters()
updateOneHandedPanelVisibility()
themeManager.requestThemeUpdate(this)
dispatchCurrentStateToInputUi()
}
/**
@@ -282,8 +331,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
imeManager = null
vibrator = null
popupLayerView = null
inputView = null
inputWindowView = null
uiBinding = null
florisboardInstance = null
eventListeners.toList().forEach { it?.onDestroy() }
@@ -297,7 +345,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
false
} else {
when (prefs.keyboard.landscapeInputUiMode) {
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeEditorInstance.imeOptions.flagNoFullscreen && !activeEditorInstance.imeOptions.flagNoExtractUi
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeState.imeOptions.flagNoFullscreen && !activeState.imeOptions.flagNoExtractUi
LandscapeInputUiMode.NEVER_SHOW -> false
LandscapeInputUiMode.ALWAYS_SHOW -> true
}
@@ -311,47 +359,41 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
}
override fun onUpdateExtractingVisibility(ei: EditorInfo?) {
isExtractViewShown = !activeEditorInstance.isRawInputEditor && when (prefs.keyboard.landscapeInputUiMode) {
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeEditorInstance.imeOptions.flagNoExtractUi
isExtractViewShown = activeState.isRichInputEditor && when (prefs.keyboard.landscapeInputUiMode) {
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeState.imeOptions.flagNoExtractUi
LandscapeInputUiMode.NEVER_SHOW -> false
LandscapeInputUiMode.ALWAYS_SHOW -> true
}
}
fun registerInputView(inputView: InputView) {
flogInfo(LogTopic.IMS_EVENTS)
window?.window?.findViewById<View>(android.R.id.content)?.let { content ->
if (content is ViewGroup) {
popupLayerView?.let { content.addView(it) }
}
}
this.inputView = inputView
updateSoftInputWindowLayoutParameters()
updateOneHandedPanelVisibility()
themeManager.notifyCallbackReceivers()
setActiveInput(R.id.text_input)
eventListeners.toList().forEach { it?.onRegisterInputView(inputView) }
}
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
flogInfo(LogTopic.IMS_EVENTS)
super.onStartInput(attribute, restarting)
responseState = if (responseState == ResponseState.RECEIVE_RESPONSE) {
ResponseState.START_INPUT
} else {
ResponseState.RESET
}
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
}
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
flogInfo(LogTopic.IMS_EVENTS) { "restarting=$restarting"}
flogInfo(LogTopic.IMS_EVENTS) { "restarting=$restarting" }
flogInfo(LogTopic.IMS_EVENTS) { info?.debugSummarize() ?: "" }
super.onStartInputView(info, restarting)
activeEditorInstance = EditorInstance.from(info, this)
prefs.sync()
if (info != null) {
activeState.update(info)
activeState.isSelectionMode = (info.initialSelEnd - info.initialSelStart) != 0
}
activeEditorInstance = EditorInstance.from(info, this, activeState)
themeManager.updateRemoteColorValues(activeEditorInstance.packageName)
eventListeners.toList().forEach {
it?.onStartInputView(activeEditorInstance, restarting)
}
dispatchCurrentStateToInputUi()
}
override fun onFinishInputView(finishingInput: Boolean) {
@@ -359,10 +401,15 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
if (finishingInput) {
activeEditorInstance = EditorInstance.default()
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
textInputManager.smartbarView?.clearInlineSuggestions()
}
}
super.onFinishInputView(finishingInput)
eventListeners.toList().forEach { it?.onFinishInputView(finishingInput) }
dispatchCurrentStateToInputUi()
}
override fun onFinishInput() {
@@ -372,6 +419,75 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
super.onFinishInput()
}
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest? {
return if (prefs.smartbar.enabled && prefs.suggestion.api30InlineSuggestionsEnabled) {
flogInfo(LogTopic.IMS_EVENTS) {
"Creating inline suggestions request because Smartbar and inline suggestions are enabled."
}
val stylesBundle = themeManager.createInlineSuggestionUiStyleBundle(themeContext)
InlinePresentationSpec.Builder(
Size(
uiBinding?.inputView?.desiredInlineSuggestionsMinWidth ?: 0,
uiBinding?.inputView?.desiredInlineSuggestionsMinHeight ?: 0
),
Size(
uiBinding?.inputView?.desiredInlineSuggestionsMaxWidth ?: 0,
uiBinding?.inputView?.desiredInlineSuggestionsMaxHeight ?: 0
)
).let { spec ->
spec.setStyle(stylesBundle)
InlineSuggestionsRequest.Builder(listOf(spec.build())).let { request ->
request.setMaxSuggestionCount(6)
request.build()
}
}
} else {
flogInfo(LogTopic.IMS_EVENTS) {
"Ignoring inline suggestions request because Smartbar and/or inline suggestions are disabled."
}
null
}
}
@RequiresApi(Build.VERSION_CODES.R)
override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean {
flogInfo(LogTopic.IMS_EVENTS) {
"Received inline suggestions response with ${response.inlineSuggestions.size} suggestion(s) provided."
}
textInputManager.smartbarView?.clearInlineSuggestions()
postPendingResponse(response)
return true
}
@RequiresApi(Build.VERSION_CODES.R)
private fun cancelPendingResponse() {
pendingResponse?.let {
handler.removeCallbacks(it)
pendingResponse = null
}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun postPendingResponse(response: InlineSuggestionsResponse) {
cancelPendingResponse()
val inlineSuggestions = response.inlineSuggestions
responseState = ResponseState.RECEIVE_RESPONSE
pendingResponse = Runnable {
pendingResponse = null
if (responseState == ResponseState.START_INPUT && inlineSuggestions.isEmpty()) {
textInputManager.smartbarView?.clearInlineSuggestions()
} else {
textInputManager.smartbarView?.showInlineSuggestions(inlineSuggestions)
}
responseState = ResponseState.RESET
}.also { handler.post(it) }
}
fun dispatchCurrentStateToInputUi() {
uiBinding?.inputView?.updateKeyboardState(activeState)
}
override fun onWindowShown() {
super.onWindowShown()
if (isWindowShown) {
@@ -383,23 +499,23 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
isWindowShown = true
prefs.sync()
val newIsNumberRowVisible = prefs.keyboard.numberRow
if (isNumberRowVisible != newIsNumberRowVisible) {
textInputManager.keyboards.clear(KeyboardMode.CHARACTERS)
isNumberRowVisible = newIsNumberRowVisible
val newActiveSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
if (newActiveSubtype != activeSubtype) {
activeSubtype = newActiveSubtype
onSubtypeChanged(activeSubtype, true)
} else {
onSubtypeChanged(activeSubtype, false)
}
themeManager.update()
updateOneHandedPanelVisibility()
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
onSubtypeChanged(activeSubtype)
setActiveInput(R.id.text_input)
updateOneHandedPanelVisibility()
themeManager.update()
if (prefs.devtools.enabled && prefs.devtools.showHeapMemoryStats) {
devtoolsOverlaySyncJob?.cancel()
devtoolsOverlaySyncJob = uiScope.launch(Dispatchers.Default) {
while (true) {
if (!isActive) break
withContext(Dispatchers.Main) { inputView?.invalidate() }
withContext(Dispatchers.Main) { uiBinding?.inputView?.invalidate() }
delay(1000)
}
}
@@ -482,8 +598,9 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
candidatesStart, candidatesEnd
)
activeState.isSelectionMode = (newSelEnd - newSelStart) != 0
if (internalBatchNestingLevel == 0) {
flogInfo(LogTopic.IMS_EVENTS) { "$oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd" }
flogInfo(LogTopic.IMS_EVENTS) { "onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)" }
activeEditorInstance.onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
@@ -491,9 +608,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
)
eventListeners.toList().forEach { it?.onUpdateSelection() }
} else {
flogInfo(LogTopic.IMS_EVENTS) {
"$oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd: caught due to internal batch level of $internalBatchNestingLevel!"
}
flogInfo(LogTopic.IMS_EVENTS) { "onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd): caught due to internal batch level of $internalBatchNestingLevel!" }
if (internalSelectionCache.selectionCatchCount++ == 0) {
internalSelectionCache.oldSelStart = oldSelStart
internalSelectionCache.oldSelEnd = oldSelEnd
@@ -503,6 +618,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
internalSelectionCache.candidatesStart = candidatesStart
internalSelectionCache.candidatesEnd = candidatesEnd
}
dispatchCurrentStateToInputUi()
}
override fun onThemeUpdated(theme: Theme) {
@@ -543,8 +659,8 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
w.decorView.systemUiVisibility = flags
// Update InputView theme
inputView?.setBackgroundColor(theme.getAttr(Theme.Attr.KEYBOARD_BACKGROUND).toSolidColor().color)
inputView?.invalidate()
uiBinding?.inputView?.setBackgroundColor(theme.getAttr(Theme.Attr.KEYBOARD_BACKGROUND).toSolidColor().color)
uiBinding?.inputView?.invalidate()
// Update ExtractTextView theme and attributes
extractEditLayout.get()?.let { eel ->
@@ -578,8 +694,8 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
override fun onComputeInsets(outInsets: Insets?) {
super.onComputeInsets(outInsets)
val inputView = this.inputView ?: return
val inputWindowView = this.inputWindowView ?: return
val inputView = uiBinding?.inputView ?: return
val inputWindowView = uiBinding?.inputWindowView ?: return
// TODO: Check also if the keyboard is currently suppressed by a hardware keyboard
if (!isInputViewShown) {
@@ -602,8 +718,8 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
*/
private fun updateSoftInputWindowLayoutParameters() {
val w = window?.window ?: return
ViewLayoutUtils.updateLayoutHeightOf(w, WindowManager.LayoutParams.MATCH_PARENT)
val inputWindowView = this.inputWindowView
ViewUtils.updateLayoutHeightOf(w, WindowManager.LayoutParams.MATCH_PARENT)
val inputWindowView = uiBinding?.inputWindowView
if (inputWindowView != null) {
val layoutHeight = if (isFullscreenMode) {
WindowManager.LayoutParams.WRAP_CONTENT
@@ -611,9 +727,9 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
WindowManager.LayoutParams.MATCH_PARENT
}
val inputArea = w.findViewById<View>(android.R.id.inputArea)
ViewLayoutUtils.updateLayoutHeightOf(inputArea, layoutHeight)
ViewLayoutUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM)
ViewLayoutUtils.updateLayoutHeightOf(inputWindowView, layoutHeight)
ViewUtils.updateLayoutHeightOf(inputArea, layoutHeight)
ViewUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM)
ViewUtils.updateLayoutHeightOf(inputWindowView, layoutHeight)
}
}
@@ -631,9 +747,9 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
val hapticsPerformed = if (vibrationDuration < 0 && vibrationStrength < 0) {
if (isMovingGestureEffect && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
inputWindowView?.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE)
uiBinding?.inputWindowView?.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE)
} else {
inputWindowView?.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
uiBinding?.inputWindowView?.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
} else {
false
@@ -763,35 +879,35 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
fun switchToPrevSubtype() {
flogInfo(LogTopic.IMS_EVENTS)
activeSubtype = subtypeManager.switchToPrevSubtype() ?: Subtype.DEFAULT
onSubtypeChanged(activeSubtype)
onSubtypeChanged(activeSubtype, true)
}
fun switchToNextSubtype() {
flogInfo(LogTopic.IMS_EVENTS)
activeSubtype = subtypeManager.switchToNextSubtype() ?: Subtype.DEFAULT
onSubtypeChanged(activeSubtype)
onSubtypeChanged(activeSubtype, true)
}
private fun onSubtypeChanged(newSubtype: Subtype) {
private fun onSubtypeChanged(newSubtype: Subtype, doRefreshLayouts: Boolean) {
flogInfo(LogTopic.SUBTYPE_MANAGER) { "New subtype: $newSubtype" }
textInputManager.onSubtypeChanged(newSubtype)
mediaInputManager.onSubtypeChanged(newSubtype)
clipInputManager.onSubtypeChanged(newSubtype)
textInputManager.onSubtypeChanged(newSubtype, doRefreshLayouts)
mediaInputManager.onSubtypeChanged(newSubtype, doRefreshLayouts)
clipInputManager.onSubtypeChanged(newSubtype, doRefreshLayouts)
}
fun setActiveInput(type: Int, forceSwitchToCharacters: Boolean = false) {
when (type) {
R.id.text_input -> {
inputView?.mainViewFlipper?.displayedChild = 0
uiBinding?.mainViewFlipper?.displayedChild = 0
if (forceSwitchToCharacters) {
textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(TextKeyData.VIEW_CHARACTERS))
}
}
R.id.media_input -> {
inputView?.mainViewFlipper?.displayedChild = 1
uiBinding?.mainViewFlipper?.displayedChild = 1
}
R.id.clip_input -> {
inputView?.mainViewFlipper?.displayedChild = 2
uiBinding?.mainViewFlipper?.displayedChild = 2
}
}
textInputManager.isGlidePostEffect = false
@@ -807,27 +923,27 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
fun updateOneHandedPanelVisibility() {
if (resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
uiBinding?.oneHandedCtrlPanelStart?.visibility = View.GONE
uiBinding?.oneHandedCtrlPanelEnd?.visibility = View.GONE
} else {
when (prefs.keyboard.oneHandedMode) {
OneHandedMode.OFF -> {
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
uiBinding?.oneHandedCtrlPanelStart?.visibility = View.GONE
uiBinding?.oneHandedCtrlPanelEnd?.visibility = View.GONE
}
OneHandedMode.START -> {
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
inputView?.oneHandedCtrlPanelEnd?.visibility = View.VISIBLE
uiBinding?.oneHandedCtrlPanelStart?.visibility = View.GONE
uiBinding?.oneHandedCtrlPanelEnd?.visibility = View.VISIBLE
}
OneHandedMode.END -> {
inputView?.oneHandedCtrlPanelStart?.visibility = View.VISIBLE
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
uiBinding?.oneHandedCtrlPanelStart?.visibility = View.VISIBLE
uiBinding?.oneHandedCtrlPanelEnd?.visibility = View.GONE
}
}
}
// Delay execution so this function can return, then refresh the whole layout
uiScope.launch {
refreshLayoutOf(inputView)
refreshLayoutOf(uiBinding?.inputView)
}
}
@@ -860,8 +976,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
interface EventListener {
fun onCreate() {}
fun onCreateInputView() {}
fun onRegisterInputView(inputView: InputView) {}
fun onInitializeInputUi(uiBinding: FlorisboardBinding) {}
fun onDestroy() {}
fun onStartInputView(instance: EditorInstance, restarting: Boolean) {}
@@ -874,7 +989,11 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
fun onApplyThemeAttributes() {}
fun onPrimaryClipChanged() {}
fun onSubtypeChanged(newSubtype: Subtype) {}
fun onSubtypeChanged(newSubtype: Subtype, doRefreshLayouts: Boolean) {}
}
private enum class ResponseState {
RESET, RECEIVE_RESPONSE, START_INPUT
}
/**
@@ -895,15 +1014,35 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
data class ImeConfig(
@SerialName("package")
val packageName: String,
@SerialName("composers")
val composers: List<Composer> = listOf(),
@SerialName("currencySets")
val currencySets: List<CurrencySet> = listOf(),
@SerialName("defaultSubtypes")
val defaultSubtypes: List<DefaultSubtype> = listOf()
) {
@Transient var currencySetNames: List<String> = listOf()
@Transient var currencySetLabels: List<String> = listOf()
@Transient var composerNames: List<String> = listOf()
@Transient var composerLabels: List<String> = listOf()
@Transient val composerFromName: Map<String, Composer> = composers.map { it.name to it }.toMap()
@Transient var defaultSubtypesLanguageCodes: List<String> = listOf()
@Transient var defaultSubtypesLanguageNames: List<String> = listOf()
init {
val tmpComposerList = composers.map { Pair(it.name, it.label) }.toMutableList()
// Sort composer list alphabetically by the label of a composer
tmpComposerList.sortBy { it.second }
// Move selected composers to the top of the list
for (composerName in listOf(Appender.name)) {
val index: Int = tmpComposerList.indexOfFirst { it.first == composerName }
if (index > 0) {
tmpComposerList.add(0, tmpComposerList.removeAt(index))
}
}
composerNames = tmpComposerList.map { it.first }.toList()
composerLabels = tmpComposerList.map { it.second }.toList()
val tmpCurrencyList = mutableListOf<Pair<String, String>>()
for (currencySet in currencySets) {
tmpCurrencyList.add(Pair(currencySet.name, currencySet.label))

View File

@@ -17,6 +17,9 @@
package dev.patrickgold.florisboard.ime.core
import android.os.SystemClock
import android.util.SparseArray
import androidx.core.util.forEach
import androidx.core.util.set
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
@@ -36,8 +39,9 @@ class InputEventDispatcher private constructor(
private val repeatableKeyCodes: IntArray
) : InputKeyEventSender {
private val channel: Channel<InputKeyEvent> = Channel(channelCapacity)
private val scope: CoroutineScope = CoroutineScope(defaultDispatcher + SupervisorJob())
private val pressedKeys: HashMap<Int, PressedKeyInfo> = hashMapOf()
private val mainScope: CoroutineScope = CoroutineScope(mainDispatcher + SupervisorJob())
private val defaultScope: CoroutineScope = CoroutineScope(defaultDispatcher + SupervisorJob())
private val pressedKeys: SparseArray<PressedKeyInfo> = SparseArray()
var lastKeyEventDown: InputKeyEvent? = null
private set
var lastKeyEventUp: InputKeyEvent? = null
@@ -53,7 +57,7 @@ class InputEventDispatcher private constructor(
/**
* The default input event channel capacity to be used in [new].
*/
private const val DEFAULT_CHANNEL_CAPACITY: Int = 32
private const val DEFAULT_CHANNEL_CAPACITY: Int = 64
/**
* Creates a new [InputEventDispatcher] instance from given arguments and returns it.
@@ -69,16 +73,26 @@ class InputEventDispatcher private constructor(
*/
fun new(
channelCapacity: Int = DEFAULT_CHANNEL_CAPACITY,
mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
mainDispatcher: CoroutineDispatcher = Dispatchers.Main.immediate,
defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
repeatableKeyCodes: IntArray = intArrayOf()
): InputEventDispatcher = InputEventDispatcher(
channelCapacity, mainDispatcher, defaultDispatcher, repeatableKeyCodes.clone()
)
private fun <T> SparseArray<T>.removeAndReturn(key: Int): T? {
val elem = get(key)
return if (elem == null) {
null
} else {
remove(key)
elem
}
}
}
init {
scope.launch(defaultDispatcher) {
defaultScope.launch {
for (ev in channel) {
if (!isActive) break
val startTime = System.nanoTime()
@@ -87,11 +101,11 @@ class InputEventDispatcher private constructor(
}
when (ev.action) {
InputKeyEvent.Action.DOWN -> {
if (pressedKeys.containsKey(ev.data.code)) continue
if (pressedKeys.indexOfKey(ev.data.code) >= 0) continue
pressedKeys[ev.data.code] = PressedKeyInfo(
eventTimeDown = ev.eventTime,
repeatKeyPressJob = if (!repeatableKeyCodes.contains(ev.data.code)) { null } else {
scope.launch(defaultDispatcher) {
defaultScope.launch {
delay(600)
while (isActive) {
channel.send(InputKeyEvent.repeat(ev.data))
@@ -108,7 +122,7 @@ class InputEventDispatcher private constructor(
}
}
InputKeyEvent.Action.DOWN_UP -> {
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
pressedKeys.removeAndReturn(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyDown(ev)
keyEventReceiver?.onInputKeyUp(ev)
@@ -119,7 +133,7 @@ class InputEventDispatcher private constructor(
}
}
InputKeyEvent.Action.UP -> {
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
pressedKeys.removeAndReturn(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyUp(ev)
}
@@ -128,14 +142,14 @@ class InputEventDispatcher private constructor(
}
}
InputKeyEvent.Action.REPEAT -> {
if (pressedKeys.containsKey(ev.data.code)) {
if (pressedKeys.indexOfKey(ev.data.code) >= 0) {
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyRepeat(ev)
}
}
}
InputKeyEvent.Action.CANCEL -> {
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
pressedKeys.removeAndReturn(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyCancel(ev)
}
@@ -145,16 +159,13 @@ class InputEventDispatcher private constructor(
Timber.d("Time elapsed: ${(System.nanoTime() - startTime) / 1_000_000}")
}
}
val pressedKeysIterator = pressedKeys.iterator()
while (pressedKeysIterator.hasNext()) {
pressedKeysIterator.next().value.repeatKeyPressJob?.cancel()
pressedKeysIterator.remove()
}
pressedKeys.forEach { _, value -> value.repeatKeyPressJob?.cancel() }
pressedKeys.clear()
}
}
override fun send(ev: InputKeyEvent) {
scope.launch(mainDispatcher) {
mainScope.launch {
channel.send(ev)
}
}
@@ -167,7 +178,7 @@ class InputEventDispatcher private constructor(
* @return True if the given [code] is currently down, false otherwise.
*/
fun isPressed(code: Int): Boolean {
return pressedKeys.containsKey(code)
return pressedKeys.indexOfKey(code) >= 0
}
/**
@@ -175,7 +186,8 @@ class InputEventDispatcher private constructor(
*/
fun close() {
keyEventReceiver = null
scope.cancel()
mainScope.cancel()
defaultScope.cancel()
}
data class PressedKeyInfo(

View File

@@ -22,26 +22,24 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.os.Build
import android.text.TextPaint
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.ViewFlipper
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import timber.log.Timber
import dev.patrickgold.florisboard.common.ViewUtils
import kotlin.math.roundToInt
/**
* Root view of the keyboard. Notifies [FlorisBoard] when it has been attached to a window.
* Root view of the keyboard.
*/
class InputView : LinearLayout {
private var florisboard: FlorisBoard = FlorisBoard.getInstance()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val florisboard get() = FlorisBoard.getInstance()
private val prefs get() = Preferences.default()
var desiredInputViewHeight: Float = resources.getDimension(R.dimen.inputView_baseHeight)
private set
@@ -56,11 +54,13 @@ class InputView : LinearLayout {
var shouldGiveAdditionalSpace: Boolean = false
private set
var mainViewFlipper: ViewFlipper? = null
var desiredInlineSuggestionsMinWidth: Int = 0
private set
var oneHandedCtrlPanelStart: ViewGroup? = null
var desiredInlineSuggestionsMinHeight: Int = 0
private set
var oneHandedCtrlPanelEnd: ViewGroup? = null
var desiredInlineSuggestionsMaxWidth: Int = 0
private set
var desiredInlineSuggestionsMaxHeight: Int = 0
private set
private val overlayTextPaint: TextPaint = TextPaint().apply {
@@ -78,18 +78,6 @@ class InputView : LinearLayout {
defStyleAttr
)
override fun onAttachedToWindow() {
Timber.i("onAttachedToWindow()")
super.onAttachedToWindow()
mainViewFlipper = findViewById(R.id.main_view_flipper)
oneHandedCtrlPanelStart = findViewById(R.id.one_handed_ctrl_panel_start)
oneHandedCtrlPanelEnd = findViewById(R.id.one_handed_ctrl_panel_end)
florisboard.registerInputView(this)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
heightFactor = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> 1.0f
@@ -123,7 +111,7 @@ class InputView : LinearLayout {
baseTextInputHeight += additionalHeight
}
val smartbarDisabled = !prefs.smartbar.enabled ||
tim.keyVariation == KeyVariation.PASSWORD && prefs.keyboard.numberRow
tim.activeState.keyVariation == KeyVariation.PASSWORD && prefs.keyboard.numberRow && !prefs.suggestion.api30InlineSuggestionsEnabled
if (smartbarDisabled) {
baseHeight = baseTextInputHeight
baseSmartbarHeight = 0.0f
@@ -134,15 +122,22 @@ class InputView : LinearLayout {
desiredMediaKeyboardViewHeight = baseHeight
// Add bottom offset for curved screens here. As the desired heights have already been set,
// adding a value to the height now will result in a bottom padding (aka offset).
baseHeight += ViewLayoutUtils.convertDpToPixel(
baseHeight += ViewUtils.dp2px(
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
florisboard.prefs.keyboard.bottomOffsetLandscape.toFloat()
prefs.keyboard.bottomOffsetLandscape.toFloat()
} else {
florisboard.prefs.keyboard.bottomOffsetPortrait.toFloat()
},
context
prefs.keyboard.bottomOffsetPortrait.toFloat()
}
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val width = MeasureSpec.getSize(widthMeasureSpec)
desiredInlineSuggestionsMinWidth = width / 3
desiredInlineSuggestionsMinHeight = desiredSmartbarHeight.toInt()
desiredInlineSuggestionsMaxWidth = (width / 1.5).toInt()
desiredInlineSuggestionsMaxHeight = desiredSmartbarHeight.toInt()
}
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(baseHeight.roundToInt(), MeasureSpec.EXACTLY))
}

View File

@@ -21,10 +21,17 @@ import android.util.AttributeSet
import android.widget.FrameLayout
/**
* Root view of the keyboard.
* Root window view of the keyboard.
*/
class InputWindowView : FrameLayout {
private val florisboard get() = FlorisBoard.getInstanceOrNull()
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
florisboard?.initWindow()
}
}

View File

@@ -26,6 +26,7 @@ import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.text.gestures.DistanceThreshold
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.gestures.VelocityThreshold
import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
import dev.patrickgold.florisboard.ime.text.smartbar.CandidateView
@@ -37,7 +38,7 @@ import java.lang.ref.WeakReference
/**
* Helper class for an organized access to the shared preferences.
*/
class PrefHelper(
class Preferences(
context: Context,
val shared: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
) {
@@ -51,6 +52,7 @@ class PrefHelper(
val clipboard = Clipboard(this)
val correction = Correction(this)
val devtools = Devtools(this)
val dictionary = Dictionary(this)
val gestures = Gestures(this)
val glide = Glide(this)
val internal = Internal(this)
@@ -128,15 +130,24 @@ class PrefHelper(
}
companion object {
private val OLD_SUBTYPES_REGEX = """^([\-0-9]+/[\-a-zA-Z0-9]+/[a-zA-Z_]+[;]*)+${'$'}""".toRegex()
private var defaultInstance: PrefHelper? = null
// old settings are id/language/layout and id/language/currencySet/layout
// new settings have composer
private val OLD_SUBTYPES_REGEX = """^([\-0-9]+/[\-a-zA-Z0-9]+(/[a-zA-Z_]+)?/[a-zA-Z_]+[;]*)+${'$'}""".toRegex()
private var defaultInstance: Preferences? = null
@Synchronized
fun getDefaultInstance(context: Context): PrefHelper {
if (defaultInstance == null) {
defaultInstance = PrefHelper(context)
}
return defaultInstance!!
fun initDefault(context: Context): Preferences {
val instance = Preferences(context.applicationContext)
defaultInstance = instance
return instance
}
fun default(): Preferences {
return defaultInstance
?: throw UninitializedPropertyAccessException("""
Default preferences not initialized! Make sure to call initDefault()
before accessing the default preferences.
""".trimIndent())
}
}
@@ -184,7 +195,7 @@ class PrefHelper(
/**
* Wrapper class for advanced preferences.
*/
class Advanced(private val prefHelper: PrefHelper) {
class Advanced(private val prefs: Preferences) {
companion object {
const val SETTINGS_THEME = "advanced__settings_theme"
const val SHOW_APP_ICON = "advanced__show_app_icon"
@@ -192,20 +203,20 @@ class PrefHelper(
}
var settingsTheme: String = ""
get() = prefHelper.getPref(SETTINGS_THEME, "auto")
get() = prefs.getPref(SETTINGS_THEME, "auto")
private set
var showAppIcon: Boolean = false
get() = prefHelper.getPref(SHOW_APP_ICON, true)
get() = prefs.getPref(SHOW_APP_ICON, true)
private set
var forcePrivateMode: Boolean
get() = prefHelper.getPref(FORCE_PRIVATE_MODE, false)
set(v) = prefHelper.setPref(FORCE_PRIVATE_MODE, v)
get() = prefs.getPref(FORCE_PRIVATE_MODE, false)
set(v) = prefs.setPref(FORCE_PRIVATE_MODE, v)
}
/**
* Wrapper class for correction preferences.
*/
class Correction(private val prefHelper: PrefHelper) {
class Correction(private val prefs: Preferences) {
companion object {
const val AUTO_CAPITALIZATION = "correction__auto_capitalization"
const val DOUBLE_SPACE_PERIOD = "correction__double_space_period"
@@ -213,37 +224,57 @@ class PrefHelper(
}
var autoCapitalization: Boolean
get() = prefHelper.getPref(AUTO_CAPITALIZATION, true)
set(v) = prefHelper.setPref(AUTO_CAPITALIZATION, v)
get() = prefs.getPref(AUTO_CAPITALIZATION, true)
set(v) = prefs.setPref(AUTO_CAPITALIZATION, v)
var doubleSpacePeriod: Boolean
get() = prefHelper.getPref(DOUBLE_SPACE_PERIOD, true)
set(v) = prefHelper.setPref(DOUBLE_SPACE_PERIOD, v)
get() = prefs.getPref(DOUBLE_SPACE_PERIOD, true)
set(v) = prefs.setPref(DOUBLE_SPACE_PERIOD, v)
var rememberCapsLockState: Boolean
get() = prefHelper.getPref(REMEMBER_CAPS_LOCK_STATE, false)
set(v) = prefHelper.setPref(REMEMBER_CAPS_LOCK_STATE, v)
get() = prefs.getPref(REMEMBER_CAPS_LOCK_STATE, false)
set(v) = prefs.setPref(REMEMBER_CAPS_LOCK_STATE, v)
}
/**
* Wrapper class for devtools preferences.
*/
class Devtools(private val prefHelper: PrefHelper) {
class Devtools(private val prefs: Preferences) {
companion object {
const val ENABLED = "devtools__enabled"
const val SHOW_HEAP_MEMORY_STATS = "devtools__show_heap_memory_stats"
const val CLEAR_UDM_INTERNAL_DATABASE = "devtools__clear_udm_internal_database"
}
var enabled: Boolean
get() = prefHelper.getPref(ENABLED, false)
set(v) = prefHelper.setPref(ENABLED, v)
get() = prefs.getPref(ENABLED, false)
set(v) = prefs.setPref(ENABLED, v)
var showHeapMemoryStats: Boolean
get() = prefHelper.getPref(SHOW_HEAP_MEMORY_STATS, false)
set(v) = prefHelper.setPref(SHOW_HEAP_MEMORY_STATS, v)
get() = prefs.getPref(SHOW_HEAP_MEMORY_STATS, false)
set(v) = prefs.setPref(SHOW_HEAP_MEMORY_STATS, v)
}
/**
* Wrapper class for dictionary preferences.
*/
class Dictionary(private val prefs: Preferences) {
companion object {
const val ENABLE_SYSTEM_USER_DICTIONARY = "suggestion__enable_system_user_dictionary"
const val MANAGE_SYSTEM_USER_DICTIONARY = "suggestion__manage_system_user_dictionary"
const val ENABLE_FLORIS_USER_DICTIONARY = "suggestion__enable_floris_user_dictionary"
const val MANAGE_FLORIS_USER_DICTIONARY = "suggestion__manage_floris_user_dictionary"
}
var enableSystemUserDictionary: Boolean
get() = prefs.getPref(ENABLE_SYSTEM_USER_DICTIONARY, true)
set(v) = prefs.setPref(ENABLE_SYSTEM_USER_DICTIONARY, v)
var enableFlorisUserDictionary: Boolean
get() = prefs.getPref(ENABLE_FLORIS_USER_DICTIONARY, true)
set(v) = prefs.setPref(ENABLE_FLORIS_USER_DICTIONARY, v)
}
/**
* Wrapper class for gestures preferences.
*/
class Gestures(private val prefHelper: PrefHelper) {
class Gestures(private val prefs: Preferences) {
companion object {
const val SWIPE_UP = "gestures__swipe_up"
const val SWIPE_DOWN = "gestures__swipe_down"
@@ -259,44 +290,44 @@ class PrefHelper(
}
var swipeUp: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_UP, "no_action"))
set(v) = prefHelper.setPref(SWIPE_UP, v)
get() = SwipeAction.fromString(prefs.getPref(SWIPE_UP, "no_action"))
set(v) = prefs.setPref(SWIPE_UP, v)
var swipeDown: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_DOWN, "no_action"))
set(v) = prefHelper.setPref(SWIPE_DOWN, v)
get() = SwipeAction.fromString(prefs.getPref(SWIPE_DOWN, "no_action"))
set(v) = prefs.setPref(SWIPE_DOWN, v)
var swipeLeft: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_LEFT, "no_action"))
set(v) = prefHelper.setPref(SWIPE_LEFT, v)
get() = SwipeAction.fromString(prefs.getPref(SWIPE_LEFT, "no_action"))
set(v) = prefs.setPref(SWIPE_LEFT, v)
var swipeRight: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_RIGHT, "no_action"))
set(v) = prefHelper.setPref(SWIPE_RIGHT, v)
get() = SwipeAction.fromString(prefs.getPref(SWIPE_RIGHT, "no_action"))
set(v) = prefs.setPref(SWIPE_RIGHT, v)
var spaceBarLongPress: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_LONG_PRESS, "no_action"))
set(v) = prefHelper.setPref(SPACE_BAR_LONG_PRESS, v)
get() = SwipeAction.fromString(prefs.getPref(SPACE_BAR_LONG_PRESS, "no_action"))
set(v) = prefs.setPref(SPACE_BAR_LONG_PRESS, v)
var spaceBarSwipeUp: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_SWIPE_UP, "no_action"))
set(v) = prefHelper.setPref(SPACE_BAR_SWIPE_UP, v)
get() = SwipeAction.fromString(prefs.getPref(SPACE_BAR_SWIPE_UP, "no_action"))
set(v) = prefs.setPref(SPACE_BAR_SWIPE_UP, v)
var spaceBarSwipeLeft: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_SWIPE_LEFT, "no_action"))
set(v) = prefHelper.setPref(SPACE_BAR_SWIPE_LEFT, v)
get() = SwipeAction.fromString(prefs.getPref(SPACE_BAR_SWIPE_LEFT, "no_action"))
set(v) = prefs.setPref(SPACE_BAR_SWIPE_LEFT, v)
var spaceBarSwipeRight: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_SWIPE_RIGHT, "no_action"))
set(v) = prefHelper.setPref(SPACE_BAR_SWIPE_RIGHT, v)
get() = SwipeAction.fromString(prefs.getPref(SPACE_BAR_SWIPE_RIGHT, "no_action"))
set(v) = prefs.setPref(SPACE_BAR_SWIPE_RIGHT, v)
var deleteKeySwipeLeft: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(DELETE_KEY_SWIPE_LEFT, "no_action"))
set(v) = prefHelper.setPref(DELETE_KEY_SWIPE_LEFT, v)
get() = SwipeAction.fromString(prefs.getPref(DELETE_KEY_SWIPE_LEFT, "no_action"))
set(v) = prefs.setPref(DELETE_KEY_SWIPE_LEFT, v)
var swipeVelocityThreshold: VelocityThreshold
get() = VelocityThreshold.fromString(prefHelper.getPref(SWIPE_VELOCITY_THRESHOLD, "normal"))
set(v) = prefHelper.setPref(SWIPE_VELOCITY_THRESHOLD, v)
get() = VelocityThreshold.fromString(prefs.getPref(SWIPE_VELOCITY_THRESHOLD, "normal"))
set(v) = prefs.setPref(SWIPE_VELOCITY_THRESHOLD, v)
var swipeDistanceThreshold: DistanceThreshold
get() = DistanceThreshold.fromString(prefHelper.getPref(SWIPE_DISTANCE_THRESHOLD, "normal"))
set(v) = prefHelper.setPref(SWIPE_DISTANCE_THRESHOLD, v)
get() = DistanceThreshold.fromString(prefs.getPref(SWIPE_DISTANCE_THRESHOLD, "normal"))
set(v) = prefs.setPref(SWIPE_DISTANCE_THRESHOLD, v)
}
/**
* Wrapper class for glide preferences.
*/
class Glide(private val prefHelper: PrefHelper) {
class Glide(private val prefs: Preferences) {
companion object {
const val ENABLED = "glide__enabled"
const val SHOW_TRAIL = "glide__show_trail"
@@ -307,30 +338,30 @@ class PrefHelper(
}
var enabled: Boolean
get() = prefHelper.getPref(ENABLED, false)
set(v) = prefHelper.setPref(ENABLED, v)
get() = prefs.getPref(ENABLED, false)
set(v) = prefs.setPref(ENABLED, v)
var showTrail: Boolean
get() = prefHelper.getPref(SHOW_TRAIL, false)
set(v) = prefHelper.setPref(SHOW_TRAIL, v)
get() = prefs.getPref(SHOW_TRAIL, false)
set(v) = prefs.setPref(SHOW_TRAIL, v)
var trailDuration: Int
get() = prefHelper.getPref(TRAIL_DURATION, 200)
set(v) = prefHelper.setPref(TRAIL_DURATION, v)
get() = prefs.getPref(TRAIL_DURATION, 200)
set(v) = prefs.setPref(TRAIL_DURATION, v)
var showPreview: Boolean
get() = prefHelper.getPref(SHOW_PREVIEW, true)
set(v) = prefHelper.setPref(SHOW_PREVIEW, v)
get() = prefs.getPref(SHOW_PREVIEW, true)
set(v) = prefs.setPref(SHOW_PREVIEW, v)
var previewRefreshDelay: Int
get() = prefHelper.getPref(PREVIEW_REFRESH_DELAY, 150)
set(v) = prefHelper.setPref(PREVIEW_REFRESH_DELAY, v)
get() = prefs.getPref(PREVIEW_REFRESH_DELAY, 150)
set(v) = prefs.setPref(PREVIEW_REFRESH_DELAY, v)
var trailMaxLength: Int
get() = prefHelper.getPref(MAX_TRAIL_LENGTH, 150)
set(v) = prefHelper.setPref(MAX_TRAIL_LENGTH, v)
get() = prefs.getPref(MAX_TRAIL_LENGTH, 150)
set(v) = prefs.setPref(MAX_TRAIL_LENGTH, v)
}
/**
* Wrapper class for internal preferences. A preference qualifies as an internal pref if the
* user has no ability to control this preference's value directly (via a UI pref view).
*/
class Internal(private val prefHelper: PrefHelper) {
class Internal(private val prefs: Preferences) {
companion object {
const val IS_IME_SET_UP = "internal__is_ime_set_up"
const val VERSION_ON_INSTALL = "internal__version_on_install"
@@ -339,23 +370,23 @@ class PrefHelper(
}
var isImeSetUp: Boolean
get() = prefHelper.getPref(IS_IME_SET_UP, false)
set(v) = prefHelper.setPref(IS_IME_SET_UP, v)
get() = prefs.getPref(IS_IME_SET_UP, false)
set(v) = prefs.setPref(IS_IME_SET_UP, v)
var versionOnInstall: String
get() = prefHelper.getPref(VERSION_ON_INSTALL, VersionName.DEFAULT_RAW)
set(v) = prefHelper.setPref(VERSION_ON_INSTALL, v)
get() = prefs.getPref(VERSION_ON_INSTALL, VersionName.DEFAULT_RAW)
set(v) = prefs.setPref(VERSION_ON_INSTALL, v)
var versionLastUse: String
get() = prefHelper.getPref(VERSION_LAST_USE, VersionName.DEFAULT_RAW)
set(v) = prefHelper.setPref(VERSION_LAST_USE, v)
get() = prefs.getPref(VERSION_LAST_USE, VersionName.DEFAULT_RAW)
set(v) = prefs.setPref(VERSION_LAST_USE, v)
var versionLastChangelog: String
get() = prefHelper.getPref(VERSION_LAST_CHANGELOG, VersionName.DEFAULT_RAW)
set(v) = prefHelper.setPref(VERSION_LAST_CHANGELOG, v)
get() = prefs.getPref(VERSION_LAST_CHANGELOG, VersionName.DEFAULT_RAW)
set(v) = prefs.setPref(VERSION_LAST_CHANGELOG, v)
}
/**
* Wrapper class for keyboard preferences.
*/
class Keyboard(private val prefHelper: PrefHelper) {
class Keyboard(private val prefs: Preferences) {
companion object {
const val BOTTOM_OFFSET_PORTRAIT = "keyboard__bottom_offset_portrait"
const val BOTTOM_OFFSET_LANDSCAPE = "keyboard__bottom_offset_landscape"
@@ -369,6 +400,7 @@ class PrefHelper(
const val KEY_SPACING_VERTICAL = "keyboard__key_spacing_vertical"
const val LANDSCAPE_INPUT_UI_MODE = "keyboard__landscape_input_ui_mode"
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
const val MERGE_HINT_POPUPS_ENABLED = "keyboard__merge_hint_popups_enabled"
const val NUMBER_ROW = "keyboard__number_row"
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
const val ONE_HANDED_MODE_SCALE_FACTOR = "keyboard__one_handed_mode_scale_factor"
@@ -384,148 +416,159 @@ class PrefHelper(
}
var bottomOffsetPortrait: Int = 0
get() = prefHelper.getPref(BOTTOM_OFFSET_PORTRAIT, 0)
get() = prefs.getPref(BOTTOM_OFFSET_PORTRAIT, 0)
private set
var bottomOffsetLandscape: Int = 0
get() = prefHelper.getPref(BOTTOM_OFFSET_LANDSCAPE, 0)
get() = prefs.getPref(BOTTOM_OFFSET_LANDSCAPE, 0)
private set
var fontSizeMultiplierPortrait: Int
get() = prefHelper.getPref(FONT_SIZE_MULTIPLIER_PORTRAIT, 100)
set(v) = prefHelper.setPref(FONT_SIZE_MULTIPLIER_PORTRAIT, v)
get() = prefs.getPref(FONT_SIZE_MULTIPLIER_PORTRAIT, 100)
set(v) = prefs.setPref(FONT_SIZE_MULTIPLIER_PORTRAIT, v)
var fontSizeMultiplierLandscape: Int
get() = prefHelper.getPref(FONT_SIZE_MULTIPLIER_LANDSCAPE, 100)
set(v) = prefHelper.setPref(FONT_SIZE_MULTIPLIER_LANDSCAPE, v)
get() = prefs.getPref(FONT_SIZE_MULTIPLIER_LANDSCAPE, 100)
set(v) = prefs.setPref(FONT_SIZE_MULTIPLIER_LANDSCAPE, v)
var heightFactor: String = ""
get() = prefHelper.getPref(HEIGHT_FACTOR, "normal")
get() = prefs.getPref(HEIGHT_FACTOR, "normal")
private set
var heightFactorCustom: Int
get() = prefHelper.getPref(HEIGHT_FACTOR_CUSTOM, 100)
set(v) = prefHelper.setPref(HEIGHT_FACTOR_CUSTOM, v)
get() = prefs.getPref(HEIGHT_FACTOR_CUSTOM, 100)
set(v) = prefs.setPref(HEIGHT_FACTOR_CUSTOM, v)
var hintedNumberRowMode: KeyHintMode
get() = KeyHintMode.fromString(prefHelper.getPref(HINTED_NUMBER_ROW_MODE, KeyHintMode.ENABLED_ACCENT_PRIORITY.toString()))
set(v) = prefHelper.setPref(HINTED_NUMBER_ROW_MODE, v)
get() = KeyHintMode.fromString(prefs.getPref(HINTED_NUMBER_ROW_MODE, KeyHintMode.ENABLED_ACCENT_PRIORITY.toString()))
set(v) = prefs.setPref(HINTED_NUMBER_ROW_MODE, v)
var hintedSymbolsMode: KeyHintMode
get() = KeyHintMode.fromString(prefHelper.getPref(HINTED_SYMBOLS_MODE, KeyHintMode.ENABLED_ACCENT_PRIORITY.toString()))
set(v) = prefHelper.setPref(HINTED_SYMBOLS_MODE, v)
get() = KeyHintMode.fromString(prefs.getPref(HINTED_SYMBOLS_MODE, KeyHintMode.ENABLED_ACCENT_PRIORITY.toString()))
set(v) = prefs.setPref(HINTED_SYMBOLS_MODE, v)
var keySpacingHorizontal: Float = 2f
get() = prefHelper.getPref(KEY_SPACING_HORIZONTAL, 4) / 2f
get() = prefs.getPref(KEY_SPACING_HORIZONTAL, 4) / 2f
private set
var keySpacingVertical: Float = 5f
get() = prefHelper.getPref(KEY_SPACING_VERTICAL, 10) / 2f
get() = prefs.getPref(KEY_SPACING_VERTICAL, 10) / 2f
private set
var landscapeInputUiMode: LandscapeInputUiMode
get() = LandscapeInputUiMode.fromString(prefHelper.getPref(LANDSCAPE_INPUT_UI_MODE, LandscapeInputUiMode.DYNAMICALLY_SHOW.toString()))
set(v) = prefHelper.setPref(LANDSCAPE_INPUT_UI_MODE, v)
get() = LandscapeInputUiMode.fromString(prefs.getPref(LANDSCAPE_INPUT_UI_MODE, LandscapeInputUiMode.DYNAMICALLY_SHOW.toString()))
set(v) = prefs.setPref(LANDSCAPE_INPUT_UI_MODE, v)
var longPressDelay: Int = 0
get() = prefHelper.getPref(LONG_PRESS_DELAY, 300)
get() = prefs.getPref(LONG_PRESS_DELAY, 300)
private set
var mergeHintPopupsEnabled: Boolean
get() = prefs.getPref(MERGE_HINT_POPUPS_ENABLED, false)
set(v) = prefs.setPref(MERGE_HINT_POPUPS_ENABLED, v)
var numberRow: Boolean
get() = prefHelper.getPref(NUMBER_ROW, false)
set(v) = prefHelper.setPref(NUMBER_ROW, v)
get() = prefs.getPref(NUMBER_ROW, false)
set(v) = prefs.setPref(NUMBER_ROW, v)
var oneHandedMode: String
get() = prefHelper.getPref(ONE_HANDED_MODE, OneHandedMode.OFF)
set(value) = prefHelper.setPref(ONE_HANDED_MODE, value)
get() = prefs.getPref(ONE_HANDED_MODE, OneHandedMode.OFF)
set(value) = prefs.setPref(ONE_HANDED_MODE, value)
var oneHandedModeScaleFactor: Int
get() = prefHelper.getPref(ONE_HANDED_MODE_SCALE_FACTOR, 87)
set(v) = prefHelper.setPref(ONE_HANDED_MODE_SCALE_FACTOR, v)
get() = prefs.getPref(ONE_HANDED_MODE_SCALE_FACTOR, 87)
set(v) = prefs.setPref(ONE_HANDED_MODE_SCALE_FACTOR, v)
var popupEnabled: Boolean = false
get() = prefHelper.getPref(POPUP_ENABLED, true)
get() = prefs.getPref(POPUP_ENABLED, true)
private set
var soundEnabled: Boolean = false
get() = prefHelper.getPref(SOUND_ENABLED, true)
get() = prefs.getPref(SOUND_ENABLED, true)
private set
var soundEnabledSystem: Boolean = false
var soundVolume: Int = 0
get() = prefHelper.getPref(SOUND_VOLUME, -1)
get() = prefs.getPref(SOUND_VOLUME, -1)
private set
var spaceBarSwitchesToCharacters: Boolean
get() = prefHelper.getPref(SPACE_BAR_SWITCHES_TO_CHARACTERS, true)
set(v) = prefHelper.setPref(SPACE_BAR_SWITCHES_TO_CHARACTERS, v)
get() = prefs.getPref(SPACE_BAR_SWITCHES_TO_CHARACTERS, true)
set(v) = prefs.setPref(SPACE_BAR_SWITCHES_TO_CHARACTERS, v)
var utilityKeyAction: UtilityKeyAction
get() = UtilityKeyAction.fromString(prefHelper.getPref(UTILITY_KEY_ACTION, UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS.toString()))
set(v) = prefHelper.setPref(UTILITY_KEY_ACTION, v)
get() = UtilityKeyAction.fromString(prefs.getPref(UTILITY_KEY_ACTION, UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS.toString()))
set(v) = prefs.setPref(UTILITY_KEY_ACTION, v)
var utilityKeyEnabled: Boolean
get() = prefHelper.getPref(UTILITY_KEY_ENABLED, true)
set(v) = prefHelper.setPref(UTILITY_KEY_ENABLED, v)
get() = prefs.getPref(UTILITY_KEY_ENABLED, true)
set(v) = prefs.setPref(UTILITY_KEY_ENABLED, v)
var vibrationEnabled: Boolean = false
get() = prefHelper.getPref(VIBRATION_ENABLED, true)
get() = prefs.getPref(VIBRATION_ENABLED, true)
private set
var vibrationEnabledSystem: Boolean = false
var vibrationDuration: Int = 0
get() = prefHelper.getPref(VIBRATION_DURATION, -1)
get() = prefs.getPref(VIBRATION_DURATION, -1)
private set
var vibrationStrength: Int = 0
get() = prefHelper.getPref(VIBRATION_STRENGTH, -1)
get() = prefs.getPref(VIBRATION_STRENGTH, -1)
private set
fun keyHintConfiguration(): KeyHintConfiguration {
return KeyHintConfiguration(hintedSymbolsMode, hintedNumberRowMode, mergeHintPopupsEnabled)
}
}
/**
* Wrapper class for localization preferences.
*/
class Localization(private val prefHelper: PrefHelper) {
class Localization(private val prefs: Preferences) {
companion object {
const val ACTIVE_SUBTYPE_ID = "localization__active_subtype_id"
const val SUBTYPES = "localization__subtypes"
}
var activeSubtypeId: Int
get() = prefHelper.getPref(ACTIVE_SUBTYPE_ID, Subtype.DEFAULT.id)
set(v) = prefHelper.setPref(ACTIVE_SUBTYPE_ID, v)
get() = prefs.getPref(ACTIVE_SUBTYPE_ID, Subtype.DEFAULT.id)
set(v) = prefs.setPref(ACTIVE_SUBTYPE_ID, v)
var subtypes: String
get() = prefHelper.getPref(SUBTYPES, "")
set(v) = prefHelper.setPref(SUBTYPES, v)
get() = prefs.getPref(SUBTYPES, "")
set(v) = prefs.setPref(SUBTYPES, v)
}
/**
* Wrapper class for Smartbar preferences.
*/
class Smartbar(private val prefHelper: PrefHelper) {
class Smartbar(private val prefs: Preferences) {
companion object {
const val ENABLED = "smartbar__enabled"
}
var enabled: Boolean
get() = prefHelper.getPref(ENABLED, true)
set(v) = prefHelper.setPref(ENABLED, v)
get() = prefs.getPref(ENABLED, true)
set(v) = prefs.setPref(ENABLED, v)
}
/**
* Wrapper class for suggestion preferences.
*/
class Suggestion(private val prefHelper: PrefHelper) {
class Suggestion(private val prefs: Preferences) {
companion object {
const val BLOCK_POSSIBLY_OFFENSIVE = "suggestion__block_possibly_offensive"
const val CLIPBOARD_CONTENT_ENABLED = "suggestion__clipboard_content_enabled"
const val CLIPBOARD_CONTENT_TIMEOUT = "suggestion__clipboard_content_timeout"
const val DISPLAY_MODE = "suggestion__display_mode"
const val ENABLED = "suggestion__enabled"
const val USE_PREV_WORDS = "suggestion__use_prev_words"
const val API30_INLINE_SUGGESTIONS_ENABLED = "suggestion__api30_inline_suggestions_enabled"
const val BLOCK_POSSIBLY_OFFENSIVE = "suggestion__block_possibly_offensive"
const val CLIPBOARD_CONTENT_ENABLED = "suggestion__clipboard_content_enabled"
const val CLIPBOARD_CONTENT_TIMEOUT = "suggestion__clipboard_content_timeout"
const val DISPLAY_MODE = "suggestion__display_mode"
const val ENABLED = "suggestion__enabled"
const val USE_PREV_WORDS = "suggestion__use_prev_words"
}
var api30InlineSuggestionsEnabled: Boolean
get() = prefs.getPref(API30_INLINE_SUGGESTIONS_ENABLED, true)
set(v) = prefs.setPref(API30_INLINE_SUGGESTIONS_ENABLED, v)
var blockPossiblyOffensive: Boolean
get() = prefHelper.getPref(BLOCK_POSSIBLY_OFFENSIVE, true)
set(v) = prefHelper.setPref(BLOCK_POSSIBLY_OFFENSIVE, v)
get() = prefs.getPref(BLOCK_POSSIBLY_OFFENSIVE, true)
set(v) = prefs.setPref(BLOCK_POSSIBLY_OFFENSIVE, v)
var clipboardContentEnabled: Boolean
get() = prefHelper.getPref(CLIPBOARD_CONTENT_ENABLED, false)
set(v) = prefHelper.setPref(CLIPBOARD_CONTENT_ENABLED, v)
get() = prefs.getPref(CLIPBOARD_CONTENT_ENABLED, false)
set(v) = prefs.setPref(CLIPBOARD_CONTENT_ENABLED, v)
var clipboardContentTimeout: Int
get() = prefHelper.getPref(CLIPBOARD_CONTENT_TIMEOUT, 30)
set(v) = prefHelper.setPref(CLIPBOARD_CONTENT_TIMEOUT, v)
get() = prefs.getPref(CLIPBOARD_CONTENT_TIMEOUT, 30)
set(v) = prefs.setPref(CLIPBOARD_CONTENT_TIMEOUT, v)
var displayMode: CandidateView.DisplayMode
get() = CandidateView.DisplayMode.fromString(prefHelper.getPref(DISPLAY_MODE, CandidateView.DisplayMode.DYNAMIC_SCROLLABLE.toString()))
set(v) = prefHelper.setPref(DISPLAY_MODE, v)
get() = CandidateView.DisplayMode.fromString(prefs.getPref(DISPLAY_MODE, CandidateView.DisplayMode.DYNAMIC_SCROLLABLE.toString()))
set(v) = prefs.setPref(DISPLAY_MODE, v)
var enabled: Boolean
get() = prefHelper.getPref(ENABLED, true)
set(v) = prefHelper.setPref(ENABLED, v)
get() = prefs.getPref(ENABLED, true)
set(v) = prefs.setPref(ENABLED, v)
var usePrevWords: Boolean
get() = prefHelper.getPref(USE_PREV_WORDS, true)
set(v) = prefHelper.setPref(USE_PREV_WORDS, v)
get() = prefs.getPref(USE_PREV_WORDS, true)
set(v) = prefs.setPref(USE_PREV_WORDS, v)
}
/**
* Wrapper class for theme preferences.
*/
class Theme(private val prefHelper: PrefHelper) {
class Theme(private val prefs: Preferences) {
companion object {
const val MODE = "theme__mode"
const val DAY_THEME_REF = "theme__day_theme_ref"
@@ -537,32 +580,32 @@ class PrefHelper(
}
var mode: ThemeMode
get() = ThemeMode.fromString(prefHelper.getPref(MODE, ThemeMode.FOLLOW_SYSTEM.toString()))
set(v) = prefHelper.setPref(MODE, v)
get() = ThemeMode.fromString(prefs.getPref(MODE, ThemeMode.FOLLOW_SYSTEM.toString()))
set(v) = prefs.setPref(MODE, v)
var dayThemeRef: String
get() = prefHelper.getPref(DAY_THEME_REF, "assets:ime/theme/floris_day.json")
set(v) = prefHelper.setPref(DAY_THEME_REF, v)
get() = prefs.getPref(DAY_THEME_REF, "assets:ime/theme/floris_day.json")
set(v) = prefs.setPref(DAY_THEME_REF, v)
var dayThemeAdaptToApp: Boolean
get() = prefHelper.getPref(DAY_THEME_ADAPT_TO_APP, false)
set(v) = prefHelper.setPref(DAY_THEME_ADAPT_TO_APP, v)
get() = prefs.getPref(DAY_THEME_ADAPT_TO_APP, false)
set(v) = prefs.setPref(DAY_THEME_ADAPT_TO_APP, v)
var nightThemeRef: String
get() = prefHelper.getPref(NIGHT_THEME_REF, "assets:ime/theme/floris_night.json")
set(v) = prefHelper.setPref(NIGHT_THEME_REF, v)
get() = prefs.getPref(NIGHT_THEME_REF, "assets:ime/theme/floris_night.json")
set(v) = prefs.setPref(NIGHT_THEME_REF, v)
var nightThemeAdaptToApp: Boolean
get() = prefHelper.getPref(NIGHT_THEME_ADAPT_TO_APP, false)
set(v) = prefHelper.setPref(NIGHT_THEME_ADAPT_TO_APP, v)
get() = prefs.getPref(NIGHT_THEME_ADAPT_TO_APP, false)
set(v) = prefs.setPref(NIGHT_THEME_ADAPT_TO_APP, v)
var sunriseTime: Int
get() = prefHelper.getPref(SUNRISE_TIME, TimeUtil.encode(6, 0))
set(v) = prefHelper.setPref(SUNRISE_TIME, v)
get() = prefs.getPref(SUNRISE_TIME, TimeUtil.encode(6, 0))
set(v) = prefs.setPref(SUNRISE_TIME, v)
var sunsetTime: Int
get() = prefHelper.getPref(SUNSET_TIME, TimeUtil.encode(18, 0))
set(v) = prefHelper.setPref(SUNSET_TIME, v)
get() = prefs.getPref(SUNSET_TIME, TimeUtil.encode(18, 0))
set(v) = prefs.setPref(SUNSET_TIME, v)
}
/**
* Wrapper class for clipboard preferences
*/
class Clipboard(private val prefHelper: PrefHelper) {
class Clipboard(private val prefs: Preferences) {
companion object {
const val ENABLE_INTERNAL = "clipboard__enable_internal"
const val SYNC_TO_SYSTEM = "clipboard__sync_to_system"
@@ -575,35 +618,35 @@ class PrefHelper(
}
var enableInternal: Boolean
get() = prefHelper.getPref(ENABLE_INTERNAL, false)
set(v) = prefHelper.setPref(ENABLE_INTERNAL, v)
get() = prefs.getPref(ENABLE_INTERNAL, false)
set(v) = prefs.setPref(ENABLE_INTERNAL, v)
var syncToSystem: Boolean
get() = prefHelper.getPref(SYNC_TO_SYSTEM, false)
set(v) = prefHelper.setPref(SYNC_TO_SYSTEM, v)
get() = prefs.getPref(SYNC_TO_SYSTEM, false)
set(v) = prefs.setPref(SYNC_TO_SYSTEM, v)
var syncToFloris: Boolean
get() = prefHelper.getPref(SYNC_TO_FLORIS, true)
set(v) = prefHelper.setPref(SYNC_TO_FLORIS, v)
get() = prefs.getPref(SYNC_TO_FLORIS, true)
set(v) = prefs.setPref(SYNC_TO_FLORIS, v)
var enableHistory: Boolean
get() = prefHelper.getPref(ENABLE_HISTORY, false)
set(v) = prefHelper.setPref(ENABLE_HISTORY, v)
get() = prefs.getPref(ENABLE_HISTORY, false)
set(v) = prefs.setPref(ENABLE_HISTORY, v)
var cleanUpOld: Boolean
get() = prefHelper.getPref(CLEAN_UP_OLD, false)
set(v) = prefHelper.setPref(CLEAN_UP_OLD, v)
get() = prefs.getPref(CLEAN_UP_OLD, false)
set(v) = prefs.setPref(CLEAN_UP_OLD, v)
var limitHistorySize: Boolean
get() = prefHelper.getPref(LIMIT_HISTORY_SIZE, true)
set(v) = prefHelper.setPref(LIMIT_HISTORY_SIZE, v)
get() = prefs.getPref(LIMIT_HISTORY_SIZE, true)
set(v) = prefs.setPref(LIMIT_HISTORY_SIZE, v)
var cleanUpAfter: Int
get() = prefHelper.getPref(CLEAN_UP_AFTER, 20)
set(v) = prefHelper.setPref(CLEAN_UP_AFTER, v)
get() = prefs.getPref(CLEAN_UP_AFTER, 20)
set(v) = prefs.setPref(CLEAN_UP_AFTER, v)
var maxHistorySize: Int
get() = prefHelper.getPref(MAX_HISTORY_SIZE, 20)
set(v) = prefHelper.setPref(MAX_HISTORY_SIZE, v)
get() = prefs.getPref(MAX_HISTORY_SIZE, 20)
set(v) = prefs.setPref(MAX_HISTORY_SIZE, v)
}
}

View File

@@ -16,6 +16,8 @@
package dev.patrickgold.florisboard.ime.core
import dev.patrickgold.florisboard.ime.text.composing.Appender
import dev.patrickgold.florisboard.ime.text.composing.Composer
import dev.patrickgold.florisboard.ime.text.layout.LayoutType
import dev.patrickgold.florisboard.util.LocaleUtils
import kotlinx.serialization.*
@@ -32,12 +34,14 @@ import java.util.*
* @property id The ID of this subtype. Although this can be any numeric value, its value
* typically matches the one of the [DefaultSubtype] with the same locale.
* @property locale The locale this subtype is bound to.
* @property composerName The composer name to composer characters the way they should.
* @property currencySetName The currency set name to display the correct currency symbols for this subtype.
* @property layoutMap The layout map to properly display the correct layout for each layout type.
*/
data class Subtype(
val id: Int,
val locale: Locale,
val composerName: String,
val currencySetName: String,
val layoutMap: SubtypeLayoutMap,
) {
@@ -50,6 +54,7 @@ data class Subtype(
val DEFAULT = Subtype(
id = -1,
locale = Locale.ENGLISH,
composerName = Appender.name,
currencySetName = "\$default",
layoutMap = SubtypeLayoutMap(characters = "qwerty")
)
@@ -67,17 +72,29 @@ data class Subtype(
*/
fun fromString(str: String): Subtype {
val data = str.split("/")
if (data.size != 4) {
throw InvalidPropertiesFormatException(
"Given string contains more or less than 4 properties..."
)
} else {
val locale = LocaleUtils.stringToLocale(data[1])
return Subtype(
data[0].toInt(),
locale,
data[2],
SubtypeLayoutMap.fromString(data[3])
when (data.size) {
4 -> {
val locale = LocaleUtils.stringToLocale(data[1])
return Subtype(
data[0].toInt(),
locale,
Appender.name,
data[2],
SubtypeLayoutMap.fromString(data[3])
)
}
5 -> {
val locale = LocaleUtils.stringToLocale(data[1])
return Subtype(
data[0].toInt(),
locale,
data[2],
data[3],
SubtypeLayoutMap.fromString(data[4])
)
}
else -> throw InvalidPropertiesFormatException(
"Given string contains more or less than 5 properties..."
)
}
}
@@ -86,6 +103,7 @@ data class Subtype(
init {
var result = id
result = 31 * result + locale.hashCode()
result = 31 * result + composerName.hashCode()
result = 31 * result + currencySetName.hashCode()
result = 31 * result + layoutMap.hashCode()
_hashCode = result
@@ -93,11 +111,11 @@ data class Subtype(
/**
* Converts this object into its string representation. Format:
* <id>/<language_tag>/<currency_set_name>/<layout_map>
* <id>/<language_tag>/<composer_name>/<currency_set_name>/<layout_map>
*/
override fun toString(): String {
val languageTag = locale.toLanguageTag()
return "$id/$languageTag/$currencySetName/$layoutMap"
return "$id/$languageTag/$composerName/$currencySetName/$layoutMap"
}
/**
@@ -117,6 +135,7 @@ data class Subtype(
if (id != other.id) return false
if (locale != other.locale) return false
if (composerName != other.composerName) return false
if (currencySetName != other.currencySetName) return false
if (layoutMap != other.layoutMap) return false
@@ -310,6 +329,8 @@ data class DefaultSubtype(
@Serializable(with = LocaleSerializer::class)
@SerialName("languageTag")
var locale: Locale,
@SerialName("composer")
var composerName: String,
@SerialName("currencySet")
var currencySetName: String,
var preferred: SubtypeLayoutMap

View File

@@ -37,10 +37,10 @@ import kotlin.collections.ArrayList
* @property subtypes The currently active subtypes.
*/
class SubtypeManager(
private val packageName: String,
private val prefs: PrefHelper
private val packageName: String
) : CoroutineScope by MainScope() {
private val assetManager get() = AssetManager.default()
private val prefs get() = Preferences.default()
companion object {
const val IME_CONFIG_FILE_PATH = "ime/config.json"
@@ -48,8 +48,8 @@ class SubtypeManager(
private var instance: SubtypeManager? = null
fun init(context: Context, prefs: PrefHelper): SubtypeManager {
val defaultInstance = SubtypeManager(context.packageName, prefs)
fun init(context: Context): SubtypeManager {
val defaultInstance = SubtypeManager(context.packageName)
instance = defaultInstance
return defaultInstance
}
@@ -112,16 +112,18 @@ class SubtypeManager(
* list, if it does not exist.
*
* @param locale The locale of the subtype to be added.
* @param composerName The composer name of the subtype to be added.
* @param currencySetName The currency set name of the subtype to be added.
* @param layoutMap The layout map of the subtype to be added.
* @return True if the subtype was added, false otherwise. A return value of false indicates
* that the subtype already exists.
*/
fun addSubtype(locale: Locale, currencySetName: String, layoutMap: SubtypeLayoutMap): Boolean {
fun addSubtype(locale: Locale, composerName: String, currencySetName: String, layoutMap: SubtypeLayoutMap): Boolean {
return addSubtype(
Subtype(
(locale.hashCode() + 31 * layoutMap.hashCode() + 31 * currencySetName.hashCode()),
locale,
composerName,
currencySetName,
layoutMap
)

View File

@@ -17,40 +17,35 @@
package dev.patrickgold.florisboard.ime.dictionary
import dev.patrickgold.florisboard.ime.extension.Asset
import dev.patrickgold.florisboard.ime.nlp.LanguageModel
import dev.patrickgold.florisboard.ime.nlp.MutableLanguageModel
import dev.patrickgold.florisboard.ime.nlp.Token
import dev.patrickgold.florisboard.ime.nlp.WeightedToken
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
import dev.patrickgold.florisboard.ime.nlp.Word
/**
* Standardized dictionary interface for interacting with dictionaries.
*/
interface Dictionary<T : Any, F : Comparable<F>> : Asset {
val languageModel: LanguageModel<T, F>
interface Dictionary : Asset {
/**
* Gets token predictions based on the given [precedingTokens] and the [currentToken]. The
* length of the returned list is limited to [maxSuggestionCount]. Note that the returned list
* may at any time give back less items than [maxSuggestionCount] indicates.
*/
fun getTokenPredictions(
precedingTokens: List<Token<T>>,
currentToken: Token<T>?,
precedingTokens: List<Word>,
currentToken: Word?,
maxSuggestionCount: Int,
allowPossiblyOffensive: Boolean
): List<WeightedToken<T, F>>
allowPossiblyOffensive: Boolean,
destSuggestionList: SuggestionList
)
fun getDate(): Long
fun getVersion(): Int
}
interface MutableDictionary<T : Any, F : Comparable<F>> : Dictionary<T, F> {
override val languageModel: MutableLanguageModel<T, F>
interface MutableDictionary : Dictionary {
fun trainTokenPredictions(
precedingTokens: List<Token<T>>,
lastToken: Token<T>
precedingTokens: List<Word>,
lastToken: Word
)
fun setDate(date: Int)

View File

@@ -17,14 +17,32 @@
package dev.patrickgold.florisboard.ime.dictionary
import android.content.Context
import androidx.room.Room
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.extension.AssetRef
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
import dev.patrickgold.florisboard.ime.nlp.Word
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import timber.log.Timber
import java.lang.ref.WeakReference
import java.util.*
/**
* TODO: document
*/
class DictionaryManager private constructor(private val applicationContext: Context) {
private val dictionaryCache: MutableMap<String, Dictionary<String, Int>> = mutableMapOf()
class DictionaryManager private constructor(
context: Context,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
private val applicationContext: WeakReference<Context> = WeakReference(context.applicationContext ?: context)
private val prefs get() = Preferences.default()
private val dictionaryCache: MutableMap<String, Dictionary> = mutableMapOf()
private var florisUserDictionaryDatabase: FlorisUserDictionaryDatabase? = null
private var systemUserDictionaryDatabase: SystemUserDictionaryDatabase? = null
companion object {
private var defaultInstance: DictionaryManager? = null
@@ -47,22 +65,118 @@ class DictionaryManager private constructor(private val applicationContext: Cont
}
}
fun loadDictionary(ref: AssetRef): Result<Dictionary<String, Int>> {
dictionaryCache[ref.toString()]?.let {
return Result.success(it)
inline fun suggest(
currentWord: Word,
preceidingWords: List<Word>,
subtype: Subtype,
allowPossiblyOffensive: Boolean,
maxSuggestionCount: Int,
block: (suggestions: SuggestionList) -> Unit
) {
val suggestions = SuggestionList.new(maxSuggestionCount)
queryUserDictionary(currentWord, subtype.locale, suggestions)
block(suggestions)
suggestions.dispose()
}
fun prepareDictionaries(subtype: Subtype) {
// TODO: Implement this
}
fun queryUserDictionary(word: Word, locale: Locale, destSuggestionList: SuggestionList) {
val florisDao = florisUserDictionaryDao()
val systemDao = systemUserDictionaryDao()
if (florisDao == null && systemDao == null) {
return
}
if (ref.path.endsWith(".flict")) {
// Assume this is a Flictionary
Flictionary.load(applicationContext, ref).onSuccess { flict ->
dictionaryCache[ref.toString()] = flict
return Result.success(flict)
}.onFailure { err ->
Timber.i(err)
return Result.failure(err)
if (prefs.dictionary.enableFlorisUserDictionary) {
florisDao?.query(word, locale)?.let {
for (entry in it) {
destSuggestionList.add(entry.word, entry.freq)
}
}
florisDao?.queryShortcut(word, locale)?.let {
for (entry in it) {
destSuggestionList.add(entry.word, entry.freq)
}
}
} else {
return Result.failure(Exception("Unable to determine supported type for given AssetRef!"))
}
return Result.failure(Exception("If this message is ever thrown, something is completely broken..."))
if (prefs.dictionary.enableSystemUserDictionary) {
systemDao?.query(word, locale)?.let {
for (entry in it) {
destSuggestionList.add(entry.word, entry.freq)
}
}
systemDao?.queryShortcut(word, locale)?.let {
for (entry in it) {
destSuggestionList.add(entry.word, entry.freq)
}
}
}
}
@Synchronized
fun florisUserDictionaryDao(): UserDictionaryDao? {
return if (prefs.suggestion.enabled && prefs.dictionary.enableFlorisUserDictionary) {
florisUserDictionaryDatabase?.userDictionaryDao()
} else {
null
}
}
@Synchronized
fun florisUserDictionaryDatabase(): FlorisUserDictionaryDatabase? {
return if (prefs.suggestion.enabled && prefs.dictionary.enableFlorisUserDictionary) {
florisUserDictionaryDatabase
} else {
null
}
}
@Synchronized
fun systemUserDictionaryDao(): UserDictionaryDao? {
return if (prefs.suggestion.enabled && prefs.dictionary.enableSystemUserDictionary) {
systemUserDictionaryDatabase?.userDictionaryDao()
} else {
null
}
}
@Synchronized
fun systemUserDictionaryDatabase(): SystemUserDictionaryDatabase? {
return if (prefs.suggestion.enabled && prefs.dictionary.enableSystemUserDictionary) {
systemUserDictionaryDatabase
} else {
null
}
}
@Synchronized
fun loadUserDictionariesIfNecessary() {
val context = applicationContext.get() ?: return
if (prefs.suggestion.enabled) {
if (florisUserDictionaryDatabase == null && prefs.dictionary.enableFlorisUserDictionary) {
florisUserDictionaryDatabase = Room.databaseBuilder(
context,
FlorisUserDictionaryDatabase::class.java,
FlorisUserDictionaryDatabase.DB_FILE_NAME
).allowMainThreadQueries().build()
}
if (systemUserDictionaryDatabase == null && prefs.dictionary.enableSystemUserDictionary) {
systemUserDictionaryDatabase = SystemUserDictionaryDatabase(context)
}
}
}
@Synchronized
fun unloadUserDictionariesIfNecessary() {
if (florisUserDictionaryDatabase != null) {
florisUserDictionaryDatabase?.close()
florisUserDictionaryDatabase = null
}
if (systemUserDictionaryDatabase != null) {
systemUserDictionaryDatabase = null
}
}
}

View File

@@ -21,7 +21,6 @@ import dev.patrickgold.florisboard.ime.extension.AssetRef
import dev.patrickgold.florisboard.ime.extension.AssetSource
import dev.patrickgold.florisboard.ime.nlp.*
import java.io.InputStream
import java.util.*
import kotlin.jvm.Throws
/**
@@ -31,15 +30,15 @@ import kotlin.jvm.Throws
* This class accepts binary dictionary files of the type "flict" as defined in here:
* https://github.com/florisboard/dictionary-tools/blob/main/flictionary.md
*/
/**
class Flictionary private constructor(
override val name: String,
override val label: String,
override val authors: List<String>,
private val date: Long,
private val version: Int,
private val headerStr: String,
override val languageModel: LanguageModel<String, Int>
) : Dictionary<String, Int> {
private val headerStr: String
) : Dictionary {
companion object {
private const val VERSION_0 = 0x0
@@ -307,7 +306,7 @@ class Flictionary private constructor(
return if (currentToken.data.isNotEmpty()) {
val retList = languageModel.matchAllNgrams(
ngram = Ngram(
_tokens = listOf(Token(currentToken.data.toLowerCase(Locale.ENGLISH))),
_tokens = listOf(Token(currentToken.data.lowercase())),
_freq = -1
),
maxEditDistance = 2,
@@ -428,3 +427,4 @@ fun InputStream.readNext(b: ByteArray, off: Int, len: Int): Int {
}
return lenRead
}
*/

View File

@@ -0,0 +1,445 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.dictionary
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.UserDictionary
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Delete
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.RoomDatabase
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import androidx.room.Update
import dev.patrickgold.florisboard.ime.extension.ExternalContentUtils
import dev.patrickgold.florisboard.util.LocaleUtils
import java.lang.ref.WeakReference
import java.util.*
private const val WORDS_TABLE = "words"
const val FREQUENCY_MIN = 1
const val FREQUENCY_MAX = 255
const val FREQUENCY_DEFAULT = 128
private const val SORT_BY_WORD_ASC = "${UserDictionary.Words.WORD} ASC"
private const val SORT_BY_WORD_DESC = "${UserDictionary.Words.WORD} DESC"
private const val SORT_BY_FREQ_ASC = "${UserDictionary.Words.FREQUENCY} ASC"
private const val SORT_BY_FREQ_DESC = "${UserDictionary.Words.FREQUENCY} DESC"
private val PROJECTIONS: Array<String> = arrayOf(
UserDictionary.Words._ID,
UserDictionary.Words.WORD,
UserDictionary.Words.FREQUENCY,
UserDictionary.Words.LOCALE,
UserDictionary.Words.SHORTCUT,
)
private val PROJECTIONS_LANGUAGE: Array<String> = arrayOf(
UserDictionary.Words.LOCALE,
)
@Entity(tableName = WORDS_TABLE)
data class UserDictionaryEntry(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = UserDictionary.Words._ID, index = true)
val id: Long,
@ColumnInfo(name = UserDictionary.Words.WORD)
val word: String,
@ColumnInfo(name = UserDictionary.Words.FREQUENCY)
val freq: Int,
@ColumnInfo(name = UserDictionary.Words.LOCALE)
val locale: String?,
@ColumnInfo(name = UserDictionary.Words.SHORTCUT)
val shortcut: String?,
)
@Dao
interface UserDictionaryDao {
companion object {
private const val SELECT_ALL_FROM_WORDS =
"SELECT * FROM $WORDS_TABLE"
private const val LOCALE_MATCHES =
"(${UserDictionary.Words.LOCALE} = :locale OR ${UserDictionary.Words.LOCALE} IS NULL)"
}
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.WORD} LIKE '%' || :word || '%'")
fun query(word: String): List<UserDictionaryEntry>
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.WORD} LIKE '%' || :word || '%' AND $LOCALE_MATCHES")
fun query(word: String, locale: Locale?): List<UserDictionaryEntry>
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.SHORTCUT} = :shortcut")
fun queryShortcut(shortcut: String): List<UserDictionaryEntry>
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.SHORTCUT} = :shortcut AND $LOCALE_MATCHES")
fun queryShortcut(shortcut: String, locale: Locale?): List<UserDictionaryEntry>
@Query(SELECT_ALL_FROM_WORDS)
fun queryAll(): List<UserDictionaryEntry>
@Query("$SELECT_ALL_FROM_WORDS WHERE (${UserDictionary.Words.LOCALE} = :locale AND :locale IS NOT NULL) OR (${UserDictionary.Words.LOCALE} IS NULL AND :locale IS NULL)")
fun queryAll(locale: Locale?): List<UserDictionaryEntry>
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.WORD} = :word")
fun queryExact(word: String): List<UserDictionaryEntry>
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.WORD} = :word AND (${UserDictionary.Words.LOCALE} = :locale OR (${UserDictionary.Words.LOCALE} IS NULL AND :locale IS NULL))")
fun queryExact(word: String, locale: Locale?): List<UserDictionaryEntry>
@Query("SELECT DISTINCT ${UserDictionary.Words.LOCALE} FROM $WORDS_TABLE")
fun queryLanguageList(): List<Locale?>
@Insert
fun insert(entry: UserDictionaryEntry)
@Update
fun update(entry: UserDictionaryEntry)
@Delete
fun delete(entry: UserDictionaryEntry)
@Query("DELETE FROM $WORDS_TABLE")
fun deleteAll()
}
interface UserDictionaryDatabase {
fun userDictionaryDao(): UserDictionaryDao
fun reset()
fun importCombinedList(context: Context, uri: Uri): Result<Unit> {
return ExternalContentUtils.readFromUri(context, uri,6_192_000) { src ->
var isFirstLine = true
src.forEachLine { line ->
if (isFirstLine) {
// Ignore
isFirstLine = false
} else {
var word: String? = null
var freq: Int? = null
var locale: String? = null
var shortcut: String? = null
line.split(';').forEach { property ->
val keyValuePair = property.split('=')
if (keyValuePair.size == 2) {
val key = keyValuePair[0].trim().lowercase()
val value = keyValuePair[1].trim()
when (key) {
"w", "word" -> word = value.ifBlank { null }
"f", "freq" -> runCatching { value.toInt(10) }.onSuccess {
freq = it.coerceIn(FREQUENCY_MIN, FREQUENCY_MAX)
}
"l", "locale" -> locale = when (value) {
"all", "null", "" -> null
else -> value.ifBlank { null }
}
"s", "shortcut" -> shortcut = value.ifBlank { null }
}
}
}
if (word != null && freq != null) {
val alreadyExistingEntries = userDictionaryDao().queryExact(
word!!, locale?.let { LocaleUtils.stringToLocale(it) }
)
if (alreadyExistingEntries.isNotEmpty()) {
userDictionaryDao().update(UserDictionaryEntry(alreadyExistingEntries[0].id, word!!, freq!!, locale, shortcut))
} else {
userDictionaryDao().insert(UserDictionaryEntry(0, word!!, freq!!, locale, shortcut))
}
}
}
}
}
}
fun exportCombinedList(context: Context, uri: Uri): Result<Unit> {
return ExternalContentUtils.writeToUri(context, uri) { dst ->
StringBuilder().apply {
append("dictionary=")
append(uri.lastPathSegment)
append(";date=")
append(System.currentTimeMillis())
append(";generated-by=")
append(context.packageName)
append(";version=1")
appendLine()
dst.write(toString())
}
for (entry in userDictionaryDao().queryAll()) {
StringBuilder().apply {
append(" w=")
append(entry.word)
append(";f=")
append(entry.freq)
append(";l=")
append(entry.locale) // always append locale even if null
if (entry.shortcut != null) {
append(";s=")
append(entry.shortcut)
}
appendLine()
dst.write(toString())
}
}
}
}
}
@Database(entities = [UserDictionaryEntry::class], version = 1)
@TypeConverters(FlorisUserDictionaryDatabase.Converters::class)
abstract class FlorisUserDictionaryDatabase : RoomDatabase(), UserDictionaryDatabase {
companion object {
const val DB_FILE_NAME = "floris_user_dictionary"
}
abstract override fun userDictionaryDao(): UserDictionaryDao
override fun reset() {
TODO("Not yet implemented")
}
class Converters {
@TypeConverter
fun localeToString(locale: Locale?): String? {
return when (locale) {
null -> null
else -> locale.toString()
}
}
@TypeConverter
fun stringToLocale(string: String?): Locale? {
return when (string) {
null, "all", "null", "" -> null
else -> LocaleUtils.stringToLocale(string)
}
}
}
}
class SystemUserDictionaryDatabase(context: Context) : UserDictionaryDatabase {
private val applicationContext: WeakReference<Context> = WeakReference(context.applicationContext ?: context)
private val dao = object : UserDictionaryDao {
override fun query(word: String): List<UserDictionaryEntry> {
return queryResolver(
selection = "${UserDictionary.Words.WORD} LIKE ?",
selectionArgs = arrayOf("%$word%"),
sortOrder = SORT_BY_FREQ_DESC,
)
}
override fun query(word: String, locale: Locale?): List<UserDictionaryEntry> {
return if (locale == null) {
queryResolver(
selection = "${UserDictionary.Words.WORD} LIKE ? AND ${UserDictionary.Words.LOCALE} IS NULL",
selectionArgs = arrayOf("%$word%"),
sortOrder = SORT_BY_FREQ_DESC,
)
} else {
queryResolver(
selection = "${UserDictionary.Words.WORD} LIKE ? AND (${UserDictionary.Words.LOCALE} = ? OR ${UserDictionary.Words.LOCALE} = ? OR ${UserDictionary.Words.LOCALE} IS NULL)",
selectionArgs = arrayOf("%$word%", locale.toString(), locale.language.toString()),
sortOrder = SORT_BY_FREQ_DESC,
)
}
}
override fun queryShortcut(shortcut: String): List<UserDictionaryEntry> {
return queryResolver(
selection = "${UserDictionary.Words.SHORTCUT} = ?",
selectionArgs = arrayOf(shortcut),
sortOrder = SORT_BY_FREQ_DESC,
)
}
override fun queryShortcut(shortcut: String, locale: Locale?): List<UserDictionaryEntry> {
return if (locale == null) {
queryResolver(
selection = "${UserDictionary.Words.SHORTCUT} = ? AND ${UserDictionary.Words.LOCALE} IS NULL",
selectionArgs = arrayOf(shortcut),
sortOrder = SORT_BY_FREQ_DESC,
)
} else {
queryResolver(
selection = "${UserDictionary.Words.SHORTCUT} = ? AND (${UserDictionary.Words.LOCALE} = ? OR ${UserDictionary.Words.LOCALE} = ? OR ${UserDictionary.Words.LOCALE} IS NULL)",
selectionArgs = arrayOf(shortcut, locale.toString(), locale.language.toString()),
sortOrder = SORT_BY_FREQ_DESC,
)
}
}
override fun queryAll(): List<UserDictionaryEntry> {
return queryResolver(
selection = null,
selectionArgs = null,
sortOrder = SORT_BY_FREQ_DESC,
)
}
override fun queryAll(locale: Locale?): List<UserDictionaryEntry> {
return if (locale == null) {
queryResolver(
selection = "${UserDictionary.Words.LOCALE} IS NULL",
selectionArgs = null,
sortOrder = SORT_BY_FREQ_DESC,
)
} else {
queryResolver(
selection = "${UserDictionary.Words.LOCALE} = ?",
selectionArgs = arrayOf(locale.toString()),
sortOrder = SORT_BY_FREQ_DESC,
)
}
}
override fun queryExact(word: String): List<UserDictionaryEntry> {
return queryResolver(
selection = "${UserDictionary.Words.WORD} = ?",
selectionArgs = arrayOf(word),
sortOrder = null,
)
}
override fun queryExact(word: String, locale: Locale?): List<UserDictionaryEntry> {
return if (locale == null) {
queryResolver(
selection = "${UserDictionary.Words.WORD} = ? AND ${UserDictionary.Words.LOCALE} IS NULL",
selectionArgs = arrayOf(word),
sortOrder = SORT_BY_FREQ_DESC,
)
} else {
queryResolver(
selection = "${UserDictionary.Words.WORD} LIKE ? AND ${UserDictionary.Words.LOCALE} = ?",
selectionArgs = arrayOf(word, locale.toString()),
sortOrder = SORT_BY_FREQ_DESC,
)
}
}
override fun queryLanguageList(): List<Locale?> {
val resolver = applicationContext.get()?.contentResolver ?: return listOf()
val cursor = resolver.query(
UserDictionary.Words.CONTENT_URI,
PROJECTIONS_LANGUAGE,
null,
null,
null
) ?: return listOf()
if (cursor.count <= 0) {
return listOf()
}
val localeIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE)
val retList = mutableSetOf<Locale?>()
while (cursor.moveToNext()) {
val localeStr = cursor.getString(localeIndex)
if (localeStr == null) {
retList.add(null)
} else {
retList.add(LocaleUtils.stringToLocale(localeStr))
}
}
cursor.close()
return retList.toList()
}
private fun queryResolver(selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): List<UserDictionaryEntry> {
val resolver = applicationContext.get()?.contentResolver ?: return listOf()
val cursor = resolver.query(
UserDictionary.Words.CONTENT_URI,
PROJECTIONS,
selection,
selectionArgs,
sortOrder
) ?: return listOf()
return parseEntries(cursor).also { cursor.close() }
}
private fun parseEntries(cursor: Cursor): List<UserDictionaryEntry> {
if (cursor.count <= 0) {
return listOf()
}
val idIndex = cursor.getColumnIndex(UserDictionary.Words._ID)
val wordIndex = cursor.getColumnIndex(UserDictionary.Words.WORD)
val freqIndex = cursor.getColumnIndex(UserDictionary.Words.FREQUENCY)
val localeIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE)
val shortcutIndex = cursor.getColumnIndex(UserDictionary.Words.SHORTCUT)
val retList = mutableListOf<UserDictionaryEntry>()
while (cursor.moveToNext()) {
retList.add(
UserDictionaryEntry(
id = cursor.getLong(idIndex),
word = cursor.getString(wordIndex),
freq = cursor.getInt(freqIndex),
locale = cursor.getString(localeIndex),
shortcut = cursor.getString(shortcutIndex)
)
)
}
return retList
}
override fun insert(entry: UserDictionaryEntry) {
val resolver = applicationContext.get()?.contentResolver ?: return
val contentValues = ContentValues(5).apply {
put(UserDictionary.Words.WORD, entry.word)
put(UserDictionary.Words.FREQUENCY, entry.freq)
put(UserDictionary.Words.LOCALE, entry.locale)
put(UserDictionary.Words.APP_ID, 0)
put(UserDictionary.Words.SHORTCUT, entry.shortcut)
}
resolver.insert(UserDictionary.Words.CONTENT_URI, contentValues)
}
override fun update(entry: UserDictionaryEntry) {
val resolver = applicationContext.get()?.contentResolver ?: return
val contentValues = ContentValues(4).apply {
put(UserDictionary.Words.WORD, entry.word)
put(UserDictionary.Words.FREQUENCY, entry.freq)
put(UserDictionary.Words.LOCALE, entry.locale)
put(UserDictionary.Words.SHORTCUT, entry.shortcut)
}
resolver.update(UserDictionary.Words.CONTENT_URI, contentValues, "${UserDictionary.Words._ID} = ${entry.id}", null)
}
override fun delete(entry: UserDictionaryEntry) {
val resolver = applicationContext.get()?.contentResolver ?: return
resolver.delete(UserDictionary.Words.CONTENT_URI, "${UserDictionary.Words._ID} = ${entry.id}", null)
}
override fun deleteAll() {
// Unsupported action
}
}
override fun userDictionaryDao(): UserDictionaryDao {
return dao
}
override fun reset() {
TODO("Not yet implemented")
}
}

View File

@@ -22,10 +22,8 @@ import dev.patrickgold.florisboard.ime.keyboard.CaseSelector
import dev.patrickgold.florisboard.ime.keyboard.KeyData
import dev.patrickgold.florisboard.ime.keyboard.VariationSelector
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
import dev.patrickgold.florisboard.ime.text.keyboard.AutoTextKeyData
import dev.patrickgold.florisboard.ime.text.keyboard.BasicTextKeyData
import dev.patrickgold.florisboard.ime.text.keyboard.MultiTextKeyData
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.text.composing.*
import dev.patrickgold.florisboard.ime.text.keyboard.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -55,6 +53,12 @@ class AssetManager private constructor(val applicationContext: Context) {
subclass(MultiTextKeyData::class, MultiTextKeyData.serializer())
default { BasicTextKeyData.serializer() }
}
polymorphic(Composer::class) {
subclass(Appender::class, Appender.serializer())
subclass(HangulUnicode::class, HangulUnicode.serializer())
subclass(WithRules::class, WithRules.serializer())
default { Appender.serializer() }
}
}
}

View File

@@ -16,8 +16,6 @@
package dev.patrickgold.florisboard.ime.extension
import java.util.*
/**
* Sealed class which specifies where an asset comes from. There are 3 different types, all of which
* require a different approach on how to access the actual asset.
@@ -50,7 +48,7 @@ sealed class AssetSource {
private val externalRegex: Regex = """^external\\(([a-z]+\\.)*[a-z]+\\)\$""".toRegex()
fun fromString(str: String): Result<AssetSource> {
return when (val string = str.toLowerCase(Locale.ENGLISH)) {
return when (val string = str.lowercase()) {
"assets" -> Result.success(Assets)
"internal" -> Result.success(Internal)
else -> {

View File

@@ -14,35 +14,58 @@
* limitations under the License.
*/
@file:OptIn(ExperimentalContracts::class)
package dev.patrickgold.florisboard.ime.extension
import android.content.Context
import android.net.Uri
import java.io.BufferedReader
import java.io.BufferedWriter
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
class ExternalContentUtils private constructor() {
companion object {
fun readTextFromUri(context: Context, uri: Uri, maxSize: Int): Result<String> {
val contentResolver = context.contentResolver
?: return Result.failure(NullPointerException("System content resolver not available"))
val inputStream = contentResolver.openInputStream(uri)
?: return Result.failure(NullPointerException("Cannot open input stream for given uri '$uri'"))
val assetFileDescriptor = contentResolver.openAssetFileDescriptor(uri, "r")
?: return Result.failure(NullPointerException("Cannot open asset file descriptor for given uri '$uri'"))
if (assetFileDescriptor.length > maxSize) {
return Result.failure(Exception("Contents of given uri '$uri' exceeds maximum size of $maxSize bytes!"))
inline fun <R> readFromUri(context: Context, uri: Uri, maxSize: Int, block: (it: BufferedReader) -> R): Result<R> {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
return runCatching {
val contentResolver = context.contentResolver
?: throw NullPointerException("System content resolver not available")
val inputStream = contentResolver.openInputStream(uri)
?: throw NullPointerException("Cannot open input stream for given uri '$uri'")
val assetFileDescriptor = contentResolver.openAssetFileDescriptor(uri, "r")
?: throw NullPointerException("Cannot open asset file descriptor for given uri '$uri'")
if (assetFileDescriptor.length > maxSize) {
throw Exception("Contents of given uri '$uri' exceeds maximum size of $maxSize bytes!")
}
inputStream.bufferedReader(Charsets.UTF_8).use { block(it) }
}
}
fun readTextFromUri(context: Context, uri: Uri, maxSize: Int): Result<String> {
return readFromUri(context, uri, maxSize) { it.readText() }
}
inline fun writeToUri(context: Context, uri: Uri, block: (it: BufferedWriter) -> Unit): Result<Unit> {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
return runCatching {
val contentResolver = context.contentResolver
?: throw NullPointerException("System content resolver not available")
// Must use "rwt" mode to ensure destination file length is truncated after writing.
val outputStream = contentResolver.openOutputStream(uri, "rwt")
?: throw NullPointerException("Cannot open output stream for given uri '$uri'")
outputStream.bufferedWriter(Charsets.UTF_8).use { block(it) }
}
val rawText = inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
return Result.success(rawText)
}
fun writeTextToUri(context: Context, uri: Uri, text: String): Result<Unit> {
val contentResolver = context.contentResolver
?: return Result.failure(NullPointerException("System content resolver not available"))
// Must use "rwt" mode to ensure destination file length is truncated after writing.
val outputStream = contentResolver.openOutputStream(uri, "rwt")
?: return Result.failure(NullPointerException("Cannot open output stream for given uri '$uri'"))
outputStream.bufferedWriter(Charsets.UTF_8).use { it.write(text) }
return Result.success(Unit)
return writeToUri(context, uri) { it.write(text) }
}
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("MemberVisibilityCanBePrivate")
package dev.patrickgold.florisboard.ime.keyboard
import android.os.Build
import android.view.inputmethod.EditorInfo
/**
* Class which holds the same information as an [EditorInfo.imeOptions] int but more accessible and
* readable.
*/
@JvmInline
value class ImeOptions(val state: KeyboardState) {
companion object {
const val M_IME_OPTIONS: ULong = 0x0F_FFu
const val O_IME_OPTIONS: Int = 32
const val M_ENTER_ACTION: ULong = 0x0Fu
const val O_ENTER_ACTION: Int = 32
const val F_FORCE_ASCII: ULong = 0x00_00_00_10_00_00_00_00u
const val F_NAVIGATE_NEXT: ULong = 0x00_00_00_20_00_00_00_00u
const val F_NAVIGATE_PREVIOUS: ULong = 0x00_00_00_40_00_00_00_00u
const val F_NO_ACCESSORY_ACTION: ULong = 0x00_00_00_80_00_00_00_00u
const val F_NO_ENTER_ACTION: ULong = 0x00_00_01_00_00_00_00_00u
const val F_NO_EXTRACT_UI: ULong = 0x00_00_02_00_00_00_00_00u
const val F_NO_FULLSCREEN: ULong = 0x00_00_04_00_00_00_00_00u
const val F_NO_PERSONALIZED_LEARNING: ULong = 0x00_00_08_00_00_00_00_00u
}
var enterAction: EnterAction
get() = EnterAction.fromInt(state.getRegion(M_ENTER_ACTION, O_ENTER_ACTION))
private set(v) = state.setRegion(M_ENTER_ACTION, O_ENTER_ACTION, v.toInt())
var flagForceAscii: Boolean
get() = state.getFlag(F_FORCE_ASCII)
private set(v) = state.setFlag(F_FORCE_ASCII, v)
var flagNavigateNext: Boolean
get() = state.getFlag(F_NAVIGATE_NEXT)
private set(v) = state.setFlag(F_NAVIGATE_NEXT, v)
var flagNavigatePrevious: Boolean
get() = state.getFlag(F_NAVIGATE_PREVIOUS)
private set(v) = state.setFlag(F_NAVIGATE_PREVIOUS, v)
var flagNoAccessoryAction: Boolean
get() = state.getFlag(F_NO_ACCESSORY_ACTION)
private set(v) = state.setFlag(F_NO_ACCESSORY_ACTION, v)
var flagNoEnterAction: Boolean
get() = state.getFlag(F_NO_ENTER_ACTION)
private set(v) = state.setFlag(F_NO_ENTER_ACTION, v)
var flagNoExtractUi: Boolean
get() = state.getFlag(F_NO_EXTRACT_UI)
private set(v) = state.setFlag(F_NO_EXTRACT_UI, v)
var flagNoFullscreen: Boolean
get() = state.getFlag(F_NO_FULLSCREEN)
private set(v) = state.setFlag(F_NO_FULLSCREEN, v)
var flagNoPersonalizedLearning: Boolean
get() = state.getFlag(F_NO_PERSONALIZED_LEARNING)
private set(v) = state.setFlag(F_NO_PERSONALIZED_LEARNING, v)
fun update(editorInfo: EditorInfo) {
val imeOptionsRaw = editorInfo.imeOptions
state.setRegion(M_IME_OPTIONS, O_IME_OPTIONS, 0) // reset imeOptions region
enterAction = EnterAction.fromInt(imeOptionsRaw and EditorInfo.IME_MASK_ACTION)
flagForceAscii = imeOptionsRaw and EditorInfo.IME_FLAG_FORCE_ASCII != 0
flagNavigateNext = imeOptionsRaw and EditorInfo.IME_FLAG_NAVIGATE_NEXT != 0
flagNavigatePrevious = imeOptionsRaw and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS != 0
flagNoAccessoryAction = imeOptionsRaw and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION != 0
flagNoEnterAction = imeOptionsRaw and EditorInfo.IME_FLAG_NO_ENTER_ACTION != 0
flagNoExtractUi = imeOptionsRaw and EditorInfo.IME_FLAG_NO_EXTRACT_UI != 0
flagNoFullscreen = imeOptionsRaw and EditorInfo.IME_FLAG_NO_FULLSCREEN != 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
flagNoPersonalizedLearning = imeOptionsRaw and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING != 0
}
}
enum class EnterAction(val value: Int) {
UNSPECIFIED(EditorInfo.IME_ACTION_UNSPECIFIED),
DONE(EditorInfo.IME_ACTION_DONE),
GO(EditorInfo.IME_ACTION_GO),
NEXT(EditorInfo.IME_ACTION_NEXT),
NONE(EditorInfo.IME_ACTION_NONE),
PREVIOUS(EditorInfo.IME_ACTION_PREVIOUS),
SEARCH(EditorInfo.IME_ACTION_SEARCH),
SEND(EditorInfo.IME_ACTION_SEND);
companion object {
fun fromInt(int: Int) = values().firstOrNull { it.value == int } ?: NONE
}
fun toInt() = value
}
}

View File

@@ -0,0 +1,224 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("MemberVisibilityCanBePrivate")
package dev.patrickgold.florisboard.ime.keyboard
import android.text.InputType
import android.view.inputmethod.EditorInfo
/**
* Class which holds the same information as an [EditorInfo.inputType] int but more accessible and
* readable.
*/
@JvmInline
value class InputAttributes(val state: KeyboardState) {
companion object {
const val M_INPUT_ATTRIBUTES: ULong = 0x0F_FF_FFu
const val O_INPUT_ATTRIBUTES: Int = 44
const val M_TYPE: ULong = 0x07u
const val O_TYPE: Int = 44
const val M_VARIATION: ULong = 0x1Fu
const val O_VARIATION: Int = 47
const val M_CAPS_MODE: ULong = 0x03u
const val O_CAPS_MODE: Int = 52
const val F_NUMBER_DECIMAL: ULong = 0x00_40_00_00_00_00_00_00u
const val F_NUMBER_SIGNED: ULong = 0x00_80_00_00_00_00_00_00u
const val F_TEXT_AUTO_COMPLETE: ULong = 0x01_00_00_00_00_00_00_00u
const val F_TEXT_AUTO_CORRECT: ULong = 0x02_00_00_00_00_00_00_00u
const val F_TEXT_IME_MULTILINE: ULong = 0x04_00_00_00_00_00_00_00u
const val F_TEXT_MULTILINE: ULong = 0x08_00_00_00_00_00_00_00u
const val F_TEXT_NO_SUGGESTIONS: ULong = 0x10_00_00_00_00_00_00_00u
}
var type: Type
get() = Type.fromInt(state.getRegion(M_TYPE, O_TYPE))
private set(v) = state.setRegion(M_TYPE, O_TYPE, v.toInt())
var variation: Variation
get() = Variation.fromInt(state.getRegion(M_VARIATION, O_VARIATION))
private set(v) = state.setRegion(M_VARIATION, O_VARIATION, v.toInt())
var capsMode: CapsMode
get() = CapsMode.fromInt(state.getRegion(M_CAPS_MODE, O_CAPS_MODE))
private set(v) = state.setRegion(M_CAPS_MODE, O_CAPS_MODE, v.toInt())
var flagNumberDecimal: Boolean
get() = state.getFlag(F_NUMBER_DECIMAL)
private set(v) = state.setFlag(F_NUMBER_DECIMAL, v)
var flagNumberSigned: Boolean
get() = state.getFlag(F_NUMBER_SIGNED)
private set(v) = state.setFlag(F_NUMBER_SIGNED, v)
var flagTextAutoComplete: Boolean
get() = state.getFlag(F_TEXT_AUTO_COMPLETE)
private set(v) = state.setFlag(F_TEXT_AUTO_COMPLETE, v)
var flagTextAutoCorrect: Boolean
get() = state.getFlag(F_TEXT_AUTO_CORRECT)
private set(v) = state.setFlag(F_TEXT_AUTO_CORRECT, v)
var flagTextImeMultiLine: Boolean
get() = state.getFlag(F_TEXT_IME_MULTILINE)
private set(v) = state.setFlag(F_TEXT_IME_MULTILINE, v)
var flagTextMultiLine: Boolean
get() = state.getFlag(F_TEXT_MULTILINE)
private set(v) = state.setFlag(F_TEXT_MULTILINE, v)
var flagTextNoSuggestions: Boolean
get() = state.getFlag(F_TEXT_NO_SUGGESTIONS)
private set(v) = state.setFlag(F_TEXT_NO_SUGGESTIONS, v)
fun update(editorInfo: EditorInfo) {
val inputAttrsRaw = editorInfo.inputType
state.setRegion(M_INPUT_ATTRIBUTES, O_INPUT_ATTRIBUTES, 0) // reset inputAttributes region
when (inputAttrsRaw and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_NULL -> {
type = Type.NULL
variation = Variation.NORMAL
capsMode = CapsMode.NONE
}
InputType.TYPE_CLASS_DATETIME -> {
type = Type.DATETIME
variation = when (inputAttrsRaw and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_DATETIME_VARIATION_DATE -> Variation.DATE
InputType.TYPE_DATETIME_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_DATETIME_VARIATION_TIME -> Variation.TIME
else -> Variation.NORMAL
}
capsMode = CapsMode.NONE
}
InputType.TYPE_CLASS_NUMBER -> {
type = Type.NUMBER
variation = when (inputAttrsRaw and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_NUMBER_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> Variation.PASSWORD
else -> Variation.NORMAL
}
capsMode = CapsMode.NONE
flagNumberDecimal = inputAttrsRaw and InputType.TYPE_NUMBER_FLAG_DECIMAL != 0
flagNumberSigned = inputAttrsRaw and InputType.TYPE_NUMBER_FLAG_SIGNED != 0
}
InputType.TYPE_CLASS_PHONE -> {
type = Type.PHONE
variation = Variation.NORMAL
capsMode = CapsMode.NONE
}
InputType.TYPE_CLASS_TEXT -> {
type = Type.TEXT
variation = when (inputAttrsRaw and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> Variation.EMAIL_ADDRESS
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> Variation.EMAIL_SUBJECT
InputType.TYPE_TEXT_VARIATION_FILTER -> Variation.FILTER
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> Variation.LONG_MESSAGE
InputType.TYPE_TEXT_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_TEXT_VARIATION_PASSWORD -> Variation.PASSWORD
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> Variation.PERSON_NAME
InputType.TYPE_TEXT_VARIATION_PHONETIC -> Variation.PHONETIC
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> Variation.POSTAL_ADDRESS
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> Variation.SHORT_MESSAGE
InputType.TYPE_TEXT_VARIATION_URI -> Variation.URI
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> Variation.VISIBLE_PASSWORD
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> Variation.WEB_EDIT_TEXT
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> Variation.WEB_EMAIL_ADDRESS
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> Variation.WEB_PASSWORD
else -> Variation.NORMAL
}
capsMode = CapsMode.fromFlags(inputAttrsRaw)
flagTextAutoComplete = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE != 0
flagTextAutoCorrect = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT != 0
flagTextImeMultiLine = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE != 0
flagTextMultiLine = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_MULTI_LINE != 0
flagTextNoSuggestions = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS != 0
}
else -> {
type = Type.TEXT
variation = Variation.NORMAL
capsMode = CapsMode.NONE
}
}
}
enum class Type(val value: Int) {
NULL(EditorInfo.TYPE_NULL),
DATETIME(EditorInfo.TYPE_CLASS_DATETIME),
NUMBER(EditorInfo.TYPE_CLASS_NUMBER),
PHONE(EditorInfo.TYPE_CLASS_PHONE),
TEXT(EditorInfo.TYPE_CLASS_TEXT);
companion object {
fun fromInt(int: Int) = values().firstOrNull { it.value == int } ?: NULL
}
fun toInt() = value
}
enum class Variation(val value: Int) {
NORMAL(0),
DATE(1),
EMAIL_ADDRESS(2),
EMAIL_SUBJECT(3),
FILTER(4),
LONG_MESSAGE(5),
PASSWORD(6),
PERSON_NAME(7),
PHONETIC(8),
POSTAL_ADDRESS(9),
SHORT_MESSAGE(10),
TIME(11),
URI(12),
VISIBLE_PASSWORD(13),
WEB_EDIT_TEXT(14),
WEB_EMAIL_ADDRESS(15),
WEB_PASSWORD(16);
companion object {
fun fromInt(int: Int) = values().firstOrNull { it.value == int } ?: NORMAL
}
fun toInt() = value
}
enum class CapsMode(val value: Int) {
NONE(0),
ALL(1),
SENTENCES(2),
WORDS(3);
companion object {
fun fromFlags(flags: Int): CapsMode {
return when {
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS != 0 -> ALL
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES != 0 -> SENTENCES
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS != 0 -> WORDS
else -> NONE
}
}
fun fromInt(int: Int) = values().firstOrNull { it.value == int } ?: NONE
}
fun toFlags(): Int {
return when (this) {
ALL -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
SENTENCES -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
WORDS -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
else -> 0
}
}
fun toInt() = value
}
}

View File

@@ -0,0 +1,262 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("MemberVisibilityCanBePrivate")
package dev.patrickgold.florisboard.ime.keyboard
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.core.view.children
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
/**
* This class is a helper managing the state of the text input logic which
* affects the keyboard view in rendering and layouting the keys.
*
* The state class can hold flags or small unsigned integers, all added up
* at max 64-bit though.
*
* The structure of this 8-byte state register is as follows: (Lower 4 bytes are pretty experimental rn)
*
* <Byte 3> | <Byte 2> | <Byte 1> | <Byte 0> | Description
* ---------|----------|----------|----------|---------------------------------
* | | | 1111 | Active [KeyboardMode]
* | | | 1111 | Active [KeyVariation]
* | | 1 | | Caps flag
* | | 1 | | Caps lock flag
* | | 1 | | Is selection active (length > 0)
* | | 1 | | Is private mode
* | 1 | | | Is Smartbar quick actions visible
* | 1 | | | Is Smartbar showing inline suggestions
* | 1 | | | Is composing enabled
*
* <Byte 7> | <Byte 6> | <Byte 5> | <Byte 4> | Description
* ---------|----------|----------|----------|---------------------------------
* | | | 1111 | [ImeOptions.enterAction]
* | | | 1 | [ImeOptions.flagForceAscii]
* | | | 1 | [ImeOptions.flagNavigateNext]
* | | | 1 | [ImeOptions.flagNavigatePrevious]
* | | | 1 | [ImeOptions.flagNoAccessoryAction]
* | | 1 | | [ImeOptions.flagNoEnterAction]
* | | 1 | | [ImeOptions.flagNoExtractUi]
* | | 1 | | [ImeOptions.flagNoFullscreen]
* | | 1 | | [ImeOptions.flagNoPersonalizedLearning]
* | | 111 | | [InputAttributes.type]
* | 1111 | 1 | | [InputAttributes.variation]
* | 11 | | | [InputAttributes.capsMode]
* | 1 | | | [InputAttributes.flagNumberDecimal]
* | 1 | | | [InputAttributes.flagNumberSigned]
* 1 | | | | [InputAttributes.flagTextAutoComplete]
* 1 | | | | [InputAttributes.flagTextAutoCorrect]
* 1 | | | | [InputAttributes.flagTextImeMultiLine]
* 1 | | | | [InputAttributes.flagTextMultiLine]
* 1 | | | | [InputAttributes.flagTextNoSuggestions]
*
* The resulting structure is only relevant during a runtime lifespan and
* thus can easily be changed without worrying about destroying some saved state.
*
* @property value The internal register used to store the flags and region ints that
* this keyboard state represents.
* @property maskOfInterest The mask which is applied when comparing this state with another.
* Is useful if only parts of a state instance is relevant to look at.
*/
class KeyboardState private constructor(var value: ULong, var maskOfInterest: ULong) {
companion object {
const val M_KEYBOARD_MODE: ULong = 0x0Fu
const val O_KEYBOARD_MODE: Int = 0
const val M_KEY_VARIATION: ULong = 0x0Fu
const val O_KEY_VARIATION: Int = 4
const val F_CAPS: ULong = 0x00000100u
const val F_CAPS_LOCK: ULong = 0x00000200u
const val F_IS_SELECTION_MODE: ULong = 0x00000400u
const val F_IS_PRIVATE_MODE: ULong = 0x00008000u
const val F_IS_QUICK_ACTIONS_VISIBLE: ULong = 0x00010000u
const val F_IS_SHOWING_INLINE_SUGGESTIONS: ULong = 0x00020000u
const val F_IS_COMPOSING_ENABLED: ULong = 0x00040000u
const val STATE_ALL_ZERO: ULong = 0uL
const val INTEREST_ALL: ULong = ULong.MAX_VALUE
const val INTEREST_NONE: ULong = 0uL
const val INTEREST_TEXT: ULong = 0xFF_FF_FF_FF_00_00_FF_FFu
const val INTEREST_MEDIA: ULong = 0x00_00_00_00_FF_00_00_00u
fun new(
value: ULong = STATE_ALL_ZERO,
maskOfInterest: ULong = INTEREST_ALL
) = KeyboardState(value, maskOfInterest)
}
val imeOptions: ImeOptions = ImeOptions(this)
val inputAttributes: InputAttributes = InputAttributes(this)
/**
* Resets this state register.
*
* @param newValue Optional, used to initialize the register value after the reset.
* Defaults to [STATE_ALL_ZERO].
*/
fun reset(newValue: ULong = STATE_ALL_ZERO) {
value = newValue
}
/**
* Resets this state register.
*
* @param newState A reference to a state which register value should be copied after
* the reset.
*/
fun reset(newState: KeyboardState) {
value = newState.value
}
/**
* Updates this state based on the info passed from [editorInfo].
*
* @param editorInfo The [EditorInfo] used to initialize all flags and regions relevant
* to the info this object provides.
*/
fun update(editorInfo: EditorInfo) {
imeOptions.update(editorInfo)
inputAttributes.update(editorInfo)
}
internal fun getFlag(f: ULong): Boolean {
return (value and f) != STATE_ALL_ZERO
}
internal fun setFlag(f: ULong, v: Boolean) {
value = if (v) { value or f } else { value and f.inv() }
}
internal fun getRegion(m: ULong, o: Int): Int {
return ((value shr o) and m).toInt()
}
internal fun setRegion(m: ULong, o: Int, v: Int) {
value = (value and (m shl o).inv()) or ((v.toULong() and m) shl o)
}
fun isEqualTo(other: KeyboardState): Boolean {
return (other.value and maskOfInterest) == (value and maskOfInterest)
}
fun isDifferentTo(other: KeyboardState): Boolean {
return !isEqualTo(other)
}
override fun hashCode(): Int {
var result = value.hashCode()
result = 31 * result + maskOfInterest.hashCode()
return result
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as KeyboardState
if (value != other.value) return false
if (maskOfInterest != other.maskOfInterest) return false
if (imeOptions != other.imeOptions) return false
if (inputAttributes != other.inputAttributes) return false
return true
}
var keyVariation: KeyVariation
get() = KeyVariation.fromInt(getRegion(M_KEY_VARIATION, O_KEY_VARIATION))
set(v) { setRegion(M_KEY_VARIATION, O_KEY_VARIATION, v.toInt()) }
var keyboardMode: KeyboardMode
get() = KeyboardMode.fromInt(getRegion(M_KEYBOARD_MODE, O_KEYBOARD_MODE))
set(v) { setRegion(M_KEYBOARD_MODE, O_KEYBOARD_MODE, v.toInt()) }
var caps: Boolean
get() = getFlag(F_CAPS)
set(v) { setFlag(F_CAPS, v) }
var capsLock: Boolean
get() = getFlag(F_CAPS_LOCK)
set(v) { setFlag(F_CAPS_LOCK, v) }
var isSelectionMode: Boolean
get() = getFlag(F_IS_SELECTION_MODE)
set(v) { setFlag(F_IS_SELECTION_MODE, v) }
var isCursorMode: Boolean
get() = !isSelectionMode
set(v) { isSelectionMode = !v }
var isPrivateMode: Boolean
get() = getFlag(F_IS_PRIVATE_MODE)
set(v) { setFlag(F_IS_PRIVATE_MODE, v) }
val isRawInputEditor: Boolean
get() = inputAttributes.type == InputAttributes.Type.NULL
val isRichInputEditor: Boolean
get() = inputAttributes.type != InputAttributes.Type.NULL
var isQuickActionsVisible: Boolean
get() = getFlag(F_IS_QUICK_ACTIONS_VISIBLE)
set(v) { setFlag(F_IS_QUICK_ACTIONS_VISIBLE, v) }
var isShowingInlineSuggestions: Boolean
get() = getFlag(F_IS_SHOWING_INLINE_SUGGESTIONS)
set(v) { setFlag(F_IS_SHOWING_INLINE_SUGGESTIONS, v) }
var isComposingEnabled: Boolean
get() = getFlag(F_IS_COMPOSING_ENABLED)
set(v) { setFlag(F_IS_COMPOSING_ENABLED, v) }
interface OnUpdateStateListener {
/**
* Adds the ability for Views to intercept a update keyboard state dispatch.
*
* @param newState Reference to the new state.
*
* @return True if the update was intercepted (and thus the child views have to
* be manually updated if needed, false if no interception happened.
*/
fun onInterceptUpdateKeyboardState(newState: KeyboardState): Boolean = false
/**
* A new keyboard state is dispatched to all views in this view tree.
*
* @param newState Reference to the new state.
*/
fun onUpdateKeyboardState(newState: KeyboardState)
}
}
fun View.updateKeyboardState(newState: KeyboardState) {
val intercepted: Boolean
if (this is KeyboardState.OnUpdateStateListener) {
intercepted = this.onInterceptUpdateKeyboardState(newState)
this.onUpdateKeyboardState(newState)
} else {
intercepted = false
}
if (this is ViewGroup && !intercepted) {
this.children.forEach { it.updateKeyboardState(newState) }
}
}

View File

@@ -20,27 +20,24 @@ import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.sendBlocking
@Suppress("MemberVisibilityCanBePrivate")
abstract class KeyboardView : View, ThemeManager.OnThemeUpdatedListener {
protected val florisboard: FlorisBoard?
get() = FlorisBoard.getInstanceOrNull()
protected val prefs: PrefHelper
get() = PrefHelper.getDefaultInstance(context)
protected val themeManager: ThemeManager?
get() = ThemeManager.defaultOrNull()
abstract class KeyboardView : ViewGroup, KeyboardState.OnUpdateStateListener, ThemeManager.OnThemeUpdatedListener {
protected val florisboard get() = FlorisBoard.getInstanceOrNull()
protected val prefs get() = Preferences.default()
protected val themeManager get() = ThemeManager.defaultOrNull()
var isMeasured: Boolean = false
private set
protected var isTouchable: Boolean = true
protected val touchEventChannel: Channel<MotionEvent> = Channel(16)
protected val touchEventChannel: Channel<MotionEvent> = Channel(64)
protected val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
constructor(context: Context) : this(context, null)
@@ -48,6 +45,7 @@ abstract class KeyboardView : View, ThemeManager.OnThemeUpdatedListener {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
layoutDirection = LAYOUT_DIRECTION_LTR
mainScope.launch {
for (event in touchEventChannel) {
if (!isActive) break
@@ -66,6 +64,10 @@ abstract class KeyboardView : View, ThemeManager.OnThemeUpdatedListener {
themeManager?.unregisterOnThemeUpdatedListener(this)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
return true
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
event ?: return false
@@ -92,10 +94,5 @@ abstract class KeyboardView : View, ThemeManager.OnThemeUpdatedListener {
isMeasured = true
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
onLayoutInternal()
}
protected abstract fun onLayoutInternal()
abstract fun sync()
}

View File

@@ -16,8 +16,6 @@
package dev.patrickgold.florisboard.ime.landscapeinput
import java.util.*
enum class LandscapeInputUiMode {
DYNAMICALLY_SHOW,
NEVER_SHOW,
@@ -25,7 +23,7 @@ enum class LandscapeInputUiMode {
companion object {
fun fromString(string: String): LandscapeInputUiMode {
return valueOf(string.toUpperCase(Locale.ENGLISH))
return valueOf(string.uppercase())
}
}
}

View File

@@ -22,17 +22,18 @@ import android.view.View
import android.widget.*
import com.google.android.material.tabs.TabLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.FlorisboardBinding
import dev.patrickgold.florisboard.debug.LogTopic
import dev.patrickgold.florisboard.debug.flogInfo
import dev.patrickgold.florisboard.ime.core.EditorInstance
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
import dev.patrickgold.florisboard.ime.core.InputView
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyboardView
import dev.patrickgold.florisboard.ime.media.emoticon.EmoticonKeyData
import dev.patrickgold.florisboard.ime.media.emoticon.EmoticonKeyboardView
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import kotlinx.coroutines.*
import timber.log.Timber
import java.util.*
/**
@@ -57,8 +58,6 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
private var tabLayout: TabLayout? = null
private val tabViews = EnumMap<Tab, LinearLayout>(Tab::class.java)
private var mediaViewGroup: LinearLayout? = null
companion object {
private var instance: MediaInputManager? = null
@@ -75,27 +74,20 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
florisboard.addEventListener(this)
}
/**
* Called when a new input view has been registered. Used to initialize all media-relevant
* views and layouts.
* TODO: evaluate if the view initializing process can be optimized.
*/
@SuppressLint("ClickableViewAccessibility")
override fun onRegisterInputView(inputView: InputView) {
Timber.i("onRegisterInputView(inputView)")
override fun onInitializeInputUi(uiBinding: FlorisboardBinding) {
flogInfo(LogTopic.IMS_EVENTS)
launch(Dispatchers.Default) {
mediaViewGroup = inputView.findViewById(R.id.media_input)
mediaViewFlipper = inputView.findViewById(R.id.media_input_view_flipper)
mediaViewFlipper = uiBinding.media.mediaInputViewFlipper
// Init bottom buttons
inputView.findViewById<Button>(R.id.media_input_switch_to_text_input_button)
.setOnTouchListener { view, event -> onBottomButtonEvent(view, event) }
inputView.findViewById<ImageButton>(R.id.media_input_backspace_button)
.setOnTouchListener { view, event -> onBottomButtonEvent(view, event) }
// Init bottom buttons
uiBinding.media.mediaInputSwitchToTextInputButton
.setOnTouchListener { view, event -> onBottomButtonEvent(view, event) }
uiBinding.media.mediaInputBackspaceButton
.setOnTouchListener { view, event -> onBottomButtonEvent(view, event) }
tabLayout = inputView.findViewById(R.id.media_input_tabs)
tabLayout?.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
tabLayout = uiBinding.media.mediaInputTabs.also {
it.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
when (tab.position) {
0 -> setActiveTab(Tab.EMOJI)
@@ -107,15 +99,15 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
override fun onTabUnselected(tab: TabLayout.Tab) {}
override fun onTabReselected(tab: TabLayout.Tab) {}
})
}
withContext(Dispatchers.Main) {
for (tab in Tab.values()) {
val tabView = createTabViewFor(tab)
tabViews[tab] = tabView
mediaViewFlipper?.addView(tabView)
}
tabLayout?.selectTab(tabLayout?.getTabAt(0))
launch(Dispatchers.Main) {
for (tab in Tab.values()) {
val tabView = createTabViewFor(tab)
tabViews[tab] = tabView
mediaViewFlipper?.addView(tabView)
}
tabLayout?.selectTab(tabLayout?.getTabAt(0))
}
}
@@ -123,7 +115,7 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
* Clean-up of resources and stopping all coroutines.
*/
override fun onDestroy() {
Timber.i("onDestroy()")
flogInfo(LogTopic.IMS_EVENTS)
cancel()
instance = null
@@ -161,9 +153,9 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
*/
private fun createTabViewFor(tab: Tab): LinearLayout {
return when (tab) {
Tab.EMOJI -> EmojiKeyboardView(florisboard)
Tab.EMOTICON -> EmoticonKeyboardView(florisboard)
else -> LinearLayout(florisboard).apply {
Tab.EMOJI -> EmojiKeyboardView(florisboard.themeContext)
Tab.EMOTICON -> EmoticonKeyboardView(florisboard.themeContext)
else -> LinearLayout(florisboard.themeContext).apply {
addView(TextView(context).apply {
text = "not yet implemented"
})

View File

@@ -72,7 +72,7 @@ class MediaInputView : LinearLayout, FlorisBoard.EventListener,
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val height = florisboard?.inputView?.desiredMediaKeyboardViewHeight ?: 0.0f
val height = florisboard?.uiBinding?.inputView?.desiredMediaKeyboardViewHeight ?: 0.0f
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height.roundToInt(), MeasureSpec.EXACTLY))
}
}

View File

@@ -16,8 +16,6 @@
package dev.patrickgold.florisboard.ime.media.emoji
import java.util.*
/**
* Enum for emoji category.
* List taken from https://unicode.org/Public/emoji/13.0/emoji-test.txt
@@ -39,7 +37,7 @@ enum class EmojiCategory {
companion object {
fun fromString(string: String): EmojiCategory {
return valueOf(string.replace(" & ", "_").toUpperCase(Locale.ENGLISH))
return valueOf(string.replace(" & ", "_").uppercase())
}
}
}

View File

@@ -23,27 +23,23 @@ import android.os.Handler
import android.util.TypedValue
import android.view.Gravity
import android.view.MotionEvent
import android.widget.ScrollView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
/**
* View class for managing the rendering and the events of a single emoji keyboard key.
*
* @property florisboard Reference to instance of core class [FlorisBoard].
* @property emojiKeyboardView Reference to the parent [EmojiKeyboardView].
* @property data The data the current key represents. Is used to determine rendering and possible
* behaviour when events occur.
* @property key The current key. Is used to determine rendering and possible behaviour when events occur.
*/
@SuppressLint("ViewConstructor")
class EmojiKeyView(
@@ -52,7 +48,7 @@ class EmojiKeyView(
) : androidx.appcompat.widget.AppCompatTextView(emojiKeyboardView.context), CoroutineScope by MainScope(),
FlorisBoard.EventListener, ThemeManager.OnThemeUpdatedListener {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val prefs get() = Preferences.default()
private var isCancelled: Boolean = false
private var osHandler: Handler? = null
@@ -107,8 +103,8 @@ class EmojiKeyView(
(parent as RecyclerView)
.requestDisallowInterceptTouchEvent(true)
emojiKeyboardView.isScrollBlocked = true
emojiKeyboardView.popupManager.show(key, KeyHintMode.DISABLED)
emojiKeyboardView.popupManager.extend(key, KeyHintMode.DISABLED)
emojiKeyboardView.popupManager.show(key, KeyHintConfiguration.HINTS_DISABLED)
emojiKeyboardView.popupManager.extend(key, KeyHintConfiguration.HINTS_DISABLED)
florisboard?.keyPressVibrate()
florisboard?.keyPressSound()
}, delayMillis.toLong())
@@ -177,7 +173,7 @@ class EmojiKeyView(
canvas ?: return
if (key.computedPopups.isNotEmpty()) {
if (key.computedPopups.getPopupKeys(KeyHintConfiguration.HINTS_DISABLED).isNotEmpty()) {
triangleDrawable?.draw(canvas)
}
}

View File

@@ -106,7 +106,7 @@ fun parseRawEmojiSpecsFile(
if (line.startsWith(GROUP_IDENTIFIER, true)) {
// A new group begins
val rawGroupName = line.trim().substring(GROUP_IDENTIFIER.length)
if (rawGroupName.toUpperCase(Locale.ENGLISH) == "COMPONENT") {
if (rawGroupName.uppercase() == "COMPONENT") {
skipUntilNextGroup = true
continue
} else {
@@ -130,7 +130,7 @@ fun parseRawEmojiSpecsFile(
val dataC = data2[0].trim()
val dataQ = data2[1].trim()
val dataN = data[1].split(NAME_JUNK_SPLIT_REGEX)[1]
if (dataQ.toLowerCase(Locale.ENGLISH) == FULLY_QUALIFIED) {
if (dataQ.lowercase() == FULLY_QUALIFIED) {
// Only fully-qualified emojis are accepted
val dataCPs = dataC.split(" ")
val key = EmojiKeyData(listStringToListInt(dataCPs), dataN)

View File

@@ -16,12 +16,15 @@
package dev.patrickgold.florisboard.ime.media.emoticon
import kotlinx.serialization.Serializable
/**
* Data class for a single emoticon.
*
* @property icon The char sequence of the emoticon.
* @property meaning List of possible meanings for this emoticon.
*/
@Serializable
data class EmoticonKeyData(
var icon: String = "",
var meaning: List<String> = listOf()

View File

@@ -36,7 +36,7 @@ class EmoticonKeyboardView : LinearLayout {
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
layout = mainScope.async(Dispatchers.IO) {
EmoticonLayoutData.fromJsonFile(context, "ime/media/emoticon/emoticons.json")
EmoticonLayoutData.fromJsonFile("ime/media/emoticon/emoticons.json")
}
orientation = VERTICAL
}

View File

@@ -16,9 +16,14 @@
package dev.patrickgold.florisboard.ime.media.emoticon
import android.content.Context
import dev.patrickgold.florisboard.ime.extension.AssetManager
import dev.patrickgold.florisboard.ime.extension.AssetRef
import dev.patrickgold.florisboard.ime.extension.AssetSource
import kotlinx.serialization.Serializable
typealias EmoticonLayoutDataArrangement = List<List<EmoticonKeyData>>
@Serializable
data class EmoticonLayoutData(
var type: String,
var name: String,
@@ -26,18 +31,10 @@ data class EmoticonLayoutData(
var arrangement: EmoticonLayoutDataArrangement = listOf()
) {
companion object {
fun fromJsonFile(context: Context, path: String): EmoticonLayoutData? {
/*val rawJsonData: String = try {
context.assets.open(path).bufferedReader().use { it.readText() }
} catch (e: Exception) {
null
} ?: return null
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val layoutAdapter = moshi.adapter(EmoticonLayoutData::class.java)
return layoutAdapter.fromJson(rawJsonData)*/
return null
fun fromJsonFile(path: String): EmoticonLayoutData? {
return AssetManager.defaultOrNull()
?.loadJsonAsset<EmoticonLayoutData>(AssetRef(AssetSource.Assets, path))
?.getOrNull()
}
}
}

View File

@@ -1,294 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.nlp
/**
* Represents the root node to a n-gram tree.
*/
open class NgramTree(
sameOrderChildren: MutableList<NgramNode> = mutableListOf(),
higherOrderChildren: MutableList<NgramNode> = mutableListOf()
) : NgramNode(0, '?', -1, sameOrderChildren, higherOrderChildren)
/**
* A node of a n-gram tree, which holds the character it represents, the corresponding frequency,
* a pre-computed string representing all parent characters and the current one as well as child
* nodes, one for the same order n-gram nodes and one for the higher order n-gram nodes.
*/
open class NgramNode(
val order: Int,
val char: Char,
val freq: Int,
val sameOrderChildren: MutableList<NgramNode> = mutableListOf(),
val higherOrderChildren: MutableList<NgramNode> = mutableListOf()
) {
companion object {
const val FREQ_CHARACTER = -1
const val FREQ_WORD_MIN = 0
const val FREQ_WORD_MAX = 255
const val FREQ_WORD_FILLER = -2
const val FREQ_IS_POSSIBLY_OFFENSIVE = 0
}
val isCharacter: Boolean
get() = freq == FREQ_CHARACTER
val isWord: Boolean
get() = freq in FREQ_WORD_MIN..FREQ_WORD_MAX
val isWordFiller: Boolean
get() = freq == FREQ_WORD_FILLER
val isPossiblyOffensive: Boolean
get() = freq == FREQ_IS_POSSIBLY_OFFENSIVE
fun findWord(word: String): NgramNode? {
var currentNode = this
for ((pos, char) in word.withIndex()) {
val childNode = if (pos == 0) {
currentNode.higherOrderChildren.find { it.char == char }
} else {
currentNode.sameOrderChildren.find { it.char == char }
}
if (childNode != null) {
currentNode = childNode
} else {
return null
}
}
return if (currentNode.isWord || currentNode.isWordFiller) {
currentNode
} else {
null
}
}
/**
* This function allows to search for a given [input] word with a given [maxEditDistance] and
* adds all matches in the trie to the [list].
*/
fun listSimilarWords(
input: String,
list: StagedSuggestionList<String, Int>,
word: StringBuilder,
allowPossiblyOffensive: Boolean,
maxEditDistance: Int,
deletionCost: Int = 0,
insertionCost: Int = 0,
substitutionCost: Int = 0,
pos: Int = -1
) {
if (pos > -1) {
word.append(char)
}
val costSum = deletionCost + insertionCost + substitutionCost
if (pos > -1 && (pos + 1 == input.length) && isWord && ((isPossiblyOffensive && allowPossiblyOffensive)
|| !isPossiblyOffensive)) {
// Using shift right instead of divide by 2^(costSum) as it is mathematically the
// same but faster.
if (list.canAdd(freq shr costSum)) {
list.add(word.toString(), freq shr costSum)
}
}
if (pos <= -1) {
for (childNode in higherOrderChildren) {
childNode.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance, 0, 0, 0, 0
)
}
} else if (maxEditDistance == costSum) {
if (pos + 1 < input.length) {
sameOrderChildren.find { it.char == input[pos + 1] }?.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost, insertionCost, substitutionCost, pos + 1
)
}
} else {
// Delete
if (pos + 2 < input.length) {
sameOrderChildren.find { it.char == input[pos + 2] }?.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost + 1, insertionCost, substitutionCost, pos + 2
)
}
for (childNode in sameOrderChildren) {
if (pos + 1 < input.length && childNode.char == input[pos + 1]) {
childNode.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost, insertionCost, substitutionCost, pos + 1
)
} else {
// Insert
childNode.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost, insertionCost + 1, substitutionCost, pos
)
if (pos + 1 < input.length) {
// Substitute
childNode.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost, insertionCost, substitutionCost + 1, pos + 1
)
}
}
}
}
if (pos > -1) {
word.deleteAt(word.lastIndex)
}
}
fun listAllSameOrderWords(list: StagedSuggestionList<String, Int>, word: StringBuilder, allowPossiblyOffensive: Boolean) {
word.append(char)
if (isWord && ((isPossiblyOffensive && allowPossiblyOffensive) || !isPossiblyOffensive)) {
if (list.canAdd(freq)) {
list.add(word.toString(), freq)
}
}
for (childNode in sameOrderChildren) {
childNode.listAllSameOrderWords(list, word, allowPossiblyOffensive)
}
word.deleteAt(word.lastIndex)
}
}
open class FlorisLanguageModel(
initTreeObj: NgramTree? = null
) : LanguageModel<String, Int> {
protected val ngramTree: NgramTree = initTreeObj ?: NgramTree()
override fun getNgram(vararg tokens: String): Ngram<String, Int> {
val ngramOut = getNgramOrNull(*tokens)
if (ngramOut != null) {
return ngramOut
} else {
throw NullPointerException("No n-gram found matching the given tokens: $tokens")
}
}
override fun getNgram(ngram: Ngram<String, Int>): Ngram<String, Int> {
val ngramOut = getNgramOrNull(ngram)
if (ngramOut != null) {
return ngramOut
} else {
throw NullPointerException("No n-gram found matching the given ngram: $ngram")
}
}
override fun getNgramOrNull(vararg tokens: String): Ngram<String, Int>? {
var currentNode: NgramNode = ngramTree
for (token in tokens) {
val childNode = currentNode.findWord(token)
if (childNode != null) {
currentNode = childNode
} else {
return null
}
}
return Ngram(tokens.toList().map { Token(it) }, currentNode.freq)
}
override fun getNgramOrNull(ngram: Ngram<String, Int>): Ngram<String, Int>? {
return getNgramOrNull(*ngram.tokens.toStringList().toTypedArray())
}
override fun hasNgram(ngram: Ngram<String, Int>, doMatchFreq: Boolean): Boolean {
val result = getNgramOrNull(ngram)
return if (result != null) {
if (doMatchFreq) {
ngram.freq == result.freq
} else {
true
}
} else {
false
}
}
override fun matchAllNgrams(
ngram: Ngram<String, Int>,
maxEditDistance: Int,
maxTokenCount: Int,
allowPossiblyOffensive: Boolean
): List<WeightedToken<String, Int>> {
val ngramList = mutableListOf<WeightedToken<String, Int>>()
var currentNode: NgramNode = ngramTree
for ((t, token) in ngram.tokens.withIndex()) {
val word = token.data
if (t + 1 >= ngram.tokens.size) {
if (word.isNotEmpty()) {
// The last word is not complete, so find all possible words and sort
val splitWord = mutableListOf<Char>()
var splitNode: NgramNode? = currentNode
for ((pos, char) in word.withIndex()) {
val node = if (pos == 0) {
splitNode?.higherOrderChildren?.find { it.char == char }
} else {
splitNode?.sameOrderChildren?.find { it.char == char }
}
splitWord.add(char)
splitNode = node
if (node == null) {
break
}
}
if (splitNode != null) {
// Input thus far is valid
val wordNodes = StagedSuggestionList<String, Int>(maxTokenCount)
val strBuilder = StringBuilder().append(word.substring(0, word.length - 1))
splitNode.listAllSameOrderWords(wordNodes, strBuilder, allowPossiblyOffensive)
ngramList.addAll(wordNodes)
}
if (ngramList.size < maxTokenCount) {
val wordNodes = StagedSuggestionList<String, Int>(maxTokenCount)
val strBuilder = StringBuilder()
currentNode.listSimilarWords(word, wordNodes, strBuilder, allowPossiblyOffensive, maxEditDistance)
ngramList.addAll(wordNodes)
}
}
} else {
val node = currentNode.findWord(word)
if (node == null) {
return ngramList
} else {
currentNode = node
}
}
}
return ngramList
}
fun toFlorisMutableLanguageModel(): FlorisMutableLanguageModel = FlorisMutableLanguageModel(ngramTree)
}
open class FlorisMutableLanguageModel(
initTreeObj: NgramTree? = null
) : MutableLanguageModel<String, Int>, FlorisLanguageModel(initTreeObj) {
override fun deleteNgram(ngram: Ngram<String, Int>) {
TODO("Not yet implemented")
}
override fun insertNgram(ngram: Ngram<String, Int>) {
TODO("Not yet implemented")
}
override fun updateNgram(ngram: Ngram<String, Int>) {
TODO("Not yet implemented")
}
fun toFlorisLanguageModel(): FlorisLanguageModel = FlorisLanguageModel(ngramTree)
}

View File

@@ -1,74 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.nlp
/**
* Abstract interface for a language model. Can house any n-grams with a minimum order of one.
*/
interface LanguageModel<T : Any, F : Comparable<F>> {
/**
* Tries to get the n-gram for the passed [tokens]. Throws a NPE if no match could be found.
*/
@Throws(NullPointerException::class)
fun getNgram(vararg tokens: T): Ngram<T, F>
/**
* Tries to get the n-gram for the passed [ngram], whereas the frequency is ignored while
* searching. Throws a NPE if no match could be found.
*/
@Throws(NullPointerException::class)
fun getNgram(ngram: Ngram<T, F>): Ngram<T, F>
/**
* Tries to get the n-gram for the passed [tokens]. Returns null if no match could be found.
*/
fun getNgramOrNull(vararg tokens: T): Ngram<T, F>?
/**
* Tries to get the n-gram for the passed [ngram], whereas the frequency is ignored while
* searching. Returns null if no match could be found.
*/
fun getNgramOrNull(ngram: Ngram<T, F>): Ngram<T, F>?
/**
* Checks if a given [ngram] exists within this model. If [doMatchFreq] is set to true, the
* frequency is also matched.
*/
fun hasNgram(ngram: Ngram<T, F>, doMatchFreq: Boolean = false): Boolean
/**
* Matches all n-grams which match the given [ngram], whereas the last item in the n-gram is
* is used to search for predictions.
*/
fun matchAllNgrams(
ngram: Ngram<T, F>,
maxEditDistance: Int,
maxTokenCount: Int,
allowPossiblyOffensive: Boolean
): List<WeightedToken<T, F>>
}
/**
* Mutable version of [LanguageModel].
*/
interface MutableLanguageModel<T : Any, F : Comparable<F>> : LanguageModel<T, F> {
fun deleteNgram(ngram: Ngram<T, F>)
fun insertNgram(ngram: Ngram<T, F>)
fun updateNgram(ngram: Ngram<T, F>)
}

View File

@@ -1,129 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.nlp
/**
* Abstract interface representing a n-gram of tokens. Each n-gram instance can be assigned a
* unique frequency [freq].
*/
open class Ngram<T : Any, F : Comparable<F>>(_tokens: List<Token<T>>, _freq: F) {
companion object {
/** Constant order value for unigrams. */
const val ORDER_UNIGRAM: Int = 1
/** Constant order value for bigrams. */
const val ORDER_BIGRAM: Int = 2
/** Constant order value for trigrams. */
const val ORDER_TRIGRAM: Int = 3
}
init {
if (_tokens.size < ORDER_UNIGRAM) {
throw Exception("A n-gram must contain at least 1 token!")
}
}
/**
* A list of tokens for this n-gram. The length of this list is guaranteed to be matching
* [order].
*/
val tokens: List<Token<T>> = _tokens
/**
* The frequency value of this n-gram.
*/
val freq: F = _freq
/**
* The order of this n-gram (1, 2, 3, ...).
*/
val order: Int
get() = tokens.size
}
/**
* Abstract interface representing a token used in [Ngram].
*/
open class Token<T : Any>(_data: T) {
/**
* The data of this token.
*/
val data: T = _data
override fun toString(): String {
return "Token(\"$data\")"
}
override fun hashCode(): Int {
return data.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Token<*>
if (data != other.data) return false
return true
}
}
/**
* Same as [Token] but allows to add a frequency value [freq].
*/
open class WeightedToken<T : Any, F : Comparable<F>>(_data: T, _freq: F) : Token<T>(_data) {
/**
* The frequency of this weighed token.
*/
val freq: F = _freq
override fun toString(): String {
return "WeightedToken(\"$data\", $freq)"
}
override fun hashCode(): Int {
return data.hashCode() + 31 * freq.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as WeightedToken<*, *>
if (data != other.data || freq != other.freq) return false
return true
}
}
/**
* Converts a list of tokens carrying [CharSequence] data to a list of [CharSequence].
*/
fun List<Token<CharSequence>>.toCharSequenceList(): List<CharSequence> {
return this.map { it.data }
}
/**
* Converts a list of tokens carrying [String] data to a list of [String].
*/
fun List<Token<String>>.toStringList(): List<String> {
return this.map { it.data }
}

View File

@@ -1,90 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.nlp
class StagedSuggestionList<T : Any, F : Comparable<F>>(
private val maxSize: Int
) : Collection<WeightedToken<T, F>> {
private val internalArray: Array<WeightedToken<T, F>?> = Array(maxSize) { null }
private var internalSize: Int = 0
override val size: Int
get() = internalSize
fun add(token: T, freq: F): Boolean {
if (internalSize < maxSize) {
internalArray[internalSize++] = WeightedToken(token, freq)
internalArray.sortByDescending { it?.freq }
return true
} else {
if (internalArray.last()!!.freq < freq) {
internalArray[internalArray.lastIndex] = WeightedToken(token, freq)
internalArray.sortByDescending { it?.freq }
return true
}
return false
}
}
fun canAdd(freq: F): Boolean {
return internalSize < maxSize || internalArray.last()!!.freq < freq
}
fun clear() {
for (n in internalArray.indices) {
internalArray[n] = null
}
internalSize = 0
}
override fun contains(element: WeightedToken<T, F>): Boolean = internalArray.contains(element)
override fun containsAll(elements: Collection<WeightedToken<T, F>>): Boolean {
elements.forEach { if (!contains(it)) return false }
return true
}
@Throws(IndexOutOfBoundsException::class)
operator fun get(index: Int): WeightedToken<T, F> {
val element = getOrNull(index)
if (element == null) {
throw IndexOutOfBoundsException("The specified index $index is not within the bounds of this list!")
} else {
return element
}
}
fun getOrNull(index: Int): WeightedToken<T, F>? {
return internalArray.getOrNull(index)
}
override fun isEmpty(): Boolean = internalSize <= 0
override fun iterator(): Iterator<WeightedToken<T, F>> {
return StagedIterator(this)
}
class StagedIterator<T : Any, F : Comparable<F>> internal constructor (
private val stagedSuggestionList: StagedSuggestionList<T, F>
) : Iterator<WeightedToken<T, F>> {
var index = 0
override fun next(): WeightedToken<T, F> = stagedSuggestionList[index++]
override fun hasNext(): Boolean = stagedSuggestionList.getOrNull(index) != null
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.nlp
import dev.patrickgold.florisboard.common.NativeInstanceWrapper
import dev.patrickgold.florisboard.common.NativePtr
@JvmInline
value class SuggestionList private constructor(
private val _nativePtr: NativePtr
) : Collection<String>, NativeInstanceWrapper {
companion object {
fun new(maxSize: Int): SuggestionList {
val nativePtr = nativeInitialize(maxSize)
return SuggestionList(nativePtr)
}
external fun nativeInitialize(maxSize: Int): NativePtr
external fun nativeDispose(nativePtr: NativePtr)
external fun nativeAdd(nativePtr: NativePtr, word: Word, freq: Freq): Boolean
external fun nativeClear(nativePtr: NativePtr)
external fun nativeContains(nativePtr: NativePtr, element: Word): Boolean
external fun nativeGetOrNull(nativePtr: NativePtr, index: Int): Word?
external fun nativeGetIsPrimaryTokenAutoInsert(nativePtr: NativePtr): Boolean
external fun nativeSetIsPrimaryTokenAutoInsert(nativePtr: NativePtr, v: Boolean)
external fun nativeSize(nativePtr: NativePtr): Int
}
override val size: Int
get() = nativeSize(_nativePtr)
fun add(word: Word, freq: Freq): Boolean {
return nativeAdd(_nativePtr, word, freq)
}
fun clear() {
nativeClear(_nativePtr)
}
override fun contains(element: Word): Boolean {
return nativeContains(_nativePtr, element)
}
override fun containsAll(elements: Collection<Word>): Boolean {
elements.forEach { if (!contains(it)) return false }
return true
}
@Throws(IndexOutOfBoundsException::class)
operator fun get(index: Int): Word {
val element = getOrNull(index)
if (element == null) {
throw IndexOutOfBoundsException("The specified index $index is not within the bounds of this list!")
} else {
return element
}
}
fun getOrNull(index: Int): Word? {
return nativeGetOrNull(_nativePtr, index)
}
override fun isEmpty(): Boolean = size <= 0
val isPrimaryTokenAutoInsert: Boolean
get() = nativeGetIsPrimaryTokenAutoInsert(_nativePtr)
override fun iterator(): Iterator<Word> {
return SuggestionListIterator(this)
}
override fun nativePtr(): NativePtr {
return _nativePtr
}
override fun dispose() {
nativeDispose(_nativePtr)
}
class SuggestionListIterator internal constructor (
private val suggestionList: SuggestionList
) : Iterator<Word> {
var index = 0
override fun next(): Word = suggestionList[index++]
override fun hasNext(): Boolean = suggestionList.getOrNull(index) != null
}
}

View File

@@ -16,9 +16,7 @@
package dev.patrickgold.florisboard.ime.nlp
class TextProcessor {
data class Word(
val word: String,
val isPossiblyOffensive: Boolean = false
)
}
typealias Word = String
typealias Freq = Int
const val NATIVE_NULLPTR = 0

View File

@@ -18,18 +18,21 @@ package dev.patrickgold.florisboard.ime.onehanded
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Resources
import android.util.AttributeSet
import android.view.Gravity
import android.widget.ImageButton
import android.widget.LinearLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
class OneHandedPanel : LinearLayout, ThemeManager.OnThemeUpdatedListener {
private var florisboard: FlorisBoard? = null
private var themeManager: ThemeManager? = null
private val prefs get() = Preferences.default()
private var closeBtn: ImageButton? = null
private var moveBtn: ImageButton? = null
@@ -55,14 +58,14 @@ class OneHandedPanel : LinearLayout, ThemeManager.OnThemeUpdatedListener {
closeBtn = findViewWithTag("one_handed_ctrl_close")
closeBtn?.setOnClickListener {
florisboard?.let {
it.prefs.keyboard.oneHandedMode = OneHandedMode.OFF
prefs.keyboard.oneHandedMode = OneHandedMode.OFF
it.updateOneHandedPanelVisibility()
}
}
moveBtn = findViewWithTag("one_handed_ctrl_move")
moveBtn?.setOnClickListener {
florisboard?.let {
it.prefs.keyboard.oneHandedMode = panelSide
prefs.keyboard.oneHandedMode = panelSide
it.updateOneHandedPanelVisibility()
}
}
@@ -94,9 +97,8 @@ class OneHandedPanel : LinearLayout, ThemeManager.OnThemeUpdatedListener {
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val florisboard = florisboard ?: return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = (florisboard.inputView?.measuredWidth ?: 0) *
((100 - florisboard.prefs.keyboard.oneHandedModeScaleFactor) / 100.0f)
val width = (Resources.getSystem().displayMetrics.widthPixels) *
((100 - prefs.keyboard.oneHandedModeScaleFactor) / 100.0f)
super.onMeasure(MeasureSpec.makeMeasureSpec(width.toInt(), MeasureSpec.EXACTLY), heightMeasureSpec)
}
}

View File

@@ -30,17 +30,17 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.keyboard.Key
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import dev.patrickgold.florisboard.common.ViewUtils
import kotlin.math.min
class PopupExtendedView : View, ThemeManager.OnThemeUpdatedListener {
private val themeManager: ThemeManager = ThemeManager.default()
private val activeBackgroundDrawable: PaintDrawable = PaintDrawable().apply {
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
setCornerRadius(ViewUtils.dp2px(6.0f))
}
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
setCornerRadius(ViewUtils.dp2px(6.0f))
}
private val labelPaint: Paint = Paint().apply {
alpha = 255
@@ -86,7 +86,7 @@ class PopupExtendedView : View, ThemeManager.OnThemeUpdatedListener {
layoutDirection = LAYOUT_DIRECTION_LTR
visibility = GONE
background = backgroundDrawable
elevation = ViewLayoutUtils.convertDpToPixel(4.0f, context)
elevation = ViewUtils.dp2px(4.0f)
}
override fun onAttachedToWindow() {

View File

@@ -45,6 +45,7 @@ class PopupLayerView : FrameLayout {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
)
setWillNotDraw(true)
}
var clipboardPopupManager: ClipboardPopupManager? = null

View File

@@ -82,16 +82,19 @@ class PopupManager<V : View>(
* Helper function to create a element for the extended popup and preconfigure it.
*
* @param key Reference to the key currently controlling the popup.
* @param keyHintConfiguration The key hint configuration to use.
* @param adjustedIndex The index of the key in the key data popup array.
* @return A preconfigured extended popup element.
*/
private fun createElement(
key: Key,
keyHintConfiguration: KeyHintConfiguration,
adjustedIndex: Int
): PopupExtendedView.Element {
return when (key) {
is TextKey -> {
when (key.computedPopups[adjustedIndex].code) {
val popupKey = key.computedPopups.getPopupKeys(keyHintConfiguration)[adjustedIndex]
when (popupKey.code) {
KeyCode.SETTINGS -> {
getDrawable(keyboardView.context, R.drawable.ic_settings)?.let {
PopupExtendedView.Element.Icon(it, adjustedIndex)
@@ -114,7 +117,7 @@ class PopupManager<V : View>(
}
KeyCode.URI_COMPONENT_TLD -> {
PopupExtendedView.Element.Tld(
key.computedPopups[adjustedIndex].asString(isForDisplay = true), adjustedIndex
popupKey.asString(isForDisplay = true), adjustedIndex
)
}
KeyCode.TOGGLE_ONE_HANDED_MODE_LEFT,
@@ -125,14 +128,15 @@ class PopupManager<V : View>(
}
else -> {
PopupExtendedView.Element.Label(
key.computedPopups[adjustedIndex].asString(isForDisplay = true), adjustedIndex
popupKey.asString(isForDisplay = true), adjustedIndex
)
}
}
}
is EmojiKey -> {
val popupKey = key.computedPopups.getPopupKeys(keyHintConfiguration)[adjustedIndex]
PopupExtendedView.Element.Label(
key.computedPopups[adjustedIndex].asString(isForDisplay = true), adjustedIndex
popupKey.asString(isForDisplay = true), adjustedIndex
)
}
else -> {
@@ -141,6 +145,28 @@ class PopupManager<V : View>(
}
}
fun isSuitableForPopups(key: Key): Boolean {
return isSuitableForBasicPopup(key) || isSuitableForExtendedPopup(key)
}
private fun isSuitableForBasicPopup(key: Key): Boolean {
return if (key is TextKey) {
val c = key.computedData.code
c > KeyCode.SPACE && c != KeyCode.MULTIPLE_CODE_POINTS
} else {
true
}
}
private fun isSuitableForExtendedPopup(key: Key): Boolean {
return if (key is TextKey) {
val c = key.computedData.code
c > KeyCode.SPACE && c != KeyCode.MULTIPLE_CODE_POINTS || exceptionsForKeyCodes.contains(c)
} else {
true
}
}
/**
* Calculates all attributes required by both the normal and the extended popup, regardless of
* the passed [key]'s code.
@@ -179,11 +205,10 @@ class PopupManager<V : View>(
* key code is equal to or less than [KeyCode.SPACE].
*
* @param key Reference to the key currently controlling the popup.
* @param keyHintConfiguration The key hint configuration to use.
*/
fun show(key: Key, keyHintMode: KeyHintMode) {
if (key is TextKey && key.computedData.code <= KeyCode.SPACE && key.computedData.code != KeyCode.MULTIPLE_CODE_POINTS) {
return
}
fun show(key: Key, keyHintConfiguration: KeyHintConfiguration) {
if (!isSuitableForBasicPopup(key)) return
calc(key)
@@ -200,8 +225,8 @@ class PopupManager<V : View>(
}
labelTextSize = keyPopupTextSize
shouldIndicateExtendedPopups = when (key) {
is TextKey -> key.computedPopups.size(keyHintMode) > 0
is EmojiKey -> key.computedPopups.isNotEmpty()
is TextKey -> key.computedPopups.getPopupKeys(keyHintConfiguration).isNotEmpty()
is EmojiKey -> key.computedPopups.getPopupKeys(keyHintConfiguration).isNotEmpty()
else -> false
}
}
@@ -228,12 +253,10 @@ class PopupManager<V : View>(
* K K ... K K
*
* @param key Reference to the key currently controlling the popup.
* @param keyHintConfiguration The key hint configuration to use.
*/
fun extend(key: Key, keyHintMode: KeyHintMode) {
if (key is TextKey && key.computedData.code <= KeyCode.SPACE && key.computedData.code != KeyCode.MULTIPLE_CODE_POINTS
&& !exceptionsForKeyCodes.contains(key.computedData.code)) {
return
}
fun extend(key: Key, keyHintConfiguration: KeyHintConfiguration) {
if (!isSuitableForExtendedPopup(key)) return
if (!isShowingPopup) {
calc(key)
@@ -245,8 +268,8 @@ class PopupManager<V : View>(
// Determine key counts for each row
val n = when (key) {
is TextKey -> key.computedPopups.size(keyHintMode)
is EmojiKey -> key.computedPopups.size(keyHintMode)
is TextKey -> key.computedPopups.getPopupKeys(keyHintConfiguration).size
is EmojiKey -> key.computedPopups.getPopupKeys(keyHintConfiguration).size
else -> 0
}
when {
@@ -303,43 +326,38 @@ class PopupManager<V : View>(
val uiIndices = IntRange(0, (n - 1).coerceAtLeast(0))
if (key is TextKey) {
popupIndices = IntArray(n) { 0 }
when (keyHintMode) {
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when {
key.computedPopups.main != null -> {
popupIndices[initUiIndex] = PopupSet.MAIN_INDEX
if (key.computedPopups.hint != null) when {
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupSet.HINT_INDEX
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupSet.HINT_INDEX
}
}
key.computedPopups.hint != null -> when {
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupSet.HINT_INDEX
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupSet.HINT_INDEX
else -> popupIndices[initUiIndex] = PopupSet.HINT_INDEX
val popupKeys = key.computedPopups.getPopupKeys(keyHintConfiguration)
when (popupKeys.prioritized.size) {
// only one key: use initial position
1 -> {
popupIndices[initUiIndex] = PopupKeys.FIRST_PRIORITIZED
}
// two keys: use initial position and one to the right if available, otherwise one to the left
2 -> {
popupIndices[initUiIndex] = PopupKeys.FIRST_PRIORITIZED
when {
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupKeys.SECOND_PRIORITIZED
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupKeys.SECOND_PRIORITIZED
}
}
KeyHintMode.ENABLED_HINT_PRIORITY -> when {
key.computedPopups.hint != null -> {
popupIndices[initUiIndex] = PopupSet.HINT_INDEX
if (key.computedPopups.main != null) when {
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupSet.MAIN_INDEX
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupSet.MAIN_INDEX
// two keys: use initial position and one to either sides if available
// otherwise two to the right or two to the left with decreasing priority
3 -> {
popupIndices[initUiIndex] = PopupKeys.FIRST_PRIORITIZED
when {
initUiIndex + 1 < n && initUiIndex - 1 >= 0 -> {
popupIndices[initUiIndex + 1] = PopupKeys.SECOND_PRIORITIZED
popupIndices[initUiIndex - 1] = PopupKeys.THIRD_PRIORITIZED
}
initUiIndex + 2 < n -> {
popupIndices[initUiIndex + 1] = PopupKeys.SECOND_PRIORITIZED
popupIndices[initUiIndex + 2] = PopupKeys.THIRD_PRIORITIZED
}
initUiIndex - 2 >= 0 -> {
popupIndices[initUiIndex - 1] = PopupKeys.SECOND_PRIORITIZED
popupIndices[initUiIndex - 2] = PopupKeys.THIRD_PRIORITIZED
}
}
key.computedPopups.main != null -> popupIndices[initUiIndex] = PopupSet.MAIN_INDEX
}
KeyHintMode.ENABLED_SMART_PRIORITY -> when {
key.computedPopups.main != null -> {
popupIndices[initUiIndex] = PopupSet.MAIN_INDEX
if (key.computedPopups.hint != null) when {
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupSet.HINT_INDEX
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupSet.HINT_INDEX
}
}
key.computedPopups.hint != null -> popupIndices[initUiIndex] = PopupSet.HINT_INDEX
}
KeyHintMode.DISABLED -> when {
key.computedPopups.main != null -> popupIndices[initUiIndex] = PopupSet.MAIN_INDEX
}
}
var offset = 0
@@ -360,7 +378,7 @@ class PopupManager<V : View>(
for (uiIndex in uiIndices) {
val rowIndex = if (uiIndex < row1count && row1count > 0) { 1 } else { 0 }
popupViewExt.properties.elements[rowIndex].add(
createElement(key, popupIndices[uiIndex])
createElement(key, keyHintConfiguration, popupIndices[uiIndex])
)
}
@@ -417,7 +435,7 @@ class PopupManager<V : View>(
return false
}
popupViewExt.properties.activeElementIndex = when {
val newActiveElementIndex = when {
anchorLeft -> when {
// check if out of boundary on x-axis
x < keyPopupDiffX - (anchorOffset + 1) * keyPopupWidth ||
@@ -462,23 +480,27 @@ class PopupManager<V : View>(
}
else -> -1
}
popupViewExt.invalidate()
if (newActiveElementIndex != popupViewExt.properties.activeElementIndex) {
popupViewExt.properties.activeElementIndex = newActiveElementIndex
popupViewExt.invalidate()
}
return true
}
/**
* Gets the [KeyData] of the currently active key. May be either the key of the popup preview
* Gets the [TextKeyData] of the currently active key. May be either the key of the popup preview
* or one of the keys in extended popup, if shown. Returns null if [key] is not a subclass of [TextKey].
*
* @param key Reference to the key currently controlling the popup.
* @return The [KeyData] object of the currently active key or null.
* @param keyHintConfiguration The key hint configuration to be used.
* @return The [TextKeyData] object of the currently active key or null.
*/
fun getActiveKeyData(key: Key): TextKeyData? {
fun getActiveKeyData(key: Key, keyHintConfiguration: KeyHintConfiguration): TextKeyData? {
return if (key is TextKey) {
val element = popupViewExt.properties.getElementOrNull()
if (element != null) {
key.computedPopups.getOrNull(element.adjustedIndex) ?: key.computedData
key.computedPopups.getPopupKeys(keyHintConfiguration).getOrNull(element.adjustedIndex) ?: key.computedData
} else {
key.computedData
}
@@ -498,7 +520,7 @@ class PopupManager<V : View>(
return if (key is EmojiKey) {
val element = popupViewExt.properties.getElementOrNull()
if (element != null) {
key.computedPopups.getOrNull(element.adjustedIndex) ?: key.computedData
key.computedPopups.getPopupKeys(KeyHintConfiguration.HINTS_DISABLED).getOrNull(element.adjustedIndex) ?: key.computedData
} else {
key.computedData
}

View File

@@ -17,43 +17,215 @@
package dev.patrickgold.florisboard.ime.popup
import dev.patrickgold.florisboard.ime.keyboard.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.text.keyboard.BasicTextKeyData
import dev.patrickgold.florisboard.ime.text.keyboard.TextComputingEvaluator
import kotlinx.serialization.Serializable
/**
* A popup set for a single key. This set describes, if the key has a [hint] character,
* a [main] character and other [relevant] popups.
* A popup set for a single key. This set describes, if the key has a [main] character and other [relevant] popups.
*
* Note, that a hint character should **never** be set in a json extended popup file, rather it
* Note that a hint character cannot and should not be set in a json extended popup file, rather it
* should only be dynamically set by the LayoutManager.
*
* The order in which these defined popups will be shown depends on the current [KeyHintMode],
* al well as the calculations made by the KeyPopupManager.
*
* The popup set can be accessed like an array with the addition that negative indexes defined
* within this companion object are allowed (as long as the corresponding [hint] or [main]
* character is *not* null).
* The order in which these defined popups will be shown depends on the current [KeyHintConfiguration].
*/
@Serializable
open class PopupSet<T : KeyData>(
open val hint: T? = null,
open val main: T? = null,
open val relevant: List<T> = listOf()
) {
private val popupKeys: PopupKeys<T> by lazy {
PopupKeys(null, listOfNotNull(main), relevant)
}
open fun getPopupKeys(keyHintConfiguration: KeyHintConfiguration): PopupKeys<T> {
return popupKeys
}
}
@Suppress("UNCHECKED_CAST")
class MutablePopupSet<T : KeyData>(
override var main: T? = null,
override val relevant: ArrayList<T> = arrayListOf(),
var symbolHint: T? = null,
var numberHint: T? = null,
private val symbolPopups: ArrayList<T> = arrayListOf(),
private val numberPopups: ArrayList<T> = arrayListOf(),
private val configCache: MutableMap<KeyHintConfiguration, PopupKeys<T>> = mutableMapOf()
) : PopupSet<T>(main, relevant) {
fun clear() {
symbolHint = null
numberHint = null
main = null
relevant.clear()
symbolPopups.clear()
numberPopups.clear()
configCache.clear()
}
override fun getPopupKeys(keyHintConfiguration: KeyHintConfiguration): PopupKeys<T> {
return configCache.getOrPut(keyHintConfiguration) {
initPopupList(keyHintConfiguration)
}
}
private fun initPopupList(keyHintConfiguration: KeyHintConfiguration): PopupKeys<T> {
val localMain = main
val localRelevant = relevant
val localSymbolHint = symbolHint
val localNumberHint = numberHint
if (localSymbolHint != null && keyHintConfiguration.symbolHintMode != KeyHintMode.DISABLED) {
if (localNumberHint != null && keyHintConfiguration.numberHintMode != KeyHintMode.DISABLED) {
val hintPopups = if (keyHintConfiguration.mergeHintPopups) { symbolPopups + numberPopups } else { listOf() }
return when (keyHintConfiguration.symbolHintMode) {
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when (keyHintConfiguration.numberHintMode) {
// when both hints are present in accent priority, always have a non-hint key first if possible
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when {
localMain != null -> PopupKeys(localSymbolHint, listOf(localMain, localSymbolHint, localNumberHint), localRelevant + hintPopups)
localRelevant.isNotEmpty() -> PopupKeys(localSymbolHint, listOf(localRelevant[0], localSymbolHint, localNumberHint), localRelevant.subList(1, localRelevant.size) + hintPopups)
else -> PopupKeys(localSymbolHint, listOf(localSymbolHint, localNumberHint), hintPopups)
}
// hint priority of number hint wins and overrules accent priority of symbol hint
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localNumberHint, localMain, localSymbolHint), localRelevant + hintPopups)
// due to smart priority of number hint, main wins if it exists, otherwise number hint overrules accent priority of symbol hint
else -> PopupKeys(localSymbolHint, listOfNotNull(localMain, localNumberHint, localSymbolHint), localRelevant + hintPopups)
}
KeyHintMode.ENABLED_HINT_PRIORITY -> when (keyHintConfiguration.symbolHintMode) {
// when both hints are present in hint priority, symbol hint wins
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localSymbolHint, localNumberHint, localMain), localRelevant + hintPopups)
// hint priority of symbol hint wins, and overrules potential accent priority of number hint
else -> PopupKeys(localSymbolHint, listOfNotNull(localSymbolHint, localMain, localNumberHint), localRelevant + hintPopups)
}
else -> when (keyHintConfiguration.numberHintMode) {
// smart priority of symbol hint wins, and overrules accent priority of number hint
KeyHintMode.ENABLED_ACCENT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localMain, localSymbolHint, localNumberHint), localRelevant + hintPopups)
// hint priority of number hint wins
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localNumberHint, localMain, localSymbolHint), localRelevant + hintPopups)
// when both hints are in smart priority, always have main first if possible
else -> PopupKeys(localSymbolHint, listOfNotNull(localMain, localSymbolHint, localNumberHint), localRelevant + hintPopups)
}
}
} else {
val hintPopups = if (keyHintConfiguration.mergeHintPopups) { symbolPopups } else { listOf() }
return when (keyHintConfiguration.symbolHintMode) {
// in accent priority, always show a non-hint key first if possible
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when {
localMain != null -> PopupKeys(localSymbolHint, listOf(localMain, localSymbolHint), localRelevant + hintPopups)
localRelevant.isNotEmpty() -> PopupKeys(localSymbolHint, listOf(localRelevant[0], localSymbolHint), localRelevant.subList(1, localRelevant.size) + hintPopups)
else -> PopupKeys(localSymbolHint, listOf(localSymbolHint), hintPopups)
}
// in hint priority, always show hint first
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localSymbolHint, localMain), localRelevant + hintPopups)
// in smart priority, show main first if possible
else -> PopupKeys(localSymbolHint, listOfNotNull(localMain, localSymbolHint), localRelevant + hintPopups)
}
}
} else if (localNumberHint != null && keyHintConfiguration.numberHintMode != KeyHintMode.DISABLED) {
val hintPopups = if (keyHintConfiguration.mergeHintPopups) { numberPopups } else { listOf() }
return when (keyHintConfiguration.numberHintMode) {
// in accent priority, always show a non-hint key first if possible
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when {
localMain != null -> PopupKeys(localNumberHint, listOf(localMain, localNumberHint), localRelevant + hintPopups)
localRelevant.isNotEmpty() -> PopupKeys(localNumberHint, listOf(localRelevant[0], localNumberHint), localRelevant.subList(1, localRelevant.size) + hintPopups)
else -> PopupKeys(localNumberHint, listOf(localNumberHint), hintPopups)
}
// in hint priority, always show hint first
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localNumberHint, listOfNotNull(localNumberHint, localMain), localRelevant + hintPopups)
// in smart priority, show main first if possible
else -> PopupKeys(localNumberHint, listOfNotNull(localMain, localNumberHint), localRelevant + hintPopups)
}
} else {
// if no hints shall be shown, use main first if possible
return PopupKeys(null, listOfNotNull(localMain), localRelevant)
}
}
fun merge(other: PopupSet<T>, evaluator: TextComputingEvaluator) {
mergeInternal(other, evaluator, relevant, true)
}
fun mergeSymbolHint(hintPopups: PopupSet<T>, evaluator: TextComputingEvaluator) {
mergeInternal(hintPopups, evaluator, symbolPopups)
}
fun mergeNumberHint(hintPopups: PopupSet<T>, evaluator: TextComputingEvaluator) {
mergeInternal(hintPopups, evaluator, numberPopups)
}
private fun mergeInternal(other: PopupSet<T>, evaluator: TextComputingEvaluator, targetList: MutableList<T>, useMain: Boolean = false) {
other.relevant.forEach {
val data = it.computeTextKeyData(evaluator) as? T
if (data != null) {
targetList.add(data)
}
}
other.main?.let {
val data = it.computeTextKeyData(evaluator) as? T
if (data != null) {
if (useMain && main == null) {
main = data
} else {
targetList.add(data)
}
}
}
}
}
/**
* A fully configured collection of popup keys. It contains a list of keys to be prioritized
* during rendering (ordered by relevance descending) by showing those keys close to the
* popup spawning point.
*
* The keys contain a separate [hint] key to ease rendering the hint label, but the hint, if
* present, also occurs in the [prioritized] list.
*
* The popup keys can be accessed like an array with the addition that negative indexes defined
* within this companion object are allowed (as long as the corresponding [prioritized] list
* contains the corresponding amount of keys.
*/
class PopupKeys<T>(
val hint: T?,
val prioritized: List<T>,
val other: List<T>
) : Collection<T> {
companion object {
const val HINT_INDEX: Int = -2
const val MAIN_INDEX: Int = -1
const val FIRST_PRIORITIZED = -1
const val SECOND_PRIORITIZED = -2
const val THIRD_PRIORITIZED = -3
}
override val size: Int
get() = if (hint != null) { 1 } else { 0 } + if (main != null) { 1 } else { 0 } + relevant.size
get() = prioritized.size + other.size
fun size(keyHintMode: KeyHintMode): Int {
return if (keyHintMode == KeyHintMode.DISABLED && hint != null) {
size - 1
} else {
size
override fun contains(element: T): Boolean {
return prioritized.contains(element) || other.contains(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
return (prioritized + other).containsAll(elements)
}
override fun isEmpty(): Boolean {
return prioritized.isEmpty() && other.isEmpty()
}
override fun iterator(): Iterator<T> {
return (prioritized + other).listIterator()
}
fun getOrNull(index: Int): T? {
if (index >= other.size || index < -prioritized.size) {
return null
}
return when (index) {
FIRST_PRIORITIZED -> prioritized[0]
SECOND_PRIORITIZED -> prioritized[1]
THIRD_PRIORITIZED -> prioritized[2]
else -> other.getOrNull(index)
}
}
@@ -61,125 +233,10 @@ open class PopupSet<T : KeyData>(
val item = getOrNull(index)
if (item == null) {
throw IndexOutOfBoundsException(
"Specified index $index is not an valid entry in this PopupSet!"
"Specified index $index is not an valid entry in this PopupKeys!"
)
} else {
return item
}
}
fun getOrNull(index: Int): T? {
if (index >= relevant.size || index < HINT_INDEX) {
return null
}
return when (index) {
HINT_INDEX -> hint
MAIN_INDEX -> main
else -> relevant.getOrNull(index)
}
}
override fun contains(element: T): Boolean {
return hint == element || main == element || relevant.contains(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
for (element in elements) {
if (!contains(element)) {
return false
}
}
return true
}
override fun iterator(): Iterator<T> {
return PopupSetIterator(this)
}
override fun isEmpty(): Boolean {
return size == 0
}
class PopupSetIterator<T : KeyData> internal constructor (
private val popupSet: PopupSet<T>
) : Iterator<T> {
var index = HINT_INDEX
override fun next(): T = popupSet[index++]
override fun hasNext(): Boolean {
if (index == HINT_INDEX) {
if (popupSet.getOrNull(index) != null) {
return true
} else {
index++
}
}
if (index == MAIN_INDEX) {
if (popupSet.getOrNull(index) != null) {
return true
} else {
index++
}
}
return popupSet.getOrNull(index) != null
}
}
}
@Suppress("UNCHECKED_CAST")
class MutablePopupSet<T : KeyData>(
override var hint: T? = null,
override var main: T? = null,
override val relevant: ArrayList<T> = arrayListOf()
) : PopupSet<T>(hint, main, relevant) {
fun clear() {
hint = null
main = null
relevant.clear()
}
fun merge(other: PopupSet<T>) {
relevant.addAll(other.relevant)
other.hint?.let {
if (hint == null) {
hint = it
} else {
relevant.add(it)
}
}
other.main?.let {
if (main == null) {
main = it
} else {
relevant.add(it)
}
}
}
fun merge(other: PopupSet<T>, evaluator: TextComputingEvaluator) {
other.relevant.forEach {
val data = it.computeTextKeyData(evaluator) as? T
if (data != null) {
relevant.add(data)
}
}
other.hint?.let {
if (hint == null) {
hint = it
} else {
relevant.add(it)
}
}
other.main?.let {
val data = it.computeTextKeyData(evaluator) as? T
if (data != null) {
if (main == null) {
main = data
} else {
relevant.add(data)
}
}
}
}
}

View File

@@ -30,13 +30,13 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.keyboard.Key
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import dev.patrickgold.florisboard.common.ViewUtils
class PopupView : View, ThemeManager.OnThemeUpdatedListener {
private val themeManager: ThemeManager = ThemeManager.default()
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
setCornerRadius(ViewUtils.dp2px(6.0f))
}
private val labelPaint: Paint = Paint().apply {
alpha = 255
@@ -91,7 +91,7 @@ class PopupView : View, ThemeManager.OnThemeUpdatedListener {
backgroundDrawable.apply {
setTint(theme.getAttr(Theme.Attr.POPUP_BACKGROUND).toSolidColor().color)
}
elevation = ViewLayoutUtils.convertDpToPixel(4.0f, context)
elevation = ViewUtils.dp2px(4.0f)
threeDotsDrawable?.apply {
setTint(theme.getAttr(Theme.Attr.POPUP_FOREGROUND).toSolidColor().color)
}

View File

@@ -16,24 +16,23 @@
package dev.patrickgold.florisboard.ime.text
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.view.KeyEvent
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.text.isDigitsOnly
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.FlorisboardBinding
import dev.patrickgold.florisboard.debug.*
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.core.*
import dev.patrickgold.florisboard.ime.dictionary.Dictionary
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
import dev.patrickgold.florisboard.ime.extension.AssetManager
import dev.patrickgold.florisboard.ime.extension.AssetRef
import dev.patrickgold.florisboard.ime.extension.AssetSource
import dev.patrickgold.florisboard.ime.nlp.Token
import dev.patrickgold.florisboard.ime.nlp.toStringList
import dev.patrickgold.florisboard.ime.keyboard.ImeOptions
import dev.patrickgold.florisboard.ime.keyboard.InputAttributes
import dev.patrickgold.florisboard.ime.keyboard.KeyboardState
import dev.patrickgold.florisboard.ime.keyboard.updateKeyboardState
import dev.patrickgold.florisboard.ime.text.gestures.GlideTypingManager
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.*
@@ -42,7 +41,6 @@ import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
import dev.patrickgold.florisboard.ime.text.smartbar.SmartbarView
import kotlinx.coroutines.*
import org.json.JSONArray
import kotlin.math.roundToLong
/**
* TextInputManager is responsible for managing everything which is related to text input. All of
@@ -59,22 +57,19 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
FlorisBoard.EventListener, SmartbarView.EventListener {
var isGlidePostEffect: Boolean = false
private val florisboard = FlorisBoard.getInstance()
private val prefs: PrefHelper get() = florisboard.prefs
private val florisboard get() = FlorisBoard.getInstance()
private val prefs get() = Preferences.default()
val symbolsWithSpaceAfter: List<String>
private val activeEditorInstance: EditorInstance
get() = florisboard.activeEditorInstance
lateinit var layoutManager: LayoutManager
private set
private var activeKeyboardMode: KeyboardMode? = null
val keyboards = TextKeyboardCache()
private var textInputKeyboardView: TextKeyboardView? = null
lateinit var textKeyboardIconSet: TextKeyboardIconSet
private set
private var textViewGroup: LinearLayout? = null
private val dictionaryManager: DictionaryManager = DictionaryManager.default()
private var activeDictionary: Dictionary<String, Int>? = null
private val dictionaryManager: DictionaryManager get() = DictionaryManager.default()
val inputEventDispatcher: InputEventDispatcher = InputEventDispatcher.new(
repeatableKeyCodes = intArrayOf(
KeyCode.ARROW_DOWN,
@@ -86,15 +81,11 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
)
)
var keyVariation: KeyVariation = KeyVariation.NORMAL
internal var smartbarView: SmartbarView? = null
// Caps/Shift related properties
var caps: Boolean = false
private set
var capsLock: Boolean = false
private set
val activeState: KeyboardState get() = florisboard.activeState
private var newCapsState: Boolean = false
private var isNumberRowVisible: Boolean = false
// Composing text related properties
var isManualSelectionMode: Boolean = false
@@ -123,7 +114,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
val evaluator = object : TextComputingEvaluator {
override fun evaluateCaps(): Boolean {
return caps || capsLock
return activeState.caps || activeState.capsLock
}
override fun evaluateCaps(data: TextKeyData): Boolean {
@@ -134,8 +125,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
return when (data.code) {
KeyCode.CLIPBOARD_COPY,
KeyCode.CLIPBOARD_CUT -> {
florisboard.activeEditorInstance.selection.isSelectionMode &&
!florisboard.activeEditorInstance.isRawInputEditor
activeState.isSelectionMode && activeState.isRichInputEditor
}
KeyCode.CLIPBOARD_PASTE -> {
// such gore. checks
@@ -146,7 +136,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
) == true
}
KeyCode.CLIPBOARD_SELECT_ALL -> {
!florisboard.activeEditorInstance.isRawInputEditor
activeState.isRichInputEditor
}
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT -> {
prefs.clipboard.enableHistory
@@ -194,7 +184,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
}
override fun getKeyVariation(): KeyVariation {
return keyVariation
return activeState.keyVariation
}
override fun getKeyboard(): TextKeyboard {
@@ -220,73 +210,33 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
layoutManager = LayoutManager()
textKeyboardIconSet = TextKeyboardIconSet.new(florisboard)
inputEventDispatcher.keyEventReceiver = this
isNumberRowVisible = prefs.keyboard.numberRow
var subtypes = florisboard.subtypeManager.subtypes
if (subtypes.isEmpty()) {
subtypes = listOf(Subtype.DEFAULT)
}
for (subtype in subtypes) {
for (mode in KeyboardMode.values()) {
keyboards.set(mode, subtype, keyboard = layoutManager.computeKeyboardAsync(mode, subtype, prefs))
keyboards.set(mode, subtype, keyboard = layoutManager.computeKeyboardAsync(mode, subtype))
}
}
}
override fun onCreateInputView() {
flogInfo(LogTopic.IMS_EVENTS)
}
/**
* Sets up the newly registered input view.
*/
override fun onRegisterInputView(inputView: InputView) {
override fun onInitializeInputUi(uiBinding: FlorisboardBinding) {
flogInfo(LogTopic.IMS_EVENTS)
textViewGroup = inputView.findViewById(R.id.text_input)
textInputKeyboardView = inputView.findViewById(R.id.text_input_keyboard_view)
textInputKeyboardView?.setIconSet(textKeyboardIconSet)
textInputKeyboardView?.setComputingEvaluator(evaluator)
launch(Dispatchers.Main) {
val animator1 = textViewGroup?.let {
ObjectAnimator.ofFloat(it, "alpha", 0.9f, 1.0f).apply {
duration = 125
repeatCount = 0
start()
}
}
val animator2 = textViewGroup?.let {
ObjectAnimator.ofFloat(it, "alpha", 1.0f, 0.4f).apply {
startDelay = 125
duration = 500
repeatCount = ValueAnimator.INFINITE
repeatMode = ValueAnimator.REVERSE
start()
}
}
setActiveKeyboardMode(getActiveKeyboardMode())
animator1?.cancel()
animator2?.cancel()
val animator3 = textViewGroup?.let {
ObjectAnimator.ofFloat(it, "alpha", it.alpha, 1.0f).apply {
duration = (((1.0f - it.alpha) / 0.6f) * 125f).roundToLong()
repeatCount = 0
start()
}
}
delay(animator3?.duration ?: 1)
animator3?.end()
textInputKeyboardView = uiBinding.text.mainKeyboardView.also {
it.setIconSet(textKeyboardIconSet)
it.setComputingEvaluator(evaluator)
it.sync()
}
}
fun registerSmartbarView(view: SmartbarView) {
smartbarView = view
smartbarView?.setEventListener(this)
}
fun unregisterSmartbarView(view: SmartbarView) {
if (smartbarView == view) {
smartbarView = null
smartbarView = uiBinding.text.smartbar.root.also {
it.setEventListener(this)
it.sync()
}
setActiveKeyboardMode(getActiveKeyboardMode(), updateState = false)
}
/**
@@ -295,32 +245,40 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
override fun onDestroy() {
flogInfo(LogTopic.IMS_EVENTS)
smartbarView?.setEventListener(null)
smartbarView = null
textInputKeyboardView?.setComputingEvaluator(null)
textInputKeyboardView = null
keyboards.clear()
inputEventDispatcher.keyEventReceiver = null
inputEventDispatcher.close()
dictionaryManager.unloadUserDictionariesIfNecessary()
cancel()
layoutManager.onDestroy()
instance = null
}
/**
* Evaluates the [activeKeyboardMode], [keyVariation] and [EditorInstance.isComposingEnabled]
* Evaluates the [KeyboardState.keyboardMode], [KeyboardState.keyVariation] and [KeyboardState.isComposingEnabled]
* property values when starting to interact with a input editor. Also resets the composing
* texts and sets the initial caps mode accordingly.
*/
override fun onStartInputView(instance: EditorInstance, restarting: Boolean) {
val keyboardMode = when (instance.inputAttributes.type) {
val keyboardMode = when (activeState.inputAttributes.type) {
InputAttributes.Type.NUMBER -> {
keyVariation = KeyVariation.NORMAL
activeState.keyVariation = KeyVariation.NORMAL
KeyboardMode.NUMERIC
}
InputAttributes.Type.PHONE -> {
keyVariation = KeyVariation.NORMAL
activeState.keyVariation = KeyVariation.NORMAL
KeyboardMode.PHONE
}
InputAttributes.Type.TEXT -> {
keyVariation = when (instance.inputAttributes.variation) {
activeState.keyVariation = when (activeState.inputAttributes.variation) {
InputAttributes.Variation.EMAIL_ADDRESS,
InputAttributes.Variation.WEB_EMAIL_ADDRESS -> {
KeyVariation.EMAIL_ADDRESS
@@ -340,92 +298,91 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
KeyboardMode.CHARACTERS
}
else -> {
keyVariation = KeyVariation.NORMAL
activeState.keyVariation = KeyVariation.NORMAL
KeyboardMode.CHARACTERS
}
}
instance.apply {
isComposingEnabled = when (keyboardMode) {
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> false
else -> keyVariation != KeyVariation.PASSWORD &&
prefs.suggestion.enabled// &&
//!instance.inputAttributes.flagTextAutoComplete &&
//!instance.inputAttributes.flagTextNoSuggestions
}
isPrivateMode = prefs.advanced.forcePrivateMode ||
imeOptions.flagNoPersonalizedLearning
activeState.isComposingEnabled = when (keyboardMode) {
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> false
else -> activeState.keyVariation != KeyVariation.PASSWORD &&
prefs.suggestion.enabled// &&
//!instance.inputAttributes.flagTextAutoComplete &&
//!instance.inputAttributes.flagTextNoSuggestions
}
val newIsNumberRowVisible = prefs.keyboard.numberRow
if (isNumberRowVisible != newIsNumberRowVisible) {
keyboards.clear(KeyboardMode.CHARACTERS)
isNumberRowVisible = newIsNumberRowVisible
}
setActiveKeyboardMode(keyboardMode, updateState = false)
instance.composingEnabledChanged()
activeState.isPrivateMode = prefs.advanced.forcePrivateMode ||
activeState.imeOptions.flagNoPersonalizedLearning
if (!prefs.correction.rememberCapsLockState) {
capsLock = false
activeState.capsLock = false
}
isGlidePostEffect = false
updateCapsState()
setActiveKeyboardMode(keyboardMode)
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
smartbarView?.updateSmartbarState()
}
/**
* Handle stuff when finishing to interact with a input editor.
*/
override fun onFinishInputView(finishingInput: Boolean) {
smartbarView?.updateSmartbarState()
}
override fun onWindowShown() {
smartbarView?.updateSmartbarState()
launch(Dispatchers.Default) {
dictionaryManager.loadUserDictionariesIfNecessary()
}
textInputKeyboardView?.sync()
smartbarView?.sync()
}
/**
* Gets [activeKeyboardMode].
* Gets the active keyboard mode.
*
* @return If null [KeyboardMode.CHARACTERS], else [activeKeyboardMode].
* @return The active keyboard mode.
*/
fun getActiveKeyboardMode(): KeyboardMode {
return activeKeyboardMode ?: KeyboardMode.CHARACTERS
return activeState.keyboardMode
}
/**
* Sets [activeKeyboardMode] and updates the [SmartbarView.isQuickActionsVisible] state.
* Sets the active keyboard mode and updates the [KeyboardState.isQuickActionsVisible] state.
*/
private fun setActiveKeyboardMode(mode: KeyboardMode) = launch {
setActiveKeyboard(mode, florisboard.activeSubtype)
activeKeyboardMode = mode
private fun setActiveKeyboardMode(mode: KeyboardMode, updateState: Boolean = true) {
activeState.keyboardMode = mode
isManualSelectionMode = false
isManualSelectionModeStart = false
isManualSelectionModeEnd = false
smartbarView?.isQuickActionsVisible = false
smartbarView?.updateSmartbarState()
activeState.isQuickActionsVisible = false
setActiveKeyboard(mode, florisboard.activeSubtype, updateState)
}
private fun setActiveKeyboard(mode: KeyboardMode, subtype: Subtype) = launch(Dispatchers.IO) {
private fun setActiveKeyboard(mode: KeyboardMode, subtype: Subtype, updateState: Boolean = true) = launch(Dispatchers.IO) {
val activeKeyboard = keyboards.getOrElseAsync(mode, subtype) {
layoutManager.computeKeyboardAsync(
keyboardMode = mode,
subtype = subtype,
prefs = prefs
subtype = subtype
).await()
}.await()
withContext(Dispatchers.Main) {
textInputKeyboardView?.setComputedKeyboard(activeKeyboard)
if (updateState) {
florisboard.dispatchCurrentStateToInputUi()
}
}
}
override fun onSubtypeChanged(newSubtype: Subtype) {
override fun onSubtypeChanged(newSubtype: Subtype, doRefreshLayouts: Boolean) {
launch {
if (activeEditorInstance.isComposingEnabled) {
withContext(Dispatchers.IO) {
dictionaryManager.loadDictionary(AssetRef(AssetSource.Assets, "ime/dict/en.flict")).let {
activeDictionary = it.getOrDefault(null)
}
}
if (activeState.isComposingEnabled) {
dictionaryManager.prepareDictionaries(newSubtype)
}
if (PrefHelper.getDefaultInstance(florisboard).glide.enabled) {
if (prefs.glide.enabled) {
GlideTypingManager.getInstance().setWordData(newSubtype)
}
setActiveKeyboard(getActiveKeyboardMode(), newSubtype)
if (doRefreshLayouts) {
setActiveKeyboard(getActiveKeyboardMode(), newSubtype)
}
}
isGlidePostEffect = false
}
@@ -438,29 +395,28 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
if (!inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
updateCapsState()
}
smartbarView?.updateSmartbarState()
flogInfo(LogTopic.IMS_EVENTS) { "current word: ${activeEditorInstance.cachedInput.currentWord.text}" }
if (activeEditorInstance.isComposingEnabled && !inputEventDispatcher.isPressed(KeyCode.DELETE)) {
if (activeState.isComposingEnabled && !inputEventDispatcher.isPressed(KeyCode.DELETE) && !isGlidePostEffect) {
if (activeEditorInstance.shouldReevaluateComposingSuggestions) {
activeEditorInstance.shouldReevaluateComposingSuggestions = false
activeDictionary?.let {
launch(Dispatchers.Default) {
val startTime = System.nanoTime()
val suggestions = it.getTokenPredictions(
precedingTokens = listOf(),
currentToken = Token(activeEditorInstance.cachedInput.currentWord.text),
maxSuggestionCount = 16,
allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive
).toStringList()
if (BuildConfig.DEBUG) {
val elapsed = (System.nanoTime() - startTime) / 1000.0
flogInfo { "sugg fetch time: $elapsed us" }
}
launch(Dispatchers.Default) {
val startTime = System.nanoTime()
dictionaryManager.suggest(
currentWord = activeEditorInstance.cachedInput.currentWord.text,
preceidingWords = listOf(),
subtype = florisboard.activeSubtype,
allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive,
maxSuggestionCount = 16
) { suggestions ->
withContext(Dispatchers.Main) {
smartbarView?.setCandidateSuggestionWords(startTime, suggestions)
smartbarView?.updateCandidateSuggestionCapsState()
}
}
if (BuildConfig.DEBUG) {
val elapsed = (System.nanoTime() - startTime) / 1000.0
flogInfo { "sugg fetch time: $elapsed us" }
}
}
} else {
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
@@ -474,13 +430,12 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
/**
* Updates the current caps state according to the [EditorInstance.cursorCapsMode], while
* respecting [capsLock] property and the correction.autoCapitalization preference.
* respecting [KeyboardState.capsLock] property and the correction.autoCapitalization preference.
*/
private fun updateCapsState() {
if (!capsLock) {
caps = prefs.correction.autoCapitalization &&
if (!activeState.capsLock) {
activeState.caps = prefs.correction.autoCapitalization &&
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
textInputKeyboardView?.notifyStateChanged()
}
}
@@ -544,8 +499,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
override fun onSmartbarQuickActionPressed(quickActionId: Int) {
when (quickActionId) {
R.id.quick_action_toggle -> {
activeState.isQuickActionsVisible = !activeState.isQuickActionsVisible
smartbarView?.updateKeyboardState(activeState)
return
}
R.id.quick_action_switch_to_editing_context -> {
if (activeKeyboardMode == KeyboardMode.EDITING) {
if (activeState.keyboardMode == KeyboardMode.EDITING) {
setActiveKeyboardMode(KeyboardMode.CHARACTERS)
} else {
setActiveKeyboardMode(KeyboardMode.EDITING)
@@ -563,8 +523,8 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
return
}
}
smartbarView?.isQuickActionsVisible = false
smartbarView?.updateSmartbarState()
activeState.isQuickActionsVisible = false
smartbarView?.updateKeyboardState(activeState)
}
/**
@@ -598,17 +558,17 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
* Handles a [KeyCode.ENTER] event.
*/
private fun handleEnter() {
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
if (activeState.imeOptions.flagNoEnterAction) {
activeEditorInstance.performEnter()
} else {
when (activeEditorInstance.imeOptions.action) {
ImeOptions.Action.DONE,
ImeOptions.Action.GO,
ImeOptions.Action.NEXT,
ImeOptions.Action.PREVIOUS,
ImeOptions.Action.SEARCH,
ImeOptions.Action.SEND -> {
activeEditorInstance.performEnterAction(activeEditorInstance.imeOptions.action)
when (activeState.imeOptions.enterAction) {
ImeOptions.EnterAction.DONE,
ImeOptions.EnterAction.GO,
ImeOptions.EnterAction.NEXT,
ImeOptions.EnterAction.PREVIOUS,
ImeOptions.EnterAction.SEARCH,
ImeOptions.EnterAction.SEND -> {
activeEditorInstance.performEnterAction(activeState.imeOptions.enterAction)
}
else -> activeEditorInstance.performEnter()
}
@@ -634,34 +594,34 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
private fun handleShiftDown(ev: InputKeyEvent) {
if (ev.isConsecutiveEventOf(inputEventDispatcher.lastKeyEventDown, prefs.keyboard.longPressDelay.toLong())) {
newCapsState = true
caps = true
capsLock = true
activeState.caps = true
activeState.capsLock = true
} else {
newCapsState = !caps
caps = true
capsLock = false
newCapsState = !activeState.caps
activeState.caps = true
activeState.capsLock = false
}
textInputKeyboardView?.notifyStateChanged()
smartbarView?.updateCandidateSuggestionCapsState()
florisboard.dispatchCurrentStateToInputUi()
}
/**
* Handles a [KeyCode.SHIFT] up event.
*/
private fun handleShiftUp() {
caps = newCapsState
textInputKeyboardView?.notifyStateChanged()
activeState.caps = newCapsState
smartbarView?.updateCandidateSuggestionCapsState()
florisboard.dispatchCurrentStateToInputUi()
}
/**
* Handles a [KeyCode.SHIFT] cancel event.
*/
private fun handleShiftCancel() {
caps = false
capsLock = false
textInputKeyboardView?.notifyStateChanged()
activeState.caps = false
activeState.capsLock = false
smartbarView?.updateCandidateSuggestionCapsState()
florisboard.dispatchCurrentStateToInputUi()
}
/**
@@ -671,10 +631,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
val lastKeyEvent = inputEventDispatcher.lastKeyEventDown ?: return
if (lastKeyEvent.data.code == KeyCode.SHIFT && lastKeyEvent.action == InputKeyEvent.Action.DOWN) {
newCapsState = true
caps = true
capsLock = true
textInputKeyboardView?.notifyStateChanged()
activeState.caps = true
activeState.capsLock = true
smartbarView?.updateCandidateSuggestionCapsState()
florisboard.dispatchCurrentStateToInputUi()
}
}
@@ -846,7 +806,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
KeyCode.VIEW_SYMBOLS2 -> setActiveKeyboardMode(KeyboardMode.SYMBOLS2)
KeyCode.UNDO -> activeEditorInstance.performUndo()
else -> {
when (activeKeyboardMode) {
when (activeState.keyboardMode) {
KeyboardMode.NUMERIC,
KeyboardMode.NUMERIC_ADVANCED,
KeyboardMode.PHONE,
@@ -885,13 +845,12 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
}
}
}
if (data.code != KeyCode.SHIFT && !capsLock && !inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
if (data.code != KeyCode.SHIFT && !activeState.capsLock && !inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
updateCapsState()
}
if (ev.data.code > KeyCode.SPACE) {
isGlidePostEffect = false
}
smartbarView?.updateSmartbarState()
}
override fun onInputKeyRepeat(ev: InputKeyEvent) {
@@ -912,14 +871,14 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
/**
* Changes a word to the current case.
* eg if [capsLock] is true, abc -> ABC
* eg if [KeyboardState.capsLock] is true, abc -> ABC
* if [caps] is true, abc -> Abc
* otherwise , abc -> abc
*/
fun fixCase(word: String): String {
return when {
capsLock -> word.toUpperCase(florisboard.activeSubtype.locale)
caps -> word.capitalize(florisboard.activeSubtype.locale)
activeState.capsLock -> word.uppercase(florisboard.activeSubtype.locale)
activeState.caps -> word.replaceFirstChar { if (it.isLowerCase()) it.titlecase(florisboard.activeSubtype.locale) else it.toString() }
else -> word
}
}

View File

@@ -0,0 +1,54 @@
package dev.patrickgold.florisboard.ime.text.composing
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
interface Composer {
val name: String
val label: String
val toRead: Int
fun getActions(s: String, c: Char): Pair<Int, String>
}
@Serializable
@SerialName("appender")
class Appender : Composer {
companion object {
const val name = "appender"
}
override val name: String = Appender.name
override val label: String = "Appender"
override val toRead: Int = 0
override fun getActions(s: String, c: Char): Pair<Int, String> {
return Pair(0, "$c")
}
}
@Serializable
@SerialName("with-rules")
class WithRules(
override val name: String,
override val label: String,
val rules: JsonObject
) : Composer {
override val toRead: Int = rules.keys.toList().sortedBy { it.length }.reversed()[0].length - 1
@Transient val ruleOrder: List<String> = rules.keys.toList().sortedBy { it.length }.reversed()
@Transient val ruleMap: Map<String, String> = rules.entries.map { Pair(it.key, (it.value as JsonPrimitive).content) }.toMap()
override fun getActions(s: String, c: Char): Pair<Int, String> {
val str = "${s}$c"
for (key in ruleOrder) {
if (str.endsWith(key)) {
return Pair(key.length-1, ruleMap.getValue(key))
}
}
return Pair(0, "$c")
}
}

View File

@@ -0,0 +1,102 @@
package dev.patrickgold.florisboard.ime.text.composing
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("hangul-unicode")
class HangulUnicode : Composer {
override val name: String = "hangul-unicode"
override val label: String = "Hangul Unicode"
override val toRead: Int = 1
// Initial consonants, ordered for syllable creation
private val initials = "ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ"
// Medial vowels, ordered for syllable creation
private val medials = "ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ"
// Final consonants (including none), ordered for syllable creation
private val finals = "_ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ"
private val medialComp = mapOf(
'ㅗ' to listOfNotNull("ㅏㅐㅣ", "ㅘㅙㅚ"),
'ㅜ' to listOfNotNull("ㅓㅔㅣ", "ㅝㅞㅟ"),
'ㅡ' to listOfNotNull("", ""),
)
private val finalComp = mapOf(
'ㄱ' to listOfNotNull("", ""),
'ㄴ' to listOfNotNull("ㅈㅎ", "ㄵㄶ"),
'ㄹ' to listOfNotNull("ㄱㅁㅂㅅㅌㅍㅎ", "ㄺㄻㄼㄽㄾㄿㅀ"),
'ㅂ' to listOfNotNull("", ""),
)
private fun reverseComp(map: Map<Char, List<String>>): Map<Char, List<Char>> {
val ret = mutableMapOf<Char, List<Char>>()
for ((first, v) in map) {
val (seconds, comps) = v
for (i in seconds.indices) {
ret[comps[i]] = listOf(first, seconds[i])
}
}
return ret
}
private val finalCompRev = reverseComp(finalComp)
private val medialCompRev = reverseComp(medialComp)
private fun syllable(ini: Int, med: Int, fin:Int): Char {
return (ini*588 + med*28 + fin + 44032).toChar()
}
private fun syllableBlocks(syllOrd: Int): List<Int> {
val initial = (syllOrd-44032)/588
val medial = (syllOrd-44032-initial*588)/28
val fin = (syllOrd-44032)%28
return listOf(initial, medial, fin)
}
override fun getActions(s: String, c: Char): Pair<Int, String> {
// s is "at least the last 1 character of what's currently here"
if (s.isEmpty()) {
return Pair(0, ""+c)
}
val lastChar = s.last()
val lastOrd = lastChar.toInt()
if (lastChar in initials && c in medials) {
return Pair(1, "${syllable(initials.indexOf(lastChar), medials.indexOf(c), 0)}")
} else if (lastOrd in 44032..55203) { // syllable
val (ini, med, fin) = syllableBlocks(lastOrd)
// underscore is a sentinel in the "finals" string
if (c == '_')
return Pair(0, ""+c)
// if there is no final and the new char is a final, merge
if (fin == 0 && c in finals)
return Pair(1, "${syllable(ini, med, finals.indexOf(c))}")
// if there is already a final but it is mergeable with the new char into a composed final, merge
if ((finals[fin] in finalComp) && c in finalComp[finals[fin]]!![0]) {
val tple = finalComp[finals[fin]]
return Pair(1, "${syllable(ini, med, finals.indexOf(tple!![1][tple[0].indexOf(c)]))}")
}
// if there is a simple final and the new char is a medial, split the old syllable
if (fin != 0 && finals[fin] !in finalCompRev && c in medials)
return Pair(1, "${syllable(ini, med, 0)}${syllable(initials.indexOf(finals[fin]), medials.indexOf(c), 0)}")
// if there is a composed final and the new char is a medial, split the old final
if (finals[fin] in finalCompRev && c in medials)
return Pair(1, "${syllable(ini, med, finals.indexOf(finalCompRev.getValue(finals[fin])[0]))}${syllable(initials.indexOf(finalCompRev.getValue(finals[fin])[1]), medials.indexOf(c), 0)}")
// if no final yet, and current medial can be composed with new char, merge
if (medials[med] in medialComp && c in medialComp.getValue(medials[med])[0] && fin == 0) {
val tple = medialComp[medials[med]]
return Pair(1, "${syllable(ini, medials.indexOf(tple!![1][tple[0].indexOf(c)]), 0)}")
}
}
return Pair(0, ""+c)
}
}

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