Compare commits

..

176 Commits

Author SHA1 Message Date
Patrick Goldinger
e581d6cbc4 Release v0.3.9 2021-03-16 20:15:45 +01:00
Patrick Goldinger
ec13d008fb Fix Greek uppercase bug (#452) 2021-03-16 20:03:51 +01:00
Patrick Goldinger
edfea1afcb Merge pull request #461 from florisboard/metadata-refresh
App Store presence metadata update
2021-03-16 16:45:10 +01:00
Patrick Goldinger
25fc23d721 Update store presence metadata to represent all implemented features 2021-03-16 16:34:54 +01:00
Patrick Goldinger
c701141be2 Remove Italian store metadata 2021-03-16 15:15:04 +01:00
Patrick Goldinger
e5b956857e Merge pull request #459 from florisboard/beta-track-prep
Beta track preperation / App icon revamp
2021-03-16 11:53:12 +01:00
Patrick Goldinger
67236ef58d Add beta build variant 2021-03-16 03:17:34 +01:00
Patrick Goldinger
2da17a0654 Add new app icons for all build variants 2021-03-16 03:16:56 +01:00
Patrick Goldinger
1f3221a886 Merge pull request #457 from florisboard/one-handed-improvements
Add one-handed width option / Improve one-handed code
2021-03-15 20:13:02 +01:00
Patrick Goldinger
47f80d00c4 Add one-handed width option / Improve one-handed code 2021-03-15 17:49:18 +01:00
Patrick Goldinger
d648c480b5 Merge pull request #455 from florisboard/theme-import-export
Add theme import/export / Fix theme editor jumping to top
2021-03-15 09:33:18 +01:00
Patrick Goldinger
9e26720674 Fix export UI not requesting to create document 2021-03-15 01:51:11 +01:00
Patrick Goldinger
a20c6bf148 Fix theme editor jumping to top (#379) 2021-03-15 00:57:35 +01:00
Patrick Goldinger
d2df5cfcdf Switch to Kotlin Result 2021-03-15 00:08:10 +01:00
Patrick Goldinger
93b5503dfc Fix file write bug and improve UI 2021-03-14 23:39:57 +01:00
Patrick Goldinger
4d4b54074a Improve import/export feature stability 2021-03-14 19:44:13 +01:00
Patrick Goldinger
904fd9b85a Add simple theme import/export functionality 2021-03-14 02:15:47 +01:00
Patrick Goldinger
e4f5fcf74b Merge pull request #451 from florisboard/alternate-shift-code
Add option for an alternate key code when caps state is active
2021-03-10 23:15:19 +01:00
Patrick Goldinger
15f0316839 Add shift variants for Colemak and Dvorak (#145) 2021-03-10 19:38:11 +01:00
Patrick Goldinger
93654c4f88 Add alternate key code option for FlorisKeyData (#145) 2021-03-10 19:37:50 +01:00
Patrick Goldinger
62fc549ea9 Fix crash on setup when no other IME is installed (#423) 2021-03-10 18:37:27 +01:00
Patrick Goldinger
d0dbd1cd4e Merge pull request #444 from florisboard/input-logic-rework
Input logic rework
2021-03-10 16:08:10 +01:00
Patrick Goldinger
af28f84b69 Fix delete precise char selection init value always 2 units (#448) 2021-03-10 12:09:18 +01:00
Patrick Goldinger
db7ee52029 Fix label text size decreasing bug in selection keyboard 2021-03-10 11:59:51 +01:00
Patrick Goldinger
7343617792 Fix space bar arrow movement initial count always 2 (#448) 2021-03-10 11:31:23 +01:00
Patrick Goldinger
5898d7006b Add internal batch edit level to prevent stuttering UI 2021-03-09 20:17:30 +01:00
Patrick Goldinger
058be7a169 Fix editor instance commit text logic 2021-03-09 02:03:17 +01:00
Patrick Goldinger
e6f2a25021 Improve input event logic / Fix extended popup bug 2021-03-08 19:51:37 +01:00
Patrick Goldinger
3a485a1574 Fix bugs and improve code 2021-03-08 01:09:43 +01:00
Patrick Goldinger
0ee0f24119 Add shift slide behavior / Improve performance of input logic 2021-03-07 19:35:08 +01:00
Patrick Goldinger
004e999259 Document InputEventDispatcher 2021-03-07 16:26:45 +01:00
Patrick Goldinger
11775c4619 Separate input event dispatcher logic into another file 2021-03-07 15:27:20 +01:00
Patrick Goldinger
177bad95b3 Clean up static KeyData object definitions 2021-03-06 19:33:53 +01:00
Patrick Goldinger
610526d845 Add multi-pointer support for gestures 2021-03-06 14:19:30 +01:00
Patrick Goldinger
55e489bc07 Complete overhaul of core input logic 2021-03-05 20:13:35 +01:00
Patrick Goldinger
589063be61 Rework cursor/selection implementation 2021-03-03 23:43:27 +01:00
Patrick Goldinger
aa73ac706a Update target SDK to API 30 (Android 11) 2021-03-01 20:13:59 +01:00
Patrick Goldinger
91cbbe74a3 Release v0.3.8 2021-02-28 23:57:29 +01:00
Patrick Goldinger
637d7fe503 Merge pull request #405 from florisboard/fix-memory-leaks
Fix memory management for Flictionaries & prediction algorithm
2021-02-28 23:44:59 +01:00
Patrick Goldinger
6a5e5a1708 Update translations from Crowdin 2021-02-28 19:48:32 +01:00
Patrick Goldinger
22fad5ba0b Fix logic bugs in prediction algorithm 2021-02-28 18:58:13 +01:00
Patrick Goldinger
f3d2c8257a Improve internal prediction algorithm memory management 2021-02-28 18:07:06 +01:00
Patrick Goldinger
bc89675269 Improve Flictionary load function 2021-02-28 15:29:23 +01:00
Patrick Goldinger
2603eb2b52 Fix end count bug in load function 2021-02-27 19:26:29 +01:00
Patrick Goldinger
38baac1af9 Correct move to start/end of line behavior (#340) 2021-02-27 14:22:03 +01:00
Patrick Goldinger
7e56094f5c Merge pull request #338 from X-yl/keyspacing
Add options for key spacing
2021-02-27 12:24:08 +01:00
Patrick Goldinger
af6ee13855 Sort language list in subtype add/edit dialog alphabetically (#361) 2021-02-27 12:15:42 +01:00
Patrick Goldinger
edb8d87fa0 Fix enter popup not working on symbol/numeric_advanced layout (#350) 2021-02-27 11:49:42 +01:00
Patrick Goldinger
ff35372945 Merge pull request #329 from florisboard/feat-suggestions-phase1
Suggestions implementation: Phase 1
2021-02-26 20:55:00 +01:00
Patrick Goldinger
b6edbf76d0 Merge branch 'master' into feat-suggestions-phase1 2021-02-26 20:40:41 +01:00
Patrick Goldinger
1bde28e288 Move Flictionary load to Dispatchers.IO context 2021-02-26 19:30:11 +01:00
Patrick Goldinger
aafb02cb68 Cleanup and improve prediction components 2021-02-26 18:58:10 +01:00
Patrick Goldinger
a07c91f089 Update precompiled Flictionary to contain basic profanity entries 2021-02-26 02:07:44 +01:00
Patrick Goldinger
1af3c1a210 Rework suggestion preferences UI for first experimental release 2021-02-26 01:07:30 +01:00
Patrick Goldinger
c13ec8aca9 Improve suggestion memory management 2021-02-25 20:39:51 +01:00
Patrick Goldinger
4a826cc0a3 Improve code and document NgramNode 2021-02-25 01:40:04 +01:00
Patrick Goldinger
1c9e4c0b4c Fix offset bug in EditorInstance 2021-02-25 01:21:04 +01:00
Patrick Goldinger
6cbbca5658 Add edit distance to prediction algorithm / Improve performance 2021-02-25 01:13:29 +01:00
Patrick Goldinger
a1b8550fe2 Rework Flictionary load() / Improve search algorithm 2021-02-24 03:04:51 +01:00
Patrick Goldinger
ab1007175d Update English Flictionary 2021-02-23 02:19:39 +01:00
Patrick Goldinger
4cf8b4af58 Add new Flictionary decoder / Add basic unigram prediction algorithm 2021-02-23 01:51:19 +01:00
Patrick Goldinger
2b001d9eb8 Add abstract implementation of NLP related interfaces 2021-02-15 18:56:55 +01:00
x-yl
286ddd9971 make key spacing for smartbar use dimens.xml 2021-02-13 22:41:39 +04:00
x-yl
d4c6411e1a Update margin for KeyboardRowView in onMeasure 2021-02-13 22:31:43 +04:00
Patrick Goldinger
915bcec0ee Merge pull request #343 from yashpalgoyal1304/patch-1
Added PR links
2021-02-13 18:09:38 +01:00
yashpalgoyal1304
93eb731bf0 Added PR links 2021-02-13 21:29:37 +05:30
x-yl
7a02f1c958 fix bug by changing desired to measured 2021-02-13 09:57:29 +04:00
x-yl
160d31beb0 Update KeyView layout in onMeasure instead of refreshing all layouts 2021-02-13 09:56:00 +04:00
x-yl
6d389b9a7f Added options for key spacing 2021-02-12 18:21:07 +04:00
Patrick Goldinger
3ea620a22e Merge pull request #333 from serebit/codebase-cleanup-final
Clean up codebase
2021-02-11 20:41:00 +01:00
Patrick Goldinger
94f9f3f3e7 Add Flictionary class / Add English binary dictionary file 2021-02-11 20:19:40 +01:00
Campbell Jones
8f28d0e81a Clean up codebase 2021-02-10 15:38:32 -05:00
Patrick Goldinger
07ce0c83fa Add phantom space behavior / Add Suggestion click functionality 2021-02-09 23:35:20 +01:00
Patrick Goldinger
c95244cc06 Merge pull request #331 from serebit/gitattributes
Add exception for bat files to gitattributes
2021-02-09 22:12:47 +01:00
Campbell Jones
3c2d427b1d Add exception for bat files to gitattributes 2021-02-09 15:19:25 -05:00
Patrick Goldinger
85da2141cb Complete overhaul of EditorInstance core class 2021-02-09 04:57:10 +01:00
Patrick Goldinger
cc9688a2dd Merge pull request #326 from serebit/master
Use haptic feedback by default if available
2021-02-08 20:46:13 +01:00
Campbell Jones
cd048af114 Use haptic feedback by default if available 2021-02-08 12:32:02 -05:00
Patrick Goldinger
4382dfc869 Merge pull request #319 from Salamandar/bépo
Edit bépo layout to add "êà" keys
2021-02-05 16:09:00 +01:00
Félix Piédallu
a622749b7b Edit bépo layout to add êà keys 2021-02-05 15:00:16 +01:00
Patrick Goldinger
1c2596147a Release v0.3.7 2021-02-04 23:56:56 +01:00
Patrick Goldinger
d355143ba1 Update selection keyboard icon (#316) 2021-02-04 23:47:00 +01:00
Patrick Goldinger
a9eb4c0eec Update translations from Crowdin 2021-02-04 19:57:17 +01:00
Patrick Goldinger
487a37bc66 Merge pull request #313 from eandersons/layout-lv_LV
Add QWERTY based extended popups for Latvian
2021-02-04 18:51:09 +01:00
Patrick Goldinger
85a54e701e Merge pull request #317 from florisboard/improve-landscape-input-ui
Improve Landscape Input UI
2021-02-04 18:48:10 +01:00
Patrick Goldinger
2666acd4ae Improve Landscape Input UI 2021-02-04 18:17:58 +01:00
Edgars
ba72e6274f Add Euro sign to e extended popup 2021-02-04 07:47:30 +02:00
Edgars
e20ce07957 Add QWERTY based extended popups for Latvian 2021-02-04 07:32:52 +02:00
Patrick Goldinger
765a12537e Update extract edit UI strings to be more user-friendly 2021-02-03 22:37:36 +01:00
Patrick Goldinger
4845ce55b5 Fix Spanish extended popups (#251) 2021-02-03 20:54:02 +01:00
Patrick Goldinger
47cd655d76 Update bug_report.md (#309) 2021-02-03 20:04:25 +01:00
Patrick Goldinger
d3edd3d332 Possible fix for background color bug (#274) 2021-02-03 19:50:16 +01:00
Patrick Goldinger
2d32364123 Fix cut/copy button not working in Smartbar (#308) 2021-02-03 19:07:40 +01:00
Patrick Goldinger
509308ec82 Fix welcome screen not appearing in rare cases (#310) 2021-02-03 18:54:53 +01:00
Patrick Goldinger
db65af5ea5 Fix symbols layouts applying the caps state (#298) 2021-02-03 00:50:14 +01:00
Patrick Goldinger
9a46cf9dff Improve one-handed toggle UX (#303) 2021-02-02 23:39:14 +01:00
Patrick Goldinger
2591eaa49d Fix Italian extended popups (#299) 2021-02-02 23:28:32 +01:00
Patrick Goldinger
57350b422d Release v0.3.6 2021-02-01 20:47:53 +01:00
Patrick Goldinger
fe8efa8496 Update translations from Crowdin 2021-02-01 20:07:28 +01:00
Patrick Goldinger
c5ce9ba252 Merge pull request #297 from serebit/master
Add Floris Black and Floris Black Borderless themes
2021-02-01 19:57:14 +01:00
Patrick Goldinger
4e39273812 Fix memory leaks and reset failures (#267) 2021-02-01 19:42:51 +01:00
Campbell Jones
43995f1ac5 Add Floris Black and Floris Black Borderless themes 2021-02-01 12:44:00 -05:00
Patrick Goldinger
3688f8e8dc Improve bottom offset feature (#58) 2021-01-31 23:40:50 +01:00
Patrick Goldinger
5cbbbc2295 Improve key hint label style (#213) 2021-01-31 17:05:06 +01:00
Karim
af08947929 Switch emoji and one-handed mode extended popup / Add Gboard Day theme (#258)
* switch positions between emojis and one hand keys

* new theme

* new theme

* Create gboard_like_day.json

* Rename gboard_like_day.json to gboard_day.json

* Changing label and name's value

* Changing label and name's value

Co-authored-by: Karim <52888633+itskarim@users.noreply.github.com>
Co-authored-by: Kareem <d.abdelkarim@outlook.com>
2021-01-31 15:25:10 +01:00
Patrick Goldinger
3e8a227320 Merge pull request #288 from florisboard/feat-improve-moving-gestures
Improve moving gestures detection and UX
2021-01-31 14:46:36 +01:00
Patrick Goldinger
adb69dc365 Improve moving gestures detection and UX 2021-01-31 03:52:16 +01:00
Patrick Goldinger
c2998c9a2e Merge branch 'feat-label-always-visible' 2021-01-30 19:23:39 +01:00
Patrick Goldinger
f801c31ebb Change bottom nav label visibility to always show 2021-01-30 19:23:02 +01:00
Patrick Goldinger
6b8652bcd9 Merge pull request #275 from GrbavaCigla/master
Serbian layouts
2021-01-30 12:16:59 +01:00
GrbavaCigla
65b5d252b6 copied hr.json to sr.json for extended popups 2021-01-30 11:09:22 +01:00
Patrick Goldinger
c5ae916ece Merge branch 'make-app-name-translatable' 2021-01-29 19:27:17 +01:00
Patrick Goldinger
de3fcceeaf Make app name translatable (#253) 2021-01-29 19:26:45 +01:00
florisboard-bot
f06ea18e89 Update Crowdin configuration file 2021-01-29 19:20:33 +01:00
GrbavaCigla
9d7754b8db Fixed layout 2021-01-29 18:51:02 +01:00
GrbavaCigla
2be1a328b6 Merge conflict solved 2021-01-29 17:45:29 +01:00
GrbavaCigla
9cd7931b3e Script moved to utils 2021-01-29 17:41:11 +01:00
Patrick Goldinger
03f9014b7c Fix machine-keyword Locale issue (#135) 2021-01-29 16:59:51 +01:00
Patrick Goldinger
62abefc36e Merge pull request #271 from kurdikeyboard/master
Added Kurdish Keyboard Qwerty(No-shift) layout
2021-01-29 15:37:54 +01:00
Patrick Goldinger
f5d79e8556 Fix semantic issues and asset naming 2021-01-29 15:33:59 +01:00
Patrick Goldinger
e68428ef11 Merge branch 'master' of https://github.com/kurdikeyboard/florisboard into kurdikeyboard-master 2021-01-29 15:15:04 +01:00
Patrick Goldinger
1cfde9c2b9 Merge pull request #276 from florisboard/feat-extract-edit-layout
Add ExtractEditLayout support
2021-01-29 15:12:42 +01:00
Patrick Goldinger
765a596eb2 Add ExtractEditLayout support 2021-01-29 14:59:27 +01:00
GrbavaCigla
a27035a81b Script for updating codes 2021-01-29 12:37:38 +01:00
GrbavaCigla
380eaffb08 Fix config 2021-01-29 12:34:06 +01:00
GrbavaCigla
0e7eac2796 Added layouts to config.json 2021-01-29 12:13:03 +01:00
GrbavaCigla
6da344fd6c Added layouts 2021-01-29 12:04:24 +01:00
Goran Gharib
7787af69fd Added Kurdish Keyboard Qwerty(No-shift) layout 2021-01-29 06:33:48 +03:00
Patrick Goldinger
288bd61fb4 Fix theme creation crash (#266) 2021-01-28 19:12:24 +01:00
Patrick Goldinger
fe69c0f3e1 Fix language code not matching in iw.json 2021-01-28 17:52:07 +01:00
antonygggg
766c5efa95 Added Hebrew layout (#264)
* Update config.json

Added characterLayouts hebrew and defaultSubtypes he-IL

* Create hebrew.json

* Create he.json

* Create hebrew.json

* Update hebrew.json

Added modifier hebrew

* Update hebrew.json

* Update hebrew.json

* Update he.json

* Update hebrew.json

* Update hebrew.json

* Update hebrew.json

* Update hebrew.json

* Update hebrew.json

* Update hebrew.json

* Fix language code for Android and fix brackets

Co-authored-by: Patrick Goldinger <patrick.goldinger@pm.me>
2021-01-28 13:43:43 +01:00
Patrick Goldinger
35bd6e7c8d Merge pull request #254 from antonygggg/patch-1
Update CONTRIBUTING.md
2021-01-28 09:56:09 +01:00
antonygggg
23f14ab57d Update CONTRIBUTING.md 2021-01-28 08:59:00 +02:00
Patrick Goldinger
ac688a38ab Merge pull request #250 from florisboard/fix-utility-key-naming
Fix utility key naming
2021-01-27 23:57:39 +01:00
Patrick Goldinger
a2e393d7dd Fix utility key naming (#246) 2021-01-27 21:00:10 +01:00
Patrick Goldinger
ba8ebaf231 Merge pull request #247 from kisekinopureya/master
Add Turkish extended popups for QWERTY
2021-01-27 18:23:56 +01:00
Gökhan Özdemir
a0e381ed93 Merge branch 'master' into master 2021-01-27 17:10:46 +00:00
Patrick Goldinger
09833a3369 Merge pull request #244 from Iorvethe/master
Add Bulgarian layouts
2021-01-27 17:22:02 +01:00
kisekinopureya
f014b010d8 Add Turkish extended popups for QWERTY 2021-01-27 17:07:39 +03:00
Iorvethe
9512eb32f0 Add popups for Bulgarian layouts 2021-01-27 11:21:28 +01:00
Patrick Goldinger
82f99bd721 Fix context for the switch_key_mode string 2021-01-26 23:55:54 +01:00
bpetrov
1d710dfb85 Add Bulgarian (BDS) layout 2021-01-26 22:26:42 +01:00
bpetrov
1328eb1f05 Add Bulgarian(Phonetic) layout 2021-01-26 21:48:29 +01:00
Patrick Goldinger
1cda0662ae Release v0.3.5 2021-01-25 21:45:27 +01:00
Patrick Goldinger
11cacb25c8 Merge pull request #241 from florisboard/feat-switch-key-customization
Add ability to customize switch key (emoji, language)
2021-01-25 21:13:17 +01:00
Patrick Goldinger
c0207fd84e Add ability to customize switch key (emoji, language) (#79) 2021-01-25 20:54:15 +01:00
Patrick Goldinger
56d3acfc67 Merge pull request #240 from florisboard/improve-adaptive-theme
Improve adaptive theme / Fix color dialog cache bug in theme editor
2021-01-25 19:44:56 +01:00
Patrick Goldinger
a3e5ae9337 Fix color dialog cache problem in theme editor (#237) 2021-01-25 18:44:31 +01:00
Patrick Goldinger
7e84f71464 Improve adaptive theme coloring (#226) 2021-01-25 18:43:57 +01:00
Patrick Goldinger
eb88fbc981 Update translations from Crowdin 2021-01-25 00:07:12 +01:00
Patrick Goldinger
96320e6b06 Merge pull request #234 from florisboard/improve-theme-editor
Improve theme editor UI and UX
2021-01-24 21:01:24 +01:00
Patrick Goldinger
fee9c2a0ac Improve theme editor UI and UX 2021-01-24 19:40:07 +01:00
Patrick Goldinger
c74a5841ec Add ext popups for less-than and greater-than symbols (#219) 2021-01-24 02:37:04 +01:00
Patrick Goldinger
aab7a6e33a Fix theme group name input validation (again) 2021-01-24 02:20:28 +01:00
Patrick Goldinger
0ea59cf2ed Merge pull request #232 from florisboard/fix-space-bar-long-press
Fix space bar long press
2021-01-24 02:08:49 +01:00
Patrick Goldinger
1be6ce1ae8 Fix space bar long press 2021-01-24 02:05:02 +01:00
Patrick Goldinger
8d06bea6bb Merge pull request #231 from florisboard/feat-proper-loading-screen
Proper loading keyboard animation
2021-01-23 19:18:39 +01:00
Patrick Goldinger
4b1a0c9972 Improve startup loading animation 2021-01-23 19:13:14 +01:00
Patrick Goldinger
3d50ea59af Add wiki page reference in Theme Editor 2021-01-22 16:00:50 +01:00
Patrick Goldinger
83bef3aee9 Release v0.3.4 2021-01-21 23:05:57 +01:00
Patrick Goldinger
c365acb800 Add InputView placeholder loading animation 2021-01-21 21:18:33 +01:00
Patrick Goldinger
ef075151fb Merge pull request #218 from florisboard/fix-key-precise-swipe
Fix precise character deletion behaviour
2021-01-21 18:33:16 +01:00
Patrick Goldinger
5316e46ba6 Fix precise character deletion behaviour 2021-01-21 17:35:09 +01:00
Patrick Goldinger
467fb62067 Merge pull request #187 from RickyM7/master
Trying to add Brazilian Portuguese (pt-BR) Layout
2021-01-20 22:59:53 +01:00
Patrick Goldinger
75c59366a9 Extended popups now respect country code (#188)
This allows for country specific popups like e.g. pt-BR to override
pt default configuration.
2021-01-20 22:47:28 +01:00
Patrick Goldinger
6df6e25b15 Fix double key press bugs for space and shift (#170) 2021-01-19 23:39:51 +01:00
Patrick Goldinger
81d89cad8c Fix key hint bug (#211) 2021-01-19 23:09:22 +01:00
Patrick Goldinger
99954237f7 Merge pull request #212 from florisboard/feat-add-swipe-actions
Add new swipe actions / Improve gesture pref structure
2021-01-19 21:19:33 +01:00
Patrick Goldinger
b5e56642fc Add new swipe actions / Improve gesture pref structure 2021-01-19 20:03:10 +01:00
Patrick Goldinger
343525f259 Merge pull request #206 from florisboard/fix-delete-shift-width
Fix delete and shift key width for 9+ char rows
2021-01-18 20:35:13 +01:00
Patrick Goldinger
a5adad714a Merge pull request #202 from Salamandar/bépo
Add Bépo french keyboard layout
2021-01-18 19:55:50 +01:00
Félix Piédallu
67214cbdf1 Add Bépo french keyboard layout 2021-01-18 19:39:12 +01:00
Patrick Goldinger
3a0284f355 Fix delete and shift key width for 9+ char rows 2021-01-18 18:51:32 +01:00
Patrick Goldinger
eecada3118 Fix group name input not accepting colons 2021-01-18 17:59:02 +01:00
Ricardo
68061f1aac Update pt-BR.json 2021-01-14 15:13:38 -03:00
Ricardo
c4ac4abd63 Brazilian Portuguese (pt-BR) Keyboard 2021-01-14 12:19:04 -03:00
288 changed files with 11284 additions and 2739 deletions

17
.editorconfig Normal file
View File

@@ -0,0 +1,17 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 120
trim_trailing_whitespace = true
[{*.har,*.json}]
indent_size = 2
[*.kt]
ij_kotlin_name_count_to_use_star_import = 99
ij_kotlin_name_count_to_use_star_import_for_members = 99

3
.gitattributes vendored
View File

@@ -1 +1,2 @@
* text=auto eol=lf
* text=auto eol=lf
*.bat text=auto eol=crlf

View File

@@ -15,18 +15,21 @@ assignees: ''
- Thank you for your help in making FlorisBoard better!
-->
**Environment information**
- FlorisBoard Version: <!-- e.g. 0.1.0 -->
- Install Source: <!-- Google PlayStore/F-Droid/GitHub/? -->
- Device: <!-- e.g. OnePlus 7T -->
- Android version, ROM: <!-- e.g. 10, Stock -->
#### Short description
Describe the bug in a short but concise way.
**Steps to reproduce**
#### Steps to reproduce
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
#### Environment information
- FlorisBoard Version: <!-- e.g. 0.3.6 -->
- Install Source: <!-- Google PlayStore/F-Droid/GitHub/? -->
- Device: <!-- e.g. OnePlus 7T -->
- Android version, ROM: <!-- e.g. 10, Stock -->
<!-- (remove this line if you paste a log)
```
If applicable, paste the captured debug log here.

View File

@@ -32,7 +32,7 @@ free to ask for help at any time!
## Adding a new keyboard layout / dictionary for locale
You can now oficially add layouts to FlorisBoard as described below.
You can now officially add layouts to FlorisBoard as described below.
FlorisBoard's core has stabilized enough that adding new content is
safe, although there will be some changes in the future.

View File

@@ -62,11 +62,9 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
* [x] Landscape orientation support (needs tweaks)
### Layouts
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish,
Norwegian, Swedish/Finnish, Icelandic, Danish, Hungarian,
Croatian, Polish, Romanian); more coming in future versions
* [x] Non-latin character layouts (Arabic, Persian, Greek, Russian
(JCUKEN))
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish, Norwegian, Swedish/Finnish, Icelandic, Danish,
Hungarian, Croatian, Polish, Romanian, Colemak, Dvorak, ...)
* [x] Non-latin character layouts (Arabic, Persian, Greek, Russian (JCUKEN))
* [x] Adapt to situation in app (password, url, text, etc. )
* [x] Special character layout(s)
* [x] Numeric layout
@@ -106,7 +104,7 @@ most likely be delayed back, even though I'm eager to stick to these as
close as possible.
### [v0.4.0](https://github.com/florisboard/florisboard/milestone/4)
- Module A: Smartbar rework (Implemented with #91)
- Module A: Smartbar rework (Implemented with [#91])
- Ability to enable/disable Smartbar (features below thus only work if
Smartbar is enabled)
- Dynamic switching between clipboard tools and word suggestions
@@ -115,14 +113,13 @@ close as possible.
- Complete rework of the Smartbar code base and the Smartbar layout
definition in XML
- Module B: Composing suggestions
- Module B: Composing suggestions (Phase 1: [#329])
- Auto-suggestion of words based of precompiled dictionaries
- Management of custom dictionary entries
- Opt-in only: Learning of often typed word pais to better predict next
words over time. Data collected here is stored locally and never leaves
- 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 (base implementation with [#162])
- 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
@@ -132,13 +129,14 @@ close as possible.
- Swiping over the characters will automatically convert this to a word
- Possibly also add improvements based on the Flow keyboard
- Module E: Theme rework (Implemented with #162)
- Module E: Theme rework (Implemented with [#162])
- Themes are now based on the Asset schema
- Dynamic theme creation
- Different theme modes (`Always day`, `Always dark`, `Follow system`
- Different theme modes (`Always day`, `Always night`, `Follow system`
and `Follow time`)
- Define a separate theme both for day and night theme
- Adapt to app theme if 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
@@ -151,9 +149,12 @@ passes...
Backlog (currently not assigned to any milestone):
- Theme import/export
- Floating keyboard
[#91]: https://github.com/florisboard/florisboard/pull/91
[#162]: https://github.com/florisboard/florisboard/pull/162
[#329]: https://github.com/florisboard/florisboard/pull/329
## Contributing
Wanna contribute to FlorisBoard? That's great to hear! There are lots of
different ways to help out. Bug reporting, making pull requests,
@@ -176,11 +177,24 @@ to get more information on this topic.
[Jared Rummler](https://github.com/jaredrummler)
* [Timber](https://github.com/JakeWharton/timber) by
[JakeWharton](https://github.com/JakeWharton)
* [kotlin-result](https://github.com/michaelbull/kotlin-result) by
[Michael Bull](https://github.com/michaelbull)
* [expandable-fab](https://github.com/nambicompany/expandable-fab) by
[Nambi](https://github.com/nambicompany)
## 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
sources, as stated below.
### Source 1: [wordfreq library by LuminosoInsight](https://github.com/LuminosoInsight/wordfreq):
`wordfreq` is a repository which provides both a Python library and raw
data (the wordlists). Only the data has been extracted in order to build
binary dictionary files from it. `wordfreq`'s data is licensed under the
Creative Commons Attribution-ShareAlike 4.0 license
(https://creativecommons.org/licenses/by-sa/4.0/).
For further information on what wordfreq's data depends on, see
(https://github.com/LuminosoInsight/wordfreq#license).
## License
```
Copyright 2020 Patrick Goldinger

1
app/.gitignore vendored
View File

@@ -1 +0,0 @@
/build

View File

@@ -1,76 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
defaultConfig {
applicationId "dev.patrickgold.florisboard"
minSdkVersion 23
targetSdkVersion 29
versionCode 22
versionName "0.3.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
viewBinding true
}
buildTypes {
debug {
applicationIdSuffix ".debug"
resValue "string", "app_name", "FlorisBoard Debug"
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
resValue "string", "app_name", "FlorisBoard"
}
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.12'
testImplementation 'androidx.test:core:1.3.0'
testImplementation 'org.mockito:mockito-core:1.10.19'
testImplementation 'org.mockito:mockito-inline:2.13.0'
testImplementation 'org.robolectric:robolectric:4.4'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'com.google.android:flexbox:2.0.1'
implementation "com.squareup.moshi:moshi-kotlin:1.9.2"
implementation 'com.squareup.moshi:moshi-adapters:1.9.2'
implementation 'com.google.android.material:material:1.2.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
implementation 'com.jaredrummler:colorpicker:1.1.0'
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation "com.michael-bull.kotlin-result:kotlin-result:1.1.9"
implementation 'com.nambimobile.widgets:expandable-fab:1.0.2'
}

97
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,97 @@
plugins {
id("com.android.application") version "4.1.2"
kotlin("android") version "1.4.30"
}
android {
compileSdkVersion(30)
buildToolsVersion("30.0.3")
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs = listOf("-Xallow-result-return-type") // enables use of kotlin.Result
}
defaultConfig {
applicationId = "dev.patrickgold.florisboard"
minSdkVersion(23)
targetSdkVersion(30)
versionCode(28)
versionName("0.3.9")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
viewBinding = true
}
buildTypes {
named("debug").configure {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
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")
}
create("beta") // Needed because by default the "beta" BuildType does not exist
named("beta").configure {
applicationIdSuffix = ".beta"
versionNameSuffix = "-beta"
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_beta")
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_beta_round")
resValue("string", "floris_app_name", "FlorisBoard Beta")
}
named("release").configure {
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_release")
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_release_round")
resValue("string", "floris_app_name", "@string/app_name")
}
}
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
lintOptions {
isAbortOnError = false
}
}
dependencies {
implementation("androidx.activity", "activity-ktx", "1.2.1")
implementation("androidx.appcompat", "appcompat", "1.2.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.squareup.moshi", "moshi-kotlin", "1.11.0")
implementation("com.squareup.moshi", "moshi-adapters", "1.11.0")
implementation("com.google.android.material", "material", "1.3.0")
implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-android", "1.4.2")
implementation("com.jaredrummler", "colorpicker", "1.1.0")
implementation("com.jakewharton.timber", "timber", "4.7.1")
implementation("com.nambimobile.widgets", "expandable-fab", "1.0.2")
testImplementation("junit", "junit", "4.13.1")
testImplementation("org.mockito", "mockito-inline", "3.7.7")
testImplementation("org.robolectric", "robolectric", "4.5.1")
androidTestImplementation("androidx.test.ext", "junit", "1.1.2")
androidTestImplementation("androidx.test.espresso", "espresso-core", "3.3.0")
}

View File

@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -1,13 +1,11 @@
package dev.patrickgold.florisboard
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*

View File

@@ -23,17 +23,16 @@
<application
android:name=".ime.core.FlorisApplication"
android:allowBackup="false"
android:extractNativeLibs="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:icon="@mipmap/floris_app_icon"
android:label="@string/floris_app_name"
android:roundIcon="@mipmap/floris_app_icon_round"
android:supportsRtl="true"
android:theme="@style/SettingsTheme">
<!-- IME service -->
<service
android:name="dev.patrickgold.florisboard.ime.core.FlorisBoard"
android:label="@string/app_name"
android:label="@string/floris_app_name"
android:permission="android.permission.BIND_INPUT_METHOD">
<meta-data
android:name="android.view.im"
@@ -47,19 +46,19 @@
<!-- Settings Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.SettingsMainActivity"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/settings__title"
android:launchMode="singleTask"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/floris_app_icon_round"
android:theme="@style/SettingsTheme"/>
<!-- Using an activity alias to disable/enable the app icon in the launcher -->
<activity-alias
android:name="dev.patrickgold.florisboard.SettingsLauncherAlias"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:icon="@mipmap/floris_app_icon"
android:label="@string/floris_app_name"
android:launchMode="singleTask"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/floris_app_icon_round"
android:targetActivity="dev.patrickgold.florisboard.setup.SetupActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -70,45 +69,45 @@
<!-- Theme Selector Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.ThemeManagerActivity"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/settings__title"
android:theme="@style/SettingsTheme"/>
<!-- Theme Editor Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.ThemeEditorActivity"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/settings__theme_editor__title"
android:theme="@style/SettingsTheme"/>
<!-- About Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.AboutActivity"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/about__title"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/floris_app_icon_round"
android:theme="@style/SettingsTheme"/>
<!-- Advanced Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.AdvancedActivity"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/settings__advanced__title"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/floris_app_icon_round"
android:theme="@style/SettingsTheme"/>
<!-- Setup Activity -->
<activity
android:name="dev.patrickgold.florisboard.setup.SetupActivity"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/setup__title"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/floris_app_icon_round"
android:theme="@style/SettingsTheme"/>
<!-- Crash Dialog Activity -->
<activity
android:name="dev.patrickgold.florisboard.crashutility.CrashDialogActivity"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/crash_dialog__title"
android:theme="@style/CrashDialogTheme"/>

View File

@@ -4,6 +4,9 @@
"qwerty": "QWERTY",
"qwertz": "QWERTZ",
"azerty": "AZERTY",
"bepo": "BÉPO",
"bulgarian_bds": "Bulgarian (BDS)",
"bulgarian_phonetic": "Bulgarian (Phonetic)",
"spanish": "Spanish (QWERTY)",
"norwegian": "Norwegian (QWERTY)",
"swedish_finnish": "Swedish/Finnish (QWERTY)",
@@ -21,7 +24,11 @@
"dvorak": "Dvorak",
"jcuken_russian": "Russian (JCUKEN)",
"canadian_french": "Canadian French (QWERTY)",
"greek": "Ελληνικά"
"greek": "Ελληνικά",
"hebrew": "עברית",
"serbian_latin": "Serbian (QWERTZ)",
"serbian_cyrillic": "Serbian (ЉЊЕРТЗ)",
"kurdish": "کوردی"
},
"defaultSubtypes": [
{
@@ -183,6 +190,36 @@
"id": 1601,
"languageTag": "pl",
"preferredLayout": "qwerty"
},
{
"id": 1701,
"languageTag": "bg-bg",
"preferredLayout": "bulgarian_phonetic"
},
{
"id": 1801,
"languageTag": "tr",
"preferredLayout": "qwerty"
},
{
"id": 1901,
"languageTag": "iw-IL",
"preferredLayout": "hebrew"
},
{
"id": 2001,
"languageTag": "ckb",
"preferredLayout": "kurdish"
},
{
"id": 2101,
"languageTag": "sr-RS",
"preferredLayout": "serbian_cyrillic"
},
{
"id": 2201,
"languageTag": "lv-LV",
"preferredLayout": "qwerty"
}
]
}

Binary file not shown.

View File

@@ -0,0 +1,53 @@
{
"type": "characters",
"name": "bepo",
"authors": [ "salamandar" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 98, "label": "b" },
{ "code": 233, "label": "é" },
{ "code": 112, "label": "p" },
{ "code": 111, "label": "o" },
{ "code": 232, "label": "è" },
{ "code": 118, "label": "v" },
{ "code": 100, "label": "d" },
{ "code": 108, "label": "l" },
{ "code": 106, "label": "j" },
{ "code": 122, "label": "z" },
{ "code": 119, "label": "w" }
],
[
{ "code": 97, "label": "a" },
{ "code": 117, "label": "u" },
{ "code": 105, "label": "i" },
{ "code": 101, "label": "e" },
{ "code": 99, "label": "c" },
{ "code": 116, "label": "t" },
{ "code": 115, "label": "s" },
{ "code": 114, "label": "r" },
{ "code": 110, "label": "n" },
{ "code": 109, "label": "m" },
{ "code": 231, "label": "ç" }
],
[
{ "code": 234, "label": "ê" },
{ "code": 224, "label": "à" },
{ "code": 121, "label": "y" },
{ "code": 120, "label": "x" },
{ "code": 107, "label": "k" },
{ "code": 113, "label": "q", "popup": {
"relevant": [
{ "code": 8218, "label": "" },
{ "code": 8216, "label": "" },
{ "code": 8217, "label": "" },
{ "code": 8249, "label": "" },
{ "code": 8250, "label": "" }
]
} },
{ "code": 103, "label": "g" },
{ "code": 104, "label": "h" },
{ "code": 102, "label": "f" }
]
]
}

View File

@@ -0,0 +1,46 @@
{
"type": "characters",
"name": "bulgarian_bds",
"authors": [ "iorvethe" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 1099, "label": "ы" },
{ "code": 1091, "label": "у" },
{ "code": 1077, "label": "е" },
{ "code": 1080, "label": "и" },
{ "code": 1096, "label": "ш" },
{ "code": 1097, "label": "щ" },
{ "code": 1082, "label": "к" },
{ "code": 1089, "label": "с" },
{ "code": 1076, "label": "д" },
{ "code": 1079, "label": "з" },
{ "code": 1094, "label": "ц" }
],
[
{ "code": 1100, "label": "ь" },
{ "code": 1103, "label": "я" },
{ "code": 1072, "label": "а" },
{ "code": 1086, "label": "о" },
{ "code": 1078, "label": "ж" },
{ "code": 1075, "label": "г" },
{ "code": 1090, "label": "т" },
{ "code": 1085, "label": "н" },
{ "code": 1074, "label": "в" },
{ "code": 1084, "label": "м" },
{ "code": 1095, "label": "ч" }
],
[
{ "code": 1102, "label": "ю" },
{ "code": 1081, "label": "й" },
{ "code": 1098, "label": "ъ" },
{ "code": 1101, "label": "э" },
{ "code": 1092, "label": "ф" },
{ "code": 1093, "label": "х" },
{ "code": 1087, "label": "п" },
{ "code": 1088, "label": "р" },
{ "code": 1083, "label": "л" },
{ "code": 1073, "label": "б" }
]
]
}

View File

@@ -0,0 +1,44 @@
{
"type": "characters",
"name": "bulgarian_phonetic",
"authors": [ "iorvethe" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 1103, "label": "я" },
{ "code": 1074, "label": "в" },
{ "code": 1077, "label": "е" },
{ "code": 1088, "label": "р" },
{ "code": 1090, "label": "т" },
{ "code": 1098, "label": "ъ" },
{ "code": 1091, "label": "у" },
{ "code": 1080, "label": "и" },
{ "code": 1086, "label": "о" },
{ "code": 1087, "label": "п" },
{ "code": 1095, "label": "ч" }
],
[
{ "code": 1072, "label": "а" },
{ "code": 1089, "label": "с" },
{ "code": 1076, "label": "д" },
{ "code": 1092, "label": "ф" },
{ "code": 1075, "label": "г" },
{ "code": 1093, "label": "х" },
{ "code": 1081, "label": "й" },
{ "code": 1082, "label": "к" },
{ "code": 1083, "label": "л" },
{ "code": 1096, "label": "ш" },
{ "code": 1097, "label": "щ" }
],
[
{ "code": 1079, "label": "з" },
{ "code": 1100, "label": "ь" },
{ "code": 1094, "label": "ц" },
{ "code": 1078, "label": "ж" },
{ "code": 1073, "label": "б" },
{ "code": 1085, "label": "н" },
{ "code": 1084, "label": "м" },
{ "code": 1102, "label": "ю" }
]
]
}

View File

@@ -18,7 +18,7 @@
"relevant": [
{ "code": 58, "label": ":" }
]
} }
}, "shift": { "code": 58, "label": ":" } }
],
[
{ "code": 97, "label": "a" },

View File

@@ -12,25 +12,25 @@
{ "code": 33, "label": "!" },
{ "code": 34, "label": "\"" }
]
} },
}, "shift": { "code": 34, "label": "\"" } },
{ "code": 39, "label": "'", "groupId": 101, "variation": "password", "popup": {
"relevant": [
{ "code": 33, "label": "!" },
{ "code": 34, "label": "\"" }
]
} },
}, "shift": { "code": 34, "label": "\"" } },
{ "code": 47, "label": "/", "groupId": 101, "variation": "uri" },
{ "code": 44, "label": ",", "popup": {
"relevant": [
{ "code": 60, "label": "<" },
{ "code": 63, "label": "?" }
]
} },
}, "shift": { "code": 60, "label": "<" } },
{ "code": 46, "label": ".", "popup": {
"relevant": [
{ "code": 62, "label": ">" }
]
} },
}, "shift": { "code": 62, "label": ">" } },
{ "code": 112, "label": "p" },
{ "code": 121, "label": "y" },
{ "code": 102, "label": "f" },

View File

@@ -5,18 +5,19 @@
"mapping": {
"all": {
"~enter": {
"main": { "code": -215, "label": "toggle_one_handed_mode", "type": "system_gui" },
"main": { "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
"relevant": [
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" }
{ "code": -216, "label": "toggle_one_handed_mode_right", "type": "system_gui" }
]
},
"~left": {
"main": { "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
"relevant": [
{ "code": -215, "label": "toggle_one_handed_mode", "type": "system_gui" },
{ "code": -215, "label": "toggle_one_handed_mode_left", "type": "system_gui" },
{ "code": -100, "label": "settings", "type": "system_gui" }
]
}
}
}
}

View File

@@ -0,0 +1,45 @@
{
"type": "characters/extended_popups",
"name": "bg",
"authors": [ "iorvethe" ],
"mapping": {
"all": {
"и": {
"relevant": [
{ "code": 1117, "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": ".bg" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}
}
}
}

View File

@@ -0,0 +1,167 @@
{
"type": "characters/extended_popups",
"name": "ckb",
"authors": [ "GoRaN" ],
"mapping": {
"all": {
"ق": {
"relevant": [
{ "code": 1647, "label": "ٯ" }
]
},
"ئ": {
"relevant": [
{"code": 1569, "label": "ء" }
]
},
"ە": {
"relevant": [
{ "code": 1577, "label": "ة" },
{ "code": 1729, "label": "ـہ" }
]
},
"ر": {
"relevant": [
{ "code": 1685, "label": "ڕ" },
{ "code": 1682, "label": "ڒ" }
]
},
"ف": {
"relevant": [
{ "code": 1701, "label": "ڥ" },
{ "code": 1698, "label": "ڢ" },
{ "code": 1700, "label": "ڤ" },
{ "code": 1697, "label": "ڡ" }
]
},
"": {
"relevant": [
{ "code": 65163, "label": "ﺋ" },
{ "code": 1569, "label": "ء" },
{ "code": 65139, "label": "ﹳ" }
]
},
"ع": {
"relevant": [
{ "code": 1551, "label": "؏" },
{ "code": 1594, "label": "غ" }
]
},
"د": {
"relevant": [
{ "code": 1676, "label": "ڌ" },
{ "code": 64390, "label": "ﮆ" },
{ "code": 1584, "label": "ذ" },
{ "code": 1774, "label": "ۮ" }
]
},
"ه": {
"relevant": [
{ "code": 1726, "label": "ھ" }
]
},
"خ": {
"relevant": [
{ "code": 1567, "label": "؟" }
]
},
"س": {
"relevant": [
{ "code": 1589, "label": "ص" }
]
},
"ش": {
"relevant": [
{ "code": 1590, "label": "ض" }
]
},
"ب": {
"relevant": [
{ "code": 65010, "label": "ﷲ" },
{ "code": 65021, "label": "﷽" },
{ "code": 65019, "label": "ﷻ" }
]
},
"م": {
"relevant": [
{ "code": 65018, "label": "ﷺ" },
{ "code": 65012, "label": "ﷴ" }
]
},
"ل": {
"relevant": [
{ "code": 1718, "label": "ڶ" },
{ "code": 1719, "label": "ڷ" },
{ "code": 1717, "label": "ڵ" },
{ "code": 1720, "label": "ڸ" }
]
},
"ا": {
"relevant": [
{ "code": 1571, "label": "أ" },
{ "code": 1573, "label": "إ" },
{ "code": 1570, "label": "آ" },
{ "code": 1649, "label": "ٱ" }
]
},
"ک": {
"relevant": [
{ "code": 1706, "label": "ڪ" },
{ "code": 1603, "label": "ك"}
]
},
"ی": {
"relevant": [
{ "code": 1746, "label": "ے" },
{ "code": 1610, "label": "ي" },
{ "code": 1744, "label": "ې" },
{ "code": 1741, "label": "ۍ" },
{ "code": 1742, "label": "ێ" },
{ "code": 1597, "label": "ؽ" }
]
},
"ۆ": {
"relevant": [
{ "code": 1743, "label": "ۏ" },
{ "code": 1735, "label": "ۇ" },
{ "code": 1737, "label": "ۉ" },
{ "code": 1738, "label": "ۊ" },
{ "code": 1572, "label": "ؤ" },
{ "code": 1739, "label": "ۋ" }
]
},
"~right": {
"main": { "code": 1567, "label": "؟" },
"relevant": [
{ "code": 1600, "label": "ــ" },
{ "code": 33, "label": "!" },
{ "code": 1548, "label": "،" },
{ "code": 44, "label": "," },
{ "code": 1549, "label": "؍" },
{ "code": 1563, "label": "؛" },
{ "code": 59, "label": ";" },
{ "code": 58, "label": ":" },
{ "code": 64, "label": "@" },
{ "code": 35, "label": "#" },
{ "code": 42, "label": "*" },
{ "code": 95, "label": "_" },
{ "code": 45, "label": "-" }
]
}
},
"uri": {
"~right": {
"main": { "code": -255, "label": ".krd"},
"relevant": [
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" },
{ "code": -255, "label": ".iq" },
{ "code": -255, "label": ".tv" }
]
}
}
}
}

View File

@@ -26,23 +26,23 @@
]
},
"e": {
"main": { "code": 233, "label": "é" },
"relevant": [
{ "code": 275, "label": "ē" },
{ "code": 281, "label": "ę" },
{ "code": 279, "label": "ė" },
{ "code": 235, "label": "ë" },
{ "code": 233, "label": "é" },
{ "code": 232, "label": "è" },
{ "code": 234, "label": "ê" }
]
},
"i": {
"main": { "code": 237, "label": "í" },
"relevant": [
{ "code": 299, "label": "ī" },
{ "code": 238, "label": "î" },
{ "code": 303, "label": "į" },
{ "code": 236, "label": "ì" },
{ "code": 237, "label": "í" },
{ "code": 239, "label": "ï" }
]
},
@@ -71,8 +71,8 @@
]
},
"u": {
"main": { "code": 250, "label": "ú" },
"relevant": [
{ "code": 250, "label": "ú" },
{ "code": 363, "label": "ū" },
{ "code": 249, "label": "ù" },
{ "code": 252, "label": "ü" },
@@ -104,9 +104,9 @@
"~right": {
"main": { "code": -255, "label": ".com" },
"relevant": [
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".com.es" },
{ "code": -255, "label": ".es" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}

View File

@@ -5,8 +5,8 @@
"mapping": {
"all": {
"a": {
"main": { "code": 224, "label": "à" },
"relevant": [
{ "code": 224, "label": "à" },
{ "code": 227, "label": "ã" },
{ "code": 229, "label": "å" },
{ "code": 257, "label": "ā" },
@@ -18,8 +18,8 @@
]
},
"e": {
"main": { "code": 232, "label": "è" },
"relevant": [
{ "code": 232, "label": "è" },
{ "code": 275, "label": "ē" },
{ "code": 281, "label": "ę" },
{ "code": 279, "label": "ė" },
@@ -29,8 +29,8 @@
]
},
"i": {
"main": { "code": 236, "label": "ì" },
"relevant": [
{ "code": 236, "label": "ì" },
{ "code": 299, "label": "ī" },
{ "code": 239, "label": "ï" },
{ "code": 303, "label": "į" },
@@ -45,8 +45,8 @@
]
},
"o": {
"main": { "code": 242, "label": "ò" },
"relevant": [
{ "code": 242, "label": "ò" },
{ "code": 186, "label": "º" },
{ "code": 333, "label": "ō" },
{ "code": 339, "label": "œ" },
@@ -58,8 +58,8 @@
]
},
"u": {
"main": { "code": 249, "label": "ù" },
"relevant": [
{ "code": 249, "label": "ù" },
{ "code": 363, "label": "ū" },
{ "code": 251, "label": "û" },
{ "code": 250, "label": "ú" },
@@ -91,10 +91,10 @@
"~right": {
"main": { "code": -255, "label": ".com" },
"relevant": [
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".it" },
{ "code": -255, "label": ".gov.it" },
{ "code": -255, "label": ".edu.it" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".it" },
{ "code": -255, "label": ".net" }
]
}

View File

@@ -0,0 +1,42 @@
{
"type": "characters/extended_popups",
"name": "iw",
"authors": [ "Antony" ],
"mapping": {
"all": {
"~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" },
{ "code": -255, "label": ".co.il" },
{ "code": -255, "label": ".gov.il" }
]
}
}
}
}

View File

@@ -0,0 +1,112 @@
{
"type": "characters/extended_popups",
"name": "lv",
"authors": [ "patrickgold", "eandersons" ],
"mapping": {
"all": {
"a": {
"relevant": [
{ "code": 257, "label": "ā" }
]
},
"c": {
"relevant": [
{ "code": 269, "label": "č" }
]
},
"e": {
"relevant": [
{ "code": 275, "label": "ē" },
{ "code": 8364, "label": "€" }
]
},
"g": {
"relevant": [
{ "code": 291, "label": "ģ" }
]
},
"i": {
"relevant": [
{ "code": 299, "label": "ī" }
]
},
"k": {
"relevant": [
{ "code": 311, "label": "ķ" }
]
},
"l": {
"relevant": [
{ "code": 316, "label": "ļ" }
]
},
"n": {
"relevant": [
{ "code": 326, "label": "ņ" }
]
},
"o": {
"relevant": [
{ "code": 333, "label": "ō" }
]
},
"r": {
"relevant": [
{ "code": 343, "label": "ŗ" }
]
},
"s": {
"relevant": [
{ "code": 353, "label": "š" }
]
},
"u": {
"relevant": [
{ "code": 363, "label": "ū" }
]
},
"z": {
"relevant": [
{ "code": 382, "label": "ž" }
]
},
"~right": {
"main": { "code": 44, "label": "," },
"relevant": [
{ "code": 38, "label": "&" },
{ "code": 37, "label": "%" },
{ "code": 43, "label": "+" },
{ "code": 34, "label": "\"" },
{ "code": 45, "label": "-" },
{ "code": 8212, "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": ".lv" },
"relevant": [
{ "code": -255, "label": ".eu" },
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".gov.lv" },
{ "code": -255, "label": ".edu.lv" },
{ "code": -255, "label": ".org.lv" },
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}
}
}
}

View File

@@ -0,0 +1,109 @@
{
"type": "characters/extended_popups",
"name": "pt-BR",
"authors": [ "rickym7" ],
"mapping": {
"all": {
"a": {
"relevant": [
{ "code": 228, "label": "ä" },
{ "code": 229, "label": "å" },
{ "code": 230, "label": "æ" },
{ "code": 170, "label": "ª" },
{ "code": 225, "label": "á" },
{ "code": 227, "label": "ã" },
{ "code": 224, "label": "à" },
{ "code": 226, "label": "â" }
]
},
"c": {
"relevant": [
{ "code": 269, "label": "č" },
{ "code": 231, "label": "ç" },
{ "code": 263, "label": "ć" }
]
},
"e": {
"relevant": [
{ "code": 275, "label": "ē" },
{ "code": 281, "label": "ę" },
{ "code": 279, "label": "ė" },
{ "code": 235, "label": "ë" },
{ "code": 234, "label": "ê" },
{ "code": 233, "label": "é" },
{ "code": 232, "label": "è" }
]
},
"i": {
"relevant": [
{ "code": 299, "label": "ī" },
{ "code": 239, "label": "ï" },
{ "code": 303, "label": "į" },
{ "code": 236, "label": "ì" },
{ "code": 238, "label": "î" },
{ "code": 237, "label": "í" }
]
},
"n": {
"relevant": [
{ "code": 241, "label": "ñ" },
{ "code": 324, "label": "ń" }
]
},
"o": {
"relevant": [
{ "code": 186, "label": "º" },
{ "code": 333, "label": "ō" },
{ "code": 248, "label": "ø" },
{ "code": 339, "label": "œ" },
{ "code": 246, "label": "ö" },
{ "code": 242, "label": "ò" },
{ "code": 244, "label": "ô" },
{ "code": 245, "label": "õ" },
{ "code": 243, "label": "ó" }
]
},
"u": {
"relevant": [
{ "code": 363, "label": "ū" },
{ "code": 249, "label": "ù" },
{ "code": 251, "label": "û" },
{ "code": 252, "label": "ü" },
{ "code": 250, "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": ".br" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}
}
}
}

View File

@@ -0,0 +1,61 @@
{
"type": "characters/extended_popups",
"name": "sr",
"authors": [ "hedidnothingwrong", "GrbavaCigla" ],
"mapping": {
"all": {
"c": {
"relevant": [
{ "code": 269, "label": "č" },
{ "code": 263, "label": "ć" }
]
},
"d": {
"relevant": [
{ "code": 273, "label": "đ" }
]
},
"s": {
"relevant": [
{ "code": 353, "label": "š" }
]
},
"z": {
"relevant": [
{ "code": 382, "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": ".eu" },
{ "code": -255, "label": ".rs" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}
}
}
}

View File

@@ -0,0 +1,78 @@
{
"type": "characters/extended_popups",
"name": "tr",
"authors": [ "kisekinopureya" ],
"mapping": {
"all": {
"a": {
"relevant": [
{ "code": 226, "label": "â" }
]
},
"c": {
"relevant": [
{ "code": 231, "label": "ç" }
]
},
"g": {
"relevant": [
{ "code": 287, "label": "ğ" }
]
},
"i": {
"relevant": [
{ "code": 238, "label": "î" },
{ "code": 305, "label": "ı" }
]
},
"o": {
"relevant": [
{ "code": 246, "label": "ö" }
]
},
"s": {
"relevant": [
{ "code": 351, "label": "ş" }
]
},
"u": {
"relevant": [
{ "code": 252, "label": "ü" },
{ "code": 251, "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": ".tr" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}
}
}
}

View File

@@ -0,0 +1,53 @@
{
"type": "characters",
"name": "hebrew",
"authors": [ "Antony" ],
"direction": "rtl",
"modifier": "hebrew",
"arrangement": [
[
{ "code": 39, "label": "'", "popup": {
"relevant": [
{ "code": 34, "label": "\"" },
{ "code": 96, "label": "`" }
]
} },
{ "code": 45, "label": "-", "popup": {
"relevant": [
{ "code": 95, "label": "_" }
]
} },
{ "code": 1511, "label": "ק" },
{ "code": 1512, "label": "ר" },
{ "code": 1488, "label": "א" },
{ "code": 1496, "label": "ט" },
{ "code": 1493, "label": "ו" },
{ "code": 1503, "label": "ן" },
{ "code": 1501, "label": "ם" },
{ "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": 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": "ז" }
]
]
}

View File

@@ -0,0 +1,57 @@
{
"type": "characters",
"name": "kurdish",
"authors": [ "GoRaN" ],
"direction": "rtl",
"modifier": "kurdish",
"arrangement": [
[
{ "code": 1602, "label": "ق", "popup": {
"main": { "code": 1647, "label": "ٯ" }
} },
{ "code": 1608, "label": "و", "popup": {
"main": { "code": -255, "label": "وو" }
} },
{ "code": 1749, "label": "", "popup": {
"main": { "code": 1577, "label": "ة" }
} },
{ "code": 1585, "label": "ر" },
{ "code": 1578, "label": "ت", "popup": {
"main": { "code": 1591, "label": "ط" }
} },
{ "code": 1740, "label": "ی" },
{ "code": 1574, "label": "ﺋ", "popup": {
"main": { "code": 1569, "label": "ء" }
} },
{ "code": 1593, "label": "ع" },
{ "code": 1734, "label": "ۆ" },
{ "code": 1662, "label": "پ", "popup": {
"main": { "code": 1579, "label": "ث" }
} }
],
[
{ "code": 1575, "label": "ا" },
{ "code": 1587, "label": "س" },
{ "code": 1588, "label": "ش" },
{ "code": 1583, "label": "د" },
{ "code": 1601, "label": "ف" },
{ "code": 1607, "label": "ھ" },
{ "code": 1688, "label": "ژ" },
{ "code": 1604, "label": "ل" },
{ "code": 1705, "label": "ک" },
{ "code": 1711, "label": "گ" }
],
[
{ "code": 1586, "label": "ز", "popup": {
"main": {"code": 1592, "label": "ظ" }
} },
{ "code": 1582, "label": "خ" },
{ "code": 1580, "label": "ج" },
{ "code": 1670, "label": "چ" },
{ "code": 1581, "label": "ح" },
{ "code": 1576, "label": "ب" },
{ "code": 1606, "label": "ن" },
{ "code": 1605, "label": "م" }
]
]
}

View File

@@ -0,0 +1,27 @@
{
"type": "characters/mod",
"name": "hebrew",
"authors": [ "Antony" ],
"direction": "rtl",
"arrangement": [
[
{ "code": 0, "type": "placeholder" },
{ "code": -5, "label": "delete", "type": "enter_editing" }
],
[
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
{ "code": 64, "label": "@", "groupId": 1, "variation": "email_address" },
{ "code": 44, "label": ",", "groupId": 1, "variation": "normal" },
{ "code": 44, "label": ",", "groupId": 1, "variation": "password" },
{ "code": 47, "label": "/", "groupId": 1, "variation": "uri" },
{ "code": -210, "label": "language_switch", "type": "system_gui" },
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
{ "code": 32, "label": "space" },
{ "code": 46, "label": ".", "groupId": 2, "variation": "email_address" },
{ "code": 46, "label": ".", "groupId": 2, "variation": "normal" },
{ "code": 46, "label": ".", "groupId": 2, "variation": "password" },
{ "code": 46, "label": ".", "groupId": 2, "variation": "uri" },
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
]
]
}

View File

@@ -0,0 +1,27 @@
{
"type": "characters/mod",
"name": "kurdish",
"authors": [ "GoRaN" ],
"direction": "rtl",
"arrangement": [
[
{ "code": 0, "type": "placeholder" },
{ "code": -5, "label": "delete", "type": "enter_editing" }
],
[
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
{ "code": 64, "label": "@", "groupId": 1, "variation": "email_address" },
{ "code": 1567, "label": "؟", "groupId": 1, "variation": "normal" },
{ "code": 1548, "label": "،", "groupId": 1, "variation": "password" },
{ "code": 47, "label": "/", "groupId": 1, "variation": "uri" },
{ "code": -210, "label": "language_switch", "type": "system_gui" },
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
{ "code": 32, "label": " " },
{ "code": 46, "label": ".", "groupId": 2, "variation": "email_address" },
{ "code": 46, "label": ".", "groupId": 2, "variation": "normal" },
{ "code": 46, "label": ".", "groupId": 2, "variation": "password" },
{ "code": 46, "label": ".", "groupId": 2, "variation": "uri" },
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
]
]
}

View File

@@ -0,0 +1,44 @@
{
"type": "characters",
"name": "serbian_cyrillic",
"authors": ["GrbavaCigla"],
"direction": "ltr",
"arrangement": [
[
{ "code": 1113, "label": "љ" },
{ "code": 1114, "label": "њ" },
{ "code": 1077, "label": "е" },
{ "code": 1088, "label": "р" },
{ "code": 1090, "label": "т" },
{ "code": 1079, "label": "з" },
{ "code": 1091, "label": "у" },
{ "code": 1080, "label": "и" },
{ "code": 1086, "label": "о" },
{ "code": 1087, "label": "п" },
{ "code": 1096, "label": "ш" }
],
[
{ "code": 1072, "label": "а" },
{ "code": 1089, "label": "с" },
{ "code": 1076, "label": "д" },
{ "code": 1092, "label": "ф" },
{ "code": 1075, "label": "г" },
{ "code": 1093, "label": "х" },
{ "code": 1112, "label": "ј" },
{ "code": 1082, "label": "к" },
{ "code": 1083, "label": "л" },
{ "code": 1095, "label": "ч" },
{ "code": 1115, "label": "ћ" }
],
[
{ "code": 1119, "label": "џ" },
{ "code": 1094, "label": "ц" },
{ "code": 1074, "label": "в" },
{ "code": 1073, "label": "б" },
{ "code": 1085, "label": "н" },
{ "code": 1084, "label": "м" },
{ "code": 1106, "label": "ђ" },
{ "code": 1078, "label": "ж" }
]
]
}

View File

@@ -0,0 +1,45 @@
{
"type": "characters",
"name": "serbian_latin",
"authors": ["GrbavaCigla"],
"direction": "ltr",
"arrangement": [
[
{ "code": 113, "label": "q" },
{ "code": 119, "label": "w" },
{ "code": 101, "label": "e" },
{ "code": 114, "label": "r" },
{ "code": 116, "label": "t" },
{ "code": 122, "label": "z" },
{ "code": 117, "label": "u" },
{ "code": 105, "label": "i" },
{ "code": 111, "label": "o" },
{ "code": 112, "label": "p" },
{ "code": 353, "label": "š" }
],
[
{ "code": 97, "label": "a" },
{ "code": 115, "label": "s" },
{ "code": 100, "label": "d" },
{ "code": 102, "label": "f" },
{ "code": 103, "label": "g" },
{ "code": 104, "label": "h" },
{ "code": 106, "label": "j" },
{ "code": 107, "label": "k" },
{ "code": 108, "label": "l" },
{ "code": 269, "label": "č" },
{ "code": 263, "label": "ć" }
],
[
{ "code": 121, "label": "y" },
{ "code": 120, "label": "x" },
{ "code": 99, "label": "c" },
{ "code": 118, "label": "v" },
{ "code": 98, "label": "b" },
{ "code": 110, "label": "n" },
{ "code": 109, "label": "m" },
{ "code": 273, "label": "đ" },
{ "code": 382, "label": "ž" }
]
]
}

View File

@@ -48,7 +48,7 @@
{ "code": 48, "label": "0", "type": "numeric" },
{ "code": 61, "label": "=" },
{ "code": 46, "label": "." },
{ "code": 10, "label": "enter", "type": "enter_editing" }
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
]
]
}

View File

@@ -11,10 +11,24 @@
],
[
{ "code": -201, "label": "view_characters", "type": "system_gui" },
{ "code": 60, "label": "<" },
{ "code": 60, "label": "<", "popup": {
"relevant": [
{ "code": 171, "label": "«" },
{ "code": 8804, "label": "≤" },
{ "code": 8249, "label": "" },
{ "code":10216, "label": "⟨" }
]
} },
{ "code": -205, "label": "view_numeric_advanced", "type": "system_gui" },
{ "code": 32, "label": "space" },
{ "code": 62, "label": ">" },
{ "code": 62, "label": ">", "popup": {
"relevant": [
{ "code":10217, "label": "⟩" },
{ "code": 8250, "label": "" },
{ "code": 8805, "label": "≥" },
{ "code": 187, "label": "»" }
]
} },
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
]
]

View File

@@ -21,6 +21,9 @@
{ "code": 960, "label": "π", "popup": {
"main": { "code": 928, "label": "Π" },
"relevant": [
{ "code": 969, "label": "ω" },
{ "code": 945, "label": "α" },
{ "code": 946, "label": "β" },
{ "code": 937, "label": "Ω" },
{ "code": 956, "label": "μ" }
]

View File

@@ -0,0 +1,73 @@
{
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
"name": "floris_black",
"label": "Floris Black",
"authors": [ "serebit" ],
"isNightTheme": true,
"attributes": {
"window": {
"colorPrimary": "#388E3C",
"colorPrimaryDark": "#306D32",
"colorAccent": "#FF9800",
"navigationBarColor": "@keyboard/background",
"navigationBarLight": "false",
"semiTransparentColor": "#20FFFFFF",
"textColor": "#EEEEEE"
},
"keyboard": {
"background": "#000000"
},
"key": {
"background": "#212121",
"backgroundPressed": "#3D3D3D",
"foreground": "@window/textColor",
"foregroundPressed": "@window/textColor",
"showBorder": "true"
},
"key:enter": {
"background": "@window/colorPrimary",
"backgroundPressed": "@window/colorPrimaryDark",
"foreground": "@window/textColor",
"foregroundPressed": "@window/textColor"
},
"key:shift:capslock": {
"foreground": "@window/colorAccent",
"foregroundPressed": "@window/colorAccent"
},
"media": {
"foreground": "@window/textColor",
"foregroundAlt": "#BDBDBD"
},
"oneHanded": {
"background": "#000000",
"foreground": "@window/textColor"
},
"popup": {
"background": "#424242",
"backgroundActive": "#707070",
"foreground": "@window/textColor"
},
"privateMode": {
"background": "#7800BF",
"foreground": "@window/textColor"
},
"smartbar": {
"background": "transparent",
"foreground": "@window/textColor",
"foregroundAlt": "#73FFFFFF"
},
"smartbarButton": {
"background": "@key/background",
"foreground": "@key/foreground"
},
"extractEditLayout": {
"background": "#282828",
"foreground": "@window/textColor",
"foregroundAlt": "#73FFFFFF"
},
"extractActionButton": {
"background": "@smartbarButton/background",
"foreground": "@smartbarButton/foreground"
}
}
}

View File

@@ -0,0 +1,76 @@
{
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
"name": "floris_black_borderless",
"label": "Floris Black Borderless",
"authors": [ "serebit" ],
"isNightTheme": true,
"attributes": {
"window": {
"colorPrimary": "#388E3C",
"colorPrimaryDark": "#306D32",
"colorAccent": "#FF9800",
"navigationBarColor": "@keyboard/background",
"navigationBarLight": "false",
"semiTransparentColor": "#20FFFFFF",
"textColor": "#EEEEEE"
},
"keyboard": {
"background": "#000000"
},
"key": {
"background": "transparent",
"backgroundPressed": "#7F616161",
"foreground": "@window/textColor",
"foregroundPressed": "@window/textColor",
"showBorder": "false"
},
"key:enter": {
"background": "@window/colorPrimary",
"backgroundPressed": "@window/colorPrimaryDark",
"foreground": "@window/textColor",
"foregroundPressed": "@window/textColor"
},
"key:shift:capslock": {
"foreground": "@window/colorAccent",
"foregroundPressed": "@window/colorAccent"
},
"key:space": {
"background": "#46616161"
},
"media": {
"foreground": "@window/textColor",
"foregroundAlt": "#BDBDBD"
},
"oneHanded": {
"background": "#000000",
"foreground": "@window/textColor"
},
"popup": {
"background": "#363636",
"backgroundActive": "#5F5F5F",
"foreground": "@window/textColor"
},
"privateMode": {
"background": "#7800BF",
"foreground": "@window/textColor"
},
"smartbar": {
"background": "transparent",
"foreground": "@window/textColor",
"foregroundAlt": "#73FFFFFF"
},
"smartbarButton": {
"background": "#212121",
"foreground": "@window/textColor"
},
"extractEditLayout": {
"background": "#282828",
"foreground": "@window/textColor",
"foregroundAlt": "#73FFFFFF"
},
"extractActionButton": {
"background": "@smartbarButton/background",
"foreground": "@smartbarButton/foreground"
}
}
}

View File

@@ -59,6 +59,15 @@
"smartbarButton": {
"background": "@key/background",
"foreground": "@key/foreground"
},
"extractEditLayout": {
"background": "#E8E8E8",
"foreground": "@window/textColor",
"foregroundAlt": "#8A8A8A"
},
"extractActionButton": {
"background": "@smartbarButton/background",
"foreground": "@smartbarButton/foreground"
}
}
}

View File

@@ -63,6 +63,15 @@
"smartbarButton": {
"background": "#FFFFFF",
"foreground": "@window/textColor"
},
"extractEditLayout": {
"background": "#E8E8E8",
"foreground": "@window/textColor",
"foregroundAlt": "#8A8A8A"
},
"extractActionButton": {
"background": "@smartbarButton/background",
"foreground": "@smartbarButton/foreground"
}
}
}

View File

@@ -59,6 +59,15 @@
"smartbarButton": {
"background": "@key/background",
"foreground": "@key/foreground"
},
"extractEditLayout": {
"background": "#282828",
"foreground": "@window/textColor",
"foregroundAlt": "#73FFFFFF"
},
"extractActionButton": {
"background": "@smartbarButton/background",
"foreground": "@smartbarButton/foreground"
}
}
}

View File

@@ -63,6 +63,15 @@
"smartbarButton": {
"background": "#424242",
"foreground": "@window/textColor"
},
"extractEditLayout": {
"background": "#282828",
"foreground": "@window/textColor",
"foregroundAlt": "#73FFFFFF"
},
"extractActionButton": {
"background": "@smartbarButton/background",
"foreground": "@smartbarButton/foreground"
}
}
}

View File

@@ -0,0 +1,64 @@
{
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
"name": "gboard_day",
"label": "Gboard Day",
"authors": [ "patrickgold", "itskareem" ],
"isNightTheme": false,
"attributes": {
"window": {
"colorPrimary": "#0479ed",
"colorPrimaryDark": "#0467c9",
"colorAccent": "#FF9800",
"navigationBarColor": "@keyboard/background",
"navigationBarLight": "true",
"semiTransparentColor": "#20000000",
"textColor": "#000000"
},
"keyboard": {
"background": "#D1D6DC"
},
"key": {
"background": "#FCFFFF",
"backgroundPressed": "#F5F5F5",
"foreground": "@window/textColor",
"foregroundPressed": "@window/textColor",
"showBorder": "true"
},
"key:enter": {
"background": "@window/colorPrimary",
"backgroundPressed": "@window/colorPrimaryDark",
"foreground": "#FFFFFF",
"foregroundPressed": "#FFFFFF"
},
"key:shift:capslock": {
"foreground": "@window/colorAccent",
"foregroundPressed": "@window/colorAccent"
},
"media": {
"foreground": "@window/textColor",
"foregroundAlt": "#757575"
},
"oneHanded": {
"background": "@keyboard/background",
"foreground": "#424242"
},
"popup": {
"background": "#EEEEEE",
"backgroundActive": "#BDBDBD",
"foreground": "@window/textColor"
},
"privateMode": {
"background": "#A000FF",
"foreground": "#FFFFFF"
},
"smartbar": {
"background": "@keyboard/background",
"foreground": "@window/textColor",
"foregroundAlt": "#8A8A8A"
},
"smartbarButton": {
"background": "@key/background",
"foreground": "@key/foreground"
}
}
}

View File

@@ -800,6 +800,384 @@ shall not be used in advertising or otherwise to promote the sale,
use or other dealings in these Data Files or Software without prior
written authorization of the copyright holder.
</pre>
<hr>
<h3>Dictionary Source 1: wordfreq data</h3>
<span>Copyright (c) 2015 Luminoso Technologies, Inc.</span>
<pre>
Creative Commons Attribution-ShareAlike 4.0 International Public
License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-ShareAlike 4.0 International Public License ("Public
License"). To the extent this Public License may be interpreted as a
contract, You are granted the Licensed Rights in consideration of Your
acceptance of these terms and conditions, and the Licensor grants You
such rights in consideration of benefits the Licensor receives from
making the Licensed Material available under these terms and
conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. BY-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
e. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name
of a Creative Commons Public License. The License Elements of this
Public License are Attribution and ShareAlike.
h. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
i. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
k. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
l. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
m. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part; and
b. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. Additional offer from the Licensor -- Adapted Material.
Every recipient of Adapted Material from You
automatically receives an offer from the Licensor to
exercise the Licensed Rights in the Adapted Material
under the conditions of the Adapter's License You apply.
c. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share
Adapted Material You produce, the following conditions also apply.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
later, or a BY-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition
in any reasonable manner based on the medium, means, and
context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms
or conditions on, or apply any Effective Technological
Measures to, Adapted Material that restrict exercise of the
rights granted under the Adapter's License You apply.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material,
including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the “Licensor.” The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.
</pre>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,117 @@
/*
* 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.core
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding
import com.google.android.material.snackbar.Snackbar
import dev.patrickgold.florisboard.R
abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity() {
private var _binding: V? = null
protected val binding: V
get() = _binding!!
private var _prefs: PrefHelper? = null
protected val prefs: PrefHelper
get() = _prefs!!
private var errorDialog: AlertDialog? = null
private var errorSnackbar: Snackbar? = null
private var errorThrowable: Throwable? = null
private var messageSnackbar: Snackbar? = null
protected abstract fun onCreateBinding(): V
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_prefs = PrefHelper.getDefaultInstance(applicationContext)
onCreateBinding().let {
_binding = it
setContentView(it.root)
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
_prefs = null
errorDialog?.dismiss()
errorDialog = null
errorSnackbar?.dismiss()
errorSnackbar = null
errorThrowable = null
messageSnackbar?.dismiss()
messageSnackbar = null
}
protected fun showMessage(@StringRes snackbarMessageResId: Int) {
val snackbarMessage = resources.getString(snackbarMessageResId)
showMessage(snackbarMessage)
}
protected fun showMessage(snackbarMessage: String) {
messageSnackbar?.dismiss()
messageSnackbar = Snackbar.make(binding.root, snackbarMessage, Snackbar.LENGTH_LONG).apply {
setAction(android.R.string.ok) {
messageSnackbar?.dismiss()
}
show() }
}
protected fun showError(throwable: Throwable) {
val snackbarMessage = resources.getString(R.string.assets__error__snackbar_message)
showError(snackbarMessage, throwable)
}
protected fun showError(@StringRes snackbarMessageResId: Int, throwable: Throwable) {
val snackbarMessage = resources.getString(snackbarMessageResId)
showError(snackbarMessage, throwable)
}
protected fun showError(snackbarMessage: String, throwable: Throwable) {
errorDialog?.dismiss()
errorDialog = null
errorSnackbar?.dismiss()
errorSnackbar = Snackbar.make(binding.root, snackbarMessage, Snackbar.LENGTH_LONG).apply {
setAction(R.string.assets__error__details) {
errorDialog?.dismiss()
errorDialog = AlertDialog.Builder(this@FlorisActivity).run {
setTitle(R.string.assets__error__details)
setMessage(errorThrowable.toString())
setPositiveButton(android.R.string.ok, null)
setNeutralButton(R.string.crash_dialog__copy_to_clipboard) { _, _ ->
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE)
if (clipboardManager != null && clipboardManager is ClipboardManager) {
clipboardManager.setPrimaryClip(ClipData.newPlainText(errorThrowable.toString(), errorThrowable.toString()))
}
}
create()
show()
}
}
show()
}
errorThrowable = throwable
}
}

View File

@@ -19,6 +19,7 @@ package dev.patrickgold.florisboard.ime.core
import android.app.Application
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.crashutility.CrashUtility
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
import dev.patrickgold.florisboard.ime.extension.AssetManager
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import timber.log.Timber
@@ -32,6 +33,7 @@ class FlorisApplication : Application() {
CrashUtility.install(this)
val prefHelper = PrefHelper.getDefaultInstance(this)
val assetManager = AssetManager.init(this)
DictionaryManager.init(this)
ThemeManager.init(this, assetManager, prefHelper)
prefHelper.initDefaultPreferences()
}

View File

@@ -20,23 +20,31 @@ import android.annotation.SuppressLint
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Color
import android.inputmethodservice.ExtractEditText
import android.inputmethodservice.InputMethodService
import android.media.AudioManager
import android.os.*
import android.os.Build
import android.os.Handler
import android.os.VibrationEffect
import android.os.Vibrator
import android.provider.Settings
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.view.*
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.lifecycle.*
import com.squareup.moshi.Json
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
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.gestures.SwipeAction
@@ -45,10 +53,11 @@ import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.settings.SettingsMainActivity
import dev.patrickgold.florisboard.setup.SetupActivity
import dev.patrickgold.florisboard.util.*
import timber.log.Timber
import java.lang.ref.WeakReference
import java.util.concurrent.CopyOnWriteArrayList
/**
* Variable which holds the current [FlorisBoard] instance. To get this instance from another
@@ -60,27 +69,42 @@ 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.
*/
class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedListener,
class FlorisBoard : InputMethodService(), LifecycleOwner, ClipboardManager.OnPrimaryClipChangedListener,
ThemeManager.OnThemeUpdatedListener {
lateinit var prefs: PrefHelper
private set
val context: Context
get() = inputWindowView?.context ?: this
private val serviceLifecycleDispatcher: ServiceLifecycleDispatcher = ServiceLifecycleDispatcher(this)
private var extractEditLayout: WeakReference<ViewGroup?> = WeakReference(null)
var inputView: InputView? = null
private set
private var inputWindowView: InputWindowView? = null
var popupLayerView: PopupLayerView? = null
private set
private var inputWindowView: InputWindowView? = null
private var eventListeners: MutableList<WeakReference<EventListener?>?> = mutableListOf()
private var eventListeners: CopyOnWriteArrayList<EventListener> = CopyOnWriteArrayList()
private var audioManager: AudioManager? = null
private var imeManager:InputMethodManager? = null
var imeManager:InputMethodManager? = null
var clipboardManager: ClipboardManager? = null
private val themeManager: ThemeManager = ThemeManager.default()
private var vibrator: Vibrator? = null
private val osHandler = Handler()
private var internalBatchNestingLevel: Int = 0
private val internalSelectionCache = object {
var selectionCatchCount: Int = 0
var oldSelStart: Int = -1
var oldSelEnd: Int = -1
var newSelStart: Int = -1
var newSelEnd: Int = -1
var candidatesStart: Int = -1
var candidatesEnd: Int = -1
}
var activeEditorInstance: EditorInstance = EditorInstance.default()
lateinit var subtypeManager: SubtypeManager
@@ -88,6 +112,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
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
@@ -101,18 +126,22 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
companion object {
private const val IME_ID: String = "dev.patrickgold.florisboard/.ime.core.FlorisBoard"
private const val IME_ID_BETA: String = "dev.patrickgold.florisboard.beta/dev.patrickgold.florisboard.ime.core.FlorisBoard"
private const val IME_ID_DEBUG: String = "dev.patrickgold.florisboard.debug/dev.patrickgold.florisboard.ime.core.FlorisBoard"
fun checkIfImeIsEnabled(context: Context): Boolean {
val activeImeIds = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ENABLED_INPUT_METHODS
)
) ?: "(none)"
Timber.i("List of active IMEs: $activeImeIds")
return when {
BuildConfig.DEBUG -> {
activeImeIds.split(":").contains(IME_ID_DEBUG)
}
context.packageName.endsWith(".beta") -> {
activeImeIds.split(":").contains(IME_ID_BETA)
}
else -> {
activeImeIds.split(":").contains(IME_ID)
}
@@ -123,12 +152,15 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
val selectedImeId = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.DEFAULT_INPUT_METHOD
)
) ?: "(none)"
Timber.i("Selected IME: $selectedImeId")
return when {
BuildConfig.DEBUG -> {
selectedImeId == IME_ID_DEBUG
}
context.packageName.endsWith(".beta") -> {
selectedImeId.split(":").contains(IME_ID_BETA)
}
else -> {
selectedImeId == IME_ID
}
@@ -153,8 +185,12 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
}
}
override fun getLifecycle(): Lifecycle {
return serviceLifecycleDispatcher.lifecycle
}
override fun onCreate() {
if (BuildConfig.DEBUG) {
/*if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
@@ -171,14 +207,16 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
.penaltyDeath()
.build()
)
}
}*/
Timber.i("onCreate()")
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
serviceLifecycleDispatcher.onServicePreSuperOnCreate()
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
audioManager = getSystemService(Context.AUDIO_SERVICE) as? AudioManager
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
clipboardManager?.addPrimaryClipChangedListener(this)
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
prefs = PrefHelper.getDefaultInstance(this)
prefs.initDefaultPreferences()
prefs.sync()
@@ -194,7 +232,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
super.onCreate()
eventListeners.toList().forEach { it?.get()?.onCreate() }
eventListeners.toList().forEach { it?.onCreate() }
}
@SuppressLint("InflateParams")
@@ -203,25 +241,41 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
baseContext.setTheme(currentThemeResId)
inputWindowView = layoutInflater.inflate(R.layout.florisboard, null) as InputWindowView
popupLayerView = inputWindowView?.findViewById(R.id.popup_layer)
inputWindowView = layoutInflater.inflate(R.layout.florisboard, null) as? InputWindowView
inputWindowView?.isHapticFeedbackEnabled = true
eventListeners.toList().forEach { it?.get()?.onCreateInputView() }
eventListeners.toList().forEach { it?.onCreateInputView() }
return inputWindowView
}
fun registerInputView(inputView: InputView) {
Timber.i("registerInputView($inputView)")
/**
* Disable the default candidates view.
*/
override fun onCreateCandidatesView(): View? {
return null
}
this.inputView = inputView
initializeOneHandedEnvironment()
updateSoftInputWindowLayoutParameters()
updateOneHandedPanelVisibility()
themeManager.notifyCallbackReceivers()
setActiveInput(R.id.text_input)
eventListeners.toList().forEach { it?.get()?.onRegisterInputView(inputView) }
@SuppressLint("InflateParams")
override fun onCreateExtractTextView(): View? {
val eel = super.onCreateExtractTextView()
if (eel !is ViewGroup) {
return null
}
extractEditLayout = WeakReference(eel)
eel.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
extractEditLayout.get()?.let { eel ->
eel.viewTreeObserver.removeOnGlobalLayoutListener(this)
eel.layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT
).also {
it.setMargins(0, 0, 0, 0)
}
}
}
})
return eel
}
override fun onDestroy() {
@@ -232,16 +286,63 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
osHandler.removeCallbacksAndMessages(null)
florisboardInstance = null
eventListeners.toList().forEach { it?.get()?.onDestroy() }
serviceLifecycleDispatcher.onServicePreSuperOnDestroy()
eventListeners.toList().forEach { it?.onDestroy() }
eventListeners.clear()
super.onDestroy()
}
override fun onEvaluateFullscreenMode(): Boolean {
return resources?.configuration?.let { config ->
if (config.orientation != Configuration.ORIENTATION_LANDSCAPE) {
false
} else {
when (prefs.keyboard.landscapeInputUiMode) {
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeEditorInstance.imeOptions.flagNoFullscreen && !activeEditorInstance.imeOptions.flagNoExtractUi
LandscapeInputUiMode.NEVER_SHOW -> false
LandscapeInputUiMode.ALWAYS_SHOW -> true
}
}
} ?: false
}
override fun updateFullscreenMode() {
super.updateFullscreenMode()
updateSoftInputWindowLayoutParameters()
}
override fun onUpdateExtractingVisibility(ei: EditorInfo?) {
isExtractViewShown = !activeEditorInstance.isRawInputEditor && when (prefs.keyboard.landscapeInputUiMode) {
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeEditorInstance.imeOptions.flagNoExtractUi
LandscapeInputUiMode.NEVER_SHOW -> false
LandscapeInputUiMode.ALWAYS_SHOW -> true
}
}
fun registerInputView(inputView: InputView) {
Timber.i("registerInputView($inputView)")
window?.window?.findViewById<View>(android.R.id.content)?.let { content ->
popupLayerView = PopupLayerView(content.context)
if (content is ViewGroup) {
content.addView(popupLayerView)
}
}
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) {
Timber.i("onStartInput($attribute, $restarting)")
super.onStartInput(attribute, restarting)
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE)
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
}
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
@@ -252,7 +353,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
activeEditorInstance = EditorInstance.from(info, this)
themeManager.updateRemoteColorValues(activeEditorInstance.packageName)
eventListeners.toList().forEach {
it?.get()?.onStartInputView(activeEditorInstance, restarting)
it?.onStartInputView(activeEditorInstance, restarting)
}
}
@@ -264,7 +365,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
}
super.onFinishInputView(finishingInput)
eventListeners.toList().forEach { it?.get()?.onFinishInputView(finishingInput) }
eventListeners.toList().forEach { it?.onFinishInputView(finishingInput) }
}
override fun onFinishInput() {
@@ -275,7 +376,13 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
}
override fun onWindowShown() {
Timber.i("onWindowShown()")
if (isWindowShown) {
Timber.i("Ignoring onWindowShown()")
return
} else {
Timber.i("onWindowShown()")
}
isWindowShown = true
prefs.sync()
val newIsNumberRowVisible = prefs.keyboard.numberRow
@@ -290,14 +397,20 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
setActiveInput(R.id.text_input)
super.onWindowShown()
eventListeners.toList().forEach { it?.get()?.onWindowShown() }
eventListeners.toList().forEach { it?.onWindowShown() }
}
override fun onWindowHidden() {
Timber.i("onWindowHidden()")
if (!isWindowShown) {
Timber.i("Ignoring onWindowHidden()")
return
} else {
Timber.i("onWindowHidden()")
}
isWindowShown = false
super.onWindowHidden()
eventListeners.toList().forEach { it?.get()?.onWindowHidden() }
eventListeners.toList().forEach { it?.onWindowHidden() }
}
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -309,23 +422,74 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
super.onConfigurationChanged(newConfig)
}
/**
* Begins a FlorisBoard internal batch edit. This enables the application to continue sending selection updates
* (some apps need to to this else they absolutely refuse to give visual feedback on cursor movement etc.). The
* selection update is then caught if [internalBatchNestingLevel] is greater than 0, thus not delegating the
* update to the editor instance. This is needed because else the UI stutters when too many updates arrive in a
* row.
*/
fun beginInternalBatchEdit() {
internalBatchNestingLevel++
}
/**
* Ends an internal batch edit, if [internalBatchNestingLevel] is <= 1 and calls [onUpdateSelection] with the
* corresponding reported selection values. This call is not caught and the editor instance and other classes are
* able to update the UI. Resets the internal selection cache and is ready for the next batch edit.
*/
fun endInternalBatchEdit() {
internalBatchNestingLevel = (internalBatchNestingLevel - 1).coerceAtLeast(0)
if (internalBatchNestingLevel == 0) {
internalSelectionCache.apply {
if (selectionCatchCount > 0) {
onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
selectionCatchCount = 0
oldSelStart = -1
oldSelEnd = -1
newSelStart = -1
newSelEnd = -1
candidatesStart = -1
candidatesEnd = -1
}
}
}
}
override fun onUpdateSelection(
oldSelStart: Int, oldSelEnd: Int,
newSelStart: Int, newSelEnd: Int,
candidatesStart: Int, candidatesEnd: Int
) {
Timber.i("onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
super.onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
activeEditorInstance.onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd
)
eventListeners.toList().forEach { it?.get()?.onUpdateSelection() }
if (internalBatchNestingLevel == 0) {
Timber.i("onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
activeEditorInstance.onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
eventListeners.toList().forEach { it?.onUpdateSelection() }
} else {
Timber.i("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
}
internalSelectionCache.newSelStart = newSelStart
internalSelectionCache.newSelEnd = newSelEnd
internalSelectionCache.candidatesStart = candidatesStart
internalSelectionCache.candidatesEnd = candidatesEnd
}
}
override fun onThemeUpdated(theme: Theme) {
@@ -367,15 +531,36 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
// Update InputView theme
inputView?.setBackgroundColor(theme.getAttr(Theme.Attr.KEYBOARD_BACKGROUND).toSolidColor().color)
inputView?.oneHandedCtrlPanelStart?.setBackgroundColor(theme.getAttr(Theme.Attr.ONE_HANDED_BACKGROUND).toSolidColor().color)
inputView?.oneHandedCtrlPanelEnd?.setBackgroundColor(theme.getAttr(Theme.Attr.ONE_HANDED_BACKGROUND).toSolidColor().color)
ColorStateList.valueOf(theme.getAttr(Theme.Attr.ONE_HANDED_FOREGROUND).toSolidColor().color).also {
inputView?.oneHandedCtrlMoveStart?.imageTintList = it
inputView?.oneHandedCtrlMoveEnd?.imageTintList = it
inputView?.oneHandedCtrlCloseStart?.imageTintList = it
inputView?.oneHandedCtrlCloseEnd?.imageTintList = it
inputView?.invalidate()
// Update ExtractTextView theme and attributes
extractEditLayout.get()?.let { eel ->
val p = resources.getDimension(R.dimen.landscapeInputUi_padding).toInt()
eel.setPadding(p, p, 0, p)
eel.setBackgroundColor(theme.getAttr(Theme.Attr.EXTRACT_EDIT_LAYOUT_BACKGROUND).toSolidColor().color)
eel.findViewById<ExtractEditText>(android.R.id.inputExtractEditText)?.let { eet ->
val p2 = resources.getDimension(R.dimen.landscapeInputUi_editText_padding).toInt()
eet.setPadding(p2, p2, p2, p2)
eet.background = ContextCompat.getDrawable(this, R.drawable.edit_text_background)?.also { d ->
DrawableCompat.setTint(d, theme.getAttr(Theme.Attr.WINDOW_COLOR_PRIMARY).toSolidColor().color)
}
eet.setTextColor(theme.getAttr(Theme.Attr.EXTRACT_EDIT_LAYOUT_FOREGROUND).toSolidColor().color)
eet.setHintTextColor(theme.getAttr(Theme.Attr.EXTRACT_EDIT_LAYOUT_FOREGROUND_ALT).toSolidColor().color)
eet.highlightColor = theme.getAttr(Theme.Attr.WINDOW_COLOR_PRIMARY).toSolidColor().color
}
eel.findViewWithType(FrameLayout::class)?.let { fra ->
fra.background = null
}
eel.findViewWithType(Button::class)?.let { btn ->
btn.background = ContextCompat.getDrawable(this, R.drawable.shape_rect_rounded)?.also { d ->
DrawableCompat.setTint(d, theme.getAttr(Theme.Attr.EXTRACT_ACTION_BUTTON_BACKGROUND).toSolidColor().color)
}
btn.setTextColor(theme.getAttr(Theme.Attr.EXTRACT_ACTION_BUTTON_FOREGROUND).toSolidColor().color)
}
eel.invalidate()
}
eventListeners.toList().forEach { it?.get()?.onApplyThemeAttributes() }
eventListeners.toList().forEach { it?.onApplyThemeAttributes() }
}
override fun onComputeInsets(outInsets: Insets?) {
@@ -393,11 +578,6 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
outInsets?.visibleTopInsets = visibleTopY
}
override fun updateFullscreenMode() {
super.updateFullscreenMode()
updateSoftInputWindowLayoutParameters()
}
/**
* Updates the layout params of the window and input view.
*/
@@ -425,7 +605,12 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
if (prefs.keyboard.vibrationEnabled) {
var vibrationStrength = prefs.keyboard.vibrationStrength
if (vibrationStrength == -1 && prefs.keyboard.vibrationEnabledSystem) {
vibrationStrength = 36
val hapticsPerformed =
inputWindowView?.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
if (hapticsPerformed == false) {
vibrationStrength = 36
}
}
if (vibrationStrength > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -477,15 +662,15 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
}
/**
* Hides the IME and launches [SettingsMainActivity].
* Hides the IME and launches [SetupActivity].
*/
fun launchSettings() {
requestHideSelf(0)
val i = Intent(this, SettingsMainActivity::class.java)
val i = Intent(this, SetupActivity::class.java)
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED or
Intent.FLAG_ACTIVITY_CLEAR_TOP
startActivity(i)
applicationContext.startActivity(i)
}
/**
@@ -501,6 +686,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
switchToPreviousInputMethod()
} else {
window.window?.let { window ->
@Suppress("DEPRECATION")
imeManager?.switchToLastInputMethod(window.attributes.token)
}
}
@@ -510,6 +696,22 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
}
}
fun switchToNextKeyboard(){
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
switchToNextInputMethod(false)
} else {
window.window?.let { window ->
@Suppress("DEPRECATION")
imeManager?.switchToNextInputMethod(window.attributes.token, false)
}
}
} catch (e: Exception) {
Timber.e(e,"Unable to switch to the next IME")
imeManager?.showInputMethodPicker()
}
}
fun switchToPrevSubtype() {
activeSubtype = subtypeManager.switchToPrevSubtype() ?: Subtype.DEFAULT
onSubtypeChanged(activeSubtype)
@@ -536,58 +738,29 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
}
}
private fun initializeOneHandedEnvironment() {
{ v:View -> onOneHandedPanelButtonClick(v) }.also {
inputView?.oneHandedCtrlMoveStart?.setOnClickListener(it)
inputView?.oneHandedCtrlMoveEnd?.setOnClickListener(it)
inputView?.oneHandedCtrlCloseStart?.setOnClickListener(it)
inputView?.oneHandedCtrlCloseEnd?.setOnClickListener(it)
}
}
private fun onOneHandedPanelButtonClick(v: View) {
when (v.id) {
R.id.one_handed_ctrl_move_start -> {
prefs.keyboard.oneHandedMode = "start"
}
R.id.one_handed_ctrl_move_end -> {
prefs.keyboard.oneHandedMode = "end"
}
R.id.one_handed_ctrl_close_start,
R.id.one_handed_ctrl_close_end -> {
prefs.keyboard.oneHandedMode = "off"
}
fun toggleOneHandedMode(isRight: Boolean) {
prefs.keyboard.oneHandedMode = when (prefs.keyboard.oneHandedMode) {
OneHandedMode.OFF -> if (isRight) { OneHandedMode.END } else { OneHandedMode.START }
else -> OneHandedMode.OFF
}
updateOneHandedPanelVisibility()
}
fun toggleOneHandedMode() {
when (prefs.keyboard.oneHandedMode) {
"off" -> {
prefs.keyboard.oneHandedMode = "end"
}
else -> {
prefs.keyboard.oneHandedMode = "off"
}
}
updateOneHandedPanelVisibility()
}
private fun updateOneHandedPanelVisibility() {
fun updateOneHandedPanelVisibility() {
if (resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
} else {
when (prefs.keyboard.oneHandedMode) {
"off" -> {
OneHandedMode.OFF -> {
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
}
"start" -> {
OneHandedMode.START -> {
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
inputView?.oneHandedCtrlPanelEnd?.visibility = View.VISIBLE
}
"end" -> {
OneHandedMode.END -> {
inputView?.oneHandedCtrlPanelStart?.visibility = View.VISIBLE
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
}
@@ -600,7 +773,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
}
override fun onPrimaryClipChanged() {
eventListeners.toList().forEach { it?.get()?.onPrimaryClipChanged() }
eventListeners.toList().forEach { it?.onPrimaryClipChanged() }
}
/**
@@ -610,7 +783,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
* @return True if the listener has been added successfully, false otherwise.
*/
fun addEventListener(listener: EventListener): Boolean {
return eventListeners.add(WeakReference(listener))
return eventListeners.add(listener)
}
/**
@@ -623,12 +796,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
* value may also indicate that the [listener] was not added previously.
*/
fun removeEventListener(listener: EventListener): Boolean {
eventListeners.toList().forEach {
if (it?.get() == listener) {
return eventListeners.remove(it)
}
}
return false
return eventListeners.remove(listener)
}
interface EventListener {
@@ -673,14 +841,21 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
val defaultSubtypesLanguageNames: List<String>
init {
val tmpCodes = mutableListOf<String>()
val tmpNames = mutableListOf<String>()
val tmpList = mutableListOf<Pair<String, String>>()
for (defaultSubtype in defaultSubtypes) {
tmpCodes.add(defaultSubtype.locale.toString())
tmpNames.add(defaultSubtype.locale.displayName)
tmpList.add(Pair(defaultSubtype.locale.toString(), defaultSubtype.locale.displayName))
}
defaultSubtypesLanguageCodes = tmpCodes.toList()
defaultSubtypesLanguageNames = tmpNames.toList()
// Sort language list alphabetically by the display name of a language
tmpList.sortBy { it.second }
// Move selected English variants to the top of the list
for (languageCode in listOf("en_CA", "en_AU", "en_UK", "en_US")) {
val index: Int = tmpList.indexOfFirst { it.first == languageCode }
if (index > 0) {
tmpList.add(0, tmpList.removeAt(index))
}
}
defaultSubtypesLanguageCodes = tmpList.map { it.first }.toList()
defaultSubtypesLanguageNames = tmpList.map { it.second }.toList()
}
}
}

View File

@@ -19,7 +19,6 @@ package dev.patrickgold.florisboard.ime.core
import android.content.Context
import android.util.AttributeSet
import android.widget.ViewFlipper
import java.lang.IllegalArgumentException
/**
* Custom ViewFlipper class used to prevent an unnecessary exception to be thrown when it is

View File

@@ -0,0 +1,373 @@
/*
* 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.core
import android.os.SystemClock
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* The main logic point of processing input events and delegating them to the registered event receivers. Currently,
* only [InputKeyEvent]s are supported, but in the future this class is thought to be the single point where input
* events can be dispatched.
*/
class InputEventDispatcher private constructor(
parentScope: CoroutineScope,
channelCapacity: Int,
private val mainDispatcher: CoroutineDispatcher,
private val defaultDispatcher: CoroutineDispatcher,
private val repeatableKeyCodes: IntArray
) : InputKeyEventSender {
private val channel: Channel<InputKeyEvent> = Channel(channelCapacity)
private val scope: CoroutineScope = CoroutineScope(parentScope.coroutineContext)
private val pressedKeys: HashMap<Int, PressedKeyInfo> = hashMapOf()
var lastKeyEventDown: InputKeyEvent? = null
private set
var lastKeyEventUp: InputKeyEvent? = null
private set
/**
* The input key event register. If null, the dispatcher will still process input, but won't dispatch them to an
* event receiver.
*/
var keyEventReceiver: InputKeyEventReceiver? = null
companion object {
/**
* The default input event channel capacity to be used in [new].
*/
private const val DEFAULT_CHANNEL_CAPACITY: Int = 32
/**
* Creates a new [InputEventDispatcher] instance from given arguments and returns it.
*
* @param parentScope The parent coroutine scope which this dispatcher will attach its own scope to.
* @param channelCapacity The capacity of this input channel, defaults to [DEFAULT_CHANNEL_CAPACITY].
* @param mainDispatcher The main dispatcher used to switch the context to call the receiver callbacks.
* Defaults to [Dispatchers.Main].
* @param defaultDispatcher The default dispatcher used to switch the context to call the receiver callbacks.
* Defaults to [Dispatchers.Default].
* @param repeatableKeyCodes An int array of all key codes which are repeatable while being pressed down.
*
* @return A new [InputEventDispatcher] instance initialized with given arguments.
*/
fun new(
parentScope: CoroutineScope,
channelCapacity: Int = DEFAULT_CHANNEL_CAPACITY,
mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
repeatableKeyCodes: IntArray = intArrayOf()
): InputEventDispatcher = InputEventDispatcher(
parentScope, channelCapacity, mainDispatcher, defaultDispatcher, repeatableKeyCodes.clone()
)
}
init {
scope.launch(defaultDispatcher) {
for (ev in channel) {
if (!isActive) break
val startTime = System.nanoTime()
if (BuildConfig.DEBUG) {
Timber.d(ev.toString())
}
when (ev.action) {
InputKeyEvent.Action.DOWN -> {
if (pressedKeys.containsKey(ev.data.code)) continue
pressedKeys[ev.data.code] = PressedKeyInfo(
eventTimeDown = ev.eventTime,
repeatKeyPressJob = if (!repeatableKeyCodes.contains(ev.data.code)) { null } else {
scope.launch(defaultDispatcher) {
delay(600)
while (isActive) {
channel.send(InputKeyEvent.repeat(ev.data))
delay(50)
}
}
}
)
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyDown(ev)
}
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
lastKeyEventDown = ev
}
}
InputKeyEvent.Action.DOWN_UP -> {
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyDown(ev)
keyEventReceiver?.onInputKeyUp(ev)
}
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
lastKeyEventDown = ev
lastKeyEventUp = ev
}
}
InputKeyEvent.Action.UP -> {
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyUp(ev)
}
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
lastKeyEventUp = ev
}
}
InputKeyEvent.Action.REPEAT -> {
if (pressedKeys.containsKey(ev.data.code)) {
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyRepeat(ev)
}
}
}
InputKeyEvent.Action.CANCEL -> {
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyCancel(ev)
}
}
}
if (BuildConfig.DEBUG) {
Timber.d("Time elapsed: ${(System.nanoTime() - startTime) / 1_000_000}")
}
}
val pressedKeysIterator = pressedKeys.iterator()
while (pressedKeysIterator.hasNext()) {
pressedKeysIterator.next().value.repeatKeyPressJob?.cancel()
pressedKeysIterator.remove()
}
}
}
override fun send(ev: InputKeyEvent) {
scope.launch(mainDispatcher) {
channel.send(ev)
}
}
/**
* Checks if there's currently a key down with given [code].
*
* @param code The key code to check for.
*
* @return True if the given [code] is currently down, false otherwise.
*/
fun isPressed(code: Int): Boolean {
return pressedKeys.containsKey(code)
}
/**
* Closes this dispatcher and cancels the local coroutine scope.
*/
fun close() {
keyEventReceiver = null
scope.cancel()
}
data class PressedKeyInfo(
val eventTimeDown: Long,
val repeatKeyPressJob: Job?
)
}
/**
* Data class representing a single input key event.
*
* @property eventTime The exact event time when this event occurred, measured in milliseconds since a static point in
* the past. The exact point is irrelevant, but while this input dispatcher is active, the point must not change in
* order for difference time calculation to succeed.
* @property action The action of this event.
* @property data The data of this event.
* @property count The count how often this event occurred. Is only respected by other methods if the [action] of this
* event is [Action.DOWN_UP] or [Action.REPEAT], else always 1 is assumed.
*/
data class InputKeyEvent(
val eventTime: Long,
val action: Action,
val data: KeyData,
val count: Int
) {
companion object {
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.DOWN].
*
* @param keyData The key data of the input key event event to create.
*
* @return The created input key event.
*/
fun down(keyData: KeyData): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.DOWN,
data = keyData,
count = 1
)
}
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.DOWN_UP].
*
* @param keyData The key data of the input key event event to create.
* @param count How often this event occurred. Must be grater or equal to 1, defaults to 1.
*
* @return The created input key event.
*/
fun downUp(keyData: KeyData, count: Int = 1): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.DOWN_UP,
data = keyData,
count = count
)
}
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.UP].
*
* @param keyData The key data of the input key event event to create.
*
* @return The created input key event.
*/
fun up(keyData: KeyData): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.UP,
data = keyData,
count = 1
)
}
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.REPEAT].
*
* @param keyData The key data of the input key event event to create.
* @param count How often this event occurred. Must be grater or equal to 1, defaults to 1.
*
* @return The created input key event.
*/
fun repeat(keyData: KeyData, count: Int = 1): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.REPEAT,
data = keyData,
count = count
)
}
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.CANCEL].
*
* @param keyData The key data of the input key event event to create.
*
* @return The created input key event.
*/
fun cancel(keyData: KeyData): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.CANCEL,
data = keyData,
count = 1
)
}
}
/**
* Checks if the [other] input key event is a consecutive event while respecting [maxEventTimeDiff].
*
* @param other The other input key event to compare with this one.
* @param maxEventTimeDiff The maximum event time diff between this event and [other], in milliseconds.
*
* @return True if this event is a consecutive event of [other], false otherwise.
*/
fun isConsecutiveEventOf(other: InputKeyEvent?, maxEventTimeDiff: Long): Boolean {
return other != null && data.code == other.data.code && eventTime - other.eventTime <= maxEventTimeDiff
}
/**
* Returns a string representation of this input key event.
*/
override fun toString(): String {
return "FlorisKeyEvent { eventTime=${eventTime}ms, action=$action, data=$data, count=$count }"
}
/**
* The action of an input key event.
*/
enum class Action {
DOWN,
DOWN_UP,
UP,
REPEAT,
CANCEL,
}
}
/**
* Interface which represents an input key event sender.
*/
interface InputKeyEventSender {
/**
* Sends given input key event [ev] to the underlying input channel, awaiting to be processed.
*
* @param ev The input key event to send.
*/
fun send(ev: InputKeyEvent)
}
/**
* Interface which represents an input key event receiver.
*/
interface InputKeyEventReceiver {
/**
* Event method which gets called when a key went down.
*
* @param ev The associated input key event.
*/
fun onInputKeyDown(ev: InputKeyEvent)
/**
* Event method which gets called when a key went up.
*
* @param ev The associated input key event.
*/
fun onInputKeyUp(ev: InputKeyEvent)
/**
* Event method which gets called when a key is called repeatedly while being pressed down.
*
* @param ev The associated input key event.
*/
fun onInputKeyRepeat(ev: InputKeyEvent)
/**
* Event method which gets called when a key press is cancelled.
*
* @param ev The associated input key event.
*/
fun onInputKeyCancel(ev: InputKeyEvent)
}

View File

@@ -20,12 +20,12 @@ import android.content.Context
import android.content.res.Configuration
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.util.Log
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.ViewFlipper
import dev.patrickgold.florisboard.BuildConfig
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
@@ -50,17 +50,9 @@ class InputView : LinearLayout {
var mainViewFlipper: ViewFlipper? = null
private set
var oneHandedCtrlPanelStart: LinearLayout? = null
var oneHandedCtrlPanelStart: ViewGroup? = null
private set
var oneHandedCtrlPanelEnd: LinearLayout? = null
private set
var oneHandedCtrlMoveStart: ImageButton? = null
private set
var oneHandedCtrlMoveEnd: ImageButton? = null
private set
var oneHandedCtrlCloseStart: ImageButton? = null
private set
var oneHandedCtrlCloseEnd: ImageButton? = null
var oneHandedCtrlPanelEnd: ViewGroup? = null
private set
constructor(context: Context) : this(context, null)
@@ -79,10 +71,6 @@ class InputView : LinearLayout {
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)
oneHandedCtrlMoveStart = findViewById(R.id.one_handed_ctrl_move_start)
oneHandedCtrlMoveEnd = findViewById(R.id.one_handed_ctrl_move_end)
oneHandedCtrlCloseStart = findViewById(R.id.one_handed_ctrl_close_start)
oneHandedCtrlCloseEnd = findViewById(R.id.one_handed_ctrl_close_end)
florisboard.registerInputView(this)
}
@@ -90,8 +78,8 @@ class InputView : LinearLayout {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightFactor = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> 1.0f
else -> if (prefs.keyboard.oneHandedMode != "off") {
0.9f
else -> if (prefs.keyboard.oneHandedMode != OneHandedMode.OFF) {
prefs.keyboard.oneHandedModeScaleFactor / 100.0f
} else {
1.0f
}
@@ -135,7 +123,11 @@ class InputView : LinearLayout {
// 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(
florisboard.prefs.keyboard.bottomOffset.toFloat(),
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
florisboard.prefs.keyboard.bottomOffsetLandscape.toFloat()
} else {
florisboard.prefs.keyboard.bottomOffsetPortrait.toFloat()
},
context
)
@@ -169,5 +161,4 @@ class InputView : LinearLayout {
resources.getDimension(R.dimen.inputView_baseHeight)
)
}
}

View File

@@ -18,12 +18,7 @@ package dev.patrickgold.florisboard.ime.core
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.ViewFlipper
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
/**
* Root view of the keyboard.

View File

@@ -21,14 +21,16 @@ import android.content.SharedPreferences
import android.provider.Settings
import androidx.preference.PreferenceManager
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
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.KeyHintMode
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
import dev.patrickgold.florisboard.ime.theme.ThemeMode
import dev.patrickgold.florisboard.util.TimeUtil
import dev.patrickgold.florisboard.util.VersionName
import kotlin.collections.HashMap
/**
* Helper class for an organized access to the shared preferences.
@@ -215,6 +217,7 @@ class PrefHelper(
const val SWIPE_DOWN = "gestures__swipe_down"
const val SWIPE_LEFT = "gestures__swipe_left"
const val SWIPE_RIGHT = "gestures__swipe_right"
const val SPACE_BAR_LONG_PRESS = "gestures__space_bar_long_press"
const val SPACE_BAR_SWIPE_LEFT = "gestures__space_bar_swipe_left"
const val SPACE_BAR_SWIPE_RIGHT = "gestures__space_bar_swipe_right"
const val SPACE_BAR_SWIPE_UP = "gestures__space_bar_swipe_up"
@@ -235,6 +238,9 @@ class PrefHelper(
var swipeRight: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_RIGHT, "no_action"))
set(v) = prefHelper.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)
var spaceBarSwipeUp: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_SWIPE_UP, "no_action"))
set(v) = prefHelper.setPref(SPACE_BAR_SWIPE_UP, v)
@@ -303,25 +309,35 @@ class PrefHelper(
*/
class Keyboard(private val prefHelper: PrefHelper) {
companion object {
const val BOTTOM_OFFSET = "keyboard__bottom_offset"
const val BOTTOM_OFFSET_PORTRAIT = "keyboard__bottom_offset_portrait"
const val BOTTOM_OFFSET_LANDSCAPE = "keyboard__bottom_offset_landscape"
const val FONT_SIZE_MULTIPLIER_PORTRAIT = "keyboard__font_size_multiplier_portrait"
const val FONT_SIZE_MULTIPLIER_LANDSCAPE = "keyboard__font_size_multiplier_landscape"
const val HEIGHT_FACTOR = "keyboard__height_factor"
const val HEIGHT_FACTOR_CUSTOM = "keyboard__height_factor_custom"
const val HINTED_NUMBER_ROW_MODE = "keyboard__hinted_number_row_mode"
const val HINTED_SYMBOLS_MODE = "keyboard__hinted_symbols_mode"
const val KEY_SPACING_HORIZONTAL = "keyboard__key_spacing_horizontal"
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 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"
const val POPUP_ENABLED = "keyboard__popup_enabled"
const val SOUND_ENABLED = "keyboard__sound_enabled"
const val SOUND_VOLUME = "keyboard__sound_volume"
const val UTILITY_KEY_ACTION = "keyboard__utility_key_action"
const val UTILITY_KEY_ENABLED = "keyboard__utility_key_enabled"
const val VIBRATION_ENABLED = "keyboard__vibration_enabled"
const val VIBRATION_STRENGTH = "keyboard__vibration_strength"
}
var bottomOffset: Int = 0
get() = prefHelper.getPref(BOTTOM_OFFSET, 0)
var bottomOffsetPortrait: Int = 0
get() = prefHelper.getPref(BOTTOM_OFFSET_PORTRAIT, 0)
private set
var bottomOffsetLandscape: Int = 0
get() = prefHelper.getPref(BOTTOM_OFFSET_LANDSCAPE, 0)
private set
var fontSizeMultiplierPortrait: Int
get() = prefHelper.getPref(FONT_SIZE_MULTIPLIER_PORTRAIT, 100)
@@ -341,6 +357,15 @@ class PrefHelper(
var hintedSymbolsMode: KeyHintMode
get() = KeyHintMode.fromString(prefHelper.getPref(HINTED_SYMBOLS_MODE, KeyHintMode.ENABLED_ACCENT_PRIORITY.toString()))
set(v) = prefHelper.setPref(HINTED_SYMBOLS_MODE, v)
var keySpacingHorizontal: Float = 2f
get() = prefHelper.getPref(KEY_SPACING_HORIZONTAL, 4) / 2f
private set
var keySpacingVertical: Float = 5f
get() = prefHelper.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)
var longPressDelay: Int = 0
get() = prefHelper.getPref(LONG_PRESS_DELAY, 300)
private set
@@ -348,8 +373,11 @@ class PrefHelper(
get() = prefHelper.getPref(NUMBER_ROW, false)
set(v) = prefHelper.setPref(NUMBER_ROW, v)
var oneHandedMode: String
get() = prefHelper.getPref(ONE_HANDED_MODE, "off")
get() = prefHelper.getPref(ONE_HANDED_MODE, OneHandedMode.OFF)
set(value) = prefHelper.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)
var popupEnabled: Boolean = false
get() = prefHelper.getPref(POPUP_ENABLED, true)
private set
@@ -360,6 +388,12 @@ class PrefHelper(
var soundVolume: Int = 0
get() = prefHelper.getPref(SOUND_VOLUME, -1)
private set
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)
var utilityKeyEnabled: Boolean
get() = prefHelper.getPref(UTILITY_KEY_ENABLED, true)
set(v) = prefHelper.setPref(UTILITY_KEY_ENABLED, v)
var vibrationEnabled: Boolean = false
get() = prefHelper.getPref(VIBRATION_ENABLED, true)
private set
@@ -404,11 +438,15 @@ class PrefHelper(
*/
class Suggestion(private val prefHelper: PrefHelper) {
companion object {
const val BLOCK_POSSIBLY_OFFENSIVE = "suggestion__block_possibly_offensive"
const val ENABLED = "suggestion__enabled"
const val SUGGEST_CLIPBOARD_CONTENT = "suggestion__suggest_clipboard_content"
const val USE_PREV_WORDS = "suggestion__use_prev_words"
}
var blockPossiblyOffensive: Boolean
get() = prefHelper.getPref(BLOCK_POSSIBLY_OFFENSIVE, true)
set(v) = prefHelper.setPref(BLOCK_POSSIBLY_OFFENSIVE, v)
var enabled: Boolean
get() = prefHelper.getPref(ENABLED, true)
set(v) = prefHelper.setPref(ENABLED, v)

View File

@@ -20,7 +20,10 @@ import android.content.Context
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dev.patrickgold.florisboard.util.LocaleUtils
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import java.util.*
/**

View File

@@ -0,0 +1,59 @@
/*
* 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 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
/**
* Standardized dictionary interface for interacting with dictionaries.
*/
interface Dictionary<T : Any, F : Comparable<F>> : Asset {
val languageModel: LanguageModel<T, F>
/**
* 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>?,
maxSuggestionCount: Int,
allowPossiblyOffensive: Boolean
): List<WeightedToken<T, F>>
fun getDate(): Long
fun getVersion(): Int
}
interface MutableDictionary<T : Any, F : Comparable<F>> : Dictionary<T, F> {
override val languageModel: MutableLanguageModel<T, F>
fun trainTokenPredictions(
precedingTokens: List<Token<T>>,
lastToken: Token<T>
)
fun setDate(date: Int)
fun setVersion(version: Int)
}

View File

@@ -0,0 +1,68 @@
/*
* 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.Context
import dev.patrickgold.florisboard.ime.extension.AssetRef
import timber.log.Timber
/**
* TODO: document
*/
class DictionaryManager private constructor(private val applicationContext: Context) {
private val dictionaryCache: MutableMap<String, Dictionary<String, Int>> = mutableMapOf()
companion object {
private var defaultInstance: DictionaryManager? = null
fun init(applicationContext: Context): DictionaryManager {
val instance = DictionaryManager(applicationContext)
defaultInstance = instance
return instance
}
fun default(): DictionaryManager {
val instance = defaultInstance
if (instance != null) {
return instance
} else {
throw UninitializedPropertyAccessException(
"${DictionaryManager::class.simpleName} has not been initialized previously. Make sure to call init(applicationContext) before using default()."
)
}
}
}
fun loadDictionary(ref: AssetRef): Result<Dictionary<String, Int>> {
dictionaryCache[ref.toString()]?.let {
return Result.success(it)
}
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)
}
} 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..."))
}
}

View File

@@ -0,0 +1,430 @@
/*
* 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.Context
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
/**
* Class Flictionary which takes care of loading the binary asset as well as providing words for
* queries.
*
* 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> {
companion object {
private const val VERSION_0 = 0x0
private const val MASK_BEGIN_PTREE_NODE = 0x80
private const val CMDB_BEGIN_PTREE_NODE = 0x00
private const val ATTR_PTREE_NODE_ORDER = 0x70
private const val ATTR_PTREE_NODE_TYPE = 0x0C
private const val ATTR_PTREE_NODE_TYPE_CHAR = 0
private const val ATTR_PTREE_NODE_TYPE_WORD_FILLER = 1
private const val ATTR_PTREE_NODE_TYPE_WORD = 2
private const val ATTR_PTREE_NODE_TYPE_SHORTCUT = 3
private const val ATTR_PTREE_NODE_SIZE = 0x03
private const val MASK_END = 0xC0
private const val CMDB_END = 0x80
private const val ATTR_END_COUNT = 0x3F
private const val MASK_BEGIN_HEADER = 0xE0
private const val CMDB_BEGIN_HEADER = 0xC0
private const val ATTR_HEADER_VERSION = 0x1F
private const val MASK_DEFINE_SHORTCUT = 0xF0
private const val CMDB_DEFINE_SHORTCUT = 0xE0
/**
* Loads a Flictionary binary asset from given [assetRef] and returns a result containing
* either the parsed dictionary or an exception giving information about the error which
* occurred.
*/
fun load(context: Context, assetRef: AssetRef): Result<Flictionary> {
val buffer = ByteArray(5000) { 0 }
val inputStream: InputStream
if (assetRef.source == AssetSource.Assets) {
inputStream = context.assets.open(assetRef.path)
} else {
return Result.failure(Exception("Only AssetSource.Assets is currently supported!"))
}
var headerStr: String? = null
var date: Long = 0
var version = 0
val ngramTree = NgramTree()
var pos = 0
val ngramOrderStack = mutableListOf<Int>()
val ngramTreeStack = mutableListOf<NgramNode>()
while (true) {
if (inputStream.readNext(buffer, 0, 1) <= 0) {
break
}
val cmd = buffer[0].toInt() and 0xFF
when {
(cmd and MASK_BEGIN_PTREE_NODE) == CMDB_BEGIN_PTREE_NODE -> {
if (pos == 0) {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_CMD_BEGIN_PTREE_NODE,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
val order = ((cmd and ATTR_PTREE_NODE_ORDER) shr 4) + 1
val type = ((cmd and ATTR_PTREE_NODE_TYPE) shr 2)
val size = (cmd and ATTR_PTREE_NODE_SIZE) + 1
val freq: Int
val freqSize: Int
when (type) {
ATTR_PTREE_NODE_TYPE_CHAR -> {
freq = NgramNode.FREQ_CHARACTER
freqSize = 0
}
ATTR_PTREE_NODE_TYPE_WORD_FILLER -> {
freq = NgramNode.FREQ_WORD_FILLER
freqSize = 0
}
ATTR_PTREE_NODE_TYPE_WORD -> {
if (inputStream.readNext(buffer, 1, 1) > 0) {
freq = buffer[1].toInt() and 0xFF
} else {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
freqSize = 1
}
else -> return Result.failure(Exception("TODO: shortcut not supported"))
}
if (inputStream.readNext(buffer, freqSize + 1, size) > 0) {
val char = String(buffer, freqSize + 1, size, Charsets.UTF_8)[0]
val node = NgramNode(order, char, freq)
val lastOrder = ngramOrderStack.lastOrNull()
if (lastOrder == null) {
ngramTree.higherOrderChildren.add(node)
} else {
if (lastOrder == order) {
ngramTreeStack.last().sameOrderChildren.add(node)
} else {
ngramTreeStack.last().higherOrderChildren.add(node)
}
}
ngramOrderStack.add(order)
ngramTreeStack.add(node)
pos += (freqSize + 1 + size)
} else {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
}
(cmd and MASK_BEGIN_HEADER) == CMDB_BEGIN_HEADER -> {
if (pos != 0) {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_CMD_BEGIN_HEADER,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
version = cmd and ATTR_HEADER_VERSION
if (version != VERSION_0) {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNSUPPORTED_FLICTIONARY_VERSION,
address = pos,
cmdByte = cmd.toByte(),
absoluteDepth = ngramTreeStack.size
)
)
}
if (inputStream.readNext(buffer, 1, 9) > 0) {
val size = (buffer[1].toInt() and 0xFF)
date =
((buffer[2].toInt() and 0xFF).toLong() shl 56) +
((buffer[3].toInt() and 0xFF).toLong() shl 48) +
((buffer[4].toInt() and 0xFF).toLong() shl 40) +
((buffer[5].toInt() and 0xFF).toLong() shl 32) +
((buffer[6].toInt() and 0xFF).toLong() shl 24) +
((buffer[7].toInt() and 0xFF).toLong() shl 16) +
((buffer[8].toInt() and 0xFF).toLong() shl 8) +
((buffer[9].toInt() and 0xFF).toLong() shl 0)
if (inputStream.readNext(buffer, 10, size) > 0) {
headerStr = String(buffer, 10, size, Charsets.UTF_8)
ngramOrderStack.add(-1)
ngramTreeStack.add(NgramTree())
pos += (10 + size)
} else {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
} else {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
}
(cmd and MASK_END) == CMDB_END -> {
if (pos == 0) {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_CMD_END,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
val n = (cmd and ATTR_END_COUNT)
if (n > 0) {
if (n <= ngramTreeStack.size) {
for (c in 0 until n) {
ngramOrderStack.removeLast()
ngramTreeStack.removeLast()
}
} else {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size - n
)
)
}
} else {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_CMD_END_ZERO_VALUE,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
pos += 1
}
else -> {
inputStream.close()
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.INVALID_CMD_BYTE_PROVIDED,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
}
}
inputStream.close()
if (ngramTreeStack.size != 0) {
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF,
address = pos, cmdByte = 0x00.toByte(), absoluteDepth = ngramTreeStack.size
)
)
}
return Result.success(
Flictionary(
name = "flict",
label = "flict",
authors = listOf(),
headerStr = headerStr ?: "",
date = date,
version = version,
languageModel = FlorisLanguageModel(ngramTree)
)
)
}
}
override fun getDate(): Long = date
override fun getVersion(): Int = version
// TODO: preceding tokens are currently ignored
override fun getTokenPredictions(
precedingTokens: List<Token<String>>,
currentToken: Token<String>?,
maxSuggestionCount: Int,
allowPossiblyOffensive: Boolean
): List<WeightedToken<String, Int>> {
currentToken ?: return listOf()
return if (currentToken.data.isNotEmpty()) {
val retList = languageModel.matchAllNgrams(
ngram = Ngram(
_tokens = listOf(Token(currentToken.data.toLowerCase(Locale.ENGLISH))),
_freq = -1
),
maxEditDistance = 2,
maxTokenCount = maxSuggestionCount,
allowPossiblyOffensive = allowPossiblyOffensive
)
retList
} else {
listOf()
}
}
/**
* A parse exception to be used by [Flictionary] to indicate where the parsing of a binary file
* failed, while also providing some additional information.
*/
class ParseException(
private val errorType: ErrorType,
private val address: Int,
private val cmdByte: Byte,
private val absoluteDepth: Int
) : Exception() {
enum class ErrorType {
UNSUPPORTED_FLICTIONARY_VERSION,
UNEXPECTED_CMD_BEGIN_HEADER,
UNEXPECTED_CMD_BEGIN_PTREE_NODE,
UNEXPECTED_CMD_DEFINE_SHORTCUT,
UNEXPECTED_CMD_END,
UNEXPECTED_CMD_END_ZERO_VALUE,
UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO,
UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF,
UNEXPECTED_EOF,
INVALID_CMD_BYTE_PROVIDED,
}
override val message: String
get() = toString()
override fun toString(): String {
return StringBuilder().run {
append(
when (errorType) {
ErrorType.UNSUPPORTED_FLICTIONARY_VERSION -> {
"Unexpected Flictionary version: ${(cmdByte.toInt() and 0xFF) and ATTR_HEADER_VERSION}"
}
ErrorType.UNEXPECTED_CMD_BEGIN_HEADER -> {
"Unexpected command: BEGIN_HEADER"
}
ErrorType.UNEXPECTED_CMD_BEGIN_PTREE_NODE -> {
"Unexpected command: BEGIN_PTREE_NODE"
}
ErrorType.UNEXPECTED_CMD_DEFINE_SHORTCUT -> {
"Unexpected command: DEFINE_SHORTCUT"
}
ErrorType.UNEXPECTED_CMD_END -> {
"Unexpected command: END"
}
ErrorType.UNEXPECTED_CMD_END_ZERO_VALUE -> {
"Unexpected zero value provided for command END"
}
ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO -> {
"Unexpected decrease in absolute depth: cannot go below zero"
}
ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF -> {
"Unexpected non-zero value in absolute depth at end of file"
}
ErrorType.UNEXPECTED_EOF -> {
"Unexpected end of file while try to do look-ahead"
}
ErrorType.INVALID_CMD_BYTE_PROVIDED -> {
"Invalid command byte provided"
}
}
)
append(
String.format(
"\n at address 0x%08X where cmd_byte=0x%02X and section_depth=%d",
address,
cmdByte,
absoluteDepth
)
)
toString()
}
}
}
}
/**
* Reads the next [len] bytes from the input stream into the given byte array [b]. This method guarantees to either
* read the full length requested or if an EOF file is encountered, -1 is returned. The first byte written is at
* `b[off]`, the second byte at `b[off+1]` and so on.
*
* @param b The byte array to read the next [len] bytes into.
* @param off The offset of the first byte written in the byte array [b]. Must be non-negative.
* @param len The number of bytes to read. Must be non-negative.
*
* @return The number of bytes read, always matching [len] or -1 if EOF was encountered.
*
* @throws IndexOutOfBoundsException if either [off] or [len] is negative or the byte array has insufficient space to
* write the request [len] bytes into it.
*/
@Throws(IndexOutOfBoundsException::class)
fun InputStream.readNext(b: ByteArray, off: Int, len: Int): Int {
if (off < 0 || len < 0 || len > b.size - off) {
throw IndexOutOfBoundsException()
} else if (len == 0) {
return 0
}
var lenRead = 0
while (lenRead < len) {
val c = read()
if (c == -1) {
return -1
} else {
b[off + lenRead++] = c.toByte()
}
}
return lenRead
}

View File

@@ -17,8 +17,6 @@
package dev.patrickgold.florisboard.ime.extension
import android.content.Context
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Result
/**
* Interface for an Asset to use within FlorisBoard. An asset is everything from a dictionary to a
@@ -61,6 +59,6 @@ interface Asset {
/**
* Loads an Asset of type [T] from the specified path.
*/
fun fromFile(context: Context, path: String): Result<T, Throwable> = Err(NotImplementedError())
fun fromFile(context: Context, path: String): Result<T> = Result.failure(NotImplementedError())
}
}

View File

@@ -17,17 +17,15 @@
package dev.patrickgold.florisboard.ime.extension
import android.content.Context
import com.github.michaelbull.result.*
import android.net.Uri
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dev.patrickgold.florisboard.ime.popup.PopupExtension
import dev.patrickgold.florisboard.ime.text.key.KeyTypeAdapter
import dev.patrickgold.florisboard.ime.text.key.KeyVariationAdapter
import dev.patrickgold.florisboard.ime.text.layout.LayoutTypeAdapter
import dev.patrickgold.florisboard.ime.theme.Theme
import timber.log.Timber
import java.io.File
import kotlin.reflect.KClass
class AssetManager private constructor(private val applicationContext: Context) {
private val moshi: Moshi = Moshi.Builder()
@@ -62,22 +60,22 @@ class AssetManager private constructor(private val applicationContext: Context)
}
}
fun deleteAsset(ref: AssetRef): Result<Nothing?, Throwable> {
fun deleteAsset(ref: AssetRef): Result<Unit> {
return when (ref.source) {
AssetSource.Internal -> {
val file = File(applicationContext.filesDir.absolutePath + "/" + ref.path)
if (file.isFile) {
val success = file.delete()
if (success) {
Ok(null)
Result.success(Unit)
} else {
Err(Exception("Could not delete file."))
Result.failure(Exception("Could not delete file."))
}
} else {
Err(Exception("Provided reference is not a file."))
Result.failure(Exception("Provided reference is not a file."))
}
}
else -> Err(Exception("Can not delete an asset in source '${ref.source}'"))
else -> Result.failure(Exception("Can not delete an asset in source '${ref.source}'"))
}
}
@@ -100,7 +98,7 @@ class AssetManager private constructor(private val applicationContext: Context)
}
}
fun <T: Asset> listAssets(ref: AssetRef, assetClass: Class<T>): Result<Map<AssetRef, T>, Throwable> {
fun <T : Asset> listAssets(ref: AssetRef, assetClass: KClass<T>): Result<Map<AssetRef, T>> {
val retMap = mutableMapOf<AssetRef, T>()
return when (ref.source) {
AssetSource.Assets -> {
@@ -117,9 +115,9 @@ class AssetManager private constructor(private val applicationContext: Context)
}
}
}
Ok(retMap.toMap())
Result.success(retMap.toMap())
} catch (e: Exception) {
Err(e)
Result.failure(e)
}
}
AssetSource.Internal -> {
@@ -139,19 +137,19 @@ class AssetManager private constructor(private val applicationContext: Context)
}
}
}
Ok(retMap.toMap())
Result.success(retMap.toMap())
}
else -> Ok(retMap.toMap())
else -> Result.success(retMap.toMap())
}
}
fun <T: Asset> loadAsset(ref: AssetRef, assetClass: Class<T>): Result<T, Throwable> {
fun <T : Asset> loadAsset(ref: AssetRef, assetClass: KClass<T>): Result<T> {
val rawJsonData = when (ref.source) {
is AssetSource.Assets -> {
try {
applicationContext.assets.open(ref.path).bufferedReader().use { it.readText() }
} catch (e: Exception) {
return Err(e)
return Result.failure(e)
}
}
is AssetSource.Internal -> {
@@ -166,28 +164,68 @@ class AssetManager private constructor(private val applicationContext: Context)
else -> "{}"
}
return try {
val adapter = moshi.adapter(assetClass)
val adapter = moshi.adapter(assetClass.java)
val asset = adapter.fromJson(rawJsonData)
if (asset != null) {
Ok(asset)
Result.success(asset)
} else {
Err(NullPointerException("Asset failed to load!"))
Result.failure(NullPointerException("Asset failed to load!"))
}
} catch (e: Exception) {
Err(e)
Result.failure(e)
}
}
fun <T: Asset> writeAsset(ref: AssetRef, assetClass: Class<T>, asset: T): Result<Boolean, Throwable> {
fun <T : Asset> loadAsset(uri: Uri, assetClass: KClass<T>): Result<T> {
val rawJsonData = ExternalContentUtils.readTextFromUri(applicationContext, uri).onFailure {
return Result.failure(it)
}
return try {
val adapter = moshi.adapter(assetClass.java)
val asset = adapter.fromJson(rawJsonData.getOrNull()!!)
if (asset != null) {
Result.success(asset)
} else {
Result.failure(NullPointerException("Asset failed to load!"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
fun loadAssetRaw(ref: AssetRef): Result<String> {
return when (ref.source) {
is AssetSource.Assets -> {
try {
Result.success(applicationContext.assets.open(ref.path).bufferedReader().use { it.readText() })
} catch (e: Exception) {
Result.failure(e)
}
}
is AssetSource.Internal -> {
val file = File(applicationContext.filesDir.absolutePath + "/" + ref.path)
val contents = readFile(file)
if (contents.isBlank()) {
Result.failure(Exception("File is blank!"))
} else {
Result.success(contents)
}
}
else -> Result.failure(Exception("Unsupported asset ref!"))
}
}
fun <T : Asset> writeAsset(ref: AssetRef, assetClass: KClass<T>, asset: T): Result<Unit> {
return when (ref.source) {
AssetSource.Internal -> {
val adapter = moshi.adapter(assetClass)
val adapter = moshi.adapter(assetClass.java)
val rawJson = adapter.toJson(asset)
val file = File(applicationContext.filesDir.absolutePath + "/" + ref.path)
writeToFile(file, rawJson)
Ok(true)
Result.success(Unit)
}
else -> Err(Exception("Can not write an asset in source '${ref.source}'"))
else -> Result.failure(Exception("Can not write an asset in source '${ref.source}'"))
}
}

View File

@@ -16,11 +16,6 @@
package dev.patrickgold.florisboard.ime.extension
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.getOrElse
/**
* Data class which is a reference to an asset file. It indicates in which storage medium the asset
* is as well as the relative path to it.
@@ -36,15 +31,15 @@ data class AssetRef(
companion object {
private const val DELIMITER: String = ":"
fun fromString(str: String): Result<AssetRef, String> {
fun fromString(str: String): Result<AssetRef> {
val items = str.split(DELIMITER)
if (items.size != 2) {
return Err("Unexpected length of given asset ref. Make sure that the asset ref string contains exactly 2 items separated by '$DELIMITER'!")
return Result.failure(Exception("Unexpected length of given asset ref. Make sure that the asset ref string contains exactly 2 items separated by '$DELIMITER'!"))
}
val retSource = AssetSource.fromString(items[0]).getOrElse {
return Err(it)
return Result.failure(Exception(it))
}
return Ok(AssetRef(retSource, items[1]))
return Result.success(AssetRef(retSource, items[1]))
}
}

View File

@@ -16,9 +16,6 @@
package dev.patrickgold.florisboard.ime.extension
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import java.util.*
/**
@@ -52,16 +49,16 @@ sealed class AssetSource {
companion object {
private val externalRegex: Regex = """^external\\(([a-z]+\\.)*[a-z]+\\)\$""".toRegex()
fun fromString(str: String): Result<AssetSource, String> {
fun fromString(str: String): Result<AssetSource> {
return when (val string = str.toLowerCase(Locale.ENGLISH)) {
"assets" -> Ok(Assets)
"internal" -> Ok(Internal)
"assets" -> Result.success(Assets)
"internal" -> Result.success(Internal)
else -> {
if (string.matches(externalRegex)) {
val packageName = string.substring(9, string.length - 1)
Ok(External(packageName))
Result.success(External(packageName))
} else {
Err("'$str' is not a valid AssetSource.")
Result.failure(Exception("'$str' is not a valid AssetSource."))
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.extension
import android.content.Context
import android.net.Uri
class ExternalContentUtils private constructor() {
companion object {
fun readTextFromUri(context: Context, uri: Uri): 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 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.flush(); it.write(text) }
return Result.success(Unit)
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2020 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.landscapeinput
import java.util.*
enum class LandscapeInputUiMode {
DYNAMICALLY_SHOW,
NEVER_SHOW,
ALWAYS_SHOW;
companion object {
fun fromString(string: String): LandscapeInputUiMode {
return valueOf(string.toUpperCase(Locale.ENGLISH))
}
}
}

View File

@@ -17,7 +17,6 @@
package dev.patrickgold.florisboard.ime.media
import android.annotation.SuppressLint
import android.os.Handler
import android.view.MotionEvent
import android.view.View
import android.widget.*
@@ -25,16 +24,13 @@ import com.google.android.material.tabs.TabLayout
import dev.patrickgold.florisboard.R
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.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyType
import dev.patrickgold.florisboard.util.cancelAll
import dev.patrickgold.florisboard.util.postAtScheduledRate
import kotlinx.coroutines.*
import timber.log.Timber
import java.util.*
@@ -58,11 +54,10 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
private var activeTab: Tab? = null
private var mediaViewFlipper: ViewFlipper? = null
private var repeatedKeyPressHandler: Handler? = null
private var tabLayout: TabLayout? = null
private val tabViews = EnumMap<Tab, LinearLayout>(Tab::class.java)
var mediaViewGroup: LinearLayout? = null
private var mediaViewGroup: LinearLayout? = null
companion object {
private var instance: MediaInputManager? = null
@@ -80,11 +75,6 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
florisboard.addEventListener(this)
}
override fun onCreateInputView() {
super.onCreateInputView()
repeatedKeyPressHandler = Handler(florisboard.context.mainLooper)
}
/**
* Called when a new input view has been registered. Used to initialize all media-relevant
* views and layouts.
@@ -145,30 +135,21 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
private fun onBottomButtonEvent(view: View, event: MotionEvent?): Boolean {
event ?: return false
val data = when (view.id) {
R.id.media_input_switch_to_text_input_button -> {
KeyData(code = KeyCode.SWITCH_TO_TEXT_CONTEXT)
}
R.id.media_input_backspace_button -> {
KeyData(code = KeyCode.DELETE, type = KeyType.ENTER_EDITING)
}
else -> null
R.id.media_input_switch_to_text_input_button -> KeyData.SWITCH_TO_TEXT_CONTEXT
R.id.media_input_backspace_button -> KeyData.DELETE
else -> return false
}
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
florisboard.keyPressVibrate()
florisboard.keyPressSound(data)
if (data?.code == KeyCode.DELETE && data.type == KeyType.ENTER_EDITING) {
val delayMillis = florisboard.prefs.keyboard.longPressDelay.toLong()
repeatedKeyPressHandler?.postAtScheduledRate(delayMillis, 25) {
florisboard.textInputManager.sendKeyPress(data)
}
}
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.down(data))
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
repeatedKeyPressHandler?.cancelAll()
if (event.actionMasked != MotionEvent.ACTION_CANCEL && data != null) {
florisboard.textInputManager.sendKeyPress(data)
}
MotionEvent.ACTION_UP -> {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.up(data))
}
MotionEvent.ACTION_CANCEL -> {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
}
}
// MUST return false here so the background selector for showing a transparent bg works

View File

@@ -43,12 +43,11 @@ class MediaInputView : LinearLayout, FlorisBoard.EventListener,
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) {
florisboard?.addEventListener(this)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
florisboard?.addEventListener(this)
themeManager.registerOnThemeUpdatedListener(this)
tabLayout = findViewById(R.id.media_input_tabs)
switchToTextInputButton = findViewById(R.id.media_input_switch_to_text_input_button)
@@ -57,8 +56,9 @@ class MediaInputView : LinearLayout, FlorisBoard.EventListener,
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
themeManager.unregisterOnThemeUpdatedListener(this)
florisboard?.removeEventListener(this)
super.onDetachedFromWindow()
}
override fun onThemeUpdated(theme: Theme) {

View File

@@ -16,7 +16,7 @@
package dev.patrickgold.florisboard.ime.media.emoji
import android.annotation.SuppressLint
import java.util.*
/**
* Enum for emoji category.
@@ -38,9 +38,8 @@ enum class EmojiCategory {
}
companion object {
@SuppressLint("DefaultLocale")
fun fromString(string: String): EmojiCategory {
return valueOf(string.replace(" & ", "_").toUpperCase())
return valueOf(string.replace(" & ", "_").toUpperCase(Locale.ENGLISH))
}
}
}

View File

@@ -33,6 +33,8 @@ import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
/**
* View class for managing the rendering and the events of a single emoji keyboard key.
@@ -46,7 +48,7 @@ import dev.patrickgold.florisboard.ime.theme.ThemeManager
class EmojiKeyView(
private val emojiKeyboardView: EmojiKeyboardView,
val data: EmojiKeyData
) : androidx.appcompat.widget.AppCompatTextView(emojiKeyboardView.context),
) : 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)
@@ -64,13 +66,16 @@ class EmojiKeyView(
triangleDrawable = ContextCompat.getDrawable(context, R.drawable.triangle_bottom_right)
text = data.getCodePointsAsString()
florisboard?.addEventListener(this)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
onApplyThemeAttributes()
florisboard?.addEventListener(this)
}
override fun onDetachedFromWindow() {
florisboard?.removeEventListener(this)
super.onDetachedFromWindow()
}
/**

View File

@@ -101,11 +101,11 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener,
override fun onTabUnselected(tab: TabLayout.Tab?) {}
})
addView(tabLayout)
florisboard?.addEventListener(this)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
florisboard?.addEventListener(this)
themeManager.registerOnThemeUpdatedListener(this)
mainScope.launch {
layouts.await()
@@ -116,8 +116,9 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener,
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
themeManager.unregisterOnThemeUpdatedListener(this)
florisboard?.removeEventListener(this)
super.onDetachedFromWindow()
}
/**

View File

@@ -19,13 +19,11 @@ package dev.patrickgold.florisboard.ime.media.emoji
import android.content.Context
import android.graphics.Paint
import android.graphics.Typeface
import android.util.Log
import androidx.core.graphics.PaintCompat
import timber.log.Timber
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.lang.Exception
import java.util.*
private const val GROUP_IDENTIFIER = "# group: "
@@ -71,6 +69,8 @@ private fun listStringToListInt(list: List<String>): List<Int> {
return ret.toList()
}
private var cachedEmojiLayoutMap: EmojiLayoutDataMap? = null
/**
* Reads the emoji list at the given [path] and returns an parsed [EmojiLayoutDataMap]. If the
* given file path does not exist, an empty [EmojiLayoutDataMap] is returned.
@@ -84,6 +84,7 @@ private fun listStringToListInt(list: List<String>): List<Int> {
fun parseRawEmojiSpecsFile(
context: Context, path: String
): EmojiLayoutDataMap {
cachedEmojiLayoutMap?.let { return it }
val layouts = EmojiLayoutDataMap(EmojiCategory::class.java)
for (category in EmojiCategory.values()) {
layouts[category] = mutableListOf()
@@ -182,5 +183,6 @@ fun parseRawEmojiSpecsFile(
}
}
}
cachedEmojiLayoutMap = layouts
return layouts
}

View File

@@ -0,0 +1,294 @@
/*
* 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

@@ -0,0 +1,74 @@
/*
* 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

@@ -0,0 +1,129 @@
/*
* 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

@@ -0,0 +1,90 @@
/*
* 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,24 @@
/*
* 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 TextProcessor {
data class Word(
val word: String,
val isPossiblyOffensive: Boolean = false
)
}

View File

@@ -0,0 +1,26 @@
/*
* 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.onehanded
/**
* Static object which contains all possible one-handed mode strings.
*/
object OneHandedMode {
const val OFF: String = "off"
const val START: String = "start"
const val END: String = "end"
}

View File

@@ -0,0 +1,102 @@
/*
* 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.onehanded
import android.content.Context
import android.content.res.ColorStateList
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.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 var closeBtn: ImageButton? = null
private var moveBtn: ImageButton? = null
private val panelSide: String
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) {
context.obtainStyledAttributes(attrs, R.styleable.OneHandedPanel).apply {
panelSide = getString(R.styleable.OneHandedPanel_panelSide) ?: OneHandedMode.START
recycle()
}
orientation = VERTICAL
gravity = Gravity.CENTER_VERTICAL
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
florisboard = FlorisBoard.getInstanceOrNull()
themeManager = ThemeManager.defaultOrNull()
closeBtn = findViewWithTag("one_handed_ctrl_close")
closeBtn?.setOnClickListener {
florisboard?.let {
it.prefs.keyboard.oneHandedMode = OneHandedMode.OFF
it.updateOneHandedPanelVisibility()
}
}
moveBtn = findViewWithTag("one_handed_ctrl_move")
moveBtn?.setOnClickListener {
florisboard?.let {
it.prefs.keyboard.oneHandedMode = panelSide
it.updateOneHandedPanelVisibility()
}
}
themeManager?.registerOnThemeUpdatedListener(this)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
florisboard = null
themeManager?.unregisterOnThemeUpdatedListener(this)
themeManager = null
closeBtn?.setOnClickListener(null)
closeBtn = null
moveBtn?.setOnClickListener(null)
moveBtn = null
}
override fun onThemeUpdated(theme: Theme) {
setBackgroundColor(theme.getAttr(Theme.Attr.ONE_HANDED_BACKGROUND).toSolidColor().color)
ColorStateList.valueOf(theme.getAttr(Theme.Attr.ONE_HANDED_FOREGROUND).toSolidColor().color).also {
closeBtn?.imageTintList = it
moveBtn?.imageTintList = it
}
closeBtn?.invalidate()
moveBtn?.invalidate()
invalidate()
}
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)
super.onMeasure(MeasureSpec.makeMeasureSpec(width.toInt(), MeasureSpec.EXACTLY), heightMeasureSpec)
}
}

View File

@@ -35,8 +35,12 @@ import kotlin.math.min
class PopupExtendedView : View, ThemeManager.OnThemeUpdatedListener {
private val themeManager: ThemeManager = ThemeManager.default()
private val activeBackgroundDrawable: PaintDrawable = PaintDrawable()
private var backgroundDrawable: PaintDrawable = PaintDrawable()
private val activeBackgroundDrawable: PaintDrawable = PaintDrawable().apply {
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
}
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
}
private val labelPaint: Paint = Paint().apply {
alpha = 255
color = 0
@@ -80,6 +84,7 @@ class PopupExtendedView : View, ThemeManager.OnThemeUpdatedListener {
init {
visibility = GONE
background = backgroundDrawable
elevation = ViewLayoutUtils.convertDpToPixel(4.0f, context)
}
override fun onAttachedToWindow() {
@@ -88,20 +93,17 @@ class PopupExtendedView : View, ThemeManager.OnThemeUpdatedListener {
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
themeManager.unregisterOnThemeUpdatedListener(this)
super.onDetachedFromWindow()
}
override fun onThemeUpdated(theme: Theme) {
activeBackgroundDrawable.apply {
setTint(theme.getAttr(Theme.Attr.POPUP_BACKGROUND_ACTIVE).toSolidColor().color)
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
}
backgroundDrawable.apply {
setTint(theme.getAttr(Theme.Attr.POPUP_BACKGROUND).toSolidColor().color)
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
}
elevation = ViewLayoutUtils.convertDpToPixel(4.0f, context)
labelPaint.color = theme.getAttr(Theme.Attr.POPUP_FOREGROUND).toSolidColor().color
tldPaint.color = theme.getAttr(Theme.Attr.POPUP_FOREGROUND).toSolidColor().color
if (isShowing) {

View File

@@ -16,17 +16,9 @@
package dev.patrickgold.florisboard.ime.popup
import android.content.Context
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dev.patrickgold.florisboard.ime.extension.Asset
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyTypeAdapter
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.text.key.KeyVariationAdapter
/**
* An object which maps each base key to its extended popups. This can be done for each
@@ -48,29 +40,5 @@ class PopupExtension(
) : Asset {
companion object : Asset.Companion<PopupExtension> {
override fun empty() = PopupExtension("", "", listOf(), mapOf())
override fun fromFile(context: Context, path: String): Result<PopupExtension, Throwable> {
return try {
val raw = context.assets.open(path).bufferedReader().use { it.readText() }
val asset = fromJsonString(raw)
if (asset != null) {
Ok(asset)
} else {
Err(NullPointerException())
}
} catch (e: Exception) {
Err(e)
}
}
fun fromJsonString(json: String): PopupExtension? {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.add(KeyTypeAdapter())
.add(KeyVariationAdapter())
.build()
val layoutAdapter = moshi.adapter(PopupExtension::class.java)
return layoutAdapter.fromJson(json)
}
}
}

View File

@@ -20,12 +20,14 @@ import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.ViewGroup
import android.widget.FrameLayout
import dev.patrickgold.florisboard.ime.core.PrefHelper
/**
* Basic helper view class which acts as a non-interactive layer view, which sits above the whole
* input UI. Automatically rejects any touch events and passes it through to the View below.
*/
class PopupLayerView : FrameLayout {
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
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)
@@ -34,6 +36,9 @@ class PopupLayerView : FrameLayout {
background = null
isClickable = false
isFocusable = false
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {

View File

@@ -17,7 +17,9 @@
package dev.patrickgold.florisboard.ime.popup
import android.content.res.Configuration
import android.view.*
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import androidx.core.content.ContextCompat.getDrawable
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
@@ -64,6 +66,10 @@ class PopupManager<T_KBD: View, T_KV: View>(
val isShowingExtendedPopup: Boolean
get() = popupViewExt.isShowing
companion object {
const val POPUP_EXTENSION_PATH_REL: String = "ime/text/characters/extended_popups"
}
init {
keyPopupWidth = keyboardView.resources.getDimension(R.dimen.key_width).toInt()
keyPopupHeight = keyboardView.resources.getDimension(R.dimen.key_height).toInt()
@@ -107,7 +113,8 @@ class PopupManager<T_KBD: View, T_KV: View>(
keyView.data.popup[adjustedIndex].label, adjustedIndex
)
}
KeyCode.TOGGLE_ONE_HANDED_MODE -> {
KeyCode.TOGGLE_ONE_HANDED_MODE_LEFT,
KeyCode.TOGGLE_ONE_HANDED_MODE_RIGHT -> {
getDrawable(keyView.context, R.drawable.ic_smartphone)?.let {
PopupExtendedView.Element.Icon(it, adjustedIndex)
} ?: PopupExtendedView.Element.Undefined

View File

@@ -34,7 +34,9 @@ import dev.patrickgold.florisboard.util.ViewLayoutUtils
class PopupView : View, ThemeManager.OnThemeUpdatedListener {
private val themeManager: ThemeManager = ThemeManager.default()
private var backgroundDrawable: PaintDrawable = PaintDrawable()
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
}
private val labelPaint: Paint = Paint().apply {
alpha = 255
color = 0
@@ -86,7 +88,6 @@ class PopupView : View, ThemeManager.OnThemeUpdatedListener {
override fun onThemeUpdated(theme: Theme) {
backgroundDrawable.apply {
setTint(theme.getAttr(Theme.Attr.POPUP_BACKGROUND).toSolidColor().color)
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
}
elevation = ViewLayoutUtils.convertDpToPixel(4.0f, context)
threeDotsDrawable?.apply {

View File

@@ -16,21 +16,24 @@
package dev.patrickgold.florisboard.ime.text
import android.content.Context
import android.os.Handler
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.view.KeyEvent
import android.view.inputmethod.*
import android.widget.LinearLayout
import android.widget.Toast
import android.widget.ViewFlipper
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
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.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.text.editing.EditingKeyboardView
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyType
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.text.key.*
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
@@ -38,6 +41,7 @@ import dev.patrickgold.florisboard.ime.text.smartbar.SmartbarView
import kotlinx.coroutines.*
import timber.log.Timber
import java.util.*
import kotlin.math.roundToLong
/**
* TextInputManager is responsible for managing everything which is related to text input. All of
@@ -50,7 +54,7 @@ import java.util.*
* TextInputManager is also the hub in the communication between the system, the active editor
* instance and the Smartbar.
*/
class TextInputManager private constructor() : CoroutineScope by MainScope(),
class TextInputManager private constructor() : CoroutineScope by MainScope(), InputKeyEventReceiver,
FlorisBoard.EventListener, SmartbarView.EventListener {
private val florisboard = FlorisBoard.getInstance()
@@ -58,28 +62,41 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
get() = florisboard.activeEditorInstance
private var activeKeyboardMode: KeyboardMode? = null
private var animator: ObjectAnimator? = null
private val keyboardViews = EnumMap<KeyboardMode, KeyboardView>(KeyboardMode::class.java)
private var editingKeyboardView: EditingKeyboardView? = null
private val osHandler = Handler()
private var loadingPlaceholderKeyboard: KeyboardView? = null
private var textViewFlipper: ViewFlipper? = null
var textViewGroup: LinearLayout? = null
private var textViewGroup: LinearLayout? = null
private val dictionaryManager: DictionaryManager = DictionaryManager.default()
private var activeDictionary: Dictionary<String, Int>? = null
val inputEventDispatcher: InputEventDispatcher = InputEventDispatcher.new(
parentScope = this,
repeatableKeyCodes = intArrayOf(
KeyCode.ARROW_DOWN,
KeyCode.ARROW_LEFT,
KeyCode.ARROW_RIGHT,
KeyCode.ARROW_UP,
KeyCode.DELETE,
KeyCode.FORWARD_DELETE
)
)
var keyVariation: KeyVariation = KeyVariation.NORMAL
val layoutManager = LayoutManager(florisboard)
private var smartbarView: SmartbarView? = null
// Caps/Space related properties
// Caps/Shift related properties
var caps: Boolean = false
private set
var capsLock: Boolean = false
private set
private var hasCapsRecentlyChanged: Boolean = false
private var hasSpaceRecentlyPressed: Boolean = false
private var newCapsState: Boolean = false
// Composing text related properties
var isManualSelectionMode: Boolean = false
private var isManualSelectionModeLeft: Boolean = false
private var isManualSelectionModeRight: Boolean = false
private var isManualSelectionModeStart: Boolean = false
private var isManualSelectionModeEnd: Boolean = false
companion object {
private var instance: TextInputManager? = null
@@ -104,6 +121,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
override fun onCreate() {
Timber.i("onCreate()")
inputEventDispatcher.keyEventReceiver = this
var subtypes = florisboard.subtypeManager.subtypes
if (subtypes.isEmpty()) {
subtypes = listOf(Subtype.DEFAULT)
@@ -115,6 +133,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
}
override fun onCreateInputView() {
keyboardViews.clear()
}
private suspend fun addKeyboardView(mode: KeyboardMode) {
val keyboardView = KeyboardView(florisboard.context)
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(mode, florisboard.activeSubtype, florisboard.prefs).await()
@@ -128,14 +150,38 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
override fun onRegisterInputView(inputView: InputView) {
Timber.i("onRegisterInputView(inputView)")
launch(Dispatchers.Main) {
textViewGroup = inputView.findViewById(R.id.text_input)
textViewFlipper = inputView.findViewById(R.id.text_input_view_flipper)
editingKeyboardView = inputView.findViewById(R.id.editing)
textViewGroup = inputView.findViewById(R.id.text_input)
textViewFlipper = inputView.findViewById(R.id.text_input_view_flipper)
editingKeyboardView = inputView.findViewById(R.id.editing)
loadingPlaceholderKeyboard = inputView.findViewById(R.id.keyboard_preview)
launch(Dispatchers.Main) {
textViewGroup?.let {
animator = ObjectAnimator.ofFloat(it, "alpha", 0.9f, 1.0f).apply {
duration = 125
repeatCount = ValueAnimator.INFINITE
repeatMode = ValueAnimator.REVERSE
start()
launch {
delay(duration)
try {
duration = 500
setFloatValues(1.0f, 0.4f)
} catch (_: Exception) {}
}
}
}
val activeKeyboardMode = getActiveKeyboardMode()
addKeyboardView(activeKeyboardMode)
setActiveKeyboardMode(activeKeyboardMode)
animator?.cancel()
textViewGroup?.let {
animator = ObjectAnimator.ofFloat(it, "alpha", it.alpha, 1.0f).apply {
duration = (((1.0f - it.alpha) / 0.6f) * 125f).roundToLong()
repeatCount = 0
start()
}
}
for (mode in KeyboardMode.values()) {
if (mode != activeKeyboardMode && mode != KeyboardMode.SMARTBAR_NUMBER_ROW) {
addKeyboardView(mode)
@@ -149,14 +195,21 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
smartbarView?.setEventListener(this)
}
fun unregisterSmartbarView(view: SmartbarView) {
if (smartbarView == view) {
smartbarView = null
}
}
/**
* Cancels all coroutines and cleans up.
*/
override fun onDestroy() {
Timber.i("onDestroy()")
inputEventDispatcher.keyEventReceiver = null
inputEventDispatcher.close()
cancel()
osHandler.removeCallbacksAndMessages(null)
layoutManager.onDestroy()
instance = null
}
@@ -246,24 +299,31 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
/**
* Sets [activeKeyboardMode] and updates the [SmartbarView.isQuickActionsVisible] state.
*/
fun setActiveKeyboardMode(mode: KeyboardMode) {
private fun setActiveKeyboardMode(mode: KeyboardMode) {
textViewFlipper?.displayedChild = textViewFlipper?.indexOfChild(when (mode) {
KeyboardMode.EDITING -> editingKeyboardView
else -> keyboardViews[mode]
}) ?: 0
})?.coerceAtLeast(0) ?: 0
keyboardViews[mode]?.updateVisibility()
keyboardViews[mode]?.requestLayout()
keyboardViews[mode]?.requestLayoutAllKeys()
activeKeyboardMode = mode
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
isManualSelectionModeStart = false
isManualSelectionModeEnd = false
smartbarView?.isQuickActionsVisible = false
smartbarView?.updateSmartbarState()
}
override fun onSubtypeChanged(newSubtype: Subtype) {
launch {
if (activeEditorInstance.isComposingEnabled) {
withContext(Dispatchers.IO) {
dictionaryManager.loadDictionary(AssetRef(AssetSource.Assets,"ime/dict/en.flict")).let {
activeDictionary = it.getOrDefault(null)
}
}
}
val keyboardView = keyboardViews[KeyboardMode.CHARACTERS]
keyboardView?.computedLayout = layoutManager.fetchComputedLayoutAsync(KeyboardMode.CHARACTERS, newSubtype, florisboard.prefs).await()
keyboardView?.updateVisibility()
@@ -275,13 +335,39 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* and passing this info on to the [SmartbarView] to turn it into candidate suggestions.
*/
override fun onUpdateSelection() {
if (!activeEditorInstance.isNewSelectionInBoundsOfOld) {
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
if (!inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
updateCapsState()
}
updateCapsState()
smartbarView?.updateSmartbarState()
if (BuildConfig.DEBUG) {
Timber.i("current word: ${activeEditorInstance.cachedInput.currentWord.text}")
}
if (activeEditorInstance.isComposingEnabled && !inputEventDispatcher.isPressed(KeyCode.DELETE)) {
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 = 3,
allowPossiblyOffensive = !florisboard.prefs.suggestion.blockPossiblyOffensive
).toStringList()
if (BuildConfig.DEBUG) {
val elapsed = (System.nanoTime() - startTime) / 1000.0
Timber.i("sugg fetch time: $elapsed us")
}
withContext(Dispatchers.Main) {
smartbarView?.setCandidateSuggestionWords(startTime, suggestions)
smartbarView?.updateCandidateSuggestionCapsState()
}
}
}
} else {
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), listOf())
}
}
}
override fun onPrimaryClipChanged() {
@@ -296,9 +382,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
if (!capsLock) {
caps = florisboard.prefs.correction.autoCapitalization &&
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
launch(Dispatchers.Main) {
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
}
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
}
}
@@ -307,14 +391,23 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* class.
*/
fun executeSwipeAction(swipeAction: SwipeAction) {
when (swipeAction) {
SwipeAction.DELETE_WORD -> handleDeleteWord()
SwipeAction.MOVE_CURSOR_DOWN -> handleArrow(KeyCode.ARROW_DOWN)
SwipeAction.MOVE_CURSOR_UP -> handleArrow(KeyCode.ARROW_UP)
SwipeAction.MOVE_CURSOR_LEFT -> handleArrow(KeyCode.ARROW_LEFT)
SwipeAction.MOVE_CURSOR_RIGHT -> handleArrow(KeyCode.ARROW_RIGHT)
SwipeAction.SHIFT -> handleShift()
else -> {}
val keyData = when (swipeAction) {
SwipeAction.DELETE_WORD -> KeyData.DELETE_WORD
SwipeAction.INSERT_SPACE -> KeyData.SPACE
SwipeAction.MOVE_CURSOR_DOWN -> KeyData.ARROW_DOWN
SwipeAction.MOVE_CURSOR_UP -> KeyData.ARROW_UP
SwipeAction.MOVE_CURSOR_LEFT -> KeyData.ARROW_LEFT
SwipeAction.MOVE_CURSOR_RIGHT -> KeyData.ARROW_RIGHT
SwipeAction.MOVE_CURSOR_START_OF_LINE -> KeyData.MOVE_START_OF_LINE
SwipeAction.MOVE_CURSOR_END_OF_LINE -> KeyData.MOVE_END_OF_LINE
SwipeAction.MOVE_CURSOR_START_OF_PAGE -> KeyData.MOVE_START_OF_PAGE
SwipeAction.MOVE_CURSOR_END_OF_PAGE -> KeyData.MOVE_END_OF_PAGE
SwipeAction.SHIFT -> KeyData.SHIFT
SwipeAction.SHOW_INPUT_METHOD_PICKER -> KeyData.SHOW_INPUT_METHOD_PICKER
else -> null
}
if (keyData != null) {
inputEventDispatcher.send(InputKeyEvent.downUp(keyData))
}
}
@@ -322,6 +415,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
setActiveKeyboardMode(KeyboardMode.CHARACTERS)
}
override fun onSmartbarCandidatePressed(word: String) {
activeEditorInstance.commitCompletion(word)
}
override fun onSmartbarPrivateModeButtonClicked() {
Toast.makeText(florisboard.context, R.string.private_mode_dialog__title, Toast.LENGTH_LONG).show()
}
@@ -337,13 +434,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
R.id.quick_action_switch_to_media_context -> florisboard.setActiveInput(R.id.media_input)
R.id.quick_action_open_settings -> florisboard.launchSettings()
R.id.quick_action_one_handed_toggle -> florisboard.toggleOneHandedMode()
R.id.quick_action_one_handed_toggle -> florisboard.toggleOneHandedMode(isRight = true)
R.id.quick_action_undo -> {
handleUndo()
activeEditorInstance.performUndo()
return
}
R.id.quick_action_redo -> {
handleRedo()
activeEditorInstance.performRedo()
return
}
}
@@ -351,21 +448,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
smartbarView?.updateSmartbarState()
}
private fun handleUndo(){
activeEditorInstance.performUndo()
}
private fun handleRedo(){
activeEditorInstance.performRedo()
}
/**
* Handles a [KeyCode.DELETE] event.
*/
private fun handleDelete() {
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
isManualSelectionModeStart = false
isManualSelectionModeEnd = false
activeEditorInstance.deleteBackwards()
}
@@ -374,8 +463,8 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
*/
private fun handleDeleteWord() {
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
isManualSelectionModeStart = false
isManualSelectionModeEnd = false
activeEditorInstance.deleteWordsBeforeCursor(1)
}
@@ -401,44 +490,79 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
/**
* Handles a [KeyCode.SHIFT] event.
* Handles a [KeyCode.LANGUAGE_SWITCH] event. Also handles if the language switch should cycle
* FlorisBoard internal or system-wide.
*/
private fun handleShift() {
if (hasCapsRecentlyChanged) {
osHandler.removeCallbacksAndMessages(null)
private fun handleLanguageSwitch() {
when (florisboard.prefs.keyboard.utilityKeyAction) {
UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS,
UtilityKeyAction.SWITCH_LANGUAGE -> florisboard.switchToNextSubtype()
else -> florisboard.switchToNextKeyboard()
}
}
/**
* Handles a [KeyCode.SHIFT] down event.
*/
private fun handleShiftDown(ev: InputKeyEvent) {
if (ev.isConsecutiveEventOf(inputEventDispatcher.lastKeyEventDown, florisboard.prefs.keyboard.longPressDelay.toLong())) {
newCapsState = true
caps = true
capsLock = true
hasCapsRecentlyChanged = false
} else {
caps = !caps
newCapsState = !caps
caps = true
capsLock = false
hasCapsRecentlyChanged = true
osHandler.postDelayed({
hasCapsRecentlyChanged = false
}, 300)
}
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
smartbarView?.updateCandidateSuggestionCapsState()
}
/**
* Handles a [KeyCode.SHIFT] up event.
*/
private fun handleShiftUp() {
caps = newCapsState
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
smartbarView?.updateCandidateSuggestionCapsState()
}
/**
* Handles a [KeyCode.SHIFT] cancel event.
*/
private fun handleShiftCancel() {
caps = false
capsLock = false
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
smartbarView?.updateCandidateSuggestionCapsState()
}
/**
* Handles a [KeyCode.SHIFT] up event.
*/
private fun handleShiftLock() {
val lastKeyEvent = inputEventDispatcher.lastKeyEventDown ?: return
if (lastKeyEvent.data.code == KeyCode.SHIFT && lastKeyEvent.action == InputKeyEvent.Action.DOWN) {
newCapsState = true
caps = true
capsLock = true
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
smartbarView?.updateCandidateSuggestionCapsState()
}
}
/**
* Handles a [KeyCode.SPACE] event. Also handles the auto-correction of two space taps if
* enabled by the user.
*/
private fun handleSpace() {
private fun handleSpace(ev: InputKeyEvent) {
if (florisboard.prefs.correction.doubleSpacePeriod) {
if (hasSpaceRecentlyPressed) {
osHandler.removeCallbacksAndMessages(null)
if (ev.isConsecutiveEventOf(inputEventDispatcher.lastKeyEventUp, florisboard.prefs.keyboard.longPressDelay.toLong())) {
val text = activeEditorInstance.getTextBeforeCursor(2)
if (text.length == 2 && !text.matches("""[.!?‽\s][\s]""".toRegex())) {
activeEditorInstance.deleteBackwards()
activeEditorInstance.commitText(".")
}
hasSpaceRecentlyPressed = false
} else {
hasSpaceRecentlyPressed = true
osHandler.postDelayed({
hasSpaceRecentlyPressed = false
}, 300)
}
}
activeEditorInstance.commitText(KeyCode.SPACE.toChar().toString())
@@ -447,114 +571,64 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
/**
* Handles [KeyCode] arrow and move events, behaves differently depending on text selection.
*/
private fun handleArrow(code: Int) = activeEditorInstance.apply {
val selectionStartMin = 0
val selectionEndMax = cachedText.length
if (selection.isSelectionMode && isManualSelectionMode) {
// Text is selected and it is manual selection -> Expand selection depending on started
// direction.
when (code) {
KeyCode.ARROW_DOWN -> {}
KeyCode.ARROW_LEFT -> {
if (isManualSelectionModeLeft) {
setSelection(
(selection.start - 1).coerceAtLeast(selectionStartMin),
selection.end
)
} else {
setSelection(selection.start, selection.end - 1)
}
}
KeyCode.ARROW_RIGHT -> {
if (isManualSelectionModeRight) {
setSelection(
selection.start,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
} else {
setSelection(selection.start + 1, selection.end)
}
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
if (isManualSelectionModeLeft) {
setSelection(selectionStartMin, selection.end)
} else {
setSelection(selectionStartMin, selection.start)
}
}
KeyCode.MOVE_END -> {
if (isManualSelectionModeRight) {
setSelection(selection.start, selectionEndMax)
} else {
setSelection(selection.end, selectionEndMax)
}
private fun handleArrow(code: Int, count: Int) = activeEditorInstance.apply {
val isShiftPressed = isManualSelectionMode || inputEventDispatcher.isPressed(KeyCode.SHIFT)
when (code) {
KeyCode.ARROW_DOWN -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = false
isManualSelectionModeEnd = true
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, meta(shift = isShiftPressed), count)
}
} else if (selection.isSelectionMode && !isManualSelectionMode) {
// Text is selected but no manual selection mode -> arrows behave as if selection was
// started in manual left mode
when (code) {
KeyCode.ARROW_DOWN -> {}
KeyCode.ARROW_LEFT -> {
setSelection(selection.start, selection.end - 1)
}
KeyCode.ARROW_RIGHT -> {
setSelection(
selection.start,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
setSelection(selectionStartMin, selection.start)
}
KeyCode.MOVE_END -> {
setSelection(selection.start, selectionEndMax)
KeyCode.ARROW_LEFT -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = true
isManualSelectionModeEnd = false
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, meta(shift = isShiftPressed), count)
}
} else if (!selection.isSelectionMode && isManualSelectionMode) {
// No text is selected but manual selection mode is active, user wants to start a new
// selection. Must set manual selection direction.
when (code) {
KeyCode.ARROW_DOWN -> {}
KeyCode.ARROW_LEFT -> {
setSelection(
(selection.start - 1).coerceAtLeast(selectionStartMin),
selection.start
)
isManualSelectionModeLeft = true
isManualSelectionModeRight = false
}
KeyCode.ARROW_RIGHT -> {
setSelection(
selection.end,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
isManualSelectionModeLeft = false
isManualSelectionModeRight = true
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
setSelection(selectionStartMin, selection.start)
isManualSelectionModeLeft = true
isManualSelectionModeRight = false
}
KeyCode.MOVE_END -> {
setSelection(selection.end, selectionEndMax)
isManualSelectionModeLeft = false
isManualSelectionModeRight = true
KeyCode.ARROW_RIGHT -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = false
isManualSelectionModeEnd = true
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, meta(shift = isShiftPressed), count)
}
} else {
// No selection and no manual selection mode -> move cursor around
when (code) {
KeyCode.ARROW_DOWN -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN)
KeyCode.ARROW_LEFT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT)
KeyCode.ARROW_RIGHT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT)
KeyCode.ARROW_UP -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_HOME -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_END -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_DOWN)
KeyCode.ARROW_UP -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = true
isManualSelectionModeEnd = false
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_UP, meta(shift = isShiftPressed), count)
}
KeyCode.MOVE_START_OF_PAGE -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = true
isManualSelectionModeEnd = false
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_UP, meta(alt = true, shift = isShiftPressed), count)
}
KeyCode.MOVE_END_OF_PAGE -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = false
isManualSelectionModeEnd = true
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, meta(alt = true, shift = isShiftPressed), count)
}
KeyCode.MOVE_START_OF_LINE -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = true
isManualSelectionModeEnd = false
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, meta(alt = true, shift = isShiftPressed), count)
}
KeyCode.MOVE_END_OF_LINE -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = false
isManualSelectionModeEnd = true
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, meta(alt = true, shift = isShiftPressed), count)
}
}
}
@@ -564,10 +638,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
*/
private fun handleClipboardSelect() = activeEditorInstance.apply {
if (selection.isSelectionMode) {
if (isManualSelectionMode && isManualSelectionModeLeft) {
setSelection(selection.start, selection.start)
if (isManualSelectionMode && isManualSelectionModeStart) {
selection.updateAndNotify(selection.start, selection.start)
} else {
setSelection(selection.end, selection.end)
selection.updateAndNotify(selection.end, selection.end)
}
isManualSelectionMode = false
} else {
@@ -578,27 +652,40 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
/**
* Handles a [KeyCode.CLIPBOARD_SELECT_ALL] event.
* Adjusts a given key data for caps state and returns the correct reference.
*/
private fun handleClipboardSelectAll() {
activeEditorInstance.setSelection(0, activeEditorInstance.cachedText.length)
private fun getAdjustedKeyData(keyData: KeyData): KeyData {
return if (caps && keyData is FlorisKeyData && keyData.shift != null) { keyData.shift!! } else { keyData }
}
/**
* Main logic point for sending a key press. Different actions may occur depending on the given
* [KeyData]. This method handles all key press send events, which are text based. For media
* input send events see MediaInputManager.
*
* @param keyData The [KeyData] object which should be sent.
*/
fun sendKeyPress(keyData: KeyData) {
when (keyData.code) {
override fun onInputKeyDown(ev: InputKeyEvent) {
val data = getAdjustedKeyData(ev.data)
when (data.code) {
KeyCode.INTERNAL_BATCH_EDIT -> {
florisboard.beginInternalBatchEdit()
return
}
KeyCode.SHIFT -> {
handleShiftDown(ev)
}
}
}
override fun onInputKeyUp(ev: InputKeyEvent) {
val data = getAdjustedKeyData(ev.data)
when (data.code) {
KeyCode.ARROW_DOWN,
KeyCode.ARROW_LEFT,
KeyCode.ARROW_RIGHT,
KeyCode.ARROW_UP,
KeyCode.MOVE_HOME,
KeyCode.MOVE_END -> handleArrow(keyData.code)
KeyCode.MOVE_START_OF_PAGE,
KeyCode.MOVE_END_OF_PAGE,
KeyCode.MOVE_START_OF_LINE,
KeyCode.MOVE_END_OF_LINE -> if (ev.action == InputKeyEvent.Action.DOWN_UP || ev.action == InputKeyEvent.Action.REPEAT) {
handleArrow(data.code, ev.count)
} else {
handleArrow(data.code, 1)
}
KeyCode.CLIPBOARD_CUT -> activeEditorInstance.performClipboardCut()
KeyCode.CLIPBOARD_COPY -> activeEditorInstance.performClipboardCopy()
KeyCode.CLIPBOARD_PASTE -> {
@@ -606,26 +693,36 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
smartbarView?.resetClipboardSuggestion()
}
KeyCode.CLIPBOARD_SELECT -> handleClipboardSelect()
KeyCode.CLIPBOARD_SELECT_ALL -> handleClipboardSelectAll()
KeyCode.CLIPBOARD_SELECT_ALL -> activeEditorInstance.performClipboardSelectAll()
KeyCode.DELETE -> {
handleDelete()
smartbarView?.resetClipboardSuggestion()
if (ev.action == InputKeyEvent.Action.DOWN_UP || ev.action == InputKeyEvent.Action.UP) {
smartbarView?.resetClipboardSuggestion()
}
}
KeyCode.DELETE_WORD -> {
handleDeleteWord()
if (ev.action == InputKeyEvent.Action.DOWN_UP || ev.action == InputKeyEvent.Action.UP) {
smartbarView?.resetClipboardSuggestion()
}
}
KeyCode.ENTER -> {
handleEnter()
smartbarView?.resetClipboardSuggestion()
}
KeyCode.LANGUAGE_SWITCH -> florisboard.switchToNextSubtype()
KeyCode.SETTINGS -> florisboard.launchSettings()
KeyCode.SHIFT -> handleShift()
KeyCode.SHOW_INPUT_METHOD_PICKER -> {
val im =
florisboard.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
im.showInputMethodPicker()
KeyCode.INTERNAL_BATCH_EDIT -> {
florisboard.endInternalBatchEdit()
return
}
KeyCode.LANGUAGE_SWITCH -> handleLanguageSwitch()
KeyCode.SETTINGS -> florisboard.launchSettings()
KeyCode.SHIFT -> handleShiftUp()
KeyCode.SHIFT_LOCK -> handleShiftLock()
KeyCode.SHOW_INPUT_METHOD_PICKER -> florisboard.imeManager?.showInputMethodPicker()
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> florisboard.setActiveInput(R.id.media_input)
KeyCode.SWITCH_TO_TEXT_CONTEXT -> florisboard.setActiveInput(R.id.text_input)
KeyCode.TOGGLE_ONE_HANDED_MODE -> florisboard.toggleOneHandedMode()
KeyCode.TOGGLE_ONE_HANDED_MODE_LEFT -> florisboard.toggleOneHandedMode(isRight = false)
KeyCode.TOGGLE_ONE_HANDED_MODE_RIGHT -> florisboard.toggleOneHandedMode(isRight = true)
KeyCode.VIEW_CHARACTERS -> setActiveKeyboardMode(KeyboardMode.CHARACTERS)
KeyCode.VIEW_NUMERIC -> setActiveKeyboardMode(KeyboardMode.NUMERIC)
KeyCode.VIEW_NUMERIC_ADVANCED -> setActiveKeyboardMode(KeyboardMode.NUMERIC_ADVANCED)
@@ -638,50 +735,59 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
KeyboardMode.NUMERIC,
KeyboardMode.NUMERIC_ADVANCED,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> when (keyData.type) {
KeyboardMode.PHONE2 -> when (data.type) {
KeyType.CHARACTER,
KeyType.NUMERIC -> {
val text = keyData.code.toChar().toString()
val text = data.code.toChar().toString()
activeEditorInstance.commitText(text)
}
else -> when (keyData.code) {
else -> when (data.code) {
KeyCode.PHONE_PAUSE,
KeyCode.PHONE_WAIT -> {
val text = keyData.code.toChar().toString()
val text = data.code.toChar().toString()
activeEditorInstance.commitText(text)
}
}
}
else -> when (keyData.type) {
KeyType.CHARACTER, KeyType.NUMERIC -> when (keyData.code) {
KeyCode.SPACE -> handleSpace()
else -> when (data.type) {
KeyType.CHARACTER, KeyType.NUMERIC -> when (data.code) {
KeyCode.SPACE -> handleSpace(ev)
KeyCode.URI_COMPONENT_TLD -> {
val tld = when (caps) {
true -> keyData.label.toUpperCase(Locale.getDefault())
false -> keyData.label.toLowerCase(Locale.getDefault())
}
val tld = data.label.toLowerCase(Locale.ENGLISH)
activeEditorInstance.commitText(tld)
}
else -> {
var text = keyData.code.toChar().toString()
text = when (caps) {
true -> text.toUpperCase(Locale.getDefault())
false -> text.toLowerCase(Locale.getDefault())
var text = data.code.toChar().toString()
val locale = if (florisboard.activeSubtype.locale.language == "el") { Locale.getDefault() } else { florisboard.activeSubtype.locale }
text = when (caps && activeKeyboardMode == KeyboardMode.CHARACTERS) {
true -> text.toUpperCase(locale)
false -> text
}
activeEditorInstance.commitText(text)
}
}
else -> {
Timber.e("sendKeyPress(keyData): Received unknown key: $keyData")
Timber.e("sendKeyPress(keyData): Received unknown key: $data")
}
}
}
smartbarView?.resetClipboardSuggestion()
}
}
if (keyData.code != KeyCode.SHIFT && !capsLock) {
if (data.code != KeyCode.SHIFT && !capsLock && !inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
updateCapsState()
}
smartbarView?.updateSmartbarState()
}
override fun onInputKeyRepeat(ev: InputKeyEvent) {
onInputKeyUp(ev)
}
override fun onInputKeyCancel(ev: InputKeyEvent) {
val data = getAdjustedKeyData(ev.data)
when (data.code) {
KeyCode.SHIFT -> handleShiftCancel()
}
}
}

View File

@@ -30,6 +30,7 @@ import android.widget.Button
import androidx.appcompat.widget.AppCompatImageButton
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
@@ -46,10 +47,25 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val themeManager: ThemeManager = ThemeManager.default()
private val data: KeyData
private val data: KeyData = when (id) {
R.id.arrow_down -> KeyData.ARROW_DOWN
R.id.arrow_left -> KeyData.ARROW_LEFT
R.id.arrow_right -> KeyData.ARROW_RIGHT
R.id.arrow_up -> KeyData.ARROW_UP
R.id.backspace -> KeyData.DELETE
R.id.clipboard_copy -> KeyData.CLIPBOARD_COPY
R.id.clipboard_cut -> KeyData.CLIPBOARD_CUT
R.id.clipboard_paste -> KeyData.CLIPBOARD_PASTE
R.id.move_start_of_line -> KeyData.MOVE_START_OF_LINE
R.id.move_end_of_line -> KeyData.MOVE_END_OF_LINE
R.id.select -> KeyData.CLIPBOARD_SELECT
R.id.select_all -> KeyData.CLIPBOARD_SELECT_ALL
else -> KeyData.UNSPECIFIED
}
private var isKeyPressed: Boolean = false
private val repeatedKeyPressHandler: Handler = Handler(context.mainLooper)
private val defaultTextSize: Float = Button(context).textSize
private var label: String? = null
private var labelPaint: Paint = Paint().apply {
alpha = 255
@@ -57,7 +73,7 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
isAntiAlias = true
isFakeBoldText = false
textAlign = Paint.Align.CENTER
textSize = Button(context).textSize
textSize = defaultTextSize
typeface = Typeface.DEFAULT
}
@@ -71,22 +87,6 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, R.style.TextEditingButton)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
val code = when (id) {
R.id.arrow_down -> KeyCode.ARROW_DOWN
R.id.arrow_left -> KeyCode.ARROW_LEFT
R.id.arrow_right -> KeyCode.ARROW_RIGHT
R.id.arrow_up -> KeyCode.ARROW_UP
R.id.backspace -> KeyCode.DELETE
R.id.clipboard_copy -> KeyCode.CLIPBOARD_COPY
R.id.clipboard_cut -> KeyCode.CLIPBOARD_CUT
R.id.clipboard_paste -> KeyCode.CLIPBOARD_PASTE
R.id.move_home -> KeyCode.MOVE_HOME
R.id.move_end -> KeyCode.MOVE_END
R.id.select -> KeyCode.CLIPBOARD_SELECT
R.id.select_all -> KeyCode.CLIPBOARD_SELECT_ALL
else -> 0
}
data = KeyData(code = code)
context.obtainStyledAttributes(attrs, R.styleable.EditingKeyView).apply {
label = getString(R.styleable.EditingKeyView_android_text)
recycle()
@@ -123,7 +123,7 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
val delayMillis = prefs.keyboard.longPressDelay.toLong()
repeatedKeyPressHandler.postAtScheduledRate(delayMillis, 25) {
if (isKeyPressed) {
florisboard?.textInputManager?.sendKeyPress(data)
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.downUp(data))
} else {
repeatedKeyPressHandler.cancelAll()
}
@@ -135,7 +135,7 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
isKeyPressed = false
repeatedKeyPressHandler.cancelAll()
if (event.actionMasked != MotionEvent.ACTION_CANCEL) {
florisboard?.textInputManager?.sendKeyPress(data)
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.downUp(data))
}
}
else -> return false
@@ -173,8 +173,10 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
}
val isPortrait =
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
if (!isPortrait) {
labelPaint.textSize *= 0.9f
labelPaint.textSize = if (isPortrait) {
defaultTextSize
} else {
defaultTextSize * 0.9f
}
val centerX = measuredWidth / 2.0f
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2

View File

@@ -35,8 +35,6 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val themeManager: ThemeManager = ThemeManager.default()
private var arrowUpKey: EditingKeyView? = null
private var arrowDownKey: EditingKeyView? = null
private var selectKey: EditingKeyView? = null
private var selectAllKey: EditingKeyView? = null
private var cutKey: EditingKeyView? = null
@@ -45,16 +43,13 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
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) {
florisboard?.addEventListener(this)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
florisboard?.addEventListener(this)
themeManager.registerOnThemeUpdatedListener(this)
arrowUpKey = findViewById(R.id.arrow_up)
arrowDownKey = findViewById(R.id.arrow_down)
selectKey = findViewById(R.id.select)
selectAllKey = findViewById(R.id.select_all)
cutKey = findViewById(R.id.clipboard_cut)
@@ -63,8 +58,9 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
themeManager.unregisterOnThemeUpdatedListener(this)
florisboard?.removeEventListener(this)
super.onDetachedFromWindow()
}
override fun onThemeUpdated(theme: Theme) {
@@ -74,8 +70,6 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
override fun onUpdateSelection() {
val isSelectionActive = florisboard?.activeEditorInstance?.selection?.isSelectionMode ?: false
val isSelectionMode = florisboard?.textInputManager?.isManualSelectionMode ?: false
arrowUpKey?.isEnabled = !(isSelectionActive || isSelectionMode)
arrowDownKey?.isEnabled = !(isSelectionActive || isSelectionMode)
selectKey?.isHighlighted = isSelectionActive || isSelectionMode
selectAllKey?.visibility = when {
isSelectionActive -> View.GONE

View File

@@ -27,11 +27,17 @@ enum class SwipeAction {
DELETE_WORD,
DELETE_WORDS_PRECISELY,
HIDE_KEYBOARD,
INSERT_SPACE,
MOVE_CURSOR_UP,
MOVE_CURSOR_DOWN,
MOVE_CURSOR_LEFT,
MOVE_CURSOR_RIGHT,
MOVE_CURSOR_START_OF_LINE,
MOVE_CURSOR_END_OF_LINE,
MOVE_CURSOR_START_OF_PAGE,
MOVE_CURSOR_END_OF_PAGE,
SHIFT,
SHOW_INPUT_METHOD_PICKER,
SWITCH_TO_PREV_SUBTYPE,
SWITCH_TO_NEXT_SUBTYPE,
SWITCH_TO_PREV_KEYBOARD;

View File

@@ -19,13 +19,44 @@ package dev.patrickgold.florisboard.ime.text.gestures
import android.content.Context
import android.view.MotionEvent
import dev.patrickgold.florisboard.R
import java.lang.Exception
import kotlin.math.*
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.atan
/**
* Wrapper class which holds all enums, interfaces and classes for detecting a swipe gesture.
*/
abstract class SwipeGesture {
companion object {
/**
* Returns a numeric value for a given [DistanceThreshold], based on the values defined in
* the resources dimens.xml file.
*/
fun numericValue(context: Context, of: DistanceThreshold): Double {
return when (of) {
DistanceThreshold.VERY_SHORT -> context.resources.getDimension(R.dimen.gesture_distance_threshold_very_short)
DistanceThreshold.SHORT -> context.resources.getDimension(R.dimen.gesture_distance_threshold_short)
DistanceThreshold.NORMAL -> context.resources.getDimension(R.dimen.gesture_distance_threshold_normal)
DistanceThreshold.LONG -> context.resources.getDimension(R.dimen.gesture_distance_threshold_long)
DistanceThreshold.VERY_LONG -> context.resources.getDimension(R.dimen.gesture_distance_threshold_very_long)
}.toDouble()
}
/**
* Returns a numeric value for a given [VelocityThreshold], based on the values defined in
* the resources dimens.xml file.
*/
fun numericValue(context: Context, of: VelocityThreshold): Double {
return when (of) {
VelocityThreshold.VERY_SLOW -> context.resources.getInteger(R.integer.gesture_velocity_threshold_very_slow)
VelocityThreshold.SLOW -> context.resources.getInteger(R.integer.gesture_velocity_threshold_slow)
VelocityThreshold.NORMAL -> context.resources.getInteger(R.integer.gesture_velocity_threshold_normal)
VelocityThreshold.FAST -> context.resources.getInteger(R.integer.gesture_velocity_threshold_fast)
VelocityThreshold.VERY_FAST -> context.resources.getInteger(R.integer.gesture_velocity_threshold_very_fast)
}.toDouble()
}
}
/**
* Class which detects swipes based on given [MotionEvent]s. Only supports single-finger swipes
* and ignores additional pointers provided, if any.
@@ -33,63 +64,116 @@ abstract class SwipeGesture {
* @property listener The listener to report detected swipes to.
*/
class Detector(private val context: Context, private val listener: Listener) {
private val eventList: MutableList<MotionEvent> = mutableListOf()
private var indexFirst: Int = 0
private var indexLastMoveRecognized: Int = 0
private var pointerDataMap: MutableMap<Int, PointerData> = mutableMapOf()
private var thresholdWidth: Double = numericValue(context, DistanceThreshold.NORMAL)
private var unitWidth: Double = thresholdWidth / 4.0
var distanceThreshold: DistanceThreshold = DistanceThreshold.NORMAL
set(value) {
field = value
thresholdWidth = numericValue(context, value)
unitWidth = thresholdWidth / 4.0
}
var velocityThreshold: VelocityThreshold = VelocityThreshold.NORMAL
fun onTouchEvent(event: MotionEvent): Boolean {
/**
* Method which evaluates if a given [event] is a gesture.
*
* @param event The MotionEvent which should be checked for a gesture.
* @param alwaysTriggerOnMove Set to true if the moving detection algorithm should always
* trigger, regardless of the distance from the previous event. Defaults to false.
* @return True if the given [event] is a gesture, false otherwise.
*/
fun onTouchEvent(event: MotionEvent, alwaysTriggerOnMove: Boolean = false): Boolean {
try {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN -> {
clearEventList()
eventList.add(MotionEvent.obtainNoHistory(event))
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
resetState()
}
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
pointerDataMap[pointerId] = PointerData().apply {
firstX = event.getX(pointerIndex)
firstY = event.getY(pointerIndex)
lastX = firstX
lastY = firstY
}
}
MotionEvent.ACTION_MOVE -> {
eventList.add(MotionEvent.obtainNoHistory(event))
val lastEvent = eventList[indexLastMoveRecognized]
val diffX = event.x - lastEvent.x
val diffY = event.y - lastEvent.y
val distanceThresholdNV = numericValue(distanceThreshold) / 4.0f
return if (abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) {
indexLastMoveRecognized = eventList.size - 1
val direction = detectDirection(diffX.toDouble(), diffY.toDouble())
listener.onSwipe(direction, Type.TOUCH_MOVE)
} else {
false
for (pointerIndex in 0 until event.pointerCount) {
val pointerId = event.getPointerId(pointerIndex)
pointerDataMap[pointerId]?.apply {
val absDiffX = event.getX(pointerIndex) - firstX
val absDiffY = event.getY(pointerIndex) - firstY
val relDiffX = event.getX(pointerIndex) - lastX
val relDiffY = event.getY(pointerIndex) - lastY
return if (alwaysTriggerOnMove || abs(relDiffX) > (thresholdWidth / 2.0) || abs(relDiffY) > (thresholdWidth / 2.0)) {
lastX = event.getX(pointerIndex)
lastY = event.getY(pointerIndex)
val direction = detectDirection(relDiffX.toDouble(), relDiffY.toDouble())
val newAbsUnitCountX = (absDiffX / unitWidth).toInt()
val newAbsUnitCountY = (absDiffY / unitWidth).toInt()
val relUnitCountX = newAbsUnitCountX - absUnitCountX
val relUnitCountY = newAbsUnitCountY - absUnitCountY
absUnitCountX = newAbsUnitCountX
absUnitCountY = newAbsUnitCountY
listener.onSwipe(Event(
direction = direction,
type = Type.TOUCH_MOVE,
pointerId,
absUnitCountX,
absUnitCountY,
relUnitCountX,
relUnitCountY
))
} else {
false
}
}
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> {
val firstEvent = eventList[indexFirst]
val diffX = event.x - firstEvent.x
val diffY = event.y - firstEvent.y
val distanceThresholdNV = numericValue(distanceThreshold)
/*val velocityThresholdNV = numericValue(velocityThreshold)
val velocity =
((convertPixelsToDp(
sqrt(diffX.pow(2) + diffY.pow(2)),
context
) / event.downTime) * 10.0f.pow(8)).toInt()*/
clearEventList()
// return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV)) {
val direction = detectDirection(diffX.toDouble(), diffY.toDouble())
listener.onSwipe(direction, Type.TOUCH_UP)
} else {
false
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
pointerDataMap.remove(pointerId)?.apply {
val absDiffX = event.getX(pointerIndex) - firstX
val absDiffY = event.getY(pointerIndex) - firstY
/*val velocityThresholdNV = numericValue(velocityThreshold)
val velocity =
((convertPixelsToDp(
sqrt(diffX.pow(2) + diffY.pow(2)),
context
) / event.downTime) * 10.0f.pow(8)).toInt()*/
// return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
return if ((abs(absDiffX) > thresholdWidth || abs(absDiffY) > thresholdWidth)) {
val direction = detectDirection(absDiffX.toDouble(), absDiffY.toDouble())
absUnitCountX = (absDiffX / unitWidth).toInt()
absUnitCountY = (absDiffY / unitWidth).toInt()
listener.onSwipe(Event(
direction = direction,
type = Type.TOUCH_UP,
pointerId,
absUnitCountX,
absUnitCountY,
absUnitCountX,
absUnitCountY
))
} else {
false
}
}
return false
}
MotionEvent.ACTION_CANCEL -> {
clearEventList()
resetState()
}
else -> return false
}
return false
} catch(e: Exception) {
} catch (e: Exception) {
return false
}
}
@@ -134,50 +218,53 @@ abstract class SwipeGesture {
}
/**
* Cleans up and clears the event list.
* Resets the state.
*/
private fun clearEventList() {
for (event in eventList) {
event.recycle()
}
eventList.clear()
indexFirst = 0
indexLastMoveRecognized = 0
private fun resetState() {
pointerDataMap.clear()
}
/**
* Returns a numeric value for a given [DistanceThreshold], based on the values defined in
* the resources dimens.xml file.
*/
private fun numericValue(of: DistanceThreshold): Double {
return when (of) {
DistanceThreshold.VERY_SHORT -> context.resources.getDimension(R.dimen.gesture_distance_threshold_very_short)
DistanceThreshold.SHORT -> context.resources.getDimension(R.dimen.gesture_distance_threshold_short)
DistanceThreshold.NORMAL -> context.resources.getDimension(R.dimen.gesture_distance_threshold_normal)
DistanceThreshold.LONG -> context.resources.getDimension(R.dimen.gesture_distance_threshold_long)
DistanceThreshold.VERY_LONG -> context.resources.getDimension(R.dimen.gesture_distance_threshold_very_long)
}.toDouble()
}
/**
* Returns a numeric value for a given [VelocityThreshold], based on the values defined in
* the resources dimens.xml file.
*/
private fun numericValue(of: VelocityThreshold): Double {
return when (of) {
VelocityThreshold.VERY_SLOW -> context.resources.getInteger(R.integer.gesture_velocity_threshold_very_slow)
VelocityThreshold.SLOW -> context.resources.getInteger(R.integer.gesture_velocity_threshold_slow)
VelocityThreshold.NORMAL -> context.resources.getInteger(R.integer.gesture_velocity_threshold_normal)
VelocityThreshold.FAST -> context.resources.getInteger(R.integer.gesture_velocity_threshold_fast)
VelocityThreshold.VERY_FAST -> context.resources.getInteger(R.integer.gesture_velocity_threshold_very_fast)
}.toDouble()
class PointerData {
var firstX: Float = 0.0f
var firstY: Float = 0.0f
var lastX: Float = 0.0f
var lastY: Float = 0.0f
var absUnitCountX: Int = 0
var absUnitCountY: Int = 0
}
}
/**
* An interface which provides an abstract callback function, which will be called for any
* detected swipe event.
*/
interface Listener {
fun onSwipe(direction: Direction, type: Type): Boolean
fun onSwipe(event: Event): Boolean
}
/**
* Data class which describes a single gesture event.
*/
data class Event(
/** The direction of the swipe. */
val direction: Direction,
/** The type of the swipe. */
val type: Type,
/** The pointer ID of this event, corresponds to the value reported by the original MotionEvent. */
val pointerId: Int,
/** The unit count on the x-axis, measured from the first event (ACTION_DOWN). */
val absUnitCountX: Int,
/** The unit count on the y-axis, measured from the first event (ACTION_DOWN). */
val absUnitCountY: Int,
/** The unit count on the x-axis, measured from the last event (ACTION_MOVE). */
val relUnitCountX: Int,
/** The unit count on the y-axis, measured from the last event (ACTION_MOVE). */
val relUnitCountY: Int
)
/**
* ENum which defines the direction of the detected swipe.
*/
enum class Direction {
UP_LEFT,
UP,
@@ -189,6 +276,9 @@ abstract class SwipeGesture {
LEFT,
}
/**
* Enum which defines the type of the gesture.
*/
enum class Type {
TOUCH_UP,
TOUCH_MOVE;

View File

@@ -40,15 +40,17 @@ object KeyCode {
const val ARROW_RIGHT = -21
const val ARROW_UP = -22
const val ARROW_DOWN = -23
const val MOVE_HOME = -24
const val MOVE_END = -25
const val MOVE_START_OF_PAGE = -24
const val MOVE_END_OF_PAGE = -25
const val MOVE_START_OF_LINE = -26
const val MOVE_END_OF_LINE = -27
const val SETTINGS = -100
const val CANCEL = -3
const val CLEAR_INPUT = -13
const val VOICE_INPUT = -4
const val DISABLED = 0
const val UNSPECIFIED = 0
const val SPLIT_LAYOUT = -110
const val MERGE_LAYOUT = -111
@@ -83,9 +85,12 @@ object KeyCode {
const val SWITCH_TO_TEXT_CONTEXT = -212
const val SWITCH_TO_MEDIA_CONTEXT = -213
const val SWITCH_TO_CLIPBOARD_CONTEXT = -214
const val TOGGLE_ONE_HANDED_MODE = -215
const val TOGGLE_ONE_HANDED_MODE_LEFT = -215
const val TOGGLE_ONE_HANDED_MODE_RIGHT =-216
const val URI_COMPONENT_TLD = -255
const val INTERNAL_BATCH_EDIT = -901
const val KESHIDA = 1600
const val HALF_SPACE = 8204
}

View File

@@ -17,6 +17,7 @@
package dev.patrickgold.florisboard.ime.text.key
import dev.patrickgold.florisboard.ime.popup.PopupSet
import dev.patrickgold.florisboard.ime.text.key.FlorisKeyData.Companion.GROUP_DEFAULT
/**
* Data class which describes a single key and its attributes.
@@ -33,7 +34,167 @@ open class KeyData(
var type: KeyType = KeyType.CHARACTER,
var code: Int = 0,
var label: String = ""
)
) {
companion object {
/** Predefined key data for [KeyCode.ARROW_DOWN] */
val ARROW_DOWN = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_DOWN,
label = "arrow_down"
)
/** Predefined key data for [KeyCode.ARROW_LEFT] */
val ARROW_LEFT = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_LEFT,
label = "arrow_left"
)
/** Predefined key data for [KeyCode.ARROW_RIGHT] */
val ARROW_RIGHT = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_RIGHT,
label = "arrow_right"
)
/** Predefined key data for [KeyCode.ARROW_UP] */
val ARROW_UP = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_UP,
label = "arrow_up"
)
/** Predefined key data for [KeyCode.CLIPBOARD_COPY] */
val CLIPBOARD_COPY = KeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_COPY,
label = "clipboard_copy"
)
/** Predefined key data for [KeyCode.CLIPBOARD_CUT] */
val CLIPBOARD_CUT = KeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_CUT,
label = "clipboard_cut"
)
/** Predefined key data for [KeyCode.CLIPBOARD_PASTE] */
val CLIPBOARD_PASTE = KeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_PASTE,
label = "clipboard_paste"
)
/** Predefined key data for [KeyCode.CLIPBOARD_SELECT] */
val CLIPBOARD_SELECT = KeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_SELECT,
label = "clipboard_select"
)
/** Predefined key data for [KeyCode.CLIPBOARD_SELECT_ALL] */
val CLIPBOARD_SELECT_ALL = KeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_SELECT_ALL,
label = "clipboard_select_all"
)
/** Predefined key data for [KeyCode.DELETE] */
val DELETE = KeyData(
type = KeyType.ENTER_EDITING,
code = KeyCode.DELETE,
label = "delete"
)
/** Predefined key data for [KeyCode.DELETE_WORD] */
val DELETE_WORD = KeyData(
type = KeyType.ENTER_EDITING,
code = KeyCode.DELETE_WORD,
label = "delete_word"
)
/** Predefined key data for [KeyCode.INTERNAL_BATCH_EDIT] */
val INTERNAL_BATCH_EDIT = KeyData(
type = KeyType.FUNCTION,
code = KeyCode.INTERNAL_BATCH_EDIT,
label = "internal_batch_edit"
)
/** Predefined key data for [KeyCode.MOVE_START_OF_LINE] */
val MOVE_START_OF_LINE = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_START_OF_LINE,
label = "move_start_of_line"
)
/** Predefined key data for [KeyCode.MOVE_END_OF_LINE] */
val MOVE_END_OF_LINE = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_END_OF_LINE,
label = "move_end_of_line"
)
/** Predefined key data for [KeyCode.MOVE_START_OF_PAGE] */
val MOVE_START_OF_PAGE = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_START_OF_PAGE,
label = "move_start_of_page"
)
/** Predefined key data for [KeyCode.MOVE_END_OF_PAGE] */
val MOVE_END_OF_PAGE = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_END_OF_PAGE,
label = "move_end_of_page"
)
/** Predefined key data for [KeyCode.SHOW_INPUT_METHOD_PICKER] */
val SHOW_INPUT_METHOD_PICKER = KeyData(
type = KeyType.FUNCTION,
code = KeyCode.SHOW_INPUT_METHOD_PICKER,
label = "show_input_method_picker"
)
/** Predefined key data for [KeyCode.SWITCH_TO_TEXT_CONTEXT] */
val SWITCH_TO_TEXT_CONTEXT = KeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.SWITCH_TO_TEXT_CONTEXT,
label = "switch_to_text_context"
)
/** Predefined key data for [KeyCode.SHIFT] */
val SHIFT = KeyData(
type = KeyType.MODIFIER,
code = KeyCode.SHIFT,
label = "shift"
)
/** Predefined key data for [KeyCode.SHIFT_LOCK] */
val SHIFT_LOCK = KeyData(
type = KeyType.MODIFIER,
code = KeyCode.SHIFT_LOCK,
label = "shift_lock"
)
/** Predefined key data for [KeyCode.SPACE] */
val SPACE = KeyData(
type = KeyType.CHARACTER,
code = KeyCode.SPACE,
label = "space"
)
/** Predefined key data for [KeyCode.UNSPECIFIED] */
val UNSPECIFIED = KeyData(
type = KeyType.UNSPECIFIED,
code = KeyCode.UNSPECIFIED,
label = "unspecified"
)
}
override fun toString(): String {
return "KeyData { type=$type code=$code label=\"$label\" }"
}
}
/**
* Data class which describes a single key and its attributes, while also providing additional
@@ -46,6 +207,10 @@ open class KeyData(
* or if the key should always be visible. Defaults to [KeyVariation.ALL].
* @property popup List of keys which will be accessible while long pressing the key. Defaults to
* an empty set (no extended popup).
* @property shift An alternative key to use when the keyboard caps state is true. Useful for layouts
* such as Colemak and Dvorak. Defaults to null (don't override base uppercase key). This override
* property should only be used to provide an uppercase variant of two else not related variants, but
* should not be used for providing an uppercase letter (e.g. 'a' -> 'A').
*/
class FlorisKeyData(
type: KeyType = KeyType.CHARACTER,
@@ -53,7 +218,8 @@ class FlorisKeyData(
label: String = "",
var groupId: Int = GROUP_DEFAULT,
var variation: KeyVariation = KeyVariation.ALL,
var popup: PopupSet<KeyData> = PopupSet()
var popup: PopupSet<KeyData> = PopupSet(),
var shift: KeyData? = null
) : KeyData(type, code, label) {
companion object {
/**

View File

@@ -16,7 +16,6 @@
package dev.patrickgold.florisboard.ime.text.key
import android.annotation.SuppressLint
import com.squareup.moshi.FromJson
import java.util.*

View File

@@ -16,8 +16,8 @@
package dev.patrickgold.florisboard.ime.text.key
import android.annotation.SuppressLint
import com.squareup.moshi.FromJson
import java.util.*
enum class KeyVariation {
ALL,
@@ -27,9 +27,8 @@ enum class KeyVariation {
URI;
companion object {
@SuppressLint("DefaultLocale")
fun fromString(string: String): KeyVariation {
return valueOf(string.toUpperCase())
return valueOf(string.toUpperCase(Locale.ENGLISH))
}
}
}

View File

@@ -18,7 +18,11 @@ package dev.patrickgold.florisboard.ime.text.key
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.graphics.*
import android.graphics.Canvas
import android.graphics.Outline
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.graphics.drawable.PaintDrawable
import android.os.Handler
@@ -32,7 +36,10 @@ import com.google.android.flexbox.FlexboxLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.ImeOptions
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.popup.PopupManager
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
@@ -40,8 +47,11 @@ import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.ime.theme.ThemeValue
import dev.patrickgold.florisboard.util.*
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import dev.patrickgold.florisboard.util.cancelAll
import dev.patrickgold.florisboard.util.postDelayed
import java.util.*
import kotlin.math.abs
/**
* View class for managing the rendering and the events of a single keyboard key.
@@ -54,17 +64,20 @@ import java.util.*
@SuppressLint("ViewConstructor")
class KeyView(
private val keyboardView: KeyboardView,
val data: FlorisKeyData
val data: FlorisKeyData,
private val florisboard: FlorisBoard?
) : View(keyboardView.context), SwipeGesture.Listener, ThemeManager.OnThemeUpdatedListener {
private var isKeyPressed: Boolean = false
set(value) {
field = value
updateKeyPressedBackground()
}
private var initSelectionStart: Int = 0
private var initSelectionEnd: Int = 0
private var hasTriggeredGestureMove: Boolean = false
private var keyHintMode: KeyHintMode = KeyHintMode.DISABLED
private val longKeyPressHandler: Handler = Handler(context.mainLooper)
private val repeatedKeyPressHandler: Handler = Handler(context.mainLooper)
val popupManager = PopupManager<KeyboardView, KeyView>(keyboardView, florisboard?.popupLayerView)
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private var shouldBlockNextKeyCode: Boolean = false
@@ -95,24 +108,34 @@ class KeyView(
isFakeBoldText = false
textAlign = Paint.Align.CENTER
textSize = resources.getDimension(R.dimen.key_textHintSize)
typeface = Typeface.DEFAULT
typeface = Typeface.MONOSPACE
}
private val tempRect: Rect = Rect()
private var themeValueCache: ThemeValueCache = ThemeValueCache()
var florisboard: FlorisBoard? = null
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
val swipeGestureDetector = SwipeGesture.Detector(context, this)
var touchHitBox: Rect = Rect(-1, -1, -1, -1)
init {
layoutParams = FlexboxLayout.LayoutParams(
FlexboxLayout.LayoutParams.WRAP_CONTENT, FlexboxLayout.LayoutParams.WRAP_CONTENT
).apply {
val keyMarginH: Int
val keyMarginV: Int
if (keyboardView.isSmartbarKeyboardView) {
keyMarginH = resources.getDimension(R.dimen.key_marginH).toInt()
keyMarginV = resources.getDimension(R.dimen.key_marginV).toInt()
} else {
keyMarginV = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingVertical, context).toInt()
keyMarginH = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
}
setMargins(
resources.getDimension((R.dimen.key_marginH)).toInt(),
resources.getDimension(R.dimen.key_marginV).toInt(),
resources.getDimension((R.dimen.key_marginH)).toInt(),
resources.getDimension(R.dimen.key_marginV).toInt()
keyMarginH,
keyMarginV,
keyMarginH,
keyMarginV
)
flexShrink = when (keyboardView.computedLayout?.mode) {
KeyboardMode.NUMERIC,
@@ -146,13 +169,13 @@ class KeyView(
setPadding(0, 0, 0, 0)
background = backgroundDrawable
elevation = if(themeValueCache.shouldShowBorder) 4.0f else 0.0f
elevation = if (themeValueCache.shouldShowBorder) 4.0f else 0.0f
if (prefs.keyboard.hintedNumberRowMode != KeyHintMode.DISABLED && data.popup.hint?.type == KeyType.NUMERIC) {
keyHintMode = prefs.keyboard.hintedNumberRowMode
}
if (prefs.keyboard.hintedSymbolsMode != KeyHintMode.DISABLED && data.popup.hint?.type == KeyType.CHARACTER) {
keyHintMode = prefs.keyboard.hintedNumberRowMode
keyHintMode = prefs.keyboard.hintedSymbolsMode
}
updateKeyPressedBackground()
@@ -162,20 +185,34 @@ class KeyView(
* Creates a label text from the given [keyData].
*
* @param keyData Optional. The key data to generate the label from. Defaults to [data].
* @return The generated label.
* @param caps If the generated text should be uppercase (true) or in lowercase (false).
* Defaults to FlorisBoard's TextInputManager's caps state or false. Ignored when the passed
* [keyData] is a TLD, in which case always the lower case variant is returned.
* @param subtype The subtype for which this label should be created. Defaults to
* [Subtype.DEFAULT]. Ignored when the passed [keyData] is a TLD.
* @return The generated label ready for usage in the front-end UI.
*/
fun getComputedLetter(keyData: KeyData = data): String {
if (keyData.code == KeyCode.URI_COMPONENT_TLD) {
return when (florisboard?.textInputManager?.caps) {
true -> keyData.label.toUpperCase(Locale.getDefault())
else -> keyData.label.toLowerCase(Locale.getDefault())
fun getComputedLetter(
keyData: KeyData = data,
caps: Boolean = florisboard?.textInputManager?.caps ?: false,
subtype: Subtype = florisboard?.activeSubtype ?: Subtype.DEFAULT
): String {
return if (caps && keyData is FlorisKeyData && keyData.shift != null) {
(keyData.shift!!.code.toChar()).toString()
} else {
when (data.code) {
KeyCode.URI_COMPONENT_TLD -> keyData.label.toLowerCase(Locale.ENGLISH)
else -> {
val labelText = (keyData.code.toChar()).toString()
val locale = if (subtype.locale.language == "el") { Locale.getDefault() } else { subtype.locale }
if (caps) {
labelText.toUpperCase(locale)
} else {
labelText
}
}
}
}
val label = (keyData.code.toChar()).toString()
return when {
florisboard?.textInputManager?.caps ?: false -> label.toUpperCase(Locale.getDefault())
else -> label
}
}
/**
@@ -204,69 +241,80 @@ class KeyView(
*/
fun onFlorisTouchEvent(event: MotionEvent?): Boolean {
if (event == null || !isEnabled) return false
if (swipeGestureDetector.onTouchEvent(event)) {
val florisboard = florisboard ?: return false
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
val alwaysTriggerOnMove = (hasTriggeredGestureMove
&& (data.code == KeyCode.DELETE && prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_CHARACTERS_PRECISELY
|| data.code == KeyCode.SPACE))
if (swipeGestureDetector.onTouchEvent(event, alwaysTriggerOnMove)) {
isKeyPressed = false
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
}
longKeyPressHandler.cancelAll()
repeatedKeyPressHandler.cancelAll()
keyboardView.popupManager.hide()
popupManager.hide()
return true
}
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (data.code == KeyCode.SHIFT) {
isKeyPressed = true
florisboard?.keyPressVibrate()
florisboard?.keyPressSound(data)
florisboard?.textInputManager?.sendKeyPress(data)
} else {
val delayMillis = prefs.keyboard.longPressDelay.toLong()
hasTriggeredGestureMove = false
shouldBlockNextKeyCode = false
florisboard?.prefs?.keyboard?.let {
if (it.popupEnabled){
keyboardView.popupManager.show(this, keyHintMode)
}
}
isKeyPressed = true
florisboard?.keyPressVibrate()
florisboard?.keyPressSound(data)
when (data.code) {
KeyCode.ARROW_DOWN,
KeyCode.ARROW_LEFT,
KeyCode.ARROW_RIGHT,
KeyCode.ARROW_UP,
KeyCode.DELETE -> {
repeatedKeyPressHandler.postAtScheduledRate(delayMillis, 25) {
if (isKeyPressed) {
florisboard?.textInputManager?.sendKeyPress(data)
} else {
repeatedKeyPressHandler.cancelAll()
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.down(data))
isKeyPressed = true
val delayMillis = prefs.keyboard.longPressDelay.toLong()
hasTriggeredGestureMove = false
shouldBlockNextKeyCode = false
if (florisboard.prefs.keyboard.popupEnabled) {
popupManager.show(this, keyHintMode)
}
isKeyPressed = true
florisboard.keyPressVibrate()
florisboard.keyPressSound(data)
when (data.code) {
KeyCode.SPACE -> {
initSelectionStart = florisboard.activeEditorInstance.selection.start
initSelectionEnd = florisboard.activeEditorInstance.selection.end
longKeyPressHandler.postDelayed((delayMillis * 2.5f).toLong()) {
when (prefs.gestures.spaceBarLongPress) {
SwipeAction.NO_ACTION,
SwipeAction.INSERT_SPACE -> {
}
else -> {
this.florisboard.executeSwipeAction(prefs.gestures.spaceBarLongPress)
shouldBlockNextKeyCode = true
}
}
}
}
longKeyPressHandler.postDelayed(delayMillis) {
if (data.popup.isNotEmpty()) {
keyboardView.popupManager.extend(this, keyHintMode)
KeyCode.SHIFT -> {
longKeyPressHandler.postDelayed((delayMillis * 2.5).toLong()) {
this.florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.SHIFT_LOCK))
}
if (data.code == KeyCode.SPACE) {
florisboard?.textInputManager?.sendKeyPress(
KeyData(
type = KeyType.FUNCTION,
code = KeyCode.SHOW_INPUT_METHOD_PICKER,
)
)
}
KeyCode.LANGUAGE_SWITCH -> {
longKeyPressHandler.postDelayed((delayMillis * 2.0).toLong()) {
shouldBlockNextKeyCode = true
this.florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.SHOW_INPUT_METHOD_PICKER))
}
}
else -> {
longKeyPressHandler.postDelayed(delayMillis) {
if (data.popup.isNotEmpty()) {
popupManager.extend(this@KeyView, keyHintMode)
}
}
}
}
}
MotionEvent.ACTION_MOVE -> {
if (keyboardView.popupManager.isShowingExtendedPopup) {
if (popupManager.isShowingExtendedPopup) {
val isPointerWithinBounds =
keyboardView.popupManager.propagateMotionEvent(this, event)
popupManager.propagateMotionEvent(this, event)
if (!isPointerWithinBounds && !shouldBlockNextKeyCode) {
keyboardView.dismissActiveKeyViewReference()
keyboardView.dismissActiveKeyViewReference(pointerId)
}
} else {
val parent = parent as ViewGroup
@@ -276,32 +324,50 @@ class KeyView(
|| event.y > 1.35f * measuredHeight
) {
if (!shouldBlockNextKeyCode) {
keyboardView.dismissActiveKeyViewReference()
keyboardView.dismissActiveKeyViewReference(pointerId)
}
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
MotionEvent.ACTION_UP -> {
longKeyPressHandler.cancelAll()
repeatedKeyPressHandler.cancelAll()
if (data.code != KeyCode.SHIFT) {
if (hasTriggeredGestureMove && data.code == KeyCode.DELETE) {
hasTriggeredGestureMove = false
florisboard?.activeEditorInstance?.apply {
if (selection.isSelectionMode) {
deleteBackwards()
if (hasTriggeredGestureMove && data.code == KeyCode.DELETE) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
florisboard.activeEditorInstance.apply {
if (selection.isSelectionMode) {
deleteBackwards()
}
}
} else {
val retData = popupManager.getActiveKeyData(this)
if (!shouldBlockNextKeyCode && retData != null) {
if (retData == data) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.up(data))
} else {
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
}
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(retData))
}
} else {
val retData = keyboardView.popupManager.getActiveKeyData(this)
if (event.actionMasked != MotionEvent.ACTION_CANCEL && !shouldBlockNextKeyCode && retData != null) {
florisboard?.textInputManager?.sendKeyPress(retData)
} else {
shouldBlockNextKeyCode = false
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
}
keyboardView.popupManager.hide()
}
popupManager.hide()
}
shouldBlockNextKeyCode = false
hasTriggeredGestureMove = false
isKeyPressed = false
}
MotionEvent.ACTION_CANCEL -> {
longKeyPressHandler.cancelAll()
if (data.code != KeyCode.SHIFT) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
}
popupManager.hide()
shouldBlockNextKeyCode = false
hasTriggeredGestureMove = false
isKeyPressed = false
}
else -> return false
@@ -313,47 +379,33 @@ class KeyView(
* Swipe event handler. Listens to touch_move left/right swipes and triggers the swipe action
* defined in the prefs.
*/
override fun onSwipe(direction: SwipeGesture.Direction, type: SwipeGesture.Type): Boolean {
override fun onSwipe(event: SwipeGesture.Event): Boolean {
val florisboard = florisboard ?: return false
return when (data.code) {
KeyCode.DELETE -> when (type) {
SwipeGesture.Type.TOUCH_MOVE -> when (direction) {
SwipeGesture.Direction.LEFT -> when (prefs.gestures.deleteKeySwipeLeft) {
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
florisboard?.activeEditorInstance?.apply {
setSelection(
if (selection.start > 0) { selection.start - 1 } else { selection.start },
selection.end
)
}
hasTriggeredGestureMove = true
shouldBlockNextKeyCode = true
true
KeyCode.DELETE -> when (event.type) {
SwipeGesture.Type.TOUCH_MOVE -> when (prefs.gestures.deleteKeySwipeLeft) {
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
florisboard.activeEditorInstance.apply {
selection.updateAndNotify(
(selection.end + event.absUnitCountX + 1).coerceIn(0, selection.end),
selection.end
)
}
SwipeAction.DELETE_WORDS_PRECISELY -> {
florisboard?.activeEditorInstance?.apply {
hasTriggeredGestureMove = true
shouldBlockNextKeyCode = true
true
}
SwipeAction.DELETE_WORDS_PRECISELY -> when (event.direction) {
SwipeGesture.Direction.LEFT -> {
florisboard.activeEditorInstance.apply {
leftAppendWordToSelection()
}
hasTriggeredGestureMove = true
shouldBlockNextKeyCode = true
true
}
else -> false
}
SwipeGesture.Direction.RIGHT -> when (prefs.gestures.deleteKeySwipeLeft) {
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
florisboard?.activeEditorInstance?.apply {
setSelection(
if (selection.start < selection.end) { selection.start + 1 } else { selection.start },
selection.end
)
}
shouldBlockNextKeyCode = true
true
}
SwipeAction.DELETE_WORDS_PRECISELY -> {
florisboard?.activeEditorInstance?.apply {
SwipeGesture.Direction.RIGHT -> {
florisboard.activeEditorInstance.apply {
leftPopWordFromSelection()
}
shouldBlockNextKeyCode = true
@@ -365,20 +417,45 @@ class KeyView(
}
else -> false
}
KeyCode.SPACE -> when (type) {
SwipeGesture.Type.TOUCH_MOVE -> when (direction) {
KeyCode.SPACE -> when (event.type) {
SwipeGesture.Type.TOUCH_MOVE -> when (event.direction) {
SwipeGesture.Direction.UP -> {
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeUp)
shouldBlockNextKeyCode = true
true
if (event.absUnitCountY.times(-1) >= 6) {
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeUp)
hasTriggeredGestureMove = true
shouldBlockNextKeyCode = true
true
} else {
false
}
}
SwipeGesture.Direction.LEFT -> {
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
if (prefs.gestures.spaceBarSwipeLeft == SwipeAction.MOVE_CURSOR_LEFT) {
abs(event.relUnitCountX).let {
val count = if (!hasTriggeredGestureMove) { it - 1 } else { it }
if (count > 0) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.ARROW_LEFT, count))
}
}
} else {
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
}
hasTriggeredGestureMove = true
shouldBlockNextKeyCode = true
true
}
SwipeGesture.Direction.RIGHT -> {
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
if (prefs.gestures.spaceBarSwipeRight == SwipeAction.MOVE_CURSOR_RIGHT) {
abs(event.relUnitCountX).let {
val count = if (!hasTriggeredGestureMove) { it - 1 } else { it }
if (count > 0) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.ARROW_RIGHT, count))
}
}
} else {
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
}
hasTriggeredGestureMove = true
shouldBlockNextKeyCode = true
true
}
@@ -397,25 +474,49 @@ class KeyView(
* by Devunwired
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
desiredWidth = when (keyboardView.computedLayout?.mode) {
val keyMarginH: Int
val keyMarginV: Int
if (keyboardView.isSmartbarKeyboardView) {
keyMarginH = resources.getDimension(R.dimen.key_marginH).toInt()
keyMarginV = resources.getDimension(R.dimen.key_marginV).toInt()
} else {
keyMarginV = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingVertical, context).toInt()
keyMarginH = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
}
(layoutParams as ViewGroup.MarginLayoutParams).setMargins(
keyMarginH,
keyMarginV,
keyMarginH,
keyMarginV
)
desiredWidth = (keyboardView.desiredKeyWidth * when (keyboardView.computedLayout?.mode) {
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> (keyboardView.desiredKeyWidth * 2.68f).toInt()
KeyboardMode.PHONE2 -> 2.68f
KeyboardMode.NUMERIC_ADVANCED -> when (data.code) {
44, 46 -> keyboardView.desiredKeyWidth
KeyCode.VIEW_SYMBOLS, 61 -> (keyboardView.desiredKeyWidth * 1.34f).toInt()
else -> (keyboardView.desiredKeyWidth * 1.56f).toInt()
44, 46 -> 1.00f
KeyCode.VIEW_SYMBOLS, 61 -> 1.34f
else -> 1.56f
}
else -> when (data.code) {
KeyCode.SHIFT,
KeyCode.DELETE ->
if ((keyboardView.computedLayout?.arrangement?.get(2)?.size ?: 0) > 10) {
1.12f
} else {
1.56f
}
KeyCode.VIEW_CHARACTERS,
KeyCode.VIEW_SYMBOLS,
KeyCode.VIEW_SYMBOLS2,
KeyCode.DELETE,
KeyCode.ENTER -> (keyboardView.desiredKeyWidth * 1.56f).toInt()
else -> keyboardView.desiredKeyWidth
KeyCode.ENTER -> 1.56f
else -> 1.00f
}
}
}).toInt()
desiredHeight = keyboardView.desiredKeyHeight
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
@@ -478,10 +579,9 @@ class KeyView(
private fun updateEnabledState() {
isEnabled = when (data.code) {
KeyCode.CLIPBOARD_COPY,
KeyCode.CLIPBOARD_CUT -> {
florisboard?.activeEditorInstance?.selection?.isSelectionMode == true &&
florisboard?.activeEditorInstance?.isRawInputEditor == false
}
KeyCode.CLIPBOARD_CUT -> (florisboard != null
&& florisboard.activeEditorInstance.selection.isSelectionMode
&& !florisboard.activeEditorInstance.isRawInputEditor)
KeyCode.CLIPBOARD_PASTE -> florisboard?.clipboardManager?.hasPrimaryClip() == true
KeyCode.CLIPBOARD_SELECT_ALL -> {
florisboard?.activeEditorInstance?.isRawInputEditor == false
@@ -494,35 +594,53 @@ class KeyView(
}
override fun onThemeUpdated(theme: Theme) {
if (keyboardView.isSmartbarKeyboardView) {
themeValueCache.apply {
keyBackground = theme.getAttr(Theme.Attr.SMARTBAR_BACKGROUND)
keyBackgroundPressed = theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_BACKGROUND)
keyForeground = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND)
keyForegroundAlt = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND_ALT)
keyForegroundPressed = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND)
shouldShowBorder = false
}
} else {
val label = data.label
val capsSpecific = when {
florisboard?.textInputManager?.capsLock == true -> {
"capslock"
}
florisboard?.textInputManager?.caps == true -> {
"caps"
}
else -> {
null
when {
keyboardView.isLoadingPlaceholderKeyboard -> {
val label = data.label
themeValueCache.apply {
shouldShowBorder = theme.getAttr(Theme.Attr.KEY_SHOW_BORDER, label).toOnOff().state
keyBackground = if (shouldShowBorder) {
theme.getAttr(Theme.Attr.KEY_BACKGROUND, label)
} else {
theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_BACKGROUND, label)
}
keyBackgroundPressed = theme.getAttr(Theme.Attr.KEY_BACKGROUND_PRESSED, label)
keyForeground = keyBackground
keyForegroundAlt = ThemeValue.SolidColor(0)
keyForegroundPressed = keyBackgroundPressed
}
}
themeValueCache.apply {
keyBackground = theme.getAttr(Theme.Attr.KEY_BACKGROUND, label, capsSpecific)
keyBackgroundPressed = theme.getAttr(Theme.Attr.KEY_BACKGROUND_PRESSED, label, capsSpecific)
keyForeground = theme.getAttr(Theme.Attr.KEY_FOREGROUND, label, capsSpecific)
keyForegroundAlt = ThemeValue.SolidColor(0)
keyForegroundPressed = theme.getAttr(Theme.Attr.KEY_FOREGROUND_PRESSED, label, capsSpecific)
shouldShowBorder = theme.getAttr(Theme.Attr.KEY_SHOW_BORDER, label, capsSpecific).toOnOff().state
keyboardView.isSmartbarKeyboardView -> {
themeValueCache.apply {
keyBackground = theme.getAttr(Theme.Attr.SMARTBAR_BACKGROUND)
keyBackgroundPressed = theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_BACKGROUND)
keyForeground = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND)
keyForegroundAlt = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND_ALT)
keyForegroundPressed = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND)
shouldShowBorder = false
}
}
else -> {
val label = data.label
val capsSpecific = when {
florisboard?.textInputManager?.capsLock == true -> {
"capslock"
}
florisboard?.textInputManager?.caps == true -> {
"caps"
}
else -> {
null
}
}
themeValueCache.apply {
keyBackground = theme.getAttr(Theme.Attr.KEY_BACKGROUND, label, capsSpecific)
keyBackgroundPressed = theme.getAttr(Theme.Attr.KEY_BACKGROUND_PRESSED, label, capsSpecific)
keyForeground = theme.getAttr(Theme.Attr.KEY_FOREGROUND, label, capsSpecific)
keyForegroundAlt = ThemeValue.SolidColor(0)
keyForegroundPressed = theme.getAttr(Theme.Attr.KEY_FOREGROUND_PRESSED, label, capsSpecific)
shouldShowBorder = theme.getAttr(Theme.Attr.KEY_SHOW_BORDER, label, capsSpecific).toOnOff().state
}
}
}
updateKeyPressedBackground()
@@ -549,8 +667,17 @@ class KeyView(
touchHitBox.set(-1, -1, -1, -1)
} else {
val parent = parent as ViewGroup
val keyMarginH = resources.getDimension((R.dimen.key_marginH)).toInt()
val keyMarginV = resources.getDimension((R.dimen.key_marginV)).toInt()
val keyMarginH: Int
val keyMarginV: Int
if (keyboardView.isSmartbarKeyboardView) {
keyMarginH = resources.getDimension(R.dimen.key_marginH).toInt()
keyMarginV = resources.getDimension(R.dimen.key_marginV).toInt()
} else {
keyMarginV = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingVertical, context).toInt()
keyMarginH = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
}
touchHitBox.apply {
left = when (this@KeyView) {
@@ -576,22 +703,48 @@ class KeyView(
when (data.code) {
KeyCode.SWITCH_TO_TEXT_CONTEXT,
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
visibility = if (florisboard?.shouldShowLanguageSwitch() == true) {
GONE
} else {
VISIBLE
val tempUtilityKeyAction = when {
prefs.keyboard.utilityKeyEnabled -> prefs.keyboard.utilityKeyAction
else -> UtilityKeyAction.DISABLED
}
visibility = when (tempUtilityKeyAction) {
UtilityKeyAction.DISABLED,
UtilityKeyAction.SWITCH_LANGUAGE,
UtilityKeyAction.SWITCH_KEYBOARD_APP -> GONE
UtilityKeyAction.SWITCH_TO_EMOJIS -> VISIBLE
UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS ->
if (florisboard?.shouldShowLanguageSwitch() == true) {
GONE
} else {
VISIBLE
}
}
}
KeyCode.LANGUAGE_SWITCH -> {
visibility = if (florisboard?.shouldShowLanguageSwitch() == true) {
VISIBLE
} else {
GONE
val tempUtilityKeyAction = when {
prefs.keyboard.utilityKeyEnabled -> prefs.keyboard.utilityKeyAction
else -> UtilityKeyAction.DISABLED
}
visibility = when (tempUtilityKeyAction) {
UtilityKeyAction.DISABLED,
UtilityKeyAction.SWITCH_TO_EMOJIS -> GONE
UtilityKeyAction.SWITCH_LANGUAGE,
UtilityKeyAction.SWITCH_KEYBOARD_APP -> VISIBLE
UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS ->
if (florisboard?.shouldShowLanguageSwitch() == true) {
VISIBLE
} else {
GONE
}
}
}
else -> if (data.variation != KeyVariation.ALL) {
val keyVariation = florisboard?.textInputManager?.keyVariation ?: KeyVariation.NORMAL
visibility = if (data.variation == keyVariation) { VISIBLE } else { GONE }
visibility = if (data.variation == keyVariation) {
VISIBLE
} else {
GONE
}
updateTouchHitBox()
}
}
@@ -631,13 +784,9 @@ class KeyView(
}
/**
* Draw the key label / drawable.
* Computes the labels and drawables needed to draw the key.
*/
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas ?: return
private fun computeLabelsAndDrawables() {
if (data.type == KeyType.CHARACTER && data.code != KeyCode.SPACE
&& data.code != KeyCode.HALF_SPACE && data.code != KeyCode.KESHIDA || data.type == KeyType.NUMERIC
) {
@@ -711,7 +860,8 @@ class KeyView(
KeyboardMode.CHARACTERS -> {
label = florisboard?.activeSubtype?.locale?.displayName
}
else -> {}
else -> {
}
}
}
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
@@ -745,6 +895,17 @@ class KeyView(
}
}
}
}
/**
* Draw the key label / drawable.
*/
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas ?: return
computeLabelsAndDrawables()
// Draw drawable
val drawable = drawable
@@ -780,11 +941,11 @@ class KeyView(
}
else -> when {
(data.type == KeyType.CHARACTER || data.type == KeyType.NUMERIC) &&
data.code != KeyCode.SPACE -> {
data.code != KeyCode.SPACE -> {
val cachedTextSize = setTextSizeFor(
labelPaint,
desiredWidth - (2.6f * drawablePaddingH),
desiredHeight - (3.4f * drawablePaddingV),
measuredWidth - (2.6f * drawablePaddingH),
measuredHeight - (3.4f * drawablePaddingV),
// Note: taking a "X" here because it is one of the biggest letters and
// the keys must have the same base character for calculation, else
// they will all look different and weird...
@@ -799,7 +960,7 @@ class KeyView(
else -> 1.0f
}
)
keyboardView.popupManager.keyPopupTextSize = cachedTextSize
popupManager.keyPopupTextSize = cachedTextSize
}
else -> {
setTextSizeFor(
@@ -816,9 +977,17 @@ class KeyView(
}
}
}
labelPaint.color = themeValueCache.keyForeground.toSolidColor().color
labelPaint.color = if (isKeyPressed && isEnabled) {
themeValueCache.keyForegroundPressed.toSolidColor().color
} else {
themeValueCache.keyForeground.toSolidColor().color
}
labelPaint.alpha = if (keyboardView.computedLayout?.mode == KeyboardMode.CHARACTERS &&
data.code == KeyCode.SPACE) { 120 } else { 255 }
data.code == KeyCode.SPACE) {
120
} else {
255
}
val centerX = measuredWidth / 2.0f
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2
if (label.contains("\n")) {
@@ -836,15 +1005,15 @@ class KeyView(
if (hintedLabel != null) {
setTextSizeFor(
hintedLabelPaint,
desiredWidth * 1.0f / 6.0f,
desiredHeight * 1.0f / 6.0f,
desiredWidth * 1.0f / 5.0f,
desiredHeight * 1.0f / 5.0f,
// Note: taking a "X" here because it is one of the biggest letters and
// the keys must have the same base character for calculation, else
// they will all look different and weird...
"X"
)
hintedLabelPaint.color = labelPaint.color
hintedLabelPaint.alpha = 120
hintedLabelPaint.alpha = 170
val centerX = measuredWidth * 5.0f / 6.0f
val centerY = measuredHeight * 1.0f / 6.0f + (hintedLabelPaint.textSize - hintedLabelPaint.descent()) / 2
canvas.drawText(hintedLabel, centerX, centerY, hintedLabelPaint)

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2020 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.text.key
import java.util.*
/**
* Enum for declaring the utility key actions.
*/
enum class UtilityKeyAction {
SWITCH_TO_EMOJIS,
SWITCH_LANGUAGE,
SWITCH_KEYBOARD_APP,
DYNAMIC_SWITCH_LANGUAGE_EMOJIS,
DISABLED;
companion object {
fun fromString(string: String): UtilityKeyAction {
return valueOf(string.toUpperCase(Locale.ENGLISH))
}
}
}

View File

@@ -23,17 +23,25 @@ import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexboxLayout
import com.google.android.flexbox.JustifyContent
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.util.ViewLayoutUtils
/**
* This class' sole purpose is to manage the layout within a row of [KeyboardView]. No logic is
* handled in this class.
*/
class KeyboardRowView(context: Context) : FlexboxLayout(context) {
class KeyboardRowView(context: Context, val keyboardView: KeyboardView) : FlexboxLayout(context) {
init {
val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
val keyMarginH = if (keyboardView.isSmartbarKeyboardView){
resources.getDimension(R.dimen.key_marginH).toInt()
}else{
ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
}
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
setMargins(
resources.getDimension(R.dimen.keyboard_row_marginH).toInt(), 0,
resources.getDimension(R.dimen.keyboard_row_marginH).toInt(), 0
keyMarginH, 0,
keyMarginH, 0
)
}
flexDirection = FlexDirection.ROW
@@ -49,4 +57,19 @@ class KeyboardRowView(context: Context) : FlexboxLayout(context) {
override fun onTouchEvent(event: MotionEvent?): Boolean {
return false
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
val keyMarginH = if (keyboardView.isSmartbarKeyboardView){
resources.getDimension(R.dimen.key_marginH).toInt()
}else{
ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
}
(layoutParams as MarginLayoutParams).setMargins(
keyMarginH, 0,
keyMarginH, 0
)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}

View File

@@ -27,15 +27,17 @@ import androidx.core.view.children
import com.google.android.flexbox.FlexboxLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.popup.PopupManager
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.KeyView
import dev.patrickgold.florisboard.ime.text.layout.ComputedLayoutData
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyView
import dev.patrickgold.florisboard.ime.text.layout.ComputedLayoutData
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import kotlin.math.roundToInt
/**
@@ -48,10 +50,8 @@ import kotlin.math.roundToInt
*/
class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Listener,
ThemeManager.OnThemeUpdatedListener {
private var activeKeyView: KeyView? = null
private var activePointerId: Int? = null
private var activeX: Float = 0.0f
private var activeY: Float = 0.0f
private var activeKeyViews: MutableMap<Int, KeyView> = mutableMapOf()
private var initialKeyCodes: MutableMap<Int, Int> = mutableMapOf()
var computedLayout: ComputedLayoutData? = null
set(v) {
@@ -61,10 +61,9 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
var desiredKeyWidth: Int = resources.getDimension(R.dimen.key_width).toInt()
var desiredKeyHeight: Int = resources.getDimension(R.dimen.key_height).toInt()
var florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private var initialKeyCode: Int = 0
private val isPreviewMode: Boolean
val isSmartbarKeyboardView: Boolean
var popupManager = PopupManager<KeyboardView, KeyView>(this, florisboard?.popupLayerView)
val isLoadingPlaceholderKeyboard: Boolean
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val themeManager: ThemeManager = ThemeManager.default()
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
@@ -75,6 +74,7 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
context.obtainStyledAttributes(attrs, R.styleable.KeyboardView).apply {
isPreviewMode = getBoolean(R.styleable.KeyboardView_isPreviewKeyboard, false)
isSmartbarKeyboardView = getBoolean(R.styleable.KeyboardView_isSmartbarKeyboard, false)
isLoadingPlaceholderKeyboard = getBoolean(R.styleable.KeyboardView_isLoadingPlaceholderKeyboard, false)
recycle()
}
orientation = VERTICAL
@@ -82,8 +82,10 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
)
florisboard?.addEventListener(this)
onWindowShown()
if (isLoadingPlaceholderKeyboard) {
computedLayout = ComputedLayoutData.PRE_GENERATED_LOADING_KEYBOARD
}
}
/**
@@ -93,16 +95,16 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
destroyLayout()
val computedLayout = computedLayout ?: return
for (row in computedLayout.arrangement) {
val rowView = KeyboardRowView(context)
val rowView = KeyboardRowView(context, this)
for (key in row) {
val keyView = KeyView(this, key)
keyView.florisboard = florisboard
val keyView = KeyView(this, key, florisboard)
rowView.addView(keyView)
}
addView(rowView)
}
if (!isPreviewMode) {
themeManager.requestThemeUpdate(this)
onWindowShown()
} else {
updateVisibility()
}
@@ -117,6 +119,7 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
override fun onAttachedToWindow() {
super.onAttachedToWindow()
florisboard?.addEventListener(this)
if (!isPreviewMode) {
themeManager.registerOnThemeUpdatedListener(this)
}
@@ -126,11 +129,11 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
* Dismisses all shown key popups when keyboard is detached from window.
*/
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
popupManager.dismissAllPopups()
if (!isPreviewMode) {
themeManager.unregisterOnThemeUpdatedListener(this)
}
florisboard?.removeEventListener(this)
super.onDetachedFromWindow()
}
override fun onWindowShown() {
@@ -138,6 +141,18 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
distanceThreshold = prefs.gestures.swipeDistanceThreshold
velocityThreshold = prefs.gestures.swipeVelocityThreshold
}
for (row in children) {
if (row is ViewGroup) {
for (keyView in row.children) {
if (keyView is KeyView) {
keyView.swipeGestureDetector.apply {
distanceThreshold = prefs.gestures.swipeDistanceThreshold
velocityThreshold = prefs.gestures.swipeVelocityThreshold
}
}
}
}
}
}
/**
@@ -152,102 +167,103 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
*/
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
event ?: return false
if (isPreviewMode) {
return false
}
val eventFloris = MotionEvent.obtainNoHistory(event)
if (event == null || isPreviewMode || isLoadingPlaceholderKeyboard) return false
if (!isSmartbarKeyboardView && swipeGestureDetector.onTouchEvent(event)) {
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_CANCEL)
activeKeyView = null
activePointerId = null
for (pointerIndex in 0 until event.pointerCount) {
val pointerId = event.getPointerId(pointerIndex)
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_CANCEL)
activeKeyViews.remove(pointerId)
}
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(KeyData.INTERNAL_BATCH_EDIT))
}
return true
}
val pointerIndex = event.actionIndex
var pointerId = event.getPointerId(pointerIndex)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_DOWN -> {
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.down(KeyData.INTERNAL_BATCH_EDIT))
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
searchForActiveKeyView(event, pointerIndex, pointerId)
initialKeyCodes[pointerId] = activeKeyViews[pointerId]?.data?.code ?: 0
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
}
MotionEvent.ACTION_POINTER_DOWN -> {
if (activePointerId == null) {
activePointerId = pointerId
activeX = event.getX(pointerIndex)
activeY = event.getY(pointerIndex)
searchForActiveKeyView()
initialKeyCode = activeKeyView?.data?.code ?: 0
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
} else if (activePointerId != pointerId) {
// New pointer arrived. Send ACTION_UP to current active view and move on
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_UP)
activePointerId = pointerId
activeX = event.getX(pointerIndex)
activeY = event.getY(pointerIndex)
searchForActiveKeyView()
initialKeyCode = activeKeyView?.data?.code ?: 0
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
}
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
searchForActiveKeyView(event, pointerIndex, pointerId)
initialKeyCodes[pointerId] = activeKeyViews[pointerId]?.data?.code ?: 0
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
}
MotionEvent.ACTION_MOVE -> {
for (index in 0 until event.pointerCount) {
pointerId = event.getPointerId(index)
if (activePointerId == pointerId) {
activeX = event.getX(index)
activeY = event.getY(index)
if (activeKeyView == null) {
searchForActiveKeyView()
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
} else {
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_MOVE)
}
for (pointerIndex in 0 until event.pointerCount) {
val pointerId = event.getPointerId(pointerIndex)
if (!activeKeyViews.containsKey(pointerId)) {
searchForActiveKeyView(event, pointerIndex, pointerId)
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
} else {
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_MOVE)
}
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_POINTER_UP -> {
if (activePointerId == pointerId) {
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_UP)
activeKeyView = null
activePointerId = null
}
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
sendFlorisTouchEvent(event, pointerIndex, pointerId, event.actionMasked)
activeKeyViews.remove(pointerId)
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
sendFlorisTouchEvent(event, pointerIndex, pointerId, event.actionMasked)
activeKeyViews.remove(pointerId)
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(KeyData.INTERNAL_BATCH_EDIT))
}
else -> return false
}
eventFloris.recycle()
return true
}
/**
* Sends a touch [event] to [activeKeyView] with action set to [actionParam]. Normalizes passed
* actions (ACTION_POINTER_* will be converted to ACTION_*). Translates the absolute coords of
* a passed [event] to relative ones so the [activeKeyView] can work with it.
* Sends a touch [event] to the active key view which is associated with given [pointerId]. The action of the
* event is set to [actionParam]. Normalizes passed actions (ACTION_POINTER_* will be converted to ACTION_*).
* Translates the absolute coords of a passed [event] to relative ones so the active key view can work with it.
*
* @param event The event to pass to [activeKeyView].
* @param event The event to pass to the active key view.
* @param pointerIndex The index of the pointer, used for getting coordinates.
* @param pointerId The unique ID of the pointer, used to reference the active key view.
* @param actionParam The action to set the [event] to.
*/
private fun sendFlorisTouchEvent(event: MotionEvent, actionParam: Int) {
val keyView = activeKeyView ?: return
val keyViewParent = keyView.parent as ViewGroup
keyView.onFlorisTouchEvent(event.apply {
action = when (actionParam) {
private fun sendFlorisTouchEvent(event: MotionEvent, pointerIndex: Int, pointerId: Int, actionParam: Int) {
val keyView = activeKeyViews[pointerId] ?: return
val keyViewParent = keyView.parent as? ViewGroup ?: return
val eventToSend = MotionEvent.obtain(
event.downTime,
event.eventTime,
when (actionParam) {
MotionEvent.ACTION_POINTER_DOWN -> MotionEvent.ACTION_DOWN
MotionEvent.ACTION_POINTER_UP -> MotionEvent.ACTION_UP
else -> actionParam
}
setLocation(
activeX - keyViewParent.x - keyView.x,
activeY - keyViewParent.y - keyView.y
)
})
},
event.getX(pointerIndex) - keyViewParent.x - keyView.x,
event.getY(pointerIndex) - keyViewParent.y - keyView.y,
0
)
keyView.onFlorisTouchEvent(eventToSend)
eventToSend.recycle()
}
/**
* Swipe event handler. Listens to touch_up swipes and executes the swipe action defined for it
* in the prefs.
*/
override fun onSwipe(direction: SwipeGesture.Direction, type: SwipeGesture.Type): Boolean {
override fun onSwipe(event: SwipeGesture.Event): Boolean {
return when {
initialKeyCode == KeyCode.DELETE -> {
if (type == SwipeGesture.Type.TOUCH_UP && direction == SwipeGesture.Direction.LEFT &&
initialKeyCodes[event.pointerId] == KeyCode.DELETE -> {
if (event.type == SwipeGesture.Type.TOUCH_UP && event.direction == SwipeGesture.Direction.LEFT &&
prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_WORD) {
florisboard?.executeSwipeAction(prefs.gestures.deleteKeySwipeLeft)
true
@@ -255,10 +271,19 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
false
}
}
initialKeyCode > KeyCode.SPACE && !popupManager.isShowingExtendedPopup -> when {
!prefs.glide.enabled -> when (type) {
initialKeyCodes[event.pointerId] == KeyCode.SHIFT && activeKeyViews[event.pointerId]?.data?.code != KeyCode.SHIFT &&
event.type == SwipeGesture.Type.TOUCH_UP -> {
activeKeyViews[event.pointerId]?.let {
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(it.popupManager.getActiveKeyData(it) ?: it.data))
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.cancel(KeyData.SHIFT))
}
true
}
initialKeyCodes[event.pointerId] ?: 0 > KeyCode.SPACE &&
activeKeyViews[event.pointerId]?.popupManager?.isShowingExtendedPopup == false -> when {
!prefs.glide.enabled -> when (event.type) {
SwipeGesture.Type.TOUCH_UP -> {
val swipeAction = when (direction) {
val swipeAction = when (event.direction) {
SwipeGesture.Direction.UP -> prefs.gestures.swipeUp
SwipeGesture.Direction.DOWN -> prefs.gestures.swipeDown
SwipeGesture.Direction.LEFT -> prefs.gestures.swipeLeft
@@ -281,15 +306,17 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
}
/**
* Searches for an active key view at [activeX]/[activeY].
* Searches for an active key view at the passed pointer location.
*/
private fun searchForActiveKeyView() {
private fun searchForActiveKeyView(event: MotionEvent, pointerIndex: Int, pointerId: Int) {
val activeX = event.getX(pointerIndex)
val activeY = event.getY(pointerIndex)
loop@ for (row in children) {
if (row is FlexboxLayout) {
for (keyView in row.children) {
if (keyView is KeyView) {
if (keyView.touchHitBox.contains(activeX.toInt(), activeY.toInt())) {
activeKeyView = keyView
activeKeyViews[pointerId] = keyView
break@loop
}
}
@@ -299,22 +326,28 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
}
/**
* Invalidates the current [activeKeyView] and sends a [MotionEvent.ACTION_CANCEL] to indicate
* the loss of focus.
* Invalidates the current active key view and sends a [MotionEvent.ACTION_CANCEL] to indicate the loss of focus.
*/
fun dismissActiveKeyViewReference() {
activeKeyView?.onFlorisTouchEvent(MotionEvent.obtain(
fun dismissActiveKeyViewReference(pointerId: Int) {
activeKeyViews.remove(pointerId)?.onFlorisTouchEvent(MotionEvent.obtain(
0, 0, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0
))
activeKeyView = null
}
/**
* The desired key heights/widths are being calculated here.
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val keyMarginH = resources.getDimension((R.dimen.key_marginH)).toInt()
val keyMarginV = resources.getDimension((R.dimen.key_marginV)).toInt()
val keyMarginH: Int
val keyMarginV: Int
if (isSmartbarKeyboardView){
keyMarginH = resources.getDimension(R.dimen.key_marginH).toInt()
keyMarginV = resources.getDimension(R.dimen.key_marginV).toInt()
}else {
keyMarginV = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingVertical, context).toInt()
keyMarginH = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
}
val desiredWidth = MeasureSpec.getSize(widthMeasureSpec).toFloat()
desiredKeyWidth = if (isSmartbarKeyboardView) {

View File

@@ -17,6 +17,8 @@
package dev.patrickgold.florisboard.ime.text.layout
import dev.patrickgold.florisboard.ime.text.key.FlorisKeyData
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyType
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
typealias LayoutDataArrangement = List<List<FlorisKeyData>>
@@ -52,4 +54,56 @@ data class ComputedLayoutData(
val name: String,
val direction: String,
val arrangement: ComputedLayoutDataArrangement = mutableListOf()
)
) {
companion object {
val PRE_GENERATED_LOADING_KEYBOARD = ComputedLayoutData(
mode = KeyboardMode.CHARACTERS,
name = "__loading_keyboard__",
direction = "ltr",
arrangement = mutableListOf(
mutableListOf(
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0)
),
mutableListOf(
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0)
),
mutableListOf(
FlorisKeyData(code = KeyCode.SHIFT, type = KeyType.MODIFIER, label = "shift"),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = KeyCode.DELETE, type = KeyType.ENTER_EDITING, label = "delete")
),
mutableListOf(
FlorisKeyData(code = KeyCode.VIEW_SYMBOLS, type = KeyType.SYSTEM_GUI, label = "view_symbols"),
FlorisKeyData(code = 0),
FlorisKeyData(code = 0),
FlorisKeyData(code = KeyCode.SPACE, label = "space"),
FlorisKeyData(code = 0),
FlorisKeyData(code = KeyCode.ENTER, type = KeyType.ENTER_EDITING, label = "enter")
)
)
)
}
}

View File

@@ -17,12 +17,15 @@
package dev.patrickgold.florisboard.ime.text.layout
import android.content.Context
import com.github.michaelbull.result.getOr
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.core.Subtype
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.popup.PopupExtension
import dev.patrickgold.florisboard.ime.popup.PopupManager
import dev.patrickgold.florisboard.ime.popup.PopupSet
import dev.patrickgold.florisboard.ime.text.key.*
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
@@ -36,6 +39,7 @@ private typealias KMS = Pair<KeyboardMode, Subtype>
* Class which manages layout loading and caching.
*/
class LayoutManager(private val context: Context) : CoroutineScope by MainScope() {
private val assetManager: AssetManager = AssetManager.default()
private val computedLayoutCache: HashMap<KMS, Deferred<ComputedLayoutData>> = hashMapOf()
/**
@@ -65,9 +69,21 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
}
private fun loadExtendedPopups(subtype: Subtype? = null): PopupExtension {
val lang = subtype?.locale?.language ?: "\$default"
val map = PopupExtension.fromFile(context, "ime/text/characters/extended_popups/$lang.json")
return map.getOr(PopupExtension.empty())
val langTagRef = AssetRef(
source = AssetSource.Assets,
path = PopupManager.POPUP_EXTENSION_PATH_REL + "/" + (subtype?.locale?.toLanguageTag() ?: "\$default") + ".json"
)
val langRef = AssetRef(
source = AssetSource.Assets,
path = PopupManager.POPUP_EXTENSION_PATH_REL + "/" + (subtype?.locale?.language ?: "\$default") + ".json"
)
assetManager.loadAsset(langTagRef, PopupExtension::class).onSuccess {
return it
}
assetManager.loadAsset(langRef, PopupExtension::class).onSuccess {
return it
}
return PopupExtension.empty()
}
/**
@@ -146,7 +162,8 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
}
// Add popup to keys
if (keyboardMode == KeyboardMode.CHARACTERS) {
if (keyboardMode == KeyboardMode.CHARACTERS || keyboardMode == KeyboardMode.NUMERIC_ADVANCED ||
keyboardMode == KeyboardMode.SYMBOLS || keyboardMode == KeyboardMode.SYMBOLS2) {
val extendedPopupsDefault = loadExtendedPopups()
val extendedPopups = loadExtendedPopups(subtype)
for (row in computedArrangement) {

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