Compare commits

...

209 Commits

Author SHA1 Message Date
Patrick Goldinger
a8213d2e2a Release v0.3.10 2021-04-08 20:37:35 +02:00
Patrick Goldinger
a88e04c8f3 Merge pull request #580 from UltimateOmega/Improve-Persian-popup-for-آ
Improve Persian popup (آ)
2021-04-07 23:45:56 +02:00
UltimateOmega
3dde47710d Improve Persian popup for آ
With this rearrange, when Accent is prioritized in hinted symbols, آ is the first option in the popup.
آ is one of the most used alphabets in Persian language.
2021-04-07 09:55:30 +04:30
Patrick Goldinger
36229136ec Release v0.3.10-beta06 2021-04-06 23:59:00 +02:00
Patrick Goldinger
00424055b5 Update translations from Crowdin 2021-04-06 23:41:10 +02:00
Patrick Goldinger
cba5a756f8 Merge pull request #578 from florisboard/improve-fa-popups
Improve Persian popups
2021-04-06 23:34:31 +02:00
Patrick Goldinger
fc401359a7 Improve Persian popups 2021-04-06 23:29:44 +02:00
Patrick Goldinger
546dad8b71 Merge pull request #576 from X-yl/master
Fix #571: Set paste key enabled when attached
2021-04-06 20:16:47 +02:00
Patrick Goldinger
b30e3b8093 Update CONTRIBUTING.md for new layout+config system 2021-04-06 01:25:33 +02:00
Patrick Goldinger
b415afe6e4 Add funding info (#257) 2021-04-06 00:56:08 +02:00
Patrick Goldinger
b69be1ab46 Merge pull request #575 from florisboard/combining-diacritical-marks
Add support for proper display of Combining Diacritical Marks
2021-04-05 20:07:14 +02:00
Patrick Goldinger
c5cf8efe82 Add support for proper display of Combining Diacritical Marks 2021-04-05 19:48:16 +02:00
x-yl
ae0ec65ce0 Fix #571: Set paste key enabled when attached 2021-04-05 21:08:30 +04:00
Patrick Goldinger
7ac3e45b34 Adjust ckb default subtype for eastern symbol layouts (#565) 2021-04-05 18:02:43 +02:00
Patrick Goldinger
5a71793f1a Merge PR #530 manually 2021-04-05 17:57:29 +02:00
Patrick Goldinger
ed040ca49b Resolve merge conflicts and adapt config.json for Kurdí layouts 2021-04-05 17:55:49 +02:00
Patrick Goldinger
c0f90a7ea4 Merge pull request #544 from X-yl/gesture-typing
Implement gesture typing
2021-04-05 16:21:05 +02:00
x-yl
2d9651da8c Make preview refresh delay be less ridiculous
A range of 50ms-1500ms was just kinda comical.
2021-04-05 15:20:43 +04:00
x-yl
9f5a126c1f Cancel gesture properly 2021-04-05 14:53:22 +04:00
x-yl
182e6c58e1 Simplify GlideTypingGesture by only tracking a single touch 2021-04-05 14:53:03 +04:00
x-yl
c7b36829df Don't cancel gesture when tapping on keys 2021-04-05 08:58:45 +04:00
Patrick Goldinger
790fd16682 Merge pull request #560 from Huy-Ngo/ipa
Add IPA keyboard layout
2021-04-04 18:54:08 +02:00
Patrick Goldinger
a2c9699c7e Merge pull request #539 from GabiK65/GabiK65-patch
Some cleanup in hungarian layout
2021-04-04 17:59:27 +02:00
Patrick Goldinger
fff8e7dab9 Merge branch 'master' into GabiK65-patch 2021-04-04 17:58:49 +02:00
Ngô Ngọc Đức Huy
9887f38b4f Rearrange symbol keys 2021-04-04 22:17:50 +07:00
x-yl
b5c2acb328 make GlideTypingClassifier work with KeyView instead of KeyData 2021-04-04 18:51:55 +04:00
x-yl
6469324572 minor fixes 2021-04-04 18:50:28 +04:00
Ngô Ngọc Đức Huy
6227e6d1a9 Add IPA symbols 2021-04-04 21:43:38 +07:00
Patrick Goldinger
80bfe03c0b Merge pull request #556 from Mahmoudk1000/master
Qwertz German layout
2021-04-03 22:31:24 +02:00
mahmoudk1000
8a82bc713b qwertz german layout 2021-04-03 17:06:31 +02:00
GoRaN
e3137db9b4 Update kurdish_standard.json 2021-04-03 17:21:57 +03:00
GoRaN
35d351c596 Update kurdish_standard.json 2021-04-03 17:21:32 +03:00
GoRaN
2163eacfbe Update kurdish_kurmanci.json
Added the Label name :)
2021-04-03 17:18:14 +03:00
x-yl
798f449cc1 Switch back to strings because char arrays broke it 2021-04-03 15:25:44 +04:00
Patrick Goldinger
0d2d560950 Merge pull request #564 from florisboard/fix-number-row-not-showing
Fix number row not displaying on characters
2021-04-03 12:33:36 +02:00
Patrick Goldinger
a4e31d0f50 Fix number row not displaying on characters (#563) 2021-04-03 12:16:04 +02:00
x-yl
96d2043ed8 Fix merge conflicts.. 2021-04-03 13:51:25 +04:00
Ngô Ngọc Đức Huy
5b3033c6da Add some modifiers 2021-04-03 16:50:16 +07:00
Ngô Ngọc Đức Huy
50b1f65f18 Remove Shift keys 2021-04-03 16:50:16 +07:00
Ngô Ngọc Đức Huy
56058d2c4b Fix JSON 2021-04-03 16:50:16 +07:00
Ngô Ngọc Đức Huy
aeb10293c6 Add IPA keyboard layout
Not all symbols are mapped yet.
2021-04-03 16:50:10 +07:00
x-yl
7132ac2479 Reduce memory usage (by a lot)
Switched out every String for a CharArray. Also got rid of the ideal
gesture cache. It had a minimal impact on performance and was taking up
a ridiculous amount of memory.
2021-04-03 12:46:46 +04:00
x-yl
d688549310 Have multiple possible ideal gestures.
This allows for words with double letters to be typed without adding a
loop, while still allowing words like feel and fell to be
differentiated.
2021-04-03 08:25:47 +04:00
x-yl
a763d38304 change multithreading lock logic 2021-04-03 07:46:08 +04:00
Patrick Goldinger
62eb97cd16 Release v0.3.10-beta05 2021-04-03 04:03:41 +02:00
Patrick Goldinger
6813616355 Fix number layout not reliably showing up (#532) 2021-04-03 03:32:55 +02:00
Patrick Goldinger
ee2d574f46 Merge pull request #562 from florisboard/suggestions-ui-bug-fixing
Suggestions UI bug fixing / minor improvements
2021-04-03 03:25:22 +02:00
Patrick Goldinger
945a57d6d8 Fix dynamic width display mode not filling in suggestions (#533) 2021-04-03 03:13:56 +02:00
Patrick Goldinger
e62ba9d156 Add auto-hide clipboard suggestion after usage (#538) 2021-04-03 02:54:30 +02:00
Patrick Goldinger
d3a4136050 Fix content provider authority clash for different tracks (#535) 2021-04-03 02:44:43 +02:00
Patrick Goldinger
7a6d95e250 Merge PR #529 manually 2021-04-03 02:24:15 +02:00
Patrick Goldinger
6fe585a7aa Resolve merge conflicts 2021-04-03 02:16:50 +02:00
Patrick Goldinger
7b25381850 Merge pull request #561 from florisboard/subtype-specific-layouts
Add subtype specific symobol / numeric layouts & currency sets
2021-04-03 01:41:54 +02:00
Patrick Goldinger
409922c3e9 Fix old subtype pref remaining causing crash 2021-04-02 19:40:55 +02:00
Patrick Goldinger
2acabf9c4a Polish UI of subtype add/edit dialog 2021-04-02 19:22:55 +02:00
X-yl
61f7abf43d Merge branch 'master' into gesture-typing 2021-04-02 21:21:58 +04:00
Patrick Goldinger
d29c753c6d Add arabic & persian symbol layouts 2021-04-02 19:10:06 +02:00
Patrick Goldinger
f25e20714c Add subtype specific currency sets 2021-04-02 18:16:26 +02:00
x-yl
2fdec33b1f Improve performance, bugfix
Increased default preview time, and added options to adjust it.
Reduced number of points on the gesture drawn.
Fixed some teeny tiny bugs which caused gesture typing to not work.
2021-04-02 17:59:57 +04:00
x-yl
64f5aea163 Fixed bug where sometimes gestures didn't work 2021-04-02 15:09:57 +04:00
x-yl
847ed1041b Made glide trail themeable 2021-04-02 12:02:58 +04:00
x-yl
74cca0bc4c Added trail fade 2021-04-02 10:02:43 +04:00
x-yl
534dd0a594 Fix case issues 2021-04-02 08:27:51 +04:00
x-yl
f84612ed75 Fix crash on non english layouts 2021-04-02 08:04:38 +04:00
x-yl
9b2b2c06e5 another hacky suggestion fix 2021-04-01 19:15:08 +04:00
x-yl
cf1c18aa70 Small suggestion bugfix 2021-04-01 17:00:54 +04:00
x-yl
418b012550 Fix bug in one handed and landscape mode 2021-04-01 16:45:21 +04:00
x-yl
af4016db43 Minor bug fixes
1. gesture suggestions don't clear after you pressed space
2. space was inserted before word when gesture typing on new line
2021-04-01 16:01:00 +04:00
x-yl
efbda2a758 Removed unnecessary change 2021-04-01 15:39:33 +04:00
x-yl
a7028d4c62 Minor pref fix 2021-04-01 15:36:43 +04:00
x-yl
fd272faebd Remove debug logging, some docs 2021-04-01 15:20:31 +04:00
x-yl
a0cbf65f24 One handed and landscape support 2021-04-01 15:11:34 +04:00
x-yl
1a4a3eb07d Docs 2021-04-01 13:38:25 +04:00
x-yl
a24e626e00 Compatibility with swipe gestures 2021-04-01 13:38:24 +04:00
x-yl
1b86f519a0 Make preferences functional.
Minor changes: Added more points to trail so it looks smoother, some
caching, and made some stuff async.
2021-04-01 13:38:24 +04:00
x-yl
72d15f1dc1 Make preferences functional.
Minor changes: Added more points to trail so it looks smoother, some
caching, and made some stuff async.
2021-04-01 11:50:12 +04:00
Patrick Goldinger
c53a6847fe Add Eastern Arabic and Persian number row 2021-04-01 01:15:05 +02:00
x-yl
a41c1b3493 Fixed issue where nothing was entered when typing quickly 2021-03-31 18:35:15 +04:00
x-yl
dd03bb1ca2 Make naming consistent 2021-03-31 18:26:06 +04:00
x-yl
a9519ceca1 Delete word when gliding 2021-03-31 18:24:13 +04:00
x-yl
ddc72042a1 Integrate suggestions 2021-03-31 18:14:17 +04:00
x-yl
a95b2a23df begin work on integrating with suggestions 2021-03-31 16:36:25 +04:00
x-yl
99187c808d Refactoring for clarity 2021-03-31 15:34:46 +04:00
x-yl
653f34cb3b Show suggestions & performance improvements
Show suggestions while gesturing. Also performance improvements like
implementing a cache, and limiting the trail size
2021-03-31 13:54:35 +04:00
x-yl
08eeea4eb4 Visual improvements
Improved the way trails look by using circles instead of lines.
2021-03-31 10:58:23 +04:00
x-yl
7477e573a5 Bug fixes
Namely, a crash that occured when a word starts and end on the same
letter (due to incorrect behaviour of resample) and also an issue where
gestures weren't reset while typing
2021-03-31 10:17:01 +04:00
x-yl
720a47920f performance improvements 2021-03-31 10:15:49 +04:00
GabiK65
d686f6f5a8 Update hu.json 2021-03-31 01:48:34 +02:00
GabiK65
c382f0bbf8 Update hungarian.json 2021-03-31 01:42:12 +02:00
Patrick Goldinger
2790052e9b Adapt existing layout files & IME config to new syntax 2021-03-31 01:28:20 +02:00
Patrick Goldinger
218a057110 Add base for subtype specific options for all layouts 2021-03-31 01:27:43 +02:00
x-yl
27e6d58ffc Revert back to old resampling method.
Seems like my method (i.e dynamically sample instead of create a whole new gesture)
has an issue with it.
2021-03-30 17:54:32 +04:00
x-yl
4c2c993f3f Added full dictionary (broken commit) 2021-03-30 16:49:54 +04:00
x-yl
faca221699 Prettied up the trail effect 2021-03-30 13:56:17 +04:00
x-yl
f4d8bdbf0f Light refactoring.
Moved Gesture to be part of StatisticalGestureTypingClassifier, cleaned
up some initialization code.
2021-03-30 13:24:59 +04:00
BinFlush
aa909d3135 Update fo.json 2021-03-30 09:30:56 +02:00
Goran Gharib
cdf5a566c6 Fix and correction on Kurdish layout
fixed the popup extended words
2021-03-30 04:43:43 +03:00
Goran Gharib
807b99ae51 Added new kurdish layouts
Added new kurdish kurmanci layout with kurdish standard layout with some correction of current layout.
2021-03-30 04:35:04 +03:00
Jakup Lutzen
d93f09078e added faroese layout 2021-03-30 01:18:03 +02:00
x-yl
cc12798a87 Naive port of the gesturing algorithm. 2021-03-29 18:21:24 +04:00
Patrick Goldinger
02b1a1d278 Merge pull request #512 from icyphox/workman-layout
Add the Workman keyboard layout
2021-03-29 15:54:49 +02:00
Patrick Goldinger
d978cdf845 Fix code of "L" key 2021-03-29 15:49:23 +02:00
Patrick Goldinger
b2ec115505 Release v0.3.10-beta04 2021-03-29 14:46:14 +02:00
x-yl
d5c0b11dbe Tweak gesture detection 2021-03-29 12:08:51 +04:00
x-yl
2a8ba07040 Unrelated change but seeing it basically reimplement Math.atan2 was just so sad. 2021-03-29 08:23:38 +04:00
Anirudh Oppiliappan
f8c9a52be5 Fix Unicode code points 2021-03-29 08:31:06 +05:30
Patrick Goldinger
670e6ca5e1 Fix emoji ABC button not leading back to characters (#521) 2021-03-29 02:55:23 +02:00
Patrick Goldinger
f2403d00e5 Add long-press caps-lock activation vibration (#523) 2021-03-29 02:46:46 +02:00
Patrick Goldinger
224d3e00e3 Merge pull request #518 from florisboard/fix-popup-width-landscape
Adjust popups in landscape mode
2021-03-28 19:47:40 +02:00
Patrick Goldinger
e89a374ce0 Adjust popup width in landscape (#504) 2021-03-28 19:35:43 +02:00
Patrick Goldinger
538e2dd9a2 Merge pull request #514 from florisboard/suggestions-phase2-frontend
Suggestions frontend rework
2021-03-28 19:15:37 +02:00
Patrick Goldinger
1d3d85c211 Fix crash for image clipboard suggestions 2021-03-28 19:08:49 +02:00
Patrick Goldinger
d6121baca9 Polish and document candidate view 2021-03-28 18:16:59 +02:00
x-yl
d6f5789659 Utilize gesture class 2021-03-28 18:09:48 +04:00
x-yl
e7b7df6987 Added Gesture class 2021-03-28 18:09:25 +04:00
Patrick Goldinger
8c0337d6c9 Fix suggestions not resetting when switching apps (#429) 2021-03-27 19:53:51 +01:00
Patrick Goldinger
563a4a919d Add new candidate+clipboard suggestion view (#38, #424, #425, #426) 2021-03-27 19:45:53 +01:00
Anirudh Oppiliappan
7d6666f7f3 Add the Workman keyboard layout 2021-03-27 20:22:12 +05:30
Patrick Goldinger
2f0d607d02 Potential fix for #484 2021-03-24 20:20:01 +01:00
Patrick Goldinger
65ae6c2b66 Merge pull request #491 from X-yl/clipboard-stuff
Fix for #481
2021-03-24 19:21:33 +01:00
x-yl
14513ec0f1 kotlin-ify 2021-03-24 18:07:07 +04:00
x-yl
3c58144a3d fix #481 2021-03-24 17:52:12 +04:00
Patrick Goldinger
d65b706f78 Release v0.3.10-beta03 2021-03-23 20:00:49 +01:00
Patrick Goldinger
9d820677db Update translations from Crowdin 2021-03-23 19:50:06 +01:00
Patrick Goldinger
69c52c00f6 Fix Ž key not available in Dvorak/Serbian (#381) 2021-03-23 19:43:41 +01:00
Patrick Goldinger
c8cf256577 Merge pull request #488 from florisboard/turkish-layouts
Add Turkish-Q / Turkish-F layouts
2021-03-23 14:35:11 +01:00
Patrick Goldinger
386a0999c4 Add Turkish-Q / Turkish-F layouts (#182) 2021-03-23 14:07:42 +01:00
Patrick Goldinger
d4ef2ea827 Merge pull request #486 from Netscaping/patch-1
Create gboard_night.json
2021-03-22 15:28:11 +01:00
Patrick Goldinger
381ec68e6c Merge pull request #482 from florisboard/rework-symbols-sizing
Rework symbols sizing when number row is enabled
2021-03-22 15:18:40 +01:00
Netscaping
a5706167b2 Create gboard_night.json
Since there is a Gboard Day theme I added the night version.
2021-03-22 15:17:46 +01:00
Patrick Goldinger
660871d6c8 Rework symbols sizing when number row is enabled 2021-03-22 00:55:47 +01:00
Patrick Goldinger
6607ad1739 Fix language selector size for keyboard height greater than 125% 2021-03-22 00:01:56 +01:00
Patrick Goldinger
55c1bc05f2 Add auto-switching to characters in symbols (#347) 2021-03-21 19:12:10 +01:00
Patrick Goldinger
7eb7f0ef80 Release v0.3.10-beta02 2021-03-19 19:42:08 +01:00
Patrick Goldinger
78e5e417ce Update README.md to include new beta track 2021-03-19 18:49:40 +01:00
Patrick Goldinger
ffbf7f8ea7 Merge pull request #454 from X-yl/clipboard-stuff
Added support for private clipboard and clipboard history
2021-03-19 17:49:16 +01:00
Patrick Goldinger
27cc4897c3 Merge pull request #479 from florisboard/fix-import-theme-crash
Fix import theme crash for big files
2021-03-19 17:18:49 +01:00
Patrick Goldinger
e5111a8efe Fix import theme crash for big files (#465) 2021-03-19 17:04:48 +01:00
Patrick Goldinger
80fd5ca84a Add beta metadata 2021-03-19 00:57:11 +01:00
x-yl
e8f2c6ce74 fix bug when history size is reduced 2021-03-18 23:21:50 +04:00
x-yl
5676cbf18e Stupid telegram, not using ContentResolver... smh 2021-03-18 17:50:48 +04:00
x-yl
2bdaea6189 revoke URI permissions, support API <25 2021-03-18 17:10:28 +04:00
Patrick Goldinger
da2287a739 Fix symbols layouts applying the caps state once again (#298) 2021-03-17 23:29:58 +01:00
Patrick Goldinger
3fafe0fac8 Release v0.3.10-beta01 2021-03-17 14:48:31 +01:00
x-yl
86042bb1e1 make popup buttons extend to the edge of popup 2021-03-17 15:30:22 +04:00
x-yl
c99673ff1d mime type fixes, remove from history after pressing delete 2021-03-17 15:20:12 +04:00
x-yl
8b89b27fb0 Misc. fixes 2021-03-17 10:57:56 +04:00
x-yl
b56c976fa0 code cleanup 2021-03-17 10:26:22 +04:00
x-yl
08889fdc60 docs 2021-03-17 10:20:21 +04:00
x-yl
e8d657e81c free storage after images leave clipboard 2021-03-17 09:38:03 +04:00
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
x-yl
bfcea8b718 Make pins persistent 2021-03-16 18:58:14 +04:00
Patrick Goldinger
c701141be2 Remove Italian store metadata 2021-03-16 15:15:04 +01:00
x-yl
7f07686b6c added proper mime type support to content provider 2021-03-16 16:38:01 +04: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
x-yl
e4ecc63b9d Added an abstraction around ClipData 2021-03-15 15:12:30 +04: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
x-yl
aacb33bd5d fixed issue when floris clipboard is disabled 2021-03-13 20:46:27 +04:00
x-yl
a0aa446988 Change back button 2021-03-13 18:06:10 +04:00
x-yl
fe086ed6d8 removed some debug logging 2021-03-13 17:39:11 +04:00
x-yl
64ddd0f421 fixed a stupid bug somehow 2021-03-13 17:35:35 +04:00
x-yl
40fe72e33c fix a few bugs 2021-03-13 14:55:58 +04:00
x-yl
b229970ec3 cleanup and documentation 2021-03-13 13:04:34 +04:00
x-yl
ec32c211f1 added delete and paste. pretty much feature complete now. 2021-03-12 23:39:23 +04:00
x-yl
e66b8a052a Pin/unpin support 2021-03-12 22:18:40 +04:00
x-yl
4a22c2698c added more ways to open clipboard context, fixed popups, refactored some code 2021-03-12 21:50:24 +04:00
x-yl
ae95bbd7c4 Added a mock popup 2021-03-11 18:03:08 +04:00
x-yl
0bdeeaa340 VERY work in progress 2021-03-11 10:24:40 +04:00
x-yl
92a885a34c Little bit of preference stuff 2021-03-11 10:24:35 +04:00
x-yl
bc2f03a920 light refactoring, some theme stuff 2021-03-11 10:24:26 +04:00
x-yl
f60827b634 small theme fix 2021-03-11 10:23:59 +04:00
x-yl
dcf81b27a0 Fixed animations, added image support, some documentation 2021-03-11 10:23:59 +04:00
x-yl
0d8601cb15 Text-only clipboard history implemented 2021-03-11 10:23:59 +04:00
x-yl
ecf3c6bf27 All clipboard actions now use FlorisClipboardManager. Added support for commiting non-text content. Added simple clipboard history layout. 2021-03-11 10:23:46 +04: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
Goran Gharib
5c83583149 Merge branch 'master' of https://github.com/kurdikeyboard/florisboard 2021-02-15 04:20:47 +03:00
Goran Gharib
0fb73ece9a Update README.md
Added Kurdish language to the list of Non-latin characters into readme file.
2021-02-15 04:20:40 +03:00
325 changed files with 11124 additions and 2106 deletions

View File

@@ -11,3 +11,7 @@ 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

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: [patrickgold]
custom: ["https://paypal.me/devpatrickgold", "https://explorer.bitcoin.com/btc/address/1GKPJuRTZbVM7L8Kd3wtrqzc259Sjmoh9x"]

View File

@@ -9,7 +9,10 @@ provides some general guidelines for each type of contribution.
Either use the review function within Google Play or email me at
[florisboard@patrickgold.dev](mailto:florisboard@patrickgold.dev). I
love to hear from you!
love to hear from you! Note, that the amount of feedback emails I get
is overwhelmingly high - so if I don't answer or answer really late, I
apologize - I guarantee though that I read through every email and that
I will use every feedback to improve FlorisBoard :)
## Translations
@@ -30,23 +33,52 @@ enables both you and the dev team that we are on the same page before
you start on working on your change. If you have any questions, feel
free to ask for help at any time!
## Adding a new keyboard layout / dictionary for locale
## Adding a new keyboard layout
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.
Adding a layout to FlorisBoard is very simple and does not require any
coding skills, although you should understand the basics of the JSON
syntax (it is very easy though by just looking at some other layout files).
There are two main steps in adding new layouts, though the config step can
be skipped if you only add a layout without a new default language support.
Currently you need to modify `app/src/main/assets/ime/config.json` to
add the filename of the language/layout to the `characterLayouts`
section and the `defaultSubtypes` section, making sure to include
the language's IETF BCP 47 code ([ISO 639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
and [ISO 3166-1 region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements)).
For example, Dutch as spoken in Belgium is `nl-be`. Use a unique value
for `id` to avoid possible crahses caused by duplicate ids.
### The config file (`app/src/main/assets/ime/config.json`)
Add the keyboard layout at `app/src/main/assets/ime/text/characters/<preferredLayout_name_here>.json`,
with `code` referring to the characters codepoint and `label` being the
respective unicode character.
This file is very important, as it defines all default currency sets as
well as all default subtypes available in the Settings Subtype UI. Note
that you don't have to modify this file if you add a layout for an already
pre-configured language.
- `currencySets`: This is a list of all currency sets, which can be chosen
for each subtype. If you consider adding a new one, make sure that the
first currency symbol matches the name of the currency set and also
ensure that you have exactly 6 currency symbols. This is important as the
symbol layouts have exactly 6 slots available to fill these defined
currency symbols in.
- `defaultSubtyes`: This is a list of all pre-made subtypes. Each time the
user selects a language in the `Subtype Add`-dialog, all options configured
here will get pre-selected. The language tag must adhere to the IETF BCP
47 code ([ISO 639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
and [ISO 3166-1 region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements)).
For example, Dutch as spoken in Belgium is `nl-be`. Use a unique value
for `id` to avoid possible crashes caused by duplicate ids.
### Adding the layout
Since v0.3.10-beta05 it is possible to add custom layouts for all types.
To add a new layout, head to `app/src/main/assets/ime/text` and then select
the correct sub-directory for the type of layout you want to add. In most cases
this will be `characters` to add a layout like QWERTY etc.
For the `code` field of each key, make sure to use the UTF-8 code. An
useful tool for finding the correct code is [unicode-table.com](https://unicode-table.com/en/).
From there, you search for your letter and then use the HTML code, but without the `&#;`
For internal codes of functional or UI keys, see
`app/src/main/java/dev/patrickgold/florisboard/ime/text/key/KeyCode.kt`.
The label is equally important and should always match up with the defined
code. If `code` and `label` don't match up, FlorisBoard won't crash but
it will most likely lead to confusion in the key processing logic.
Any accents or diacritics that should be exposed via long press can be
added at `assets/ime/text/characters/extended_popups/<languageTag_name_here>.json`.
@@ -56,12 +88,21 @@ you add. The main field is used for determining if a hint or an accent
should take priority, so please make sure to leave main empty and just
use relevant for accents which are not-so important.
For popups of non-`characters` layout, simply add the popup directly to
each key via the `popup` field.
## Adding a new dictionary for a language
Currently the suggestions implementation is highly experimental and will
get a major if not complete rework, so dictionaries are currently not
accepted.
## Bug reporting
This kind of contribution is the most important, as it tells where
FlorisBoard has flaws and thus should be improved to maximize stability
and user experience. To make this process as smooth as possible, please
use the premade [issue template](.github/ISSUE_TEMPLATE/bug_report.md)
use the pre-made [issue template](.github/ISSUE_TEMPLATE/bug_report.md)
for bug reporting. This makes it easy for us to understand what the bug
is and how to solve it.
@@ -73,3 +114,10 @@ preferred way to capture logs.
Alternatively, you can also use ADB (Android Debug Bridge) to capture
the error log. This is recommended for experienced users only.
## Donating
If none of the above options are feasible for you but you still want to
show your support, you can also buy me a coffee, so I can stay up all night
and chase away bugs or add new cool stuff :)
See the `Sponsors` button for available options!

View File

@@ -1,14 +1,15 @@
<img align="left" width="80" height="80"
src="fastlane/metadata/android/en-US/images/icon.png" alt="App icon">
# FlorisBoard [![Release](https://img.shields.io/github/v/release/florisboard/florisboard)](https://github.com/florisboard/florisboard/releases) [![Crowdin](https://badges.crowdin.net/florisboard/localized.svg)](https://crowdin.florisboard.patrickgold.dev) ![FlorisBoard CI](https://github.com/florisboard/florisboard/workflows/FlorisBoard%20CI/badge.svg?event=push)
# FlorisBoard [![Crowdin](https://badges.crowdin.net/florisboard/localized.svg)](https://crowdin.florisboard.patrickgold.dev) ![FlorisBoard CI](https://github.com/florisboard/florisboard/workflows/FlorisBoard%20CI/badge.svg?event=push)
**FlorisBoard** is a free and open-source keyboard for Android 6.0+
devices. It aims at being modern, user-friendly and customizable while
fully respecting your privacy. Currently in alpha/early-beta state.
fully respecting your privacy. Currently in early-beta state.
## Public Alpha Test Programme
Wanna try it out on your device? Use one of the following options:
### Stable [![Latest stable release](https://img.shields.io/github/v/release/florisboard/florisboard)](https://github.com/florisboard/florisboard/releases/latest)
Releases on this track are in general stable and ready for everyday use, except for features marked as experimental. Use one of the following options to receive FlorisBoard's stable releases:
_A. Get it on F-Droid_:
@@ -36,6 +37,16 @@ for and download FlorisBoard without prior joining the alpha group.
_C. Use the APK provided in the release section of this repo_
### Beta [![Latest beta release](https://img.shields.io/github/v/release/florisboard/florisboard?include_prereleases)](https://github.com/florisboard/florisboard/releases)
Releases on this track are also in general stable and should be ready for everyday use, though crashes and bugs are more likely to occur. Use releases from this track if you want to get new features faster and give feedback for brand-new stuff. Options to get beta releases:
_A. IzzySoft's repo for F-Droid_:
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" height="64" alt="IzzySoft repo badge">](https://apt.izzysoft.de/fdroid/index/apk/dev.patrickgold.florisboard.beta)
_B. Use the APK provided in the release section of this repo_
### Giving feedback
If you want to give feedback to FlorisBoard, there are several ways to
do so, as listed [here](CONTRIBUTING.md#giving-general-feedback).
@@ -62,11 +73,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, Colemak, Dvorak); 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, Turkish-Q, Turkish-F, ...)
* [x] Non-latin character layouts (Arabic, Persian, Kurdish, Greek, Russian (JCUKEN))
* [x] Adapt to situation in app (password, url, text, etc. )
* [x] Special character layout(s)
* [x] Numeric layout
@@ -88,6 +97,7 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
### Other useful features
* [x] One-handed mode
* [x] Clipboard/cursor tools
* [x] Clipboard manager/history
* [x] Integrated number row / symbols in character layouts
* [x] Gesture support
* [x] Full integration in IME service list of Android (xml/method)
@@ -138,6 +148,7 @@ close as possible.
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
@@ -150,7 +161,6 @@ passes...
Backlog (currently not assigned to any milestone):
- Theme import/export
- Floating keyboard
[#91]: https://github.com/florisboard/florisboard/pull/91
@@ -179,8 +189,6 @@ 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)

View File

@@ -1,11 +1,13 @@
plugins {
id("com.android.application") version "4.1.2"
kotlin("android") version "1.4.30"
kotlin("kapt") version "1.4.30"
}
android {
compileSdkVersion(29)
buildToolsVersion("29.0.2")
compileSdkVersion(30)
buildToolsVersion("30.0.3")
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
@@ -20,9 +22,9 @@ android {
defaultConfig {
applicationId = "dev.patrickgold.florisboard"
minSdkVersion(23)
targetSdkVersion(29)
versionCode(27)
versionName("0.3.8")
targetSdkVersion(30)
versionCode(35)
versionName("0.3.10")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -34,11 +36,29 @@ android {
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 = "-beta06"
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")
}
}
@@ -54,11 +74,15 @@ android {
}
}
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")
@@ -66,8 +90,9 @@ dependencies {
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.michael-bull.kotlin-result", "kotlin-result", "1.1.10")
implementation("com.nambimobile.widgets", "expandable-fab", "1.0.2")
implementation("androidx.room", "room-runtime", "2.2.6")
kapt("androidx.room", "room-compiler","2.2.6")
testImplementation("junit", "junit", "4.13.1")
testImplementation("org.mockito", "mockito-inline", "3.7.7")

View File

@@ -23,9 +23,9 @@
<application
android:name=".ime.core.FlorisApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/floris_app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/floris_app_icon_round"
android:supportsRtl="true"
android:theme="@style/SettingsTheme">
@@ -46,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: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"/>
@@ -69,48 +69,55 @@
<!-- 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"/>
<provider
android:name="dev.patrickgold.florisboard.ime.clip.provider.FlorisContentProvider"
android:authorities="${applicationId}.provider.clip"
android:grantUriPermissions="true"
android:exported="false">
</provider>
</application>
</manifest>

View File

@@ -1,225 +1,586 @@
{
"package": "dev.patrickgold.florisboard",
"characterLayouts": {
"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)",
"danish": "Danish (QWERTY)",
"icelandic": "Icelandic (QWERTY)",
"swiss_german": "Swiss German (QWERTZ)",
"swiss_french": "Swiss French (QWERTZ)",
"swiss_italian": "Swiss Italian (QWERTZ)",
"hungarian": "Hungarian (QWERTZ)",
"persian": "Persian",
"arabic": "Arabic",
"esperanto": "Esperanto",
"esperanto_with_hx": "Esperanto with 'ĥ'",
"colemak": "Colemak",
"dvorak": "Dvorak",
"jcuken_russian": "Russian (JCUKEN)",
"canadian_french": "Canadian French (QWERTY)",
"greek": "Ελληνικά",
"hebrew": "עברית",
"serbian_latin": "Serbian (QWERTZ)",
"serbian_cyrillic": "Serbian (ЉЊЕРТЗ)",
"kurdish": "کوردی"
},
"currencySets": [
{
"name": "azerbaijani_manat",
"label": "Azerbaijani manat (₼)",
"slots": [
{ "code": 8380, "label": "₼" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "bitcoin",
"label": "Bitcoin (₿)",
"slots": [
{ "code": 8383, "label": "₿" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "dollar",
"label": "Dollar ($)",
"slots": [
{ "code": 36, "label": "$" },
{ "code": 162, "label": "¢" },
{ "code": 8364, "label": "€" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" },
{ "code": 8369, "label": "₱" }
]
},
{
"name": "euro",
"label": "Euro (€)",
"slots": [
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 36, "label": "$" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" },
{ "code": 8369, "label": "₱" }
]
},
{
"name": "indian_rupee",
"label": "Indian rupee (₹)",
"slots": [
{ "code": 8377, "label": "₹" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "iranian_rial",
"label": "Iranian rial (﷼)",
"slots": [
{ "code":65020, "label": "﷼" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "israeli_new_shekel",
"label": "Israeli new shekel (₪)",
"slots": [
{ "code": 8362, "label": "₪" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "kazakhstani_tenge",
"label": "Kazakhstani tenge (₸)",
"slots": [
{ "code": 8380, "label": "₸" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "lao_kip",
"label": "Lao kip (₭)",
"slots": [
{ "code": 8365, "label": "₭" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "mongolian_togrog",
"label": "Mongolian tögrög (₮)",
"slots": [
{ "code": 8366, "label": "₮" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "nigerian_naira",
"label": "Nigerian naira (₦)",
"slots": [
{ "code": 8358, "label": "₦" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "pakistani_rupee",
"label": "Pakistani rupee (₨)",
"slots": [
{ "code": 8360, "label": "₨" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "paraguayan_guarani",
"label": "Paraguayan guaraní (₲)",
"slots": [
{ "code": 8370, "label": "₲" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "peso",
"label": "Peso (₱)",
"slots": [
{ "code": 8369, "label": "₱" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "pound",
"label": "Pound (£)",
"slots": [
{ "code": 163, "label": "£" },
{ "code": 162, "label": "¢" },
{ "code": 8364, "label": "€" },
{ "code": 36, "label": "$" },
{ "code": 165, "label": "¥" },
{ "code": 8369, "label": "₱" }
]
},
{
"name": "russian_ruble",
"label": "Russian ruble (₽)",
"slots": [
{ "code": 8381, "label": "₽" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "south_korean_won",
"label": "South Korean won (₩)",
"slots": [
{ "code": 8361, "label": "₩" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "turkish_lira",
"label": "Turkish lira (₺)",
"slots": [
{ "code": 8378, "label": "₺" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "ukrainian_hryvnia",
"label": "Ukrainian hryvnia (₴)",
"slots": [
{ "code": 8372, "label": "₴" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
]
},
{
"name": "yen",
"label": "Yen (¥)",
"slots": [
{ "code": 165, "label": "¥" },
{ "code": 36, "label": "$" },
{ "code": 8364, "label": "€" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 8369, "label": "₱" }
]
}
],
"defaultSubtypes": [
{
"id": 101,
"languageTag": "en-US",
"preferredLayout": "qwerty"
"currencySet": "dollar",
"preferred": {
"characters": "qwerty"
}
},
{
"id": 102,
"languageTag": "en-UK",
"preferredLayout": "qwerty"
"currencySet": "pound",
"preferred": {
"characters": "qwerty"
}
},
{
"id": 103,
"languageTag": "en-CA",
"preferredLayout": "qwerty"
"currencySet": "dollar",
"preferred": {
"characters": "qwerty"
}
},
{
"id": 104,
"languageTag": "en-AU",
"preferredLayout": "qwerty"
"currencySet": "dollar",
"preferred": {
"characters": "qwerty"
}
},
{
"id": 201,
"languageTag": "de-DE",
"preferredLayout": "qwertz"
"currencySet": "euro",
"preferred": {
"characters": "qwertz"
}
},
{
"id": 202,
"languageTag": "de-AT",
"preferredLayout": "qwertz"
"currencySet": "euro",
"preferred": {
"characters": "qwertz"
}
},
{
"id": 203,
"languageTag": "de-CH",
"preferredLayout": "swiss_german"
"currencySet": "euro",
"preferred": {
"characters": "swiss_german"
}
},
{
"id": 301,
"languageTag": "fr-FR",
"preferredLayout": "azerty"
"currencySet": "euro",
"preferred": {
"characters": "azerty"
}
},
{
"id": 302,
"languageTag": "fr-CA",
"preferredLayout": "canadian_french"
"currencySet": "dollar",
"preferred": {
"characters": "canadian_french"
}
},
{
"id": 303,
"languageTag": "fr-CH",
"preferredLayout": "swiss_french"
"currencySet": "euro",
"preferred": {
"characters": "swiss_french"
}
},
{
"id": 401,
"languageTag": "it-IT",
"preferredLayout": "qwerty"
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
}
},
{
"id": 402,
"languageTag": "it-CH",
"preferredLayout": "swiss_italian"
"currencySet": "euro",
"preferred": {
"characters": "swiss_italian"
}
},
{
"id": 501,
"languageTag": "es-ES",
"preferredLayout": "spanish"
"currencySet": "euro",
"preferred": {
"characters": "spanish"
}
},
{
"id": 502,
"languageTag": "es-US",
"preferredLayout": "spanish"
"currencySet": "dollar",
"preferred": {
"characters": "spanish"
}
},
{
"id": 503,
"languageTag": "es-419",
"preferredLayout": "spanish"
"currencySet": "dollar",
"preferred": {
"characters": "spanish"
}
},
{
"id": 601,
"languageTag": "pt-PT",
"preferredLayout": "qwerty"
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
}
},
{
"id": 602,
"languageTag": "pt-BR",
"preferredLayout": "qwerty"
"currencySet": "dollar",
"preferred": {
"characters": "qwerty"
}
},
{
"id": 701,
"languageTag": "nb-NO",
"preferredLayout": "norwegian"
"currencySet": "dollar",
"preferred": {
"characters": "norwegian"
}
},
{
"id": 702,
"languageTag": "nn-NO",
"preferredLayout": "norwegian"
"currencySet": "dollar",
"preferred": {
"characters": "norwegian"
}
},
{
"id": 711,
"languageTag": "sv-SE",
"preferredLayout": "swedish_finnish"
"currencySet": "dollar",
"preferred": {
"characters": "swedish_finnish"
}
},
{
"id": 721,
"languageTag": "fi-FI",
"preferredLayout": "swedish_finnish"
"currencySet": "euro",
"preferred": {
"characters": "swedish_finnish"
}
},
{
"id": 731,
"languageTag": "da-DK",
"preferredLayout": "danish"
"currencySet": "dollar",
"preferred": {
"characters": "danish"
}
},
{
"id": 741,
"languageTag": "is-IS",
"preferredLayout": "icelandic"
"currencySet": "dollar",
"preferred": {
"characters": "icelandic"
}
},
{
"id": 751,
"languageTag": "fo",
"currencySet": "dollar",
"preferred": {
"characters": "faroese"
}
},
{
"id": 801,
"languageTag": "fa-FA",
"preferredLayout": "persian"
"currencySet": "iranian_rial",
"preferred": {
"characters": "persian",
"symbols": "persian",
"symbols2": "persian",
"numericRow": "persian"
}
},
{
"id": 901,
"languageTag": "ar",
"preferredLayout": "arabic"
"currencySet": "dollar",
"preferred": {
"characters": "arabic",
"symbols": "eastern",
"symbols2": "eastern",
"numericRow": "eastern_arabic"
}
},
{
"id": 1001,
"languageTag": "hu",
"preferredLayout": "hungarian"
"currencySet": "euro",
"preferred": {
"characters": "hungarian"
}
},
{
"id": 1101,
"languageTag": "eo",
"preferredLayout": "esperanto"
"currencySet": "dollar",
"preferred": {
"characters": "esperanto"
}
},
{
"id": 1201,
"languageTag": "hr",
"preferredLayout": "qwertz"
"currencySet": "euro",
"preferred": {
"characters": "qwertz"
}
},
{
"id": 1301,
"languageTag": "ru",
"preferredLayout": "jcuken_russian"
"currencySet": "russian_ruble",
"preferred": {
"characters": "jcuken_russian"
}
},
{
"id": 1401,
"languageTag": "el",
"preferredLayout": "greek"
"currencySet": "euro",
"preferred": {
"characters": "greek"
}
},
{
"id": 1501,
"languageTag": "ro",
"preferredLayout": "qwerty"
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
}
},
{
"id": 1601,
"languageTag": "pl",
"preferredLayout": "qwerty"
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
}
},
{
"id": 1701,
"languageTag": "bg-bg",
"preferredLayout": "bulgarian_phonetic"
"currencySet": "dollar",
"preferred": {
"characters": "bulgarian_phonetic"
}
},
{
"id": 1801,
"languageTag": "tr",
"preferredLayout": "qwerty"
"currencySet": "turkish_lira",
"preferred": {
"characters": "qwerty"
}
},
{
"id": 1901,
"languageTag": "iw-IL",
"preferredLayout": "hebrew"
"currencySet": "israeli_new_shekel",
"preferred": {
"characters": "hebrew"
}
},
{
"id": 2001,
"languageTag": "ckb",
"preferredLayout": "kurdish"
"currencySet": "dollar",
"preferred": {
"characters": "kurdish",
"symbols": "eastern",
"symbols2": "eastern",
"numericRow": "eastern_arabic"
}
},
{
"id": 2101,
"languageTag": "sr-RS",
"preferredLayout": "serbian_cyrillic"
"currencySet": "dollar",
"preferred": {
"characters": "serbian_cyrillic"
}
},
{
"id": 2201,
"languageTag": "lv-LV",
"preferredLayout": "qwerty"
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
}
},
{
"id": 2301,
"languageTag": "ku",
"currencySet": "dollar",
"preferred": {
"characters": "kurdish_kurmanci"
}
},
{
"id": 2601,
"languageTag": "IPA-IPA",
"currencySet": "dollar",
"preferred": {
"characters": "ipa",
"symbols": "ipa",
"symbols2": "ipa"
}
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "arabic",
"label": "Arabic",
"authors": [ "HeiWiper" ],
"direction": "rtl",
"modifier": "arabic",

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "azerty",
"label": "AZERTY",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "bepo",
"label": "BÉPO",
"authors": [ "salamandar" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "bulgarian_bds",
"label": "Bulgarian (BDS)",
"authors": [ "iorvethe" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "bulgarian_phonetic",
"label": "Bulgarian (Phonetic)",
"authors": [ "iorvethe" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "canadian_french",
"label": "Canadian French (QWERTY)",
"authors": [ "The-Quantum-Alpha" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "colemak",
"label": "Colemak",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [
@@ -18,7 +19,7 @@
"relevant": [
{ "code": 58, "label": ":" }
]
} }
}, "shift": { "code": 58, "label": ":" } }
],
[
{ "code": 97, "label": "a" },

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "danish",
"label": "Danish (QWERTY)",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "dvorak",
"label": "Dvorak",
"authors": [ "patrickgold" ],
"direction": "ltr",
"modifier": "dvorak",
@@ -12,25 +13,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

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "esperanto",
"label": "Esperanto",
"authors": [ "jeremiah-miller", "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "esperanto_with_hx",
"label": "Esperanto with 'ĥ'",
"authors": [ "jeremiah-miller", "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -7,7 +7,8 @@
"~enter": {
"main": { "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
"relevant": [
{ "code": -216, "label": "toggle_one_handed_mode_right", "type": "system_gui" }
{ "code": -216, "label": "toggle_one_handed_mode_right", "type": "system_gui" },
{ "code": -214, "label": "switch_to_clipboard_context", "type": "system_gui"}
]
},
"~left": {

View File

@@ -4,17 +4,8 @@
"authors": [ "GoRaN" ],
"mapping": {
"all": {
"ق": {
"relevant": [
{ "code": 1647, "label": "ٯ" }
]
},
"ئ": {
"relevant": [
{"code": 1569, "label": "ء" }
]
},
"ە": {
"": {
"relevant": [
{ "code": 1577, "label": "ة" },
{ "code": 1729, "label": "ـہ" }
@@ -26,45 +17,70 @@
{ "code": 1682, "label": "ڒ" }
]
},
"ف": {
"ی": {
"relevant": [
{ "code": 1701, "label": "ڥ" },
{ "code": 1698, "label": "ڢ" },
{ "code": 1700, "label": "ڤ" },
{ "code": 1697, "label": "ڡ" }
{ "code": 1746, "label": "ے" },
{ "code": 1610, "label": "ي" },
{ "code": 1744, "label": "ې" },
{ "code": 1741, "label": "ۍ" },
{ "code": 1742, "label": "ێ" },
{ "code": 1597, "label": "ؽ" }
]
},
"": {
"ﺋ": {
"relevant": [
{ "code": 65163, "label": "ﺋ" },
{ "code": 1569, "label": "ء" },
{ "code": 65139, "label": "ﹳ" }
]
},
"ع": {
"relevant": [
{ "code": 1551, "label": "؏" },
{ "code": 1594, "label": "غ" }
{ "code": 1551, "label": "؏" }
]
},
"ۆ": {
"relevant": [
{ "code": 1743, "label": "ۏ" },
{ "code": 1735, "label": "ۇ" },
{ "code": 1737, "label": "ۉ" },
{ "code": 1738, "label": "ۊ" },
{ "code": 1572, "label": "ؤ" },
{ "code": 1739, "label": "ۋ" }
]
},
"ف": {
"relevant": [
{ "code": 1701, "label": "ڥ" },
{ "code": 1700, "label": "ڤ" },
{ "code": 1698, "label": "ڢ" },
{ "code": 1697, "label": "ڡ" }
]
},
"د": {
"relevant": [
{ "code": 1676, "label": "ڌ" },
{ "code": 64390, "label": "ﮆ" },
{ "code": 1584, "label": "ذ" },
{ "code": 64390, "label": "ﮆ" },
{ "code": 1774, "label": "ۮ" }
]
},
"ه": {
"ھ": {
"relevant": [
{ "code": 1726, "label": "ھ" }
]
},
"خ": {
"relevant": [
{ "code": 1567, "label": "؟" }
]
},
"س": {
"relevant": [
{ "code": 1589, "label": "ص" }
@@ -110,42 +126,23 @@
{ "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": 1563, "label": "؛" },
{ "code": 59, "label": ";" },
{ "code": 58, "label": ":" },
{ "code": 44, "label": "," },
{ "code": 1549, "label": "؍" },
{ "code": 45, "label": "-" },
{ "code": 95, "label": "_" },
{ "code": 45, "label": "-" }
{ "code": 1600, "label": "" },
{ "code": 33, "label": "!" },
{ "code": 1548, "label": "،" }
]
}
},

View File

@@ -4,69 +4,27 @@
"authors": [ "PHELAT" ],
"mapping": {
"all": {
"ض": {
"relevant": [
{ "code": 1777, "label": "۱" }
]
},
"ص": {
"relevant": [
{ "code": 1778, "label": "۲" }
]
},
"ث": {
"relevant": [
{ "code": 1779, "label": "۳" }
]
},
"ق": {
"relevant": [
{ "code": 1780, "label": "۴" }
]
},
"ف": {
"relevant": [
{ "code": 1781, "label": "۵" }
]
},
"غ": {
"relevant": [
{ "code": 1782, "label": "۶" }
]
},
"ع": {
"relevant": [
{ "code": 1783, "label": "۷" }
]
},
"ه": {
"relevant": [
{ "code": 1784, "label": "۸" }
]
},
"خ": {
"relevant": [
{ "code": 1785, "label": "۹" }
]
},
"ح": {
"relevant": [
{ "code": 1776, "label": "۰" }
]
},
"ی": {
"relevant": [
{ "code": 1574, "label": "ئ" },
{ "code": 1610, "label": "ي" }
{ "code": 1610, "label": "ي" },
{ "code": 1746, "label": "ے" }
]
},
"ا": {
"relevant": [
{ "code": 1570, "label": "آ" },
{ "code": 1649, "label": "ٱ" },
{ "code": 1569, "label": "ء" },
{ "code": 1571, "label": "أ" },
{ "code": 1573, "label": "إ" }
{ "code": 1573, "label": "إ" },
{ "code": 1570, "label": "آ" }
]
},
"ه": {
"relevant": [
{ "code": 1729, "label": "ہ" },
{ "code": 1728, "label": "ۀ" },
{ "code": 1726, "label": "ھ" }
]
},
"ت": {
@@ -76,8 +34,7 @@
},
"ک": {
"relevant": [
{ "code": 1706, "label": "ڪ"},
{ "code": 1603, "label": "ك" }
{ "code": 1706, "label": "ڪ"}
]
},
"ز": {
@@ -114,9 +71,9 @@
"main": { "code": -255, "label": ".ir"},
"relevant": [
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}

View File

@@ -0,0 +1,138 @@
{
"type": "characters/extended_popups",
"name": "fo",
"authors": [ "BinFlush" ],
"mapping": {
"all": {
"a": {
"main": { "code": 225, "label": "á" },
"relevant": [
{ "code": 224, "label": "à" },
{ "code": 226, "label": "â" },
{ "code": 227, "label": "ã" },
{ "code": 257, "label": "ā" },
{ "code": 229, "label": "å" },
{ "code": 230, "label": "æ" },
{ "code": 228, "label": "ä" }
]
},
"e": {
"relevant": [
{ "code": 233, "label": "é" },
{ "code": 275, "label": "ē" },
{ "code": 281, "label": "ę" },
{ "code": 279, "label": "ė" },
{ "code": 232, "label": "è" },
{ "code": 235, "label": "ë" },
{ "code": 234, "label": "ê" }
]
},
"i": {
"main": { "code": 237, "label": "í" },
"relevant": [
{ "code": 299, "label": "ī" },
{ "code": 236, "label": "ì" },
{ "code": 303, "label": "į" },
{ "code": 238, "label": "î" },
{ "code": 239, "label": "ï" }
]
},
"l": {
"relevant": [
{ "code": 322, "label": "ł" }
]
},
"n": {
"relevant": [
{ "code": 241, "label": "ñ" },
{ "code": 324, "label": "ń" }
]
},
"o": {
"main": { "code": 243, "label": "ó" },
"relevant": [
{ "code": 248, "label": "ø" },
{ "code": 333, "label": "ō" },
{ "code": 339, "label": "œ" },
{ "code": 242, "label": "ò" },
{ "code": 245, "label": "õ" },
{ "code": 244, "label": "ô" },
{ "code": 246, "label": "ö" }
]
},
"t": {
"relevant": [
{ "code": 254, "label": "þ" }
]
},
"s": {
"relevant": [
{ "code": 223, "label": "ß" },
{ "code": 347, "label": "ś" },
{ "code": 353, "label": "š" }
]
},
"u": {
"main": { "code": 250, "label": "ú" },
"relevant": [
{ "code": 363, "label": "ū" },
{ "code": 251, "label": "û" },
{ "code": 252, "label": "ü" },
{ "code": 249, "label": "ù" }
]
},
"y": {
"main": { "code": 253, "label": "ý" },
"relevant": [
{ "code": 255, "label": "ÿ" }
]
},
"æ": {
"relevant": [
{ "code": 228, "label": "ä" }
]
},
"ð": {
"relevant": [
{ "code": 254, "label": "þ" }
]
},
"ø": {
"relevant": [
{ "code": 246, "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": ".fo" },
{ "code": -255, "label": ".dk" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}
}
}
}

View File

@@ -1,7 +1,7 @@
{
"type": "characters/extended_popups",
"name": "hu",
"authors": [ "zoli111" ],
"authors": [ "zoli111, gabik65" ],
"mapping": {
"all": {
"a": {
@@ -26,11 +26,6 @@
{ "code": 337, "label": "ő" }
]
},
"ö": {
"relevant": [
{ "code": 337, "label": "ő" }
]
},
"u": {
"relevant": [
{ "code": 250, "label": "ú" },
@@ -38,11 +33,6 @@
{ "code": 369, "label": "ű" }
]
},
"ü": {
"relevant": [
{ "code": 369, "label": "ű" }
]
},
"~right": {
"main": { "code": 44, "label": "," },
"relevant": [

View File

@@ -0,0 +1,132 @@
{
"type": "characters/extended_popups",
"name": "ku",
"authors": [ "GoRaN" ],
"mapping": {
"all": {
"a": {
"relevant": [
{ "code": 229, "label": "å" },
{ "code": 225, "label": "á" },
{ "code": 226, "label": "â" },
{ "code": 227, "label": "ã" },
{ "code": 257, "label": "ā" },
{ "code": 230, "label": "æ" },
{ "code": 228, "label": "ä" },
{ "code": 224, "label": "à" }
]
},
"c": {
"main": { "code": 231, "label": "ç" },
"relevant": [
{ "code": 269, "label": "č" },
{ "code": 265, "label": "ĉ" },
{ "code": 263, "label": "ć" }
]
},
"e": {
"relevant": [
{ "code": 233, "label": "é" },
{ "code": 275, "label": "ē" },
{ "code": 281, "label": "ę" },
{ "code": 279, "label": "ė" },
{ "code": 234, "label": "ê" },
{ "code": 232, "label": "è" },
{ "code": 235, "label": "ë" }
]
},
"r": {
"main": { "code": 345, "label": "ř" }
},
"g": {
"main": { "code": 285, "label": "ĝ" }
},
"h": {
"main": { "code": 293, "label": "ĥ" }
}
},
"j": {
"main": { "code": 309, "label": "ĵ" }
},
"n": {
"relevant": [
{ "code": 328, "label": "ň" },
{ "code": 241, "label": "ñ" }
]
},
"o": {
"main": { "code": 246, "label": "ö" },
"relevant": [
{ "code": 333, "label": "ō" },
{ "code": 248, "label": "ø" },
{ "code": 243, "label": "ó" },
{ "code": 245, "label": "õ" },
{ "code": 242, "label": "ò" },
{ "code": 339, "label": "œ" },
{ "code": 244, "label": "ô" }
]
},
"s": {
"main": { "code": 219, "label": "ș" },
"relevant": [
{ "code": 347, "label": "ś" },
{ "code": 349, "label": "ŝ" },
{ "code": 353, "label": "š" }
]
},
"u": {
"main": { "code": 251, "label": "û" },
"relevant": [
{ "code": 363, "label": "ū" },
{ "code": 249, "label": "ù" },
{ "code": 250, "label": "ú" },
{ "code": 252, "label": "ü" }
]
},
"y": {
"relevant": [
{ "code": 253, "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": ".krd" },
"relevant": [
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".net" }
]
}
}
}

View File

@@ -21,9 +21,7 @@
]
},
"z": {
"relevant": [
{ "code": 382, "label": "ž" }
]
"main": { "code": 382, "label": "ž" }
},
"~right": {
"main": { "code": 44, "label": "," },
@@ -50,9 +48,9 @@
"~right": {
"main": { "code": -255, "label": ".com" },
"relevant": [
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".eu" },
{ "code": -255, "label": ".rs" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}

View File

@@ -1,46 +1,100 @@
{
"type": "characters/extended_popups",
"name": "tr",
"authors": [ "kisekinopureya" ],
"authors": [ "kisekinopureya", "patrickgold" ],
"mapping": {
"all": {
"a": {
"relevant": [
{ "code": 226, "label": "â" }
{ "code": 226, "label": "â" },
{ "code": 228, "label": "ä" },
{ "code": 225, "label": "á" }
]
},
"c": {
"main": { "code": 231, "label": "ç" },
"relevant": [
{ "code": 231, "label": "ç" }
{ "code": 269, "label": "č" },
{ "code": 263, "label": "ć" }
]
},
"e": {
"relevant": [
{ "code": 233, "label": "é" },
{ "code": 601, "label": "ə" },
{ "code": 234, "label": "ê" }
]
},
"g": {
"relevant": [
{ "code": 287, "label": "ğ" }
]
"main": { "code": 287, "label": "ğ" }
},
"i": {
"main": { "code": 305, "label": "ı" },
"relevant": [
{ "code": 303, "label": "į" },
{ "code": 236, "label": "ì" },
{ "code": 237, "label": "í" },
{ "code": 299, "label": "ī" },
{ "code": 238, "label": "î" },
{ "code": 305, "label": "ı" }
{ "code": 239, "label": "ï" }
]
},
"ı": {
"main": { "code": 105, "label": "i" },
"relevant": [
{ "code": 303, "label": "į" },
{ "code": 236, "label": "ì" },
{ "code": 237, "label": "í" },
{ "code": 299, "label": "ī" },
{ "code": 238, "label": "î" },
{ "code": 239, "label": "ï" }
]
},
"n": {
"relevant": [
{ "code": 328, "label": "ň" },
{ "code": 241, "label": "ñ" }
]
},
"o": {
"main": { "code": 246, "label": "ö" },
"relevant": [
{ "code": 246, "label": "ö" }
{ "code": 333, "label": "ō" },
{ "code": 248, "label": "ø" },
{ "code": 243, "label": "ó" },
{ "code": 245, "label": "õ" },
{ "code": 242, "label": "ò" },
{ "code": 339, "label": "œ" },
{ "code": 244, "label": "ô" }
]
},
"s": {
"main": { "code": 351, "label": "ş" },
"relevant": [
{ "code": 351, "label": "ş" }
{ "code": 347, "label": "ś" },
{ "code": 223, "label": "ß" },
{ "code": 353, "label": "š" }
]
},
"u": {
"main": { "code": 252, "label": "ü" },
"relevant": [
{ "code": 252, "label": "ü" },
{ "code": 363, "label": "ū" },
{ "code": 249, "label": "ù" },
{ "code": 250, "label": "ú" },
{ "code": 251, "label": "û" }
]
},
"y": {
"relevant": [
{ "code": 253, "label": "ý" }
]
},
"z": {
"relevant": [
{ "code": 382, "label": "ž" }
]
},
"~right": {
"main": { "code": 44, "label": "," },
"relevant": [
@@ -67,9 +121,9 @@
"main": { "code": -255, "label": ".com" },
"relevant": [
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".tr" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}

View File

@@ -0,0 +1,44 @@
{
"type": "characters",
"name": "faroese",
"label": "Faroese (QWERTY)",
"authors": [ "BinFlush" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 113, "label": "q" },
{ "code": 119, "label": "w" },
{ "code": 101, "label": "e" },
{ "code": 114, "label": "r" },
{ "code": 116, "label": "t" },
{ "code": 121, "label": "y" },
{ "code": 117, "label": "u" },
{ "code": 105, "label": "i" },
{ "code": 111, "label": "o" },
{ "code": 112, "label": "p" },
{ "code": 240, "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": 230, "label": "æ" },
{ "code": 248, "label": "ø" }
],
[
{ "code": 122, "label": "z" },
{ "code": 120, "label": "x" },
{ "code": 99, "label": "c" },
{ "code": 118, "label": "v" },
{ "code": 98, "label": "b" },
{ "code": 110, "label": "n" },
{ "code": 109, "label": "m" }
]
]
}

View File

@@ -0,0 +1,46 @@
{
"type": "characters",
"name": "german",
"label": "German (QWERTZ)",
"authors": [ "mahmoudk1000" ],
"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": 252, "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": 246, "label": "ö" },
{ "code": 228, "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": 223, "label": "ß", "popup": {
}, "shift": { "code": 7838, "label": "ẞ" } }
]
]
}

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "greek",
"label": "Ελληνικά",
"authors": [ "tsiflimagas" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "hebrew",
"label": "עברית",
"authors": [ "Antony" ],
"direction": "rtl",
"modifier": "hebrew",

View File

@@ -1,7 +1,8 @@
{
"type": "characters",
"name": "hungarian",
"authors": [ "zoli111" ],
"label": "Hungarian (QWERTZ)",
"authors": [ "zoli111, gabik65" ],
"direction": "ltr",
"arrangement": [
[
@@ -14,8 +15,7 @@
{ "code": 117, "label": "u" },
{ "code": 105, "label": "i" },
{ "code": 111, "label": "o" },
{ "code": 112, "label": "p" },
{ "code": 246, "label": "ö" }
{ "code": 112, "label": "p" }
],
[
{ "code": 97, "label": "a" },
@@ -26,9 +26,7 @@
{ "code": 104, "label": "h" },
{ "code": 106, "label": "j" },
{ "code": 107, "label": "k" },
{ "code": 108, "label": "l" },
{ "code": 233, "label": "é" },
{ "code": 225, "label": "á" }
{ "code": 108, "label": "l" }
],
[
{ "code": 121, "label": "y" },
@@ -37,8 +35,7 @@
{ "code": 118, "label": "v" },
{ "code": 98, "label": "b" },
{ "code": 110, "label": "n" },
{ "code": 109, "label": "m" },
{ "code": 252, "label": "ü" }
{ "code": 109, "label": "m" }
]
]
}

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "icelandic",
"label": "Icelandic (QWERTY)",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -0,0 +1,285 @@
{
"type": "characters",
"name": "ipa",
"label": "International Phonetic Alphabet",
"authors": [
"Huy-Ngo"
],
"direction": "ltr",
"arrangement": [
[
{
"code": 113, "label": "q"
},
{
"code": 119, "label": "w",
"popup": {
"relevant": [
{ "code": 695, "label": "◌ʷ" },
{ "code": 653, "label": "ʍ" }
]
}
},
{
"code": 101, "label": "e",
"popup": {
"relevant": [
{ "code": 600, "label": "ɘ" },
{ "code": 604, "label": "ɜ" },
{ "code": 601, "label": "ə" },
{ "code": 602, "label": "ɚ" },
{ "code": 7498, "label": "◌ᵊ" },
{ "code": 603, "label": "ɛ" }
]
}
},
{
"code": 114, "label": "r",
"popup": {
"relevant": [
{ "code": 637, "label": "ɽ" },
{ "code": 633, "label": "ɹ" },
{ "code": 638, "label": "ɾ" },
{ "code": 635, "label": "ɻ" },
{ "code": 641, "label": "ʁ" },
{ "code": 734, "label": "◌˞" },
{ "code": 640, "label": "ʀ" }
]
}
},
{
"code": 116, "label": "t",
"popup": {
"relevant": [
{ "code": 648, "label": "ʈ" },
{ "code": 7615, "label": "◌ᶿ" },
{ "code": 952, "label": "θ" }
]
}
},
{
"code": 121, "label": "y",
"popup": {
"relevant": [
{ "code": 612, "label": "ɤ" },
{ "code": 655, "label": "ʏ" }
]
}
},
{
"code": 117, "label": "u",
"popup": {
"relevant": [
{ "code": 7551, "label": "ᵿ" },
{ "code": 649, "label": "ʉ" },
{ "code": 650, "label": "ʊ" }
]
}
},
{
"code": 105, "label": "i",
"popup": {
"relevant": [
{ "code": 7574, "label": "ᵻ" },
{ "code": 616, "label": "ɨ" },
{ "code": 618, "label": "ɪ" }
]
}
},
{
"code": 111, "label": "o",
"popup": {
"relevant": [
{ "code": 664, "label": "ʘ" },
{ "code": 248, "label": "ø" },
{ "code": 606, "label": "ɞ" },
{ "code": 339, "label": "œ" },
{ "code": 629, "label": "ɵ" },
{ "code": 630, "label": "ɶ" },
{ "code": 596, "label": "ɔ" }
]
}
},
{
"code": 112, "label": "p"
}
],
[
{
"code": 97, "label": "a",
"popup": {
"relevant": [
{ "code": 230, "label": "æ" },
{ "code": 594, "label": "ɒ" },
{ "code": 592, "label": "ɐ" },
{ "code": 593, "label": "ɑ" }
]
}
},
{
"code": 115, "label": "s",
"popup": {
"relevant": [
{ "code": 642, "label": "ʂ" },
{ "code": 597, "label": "ɕ" },
{ "code": 643, "label": "ʃ" }
]
}
},
{
"code": 100, "label": "d",
"popup": {
"relevant": [
{ "code": 598, "label": "ɖ" },
{ "code": 599, "label": "ɗ" },
{ "code": 240, "label": "ð" }
]
}
},
{
"code": 102, "label": "f",
"popup": {
"relevant": [
{ "code": 632, "label": "ɸ" }
]
}
},
{
"code": 609, "label": "ɡ",
"popup": {
"main": { "code": 103, "label": "g" },
"relevant": [
{ "code": 608, "label": "ɠ" },
{ "code": 610, "label": "ɢ" },
{ "code": 667, "label": "ʛ" },
{ "code": 667, "label": "ʛ" },
{ "code": 736, "label": "◌ˠ" },
{ "code": 611, "label": "ɣ" }
]
}
},
{
"code": 104, "label": "h",
"popup": {
"relevant": [
{ "code": 614, "label": "ɦ" },
{ "code": 615, "label": "ɧ" },
{ "code": 295, "label": "ħ" },
{ "code": 613, "label": "ɥ" },
{ "code": 688, "label": "◌ʰ" },
{ "code": 668, "label": "ʜ" }
]
}
},
{
"code": 106, "label": "j",
"popup": {
"relevant": [
{ "code": 668, "label": "ʝ" },
{ "code": 607, "label": "ɟ" },
{ "code": 690, "label": "◌ʲ" },
{ "code": 664, "label": "ʄ" }
]
}
},
{
"code": 107, "label": "k"
},
{
"code": 108, "label": "l",
"popup": {
"relevant": [
{ "code": 620, "label": "ɬ" },
{ "code": 634, "label": "ɺ" },
{ "code": 671, "label": "ʟ" },
{ "code": 654, "label": "ʎ" },
{ "code": 737, "label": "◌ˡ" },
{ "code": 622, "label": "ɮ" }
]
}
},
{
"code": 660, "label": "ʔ",
"popup": {
"relevant": [
{ "code": 661, "label": "ʕ" },
{ "code": 674, "label": "ʢ" },
{ "code": 740, "label": "◌ˤ" },
{ "code": 673, "label": "ʡ" }
]
}
}
],
[
{
"code": 122, "label": "z",
"popup": {
"relevant": [
{ "code": 656, "label": "ʐ" },
{ "code": 657, "label": "ʑ" },
{ "code": 658, "label": "ʒ" }
]
}
},
{
"code": 120, "label": "x",
"popup": {
"relevant": [
{ "code": 739, "label": "◌ˣ" },
{ "code": 967, "label": "χ" }
]
}
},
{
"code": 99, "label": "c",
"popup": {
"relevant": [
{ "code": 231, "label": "ç" }
]
}
},
{
"code": 118, "label": "v",
"popup": {
"relevant": [
{ "code": 651, "label": "ʋ" },
{ "code": 652, "label": "ʌ" }
]
}
},
{
"code": 98, "label": "b",
"popup": {
"relevant": [
{ "code": 595, "label": "ɓ" },
{ "code": 665, "label": "ʙ" },
{ "code": 946, "label": "β" }
]
}
},
{
"code": 110, "label": "n",
"popup": {
"relevant": [
{ "code": 626, "label": "ɲ" },
{ "code": 627, "label": "ɳ" },
{ "code": 628, "label": "ɴ" },
{ "code": 8319, "label": "◌ⁿ" },
{ "code": 771, "label": "◌̃" },
{ "code": 631, "label": "ŋ" }
]
}
},
{
"code": 109, "label": "m",
"popup": {
"relevant": [
{ "code": 625, "label": "ɱ" },
{ "code": 624, "label": "ɰ" },
{ "code": 623, "label": "ɯ" }
]
}
}
]
]
}

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "jcuken_russian",
"label": "Russian (JCUKEN)",
"authors": [ "williamtheaker" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "kurdish",
"label": "کوردی",
"authors": [ "GoRaN" ],
"direction": "rtl",
"modifier": "kurdish",
@@ -12,19 +13,21 @@
{ "code": 1608, "label": "و", "popup": {
"main": { "code": -255, "label": "وو" }
} },
{ "code": 1749, "label": "", "popup": {
"main": { "code": 1577, "label": "ة" }
} },
{ "code": 1749, "label": "" },
{ "code": 1585, "label": "ر" },
{ "code": 1578, "label": "ت", "popup": {
"main": { "code": 1591, "label": "ط" }
} },
{ "code": 1740, "label": "ی" },
{ "code": 1574, "label": "ﺋ", "popup": {
"main": { "code": 1569, "label": "ء" }
{ "code": 1574, "label": ""},
{ "code": 1593, "label": "ع", "popup": {
"main": { "code": 1594, "label": "غ" }
} },
{ "code": 1593, "label": "ع" },
{ "code": 1734, "label": "ۆ" },
{ "code": 1662, "label": "پ", "popup": {
"main": { "code": 1579, "label": "ث" }
} }

View File

@@ -0,0 +1,46 @@
{
"type": "characters",
"name": "kurdish_kurmanci",
"label": "Kurdî",
"authors": [ "GoRaN" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 113, "label": "q" },
{ "code": 119, "label": "w" },
{ "code": 101, "label": "e" },
{ "code": 114, "label": "r" },
{ "code": 116, "label": "t" },
{ "code": 121, "label": "y" },
{ "code": 117, "label": "u" },
{ "code": 305, "label": "ı" },
{ "code": 111, "label": "o" },
{ "code": 112, "label": "p" },
{ "code": 251, "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": 234, "label": "ê" },
{ "code": 238, "label": "î" }
],
[
{ "code": 122, "label": "z" },
{ "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": 231, "label": "ç" },
{ "code": 351, "label": "ş" }
]
]
}

View File

@@ -0,0 +1,72 @@
{
"type": "characters",
"name": "kurdish_standard",
"label": "کوردی - ستاندارد",
"authors": [ "GoRaN" ],
"direction": "rtl",
"modifier": "kurdish",
"arrangement": [
[
{ "code": 1602, "label": "ق", "popup": {
"main": { "code": 1647, "label": "ٯ" }
} },
{ "code": 1700, "label": "ڤ", "popup": {
"main": { "code": 1701, "label": "ڥ" }
} },
{ "code": 1601, "label": "ف", "popup": {
"main": { "code": 1698, "label": "ڢ" }
} },
{ "code": 1594, "label": "غ" },
{ "code": 1593, "label": "ع"},
{ "code": 1607, "label": "ھ" },
{ "code": 1749, "label": "" },
{ "code": 1578, "label": "ت", "popup": {
"main": { "code": 1591, "label": "ط" }
} },
{ "code": 1581, "label": "ح" },
{ "code": 1582, "label": "خ" }
],
[
{ "code": 1588, "label": "ش" },
{ "code": 1587, "label": "س" },
{ "code": 1740, "label": "ی" },
{ "code": 1742, "label": "ێ" },
{ "code": 1604, "label": "ل" },
{ "code": 1717, "label": "ڵ" },
{ "code": 1575, "label": "ا" },
{ "code": 1606, "label": "ن" },
{ "code": 1580, "label": "ج" },
{ "code": 1670, "label": "چ" }
],
[
{ "code": 1576, "label": "ب" },
{ "code": 1586, "label": "ز", "popup": {
"main": {"code": 1592, "label": "ظ" }
} },
{ "code": 1585, "label": "ر" },
{ "code": 1685, "label": "ڕ" },
{ "code": 1583, "label": "د" },
{ "code": -255, "label": "وو" },
{ "code": 1608, "label": "و" },
{ "code": 1734, "label": "ۆ" },
{ "code": 1705, "label": "ک" },
{ "code": 1711, "label": "گ" }
],
[
{ "code": 1600, "label": "kashida", "variation": "normal" },
{ "code": 1574, "label": "ﺋ"},
{ "code": 1662, "label": "پ", "popup": {
"main": { "code": 1579, "label": "ث" }
} },
{ "code": 1688, "label": "ژ" },
{ "code": 1605, "label": "م" },
{ "code": 1567, "label": "؟" },
{ "code": 1548, "label": "،" },
{ "code": 46, "label": "." }
]
]
}

View File

@@ -1,6 +1,7 @@
{
"type": "characters/mod",
"name": "default",
"name": "$default",
"label": "Default",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters/mod",
"name": "arabic",
"label": "Arabic",
"authors": [ "HeiWiper" ],
"direction": "rtl",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters/mod",
"name": "dvorak",
"label": "Dvorak",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters/mod",
"name": "hebrew",
"label": "עברית",
"authors": [ "Antony" ],
"direction": "rtl",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters/mod",
"name": "kurdish",
"label": "کوردی",
"authors": [ "GoRaN" ],
"direction": "rtl",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters/mod",
"name": "persian",
"label": "Persian",
"authors": [ "PHELAT" ],
"direction": "rtl",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "norwegian",
"label": "Norwegian (QWERTY)",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "persian",
"label": "Persian",
"authors": [ "PHELAT" ],
"direction": "rtl",
"modifier": "persian",

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "qwerty",
"label": "QWERTY",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "qwertz",
"label": "QWERTZ",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "serbian_cyrillic",
"label": "Serbian (ЉЊЕРТЗ)",
"authors": ["GrbavaCigla"],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "serbian_latin",
"label": "Serbian (QWERTZ)",
"authors": ["GrbavaCigla"],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "spanish",
"label": "Spanish (QWERTY)",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "swedish_finnish",
"label": "Swedish/Finnish (QWERTY)",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "swiss_french",
"label": "Swiss French (QWERTZ)",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "swiss_german",
"label": "Swiss German (QWERTZ)",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "characters",
"name": "swiss_italian",
"label": "Swiss Italian (QWERTZ)",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -0,0 +1,47 @@
{
"type": "characters",
"name": "turkish_f",
"label": "Turkish-F",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 102, "label": "f" },
{ "code": 103, "label": "g" },
{ "code": 287, "label": "ğ" },
{ "code": 305, "label": "ı" },
{ "code": 111, "label": "o" },
{ "code": 100, "label": "d" },
{ "code": 114, "label": "r" },
{ "code": 110, "label": "n" },
{ "code": 104, "label": "h" },
{ "code": 112, "label": "p" },
{ "code": 113, "label": "q" },
{ "code": 119, "label": "w" }
],
[
{ "code": 117, "label": "u" },
{ "code": 105, "label": "i" },
{ "code": 101, "label": "e" },
{ "code": 97, "label": "a" },
{ "code": 252, "label": "ü" },
{ "code": 116, "label": "t" },
{ "code": 107, "label": "k" },
{ "code": 109, "label": "m" },
{ "code": 108, "label": "l" },
{ "code": 121, "label": "y" },
{ "code": 351, "label": "ş" }
],
[
{ "code": 106, "label": "j" },
{ "code": 246, "label": "ö" },
{ "code": 118, "label": "v" },
{ "code": 99, "label": "c" },
{ "code": 231, "label": "ç" },
{ "code": 122, "label": "z" },
{ "code": 115, "label": "s" },
{ "code": 98, "label": "b" },
{ "code": 120, "label": "x" }
]
]
}

View File

@@ -0,0 +1,47 @@
{
"type": "characters",
"name": "turkish_q",
"label": "Turkish-Q",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 113, "label": "q" },
{ "code": 119, "label": "w" },
{ "code": 101, "label": "e" },
{ "code": 114, "label": "r" },
{ "code": 116, "label": "t" },
{ "code": 121, "label": "y" },
{ "code": 117, "label": "u" },
{ "code": 305, "label": "ı" },
{ "code": 111, "label": "o" },
{ "code": 112, "label": "p" },
{ "code": 287, "label": "ğ" },
{ "code": 252, "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": 351, "label": "ş" },
{ "code": 105, "label": "i" }
],
[
{ "code": 122, "label": "z" },
{ "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": 246, "label": "ö" },
{ "code": 231, "label": "ç" }
]
]
}

View File

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

View File

@@ -1,6 +1,7 @@
{
"type": "extension",
"name": "clipboard_cursor_row",
"label": "Clipboard Cursor Row",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [
@@ -10,7 +11,8 @@
{ "code": -20, "label": "arrow_left", "type": "navigation" },
{ "code": -21, "label": "arrow_right", "type": "navigation" },
{ "code": -131, "label": "clipboard_cut", "type": "enter_editing" },
{ "code": -132, "label": "clipboard_paste", "type": "enter_editing" }
{ "code": -132, "label": "clipboard_paste", "type": "enter_editing" },
{ "code": -214, "label": "switch_to_clipboard_context", "type": "system_gui"}
]
]
}

View File

@@ -1,6 +1,7 @@
{
"type": "numeric_advanced",
"name": "default",
"name": "western_arabic",
"label": "Western Arabic",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "eastern_arabic",
"label": "Eastern Arabic",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 1633, "label": "١", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 1634, "label": "٢", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 1635, "label": "٣", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 1636, "label": "٤", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 1637, "label": "٥", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 1638, "label": "٦", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 1639, "label": "٧", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 1640, "label": "٨", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 1641, "label": "٩", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 1632, "label": "٠", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "persian",
"label": "Persian",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 1777, "label": "۱", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 1778, "label": "۲", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 1779, "label": "۳", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 1780, "label": "۴", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 1781, "label": "۵", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 1782, "label": "۶", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 1783, "label": "۷", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 1784, "label": "۸", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 1785, "label": "۹", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 1776, "label": "۰", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -1,6 +1,7 @@
{
"type": "extension",
"name": "number_row",
"type": "numeric_row",
"name": "western_arabic",
"label": "Western Arabic",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "numeric",
"name": "default",
"name": "western_arabic",
"label": "Western Arabic",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "phone",
"name": "default",
"name": "telpad",
"label": "Telephone Pad",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -1,6 +1,7 @@
{
"type": "phone2",
"name": "default",
"name": "telpad",
"label": "Telephone Pad",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -0,0 +1,102 @@
{
"type": "symbols",
"name": "eastern",
"label": "Eastern",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 64, "label": "@" },
{ "code": 35, "label": "#", "popup": {
"main": { "code": 8470, "label": "№" }
} },
{ "code": -801, "label": "currency_slot_1", "popup": {
"main": { "code": -802, "label": "currency_slot_2" },
"relevant": [
{ "code": -806, "label": "currency_slot_6" },
{ "code": -803, "label": "currency_slot_3" },
{ "code": -804, "label": "currency_slot_4" },
{ "code": -805, "label": "currency_slot_5" }
]
} },
{ "code": 1642, "label": "٪", "popup": {
"main": { "code": 37, "label": "%" },
"relevant": [
{ "code": 8453, "label": "℅" },
{ "code": 8240, "label": "‰" }
]
} },
{ "code": 38, "label": "&" },
{ "code": 45, "label": "-", "popup": {
"main": { "code": 95, "label": "_" },
"relevant": [
{ "code": 8212, "label": "—" },
{ "code": 8211, "label": "" },
{ "code": 183, "label": "·" }
]
} },
{ "code": 43, "label": "+", "popup": {
"main": { "code": 177, "label": "±" }
} },
{ "code": 40, "label": "(", "popup": {
"main": { "code":64830, "label": "" },
"relevant": [
{ "code": 91, "label": "[" },
{ "code": 60, "label": "<" },
{ "code": 123, "label": "{" }
]
} },
{ "code": 41, "label": ")", "popup": {
"main": { "code":64831, "label": "﴿" },
"relevant": [
{ "code": 93, "label": "]" },
{ "code": 62, "label": ">" },
{ "code": 125, "label": "}" }
]
} },
{ "code": 47, "label": "/" }
],
[
{ "code": 42, "label": "*", "popup": {
"main": { "code": 9733, "label": "★" },
"relevant": [
{ "code": 1645, "label": "٭" }
]
} },
{ "code": 34, "label": "\"", "popup": {
"main": { "code": 8221, "label": "”" },
"relevant": [
{ "code": 8222, "label": "„" },
{ "code": 8220, "label": "“" },
{ "code": 171, "label": "«" },
{ "code": 187, "label": "»" }
]
} },
{ "code": 39, "label": "'", "popup": {
"main": { "code": 8217, "label": "" },
"relevant": [
{ "code": 8218, "label": "" },
{ "code": 8216, "label": "" },
{ "code": 8249, "label": "" },
{ "code": 8250, "label": "" }
]
} },
{ "code": 58, "label": ":", "popup": {
"main": { "code": 8942, "label": "⋮" }
} },
{ "code": 1563, "label": "؛", "popup": {
"main": { "code": 59, "label": ";" }
} },
{ "code": 33, "label": "!", "popup": {
"main": { "code": 161, "label": "¡" }
} },
{ "code": 1567, "label": "؟", "popup": {
"main": { "code": 63, "label": "?" },
"relevant": [
{ "code": 191, "label": "¿" },
{ "code": 8253, "label": "‽" }
]
} }
]
]
}

View File

@@ -0,0 +1,97 @@
{
"type": "symbols",
"name": "ipa",
"label": "International Phonetic Alphabet",
"authors": [ "Huy-Ngo" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 712, "label": "ˈ", "popup": {
"main": { "code": 716, "label": "ˌ" }
} },
{ "code": 720, "label": "ː", "popup": {
"main": { "code": 721, "label": "ˑ" },
"relevant": [
{ "code": 774, "label": "◌̆" }
]
} },
{ "code": 8599, "label": "↗︎", "popup": {
"main": { "code": 42779, "label": "ꜛ" }
} },
{ "code": 8600, "label": "↘︎", "popup": {
"main": { "code": 42780, "label": "ꜜ" }
} },
{ "code": 745, "label": "˩", "popup": {
"main": { "code": 783, "label": "◌̏" }
} },
{ "code": 744, "label": "˨", "popup": {
"main": { "code": 768, "label": "◌̀" },
"relevant": [
{ "code": 780, "label": "◌̌" }
]
} },
{ "code": 743, "label": "˧", "popup": {
"main": { "code": 772, "label": "◌̄" }
} },
{ "code": 742, "label": "˦", "popup": {
"main": { "code": 769, "label": "◌́" },
"relevant": [
{ "code": 770, "label": "◌̂" }
]
} },
{ "code": 741, "label": "˥", "popup": {
"main": { "code": 779, "label": "◌̋" }
} },
{ "code": 865, "label": "◌͡", "popup": {
"main": { "code": 8255, "label": "‿" }
} }
],
[
{ "code": 776, "label": "◌̈" },
{ "code": 825, "label": "◌̹", "popup": {
"main": { "code": 855, "label": "◌͗" },
"relevant": [
{ "code": 796, "label": "◌̜" },
{ "code": 849, "label": "◌͑" }
]
} },
{ "code": 800, "label": "◌̠", "popup": {
"main": { "code": 727, "label": "◌˗" }
} },
{ "code": 771, "label": "◌̃", "popup": {
"main": { "code": 820, "label": "◌̴" },
"relevant": [
{ "code": 734, "label": "◌˞" }
]
} },
{ "code": 91, "label": "[", "popup": {
"relevant": [
{ "code": 40, "label": "(" },
{ "code": 11816, "label": "⸨" },
{ "code": 10214, "label": "⟦" },
{ "code": 10216, "label": "⟨" },
{ "code": 10218, "label": "⟩" },
{ "code": 123, "label": "{" }
]
} },
{ "code": 93, "label": "]", "popup": {
"relevant": [
{ "code": 41, "label": ")" },
{ "code": 11817, "label": "⸩" },
{ "code": 10215, "label": "⟦" },
{ "code": 10217, "label": "⟩" },
{ "code": 10219, "label": "⟫" },
{ "code": 125, "label": "}" }
]
} },
{ "code": 47, "label": "/", "popup": {
"main": { "code": 92, "label": "\\" },
"relevant": [
{ "code": 11005, "label": "⫽" },
{ "code": 8214, "label": "‖" },
{ "code": 124, "label": "|" }
]
} }
]
]
}

View File

@@ -1,6 +1,7 @@
{
"type": "symbols/mod",
"name": "default",
"name": "$default",
"label": "Default",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -0,0 +1,102 @@
{
"type": "symbols",
"name": "persian",
"label": "Persian",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 64, "label": "@" },
{ "code": 35, "label": "#", "popup": {
"main": { "code": 8470, "label": "№" }
} },
{ "code": -801, "label": "currency_slot_1", "popup": {
"main": { "code": -802, "label": "currency_slot_2" },
"relevant": [
{ "code": -806, "label": "currency_slot_6" },
{ "code": -803, "label": "currency_slot_3" },
{ "code": -804, "label": "currency_slot_4" },
{ "code": -805, "label": "currency_slot_5" }
]
} },
{ "code": 1642, "label": "٪", "popup": {
"main": { "code": 37, "label": "%" },
"relevant": [
{ "code": 8453, "label": "℅" },
{ "code": 8240, "label": "‰" }
]
} },
{ "code": 38, "label": "&" },
{ "code": 45, "label": "-", "popup": {
"main": { "code": 95, "label": "_" },
"relevant": [
{ "code": 8212, "label": "—" },
{ "code": 8211, "label": "" },
{ "code": 183, "label": "·" }
]
} },
{ "code": 43, "label": "+", "popup": {
"main": { "code": 177, "label": "±" }
} },
{ "code": 40, "label": "(", "popup": {
"main": { "code":64830, "label": "" },
"relevant": [
{ "code": 91, "label": "[" },
{ "code": 60, "label": "<" },
{ "code": 123, "label": "{" }
]
} },
{ "code": 41, "label": ")", "popup": {
"main": { "code":64831, "label": "﴿" },
"relevant": [
{ "code": 93, "label": "]" },
{ "code": 62, "label": ">" },
{ "code": 125, "label": "}" }
]
} },
{ "code": 1643, "label": "٫", "popup": {
"main": { "code": 1644, "label": "٬" },
"relevant": [
{ "code": 42, "label": "*" },
{ "code": 34, "label": "\"" },
{ "code": 39, "label": "'" }
]
} }
],
[
{ "code": 47, "label": "/" },
{ "code": 171, "label": "«", "popup": {
"main": { "code": 8249, "label": "" },
"relevant": [
{ "code": 60, "label": "<" },
{ "code": 8804, "label": "≤" },
{ "code":10216, "label": "⟨" }
]
} },
{ "code": 187, "label": "»", "popup": {
"main": { "code": 8250, "label": "" },
"relevant": [
{ "code":10217, "label": "⟩" },
{ "code": 8805, "label": "≥" },
{ "code": 62, "label": ">" }
]
} },
{ "code": 58, "label": ":", "popup": {
"main": { "code": 8942, "label": "⋮" }
} },
{ "code": 1563, "label": "؛", "popup": {
"main": { "code": 59, "label": ";" }
} },
{ "code": 33, "label": "!", "popup": {
"main": { "code": 161, "label": "¡" }
} },
{ "code": 1567, "label": "؟", "popup": {
"main": { "code": 63, "label": "?" },
"relevant": [
{ "code": 191, "label": "¿" },
{ "code": 8253, "label": "‽" }
]
} }
]
]
}

View File

@@ -1,6 +1,7 @@
{
"type": "symbols",
"name": "western_default",
"name": "western",
"label": "Western",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [
@@ -9,13 +10,13 @@
{ "code": 35, "label": "#", "popup": {
"main": { "code": 8470, "label": "№" }
} },
{ "code": 36, "label": "$", "popup": {
"main": { "code": 8364, "label": "" },
{ "code": -801, "label": "currency_slot_1", "popup": {
"main": { "code": -802, "label": "currency_slot_2" },
"relevant": [
{ "code": 8369, "label": "" },
{ "code": 162, "label": "¢" },
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
{ "code": -806, "label": "currency_slot_6" },
{ "code": -803, "label": "currency_slot_3" },
{ "code": -804, "label": "currency_slot_4" },
{ "code": -805, "label": "currency_slot_5" }
]
} },
{ "code": 37, "label": "%", "popup": {

View File

@@ -1,6 +1,7 @@
{
"type": "symbols2",
"name": "western_default",
"name": "eastern",
"label": "Eastern",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [
@@ -36,10 +37,10 @@
{ "code": 8710, "label": "∆" }
],
[
{ "code": 163, "label": "£" },
{ "code": 162, "label": "¢" },
{ "code": 8364, "label": "" },
{ "code": 165, "label": "¥" },
{ "code": -805, "label": "currency_slot_5" },
{ "code": -804, "label": "currency_slot_4" },
{ "code": -803, "label": "currency_slot_3" },
{ "code": -802, "label": "currency_slot_2" },
{ "code": 94, "label": "^", "popup": {
"main": { "code": 8593, "label": "↑" },
"relevant": [

View File

@@ -0,0 +1,117 @@
{
"type": "symbols2",
"name": "ipa",
"label": "International Phonetic Alphabet",
"authors": [ "Huy-Ngo" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 809, "label": "◌̩", "popup": {
"main": { "code": 781, "label": "◌̍" }
} },
{ "code": 815, "label": "◌̯", "popup": {
"main": { "code": 785, "label": "◌̑" }
} },
{ "code": 794, "label": "◌̚" },
{ "code": 805, "label": "◌̥", "popup": {
"main": { "code": 778, "label": "◌̊" }
} },
{ "code": 828, "label": "◌̼" },
{ "code": 827, "label": "◌̻" },
{ "code": 804, "label": "◌̤" },
{ "code": 797, "label": "◌̝", "popup": {
"main": { "code": 724, "label": "◌˔" },
"relevant": [
{ "code": 798, "label": "◌̞" },
{ "code": 725, "label": "◌˕" },
{ "code": 792, "label": "◌̘" },
{ "code": 793, "label": "◌̙" }
]
} },
{ "code": 812, "label": "◌̬" },
{ "code": 829, "label": "◌̽" },
{ "code": 826, "label": "◌̺" }
],
[
{ "code": 816, "label": "◌̰" },
{ "code": 810, "label": "◌̪", "popup": {
"main": { "code": 838, "label": "◌͆" }
} },
{ "code": 826, "label": "◌̺" },
{ "code": 799, "label": "◌̟", "popup": {
"main": { "code": 726, "label": "◌˖" }
} },
{ "code": 64, "label": "@" },
{ "code": 35, "label": "#", "popup": {
"main": { "code": 8470, "label": "№" }
} },
{ "code": -801, "label": "currency_slot_1", "popup": {
"main": { "code": -802, "label": "currency_slot_2" },
"relevant": [
{ "code": -806, "label": "currency_slot_6" },
{ "code": -803, "label": "currency_slot_3" },
{ "code": -804, "label": "currency_slot_4" },
{ "code": -805, "label": "currency_slot_5" }
]
} },
{ "code": 37, "label": "%", "popup": {
"main": { "code": 8240, "label": "‰" },
"relevant": [
{ "code": 8453, "label": "℅" }
]
} },
{ "code": 38, "label": "&" },
{ "code": 45, "label": "-", "popup": {
"main": { "code": 95, "label": "_" },
"relevant": [
{ "code": 8212, "label": "—" },
{ "code": 8211, "label": "" },
{ "code": 183, "label": "·" }
]
} },
{ "code": 43, "label": "+", "popup": {
"main": { "code": 177, "label": "±" }
} }
],
[
{ "code": 42, "label": "*", "popup": {
"main": { "code": 664, "label": "ʘ" },
"relevant": [
{ "code": 450, "label": "ǂ" }
]
} },
{ "code": 34, "label": "\"", "popup": {
"main": { "code": 8221, "label": "”" },
"relevant": [
{ "code": 8222, "label": "„" },
{ "code": 8220, "label": "“" },
{ "code": 171, "label": "«" },
{ "code": 187, "label": "»" }
]
} },
{ "code": 39, "label": "'", "popup": {
"main": { "code": 700, "label": "ʼ" },
"relevant": [
{ "code": 8218, "label": "" },
{ "code": 8216, "label": "" },
{ "code": 8217, "label": "" },
{ "code": 8249, "label": "" },
{ "code": 8250, "label": "" }
]
} },
{ "code": 58, "label": ":", "popup": {
"main": { "code": 8942, "label": "⋮" }
} },
{ "code": 59, "label": ";" },
{ "code": 33, "label": "!", "popup": {
"main": { "code": 161, "label": "¡" }
} },
{ "code": 63, "label": "?", "popup": {
"main": { "code": 191, "label": "¿" },
"relevant": [
{ "code": 8253, "label": "‽" }
]
} }
]
]
}

View File

@@ -1,6 +1,7 @@
{
"type": "symbols2/mod",
"name": "default",
"name": "$default",
"label": "Default",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [

View File

@@ -0,0 +1,83 @@
{
"type": "symbols2",
"name": "persian",
"label": "Persian",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 126, "label": "~" },
{ "code": 96, "label": "`" },
{ "code": 124, "label": "|" },
{ "code": 8226, "label": "•", "popup": {
"main": { "code": 9834, "label": "♪" },
"relevant": [
{ "code": 9827, "label": "♣" },
{ "code": 9824, "label": "♠" },
{ "code": 9829, "label": "♥" },
{ "code": 9830, "label": "♦" }
]
} },
{ "code": 8730, "label": "√" },
{ "code": 960, "label": "π", "popup": {
"main": { "code": 928, "label": "Π" },
"relevant": [
{ "code": 969, "label": "ω" },
{ "code": 945, "label": "α" },
{ "code": 946, "label": "β" },
{ "code": 937, "label": "Ω" },
{ "code": 956, "label": "μ" }
]
} },
{ "code": 247, "label": "÷" },
{ "code": 215, "label": "×" },
{ "code": 182, "label": "¶", "popup": {
"main": { "code": 167, "label": "§" }
} },
{ "code": 8710, "label": "∆" }
],
[
{ "code": -805, "label": "currency_slot_5" },
{ "code": -804, "label": "currency_slot_4" },
{ "code": -803, "label": "currency_slot_3" },
{ "code": -802, "label": "currency_slot_2" },
{ "code": 94, "label": "^", "popup": {
"main": { "code": 8593, "label": "↑" },
"relevant": [
{ "code": 8592, "label": "←" },
{ "code": 8595, "label": "↓" },
{ "code": 8594, "label": "→" }
]
} },
{ "code": 176, "label": "°", "popup": {
"main": { "code": 8242, "label": "" },
"relevant": [
{ "code": 8243, "label": "″" }
]
} },
{ "code": 61, "label": "=", "popup": {
"main": { "code": 8800, "label": "≠" },
"relevant": [
{ "code": 8734, "label": "∞" },
{ "code": 8776, "label": "≈" }
]
} },
{ "code": 123, "label": "{", "popup": {
"main": { "code": 40, "label": "(" }
} },
{ "code": 125, "label": "}", "popup": {
"main": { "code": 41, "label": ")" }
} },
{ "code": 92, "label": "\\" }
],
[
{ "code": 95, "label": "_" },
{ "code": 169, "label": "©" },
{ "code": 174, "label": "®" },
{ "code": 8482, "label": "™" },
{ "code": 10003, "label": "✓" },
{ "code": 91, "label": "[" },
{ "code": 93, "label": "]" }
]
]
}

View File

@@ -0,0 +1,83 @@
{
"type": "symbols2",
"name": "western",
"label": "Western",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 126, "label": "~" },
{ "code": 96, "label": "`" },
{ "code": 124, "label": "|" },
{ "code": 8226, "label": "•", "popup": {
"main": { "code": 9834, "label": "♪" },
"relevant": [
{ "code": 9827, "label": "♣" },
{ "code": 9824, "label": "♠" },
{ "code": 9829, "label": "♥" },
{ "code": 9830, "label": "♦" }
]
} },
{ "code": 8730, "label": "√" },
{ "code": 960, "label": "π", "popup": {
"main": { "code": 928, "label": "Π" },
"relevant": [
{ "code": 969, "label": "ω" },
{ "code": 945, "label": "α" },
{ "code": 946, "label": "β" },
{ "code": 937, "label": "Ω" },
{ "code": 956, "label": "μ" }
]
} },
{ "code": 247, "label": "÷" },
{ "code": 215, "label": "×" },
{ "code": 182, "label": "¶", "popup": {
"main": { "code": 167, "label": "§" }
} },
{ "code": 8710, "label": "∆" }
],
[
{ "code": -805, "label": "currency_slot_5" },
{ "code": -804, "label": "currency_slot_4" },
{ "code": -803, "label": "currency_slot_3" },
{ "code": -802, "label": "currency_slot_2" },
{ "code": 94, "label": "^", "popup": {
"main": { "code": 8593, "label": "↑" },
"relevant": [
{ "code": 8592, "label": "←" },
{ "code": 8595, "label": "↓" },
{ "code": 8594, "label": "→" }
]
} },
{ "code": 176, "label": "°", "popup": {
"main": { "code": 8242, "label": "" },
"relevant": [
{ "code": 8243, "label": "″" }
]
} },
{ "code": 61, "label": "=", "popup": {
"main": { "code": 8800, "label": "≠" },
"relevant": [
{ "code": 8734, "label": "∞" },
{ "code": 8776, "label": "≈" }
]
} },
{ "code": 123, "label": "{", "popup": {
"main": { "code": 40, "label": "(" }
} },
{ "code": 125, "label": "}", "popup": {
"main": { "code": 41, "label": ")" }
} },
{ "code": 92, "label": "\\" }
],
[
{ "code": 95, "label": "_" },
{ "code": 169, "label": "©" },
{ "code": 174, "label": "®" },
{ "code": 8482, "label": "™" },
{ "code": 10003, "label": "✓" },
{ "code": 91, "label": "[" },
{ "code": 93, "label": "]" }
]
]
}

View File

@@ -68,6 +68,7 @@
"extractActionButton": {
"background": "@smartbarButton/background",
"foreground": "@smartbarButton/foreground"
}
},
"glideTrail": {"foreground": "#20388E3C"}
}
}

View File

@@ -71,6 +71,7 @@
"extractActionButton": {
"background": "@smartbarButton/background",
"foreground": "@smartbarButton/foreground"
}
},
"glideTrail": {"foreground": "#20388E3C"}
}
}

View File

@@ -68,6 +68,7 @@
"extractActionButton": {
"background": "@smartbarButton/background",
"foreground": "@smartbarButton/foreground"
}
},
"glideTrail": {"foreground": "#204CAF50"}
}
}

View File

@@ -72,6 +72,7 @@
"extractActionButton": {
"background": "@smartbarButton/background",
"foreground": "@smartbarButton/foreground"
}
},
"glideTrail": {"foreground": "#204CAF50"}
}
}

View File

@@ -68,6 +68,7 @@
"extractActionButton": {
"background": "@smartbarButton/background",
"foreground": "@smartbarButton/foreground"
}
},
"glideTrail": {"foreground": "#204CAF50"}
}
}

View File

@@ -72,6 +72,7 @@
"extractActionButton": {
"background": "@smartbarButton/background",
"foreground": "@smartbarButton/foreground"
}
},
"glideTrail": {"foreground": "#204CAF50"}
}
}

View File

@@ -59,6 +59,7 @@
"smartbarButton": {
"background": "@key/background",
"foreground": "@key/foreground"
}
},
"glideTrail": {"foreground": "#200479ed"}
}
}

View File

@@ -0,0 +1,65 @@
{
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
"name": "gboard_night",
"label": "Gboard Night",
"authors": [ "Netscaping" ],
"isNightTheme": true,
"attributes": {
"window": {
"colorPrimary": "#5e97f6",
"colorPrimaryDark": "#4285f4",
"colorAccent": "#FF9800",
"navigationBarColor": "@keyboard/background",
"navigationBarLight": "false",
"semiTransparentColor": "#20FFFFFF",
"textColor": "#FFFFFF"
},
"keyboard": {
"background": "#292e33"
},
"key": {
"background": "#484c4f",
"backgroundPressed": "#5e5e60",
"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": "#BDBDBD"
},
"oneHanded": {
"background": "#373c41",
"foreground": "#9b9da0"
},
"popup": {
"background": "#373c41",
"backgroundActive": "#5a5e60",
"foreground": "@window/textColor"
},
"privateMode": {
"background": "#A000FF",
"foreground": "#FFFFFF"
},
"smartbar": {
"background": "transparent",
"foreground": "#d4d5d6",
"foregroundAlt": "#73FFFFFF"
},
"smartbarButton": {
"background": "#FFFFFF",
"foreground": "#686868"
},
"glideTrail": {"foreground": "#205e97f6"}
}
}

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,114 @@
package dev.patrickgold.florisboard.ime.clip
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.clip.provider.ItemType
import dev.patrickgold.florisboard.ime.core.FlorisBoard
class ClipboardHistoryItemAdapter(
private val dataSet: ArrayDeque<FlorisClipboardManager.TimedClipData>,
private val pins: ArrayDeque<ClipboardItem>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class ClipboardHistoryTextViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.clipboard_history_item_text)
}
class ClipboardHistoryImageViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val imgView: ImageView = view.findViewById(R.id.clipboard_history_item_img)
}
companion object {
private const val MAX_SIZE: Int = 256
}
override fun getItemViewType(position: Int): Int {
return if (position < pins.size) {
// is a pin
pins[position].type.value
}else {
// regular history item
dataSet[position - pins.size].data.type.value
}
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
// Create a new view, which defines the UI of the list item
val vh = when (viewType) {
ItemType.IMAGE.value -> {
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.clipboard_history_item_image, viewGroup, false)
ClipboardHistoryImageViewHolder(view)
}
ItemType.TEXT.value -> {
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.clipboard_history_item_text, viewGroup, false)
ClipboardHistoryTextViewHolder(view)
}
else -> null
}!!
val clipboardInputManager = ClipboardInputManager.getInstance()
(vh.itemView as ClipboardHistoryItemView).keyboardView = clipboardInputManager.getClipboardHistoryView()
return vh
}
// Replace the contents of a view (invoked by the layout manager)
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
when (viewHolder) {
is ClipboardHistoryTextViewHolder -> {
var text = if (position < pins.size) {
(viewHolder.itemView as ClipboardHistoryItemView).setPinned()
pins[position].text
}else {
(viewHolder.itemView as ClipboardHistoryItemView).setUnpinned()
dataSet[position - pins.size].data.text
}
if (text!!.length > MAX_SIZE) {
text = text.subSequence(0 until MAX_SIZE).toString() + "..."
}
viewHolder.textView.text = text
}
is ClipboardHistoryImageViewHolder -> {
val uri = if (position < pins.size) {
(viewHolder.itemView as ClipboardHistoryItemView).setPinned()
pins[position].uri
}else {
(viewHolder.itemView as ClipboardHistoryItemView).setUnpinned()
dataSet[position - pins.size].data.uri
}
viewHolder.imgView.clipToOutline = true
viewHolder.imgView.visibility = GONE
// For very large images, this can take a bit
FlorisClipboardManager.getInstance().executor.execute {
val resolver = FlorisBoard.getInstance().context.contentResolver
val inputStream = resolver.openInputStream(uri!!)
val drawable = Drawable.createFromStream(inputStream, "clipboard URI")
viewHolder.itemView.post {
viewHolder.imgView.setImageDrawable(drawable)
viewHolder.imgView.visibility = VISIBLE
}
}
}
}
}
override fun getItemCount() = pins.size + dataSet.size
}

View File

@@ -0,0 +1,86 @@
package dev.patrickgold.florisboard.ime.clip
import android.content.Context
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
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 ClipboardHistoryItemView: ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
lateinit var keyboardView: ClipboardHistoryView
constructor(context: Context) : this(context, null as AttributeSet?)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private var popupManager: ClipboardPopupManager? = null
override fun onAttachedToWindow() {
super.onAttachedToWindow()
popupManager = ClipboardPopupManager(keyboardView, FlorisBoard.getInstance().popupLayerView, this)
setOnClickListener{
onClickItem()
}
setOnLongClickListener{
onLongClickItem()
}
val themeManager = ThemeManager.default()
themeManager.registerOnThemeUpdatedListener(this)
}
override fun onThemeUpdated(theme: Theme) {
background.setTint(theme.getAttr(Theme.Attr.KEY_BACKGROUND).toSolidColor().color)
val pin = findViewById<ImageView>(R.id.clipboard_pin).drawable
pin?.setTint(theme.getAttr(Theme.Attr.KEY_FOREGROUND).toSolidColor().color)
}
private fun onLongClickItem() : Boolean {
popupManager?.show(this)
return true
}
private fun onClickItem(){
val position = ClipboardInputManager.getInstance().getPositionOfView(this)
val instance = FlorisClipboardManager.getInstance()
val canPaste = instance.canBePasted(instance.peekHistoryOrPin(position))
if (canPaste) {
instance.pasteItem(position)
}else {
Toast.makeText(context, context.getString(R.string.clip__cant_paste), Toast.LENGTH_SHORT).show()
}
}
fun setPinned() {
val view = findViewById<TextView>(R.id.clipboard_history_item_text)
view?.run {
val params = layoutParams as LayoutParams
params.marginEnd = resources.getDimensionPixelSize(R.dimen.clipboard_text_item_pin_margin)
layoutParams = params
}
findViewById<ImageView>(R.id.clipboard_pin).visibility = VISIBLE
invalidate()
val themeManager = ThemeManager.default()
onThemeUpdated(themeManager.activeTheme)
}
fun setUnpinned(){
val view = findViewById<TextView>(R.id.clipboard_history_item_text)
// if text view, also update margin.
view?.run {
val params = layoutParams as LayoutParams
params.marginEnd = 0
layoutParams = params
invalidate()
}
findViewById<ImageView>(R.id.clipboard_pin).visibility = INVISIBLE
}
}

View File

@@ -0,0 +1,71 @@
package dev.patrickgold.florisboard.ime.clip
import android.content.Context
import android.util.AttributeSet
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
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
import kotlin.math.roundToInt
class ClipboardHistoryView : LinearLayout, FlorisBoard.EventListener,
ThemeManager.OnThemeUpdatedListener {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val themeManager: ThemeManager = ThemeManager.default()
var backButton: ImageButton? = null
private set
var clipText: TextView? = null
private set
var clipboardBar: LinearLayout? = null
private set
private var clipboardHistory: RecyclerView? = null
private var clearAll: ImageButton? = null
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
florisboard?.addEventListener(this)
themeManager.registerOnThemeUpdatedListener(this)
backButton = findViewById(R.id.back_to_keyboard_button)
clipText = findViewById(R.id.clipboard_text)
clipboardBar = findViewById(R.id.clipboard_bar)
clipboardHistory = findViewById(R.id.clipboard_history_items)
clearAll = findViewById(R.id.clear_clipboard_history)
onApplyThemeAttributes()
// lord alone knows why it doesn't work without this..
onThemeUpdated(themeManager.activeTheme)
}
override fun onDetachedFromWindow() {
themeManager.unregisterOnThemeUpdatedListener(this)
florisboard?.removeEventListener(this)
super.onDetachedFromWindow()
}
override fun onThemeUpdated(theme: Theme) {
val fgColor = theme.getAttr(Theme.Attr.KEY_FOREGROUND).toSolidColor().color
clipText?.setTextColor(fgColor)
backButton?.drawable?.setTint(fgColor)
clearAll?.setColorFilter(fgColor)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val height = florisboard?.inputView?.desiredMediaKeyboardViewHeight ?: 0.0f
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height.roundToInt(), MeasureSpec.EXACTLY))
}
}

View File

@@ -0,0 +1,218 @@
package dev.patrickgold.florisboard.ime.clip
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import android.view.MotionEvent
import android.view.View
import android.widget.*
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
import dev.patrickgold.florisboard.ime.core.InputView
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import kotlinx.coroutines.*
import kotlin.math.pow
/**
* Handles the clipboard view and allows for communication between UI and logic.
*/
class ClipboardInputManager private constructor() : CoroutineScope by MainScope(),
FlorisBoard.EventListener{
private val florisboard = FlorisBoard.getInstance()
private var repeatedKeyPressHandler: Handler? = null
private var recyclerView: RecyclerView? = null
private var adapter: ClipboardHistoryItemAdapter? = null
companion object {
private var instance: ClipboardInputManager? = null
@Synchronized
fun getInstance(): ClipboardInputManager {
if (instance == null) {
instance = ClipboardInputManager()
}
return instance!!
}
}
init {
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.
*/
@SuppressLint("ClickableViewAccessibility")
override fun onRegisterInputView(inputView: InputView) {
launch(Dispatchers.Default) {
inputView.findViewById<ImageButton>(R.id.back_to_keyboard_button)
.setOnTouchListener { view, event -> onButtonPressEvent(view, event) }
inputView.findViewById<ImageButton>(R.id.clear_clipboard_history)
.setOnTouchListener { view, event -> onButtonPressEvent(view, event) }
recyclerView = inputView.findViewById(R.id.clipboard_history_items)
if (BuildConfig.DEBUG && adapter == null) {
error("initClipboard() not called")
}
recyclerView!!.adapter = adapter
val manager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
recyclerView!!.layoutManager = manager
}
}
/**
* Clean-up of resources and stopping all coroutines.
*/
override fun onDestroy() {
cancel()
instance = null
}
/**
* Returns a reference to the [ClipboardHistoryView]
*/
fun getClipboardHistoryView() : ClipboardHistoryView{
return FlorisBoard.getInstance().inputView?.mainViewFlipper?.getChildAt(2) as ClipboardHistoryView
}
/**
* Returns the adapter position of the view, i.e the position that the item is displayed at (including pins and
* history items).
*
* @param view The ClipboardHistoryItemView whose position is to be determined.
* @return The adapter position of the view
*/
fun getPositionOfView(view: View): Int {
return recyclerView?.getChildLayoutPosition(view)!!
}
/**
* Notify adapter that an item was inserted.
*
* @param position The position the item was inserted at
*/
fun notifyItemInserted(position: Int) = adapter?.notifyItemInserted(position)
/**
* Notify adapter that an item was removed
* @param position The position the item was removed from
*/
fun notifyItemRemoved(position: Int) = adapter?.notifyItemRemoved(position)
/**
* Notify adapter that an item range was removed.
* @param start The index the range starts at (inclusive)
* @param numberOfItems The number of items removed
*/
fun notifyItemRangeRemoved(start: Int, numberOfItems: Int) = adapter?.notifyItemRangeRemoved(start, numberOfItems)
/**
* Notify adapter that an item was moved
* @param from The original position
* @param to The final position
*/
fun notifyItemMoved(from: Int, to: Int) = adapter?.notifyItemMoved(from, to)
/**
* Notify adapter that an item was changed.
*
* @param i The position of the item
*/
fun notifyItemChanged(i: Int) = adapter?.notifyItemChanged(i)
/**
* Handles clicks on the back to keyboard button.
*/
private fun onButtonPressEvent(view: View, event: MotionEvent?): Boolean {
event ?: return false
val data = when (view.id) {
R.id.back_to_keyboard_button -> KeyData(code = KeyCode.SWITCH_TO_TEXT_CONTEXT)
R.id.clear_clipboard_history -> KeyData(code = KeyCode.CLEAR_CLIPBOARD_HISTORY)
else -> null
}!!
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
florisboard.keyPressVibrate()
florisboard.keyPressSound(data)
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.down(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
return false
}
/**
* [recyclerView] will be linked to [dataSet] and [pins] when initialized.
*
* @param dataSet the data set to link to
* @param pins The pins to link to
*/
fun initClipboard(dataSet: ArrayDeque<FlorisClipboardManager.TimedClipData>, pins: ArrayDeque<ClipboardItem>) {
this.adapter = ClipboardHistoryItemAdapter(dataSet = dataSet, pins= pins)
}
/**
* Plays an animation of all items moving off the the clipboard from the top.
*
* @param start The index to start at (to ignore pins)
* @param size The size of the clipboard
* @return The time in millis till the last animation will complete.
*/
fun clearClipboardWithAnimation(start: Int, size: Int): Long {
// list of views to animate
val views = arrayListOf<View>()
for(i in 0 until size){
recyclerView?.findViewHolderForLayoutPosition(i + start)?.let {
views.add(it.itemView)
}
}
// animate the views
var delay = 1L
for (view in views) {
delay += (10 * delay.toDouble().pow(0.1)).toLong()
val an = view.animate().translationX(1500f)
an.startDelay = delay
an.duration = 250
}
// a little while later we reset the views so they can be reused.
Handler(Looper.getMainLooper()).postDelayed({
for (view in views) {
view.translationX = 0f
}
}, 450 + delay)
return 280 + delay
}
}

View File

@@ -0,0 +1,127 @@
package dev.patrickgold.florisboard.ime.clip
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.LinearLayout
import android.widget.Space
import android.widget.TextView
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.popup.PopupLayerView
import kotlin.math.max
class ClipboardPopupManager(private val keyboardView: ClipboardHistoryView,
private val popupLayerView: PopupLayerView?,
private val clipboardHistoryItem: ClipboardHistoryItemView) {
private val popupView: ClipboardPopupView = LayoutInflater.from(keyboardView.context).inflate(R.layout.clip_popup_layout, null) as ClipboardPopupView
private var width = 0
private var height = 0
private var xOffset = 0
private var yOffset = 0
init {
popupLayerView?.addView(popupView)
}
private fun pinButtonListener() {
val pos = ClipboardInputManager.getInstance().getPositionOfView(clipboardHistoryItem)
val pinned = FlorisClipboardManager.getInstance().isPinned(pos)
if (pinned) {
FlorisClipboardManager.getInstance().unpinClip(pos)
hide()
} else {
FlorisClipboardManager.getInstance().pinClip(pos)
hide()
}
}
/**
* Show a popup.
*/
fun show(view: ClipboardHistoryItemView) {
val pinButton = popupView.findViewById<LinearLayout>(R.id.pin_clip_item)
pinButton.setOnClickListener {
pinButtonListener()
}
val pos = ClipboardInputManager.getInstance().getPositionOfView(clipboardHistoryItem)
val pinned = FlorisClipboardManager.getInstance().isPinned(pos)
if (pinned) {
pinButton.findViewById<TextView>(R.id.pin_clip_item_text).text = view.context.getString(R.string.clip__unpin_item)
}
val delete = popupView.findViewById<LinearLayout>(R.id.remove_from_history)
delete.setOnClickListener {
FlorisClipboardManager.getInstance().removeClip(pos)
hide()
}
val clipboardManager = FlorisClipboardManager.getInstance()
val clipItem = clipboardManager.peekHistoryOrPin(pos)
val pasteShouldBeEnabled = FlorisClipboardManager.getInstance().canBePasted(clipItem)
// the clipboard item has any of the supported mime types of the editor OR is plain text.
val paste = popupView.findViewById<LinearLayout>(R.id.paste_clip_item)
if (pasteShouldBeEnabled) {
paste.setOnClickListener {
FlorisClipboardManager.getInstance().pasteItem(pos)
hide()
}
popupView.findViewById<Space>(R.id.paste_clip_item_space).visibility = VISIBLE
paste.visibility = VISIBLE
}else {
popupView.findViewById<Space>(R.id.paste_clip_item_space).visibility = GONE
paste.visibility = GONE
}
FlorisBoard.getInstance().isClipboardContextMenuShown = true
popupLayerView?.clipboardPopupManager = this
popupLayerView?.intercept = popupView
calc(view)
popupView.properties.let {
it.width = this.width
it.height = this.height
it.xOffset = this.xOffset
it.yOffset = this.yOffset
}
popupView.show(keyboardView)
}
/**
* Calculate sizes of popup.
*/
private fun calc(view: ClipboardHistoryItemView) {
val widthMeasureSpec: Int = View.MeasureSpec.makeMeasureSpec(view.width, View.MeasureSpec.AT_MOST)
val heightMeasureSpec: Int = View.MeasureSpec.makeMeasureSpec(100000, View.MeasureSpec.AT_MOST)
popupView.invalidate()
popupView.measure(widthMeasureSpec, heightMeasureSpec)
width = view.width * 4 / 5
height = popupView.measuredHeight
xOffset = view.x.toInt() + (view.width - width) / 2
// y offset is either where the top of the item is OR if the top is off screen, the top of the keyboard.
yOffset = max(view.y.toInt() - keyboardView.height - height / 2 - 20, keyboardView.y.toInt() - keyboardView.height - height / 2 - 20)
}
/**
* Hides a popup.
*/
fun hide() {
popupView.hide()
popupLayerView?.intercept = null
popupLayerView?.clipboardPopupManager = null
FlorisBoard.getInstance().isClipboardContextMenuShown = false
popupView.apply {
visibility = GONE
}
}
}

View File

@@ -0,0 +1,130 @@
package dev.patrickgold.florisboard.ime.clip
import android.content.Context
import android.graphics.drawable.PaintDrawable
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.util.ViewLayoutUtils
class ClipboardPopupView: LinearLayout, ThemeManager.OnThemeUpdatedListener {
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
)
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
}
private val themeManager: ThemeManager = ThemeManager.default()
val properties: Properties = Properties(
width = 0,
height = 0,
xOffset = 0,
yOffset = 0
)
private val isShowing: Boolean
get() = visibility == VISIBLE
init {
visibility = GONE
background = backgroundDrawable
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
themeManager.registerOnThemeUpdatedListener(this)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
themeManager.unregisterOnThemeUpdatedListener(this)
}
override fun onThemeUpdated(theme: Theme) {
backgroundDrawable.apply {
setTint(theme.getAttr(Theme.Attr.POPUP_BACKGROUND).toSolidColor().color)
}
this.findViewById<ImageView>(R.id.pin_clip_item_icon).drawable.apply {
setTint(theme.getAttr(Theme.Attr.WINDOW_TEXT_COLOR).toSolidColor().color)
}
this.findViewById<ImageView>(R.id.remove_from_history_icon).drawable.apply {
setTint(theme.getAttr(Theme.Attr.WINDOW_TEXT_COLOR).toSolidColor().color)
}
this.findViewById<ImageView>(R.id.paste_clip_item_icon).drawable.apply {
setTint(theme.getAttr(Theme.Attr.WINDOW_TEXT_COLOR).toSolidColor().color)
}
if (isShowing) {
invalidate()
}
}
private fun applyProperties(anchor: View) {
val anchorCoords = IntArray(2)
anchor.getLocationInWindow(anchorCoords)
val anchorX = anchorCoords[0]
val anchorY = anchorCoords[1] + anchor.measuredHeight
when (val lp = layoutParams) {
is FrameLayout.LayoutParams -> lp.apply {
width = properties.width
height = properties.height
setMargins(
anchorX + properties.xOffset,
anchorY + properties.yOffset,
0,
0
)
}
else -> {
layoutParams = FrameLayout.LayoutParams(properties.width, properties.height).apply {
setMargins(
anchorX + properties.xOffset,
anchorY + properties.yOffset,
0,
0
)
}
}
}
if (isShowing) {
requestLayout()
invalidate()
}
}
fun show(anchor: View) {
applyProperties(anchor)
visibility = VISIBLE
requestLayout()
invalidate()
}
fun hide() {
visibility = GONE
requestLayout()
invalidate()
}
data class Properties(
var width: Int,
var height: Int,
var xOffset: Int,
var yOffset: Int
)
}

View File

@@ -0,0 +1,405 @@
package dev.patrickgold.florisboard.ime.clip
import android.content.ClipboardManager
import android.content.Context
import android.content.Context.CLIPBOARD_SERVICE
import android.os.Handler
import android.os.Looper
import dev.patrickgold.florisboard.ime.clip.provider.*
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.util.cancelAll
import dev.patrickgold.florisboard.util.postAtScheduledRate
import timber.log.Timber
import java.io.Closeable
import java.util.*
import java.util.concurrent.ExecutorService
import kotlin.collections.ArrayDeque
/**
* [FlorisClipboardManager] manages the clipboard and clipboard history.
*
* Also just going to document how all the classes here work.
*
* [FlorisClipboardManager] handles storage and retrieval of clipboard items. All manipulation of the
* clipboard goes through here.
*
* [ClipboardInputManager] handles the input view and allows for communication between UI and logic
*
* [ClipboardHistoryView] is the view representing the clipboard context. Only does some theme stuff.
*
* [ClipboardHistoryItemView] is the view representing an item in the clipboard history (either image or text). Only
* does UI stuff.
*
* [ClipboardHistoryItemAdapter] is the recyclerview adapter that backs the clipboard history.
*
* [ClipboardPopupManager] handles the popups for each [ClipboardHistoryItemView] (each item has its own popup manager)
*
* [ClipboardPopupView] is the view representing a popup displayed when long pressing on a clipboard history item.
*/
class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryClipChangedListener, Closeable {
private lateinit var pinsDao: PinnedClipboardItemDao
lateinit var executor: ExecutorService
// Using ArrayDeque because it's "technically" the correct data structure (I think).
// Newest stored first, oldest stored last.
private var history: ArrayDeque<TimedClipData> = ArrayDeque()
private var pins: ArrayDeque<ClipboardItem> = ArrayDeque()
private var current: ClipboardItem? = null
private var onPrimaryClipChangedListeners: ArrayList<OnPrimaryClipChangedListener> = arrayListOf()
private lateinit var systemClipboardManager: ClipboardManager
private lateinit var handler: Handler
private lateinit var prefHelper: PrefHelper
data class TimedClipData(val data: ClipboardItem, val timeUTC: Long)
interface OnPrimaryClipChangedListener {
fun onPrimaryClipChanged()
}
companion object {
private var instance: FlorisClipboardManager? = null
// 1 minute
private const val INTERVAL = 60 * 1000L
@Synchronized
fun getInstance(): FlorisClipboardManager {
if (instance == null) {
instance = FlorisClipboardManager()
}
return instance!!
}
@Synchronized
fun getInstanceOrNull(): FlorisClipboardManager? = instance
/**
* Taken from ClipboardDescription.java from the AOSP
*
* Helper to compare two MIME types, where one may be a pattern.
* @param concreteType A fully-specified MIME type.
* @param desiredType A desired MIME type that may be a pattern such as * / *.
* @return Returns true if the two MIME types match.
*/
fun compareMimeTypes(concreteType: String, desiredType: String): Boolean {
val typeLength = desiredType.length
if (typeLength == 3 && desiredType == "*/*") {
return true
}
val slashpos = desiredType.indexOf('/')
if (slashpos > 0) {
if (typeLength == slashpos + 2 && desiredType[slashpos + 1] == '*') {
if (desiredType.regionMatches(0, concreteType, 0, slashpos + 1)) {
return true
}
} else if (desiredType == concreteType) {
return true
}
}
return false
}
}
/**
* Adds a new item to the clipboard history (if enabled).
*/
fun updateHistory(newData: ClipboardItem) {
val clipboardPrefs = prefHelper.clipboard
if (clipboardPrefs.enableHistory) {
if (clipboardPrefs.limitHistorySize) {
var numRemoved = 0
while (history.size >= clipboardPrefs.maxHistorySize) {
numRemoved += 1
history.removeLast().data.close()
}
ClipboardInputManager.getInstance().notifyItemRangeRemoved(history.size, numRemoved)
}
val timed = TimedClipData(newData, System.currentTimeMillis())
history.addFirst(timed)
ClipboardInputManager.getInstance().notifyItemInserted(pins.size)
}
}
/**
* Used so that [onPrimaryClipChanged] knows whether it was called by [changeCurrent] (and hence shouldn't update
* history)
*/
private var shouldUpdateHistory = true
/**
* Changes current clipboard item. WITHOUT updating the history.
*/
fun changeCurrent(newData: ClipboardItem, closePrevious: Boolean) {
if (prefHelper.clipboard.enableInternal) {
if (closePrevious) current?.close()
current = newData
val isEqual = when (newData.type) {
ItemType.TEXT -> newData.text == systemClipboardManager.primaryClip?.getItemAt(0)?.text
ItemType.IMAGE -> newData.uri == systemClipboardManager.primaryClip?.getItemAt(0)?.uri
}
if (prefHelper.clipboard.syncToSystem && !isEqual)
systemClipboardManager.setPrimaryClip(newData.toClipData())
} else {
shouldUpdateHistory = false
systemClipboardManager.setPrimaryClip(newData.toClipData())
}
onPrimaryClipChangedListeners.forEach { it.onPrimaryClipChanged() }
}
/**
* Change the current text on clipboard, update history (if enabled).
*
*/
fun addNewClip(newData: ClipboardItem) {
updateHistory(newData)
// If history is disabled, this new item will replace the old one and hence should be closed.
changeCurrent(newData, !prefHelper.clipboard.enableHistory)
}
/**
* Wraps some plaintext in a ClipData and calls [addNewClip]
*/
fun addNewPlaintext(newText: String) {
val newData = ClipboardItem(null, ItemType.TEXT, null, newText, ClipboardItem.TEXT_PLAIN)
addNewClip(newData)
}
val primaryClip: ClipboardItem?
get() = if (prefHelper.clipboard.enableInternal) {
current
} else {
systemClipboardManager.primaryClip?.let { ClipboardItem.fromClipData(it, false) }
}
fun peekHistory(index: Int): ClipboardItem? {
return history.getOrNull(index)?.data
}
fun addPrimaryClipChangedListener(listener: OnPrimaryClipChangedListener) {
onPrimaryClipChangedListeners.add(listener)
}
fun removePrimaryClipChangedListener(listener: OnPrimaryClipChangedListener) {
onPrimaryClipChangedListeners.remove(listener)
}
/**
* Called by system clipboard when the contents are changed
*/
override fun onPrimaryClipChanged() {
// Run on async thread to avoid blocking.
if (systemClipboardManager.primaryClip?.getItemAt(0)?.text == null &&
systemClipboardManager.primaryClip?.getItemAt(0)?.uri == null) {
return
}
val isEqual = when (primaryClip?.type) {
ItemType.TEXT -> primaryClip?.text == systemClipboardManager.primaryClip?.getItemAt(0)?.text
ItemType.IMAGE -> primaryClip?.uri == systemClipboardManager.primaryClip?.getItemAt(0)?.uri
null -> false
}
systemClipboardManager.primaryClip?.let {
if (prefHelper.clipboard.enableInternal) {
// In the event that the internal clipboard is enabled, sync to internal clipboard is enabled
// and the item is not already in internal clipboard, add it.
if (prefHelper.clipboard.syncToFloris && !isEqual) {
addNewClip(ClipboardItem.fromClipData(it, true))
}
} else if (prefHelper.clipboard.enableHistory) {
// in the event history is enabled, and it should be updated it is updated
if (shouldUpdateHistory) {
updateHistory(ClipboardItem.fromClipData(it, false))
} else {
shouldUpdateHistory = true
}
}
}
}
fun hasPrimaryClip(): Boolean {
return this.primaryClip != null
}
/**
* Cleans up.
*
* Sets [instance] to null for GC. Unregisters the system clipboard listener, cancels clipboard clean ups.
*/
override fun close() {
systemClipboardManager.removePrimaryClipChangedListener(this)
handler.cancelAll()
instance = null
}
/**
* Initialize the floris clipboard manager. Exists to avoid dependency loop due to reference
* to [FlorisBoard.context]
*
* Sets up the clipboard cleanup task, links the recycler view in clipInputManager to [history].
*
* @param context Required to register as an onPrimaryClipChangedListener of ClipboardManager
*/
fun initialize(context: Context) {
this.systemClipboardManager = (context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager)
systemClipboardManager.addPrimaryClipChangedListener(this)
prefHelper = PrefHelper.getDefaultInstance(context)
val cleanUpClipboard = Runnable {
if (!prefHelper.clipboard.cleanUpOld) {
return@Runnable
}
val currentTime = System.currentTimeMillis()
var numToPop = 0
val expiryTime = prefHelper.clipboard.cleanUpAfter * 60 * 1000
for (item in history.asReversed()) {
if (item.timeUTC + expiryTime < currentTime) {
numToPop += 1
} else {
break
}
}
for (i in 0 until numToPop) {
history.removeLast().data.close()
}
ClipboardInputManager.getInstance().notifyItemRangeRemoved(pins.size + history.size, numToPop)
}
FlorisBoard.getInstance().clipInputManager.initClipboard(this.history, this.pins)
handler = Handler(Looper.getMainLooper())
prefHelper
handler.postAtScheduledRate(0, INTERVAL, cleanUpClipboard)
executor = FlorisBoard.getInstance().asyncExecutor
executor.execute {
pinsDao = PinnedItemsDatabase.getInstance().clipboardItemDao()
pinsDao.getAll().toCollection(this.pins)
FlorisContentProvider.getInstance().initIfNotAlready()
}
}
/**
* Clears the history with an animation.
*/
fun clearHistoryWithAnimation() {
val clipInputManager = FlorisBoard.getInstance().clipInputManager
val delay = clipInputManager.clearClipboardWithAnimation(pins.size, history.size)
handler.postDelayed({
val size = history.size
for (item in history) {
item.data.close()
}
history.clear()
clipInputManager.notifyItemRangeRemoved(pins.size, size)
}, delay)
}
fun pinClip(adapterPos: Int) {
val clipInputManager = FlorisBoard.getInstance().clipInputManager
val pin = history.removeAt(adapterPos - pins.size)
pins.addFirst(pin.data)
clipInputManager.notifyItemMoved(adapterPos, 0)
clipInputManager.notifyItemChanged(0)
executor.execute {
val uid = pinsDao.insert(pin.data)
pin.data.uid = uid
}
}
/**
* Get the item at a particular [adapterPos] (i.e the position the item is displayed at.)
*/
fun peekHistoryOrPin(adapterPos: Int): ClipboardItem {
return when {
adapterPos < pins.size -> pins[adapterPos]
else -> history[adapterPos - pins.size].data
}
}
fun isPinned(position: Int): Boolean {
return when {
position < pins.size -> true
else -> false
}
}
fun unpinClip(adapterPos: Int) {
val clipInputManager = FlorisBoard.getInstance().clipInputManager
val item = pins.removeAt(adapterPos)
val clipboardPrefs = prefHelper.clipboard
if (clipboardPrefs.limitHistorySize) {
var numRemoved = 0
while (history.size >= clipboardPrefs.maxHistorySize) {
numRemoved += 1
history.removeLast().data.close()
}
ClipboardInputManager.getInstance().notifyItemRangeRemoved(history.size, numRemoved)
}
val timed = TimedClipData(item, System.currentTimeMillis())
history.addFirst(timed)
clipInputManager.notifyItemMoved(adapterPos, pins.size)
clipInputManager.notifyItemChanged(pins.size)
executor.execute {
pinsDao.delete(item)
}
}
fun removeClip(pos: Int) {
when {
pos < pins.size -> {
val item = pins.removeAt(pos)
executor.execute {
Timber.d("removing pin")
pinsDao.delete(item)
}
item.close()
}
else -> {
history.removeAt(pos - pins.size).data.close()
}
}
val clipboardInputManager = ClipboardInputManager.getInstance()
clipboardInputManager.notifyItemRemoved(pos)
}
fun pasteItem(pos: Int) {
val item = peekHistoryOrPin(pos)
FlorisBoard.getInstance().activeEditorInstance.commitClipboardItem(item)
}
/**
* Returns true if the editor can accept the clip item, else false.
*/
fun canBePasted(clipItem: ClipboardItem?): Boolean {
if (clipItem == null) return false
return clipItem.mimeTypes.contains("text/plain") || FlorisBoard.getInstance().activeEditorInstance.contentMimeTypes?.any { editorType ->
clipItem.mimeTypes.any { clipType ->
if (editorType != null) {
compareMimeTypes(clipType, editorType)
}else { false }
}
} == true
}
}

View File

@@ -0,0 +1,75 @@
package dev.patrickgold.florisboard.ime.clip.provider
import android.net.Uri
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import timber.log.Timber
import java.io.File
/**
* Backend class which is used by [FlorisContentProvider] to serve content.
*/
class FileStorage private constructor() {
companion object {
private const val BUF_SIZE = 1024 * 8
private var instance: FileStorage? = null
private var offset = 0
fun getInstance() : FileStorage {
if (this.instance == null){
this.instance = FileStorage()
}
return instance!!
}
}
/**
* Clones a content URI to internal storage.
* @param uri The URI
* @return the file's name which is a unique long
*/
@Synchronized
fun cloneURI(uri: Uri) : Long {
val context = FlorisBoard.getInstance().context
// nanoTime + the number of items created so that it's unique.
val name = (System.nanoTime() + offset)
// Just a normal copy from input stream to output stream.
val source = context.contentResolver.openInputStream(uri)!!
val sink = File(context.filesDir, name.toString()).outputStream()
var nread = 0L
val buf = ByteArray(BUF_SIZE)
var n: Int
while (source.read(buf).also { n = it } > 0) {
sink.write(buf, 0, n)
nread += n.toLong()
}
source.close()
sink.close()
return name
}
/**
* Deletes the file corresponding to an id.
*/
fun deleteById(id: Long) {
Timber.d("Cleaning up $id")
val file = File(FlorisBoard.getInstance().filesDir, id.toString())
file.delete()
}
/**
* Get the file address of an id.
*/
fun getAddress(id: Long): String {
return FlorisBoard.getInstance().filesDir.toString() + "/$id"
}
}

View File

@@ -0,0 +1,131 @@
package dev.patrickgold.florisboard.ime.clip.provider
import android.content.*
import android.database.Cursor
import android.net.Uri
import android.os.ParcelFileDescriptor
import androidx.room.Room
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import timber.log.Timber
import java.io.File
import java.util.concurrent.ExecutorService
/**
* Allows apps to access images on the clipboard.
*
* This is sometimes called by the UI thread, so all functions are non blocking.
* Database accesses are performed async.
*/
class FlorisContentProvider : ContentProvider() {
private lateinit var fileUriDao: FileUriDao
private val mimeTypes: HashMap<Long, Array<String>> = hashMapOf()
private lateinit var executor: ExecutorService
override fun onCreate(): Boolean {
instance = this
return true
}
fun initIfNotAlready(){
if (this::fileUriDao.isInitialized){
return
}
fileUriDao = Room.databaseBuilder(
context!!,
FileUriDatabase::class.java, "fileuridb"
).build().fileUriDao()
executor = FlorisBoard.getInstance().asyncExecutor
for (fileUri in fileUriDao.getAll()) {
mimeTypes[fileUri.fileName] = fileUri.mimeTypes
}
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
// just return nothing, nothing should call this function at all.
return null
}
override fun getType(uri: Uri): String {
return when (matcher.match(uri)) {
CLIP_ITEM -> mimeTypes.getOrElse(ContentUris.parseId(uri), { throw IllegalArgumentException("Don't have this item!") })[0]
CLIPS_TABLE -> "vnd.android.cursor.dir/$AUTHORITY.clip"
else -> throw IllegalArgumentException("Don't know what this is $uri")
}
}
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
val id = ContentUris.parseId(uri)
val path = File(FileStorage.getInstance().getAddress(id))
// Nothing has permission to write anyway.
return ParcelFileDescriptor.open(path, ParcelFileDescriptor.MODE_READ_ONLY)
}
override fun insert(uri: Uri, values: ContentValues?): Uri {
when (matcher.match(uri)){
CLIPS_TABLE -> {
val id = FileStorage.getInstance().cloneURI(Uri.parse(values?.getAsString("uri")))
val mimes = values?.getAsString("mimetypes")?.split(",")?.toTypedArray()
mimes?.let {
mimeTypes[id] = mimes
executor.execute {
Timber.d("Inserted file uri $id")
fileUriDao.insert(FileUri(id, mimes))
}
}
return ContentUris.withAppendedId(CLIPS_URI, id)
}
else -> throw IllegalArgumentException("Don't know what this is $uri")
}
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
when (matcher.match(uri)){
CLIP_ITEM -> {
val id = ContentUris.parseId(uri)
FileStorage.getInstance().deleteById(id)
mimeTypes.remove(id)
context?.revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
executor.execute {
fileUriDao.delete(id)
}
return 1
}
else -> throw IllegalArgumentException("Don't know what this is $uri")
}
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
throw IllegalArgumentException("This ContentProvider does not support update.")
}
companion object {
private var instance: FlorisContentProvider? = null
const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider.clip"
val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY")
val CLIPS_URI: Uri = Uri.parse("content://$AUTHORITY/clips")
fun getInstance(): FlorisContentProvider {
return instance!!
}
private const val CLIPS_TABLE = 1
private const val CLIP_ITEM = 0
val matcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(AUTHORITY, "clips/#", CLIP_ITEM)
addURI(AUTHORITY, "clips", CLIPS_TABLE)
}
}
}

View File

@@ -0,0 +1,258 @@
package dev.patrickgold.florisboard.ime.clip.provider
import android.content.ClipData
import android.content.ContentValues
import android.net.Uri
import android.provider.BaseColumns
import androidx.room.*
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import java.io.Closeable
enum class ItemType(val value: Int) {
TEXT(1),
IMAGE(2);
companion object {
fun fromInt(value : Int) : ItemType {
return values().first { it.value == value }
}
}
}
/**
* Represents an item on the clipboard.
* The URI stored belongs to FlorisContentProvider, not whatever app copied the image
*
* If type == ItemType.IMAGE there must be a uri set
* if type == ItemType.TEXT there must be a text set
*/
@Entity(tableName = "pins")
data class ClipboardItem(
/** Only used for pins */
@PrimaryKey(autoGenerate = true) @ColumnInfo(name=BaseColumns._ID, index=true) var uid: Long?,
val type: ItemType,
val uri: Uri?,
val text: String?,
val mimeTypes: Array<String>) : Closeable{
/**
* Creates a new ClipData which has the same contents as this.
*/
fun toClipData(): ClipData {
return when (type) {
ItemType.IMAGE -> {
ClipData.newUri(FlorisBoard.getInstance().context.contentResolver, "Clipboard data", uri)
}
ItemType.TEXT -> {
ClipData.newPlainText("Clipboard data", text)
}
}
}
/**
* Instructs the content provider to delete this URI. If not an image, is a noop
*/
override fun close() {
if (type == ItemType.IMAGE) {
FlorisBoard.getInstance().context.contentResolver.delete(this.uri!!, null, null)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ClipboardItem
if (uid != other.uid) return false
if (type != other.type) return false
if (uri != other.uri) return false
if (text != other.text) return false
if (!mimeTypes.contentEquals(other.mimeTypes)) return false
return true
}
override fun hashCode(): Int {
var result = uid.hashCode()
result = 31 * result + type.hashCode()
result = 31 * result + (uri?.hashCode() ?: 0)
result = 31 * result + (text?.hashCode() ?: 0)
result = 31 * result + mimeTypes.contentHashCode()
return result
}
fun stringRepresentation(): String {
return when {
uri != null -> "(Image) $uri"
text != null -> text
else -> "#ERROR"
}
}
companion object {
/**
* So that every item doesn't have to allocate its own array.
*/
val TEXT_PLAIN = arrayOf("text/plain")
/**
* Returns a new ClipboardItem based on a ClipData
*
* @param data The ClipData to clone.
* @param cloneUri Whether to store the image using [FlorisContentProvider].
*/
fun fromClipData(data: ClipData, cloneUri: Boolean) : ClipboardItem {
val type = when {
data.getItemAt(0)?.uri != null -> ItemType.IMAGE
data.getItemAt(0)?.text != null -> ItemType.TEXT
else -> null
}!!
val uri = if (type == ItemType.IMAGE) {
if (data.getItemAt(0).uri.authority == FlorisContentProvider.CONTENT_URI.authority || !cloneUri){
data.getItemAt(0).uri
}else {
val values = ContentValues().apply{
put("uri", data.getItemAt(0).uri.toString())
put("mimetypes", data.description.filterMimeTypes("*/*").joinToString(","))
}
FlorisBoard.getInstance().context.contentResolver.insert(FlorisContentProvider.CLIPS_URI, values)
}
} else { null }
val text = data.getItemAt(0).text?.toString()
val mimeTypes = when (type) {
ItemType.IMAGE -> {
(0 until data.description.mimeTypeCount).map {
data.description.getMimeType(it)
}.toTypedArray()
}
ItemType.TEXT -> { TEXT_PLAIN }
}
return ClipboardItem(null, type, uri, text, mimeTypes)
}
}
}
class Converters {
@TypeConverter
fun uriFromString(value: String?): Uri? {
return Uri.parse(value)
}
@TypeConverter
fun stringFromUri(value: Uri?): String {
return value.toString()
}
@TypeConverter
fun itemTypeToInt(value: ItemType?): Int? {
return value?.value
}
@TypeConverter
fun intToItemType(value: Int?): ItemType? {
return value?.let { ItemType.fromInt(it) }
}
/**
* Only works because the string array is a mimetype.
* DOES NOT USE A GENERALIZED FORMAT.
*/
@TypeConverter
fun mimeTypesToString(mimeTypes: Array<String>): String {
return mimeTypes.joinToString(",")
}
@TypeConverter
fun stringToMimeTypes(value: String): Array<String> {
return value.split(",").toTypedArray()
}
}
@Dao
interface PinnedClipboardItemDao {
@Query("SELECT * FROM pins")
fun getAll(): List<ClipboardItem>
@Insert
fun insert(item: ClipboardItem) : Long
@Delete
fun delete(item: ClipboardItem)
}
@Database(entities = [ClipboardItem::class], version = 1)
@TypeConverters(Converters::class)
abstract class PinnedItemsDatabase : RoomDatabase() {
abstract fun clipboardItemDao() : PinnedClipboardItemDao
companion object {
private var instance: PinnedItemsDatabase? = null
fun getInstance(): PinnedItemsDatabase {
if (instance == null) {
instance = Room.databaseBuilder(
FlorisBoard.getInstance().context,
PinnedItemsDatabase::class.java,
"pins").build()
}
return instance!!
}
}
}
@Entity(tableName = "file_uris")
data class FileUri(
@PrimaryKey @ColumnInfo(name=BaseColumns._ID, index=true) val fileName: Long,
val mimeTypes: Array<String>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FileUri
if (fileName != other.fileName) return false
if (!mimeTypes.contentEquals(other.mimeTypes)) return false
return true
}
override fun hashCode(): Int {
var result = 31 + fileName.hashCode()
result = 31 * result + mimeTypes.contentHashCode()
return result
}
}
@Dao
interface FileUriDao {
@Query("SELECT * FROM file_uris WHERE ${BaseColumns._ID} == (:uid)")
fun getById(uid: Long) : FileUri
@Query("DELETE FROM file_uris WHERE ${BaseColumns._ID} == (:id)")
fun delete(id: Long)
@Insert
fun insert(vararg fileUris: FileUri)
@Query("SELECT COUNT(*) FROM file_uris WHERE ${BaseColumns._ID} == (:id)")
fun numberWithId(id: Long): Int
@Query("SELECT * FROM file_uris")
fun getAll(): List<FileUri>
}
@Database(entities = [FileUri::class], version = 1)
@TypeConverters(Converters::class)
abstract class FileUriDatabase : RoomDatabase() {
abstract fun fileUriDao() : FileUriDao
}

View File

@@ -16,15 +16,24 @@
package dev.patrickgold.florisboard.ime.core
import android.content.ClipDescription
import android.content.Intent
import android.inputmethodservice.InputMethodService
import android.os.Build
import android.os.SystemClock
import android.text.InputType
import android.view.InputDevice
import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import androidx.annotation.RequiresApi
import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.view.inputmethod.InputContentInfoCompat
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.clip.provider.ItemType
import timber.log.Timber
/**
* Class which holds information relevant to an editor instance like the [cachedInput], [selection],
@@ -35,10 +44,12 @@ class EditorInstance private constructor(
private val ims: InputMethodService?,
val imeOptions: ImeOptions,
val inputAttributes: InputAttributes,
val packageName: String
val packageName: String,
private val editorInfo: EditorInfo
) {
val cachedInput: CachedInput = CachedInput(this)
var contentMimeTypes: Array<out String?>? = null
private val florisClipboardManager: FlorisClipboardManager = FlorisClipboardManager.getInstance()
val cursorCapsMode: InputAttributes.CapsMode
get() {
val ic = inputConnection ?: return InputAttributes.CapsMode.NONE
@@ -59,8 +70,6 @@ class EditorInstance private constructor(
}
}
var shouldReevaluateComposingSuggestions: Boolean = false
var isNewSelectionInBoundsOfOld: Boolean = false
private set
var isPrivateMode: Boolean = false
val isRawInputEditor: Boolean
get() = inputAttributes.type == InputAttributes.Type.NULL
@@ -76,7 +85,8 @@ class EditorInstance private constructor(
ims = null,
imeOptions = ImeOptions.fromImeOptionsInt(EditorInfo.IME_NULL),
inputAttributes = InputAttributes.fromInputTypeInt(InputType.TYPE_NULL),
packageName = "undefined"
packageName = "undefined",
editorInfo = EditorInfo()
)
}
@@ -86,7 +96,8 @@ class EditorInstance private constructor(
ims = ims,
imeOptions = ImeOptions.fromImeOptionsInt(editorInfo.imeOptions),
inputAttributes = InputAttributes.fromInputTypeInt(editorInfo.inputType),
packageName = editorInfo.packageName
packageName = editorInfo.packageName,
editorInfo = editorInfo
).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
contentMimeTypes = editorInfo.contentMimeTypes
@@ -109,24 +120,25 @@ class EditorInstance private constructor(
newSelStart: Int, newSelEnd: Int,
candidatesStart: Int, candidatesEnd: Int
) {
isNewSelectionInBoundsOfOld =
newSelStart >= (oldSelStart - 1) &&
newSelStart <= (oldSelStart + 1) &&
newSelEnd >= (oldSelEnd - 1) &&
newSelEnd <= (oldSelEnd + 1)
selection.update(newSelStart, newSelEnd)
cachedInput.update()
// The Android Framework allows that start can be greater than end in some cases. To prevent bugs in the Floris
// input logic, we swap start and end here if this should really be the case.
if (newSelEnd < newSelStart) {
selection.update(newSelEnd, newSelStart)
} else {
selection.update(newSelStart, newSelEnd)
}
if (isPhantomSpaceActive && wasPhantomSpaceActiveLastUpdate) {
isPhantomSpaceActive = false
} else if (isPhantomSpaceActive && !wasPhantomSpaceActiveLastUpdate) {
wasPhantomSpaceActiveLastUpdate = true
}
cachedInput.update()
if (isComposingEnabled && candidatesStart >= 0 && candidatesEnd >= 0) {
shouldReevaluateComposingSuggestions = true
}
if (selection.isCursorMode && isComposingEnabled && !isRawInputEditor && !isPhantomSpaceActive) {
markComposingRegion(cachedInput.currentWord)
} else {
} else if (newSelStart >= 0) {
markComposingRegion(null)
}
}
@@ -169,22 +181,109 @@ class EditorInstance private constructor(
*/
fun commitText(text: String): Boolean {
val ic = inputConnection ?: return false
return if (isRawInputEditor) {
return if (isRawInputEditor || selection.isSelectionMode || !isComposingEnabled) {
ic.commitText(text, 1)
} else {
ic.beginBatchEdit()
markComposingRegion(null)
if (isPhantomSpaceActive && selection.start > 0 && getTextBeforeCursor(1) != " " && text != " ") {
ic.commitText(" ", 1)
val isWordComponent = CachedInput.isWordComponent(text)
val isPhantomSpace = isPhantomSpaceActive && selection.start > 0 && getTextBeforeCursor(1) != " "
when {
isPhantomSpace && isWordComponent -> {
ic.finishComposingText()
ic.commitText(" ", 1)
ic.setComposingText(text, 1)
}
!isPhantomSpace && isWordComponent -> {
ic.finishComposingText()
ic.commitText(text, 1)
ic.setComposingRegion(cachedInput.currentWord.start, cachedInput.currentWord.end + text.length)
}
else -> {
ic.finishComposingText()
ic.commitText(text, 1)
}
}
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
ic.commitText(text, 1)
ic.endBatchEdit()
true
}
}
/**
* Commit a word generated by a gesture.
*/
fun commitGesture(text: String): Boolean{
val ic = inputConnection ?: return false
return if (isRawInputEditor) {
false
} else {
ic.beginBatchEdit()
ic.finishComposingText()
if (selection.start > 0 && getTextBeforeCursor(1).isNotBlank()) {
ic.commitText(" ", 1)
}
ic.commitText(text, 1)
isPhantomSpaceActive = true
wasPhantomSpaceActiveLastUpdate = false
ic.endBatchEdit()
true
}
}
/**
* Replaces the previous word with the given [text]. Used to correct gestures.
*/
fun commitGestureCorrection(text: String): Boolean {
val ic = inputConnection ?: return false
return if (isRawInputEditor) {
false
} else {
ic.beginBatchEdit()
markComposingRegion(Region(this, cachedInput.getWordForIndex(-1).start, cachedInput.getWordForIndex(-1).end))
ic.commitText(text, 1)
markComposingRegion(null)
isPhantomSpaceActive = true
wasPhantomSpaceActiveLastUpdate = false
ic.endBatchEdit()
true
}
}
/**
* Commits the given [ClipboardItem]. If the clip data is text (incl. HTML), it delegates to [commitText].
* If the item has a content URI (and the EditText supports it), the item is committed as rich data.
* This allows for committing (e.g) images.
*
* @param item The ClipboardItem to commit
* @return True on success, false if something went wrong.
*/
fun commitClipboardItem(item: ClipboardItem): Boolean {
val mimeTypes = item.mimeTypes
return when (item.type){
ItemType.IMAGE -> {
val inputContentInfo = InputContentInfoCompat(
item.uri!!,
ClipDescription("clipboard image", mimeTypes),
null
)
val ic = inputConnection ?: return false
ic.finishComposingText()
var flags = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
flags = flags or InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION
}else {
FlorisBoard.getInstance().context.grantUriPermission(editorInfo.packageName, item.uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
InputConnectionCompat.commitContent(ic, editorInfo, inputContentInfo, flags, null)
}
ItemType.TEXT -> {
commitText(item.text.toString())
}
}
}
/**
* Executes a backward delete on this editor's text. If a text selection is active, all
* characters inside this selection will be removed, else only the left-most character from
@@ -193,22 +292,9 @@ class EditorInstance private constructor(
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun deleteBackwards(): Boolean {
val ic = inputConnection ?: return false
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
return if (isRawInputEditor) {
sendSystemKeyEvent(KeyEvent.KEYCODE_DEL)
} else {
ic.beginBatchEdit()
markComposingRegion(null)
sendSystemKeyEvent(KeyEvent.KEYCODE_DEL)
cachedInput.update()
if (isComposingEnabled) {
markComposingRegion(cachedInput.currentWord)
}
ic.endBatchEdit()
true
}
return sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL)
}
/**
@@ -230,12 +316,14 @@ class EditorInstance private constructor(
ic.beginBatchEdit()
markComposingRegion(null)
getWordsInString(cachedInput.rawText.substring(0,
(selection.start - cachedInput.offset).coerceAtLeast(0))).run {
get(size - n.coerceAtLeast(0)).range
}.run {
ic.setSelection(first + cachedInput.offset, selection.start)
}
try {
getWordsInString(cachedInput.rawText.substring(0,
(selection.start - cachedInput.offset).coerceAtLeast(0))).run {
get(size - n.coerceAtLeast(0)).range
}.run {
ic.setSelection(first + cachedInput.offset, selection.start)
}
} catch (e: Exception) {}
ic.commitText("", 1)
@@ -355,9 +443,11 @@ class EditorInstance private constructor(
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun performClipboardCut(): Boolean {
Timber.d("performClipboardCut")
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_X)
florisClipboardManager.addNewPlaintext(selection.text)
return sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL)
}
/**
@@ -367,10 +457,11 @@ class EditorInstance private constructor(
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun performClipboardCopy(): Boolean {
Timber.d("performClipboardCopy")
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_C) &&
selection.updateAndNotify(selection.end, selection.end)
florisClipboardManager.addNewPlaintext(selection.text)
return selection.updateAndNotify(selection.end, selection.end)
}
/**
@@ -382,7 +473,8 @@ class EditorInstance private constructor(
fun performClipboardPaste(): Boolean {
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_V)
Timber.d("Before commit clip data")
return commitClipboardItem(florisClipboardManager.primaryClip!!)
}
/**
@@ -394,7 +486,14 @@ class EditorInstance private constructor(
fun performClipboardSelectAll(): Boolean {
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_A)
markComposingRegion(null)
val ic = inputConnection ?: return false
if (isRawInputEditor) {
sendDownUpKeyEvent(KeyEvent.KEYCODE_A, meta(ctrl = true))
} else {
ic.performContextMenuAction(android.R.id.selectAll)
}
return true
}
/**
@@ -406,7 +505,7 @@ class EditorInstance private constructor(
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
return if (isRawInputEditor) {
sendSystemKeyEvent(KeyEvent.KEYCODE_ENTER)
sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER)
} else {
commitText("\n")
}
@@ -434,21 +533,7 @@ class EditorInstance private constructor(
fun performUndo(): Boolean {
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
val ic = inputConnection ?: return false
return if (isRawInputEditor) {
sendSystemKeyEventCtrl(KeyEvent.KEYCODE_Z)
true
} else {
ic.beginBatchEdit()
markComposingRegion(null)
sendSystemKeyEventCtrl(KeyEvent.KEYCODE_Z)
cachedInput.update()
if (isComposingEnabled) {
markComposingRegion(cachedInput.currentWord)
}
ic.endBatchEdit()
true
}
return sendDownUpKeyEvent(KeyEvent.KEYCODE_Z, meta(ctrl = true))
}
/**
@@ -459,73 +544,40 @@ class EditorInstance private constructor(
fun performRedo(): Boolean {
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
val ic = inputConnection ?: return false
return if (isRawInputEditor) {
sendSystemKeyEventCtrlShift(KeyEvent.KEYCODE_Z)
true
} else {
ic.beginBatchEdit()
markComposingRegion(null)
sendSystemKeyEventCtrlShift(KeyEvent.KEYCODE_Z)
cachedInput.update()
if (isComposingEnabled) {
markComposingRegion(cachedInput.currentWord)
}
ic.endBatchEdit()
true
return sendDownUpKeyEvent(KeyEvent.KEYCODE_Z, meta(ctrl = true, shift = true))
}
/**
* Constructs a meta state integer flag which can be used for setting the `metaState` field when sending a KeyEvent
* to the input connection. If this method is called without a meta modifier set to true, the default value `0` is
* returned.
*
* @param ctrl Set to true to enable the CTRL meta modifier. Defaults to false.
* @param alt Set to true to enable the ALT meta modifier. Defaults to false.
* @param shift Set to true to enable the SHIFT meta modifier. Defaults to false.
*
* @return An integer containing all meta flags passed and formatted for use in a [KeyEvent].
*/
fun meta(
ctrl: Boolean = false,
alt: Boolean = false,
shift: Boolean = false
): Int {
var metaState = 0
if (ctrl) {
metaState = metaState or KeyEvent.META_CTRL_ON or KeyEvent.META_CTRL_LEFT_ON
}
if (alt) {
metaState = metaState or KeyEvent.META_ALT_ON or KeyEvent.META_ALT_LEFT_ON
}
if (shift) {
metaState = metaState or KeyEvent.META_SHIFT_ON or KeyEvent.META_SHIFT_LEFT_ON
}
return metaState
}
/**
* Sends a given [keyEventCode] with [sendDownUpKeyEvent].
*
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun sendSystemKeyEvent(keyEventCode: Int): Boolean {
return sendDownUpKeyEvent(keyEventCode, 0)
}
/**
* Sends a given [keyEventCode] with Ctrl pressed with [sendDownUpKeyEvent].
*
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
* @return True on success, false if an error occurred or the input connection is invalid.
*/
private fun sendSystemKeyEventCtrl(keyEventCode: Int): Boolean {
return sendDownUpKeyEvent(keyEventCode, KeyEvent.META_CTRL_ON)
}
/**
* Sends a given [keyEventCode] with Ctrl and Shift pressed with [sendDownUpKeyEvent].
*
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
* @return True on success, false if an error occurred or the input connection is invalid.
*/
private fun sendSystemKeyEventCtrlShift(keyEventCode: Int): Boolean {
return sendDownUpKeyEvent(keyEventCode, KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON)
}
/**
* Sends a given [keyEventCode] with Alt pressed with [sendDownUpKeyEvent].
*
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun sendSystemKeyEventAlt(keyEventCode: Int): Boolean {
return sendDownUpKeyEvent(keyEventCode, KeyEvent.META_ALT_LEFT_ON)
}
/**
* Same as [InputMethodService.sendDownUpKeyEvents] but also allows to set meta state.
*
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
* @param metaState Flags indicating which meta keys are currently pressed.
* @return True on success, false if an error occurred or the input connection is invalid.
*/
private fun sendDownUpKeyEvent(keyEventCode: Int, metaState: Int): Boolean {
private fun sendDownKeyEvent(eventTime: Long, keyEventCode: Int, metaState: Int): Boolean {
val ic = inputConnection ?: return false
val eventTime = SystemClock.uptimeMillis()
return ic.sendKeyEvent(
KeyEvent(
eventTime,
@@ -536,9 +588,15 @@ class EditorInstance private constructor(
metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD,
0,
KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE
KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE,
InputDevice.SOURCE_KEYBOARD
)
) && ic.sendKeyEvent(
)
}
private fun sendUpKeyEvent(eventTime: Long, keyEventCode: Int, metaState: Int): Boolean {
val ic = inputConnection ?: return false
return ic.sendKeyEvent(
KeyEvent(
eventTime,
SystemClock.uptimeMillis(),
@@ -548,10 +606,52 @@ class EditorInstance private constructor(
metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD,
0,
KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE
KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE,
InputDevice.SOURCE_KEYBOARD
)
)
}
/**
* Same as [InputMethodService.sendDownUpKeyEvents] but also allows to set meta state.
*
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
* @param metaState Flags indicating which meta keys are currently pressed.
* @param count How often the key is pressed while the meta keys passed are down. Must be greater than or equal to
* `1`, else this method will immediately return false.
*
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun sendDownUpKeyEvent(keyEventCode: Int, metaState: Int = meta(), count: Int = 1): Boolean {
if (count < 1) return false
val ic = inputConnection ?: return false
ic.beginBatchEdit()
val eventTime = SystemClock.uptimeMillis()
if (metaState and KeyEvent.META_CTRL_ON > 0) {
sendDownKeyEvent(eventTime, KeyEvent.KEYCODE_CTRL_LEFT, 0)
}
if (metaState and KeyEvent.META_ALT_ON > 0) {
sendDownKeyEvent(eventTime, KeyEvent.KEYCODE_ALT_LEFT, 0)
}
if (metaState and KeyEvent.META_SHIFT_ON > 0) {
sendDownKeyEvent(eventTime, KeyEvent.KEYCODE_SHIFT_LEFT, 0)
}
for (n in 0 until count) {
sendDownKeyEvent(eventTime, keyEventCode, metaState)
sendUpKeyEvent(eventTime, keyEventCode, metaState)
}
if (metaState and KeyEvent.META_SHIFT_ON > 0) {
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_SHIFT_LEFT, 0)
}
if (metaState and KeyEvent.META_ALT_ON > 0) {
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_ALT_LEFT, 0)
}
if (metaState and KeyEvent.META_CTRL_ON > 0) {
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_CTRL_LEFT, 0)
}
ic.endBatchEdit()
return true
}
}
/**
@@ -893,7 +993,7 @@ class CachedInput(private val editorInstance: EditorInstance) {
* the target app's editor text is bigger than [CACHED_TEXT_N_CHARS_BEFORE_CURSOR] and
* [CACHED_TEXT_N_CHARS_AFTER_CURSOR], but always caches the relevant text around the cursor.
*/
var rawText: String = ""
var rawText: StringBuilder = StringBuilder()
private set
companion object {
@@ -902,6 +1002,10 @@ class CachedInput(private val editorInstance: EditorInstance) {
private val WORD_EVAL_REGEX = """[^\p{L}\']""".toRegex()
private val WORD_SPLIT_REGEX_EN = """((?<=$WORD_EVAL_REGEX)|(?=$WORD_EVAL_REGEX))""".toRegex()
fun isWordComponent(string: String): Boolean {
return !WORD_EVAL_REGEX.matches(string)
}
}
/**
@@ -945,18 +1049,18 @@ class CachedInput(private val editorInstance: EditorInstance) {
val ic = inputConnection
if (ic == null) {
offset = 0
rawText = ""
rawText.clear()
expectedMaxLength = 0
} else {
val textBefore = getTextBeforeCursor(CACHED_TEXT_N_CHARS_BEFORE_CURSOR)
val textSelected = ic.getSelectedText(0) ?: ""
val textAfter = getTextAfterCursor(CACHED_TEXT_N_CHARS_AFTER_CURSOR)
offset = (selection.start - textBefore.length).coerceAtLeast(0)
rawText = StringBuilder().run {
rawText.apply {
clear()
append(textBefore)
append(textSelected)
append(textAfter)
toString()
}
expectedMaxLength = offset + rawText.length
}

View File

@@ -0,0 +1,121 @@
/*
* 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
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity(), CoroutineScope by MainScope() {
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()
cancel()
_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

@@ -22,9 +22,11 @@ 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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import timber.log.Timber
class FlorisApplication : Application() {
class FlorisApplication : Application(), CoroutineScope by MainScope() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {

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