Compare commits

..

107 Commits

Author SHA1 Message Date
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
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
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
Anirudh Oppiliappan
7d6666f7f3 Add the Workman keyboard layout 2021-03-27 20:22:12 +05:30
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
151 changed files with 5294 additions and 725 deletions

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

@@ -75,7 +75,7 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
### Layouts
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish, Norwegian, Swedish/Finnish, Icelandic, Danish,
Hungarian, Croatian, Polish, Romanian, Colemak, Dvorak, Turkish-Q, Turkish-F, ...)
* [x] Non-latin character layouts (Arabic, Persian, Greek, Russian (JCUKEN))
* [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

View File

@@ -23,7 +23,7 @@ android {
applicationId = "dev.patrickgold.florisboard"
minSdkVersion(23)
targetSdkVersion(30)
versionCode(32)
versionCode(34)
versionName("0.3.10")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -46,7 +46,7 @@ android {
create("beta") // Needed because by default the "beta" BuildType does not exist
named("beta").configure {
applicationIdSuffix = ".beta"
versionNameSuffix = "-beta04"
versionNameSuffix = "-beta06"
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_beta")

View File

@@ -113,12 +113,11 @@
<provider
android:name="dev.patrickgold.florisboard.ime.clip.provider.FlorisContentProvider"
android:authorities="dev.patrickgold.florisboard.provider.clip"
android:authorities="${applicationId}.provider.clip"
android:grantUriPermissions="true"
android:exported="false">
</provider>
</application>
</manifest>

View File

@@ -1,227 +1,586 @@
{
"package": "dev.patrickgold.florisboard",
"characterLayouts": {
"qwerty": "QWERTY",
"qwertz": "QWERTZ",
"azerty": "AZERTY",
"arabic": "Arabic",
"bepo": "BÉPO",
"bulgarian_bds": "Bulgarian (BDS)",
"bulgarian_phonetic": "Bulgarian (Phonetic)",
"canadian_french": "Canadian French (QWERTY)",
"colemak": "Colemak",
"danish": "Danish (QWERTY)",
"dvorak": "Dvorak",
"esperanto": "Esperanto",
"esperanto_with_hx": "Esperanto with 'ĥ'",
"greek": "Ελληνικά",
"hebrew": "עברית",
"hungarian": "Hungarian (QWERTZ)",
"icelandic": "Icelandic (QWERTY)",
"kurdish": "کوردی",
"norwegian": "Norwegian (QWERTY)",
"persian": "Persian",
"jcuken_russian": "Russian (JCUKEN)",
"serbian_latin": "Serbian (QWERTZ)",
"serbian_cyrillic": "Serbian (ЉЊЕРТЗ)",
"spanish": "Spanish (QWERTY)",
"swedish_finnish": "Swedish/Finnish (QWERTY)",
"swiss_german": "Swiss German (QWERTZ)",
"swiss_french": "Swiss French (QWERTZ)",
"swiss_italian": "Swiss Italian (QWERTZ)",
"turkish_q": "Turkish-Q",
"turkish_f": "Turkish-F"
},
"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": [

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",

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

@@ -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,71 +4,29 @@
"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": 1570, "label": "آ" },
{ "code": 1569, "label": "ء" },
{ "code": 1571, "label": "أ" },
{ "code": 1573, "label": "إ" }
]
},
"ه": {
"relevant": [
{ "code": 1729, "label": "ہ" },
{ "code": 1728, "label": "ۀ" },
{ "code": 1726, "label": "ھ" }
]
},
"ت": {
"relevant": [
{ "code": 1577, "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

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

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

View File

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

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": [

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

@@ -59,6 +59,7 @@
"smartbarButton": {
"background": "#FFFFFF",
"foreground": "#686868"
}
},
"glideTrail": {"foreground": "#205e97f6"}
}
}

View File

@@ -5,6 +5,7 @@ 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
@@ -109,10 +110,9 @@ class FlorisContentProvider : ContentProvider() {
throw IllegalArgumentException("This ContentProvider does not support update.")
}
companion object {
private var instance: FlorisContentProvider? = null
const val AUTHORITY = "dev.patrickgold.florisboard.provider.clip"
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")
@@ -127,6 +127,5 @@ class FlorisContentProvider : ContentProvider() {
addURI(AUTHORITY, "clips/#", CLIP_ITEM)
addURI(AUTHORITY, "clips", CLIPS_TABLE)
}
}
}

View File

@@ -210,6 +210,45 @@ class EditorInstance private constructor(
}
}
/**
* 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].

View File

@@ -26,8 +26,11 @@ 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() {
abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity(), CoroutineScope by MainScope() {
private var _binding: V? = null
protected val binding: V
get() = _binding!!
@@ -54,6 +57,7 @@ abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
cancel()
_binding = null
_prefs = null
errorDialog?.dismiss()

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) {

View File

@@ -49,6 +49,7 @@ import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.popup.PopupLayerView
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.CurrencySet
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
@@ -59,7 +60,6 @@ import dev.patrickgold.florisboard.util.*
import timber.log.Timber
import java.lang.ref.WeakReference
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -402,6 +402,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
textInputManager.layoutManager.clearLayoutCache(KeyboardMode.CHARACTERS)
isNumberRowVisible = newIsNumberRowVisible
}
textInputManager.layoutManager.clearLayoutCache()
themeManager.update()
updateOneHandedPanelVisibility()
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
@@ -746,11 +747,13 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
clipInputManager.onSubtypeChanged(newSubtype)
}
fun setActiveInput(type: Int) {
fun setActiveInput(type: Int, forceSwitchToCharacters: Boolean = false) {
when (type) {
R.id.text_input -> {
inputView?.mainViewFlipper?.displayedChild = 0
textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.VIEW_CHARACTERS))
if (forceSwitchToCharacters) {
textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.VIEW_CHARACTERS))
}
}
R.id.media_input -> {
inputView?.mainViewFlipper?.displayedChild = 1
@@ -846,39 +849,58 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
* ime/config.json so it can be parsed. Used by [SubtypeManager] and by the prefs.
* NOTE: this class and its corresponding json file is subject to change in future versions.
* @property packageName The package name of this IME.
* @property characterLayouts A map of valid layout names to use from. Each value defined
* should have a <layout_name>.json file in ime/text/characters/ to avoid empty layouts.
* The key is the layout name, the value is the layout label (string shown in UI).
* @property currencySets The predefined currency sets for this IME, available for selection
* in the Settings UI.
* @property defaultSubtypes A list of predefined default subtypes. This subtypes are used to
* define which locales are supported and which layout is preferred for that locale.
* @property currencySetNames Helper list for Settings Subtype Spinner elements.
* @property currencySetLabels Helper list for Settings Subtype Spinner elements.
* @property defaultSubtypesLanguageCodes Helper list for Settings Subtype Spinner elements.
* @property defaultSubtypesLanguageNames Helper list for Settings Subtype Spinner elements.
*/
data class ImeConfig(
@Json(name = "package")
val packageName: String,
val characterLayouts: Map<String, String> = mapOf(),
val currencySets: List<CurrencySet> = listOf(),
val defaultSubtypes: List<DefaultSubtype> = listOf()
) {
val currencySetNames: List<String>
val currencySetLabels: List<String>
val defaultSubtypesLanguageCodes: List<String>
val defaultSubtypesLanguageNames: List<String>
init {
val tmpList = mutableListOf<Pair<String, String>>()
for (defaultSubtype in defaultSubtypes) {
tmpList.add(Pair(defaultSubtype.locale.toString(), defaultSubtype.locale.displayName))
val tmpCurrencyList = mutableListOf<Pair<String, String>>()
for (currencySet in currencySets) {
tmpCurrencyList.add(Pair(currencySet.name, currencySet.label))
}
// Sort language list alphabetically by the display name of a language
tmpList.sortBy { it.second }
// Move selected English variants to the top of the list
for (languageCode in listOf("en_CA", "en_AU", "en_UK", "en_US")) {
val index: Int = tmpList.indexOfFirst { it.first == languageCode }
// Sort currency set list alphabetically by the label of a currency set
tmpCurrencyList.sortBy { it.second }
// Move selected currency variants to the top of the list
for (currencyName in listOf("euro", "dollar")) {
val index: Int = tmpCurrencyList.indexOfFirst { it.first == currencyName }
if (index > 0) {
tmpList.add(0, tmpList.removeAt(index))
tmpCurrencyList.add(0, tmpCurrencyList.removeAt(index))
}
}
defaultSubtypesLanguageCodes = tmpList.map { it.first }.toList()
defaultSubtypesLanguageNames = tmpList.map { it.second }.toList()
currencySetNames = tmpCurrencyList.map { it.first }.toList()
currencySetLabels = tmpCurrencyList.map { it.second }.toList()
val tmpSubtypeList = mutableListOf<Pair<String, String>>()
for (defaultSubtype in defaultSubtypes) {
tmpSubtypeList.add(Pair(defaultSubtype.locale.toString(), defaultSubtype.locale.displayName))
}
// Sort language list alphabetically by the display name of a language
tmpSubtypeList.sortBy { it.second }
// Move selected English variants to the top of the list
for (languageCode in listOf("en_CA", "en_AU", "en_UK", "en_US")) {
val index: Int = tmpSubtypeList.indexOfFirst { it.first == languageCode }
if (index > 0) {
tmpSubtypeList.add(0, tmpSubtypeList.removeAt(index))
}
}
defaultSubtypesLanguageCodes = tmpSubtypeList.map { it.first }.toList()
defaultSubtypesLanguageNames = tmpSubtypeList.map { it.second }.toList()
}
}
}

View File

@@ -124,6 +124,7 @@ class PrefHelper(
}
companion object {
private val OLD_SUBTYPES_REGEX = """^([\-0-9]+\/[\-a-zA-Z0-9]+\/[a-zA-Z\_]+[;]*)+${'$'}""".toRegex()
private var defaultInstance: PrefHelper? = null
@Synchronized
@@ -147,8 +148,11 @@ class PrefHelper(
PreferenceManager.setDefaultValues(context, R.xml.prefs_typing, true)
//theme.dayThemeRef = "assets:ime/theme/floris_day.json"
//theme.nightThemeRef = "assets:ime/theme/floris_night.json"
//setPref(Keyboard.SUBTYPES, "")
//setPref(Internal.IS_IME_SET_UP, false)
//setPref(Localization.SUBTYPES, "-234/de-AT/euro/c=qwertz")
val subtypes = getPref(Localization.SUBTYPES, "")
if (subtypes.matches(OLD_SUBTYPES_REGEX)) {
setPref(Localization.SUBTYPES, "")
}
}
/**
@@ -270,6 +274,9 @@ class PrefHelper(
companion object {
const val ENABLED = "glide__enabled"
const val SHOW_TRAIL = "glide__show_trail"
const val TRAIL_DURATION = "glide__trail_fade_duration"
const val SHOW_PREVIEW = "glide__show_preview"
const val PREVIEW_REFRESH_DELAY = "glide__preview_refresh_delay"
}
var enabled: Boolean
@@ -278,6 +285,15 @@ class PrefHelper(
var showTrail: Boolean
get() = prefHelper.getPref(SHOW_TRAIL, false)
set(v) = prefHelper.setPref(SHOW_TRAIL, v)
var trailDuration: Int
get() = prefHelper.getPref(TRAIL_DURATION, 200)
set(v) = prefHelper.setPref(TRAIL_DURATION, v)
var showPreview: Boolean
get() = prefHelper.getPref(SHOW_PREVIEW, true)
set(v) = prefHelper.setPref(SHOW_PREVIEW, v)
var previewRefreshDelay: Int
get() = prefHelper.getPref(PREVIEW_REFRESH_DELAY, 150)
set(v) = prefHelper.setPref(PREVIEW_REFRESH_DELAY, v)
}
/**

View File

@@ -17,6 +17,7 @@
package dev.patrickgold.florisboard.ime.core
import com.squareup.moshi.Json
import dev.patrickgold.florisboard.ime.text.layout.LayoutType
import dev.patrickgold.florisboard.util.LocaleUtils
import java.util.*
@@ -26,44 +27,50 @@ import java.util.*
* @property id The ID of this subtype. Although this can be any numeric value, its value
* typically matches the one of the [DefaultSubtype] with the same locale.
* @property locale The locale this subtype is bound to.
* @property layout The name of the layout the user wants to use within the bounds of this subtype.
* Must be a string which also exists in [FlorisBoard.ImeConfig.characterLayouts]. If the value is
* not included within this list, no layout will be shown to the user.
* @property currencySetName The currency set name to display the correct currency symbols for this subtype.
* @property layoutMap The layout map to properly display the correct layout for each layout type.
*/
data class Subtype(
var id: Int,
var locale: Locale,
var layout: String
var currencySetName: String,
var layoutMap: SubtypeLayoutMap,
) {
companion object {
/**
* Subtype to use when prefs do not contain any valid subtypes.
*/
val DEFAULT = Subtype(-1, Locale.ENGLISH, "qwerty")
val DEFAULT = Subtype(
id = -1,
locale = Locale.ENGLISH,
currencySetName = "\$default",
layoutMap = SubtypeLayoutMap(characters = "qwerty")
)
/**
* Converts the string representation of this object to a [Subtype]. Must be in the
* following format:
* <id>/<language_code>/<layout_name>
* <id>/<language_code>/<currency_set_name>/c=<layout_name>
* or
* <id>/<language_tag>/<layout_name>
* Eg: 101/en_US/qwerty
* 201/de-DE/qwertz
* If the given [string] does not match this format an [InvalidPropertiesFormatException]
* <id>/<language_tag>/<currency_set_name>/c=<layout_name>
* Eg: 101/en_US/dollar/c=qwerty
* 201/de-DE/euro/c=qwertz
* If the given [str] does not match this format an [InvalidPropertiesFormatException]
* will be thrown.
*/
fun fromString(string: String): Subtype {
val data = string.split("/")
if (data.size != 3) {
fun fromString(str: String): Subtype {
val data = str.split("/")
if (data.size != 4) {
throw InvalidPropertiesFormatException(
"Given string contains more or less than 3 properties..."
"Given string contains more or less than 4 properties..."
)
} else {
val locale = LocaleUtils.stringToLocale(data[1])
return Subtype(
data[0].toInt(),
locale,
data[2]
data[2],
SubtypeLayoutMap.fromString(data[3])
)
}
}
@@ -71,26 +78,210 @@ data class Subtype(
/**
* Converts this object into its string representation. Format:
* <id>/<language_tag>/<layout_name>
* <id>/<language_tag>/<currency_set_name>/<layout_map>
*/
override fun toString(): String {
val languageTag = locale.toLanguageTag()
return "$id/$languageTag/$layout"
return "$id/$languageTag/$currencySetName/$layoutMap"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Subtype
if (id != other.id) return false
if (locale != other.locale) return false
if (currencySetName != other.currencySetName) return false
if (layoutMap != other.layoutMap) return false
return true
}
override fun hashCode(): Int {
var result = id
result = 31 * result + locale.hashCode()
result = 31 * result + currencySetName.hashCode()
result = 31 * result + layoutMap.hashCode()
return result
}
}
data class SubtypeLayoutMap(
val characters: String = CHARACTERS_DEFAULT,
val symbols: String = SYMBOLS_DEFAULT,
val symbols2: String = SYMBOLS2_DEFAULT,
val numeric: String = NUMERIC_DEFAULT,
val numericAdvanced: String = NUMERIC_ADVANCED_DEFAULT,
val numericRow: String = NUMERIC_ROW_DEFAULT,
val phone: String = PHONE_DEFAULT,
val phone2: String = PHONE2_DEFAULT,
) {
companion object {
private const val CHARACTERS_CODE = "c"
private const val SYMBOLS_CODE = "s"
private const val SYMBOLS2_CODE = "s2"
private const val NUMERIC_CODE = "n"
private const val NUMERIC_ADVANCED_CODE = "na"
private const val NUMERIC_ROW_CODE = "nr"
private const val PHONE_CODE = "p"
private const val PHONE2_CODE = "p2"
private const val EQUALS = "="
private const val DELIMITER = ","
private const val CHARACTERS_DEFAULT = "qwerty"
private const val SYMBOLS_DEFAULT = "western"
private const val SYMBOLS2_DEFAULT = "western"
private const val NUMERIC_DEFAULT = "western_arabic"
private const val NUMERIC_ADVANCED_DEFAULT = "western_arabic"
private const val NUMERIC_ROW_DEFAULT = "western_arabic"
private const val PHONE_DEFAULT = "telpad"
private const val PHONE2_DEFAULT = "telpad"
fun fromString(str: String): SubtypeLayoutMap {
var characters: String = CHARACTERS_DEFAULT
var symbols: String = SYMBOLS_DEFAULT
var symbols2: String = SYMBOLS2_DEFAULT
var numeric: String = NUMERIC_DEFAULT
var numericAdvanced: String = NUMERIC_ADVANCED_DEFAULT
var numericRow: String = NUMERIC_ROW_DEFAULT
var phone: String = PHONE_DEFAULT
var phone2: String = PHONE2_DEFAULT
for (layout in str.split(DELIMITER)) {
val layoutSplit = layout.split(EQUALS)
if (layoutSplit.size == 2) {
val code = layoutSplit[0].trim()
val layoutName = layoutSplit[1].trim()
when (code) {
CHARACTERS_CODE -> characters = layoutName
SYMBOLS_CODE -> symbols = layoutName
SYMBOLS2_CODE -> symbols2 = layoutName
NUMERIC_CODE -> numeric = layoutName
NUMERIC_ADVANCED_CODE -> numericAdvanced = layoutName
NUMERIC_ROW_CODE -> numericRow = layoutName
PHONE_CODE -> phone = layoutName
PHONE2_CODE -> phone2 = layoutName
}
}
}
return SubtypeLayoutMap(
characters, symbols, symbols2, numeric, numericAdvanced, numericRow, phone, phone2
)
}
}
operator fun get(layoutType: LayoutType): String? {
return when (layoutType) {
LayoutType.CHARACTERS -> characters
LayoutType.SYMBOLS -> symbols
LayoutType.SYMBOLS2 -> symbols2
LayoutType.NUMERIC -> numeric
LayoutType.NUMERIC_ADVANCED -> numericAdvanced
LayoutType.NUMERIC_ROW -> numericRow
LayoutType.PHONE -> phone
LayoutType.PHONE2 -> phone2
else -> null
}
}
override fun toString(): String {
return StringBuilder(128).run {
append(CHARACTERS_CODE)
append(EQUALS)
append(characters)
append(DELIMITER)
append(SYMBOLS_CODE)
append(EQUALS)
append(symbols)
append(DELIMITER)
append(SYMBOLS2_CODE)
append(EQUALS)
append(symbols2)
append(DELIMITER)
append(NUMERIC_ROW_CODE)
append(EQUALS)
append(numericRow)
append(DELIMITER)
append(NUMERIC_CODE)
append(EQUALS)
append(numeric)
append(DELIMITER)
append(NUMERIC_ADVANCED_CODE)
append(EQUALS)
append(numericAdvanced)
append(DELIMITER)
append(PHONE_CODE)
append(EQUALS)
append(phone)
append(DELIMITER)
append(PHONE2_CODE)
append(EQUALS)
append(phone2)
toString()
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SubtypeLayoutMap
if (characters != other.characters) return false
if (symbols != other.symbols) return false
if (symbols2 != other.symbols2) return false
if (numeric != other.numeric) return false
if (numericAdvanced != other.numericAdvanced) return false
if (numericRow != other.numericRow) return false
if (phone != other.phone) return false
if (phone2 != other.phone2) return false
return true
}
override fun hashCode(): Int {
var result = characters.hashCode()
result = 31 * result + symbols.hashCode()
result = 31 * result + symbols2.hashCode()
result = 31 * result + numeric.hashCode()
result = 31 * result + numericAdvanced.hashCode()
result = 31 * result + numericRow.hashCode()
result = 31 * result + phone.hashCode()
result = 31 * result + phone2.hashCode()
return result
}
}
/**
* Data class which represents a predefined set of language and preferred layout.
*
* @property id The ID of this subtype.
* @property locale The locale of this subtype. Beware its different name in json: 'languageTag'.
* @property preferredLayout The preferred layout for this subtype's locale.
* Must be a string which also exists in [FlorisBoard.ImeConfig.characterLayouts]. If the value is
* not included within this list, no layout will be shown to the user if the user selects the
* predefined layout value.
* @property currencySetName The currency set name of this subtype. Beware its different name in json: 'currencySet'.
* @property preferred The preferred layout map for this subtype's locale.
*/
data class DefaultSubtype(
var id: Int,
@Json(name = "languageTag")
var locale: Locale,
var preferredLayout: String
@Json(name = "currencySet")
var currencySetName: String,
var preferred: SubtypeLayoutMap
)

View File

@@ -19,11 +19,10 @@ package dev.patrickgold.florisboard.ime.core
import android.content.Context
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dev.patrickgold.florisboard.ime.text.key.CurrencySet
import dev.patrickgold.florisboard.util.LocaleUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import java.util.*
/**
@@ -37,7 +36,6 @@ import java.util.*
* list of [Subtype]s. When setting this property, the given list is converted to a raw string
* and written to prefs.
*/
@Suppress("SameParameterValue")
class SubtypeManager(
private val context: Context,
private val prefs: PrefHelper
@@ -65,9 +63,7 @@ class SubtypeManager(
}
init {
launch(Dispatchers.IO) {
imeConfig = loadImeConfig(IME_CONFIG_FILE_PATH)
}
imeConfig = loadImeConfig(IME_CONFIG_FILE_PATH)
}
/**
@@ -110,24 +106,36 @@ class SubtypeManager(
}
/**
* Creates a [Subtype] from the given [locale] and [layoutName] and adds it to the subtype
* Creates a [Subtype] from the given [locale] and [layoutMap] and adds it to the subtype
* list, if it does not exist.
*
* @param locale The locale of the subtype to be added.
* @param layoutName The layout name of the subtype to be added.
* @param currencySetName The currency set name of the subtype to be added.
* @param layoutMap The layout map of the subtype to be added.
* @return True if the subtype was added, false otherwise. A return value of false indicates
* that the subtype already exists.
*/
fun addSubtype(locale: Locale, layoutName: String): Boolean {
fun addSubtype(locale: Locale, currencySetName: String, layoutMap: SubtypeLayoutMap): Boolean {
return addSubtype(
Subtype(
(locale.hashCode() + layoutName.hashCode()),
(locale.hashCode() + 31 * layoutMap.hashCode() + 31 * currencySetName.hashCode()),
locale,
layoutName
currencySetName,
layoutMap
)
)
}
/**
* Gets the currency set from the given subtype and returns it. Falls back to a default one if the subtype does not
* exist.
*
* @return The currency set or a fallback.
*/
fun getCurrencySet(subtypeToSearch: Subtype): CurrencySet {
return imeConfig.currencySets.find { it.name == subtypeToSearch.currencySetName } ?: CurrencySet.default()
}
/**
* Gets the active subtype and returns it. If the activeSubtypeId points to a non-existent
* subtype, this method tries to determine a new active subtype.
@@ -193,7 +201,8 @@ class SubtypeManager(
for (subtype in subtypeList) {
if (subtype.id == subtypeToModify.id) {
subtype.locale = subtypeToModify.locale
subtype.layout = subtypeToModify.layout
subtype.currencySetName = subtypeToModify.currencySetName
subtype.layoutMap = subtypeToModify.layoutMap
break
}
}

View File

@@ -55,10 +55,5 @@ interface Asset {
* Creates an empty Asset of type [T].
*/
fun empty(): T
/**
* Loads an Asset of type [T] from the specified path.
*/
fun fromFile(context: Context, path: String): Result<T> = Result.failure(NotImplementedError())
}
}

View File

@@ -58,6 +58,8 @@ class AssetManager private constructor(private val applicationContext: Context)
)
}
}
fun defaultOrNull(): AssetManager? = defaultInstance
}
fun deleteAsset(ref: AssetRef): Result<Unit> {

View File

@@ -127,7 +127,7 @@ class PopupManager<T_KBD: View, T_KV: View>(
}
else -> {
PopupExtendedView.Element.Label(
keyView.getComputedLetter(keyView.data.popup[adjustedIndex]), adjustedIndex
keyView.getComputedLetter(keyView.data.popup[adjustedIndex], isForDisplay = true), adjustedIndex
)
}
}

View File

@@ -90,7 +90,7 @@ class PopupSet<T> (
}
override fun iterator(): Iterator<T> {
TODO("Not yet implemented")
return PopupSetIterator(this)
}
override fun isEmpty(): Boolean {
@@ -116,4 +116,30 @@ class PopupSet<T> (
}
relevant = tempRelevant.toList()
}
class PopupSetIterator<T> internal constructor (
private val popupSet: PopupSet<T>
) : Iterator<T> {
var index = HINT_INDEX
override fun next(): T = popupSet[index++]
override fun hasNext(): Boolean {
if (index == HINT_INDEX) {
if (popupSet.getOrNull(index) != null) {
return true
} else {
index++
}
}
if (index == MAIN_INDEX) {
if (popupSet.getOrNull(index) != null) {
return true
} else {
index++
}
}
return popupSet.getOrNull(index) != null
}
}
}

View File

@@ -33,6 +33,7 @@ import dev.patrickgold.florisboard.ime.extension.AssetSource
import dev.patrickgold.florisboard.ime.nlp.Token
import dev.patrickgold.florisboard.ime.nlp.toStringList
import dev.patrickgold.florisboard.ime.text.editing.EditingKeyboardView
import dev.patrickgold.florisboard.ime.text.gestures.GlideTypingManager
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.*
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
@@ -58,10 +59,13 @@ import kotlin.math.roundToLong
class TextInputManager private constructor() : CoroutineScope by MainScope(), InputKeyEventReceiver,
FlorisBoard.EventListener, SmartbarView.EventListener {
var glideSuggestionsActive: Boolean = false
private val florisboard = FlorisBoard.getInstance()
private val activeEditorInstance: EditorInstance
get() = florisboard.activeEditorInstance
lateinit var layoutManager: LayoutManager
private set
private var activeKeyboardMode: KeyboardMode? = null
private var animator: ObjectAnimator? = null
private val keyboardViews = EnumMap<KeyboardMode, KeyboardView>(KeyboardMode::class.java)
@@ -84,8 +88,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
)
var keyVariation: KeyVariation = KeyVariation.NORMAL
val layoutManager = LayoutManager(florisboard)
private var smartbarView: SmartbarView? = null
internal var smartbarView: SmartbarView? = null
// Caps/Shift related properties
var caps: Boolean = false
@@ -122,6 +125,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
override fun onCreate() {
Timber.i("onCreate()")
layoutManager = LayoutManager(this)
inputEventDispatcher.keyEventReceiver = this
var subtypes = florisboard.subtypeManager.subtypes
if (subtypes.isEmpty()) {
@@ -129,7 +133,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
}
for (subtype in subtypes) {
for (mode in KeyboardMode.values()) {
layoutManager.preloadComputedLayout(mode, subtype, florisboard.prefs)
layoutManager.preloadComputedLayout(mode, subtype, florisboard.prefs, florisboard.subtypeManager.getCurrencySet(subtype))
}
}
}
@@ -140,7 +144,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
private suspend fun addKeyboardView(mode: KeyboardMode) {
val keyboardView = KeyboardView(florisboard.context)
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(mode, florisboard.activeSubtype, florisboard.prefs).await()
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(mode, florisboard.activeSubtype, florisboard.prefs, florisboard.subtypeManager.getCurrencySet(florisboard.activeSubtype)).await()
keyboardViews[mode] = keyboardView
textViewFlipper?.addView(keyboardView)
}
@@ -321,16 +325,31 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
launch {
if (activeEditorInstance.isComposingEnabled) {
withContext(Dispatchers.IO) {
dictionaryManager.loadDictionary(AssetRef(AssetSource.Assets,"ime/dict/en.flict")).let {
dictionaryManager.loadDictionary(AssetRef(AssetSource.Assets, "ime/dict/en.flict")).let {
activeDictionary = it.getOrDefault(null)
}
}
}
val keyboardView = keyboardViews[KeyboardMode.CHARACTERS]
keyboardView?.computedLayout = layoutManager.fetchComputedLayoutAsync(KeyboardMode.CHARACTERS, newSubtype, florisboard.prefs).await()
keyboardView?.updateVisibility()
if (PrefHelper.getDefaultInstance(florisboard.context).glide.enabled) {
GlideTypingManager.getInstance().setWordData(newSubtype)
}
// TODO: heavy load on main thread
for (keyboardMode in KeyboardMode.values()) {
val keyboardView = keyboardViews[keyboardMode]
if (keyboardView != null) {
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(
keyboardMode,
newSubtype,
florisboard.prefs,
florisboard.subtypeManager.getCurrencySet(newSubtype)
).await()
keyboardView.updateVisibility()
}
}
}
}
// this is so unfortunate... but we need to skip clearing the suggestion only one time.
var hackyGlideSuggestionSkip = false
/**
* Main logic point for processing cursor updates as well as parsing the current composing word
@@ -367,7 +386,17 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
}
}
} else {
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
if (glideSuggestionsActive){
if (hackyGlideSuggestionSkip) {
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
hackyGlideSuggestionSkip = false
glideSuggestionsActive = false
}else {
hackyGlideSuggestionSkip = true
}
}else {
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
}
}
}
}
@@ -419,7 +448,14 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
}
override fun onSmartbarCandidatePressed(word: String) {
activeEditorInstance.commitCompletion(word)
if (glideSuggestionsActive) {
activeEditorInstance.commitGestureCorrection(word)
glideSuggestionsActive = false
hackyGlideSuggestionSkip = false
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
}else {
activeEditorInstance.commitCompletion(word)
}
}
override fun onSmartbarClipboardCandidatePressed(clipboardItem: ClipboardItem) {
@@ -459,10 +495,17 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
* Handles a [KeyCode.DELETE] event.
*/
private fun handleDelete() {
isManualSelectionMode = false
isManualSelectionModeStart = false
isManualSelectionModeEnd = false
activeEditorInstance.deleteBackwards()
if (glideSuggestionsActive){
handleDeleteWord()
glideSuggestionsActive = false
hackyGlideSuggestionSkip = false
this.smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
}else {
isManualSelectionMode = false
isManualSelectionModeStart = false
isManualSelectionModeEnd = false
activeEditorInstance.deleteBackwards()
}
}
/**
@@ -716,7 +759,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
KeyCode.SPACE -> handleSpace(ev)
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> florisboard.setActiveInput(R.id.media_input)
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT -> florisboard.setActiveInput(R.id.clip_input)
KeyCode.SWITCH_TO_TEXT_CONTEXT -> florisboard.setActiveInput(R.id.text_input)
KeyCode.SWITCH_TO_TEXT_CONTEXT -> florisboard.setActiveInput(R.id.text_input, forceSwitchToCharacters = true)
KeyCode.CLEAR_CLIPBOARD_HISTORY -> florisboard.florisClipboardManager?.clearHistoryWithAnimation()
KeyCode.TOGGLE_ONE_HANDED_MODE_LEFT -> florisboard.toggleOneHandedMode(isRight = false)
KeyCode.TOGGLE_ONE_HANDED_MODE_RIGHT -> florisboard.toggleOneHandedMode(isRight = true)
@@ -785,4 +828,22 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
KeyCode.SHIFT -> handleShiftCancel()
}
}
fun handleGesture(word: String) {
activeEditorInstance.commitGesture(fixCase(word))
}
/**
* Changes a word to the current case.
* eg if [capsLock] is true, abc -> ABC
* if [caps] is true, abc -> Abc
* otherwise , abc -> abc
*/
fun fixCase(word: String): String {
return when {
capsLock -> word.toUpperCase(florisboard.activeSubtype.locale)
caps -> word.capitalize(florisboard.activeSubtype.locale)
else -> word
}
}
}

View File

@@ -21,6 +21,7 @@ import android.util.AttributeSet
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
@@ -55,6 +56,9 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
cutKey = findViewById(R.id.clipboard_cut)
copyKey = findViewById(R.id.clipboard_copy)
pasteKey = findViewById(R.id.clipboard_paste)
val clipboardManager = FlorisClipboardManager.getInstance()
pasteKey?.isEnabled = clipboardManager.canBePasted(clipboardManager.primaryClip)
}
override fun onDetachedFromWindow() {
@@ -80,11 +84,8 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
else -> View.GONE
}
copyKey?.isEnabled = isSelectionActive
pasteKey?.isEnabled =
florisboard?.florisClipboardManager?.hasPrimaryClip() == true &&
florisboard.activeEditorInstance.contentMimeTypes?.any {
florisboard.florisClipboardManager!!.primaryClip?.mimeTypes?.contains(it) ?: false
} == true
val clipboardManager = FlorisClipboardManager.getInstance()
pasteKey?.isEnabled = clipboardManager.canBePasted(clipboardManager.primaryClip)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

View File

@@ -0,0 +1,43 @@
package dev.patrickgold.florisboard.ime.text.gestures
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.text.key.KeyView
import dev.patrickgold.florisboard.ime.text.layout.ComputedLayout
/**
* Inherit this to be able to handle gesture typing. Takes in raw pointer data, and
* spits out what it thinks the gesture is.
*/
interface GlideTypingClassifier {
/**
* Called to notify gesture classifier that it can add a new point to the gesture.
* @param position The position to add
*/
fun addGesturePoint(position: GlideTypingGesture.Detector.Position)
/**
* Change the layout of the gesture classifier.
*/
fun setLayout(keyViews: Sequence<KeyView>, subtype: Subtype)
/**
* Change the word data of the gesture classifier.
*/
fun setWordData(words: HashMap<String, Int>, subtype: Subtype)
/**
* Process a completed gesture and find its location.
*/
fun initGestureFromPointerData(pointerData: GlideTypingGesture.Detector.PointerData)
/**
* Generate suggestions to show to the user.
*
* @param maxSuggestionCount The maximum number of suggestions that are accepted.
* @param gestureCompleted Whether the gesture is finished. (e.g to use a different algorithm for in progress words)
*/
fun getSuggestions(maxSuggestionCount: Int, gestureCompleted: Boolean): List<String>
fun clear()
}

View File

@@ -0,0 +1,160 @@
package dev.patrickgold.florisboard.ime.text.gestures
import android.content.Context
import android.view.MotionEvent
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import kotlin.math.pow
import kotlin.math.sqrt
/**
* Wrapper class which holds all enums, interfaces and classes for detecting a gesture.
*/
class GlideTypingGesture {
/**
* Class which detects swipes based on given [MotionEvent]s. Only supports single-finger swipes
* and ignores additional pointers provided, if any.
*/
class Detector(context: Context) {
private var pointerData: PointerData = PointerData(mutableListOf(), 0)
var velocityThreshold: VelocityThreshold = VelocityThreshold.NORMAL
private val keySize = context.resources.getDimensionPixelSize(R.dimen.key_width).toDouble()
private val listeners: ArrayList<Listener> = arrayListOf()
private var pointerId: Int = -1
companion object {
private const val MAX_DETECT_TIME = 500
private const val VELOCITY_THRESHOLD = 0.65
private val SWIPE_GESTURE_KEYS = arrayOf(KeyCode.DELETE, KeyCode.SHIFT, KeyCode.SPACE)
}
/**
* Method which evaluates if a given [event] is a gesture.
* @return whether or not the event was interpreted as part of a gesture.
*/
fun onTouchEvent(event: MotionEvent, initialKeyCodes: MutableMap<Int, Int>): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN -> {
if (pointerId != -1) {
// if we already have another pointer, we don't care
return false
}
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
resetState()
}
val pointerIndex = event.actionIndex
pointerId = event.getPointerId(pointerIndex)
pointerData.apply {
positions.add(Position(event.getX(pointerIndex), event.getY(pointerIndex)))
startTime = System.currentTimeMillis()
}
return false
}
MotionEvent.ACTION_MOVE -> {
if (pointerId != event.getPointerId(event.actionIndex)) {
// not our pointer.
return false
}
val pointerIndex = event.findPointerIndex(pointerId)
val pos = Position(event.getX(pointerIndex), event.getY(pointerIndex))
pointerData.positions.add(
pos
)
if (pointerData.isActuallyGesture == null) {
// evaluate whether is actually a gesture
val dist = pointerData.positions[0].dist(pos)
val time = (System.currentTimeMillis() - pointerData.startTime) + 1
if (dist > keySize && (dist / time) > VELOCITY_THRESHOLD && (initialKeyCodes[pointerId] !in SWIPE_GESTURE_KEYS)) {
pointerData.isActuallyGesture = true
// Let listener know all those points need to be added.
pointerData.positions.take(pointerData.positions.size - 1).forEach { point ->
listeners.forEach {
it.onGestureAdd(point)
}
}
} else if (time > MAX_DETECT_TIME) {
pointerData.isActuallyGesture = false
}
}
if (pointerData.isActuallyGesture == true)
pointerData.positions.last().let { point -> listeners.forEach { it.onGestureAdd(point) } }
return pointerData.isActuallyGesture ?: false
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> {
if (pointerId != event.getPointerId(event.actionIndex)) {
// not our pointer.
return false
}
if (pointerData.isActuallyGesture == true) {
listeners.forEach { listener -> listener.onGestureComplete(pointerData) }
}
resetState()
return false
}
MotionEvent.ACTION_CANCEL -> {
if (pointerData.isActuallyGesture == true) {
listeners.forEach { it.onGestureCancelled() }
}
resetState()
}
else -> return false
}
return false
}
fun registerListener(listener: Listener) {
this.listeners.add(listener)
}
private fun resetState() {
pointerData.apply {
positions.clear()
startTime = 0
isActuallyGesture = null
}
pointerId = -1
}
data class PointerData(
val positions: MutableList<Position>,
var startTime: Long,
var isActuallyGesture: Boolean? = null
)
data class Position(
val x: Float,
val y: Float
) {
fun dist(p2: Position): Float {
return sqrt((p2.x - x).pow(2) + (p2.y - y).pow(2))
}
}
}
interface Listener {
/**
* Called when a gesture is complete.
*/
fun onGestureComplete(data: Detector.PointerData) {}
/**
* Called when a point is added to a gesture.
* Will not be called before a series of events is detected as a gesture.
*/
fun onGestureAdd(point: Detector.Position) {}
/**
* Called to cancel a gesture
*/
fun onGestureCancelled() {}
}
}

View File

@@ -0,0 +1,167 @@
package dev.patrickgold.florisboard.ime.text.gestures
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.extension.AssetManager
import dev.patrickgold.florisboard.ime.extension.AssetRef
import dev.patrickgold.florisboard.ime.extension.AssetSource
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.key.KeyView
import kotlinx.coroutines.*
import org.json.JSONObject
/**
* Handles the [GlideTypingClassifier]. Basically responsible for linking [GlideTypingGesture.Detector]
* with [GlideTypingClassifier].
*/
class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainScope() {
private var glideTypingClassifier = StatisticalGlideTypingClassifier()
private val initialDimensions: HashMap<Subtype, Dimensions> = hashMapOf()
private var currentDimensions: Dimensions = Dimensions(0f, 0f)
private lateinit var prefHelper: PrefHelper
companion object {
private const val MAX_SUGGESTION_COUNT = 8
private lateinit var glideTypingManager: GlideTypingManager
fun getInstance(): GlideTypingManager {
if (!this::glideTypingManager.isInitialized) {
glideTypingManager = GlideTypingManager()
glideTypingManager.prefHelper = PrefHelper.getDefaultInstance(FlorisBoard.getInstance().context)
}
return glideTypingManager
}
}
override fun onGestureComplete(data: GlideTypingGesture.Detector.PointerData) {
updateSuggestionsAsync(MAX_SUGGESTION_COUNT, true) {
glideTypingClassifier.clear()
}
}
override fun onGestureCancelled() {
glideTypingClassifier.clear()
}
private var lastTime = System.currentTimeMillis()
override fun onGestureAdd(point: GlideTypingGesture.Detector.Position) {
val normalized = GlideTypingGesture.Detector.Position(normalizeX(point.x), normalizeY(point.y))
this.glideTypingClassifier.addGesturePoint(normalized)
val time = System.currentTimeMillis()
if (prefHelper.glide.showPreview && time - lastTime > prefHelper.glide.previewRefreshDelay) {
updateSuggestionsAsync(1, false) {}
lastTime = time
}
}
/**
* Change the layout of the internal gesture classifier
*/
fun setLayout(keys: Sequence<KeyView>, dimensions: Dimensions) {
glideTypingClassifier.setLayout(keys, FlorisBoard.getInstance().activeSubtype)
initialDimensions.getOrPut(FlorisBoard.getInstance().activeSubtype, {
dimensions
})
}
/**
* Set the word data for the internal gesture classifier
*/
fun setWordData(subtype: Subtype) {
launch(Dispatchers.Default) {
// FIXME: get this info from dictionary.
val data =
AssetManager.default().loadAssetRaw(AssetRef(AssetSource.Assets, "ime/dict/data.json")).getOrThrow()
val json = JSONObject(data)
val map = hashMapOf<String, Int>()
map.putAll(json.keys().asSequence().map { Pair(it, json.getInt(it)) })
glideTypingClassifier.setWordData(map, subtype)
}
}
fun updateDimensions(dimensions: Dimensions) {
this.currentDimensions = dimensions
}
/**
* To avoid constantly having to regenerate Pruners every time we switch between landscape and portrait or enable/
* disable one handed mode, we just normalize the x, y coordinates to the same range as the original which were
* active when the Pruner was created.
*/
private fun normalizeX(x: Float): Float {
val initial = initialDimensions[FlorisBoard.getInstance().activeSubtype] ?: return x
return scaleRange(
x,
0f,
currentDimensions.width,
0f,
initial.width
)
}
/**
* To avoid constantly having to regenerate Pruners every time we switch between landscape and portrait or enable/
* disable one handed mode, we just normalize the x, y coordinates to the same range as the original which were
* active when the Pruner was created.
*/
private fun normalizeY(y: Float): Float {
val initial = initialDimensions[FlorisBoard.getInstance().activeSubtype] ?: return y
return scaleRange(
y,
0f,
currentDimensions.height,
0f,
initial.height
)
}
private fun scaleRange(x: Float, oldMin: Float, oldMax: Float, newMin: Float, newMax: Float): Float {
return (((x - oldMin) * (newMax - newMin)) / (oldMax - oldMin)) + newMin
}
/**
* Asks gesture classifier for suggestions and then passes that on to the smartbar.
* Also commits the most confident suggestion if [commit] is set. All happens on an async executor.
* NB: only fetches [MAX_SUGGESTION_COUNT] suggestions.
*
* @param callback Called when this function completes. Takes a boolean, which indicates if suggestions
* were successfully set.
*/
private fun updateSuggestionsAsync(maxSuggestionsToShow: Int, commit: Boolean, callback: (Boolean) -> Unit) {
if (!glideTypingClassifier.ready) {
callback.invoke(false)
return
}
launch(Dispatchers.Default) {
// To avoid cache misses when maxSuggestions goes from 5 to 1.
val suggestions = glideTypingClassifier.getSuggestions(MAX_SUGGESTION_COUNT, true)
withContext(Dispatchers.Main) {
val textInputManager = TextInputManager.getInstance()
textInputManager.glideSuggestionsActive = true
textInputManager.hackyGlideSuggestionSkip = false
textInputManager.smartbarView?.setCandidateSuggestionWords(
System.nanoTime(),
suggestions.take(maxSuggestionsToShow).map { textInputManager.fixCase(it) }
)
textInputManager.smartbarView?.updateCandidateSuggestionCapsState()
if (commit && suggestions.isNotEmpty()) {
textInputManager.handleGesture(suggestions.first())
}
callback.invoke(true)
}
}
}
}
data class Dimensions(
val width: Float,
val height: Float
)

View File

@@ -0,0 +1,593 @@
package dev.patrickgold.florisboard.ime.text.gestures
import android.util.SparseArray
import androidx.collection.LruCache
import androidx.core.util.set
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.text.key.KeyView
import java.text.Normalizer
import java.util.*
import kotlin.collections.HashMap
import kotlin.math.*
/**
* Classifies gestures by comparing them with an "ideal gesture".
*
* Check out Étienne Desticourt's excellent write up at https://github.com/AnySoftKeyboard/AnySoftKeyboard/pull/1870
*/
class StatisticalGlideTypingClassifier : GlideTypingClassifier {
private val gesture = Gesture()
private var keysByCharacter: SparseArray<KeyView> = SparseArray()
private var words: Set<String> = setOf()
private var wordFrequencies: Map<String, Int> = hashMapOf()
private var keys: ArrayList<KeyView> = arrayListOf()
private lateinit var pruner: Pruner
private var wordDataSubtype: Subtype? = null
private var layoutSubtype: Subtype? = null
private var currentSubtype: Subtype? = null
val ready: Boolean
get() = currentSubtype == layoutSubtype && wordDataSubtype == layoutSubtype && wordDataSubtype != null
companion object {
/**
* Describes the allowed length variance in a gesture. If a gesture is too long or too short, it is immediately
* discarded to save cycles.
*/
private const val PRUNING_LENGTH_THRESHOLD = 8.42
/**
* describes the number of points to sample a gesture at, i.e the resolution.
*/
private const val SAMPLING_POINTS: Int = 200
/**
* The minimum distance between points to be added to a gesture.
*/
private const val MIN_DIST_TO_ADD = 1000
/**
* Standard deviation of the distribution of distances between the shapes of two gestures
* representing the same word. It's expressed for normalized gestures and is therefore
* independent of the keyboard or key size.
*/
private const val SHAPE_STD = 22.08f
/**
* Standard deviation of the distribution of distances between the locations of two gestures
* representing the same word. It's expressed as a factor of key radius as it's applied to
* un-normalized gestures and is therefore dependent on the size of the keys/keyboard.
*/
private const val LOCATION_STD = 0.5109f
/**
* This is a very small cache that caches suggestions, so that they aren't recalculated e.g when releasing
* a pointer when the suggestions were already calculated. Avoids a lot of micro pauses.
*/
private const val SUGGESTION_CACHE_SIZE = 5
/**
* For multiple subtypes, the pruner is cached.
*/
private const val PRUNER_CACHE_SIZE = 5
}
override fun addGesturePoint(position: GlideTypingGesture.Detector.Position) {
if (!gesture.isEmpty) {
val dx = gesture.getLastX() - position.x
val dy = gesture.getLastY() - position.y
if (dx * dx + dy * dy > MIN_DIST_TO_ADD) {
gesture.addPoint(position.x, position.y)
}
} else {
gesture.addPoint(position.x, position.y)
}
}
override fun setLayout(keyViews: Sequence<KeyView>, subtype: Subtype) {
// stop duplicate calls
if (this.layoutSubtype == subtype) {
return
}
keysByCharacter.clear()
keys.clear()
keyViews.forEach {
keysByCharacter[it.data.code] = it
this.keys.add(it)
}
layoutSubtype = subtype
initializePruner()
}
override fun setWordData(words: HashMap<String, Int>, subtype: Subtype) {
// stop duplicate calls..
if (this.wordDataSubtype == subtype) {
return
}
this.words = words.keys
this.wordFrequencies = words
this.wordDataSubtype = subtype
initializePruner()
}
private val prunerCache = LruCache<Subtype, Pruner>(PRUNER_CACHE_SIZE)
/**
* Exists because Pruner requires both word data and layout are initialized,
* however we don't know what order they're initialized in.
*/
private fun initializePruner() {
if (this.layoutSubtype == null || this.wordDataSubtype != this.layoutSubtype) {
// not yet ready
return
}
val currentSubtype = this.layoutSubtype!!
val cached = prunerCache.get(currentSubtype)
if (cached == null) {
this.pruner = Pruner(PRUNING_LENGTH_THRESHOLD, this.words, keysByCharacter)
prunerCache.put(currentSubtype, this.pruner)
} else {
this.pruner = cached
}
this.currentSubtype = currentSubtype
}
override fun initGestureFromPointerData(pointerData: GlideTypingGesture.Detector.PointerData) {
for (position in pointerData.positions) {
addGesturePoint(position)
}
}
private val lruSuggestionCache = LruCache<Pair<Gesture, Int>, List<String>>(SUGGESTION_CACHE_SIZE)
override fun getSuggestions(maxSuggestionCount: Int, gestureCompleted: Boolean): List<String> {
return when (val cached = lruSuggestionCache.get(Pair(this.gesture, maxSuggestionCount))) {
null -> {
val suggestions = unCachedGetSuggestions(maxSuggestionCount)
lruSuggestionCache.put(Pair(this.gesture.clone(), maxSuggestionCount), suggestions)
suggestions
}
else -> {
cached
}
}
}
private fun unCachedGetSuggestions(maxSuggestionCount: Int): List<String> {
val candidates = arrayListOf<String>()
val candidateWeights = arrayListOf<Float>()
val key = keys.firstOrNull() ?: return listOf()
val radius: Int = min(key.height, key.width)
var remainingWords = pruner.pruneByExtremities(gesture, this.keys)
val userGesture = gesture.resample(SAMPLING_POINTS)
val normalizedUserGesture: Gesture = userGesture.normalizeByBoxSide()
remainingWords = pruner.pruneByLength(gesture, remainingWords, keysByCharacter, keys)
for (i in remainingWords.indices) {
val word = remainingWords[i]
val idealGestures = Gesture.generateIdealGestures(word, keysByCharacter)
for (idealGesture in idealGestures) {
val wordGesture = idealGesture.resample(SAMPLING_POINTS)
val normalizedGesture: Gesture = wordGesture.normalizeByBoxSide()
val shapeDistance = calcShapeDistance(normalizedGesture, normalizedUserGesture)
val locationDistance = calcLocationDistance(wordGesture, userGesture)
val shapeProbability = calcGaussianProbability(shapeDistance, 0.0f, SHAPE_STD)
val locationProbability = calcGaussianProbability(locationDistance, 0.0f, LOCATION_STD * radius)
val frequency = wordFrequencies[word]!!
val confidence = 1.0f / (shapeProbability * locationProbability * frequency)
var candidateDistanceSortedIndex = 0
var duplicateIndex = Int.MAX_VALUE
while (candidateDistanceSortedIndex < candidateWeights.size
&& candidateWeights[candidateDistanceSortedIndex] <= confidence
) {
if (candidates[candidateDistanceSortedIndex].contentEquals(word)) duplicateIndex =
candidateDistanceSortedIndex
candidateDistanceSortedIndex++
}
if (candidateDistanceSortedIndex < maxSuggestionCount && candidateDistanceSortedIndex <= duplicateIndex) {
if (duplicateIndex < Int.MAX_VALUE) {
candidateWeights.removeAt(duplicateIndex)
candidates.removeAt(duplicateIndex)
}
candidateWeights.add(candidateDistanceSortedIndex, confidence)
candidates.add(candidateDistanceSortedIndex, word)
if (candidateWeights.size > maxSuggestionCount) {
candidateWeights.removeAt(maxSuggestionCount)
candidates.removeAt(maxSuggestionCount)
}
}
}
}
return candidates
}
override fun clear() {
this.gesture.clear()
}
private fun calcLocationDistance(gesture1: Gesture, gesture2: Gesture): Float {
var totalDistance = 0.0f
for (i in 0 until SAMPLING_POINTS) {
val x1 = gesture1.getX(i)
val x2 = gesture2.getX(i)
val y1 = gesture1.getY(i)
val y2 = gesture2.getY(i)
val distance = abs(x1 - x2) + abs(y1 - y2)
totalDistance += distance
}
return totalDistance / SAMPLING_POINTS / 2
}
private fun calcGaussianProbability(value: Float, mean: Float, standardDeviation: Float): Float {
val factor = 1.0 / (standardDeviation * sqrt(2 * PI))
val exponent = ((value - mean) / standardDeviation).toDouble().pow(2.0)
val probability = factor * exp(-1.0 / 2 * exponent)
return probability.toFloat()
}
private fun calcShapeDistance(gesture1: Gesture, gesture2: Gesture): Float {
var distance: Float
var totalDistance = 0.0f
for (i in 0 until SAMPLING_POINTS) {
val x1 = gesture1.getX(i)
val x2 = gesture2.getX(i)
val y1 = gesture1.getY(i)
val y2 = gesture2.getY(i)
distance = Gesture.distance(x1, y1, x2, y2)
totalDistance += distance
}
return totalDistance
}
class Pruner(
/**
* The length difference between a user gesture and a word gesture above which a word will
* be pruned.
*/
private val lengthThreshold: Double, words: Set<String>, keysByCharacter: SparseArray<KeyView>
) {
/** A tree that provides fast access to words based on their first and last letter. */
private val wordTree = HashMap<Pair<Int, Int>, ArrayList<String>>()
/**
* Finds the words whose start and end letter are closest to the start and end points of the
* user gesture.
*
* @param userGesture The current user gesture.
* @param keys The keys on the keyboard.
* @return A list of likely words.
*/
fun pruneByExtremities(
userGesture: Gesture, keys: Iterable<KeyView>
): ArrayList<String> {
val remainingWords = ArrayList<String>()
val startX = userGesture.getFirstX()
val startY = userGesture.getFirstY()
val endX = userGesture.getLastX()
val endY = userGesture.getLastY()
val startKeys = findNClosestKeys(startX, startY, 2, keys)
val endKeys = findNClosestKeys(endX, endY, 2, keys)
for (startKey in startKeys) {
for (endKey in endKeys) {
val keyPair = Pair(startKey, endKey)
val wordsForKeys = wordTree[keyPair]
if (wordsForKeys != null) {
remainingWords.addAll(wordsForKeys)
}
}
}
return remainingWords
}
/**
* Finds the words whose ideal gesture length is within a certain threshold of the user
* gesture's length.
*
* @param userGesture The current user gesture.
* @param words A list of words to consider.
* @return A list of words that remained after pruning the input list by length.
*/
fun pruneByLength(
userGesture: Gesture,
words: ArrayList<String>,
keysByCharacter: SparseArray<KeyView>,
keys: List<KeyView>
): ArrayList<String> {
val remainingWords = ArrayList<String>()
val key = keys.firstOrNull() ?: return arrayListOf()
val radius = min(key.height, key.width)
val userLength = userGesture.getLength()
for (word in words) {
val idealGestures = Gesture.generateIdealGestures(word, keysByCharacter)
for (idealGesture in idealGestures) {
val wordIdealLength = idealGesture.getLength()
if (abs(userLength - wordIdealLength) < lengthThreshold * radius) {
remainingWords.add(word)
}
}
}
return remainingWords
}
companion object {
private fun getFirstKeyLastKey(
word: String, keysByCharacter: SparseArray<KeyView>
): Pair<Int, Int>? {
val firstLetter = word[0]
val lastLetter = word[word.length - 1]
val firstBaseChar = Normalizer.normalize(firstLetter.toString(), Normalizer.Form.NFD)[0]
val lastBaseChar = Normalizer.normalize(lastLetter.toString(), Normalizer.Form.NFD)[0]
return when {
keysByCharacter.indexOfKey(firstBaseChar.toInt()) < 0 || keysByCharacter.indexOfKey(lastBaseChar.toInt()) < 0 -> {
null
}
else -> {
val firstKey = keysByCharacter[firstBaseChar.toInt()]
val lastKey = keysByCharacter[lastBaseChar.toInt()]
Pair(firstKey.data.code, lastKey.data.code)
}
}
}
/**
* Finds a chosen number of keys closest to a given point on the keyboard.
*
* @param x X coordinate of the point.
* @param y Y coordinate of the point.
* @param n The number of keys to return.
* @param keys The keys of the keyboard.
* @return A list of the n closest keys.
*/
private fun findNClosestKeys(
x: Float, y: Float, n: Int, keys: Iterable<KeyView>
): Iterable<Int> {
val keyDistances = HashMap<KeyView, Float>()
for (key in keys) {
val distance = Gesture.distance(key.centerX, key.centerY, x, y)
keyDistances[key] = distance
}
return keyDistances.entries.sortedWith { c1, c2 -> c1.value.compareTo(c2.value) }.take(n)
.map { it.key.data.code }
}
}
init {
for (word in words) {
val keyPair = getFirstKeyLastKey(word, keysByCharacter)
keyPair?.let {
wordTree.getOrPut(keyPair, { arrayListOf() }).add(word)
}
}
}
}
class Gesture(private val xs: FloatArray, private val ys: FloatArray, private var size: Int) {
val isEmpty: Boolean
get() = size == 0
constructor() : this(FloatArray(MAX_SIZE), FloatArray(MAX_SIZE), 0)
companion object {
private const val MAX_SIZE = 300
fun generateIdealGestures(word: String, keysByCharacter: SparseArray<KeyView>): List<Gesture> {
val idealGesture = Gesture()
val idealGestureWithLoops = Gesture()
var previousLetter = '\u0000'
// Add points for each key
for (c in word) {
val lc = Character.toLowerCase(c)
var key = keysByCharacter[lc.toInt()]
if (key == null) {
// Try finding the base character instead, e.g., the "e" key instead of "é"
val baseCharacter: Char = Normalizer.normalize(lc.toString(), Normalizer.Form.NFD)[0]
key = keysByCharacter[baseCharacter.toInt()]
if (key == null) {
continue
}
}
// We adda little loop on the key for duplicate letters
// so that we can differentiate words like pool and poll, lull and lul, etc...
if (previousLetter == lc) {
// bottom right
idealGestureWithLoops.addPoint(
key.centerX + key.width / 4.0f, key.centerY + key.height / 4.0f
)
// top right
idealGestureWithLoops.addPoint(
key.centerX + key.width / 4.0f, key.centerY - key.height / 4.0f
)
// top left
idealGestureWithLoops.addPoint(
key.centerX - key.width / 4.0f, key.centerY - key.height / 4.0f
)
// bottom left
idealGestureWithLoops.addPoint(
key.centerX - key.width / 4.0f, key.centerY + key.height / 4.0f
)
idealGesture.addPoint(key.centerX, key.centerY)
} else {
idealGesture.addPoint(key.centerX, key.centerY)
idealGestureWithLoops.addPoint(key.centerX, key.centerY)
}
previousLetter = lc
}
return listOf(idealGesture, idealGestureWithLoops)
}
fun distance(x1: Float, y1: Float, x2: Float, y2: Float): Float {
return sqrt((x1 - x2).pow(2) + (y1 - y2).pow(2))
}
}
fun addPoint(x: Float, y: Float) {
if (size >= MAX_SIZE) {
return
}
xs[size] = x
ys[size] = y
size += 1
}
/**
* Resamples the gesture into a new gesture with the chosen number of points by oversampling
* it.
*
* @param numPoints The number of points that the new gesture will have. Must be superior to
* the number of points in the current gesture.
* @return An oversampled copy of the gesture.
*/
fun resample(numPoints: Int): Gesture {
val interpointDistance = (getLength() / numPoints)
val resampledGesture = Gesture()
resampledGesture.addPoint(xs[0], ys[0])
var lastX = xs[0]
var lastY = ys[0]
var newX: Float
var newY: Float
var cumulativeError = 0.0f
// otherwise nothing happens if size is only 1:
if (this.size == 1) {
for (i in 0 until SAMPLING_POINTS) {
resampledGesture.addPoint(xs[0], ys[0])
}
}
for (i in 0 until size - 1) {
// We calculate the unit vector from the two points we're between in the actual
// gesture
var dx = xs[i + 1] - xs[i]
var dy = ys[i + 1] - ys[i]
val norm = sqrt(dx.pow(2.0f) + dy.pow(2.0f))
dx /= norm
dy /= norm
// The number of evenly sampled points that fit between the two actual points
var numNewPoints = norm / interpointDistance
// The number of point that'd fit between the two actual points is often not round,
// which means we'll get an increasingly large error as we resample the gesture
// and round down that number. To compensate for this we keep track of the error
// and add additional points when it gets too large.
cumulativeError += numNewPoints - numNewPoints.toInt()
if (cumulativeError > 1) {
numNewPoints = (numNewPoints.toInt() + cumulativeError.toInt()).toFloat()
cumulativeError %= 1
}
for (j in 0 until numNewPoints.toInt()) {
newX = lastX + dx * interpointDistance
newY = lastY + dy * interpointDistance
lastX = newX
lastY = newY
resampledGesture.addPoint(newX, newY)
}
}
return resampledGesture
}
fun normalizeByBoxSide(): Gesture {
val normalizedGesture = Gesture()
var maxX = -1.0f
var maxY = -1.0f
var minX = 10000.0f
var minY = 10000.0f
for (i in 0 until size) {
maxX = max(xs[i], maxX)
maxY = max(ys[i], maxY)
minX = min(xs[i], minX)
minY = min(ys[i], minY)
}
val width = maxX - minX
val height = maxY - minY
val longestSide = max(width, height)
val centroidX = (width / 2 + minX) / longestSide
val centroidY = (height / 2 + minY) / longestSide
for (i in 0 until size) {
val x = xs[i] / longestSide - centroidX
val y = ys[i] / longestSide - centroidY
normalizedGesture.addPoint(x, y)
}
return normalizedGesture
}
fun getFirstX(): Float = xs[0]
fun getFirstY(): Float = ys[0]
fun getLastX(): Float = xs[size - 1]
fun getLastY(): Float = ys[size - 1]
fun getLength(): Float {
var length = 0f
for (i in 1 until size) {
val previousX = xs[i - 1]
val previousY = ys[i - 1]
val currentX = xs[i]
val currentY = ys[i]
length += distance(previousX, previousY, currentX, currentY)
}
return length
}
fun clear() {
this.size = 0
}
fun getX(i: Int): Float = xs[i]
fun getY(i: Int): Float = ys[i]
fun clone(): Gesture {
return Gesture(xs.clone(), ys.clone(), size)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Gesture
if (this.size != other.size) return false
for (i in 0 until size) {
if (xs[i] != other.xs[i] || ys[i] != other.ys[i]) return false
}
return true
}
override fun hashCode(): Int {
var result = xs.contentHashCode()
result = 31 * result + ys.contentHashCode()
result = 31 * result + size
return result
}
}
}

View File

@@ -19,9 +19,8 @@ package dev.patrickgold.florisboard.ime.text.gestures
import android.content.Context
import android.view.MotionEvent
import dev.patrickgold.florisboard.R
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.atan
import kotlin.math.atan2
/**
* Wrapper class which holds all enums, interfaces and classes for detecting a swipe gesture.
@@ -188,16 +187,7 @@ abstract class SwipeGesture {
* +y
*/
private fun angle(diffX: Double, diffY: Double): Double {
val tmpAngle = abs(360 * atan(diffY / diffX) / (2 * PI))
return if (diffX < 0 && diffY >= 0) {
180.0f - tmpAngle
} else if (diffX < 0 && diffY < 0) {
180.0f + tmpAngle
} else if (diffX >= 0 && diffY < 0) {
360.0f - tmpAngle
} else {
tmpAngle
}
return (Math.toDegrees(atan2(diffY, diffX)) + 360) % 360
}
/**

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.text.key
import kotlin.math.abs
class CurrencySet(
val name: String,
val label: String,
private val slots: List<KeyData>
) {
companion object {
fun isCurrencySlot(keyCode: Int): Boolean {
return when (keyCode) {
KeyCode.CURRENCY_SLOT_1,
KeyCode.CURRENCY_SLOT_2,
KeyCode.CURRENCY_SLOT_3,
KeyCode.CURRENCY_SLOT_4,
KeyCode.CURRENCY_SLOT_5,
KeyCode.CURRENCY_SLOT_6 -> true
else -> false
}
}
fun default(): CurrencySet = CurrencySet(
name = "\$default",
label = "Default",
slots = listOf(
KeyData(code = 36, label = "$"),
KeyData(code = 162, label = "¢"),
KeyData(code = 8364, label = ""),
KeyData(code = 163, label = "£"),
KeyData(code = 165, label = "¥"),
KeyData(code = 8369, label = "")
)
)
}
fun getSlot(keyCode: Int): KeyData? {
val slot = abs(keyCode) - abs(KeyCode.CURRENCY_SLOT_1)
return slots.getOrNull(slot)
}
override fun toString(): String {
return "${CurrencySet::class.simpleName} { name=$name, label\"$label\", slots=$slots }"
}
}

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