Compare commits

...

110 Commits

Author SHA1 Message Date
Patrick Goldinger
2a317372b2 Release v0.3.13-beta03 2021-05-31 20:18:43 +02:00
Patrick Goldinger
402f7bd267 Update translations from Crowdin 2021-05-31 20:02:33 +02:00
Patrick Goldinger
e8eb6e3068 Fix emoticon layout missing (#950) 2021-05-31 19:17:38 +02:00
Patrick Goldinger
3dd9c45777 Fix crash when using delete left swipe in raw editors (#967) 2021-05-31 18:30:24 +02:00
Patrick Goldinger
7255229361 Merge pull request #966 from florisboard/major-input-logic-overhaul
Major input logic overhaul
2021-05-31 17:52:19 +02:00
Patrick Goldinger
4d2fa29886 Fix IME checking utility not using new ID 2021-05-31 12:46:14 +02:00
Patrick Goldinger
ef90faf98b Merge pull request #963 from Hayleia/composingFix
Fix getting composer from name
2021-05-31 06:11:38 +02:00
Patrick Goldinger
82caa8365e Fix glide trail stuck after initial touch down 2021-05-31 05:16:20 +02:00
Patrick Goldinger
391257e9e9 Re-add simple key shadows 2021-05-31 05:04:02 +02:00
Patrick Goldinger
b082253167 Fix keys not registered correctly (#953) 2021-05-31 03:59:31 +02:00
Patrick Goldinger
8df701e3fe Adapt input view to new keyboard state register 2021-05-31 03:56:08 +02:00
Patrick Goldinger
9f232f5dbf Add new keyboard state register 2021-05-31 03:55:05 +02:00
Hayleia
7017726dcb Fix getting composer from name
also use an available constant when possible rather than a hardcoded string
2021-05-30 11:05:28 +02:00
Patrick Goldinger
b48ca8fd1e Restructure the package structure 2021-05-28 21:04:27 +02:00
Patrick Goldinger
88d5e15a5e Introduce TextKeyboardState 2021-05-28 03:36:54 +02:00
Patrick Goldinger
e9537cbd1d Merge pull request #947 from yashpalgoyal1304/devanagari-fix
Fixed Devanagari Codes
2021-05-26 23:32:10 +02:00
yashpalgoyal1304
8e216bf3ac Fixed Devanagari Codes 2021-05-27 02:37:14 +05:30
Patrick Goldinger
63352cc615 Improve logic and rendering performance a bit 2021-05-26 17:12:28 +02:00
Patrick Goldinger
e9e2563739 Release v0.3.13-beta02 2021-05-26 01:26:33 +02:00
Patrick Goldinger
87bb098445 Fix batch level preventing cached input from updating 2021-05-26 01:26:17 +02:00
Patrick Goldinger
da1944bedf Temporarily remove key shadow support (#943) 2021-05-26 01:09:50 +02:00
Patrick Goldinger
d4a92e0d46 Merge pull request #942 from florisboard/new-touch-logic
Introduce new touch logic to TextKeyboardView
2021-05-26 00:46:31 +02:00
yashpalgoyal1304
0fa6c1f235 Added Indic Numerals (#940)
* Indic Devanagari Numeric

* Fixed name and label

* Fixed file name

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

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

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

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

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

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

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

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

If the symbol layout (excluding numeric row) has at least the same
number of rows as the character layout, the numerical row is disabled
and numerical hinting is enabled, the symbol keys take precedence. The
numeric hints are instead added as additional popup characters.
2021-05-11 23:58:25 +02:00
Hayleia
df393ff607 composers can be specified in config.json
no compatibility with previous settings, need to update the regex
2021-05-11 19:03:30 +02:00
Patrick Goldinger
64040f0407 Release v0.3.12 2021-05-10 15:47:05 +02:00
Patrick Goldinger
0c1abdd507 Merge pull request #850 from X-yl/master
Stop glide suggestions disappearing and remove the redundant first suggestion
2021-05-10 15:28:21 +02:00
Patrick Goldinger
53594e3343 Fix glide logic not triggering when shift/caps is active (#847) 2021-05-10 15:22:45 +02:00
X-yl
c6c06b87c5 Stop glide suggestions disappearing and remove redundant first option 2021-05-10 16:52:50 +04:00
Patrick Goldinger
ae6eb5d72d Release v0.3.11 2021-05-10 00:06:07 +02:00
Patrick Goldinger
bbce53fdf4 Update README and open-source licenses 2021-05-09 20:45:33 +02:00
Hayleia
88a6f436ef Merge branch 'master' of https://github.com/florisboard/florisboard into composing1 2021-05-05 10:02:17 +02:00
ostrya
ee8f44d816 Use new currency set mechanism 2021-05-04 20:52:53 +02:00
ostrya
0308ec355f Adapt to new layout rework 2021-05-04 20:44:57 +02:00
Hayleia
3ac14f8a2a remove pointless reflection (going to use serialization anyways) 2021-05-04 20:16:23 +02:00
Hayleia
2b087b76dc korean double consonants and two vowels on shift key 2021-05-04 20:12:03 +02:00
Hayleia
1715e5ddfa support specifying uppercase and lowercase separately in json 2021-05-04 20:11:27 +02:00
Hayleia
6cc17161a5 factor stuff 2021-05-03 21:00:04 +02:00
Hayleia
5d1c20617b Merge branch 'master' of https://github.com/florisboard/florisboard into composing1 2021-05-03 19:22:23 +02:00
Hayleia
d9efa48c9c copy pasted code to compose texte with suggestions enabled too 2021-05-03 19:15:03 +02:00
ostrya
dedd4cb7f0 Use custom modifier for symbol layer
To make the switch from character to symbol layer more consistent,
a neo specific symbol modifier layout was added. This also allows
overriding the comma and full stop with their layer 3 equivalents.
2021-05-02 17:06:07 +02:00
ostrya
42b147b656 Add neo/bone locale variant for better compatibility
The default de locale already defines a lot of extended popups which
do not match the Neo2 / Bone layout logic. Adding a locale variant
allows overriding those defaults.

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

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

View File

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

View File

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

3
.gitignore vendored
View File

@@ -41,5 +41,8 @@ captures/
*.jks
crowdin.properties
# C++
.cxx/
# AndroidX Room schema JSONs
/app/schemas/

View File

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

View File

@@ -1,6 +1,6 @@
plugins {
id("com.android.application") version "4.2.0"
id("com.android.application") version "4.2.1"
kotlin("android") version "1.5.0"
kotlin("kapt") version "1.5.0"
kotlin("plugin.serialization") version "1.5.0"
@@ -24,8 +24,8 @@ android {
applicationId = "dev.patrickgold.florisboard"
minSdkVersion(23)
targetSdkVersion(30)
versionCode(41)
versionName("0.3.11")
versionCode(46)
versionName("0.3.13")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -38,17 +38,33 @@ android {
)
}
}
externalNativeBuild {
cmake {
cppFlags("-std=c++17", "-fexceptions", "-frtti")
arguments("-DANDROID_STL=c++_static")
}
}
}
buildFeatures {
viewBinding = true
}
externalNativeBuild {
cmake {
path("src/main/cpp/CMakeLists.txt")
}
}
buildTypes {
named("debug").configure {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
isDebuggable = true
isJniDebuggable = true
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_debug")
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_debug_round")
resValue("string", "floris_app_name", "FlorisBoard Debug")
@@ -57,7 +73,7 @@ android {
create("beta") // Needed because by default the "beta" BuildType does not exist
named("beta").configure {
applicationIdSuffix = ".beta"
versionNameSuffix = "-beta06"
versionNameSuffix = "-beta03"
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_beta")
@@ -89,6 +105,7 @@ android {
dependencies {
implementation("androidx.activity", "activity-ktx", "1.2.1")
implementation("androidx.appcompat", "appcompat", "1.2.0")
implementation("androidx.autofill", "autofill", "1.1.0")
implementation("androidx.core", "core-ktx", "1.3.2")
implementation("androidx.fragment", "fragment-ktx", "1.3.0")
implementation("androidx.preference", "preference-ktx", "1.1.1")

View File

@@ -21,7 +21,7 @@
<uses-permission android:name="android.permission.VIBRATE"/>
<application
android:name=".ime.core.FlorisApplication"
android:name="dev.patrickgold.florisboard.FlorisApplication"
android:allowBackup="false"
android:icon="@mipmap/floris_app_icon"
android:label="@string/floris_app_name"
@@ -31,16 +31,13 @@
<!-- IME service -->
<service
android:name="dev.patrickgold.florisboard.ime.core.FlorisBoard"
android:name="dev.patrickgold.florisboard.FlorisImeService"
android:label="@string/floris_app_name"
android:permission="android.permission.BIND_INPUT_METHOD">
<meta-data
android:name="android.view.im"
android:resource="@xml/method"/>
<intent-filter>
<action android:name="android.view.InputMethod"/>
</intent-filter>
<meta-data android:name="android.view.im" android:resource="@xml/method"/>
</service>
<!-- Settings Activity -->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Patrick Goldinger
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,13 +14,14 @@
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.core
package dev.patrickgold.florisboard
import android.app.Application
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.crashutility.CrashUtility
import dev.patrickgold.florisboard.debug.Flog
import dev.patrickgold.florisboard.debug.LogTopic
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.ime.core.SubtypeManager
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
import dev.patrickgold.florisboard.ime.extension.AssetManager
import dev.patrickgold.florisboard.ime.theme.ThemeManager

View File

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

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.core
package dev.patrickgold.florisboard.common
import android.content.ClipData
import android.content.ClipboardManager
@@ -26,6 +26,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding
import com.google.android.material.snackbar.Snackbar
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ object LogTopic {
const val LAYOUT_MANAGER: FlogTopic = 8u
const val TEXT_KEYBOARD_VIEW: FlogTopic = 16u
const val GESTURES: FlogTopic = 32u
const val SMARTBAR: FlogTopic = 64u
const val GLIDE: FlogTopic = 512u
const val CLIPBOARD: FlogTopic = 1024u

View File

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

View File

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

View File

@@ -25,19 +25,35 @@ import android.inputmethodservice.ExtractEditText
import android.inputmethodservice.InputMethodService
import android.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.VibrationEffect
import android.os.Vibrator
import android.view.*
import android.util.Size
import android.view.ContextThemeWrapper
import android.view.Gravity
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InlineSuggestionsRequest
import android.view.inputmethod.InlineSuggestionsResponse
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.FrameLayout
import android.widget.inline.InlinePresentationSpec
import androidx.annotation.RequiresApi
import androidx.annotation.StyleRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.lifecycle.*
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.crashutility.CrashUtility
import dev.patrickgold.florisboard.debug.*
import dev.patrickgold.florisboard.ime.clip.ClipboardInputManager
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
@@ -47,15 +63,29 @@ import dev.patrickgold.florisboard.ime.media.MediaInputManager
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.popup.PopupLayerView
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.composing.Appender
import dev.patrickgold.florisboard.ime.text.composing.Composer
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.*
import dev.patrickgold.florisboard.ime.text.key.CurrencySet
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.setup.SetupActivity
import dev.patrickgold.florisboard.util.*
import kotlinx.coroutines.*
import dev.patrickgold.florisboard.util.AppVersionUtils
import dev.patrickgold.florisboard.common.ViewUtils
import dev.patrickgold.florisboard.ime.keyboard.KeyboardState
import dev.patrickgold.florisboard.ime.keyboard.updateKeyboardState
import dev.patrickgold.florisboard.util.debugSummarize
import dev.patrickgold.florisboard.util.findViewWithType
import dev.patrickgold.florisboard.util.refreshLayoutOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@@ -75,8 +105,11 @@ private var florisboardInstance: FlorisBoard? = null
/**
* Core class responsible to link together both the text and media input managers as well as
* managing the one-handed UI.
*
* All inline suggestion code has been added based on this demo autofill IME provided by Android directly:
* https://cs.android.com/android/platform/superproject/+/master:development/samples/AutofillKeyboard/src/com/example/android/autofillkeyboard/AutofillImeService.java
*/
class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager.OnPrimaryClipChangedListener,
open class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager.OnPrimaryClipChangedListener,
ThemeManager.OnThemeUpdatedListener {
private val serviceLifecycleDispatcher: ServiceLifecycleDispatcher = ServiceLifecycleDispatcher(this)
@@ -93,6 +126,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
get() = _themeContext ?: this
private val prefs: Preferences get() = Preferences.default()
val activeState: KeyboardState = KeyboardState.new()
private var extractEditLayout: WeakReference<ViewGroup?> = WeakReference(null)
var inputView: InputView? = null
@@ -122,24 +156,36 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
var activeEditorInstance: EditorInstance = EditorInstance.default()
val subtypeManager: SubtypeManager get() = SubtypeManager.default()
val composer: Composer get() = subtypeManager.imeConfig.composerFromName.getValue(activeSubtype.composerName)
lateinit var activeSubtype: Subtype
private var currentThemeIsNight: Boolean = false
private var currentThemeResId: Int = 0
private var isNumberRowVisible: Boolean = false
private var isWindowShown: Boolean = false
val textInputManager: TextInputManager
val mediaInputManager: MediaInputManager
val clipInputManager: ClipboardInputManager
private var responseState = ResponseState.RESET
private var pendingResponse: Runnable? = null
private val handler: Handler = Handler(Looper.getMainLooper())
lateinit var textInputManager: TextInputManager
lateinit var mediaInputManager: MediaInputManager
lateinit var clipInputManager: ClipboardInputManager
var isClipboardContextMenuShown = false
init {
florisboardInstance = this
// MUST WRAP all code within Service init in try..catch to prevent any crash loops
try {
florisboardInstance = this
textInputManager = TextInputManager.getInstance()
mediaInputManager = MediaInputManager.getInstance()
clipInputManager = ClipboardInputManager.getInstance()
textInputManager = TextInputManager.getInstance()
mediaInputManager = MediaInputManager.getInstance()
clipInputManager = ClipboardInputManager.getInstance()
System.loadLibrary("florisboard-native")
} catch (e: Exception) {
CrashUtility.stageException(e)
}
}
lateinit var asyncExecutor: ExecutorService
@@ -172,54 +218,51 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
}
override fun onCreate() {
/*if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog()
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.penaltyDeath()
.build()
)
}*/
flogInfo(LogTopic.IMS_EVENTS)
serviceLifecycleDispatcher.onServicePreSuperOnCreate()
// MUST WRAP all code within Service onCreate() in try..catch to prevent any crash loops
try {
// Additional try..catch wrapper as the event listeners chain or the super.onCreate() method could crash
// and lead to a crash loop
try {
// "Main" try..catch block
flogInfo(LogTopic.IMS_EVENTS)
serviceLifecycleDispatcher.onServicePreSuperOnCreate()
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
audioManager = getSystemService(Context.AUDIO_SERVICE) as? AudioManager
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
prefs.sync()
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
audioManager = getSystemService(Context.AUDIO_SERVICE) as? AudioManager
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
prefs.sync()
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
currentThemeIsNight = themeManager.activeTheme.isNightTheme
currentThemeResId = getDayNightBaseThemeId(currentThemeIsNight)
isNumberRowVisible = prefs.keyboard.numberRow
setTheme(currentThemeResId)
themeManager.registerOnThemeUpdatedListener(this)
currentThemeIsNight = themeManager.activeTheme.isNightTheme
currentThemeResId = getDayNightBaseThemeId(currentThemeIsNight)
isNumberRowVisible = prefs.keyboard.numberRow
setTheme(currentThemeResId)
themeManager.registerOnThemeUpdatedListener(this)
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
asyncExecutor = Executors.newSingleThreadExecutor()
florisClipboardManager = FlorisClipboardManager.getInstance().also {
it.initialize(this)
it.addPrimaryClipChangedListener(this)
asyncExecutor = Executors.newSingleThreadExecutor()
florisClipboardManager = FlorisClipboardManager.getInstance().also {
it.initialize(this)
it.addPrimaryClipChangedListener(this)
}
} catch (e: Exception) {
super.onCreate() // MUST CALL even if exception thrown or crash loop is imminent
CrashUtility.stageException(e)
return
}
// Code executed here indicates no crashes occurred, so we execute the onCreate() event as normal
super.onCreate()
eventListeners.toList().forEach { it?.onCreate() }
} catch (e: Exception) {
CrashUtility.stageException(e)
}
super.onCreate()
eventListeners.toList().forEach { it?.onCreate() }
}
@SuppressLint("InflateParams")
override fun onCreateInputView(): View? {
flogInfo(LogTopic.IMS_EVENTS)
CrashUtility.handleStagedButUnhandledExceptions()
updateThemeContext(currentThemeResId)
@@ -291,7 +334,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
false
} else {
when (prefs.keyboard.landscapeInputUiMode) {
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeEditorInstance.imeOptions.flagNoFullscreen && !activeEditorInstance.imeOptions.flagNoExtractUi
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeState.imeOptions.flagNoFullscreen && !activeState.imeOptions.flagNoExtractUi
LandscapeInputUiMode.NEVER_SHOW -> false
LandscapeInputUiMode.ALWAYS_SHOW -> true
}
@@ -305,8 +348,8 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
}
override fun onUpdateExtractingVisibility(ei: EditorInfo?) {
isExtractViewShown = !activeEditorInstance.isRawInputEditor && when (prefs.keyboard.landscapeInputUiMode) {
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeEditorInstance.imeOptions.flagNoExtractUi
isExtractViewShown = activeState.isRichInputEditor && when (prefs.keyboard.landscapeInputUiMode) {
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeState.imeOptions.flagNoExtractUi
LandscapeInputUiMode.NEVER_SHOW -> false
LandscapeInputUiMode.ALWAYS_SHOW -> true
}
@@ -325,6 +368,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
updateOneHandedPanelVisibility()
themeManager.notifyCallbackReceivers()
setActiveInput(R.id.text_input)
dispatchCurrentStateToInputUi()
eventListeners.toList().forEach { it?.onRegisterInputView(inputView) }
}
@@ -333,19 +377,28 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
flogInfo(LogTopic.IMS_EVENTS)
super.onStartInput(attribute, restarting)
responseState = if (responseState == ResponseState.RECEIVE_RESPONSE) {
ResponseState.START_INPUT
} else {
ResponseState.RESET
}
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
}
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
flogInfo(LogTopic.IMS_EVENTS) { "restarting=$restarting"}
flogInfo(LogTopic.IMS_EVENTS) { "restarting=$restarting" }
flogInfo(LogTopic.IMS_EVENTS) { info?.debugSummarize() ?: "" }
super.onStartInputView(info, restarting)
activeEditorInstance = EditorInstance.from(info, this)
if (info != null) {
activeState.update(info)
}
activeEditorInstance = EditorInstance.from(info, this, activeState)
themeManager.updateRemoteColorValues(activeEditorInstance.packageName)
eventListeners.toList().forEach {
it?.onStartInputView(activeEditorInstance, restarting)
}
dispatchCurrentStateToInputUi()
}
override fun onFinishInputView(finishingInput: Boolean) {
@@ -353,10 +406,15 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
if (finishingInput) {
activeEditorInstance = EditorInstance.default()
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
textInputManager.smartbarView?.clearInlineSuggestions()
}
}
super.onFinishInputView(finishingInput)
eventListeners.toList().forEach { it?.onFinishInputView(finishingInput) }
dispatchCurrentStateToInputUi()
}
override fun onFinishInput() {
@@ -366,6 +424,75 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
super.onFinishInput()
}
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest? {
return if (prefs.smartbar.enabled && prefs.suggestion.api30InlineSuggestionsEnabled) {
flogInfo(LogTopic.IMS_EVENTS) {
"Creating inline suggestions request because Smartbar and inline suggestions are enabled."
}
val stylesBundle = themeManager.createInlineSuggestionUiStyleBundle(themeContext)
InlinePresentationSpec.Builder(
Size(
inputView?.desiredInlineSuggestionsMinWidth ?: 0,
inputView?.desiredInlineSuggestionsMinHeight ?: 0
),
Size(
inputView?.desiredInlineSuggestionsMaxWidth ?: 0,
inputView?.desiredInlineSuggestionsMaxHeight ?: 0
)
).let { spec ->
spec.setStyle(stylesBundle)
InlineSuggestionsRequest.Builder(listOf(spec.build())).let { request ->
request.setMaxSuggestionCount(6)
request.build()
}
}
} else {
flogInfo(LogTopic.IMS_EVENTS) {
"Ignoring inline suggestions request because Smartbar and/or inline suggestions are disabled."
}
null
}
}
@RequiresApi(Build.VERSION_CODES.R)
override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean {
flogInfo(LogTopic.IMS_EVENTS) {
"Received inline suggestions response with ${response.inlineSuggestions.size} suggestion(s) provided."
}
textInputManager.smartbarView?.clearInlineSuggestions()
postPendingResponse(response)
return true
}
@RequiresApi(Build.VERSION_CODES.R)
private fun cancelPendingResponse() {
pendingResponse?.let {
handler.removeCallbacks(it)
pendingResponse = null
}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun postPendingResponse(response: InlineSuggestionsResponse) {
cancelPendingResponse()
val inlineSuggestions = response.inlineSuggestions
responseState = ResponseState.RECEIVE_RESPONSE
pendingResponse = Runnable {
pendingResponse = null
if (responseState == ResponseState.START_INPUT && inlineSuggestions.isEmpty()) {
textInputManager.smartbarView?.clearInlineSuggestions()
} else {
textInputManager.smartbarView?.showInlineSuggestions(inlineSuggestions)
}
responseState = ResponseState.RESET
}.also { handler.post(it) }
}
fun dispatchCurrentStateToInputUi() {
inputView?.updateKeyboardState(activeState)
}
override fun onWindowShown() {
super.onWindowShown()
if (isWindowShown) {
@@ -383,10 +510,10 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
isNumberRowVisible = newIsNumberRowVisible
}
themeManager.update()
updateOneHandedPanelVisibility()
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
onSubtypeChanged(activeSubtype)
setActiveInput(R.id.text_input)
updateOneHandedPanelVisibility()
if (prefs.devtools.enabled && prefs.devtools.showHeapMemoryStats) {
devtoolsOverlaySyncJob?.cancel()
@@ -476,18 +603,18 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
candidatesStart, candidatesEnd
)
activeState.isSelectionMode = (newSelEnd - newSelStart) != 0
if (internalBatchNestingLevel == 0) {
flogInfo(LogTopic.IMS_EVENTS) { "$oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd" }
flogInfo(LogTopic.IMS_EVENTS) { "onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)" }
activeEditorInstance.onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
eventListeners.toList().forEach { it?.onUpdateSelection() }
dispatchCurrentStateToInputUi()
} else {
flogInfo(LogTopic.IMS_EVENTS) {
"$oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd: caught due to internal batch level of $internalBatchNestingLevel!"
}
flogInfo(LogTopic.IMS_EVENTS) { "onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd): caught due to internal batch level of $internalBatchNestingLevel!" }
if (internalSelectionCache.selectionCatchCount++ == 0) {
internalSelectionCache.oldSelStart = oldSelStart
internalSelectionCache.oldSelEnd = oldSelEnd
@@ -596,7 +723,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
*/
private fun updateSoftInputWindowLayoutParameters() {
val w = window?.window ?: return
ViewLayoutUtils.updateLayoutHeightOf(w, WindowManager.LayoutParams.MATCH_PARENT)
ViewUtils.updateLayoutHeightOf(w, WindowManager.LayoutParams.MATCH_PARENT)
val inputWindowView = this.inputWindowView
if (inputWindowView != null) {
val layoutHeight = if (isFullscreenMode) {
@@ -605,9 +732,9 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
WindowManager.LayoutParams.MATCH_PARENT
}
val inputArea = w.findViewById<View>(android.R.id.inputArea)
ViewLayoutUtils.updateLayoutHeightOf(inputArea, layoutHeight)
ViewLayoutUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM)
ViewLayoutUtils.updateLayoutHeightOf(inputWindowView, layoutHeight)
ViewUtils.updateLayoutHeightOf(inputArea, layoutHeight)
ViewUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM)
ViewUtils.updateLayoutHeightOf(inputWindowView, layoutHeight)
}
}
@@ -871,6 +998,10 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
fun onSubtypeChanged(newSubtype: Subtype) {}
}
private enum class ResponseState {
RESET, RECEIVE_RESPONSE, START_INPUT
}
/**
* Data class which holds the base information for this IME. Matches the structure of
* ime/config.json so it can be parsed. Used by [SubtypeManager] and by the prefs.
@@ -889,15 +1020,35 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
data class ImeConfig(
@SerialName("package")
val packageName: String,
@SerialName("composers")
val composers: List<Composer> = listOf(),
@SerialName("currencySets")
val currencySets: List<CurrencySet> = listOf(),
@SerialName("defaultSubtypes")
val defaultSubtypes: List<DefaultSubtype> = listOf()
) {
@Transient var currencySetNames: List<String> = listOf()
@Transient var currencySetLabels: List<String> = listOf()
@Transient var composerNames: List<String> = listOf()
@Transient var composerLabels: List<String> = listOf()
@Transient val composerFromName: Map<String, Composer> = composers.map { it.name to it }.toMap()
@Transient var defaultSubtypesLanguageCodes: List<String> = listOf()
@Transient var defaultSubtypesLanguageNames: List<String> = listOf()
init {
val tmpComposerList = composers.map { Pair(it.name, it.label) }.toMutableList()
// Sort composer list alphabetically by the label of a composer
tmpComposerList.sortBy { it.second }
// Move selected composers to the top of the list
for (composerName in listOf(Appender.name)) {
val index: Int = tmpComposerList.indexOfFirst { it.first == composerName }
if (index > 0) {
tmpComposerList.add(0, tmpComposerList.removeAt(index))
}
}
composerNames = tmpComposerList.map { it.first }.toList()
composerLabels = tmpComposerList.map { it.second }.toList()
val tmpCurrencyList = mutableListOf<Pair<String, String>>()
for (currencySet in currencySets) {
tmpCurrencyList.add(Pair(currencySet.name, currencySet.label))

View File

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

View File

@@ -22,6 +22,7 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.os.Build
import android.text.TextPaint
import android.util.AttributeSet
import android.util.DisplayMetrics
@@ -32,7 +33,7 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import dev.patrickgold.florisboard.common.ViewUtils
import timber.log.Timber
import kotlin.math.roundToInt
@@ -56,6 +57,15 @@ class InputView : LinearLayout {
var shouldGiveAdditionalSpace: Boolean = false
private set
var desiredInlineSuggestionsMinWidth: Int = 0
private set
var desiredInlineSuggestionsMinHeight: Int = 0
private set
var desiredInlineSuggestionsMaxWidth: Int = 0
private set
var desiredInlineSuggestionsMaxHeight: Int = 0
private set
var mainViewFlipper: ViewFlipper? = null
private set
var oneHandedCtrlPanelStart: ViewGroup? = null
@@ -123,7 +133,7 @@ class InputView : LinearLayout {
baseTextInputHeight += additionalHeight
}
val smartbarDisabled = !prefs.smartbar.enabled ||
tim.keyVariation == KeyVariation.PASSWORD && prefs.keyboard.numberRow
tim.activeState.keyVariation == KeyVariation.PASSWORD && prefs.keyboard.numberRow && !prefs.suggestion.api30InlineSuggestionsEnabled
if (smartbarDisabled) {
baseHeight = baseTextInputHeight
baseSmartbarHeight = 0.0f
@@ -134,15 +144,22 @@ class InputView : LinearLayout {
desiredMediaKeyboardViewHeight = baseHeight
// Add bottom offset for curved screens here. As the desired heights have already been set,
// adding a value to the height now will result in a bottom padding (aka offset).
baseHeight += ViewLayoutUtils.convertDpToPixel(
baseHeight += ViewUtils.dp2px(
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
prefs.keyboard.bottomOffsetLandscape.toFloat()
} else {
prefs.keyboard.bottomOffsetPortrait.toFloat()
},
context
}
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val width = MeasureSpec.getSize(widthMeasureSpec)
desiredInlineSuggestionsMinWidth = width / 3
desiredInlineSuggestionsMinHeight = desiredSmartbarHeight.toInt()
desiredInlineSuggestionsMaxWidth = (width / 1.5).toInt()
desiredInlineSuggestionsMaxHeight = desiredSmartbarHeight.toInt()
}
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(baseHeight.roundToInt(), MeasureSpec.EXACTLY))
}

View File

@@ -26,6 +26,7 @@ import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.text.gestures.DistanceThreshold
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.gestures.VelocityThreshold
import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
import dev.patrickgold.florisboard.ime.text.smartbar.CandidateView
@@ -129,7 +130,9 @@ class Preferences(
}
companion object {
private val OLD_SUBTYPES_REGEX = """^([\-0-9]+/[\-a-zA-Z0-9]+/[a-zA-Z_]+[;]*)+${'$'}""".toRegex()
// old settings are id/language/layout and id/language/currencySet/layout
// new settings have composer
private val OLD_SUBTYPES_REGEX = """^([\-0-9]+/[\-a-zA-Z0-9]+(/[a-zA-Z_]+)?/[a-zA-Z_]+[;]*)+${'$'}""".toRegex()
private var defaultInstance: Preferences? = null
@Synchronized
@@ -397,6 +400,7 @@ class Preferences(
const val KEY_SPACING_VERTICAL = "keyboard__key_spacing_vertical"
const val LANDSCAPE_INPUT_UI_MODE = "keyboard__landscape_input_ui_mode"
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
const val MERGE_HINT_POPUPS_ENABLED = "keyboard__merge_hint_popups_enabled"
const val NUMBER_ROW = "keyboard__number_row"
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
const val ONE_HANDED_MODE_SCALE_FACTOR = "keyboard__one_handed_mode_scale_factor"
@@ -447,6 +451,9 @@ class Preferences(
var longPressDelay: Int = 0
get() = prefs.getPref(LONG_PRESS_DELAY, 300)
private set
var mergeHintPopupsEnabled: Boolean
get() = prefs.getPref(MERGE_HINT_POPUPS_ENABLED, false)
set(v) = prefs.setPref(MERGE_HINT_POPUPS_ENABLED, v)
var numberRow: Boolean
get() = prefs.getPref(NUMBER_ROW, false)
set(v) = prefs.setPref(NUMBER_ROW, v)
@@ -485,6 +492,10 @@ class Preferences(
var vibrationStrength: Int = 0
get() = prefs.getPref(VIBRATION_STRENGTH, -1)
private set
fun keyHintConfiguration(): KeyHintConfiguration {
return KeyHintConfiguration(hintedSymbolsMode, hintedNumberRowMode, mergeHintPopupsEnabled)
}
}
/**
@@ -522,14 +533,18 @@ class Preferences(
*/
class Suggestion(private val prefs: Preferences) {
companion object {
const val BLOCK_POSSIBLY_OFFENSIVE = "suggestion__block_possibly_offensive"
const val CLIPBOARD_CONTENT_ENABLED = "suggestion__clipboard_content_enabled"
const val CLIPBOARD_CONTENT_TIMEOUT = "suggestion__clipboard_content_timeout"
const val DISPLAY_MODE = "suggestion__display_mode"
const val ENABLED = "suggestion__enabled"
const val USE_PREV_WORDS = "suggestion__use_prev_words"
const val API30_INLINE_SUGGESTIONS_ENABLED = "suggestion__api30_inline_suggestions_enabled"
const val BLOCK_POSSIBLY_OFFENSIVE = "suggestion__block_possibly_offensive"
const val CLIPBOARD_CONTENT_ENABLED = "suggestion__clipboard_content_enabled"
const val CLIPBOARD_CONTENT_TIMEOUT = "suggestion__clipboard_content_timeout"
const val DISPLAY_MODE = "suggestion__display_mode"
const val ENABLED = "suggestion__enabled"
const val USE_PREV_WORDS = "suggestion__use_prev_words"
}
var api30InlineSuggestionsEnabled: Boolean
get() = prefs.getPref(API30_INLINE_SUGGESTIONS_ENABLED, true)
set(v) = prefs.setPref(API30_INLINE_SUGGESTIONS_ENABLED, v)
var blockPossiblyOffensive: Boolean
get() = prefs.getPref(BLOCK_POSSIBLY_OFFENSIVE, true)
set(v) = prefs.setPref(BLOCK_POSSIBLY_OFFENSIVE, v)

View File

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

View File

@@ -112,16 +112,18 @@ class SubtypeManager(
* list, if it does not exist.
*
* @param locale The locale of the subtype to be added.
* @param composerName The composer name of the subtype to be added.
* @param currencySetName The currency set name of the subtype to be added.
* @param layoutMap The layout map of the subtype to be added.
* @return True if the subtype was added, false otherwise. A return value of false indicates
* that the subtype already exists.
*/
fun addSubtype(locale: Locale, currencySetName: String, layoutMap: SubtypeLayoutMap): Boolean {
fun addSubtype(locale: Locale, composerName: String, currencySetName: String, layoutMap: SubtypeLayoutMap): Boolean {
return addSubtype(
Subtype(
(locale.hashCode() + 31 * layoutMap.hashCode() + 31 * currencySetName.hashCode()),
locale,
composerName,
currencySetName,
layoutMap
)

View File

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

View File

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

View File

@@ -30,15 +30,15 @@ import kotlin.jvm.Throws
* This class accepts binary dictionary files of the type "flict" as defined in here:
* https://github.com/florisboard/dictionary-tools/blob/main/flictionary.md
*/
/**
class Flictionary private constructor(
override val name: String,
override val label: String,
override val authors: List<String>,
private val date: Long,
private val version: Int,
private val headerStr: String,
override val languageModel: LanguageModel<String, Int>
) : Dictionary<String, Int> {
private val headerStr: String
) : Dictionary {
companion object {
private const val VERSION_0 = 0x0
@@ -427,3 +427,4 @@ fun InputStream.readNext(b: ByteArray, off: Int, len: Int): Int {
}
return lenRead
}
*/

View File

@@ -131,7 +131,7 @@ interface UserDictionaryDatabase {
fun reset()
fun importCombinedList(context: Context, uri: Uri): Result<Unit> {
return ExternalContentUtils.readFromUri(context, uri,2048) { src ->
return ExternalContentUtils.readFromUri(context, uri,6_192_000) { src ->
var isFirstLine = true
src.forEachLine { line ->
if (isFirstLine) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.sendBlocking
@Suppress("MemberVisibilityCanBePrivate")
abstract class KeyboardView : View, ThemeManager.OnThemeUpdatedListener {
abstract class KeyboardView : View, KeyboardState.OnUpdateStateListener, ThemeManager.OnThemeUpdatedListener {
protected val florisboard get() = FlorisBoard.getInstanceOrNull()
protected val prefs get() = Preferences.default()
protected val themeManager get() = ThemeManager.defaultOrNull()
@@ -45,6 +45,7 @@ abstract class KeyboardView : View, ThemeManager.OnThemeUpdatedListener {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
layoutDirection = LAYOUT_DIRECTION_LTR
mainScope.launch {
for (event in touchEventChannel) {
if (!isActive) break
@@ -89,10 +90,5 @@ abstract class KeyboardView : View, ThemeManager.OnThemeUpdatedListener {
isMeasured = true
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
onLayoutInternal()
}
protected abstract fun onLayoutInternal()
abstract fun sync()
}

View File

@@ -28,7 +28,7 @@ import androidx.recyclerview.widget.RecyclerView
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import kotlinx.coroutines.CoroutineScope
@@ -39,8 +39,7 @@ import kotlinx.coroutines.MainScope
*
* @property florisboard Reference to instance of core class [FlorisBoard].
* @property emojiKeyboardView Reference to the parent [EmojiKeyboardView].
* @property data The data the current key represents. Is used to determine rendering and possible
* behaviour when events occur.
* @property key The current key. Is used to determine rendering and possible behaviour when events occur.
*/
@SuppressLint("ViewConstructor")
class EmojiKeyView(
@@ -104,8 +103,8 @@ class EmojiKeyView(
(parent as RecyclerView)
.requestDisallowInterceptTouchEvent(true)
emojiKeyboardView.isScrollBlocked = true
emojiKeyboardView.popupManager.show(key, KeyHintMode.DISABLED)
emojiKeyboardView.popupManager.extend(key, KeyHintMode.DISABLED)
emojiKeyboardView.popupManager.show(key, KeyHintConfiguration.HINTS_DISABLED)
emojiKeyboardView.popupManager.extend(key, KeyHintConfiguration.HINTS_DISABLED)
florisboard?.keyPressVibrate()
florisboard?.keyPressSound()
}, delayMillis.toLong())
@@ -174,7 +173,7 @@ class EmojiKeyView(
canvas ?: return
if (key.computedPopups.isNotEmpty()) {
if (key.computedPopups.getPopupKeys(KeyHintConfiguration.HINTS_DISABLED).isNotEmpty()) {
triangleDrawable?.draw(canvas)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ package dev.patrickgold.florisboard.ime.onehanded
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Resources
import android.util.AttributeSet
import android.view.Gravity
import android.widget.ImageButton
@@ -96,8 +97,7 @@ class OneHandedPanel : LinearLayout, ThemeManager.OnThemeUpdatedListener {
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val florisboard = florisboard ?: return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = (florisboard.inputView?.measuredWidth ?: 0) *
val width = (Resources.getSystem().displayMetrics.widthPixels) *
((100 - prefs.keyboard.oneHandedModeScaleFactor) / 100.0f)
super.onMeasure(MeasureSpec.makeMeasureSpec(width.toInt(), MeasureSpec.EXACTLY), heightMeasureSpec)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,13 +27,14 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.debug.*
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.core.*
import dev.patrickgold.florisboard.ime.dictionary.Dictionary
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
import dev.patrickgold.florisboard.ime.extension.AssetManager
import dev.patrickgold.florisboard.ime.extension.AssetRef
import dev.patrickgold.florisboard.ime.extension.AssetSource
import dev.patrickgold.florisboard.ime.nlp.Token
import dev.patrickgold.florisboard.ime.nlp.toStringList
import dev.patrickgold.florisboard.ime.keyboard.ImeOptions
import dev.patrickgold.florisboard.ime.keyboard.InputAttributes
import dev.patrickgold.florisboard.ime.keyboard.KeyboardState
import dev.patrickgold.florisboard.ime.keyboard.updateKeyboardState
import dev.patrickgold.florisboard.ime.text.gestures.GlideTypingManager
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.*
@@ -42,7 +43,6 @@ import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
import dev.patrickgold.florisboard.ime.text.smartbar.SmartbarView
import kotlinx.coroutines.*
import org.json.JSONArray
import java.util.*
import kotlin.math.roundToLong
/**
@@ -68,14 +68,12 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
lateinit var layoutManager: LayoutManager
private set
private var activeKeyboardMode: KeyboardMode? = null
val keyboards = TextKeyboardCache()
private var textInputKeyboardView: TextKeyboardView? = null
lateinit var textKeyboardIconSet: TextKeyboardIconSet
private set
private var textViewGroup: LinearLayout? = null
private val dictionaryManager: DictionaryManager = DictionaryManager.default()
private var activeDictionary: Dictionary<String, Int>? = null
private val dictionaryManager: DictionaryManager get() = DictionaryManager.default()
val inputEventDispatcher: InputEventDispatcher = InputEventDispatcher.new(
repeatableKeyCodes = intArrayOf(
KeyCode.ARROW_DOWN,
@@ -87,14 +85,9 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
)
)
var keyVariation: KeyVariation = KeyVariation.NORMAL
internal var smartbarView: SmartbarView? = null
// Caps/Shift related properties
var caps: Boolean = false
private set
var capsLock: Boolean = false
private set
val activeState: KeyboardState get() = florisboard.activeState
private var newCapsState: Boolean = false
// Composing text related properties
@@ -124,7 +117,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
val evaluator = object : TextComputingEvaluator {
override fun evaluateCaps(): Boolean {
return caps || capsLock
return activeState.caps || activeState.capsLock
}
override fun evaluateCaps(data: TextKeyData): Boolean {
@@ -135,8 +128,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
return when (data.code) {
KeyCode.CLIPBOARD_COPY,
KeyCode.CLIPBOARD_CUT -> {
florisboard.activeEditorInstance.selection.isSelectionMode &&
!florisboard.activeEditorInstance.isRawInputEditor
activeState.isSelectionMode && activeState.isRichInputEditor
}
KeyCode.CLIPBOARD_PASTE -> {
// such gore. checks
@@ -147,7 +139,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
) == true
}
KeyCode.CLIPBOARD_SELECT_ALL -> {
!florisboard.activeEditorInstance.isRawInputEditor
activeState.isRichInputEditor
}
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT -> {
prefs.clipboard.enableHistory
@@ -195,7 +187,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
}
override fun getKeyVariation(): KeyVariation {
return keyVariation
return activeState.keyVariation
}
override fun getKeyboard(): TextKeyboard {
@@ -246,6 +238,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
textInputKeyboardView = inputView.findViewById(R.id.text_input_keyboard_view)
textInputKeyboardView?.setIconSet(textKeyboardIconSet)
textInputKeyboardView?.setComputingEvaluator(evaluator)
textInputKeyboardView?.sync()
launch(Dispatchers.Main) {
val animator1 = textViewGroup?.let {
@@ -282,6 +275,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
fun registerSmartbarView(view: SmartbarView) {
smartbarView = view
smartbarView?.setEventListener(this)
smartbarView?.sync()
}
fun unregisterSmartbarView(view: SmartbarView) {
@@ -307,22 +301,22 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
}
/**
* Evaluates the [activeKeyboardMode], [keyVariation] and [EditorInstance.isComposingEnabled]
* Evaluates the [KeyboardState.keyboardMode], [KeyboardState.keyVariation] and [EditorInstance.isComposingEnabled]
* property values when starting to interact with a input editor. Also resets the composing
* texts and sets the initial caps mode accordingly.
*/
override fun onStartInputView(instance: EditorInstance, restarting: Boolean) {
val keyboardMode = when (instance.inputAttributes.type) {
val keyboardMode = when (activeState.inputAttributes.type) {
InputAttributes.Type.NUMBER -> {
keyVariation = KeyVariation.NORMAL
activeState.keyVariation = KeyVariation.NORMAL
KeyboardMode.NUMERIC
}
InputAttributes.Type.PHONE -> {
keyVariation = KeyVariation.NORMAL
activeState.keyVariation = KeyVariation.NORMAL
KeyboardMode.PHONE
}
InputAttributes.Type.TEXT -> {
keyVariation = when (instance.inputAttributes.variation) {
activeState.keyVariation = when (activeState.inputAttributes.variation) {
InputAttributes.Variation.EMAIL_ADDRESS,
InputAttributes.Variation.WEB_EMAIL_ADDRESS -> {
KeyVariation.EMAIL_ADDRESS
@@ -342,7 +336,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
KeyboardMode.CHARACTERS
}
else -> {
keyVariation = KeyVariation.NORMAL
activeState.keyVariation = KeyVariation.NORMAL
KeyboardMode.CHARACTERS
}
}
@@ -351,61 +345,53 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> false
else -> keyVariation != KeyVariation.PASSWORD &&
else -> activeState.keyVariation != KeyVariation.PASSWORD &&
prefs.suggestion.enabled// &&
//!instance.inputAttributes.flagTextAutoComplete &&
//!instance.inputAttributes.flagTextNoSuggestions
}
isPrivateMode = prefs.advanced.forcePrivateMode ||
imeOptions.flagNoPersonalizedLearning
activeState.isPrivateMode = prefs.advanced.forcePrivateMode ||
activeState.imeOptions.flagNoPersonalizedLearning
}
if (!prefs.correction.rememberCapsLockState) {
capsLock = false
activeState.capsLock = false
}
isGlidePostEffect = false
updateCapsState()
setActiveKeyboardMode(keyboardMode)
setActiveKeyboardMode(keyboardMode, updateState = false)
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
smartbarView?.updateSmartbarState()
}
/**
* Handle stuff when finishing to interact with a input editor.
*/
override fun onFinishInputView(finishingInput: Boolean) {
smartbarView?.updateSmartbarState()
}
override fun onWindowShown() {
launch(Dispatchers.Default) {
dictionaryManager.loadUserDictionariesIfNecessary()
}
smartbarView?.updateSmartbarState()
textInputKeyboardView?.sync()
smartbarView?.sync()
}
/**
* Gets [activeKeyboardMode].
* Gets the active keyboard mode.
*
* @return If null [KeyboardMode.CHARACTERS], else [activeKeyboardMode].
* @return The active keyboard mode.
*/
fun getActiveKeyboardMode(): KeyboardMode {
return activeKeyboardMode ?: KeyboardMode.CHARACTERS
return activeState.keyboardMode
}
/**
* Sets [activeKeyboardMode] and updates the [SmartbarView.isQuickActionsVisible] state.
* Sets the active keyboard mode and updates the [KeyboardState.isQuickActionsVisible] state.
*/
private fun setActiveKeyboardMode(mode: KeyboardMode) = launch {
setActiveKeyboard(mode, florisboard.activeSubtype)
activeKeyboardMode = mode
private fun setActiveKeyboardMode(mode: KeyboardMode, updateState: Boolean = true) = launch {
activeState.keyboardMode = mode
isManualSelectionMode = false
isManualSelectionModeStart = false
isManualSelectionModeEnd = false
smartbarView?.isQuickActionsVisible = false
smartbarView?.updateSmartbarState()
activeState.isQuickActionsVisible = false
setActiveKeyboard(mode, florisboard.activeSubtype, updateState)
}
private fun setActiveKeyboard(mode: KeyboardMode, subtype: Subtype) = launch(Dispatchers.IO) {
private fun setActiveKeyboard(mode: KeyboardMode, subtype: Subtype, updateState: Boolean = true) = launch(Dispatchers.IO) {
val activeKeyboard = keyboards.getOrElseAsync(mode, subtype) {
layoutManager.computeKeyboardAsync(
keyboardMode = mode,
@@ -414,17 +400,16 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
}.await()
withContext(Dispatchers.Main) {
textInputKeyboardView?.setComputedKeyboard(activeKeyboard)
if (updateState) {
florisboard.dispatchCurrentStateToInputUi()
}
}
}
override fun onSubtypeChanged(newSubtype: Subtype) {
launch {
if (activeEditorInstance.isComposingEnabled) {
withContext(Dispatchers.IO) {
dictionaryManager.loadDictionary(AssetRef(AssetSource.Assets, "ime/dict/en.flict")).let {
activeDictionary = it.getOrDefault(null)
}
}
dictionaryManager.prepareDictionaries(newSubtype)
}
if (prefs.glide.enabled) {
GlideTypingManager.getInstance().setWordData(newSubtype)
@@ -442,33 +427,28 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
if (!inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
updateCapsState()
}
smartbarView?.updateSmartbarState()
flogInfo(LogTopic.IMS_EVENTS) { "current word: ${activeEditorInstance.cachedInput.currentWord.text}" }
if (activeEditorInstance.isComposingEnabled && !inputEventDispatcher.isPressed(KeyCode.DELETE)) {
if (activeEditorInstance.isComposingEnabled && !inputEventDispatcher.isPressed(KeyCode.DELETE) && !isGlidePostEffect) {
if (activeEditorInstance.shouldReevaluateComposingSuggestions) {
activeEditorInstance.shouldReevaluateComposingSuggestions = false
activeDictionary?.let {
launch(Dispatchers.Default) {
val startTime = System.nanoTime()
val suggestions = queryUserDictionary(
activeEditorInstance.cachedInput.currentWord.text,
florisboard.activeSubtype.locale
).toMutableList()
suggestions.addAll(it.getTokenPredictions(
precedingTokens = listOf(),
currentToken = Token(activeEditorInstance.cachedInput.currentWord.text),
maxSuggestionCount = 16,
allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive
).toStringList())
if (BuildConfig.DEBUG) {
val elapsed = (System.nanoTime() - startTime) / 1000.0
flogInfo { "sugg fetch time: $elapsed us" }
}
launch(Dispatchers.Default) {
val startTime = System.nanoTime()
dictionaryManager.suggest(
currentWord = activeEditorInstance.cachedInput.currentWord.text,
preceidingWords = listOf(),
subtype = florisboard.activeSubtype,
allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive,
maxSuggestionCount = 16
) { suggestions ->
withContext(Dispatchers.Main) {
smartbarView?.setCandidateSuggestionWords(startTime, suggestions)
smartbarView?.updateCandidateSuggestionCapsState()
}
}
if (BuildConfig.DEBUG) {
val elapsed = (System.nanoTime() - startTime) / 1000.0
flogInfo { "sugg fetch time: $elapsed us" }
}
}
} else {
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
@@ -480,49 +460,14 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
smartbarView?.onPrimaryClipChanged()
}
private fun queryUserDictionary(word: String, locale: Locale): Set<String> {
val florisDao = dictionaryManager.florisUserDictionaryDao()
val systemDao = dictionaryManager.systemUserDictionaryDao()
if (florisDao == null && systemDao == null) {
return setOf()
}
val retList = mutableSetOf<String>()
if (prefs.dictionary.enableFlorisUserDictionary) {
florisDao?.query(word, locale)?.let {
for (entry in it) {
retList.add(entry.word)
}
}
florisDao?.queryShortcut(word, locale)?.let {
for (entry in it) {
retList.add(entry.word)
}
}
}
if (prefs.dictionary.enableSystemUserDictionary) {
systemDao?.query(word, locale)?.let {
for (entry in it) {
retList.add(entry.word)
}
}
systemDao?.queryShortcut(word, locale)?.let {
for (entry in it) {
retList.add(entry.word)
}
}
}
return retList
}
/**
* Updates the current caps state according to the [EditorInstance.cursorCapsMode], while
* respecting [capsLock] property and the correction.autoCapitalization preference.
* respecting [KeyboardState.capsLock] property and the correction.autoCapitalization preference.
*/
private fun updateCapsState() {
if (!capsLock) {
caps = prefs.correction.autoCapitalization &&
if (!activeState.capsLock) {
activeState.caps = prefs.correction.autoCapitalization &&
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
textInputKeyboardView?.notifyStateChanged()
}
}
@@ -586,8 +531,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
override fun onSmartbarQuickActionPressed(quickActionId: Int) {
when (quickActionId) {
R.id.quick_action_toggle -> {
activeState.isQuickActionsVisible = !activeState.isQuickActionsVisible
smartbarView?.updateKeyboardState(activeState)
return
}
R.id.quick_action_switch_to_editing_context -> {
if (activeKeyboardMode == KeyboardMode.EDITING) {
if (activeState.keyboardMode == KeyboardMode.EDITING) {
setActiveKeyboardMode(KeyboardMode.CHARACTERS)
} else {
setActiveKeyboardMode(KeyboardMode.EDITING)
@@ -605,8 +555,8 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
return
}
}
smartbarView?.isQuickActionsVisible = false
smartbarView?.updateSmartbarState()
activeState.isQuickActionsVisible = false
smartbarView?.updateKeyboardState(activeState)
}
/**
@@ -640,17 +590,17 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
* Handles a [KeyCode.ENTER] event.
*/
private fun handleEnter() {
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
if (activeState.imeOptions.flagNoEnterAction) {
activeEditorInstance.performEnter()
} else {
when (activeEditorInstance.imeOptions.action) {
ImeOptions.Action.DONE,
ImeOptions.Action.GO,
ImeOptions.Action.NEXT,
ImeOptions.Action.PREVIOUS,
ImeOptions.Action.SEARCH,
ImeOptions.Action.SEND -> {
activeEditorInstance.performEnterAction(activeEditorInstance.imeOptions.action)
when (activeState.imeOptions.enterAction) {
ImeOptions.EnterAction.DONE,
ImeOptions.EnterAction.GO,
ImeOptions.EnterAction.NEXT,
ImeOptions.EnterAction.PREVIOUS,
ImeOptions.EnterAction.SEARCH,
ImeOptions.EnterAction.SEND -> {
activeEditorInstance.performEnterAction(activeState.imeOptions.enterAction)
}
else -> activeEditorInstance.performEnter()
}
@@ -676,34 +626,34 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
private fun handleShiftDown(ev: InputKeyEvent) {
if (ev.isConsecutiveEventOf(inputEventDispatcher.lastKeyEventDown, prefs.keyboard.longPressDelay.toLong())) {
newCapsState = true
caps = true
capsLock = true
activeState.caps = true
activeState.capsLock = true
} else {
newCapsState = !caps
caps = true
capsLock = false
newCapsState = !activeState.caps
activeState.caps = true
activeState.capsLock = false
}
textInputKeyboardView?.notifyStateChanged()
smartbarView?.updateCandidateSuggestionCapsState()
florisboard.dispatchCurrentStateToInputUi()
}
/**
* Handles a [KeyCode.SHIFT] up event.
*/
private fun handleShiftUp() {
caps = newCapsState
textInputKeyboardView?.notifyStateChanged()
activeState.caps = newCapsState
smartbarView?.updateCandidateSuggestionCapsState()
florisboard.dispatchCurrentStateToInputUi()
}
/**
* Handles a [KeyCode.SHIFT] cancel event.
*/
private fun handleShiftCancel() {
caps = false
capsLock = false
textInputKeyboardView?.notifyStateChanged()
activeState.caps = false
activeState.capsLock = false
smartbarView?.updateCandidateSuggestionCapsState()
florisboard.dispatchCurrentStateToInputUi()
}
/**
@@ -713,10 +663,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
val lastKeyEvent = inputEventDispatcher.lastKeyEventDown ?: return
if (lastKeyEvent.data.code == KeyCode.SHIFT && lastKeyEvent.action == InputKeyEvent.Action.DOWN) {
newCapsState = true
caps = true
capsLock = true
textInputKeyboardView?.notifyStateChanged()
activeState.caps = true
activeState.capsLock = true
smartbarView?.updateCandidateSuggestionCapsState()
florisboard.dispatchCurrentStateToInputUi()
}
}
@@ -888,7 +838,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
KeyCode.VIEW_SYMBOLS2 -> setActiveKeyboardMode(KeyboardMode.SYMBOLS2)
KeyCode.UNDO -> activeEditorInstance.performUndo()
else -> {
when (activeKeyboardMode) {
when (activeState.keyboardMode) {
KeyboardMode.NUMERIC,
KeyboardMode.NUMERIC_ADVANCED,
KeyboardMode.PHONE,
@@ -927,13 +877,12 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
}
}
}
if (data.code != KeyCode.SHIFT && !capsLock && !inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
if (data.code != KeyCode.SHIFT && !activeState.capsLock && !inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
updateCapsState()
}
if (ev.data.code > KeyCode.SPACE) {
isGlidePostEffect = false
}
smartbarView?.updateSmartbarState()
}
override fun onInputKeyRepeat(ev: InputKeyEvent) {
@@ -954,14 +903,14 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
/**
* Changes a word to the current case.
* eg if [capsLock] is true, abc -> ABC
* eg if [KeyboardState.capsLock] is true, abc -> ABC
* if [caps] is true, abc -> Abc
* otherwise , abc -> abc
*/
fun fixCase(word: String): String {
return when {
capsLock -> word.uppercase(florisboard.activeSubtype.locale)
caps -> word.replaceFirstChar { if (it.isLowerCase()) it.titlecase(florisboard.activeSubtype.locale) else it.toString() }
activeState.capsLock -> word.uppercase(florisboard.activeSubtype.locale)
activeState.caps -> word.replaceFirstChar { if (it.isLowerCase()) it.titlecase(florisboard.activeSubtype.locale) else it.toString() }
else -> word
}
}

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.keyboard.TextKey
import kotlinx.coroutines.*
import org.json.JSONObject
import kotlin.math.min
/**
* Handles the [GlideTypingClassifier]. Basically responsible for linking [GlideTypingGesture.Detector]
@@ -62,6 +63,7 @@ class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainSc
}
private val wordDataCache = hashMapOf<String, Int>()
/**
* Set the word data for the internal gesture classifier
*/
@@ -70,7 +72,8 @@ class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainSc
if (wordDataCache.isEmpty()) {
// FIXME: get this info from dictionary.
val data =
AssetManager.default().loadTextAsset(AssetRef(AssetSource.Assets, "ime/dict/data.json")).getOrThrow()
AssetManager.default().loadTextAsset(AssetRef(AssetSource.Assets, "ime/dict/data.json"))
.getOrThrow()
val json = JSONObject(data)
wordDataCache.putAll(json.keys().asSequence().map { Pair(it, json.getInt(it)) })
}
@@ -102,7 +105,12 @@ class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainSc
textInputManager.isGlidePostEffect = true
textInputManager.smartbarView?.setCandidateSuggestionWords(
time,
suggestions.take(maxSuggestionsToShow).map { textInputManager.fixCase(it) }
// FIXME
/*suggestions.subList(
1.coerceAtMost(min(commit.compareTo(false), suggestions.size)),
maxSuggestionsToShow.coerceAtMost(suggestions.size)
).map { textInputManager.fixCase(it) }*/
null
)
textInputManager.smartbarView?.updateCandidateSuggestionCapsState()
if (commit && suggestions.isNotEmpty()) {

View File

@@ -4,14 +4,18 @@ 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.keyboard.Key
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.keyboard.TextKey
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import java.text.Normalizer
import java.util.*
import kotlin.collections.HashMap
import kotlin.math.*
private fun TextKey.baseCode(): Int {
return (data as? TextKeyData)?.code ?: KeyCode.UNSPECIFIED
}
/**
* Classifies gestures by comparing them with an "ideal gesture".
*
@@ -88,20 +92,29 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
override fun setLayout(keyViews: List<TextKey>, subtype: Subtype) {
// stop duplicate calls
if (layoutSubtype == subtype) {
if (layoutSubtype == subtype && keys == keyViews) {
return
}
// if only layout changed but not subtype
val layoutChanged = layoutSubtype == subtype
keysByCharacter.clear()
keys.clear()
keyViews.forEach {
keysByCharacter[it.computedData.code] = it
keysByCharacter[it.baseCode()] = it
keys.add(it)
}
layoutSubtype = subtype
distanceThresholdSquared = (keyViews.first().visibleBounds.width() / 4)
distanceThresholdSquared *= distanceThresholdSquared
initializePruner()
if (
(wordDataSubtype == layoutSubtype)
|| layoutChanged // should force a re-initialize
) {
initializePruner(layoutChanged)
}
}
override fun setWordData(words: HashMap<String, Int>, subtype: Subtype) {
@@ -114,7 +127,9 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
this.wordFrequencies = words
this.wordDataSubtype = subtype
initializePruner()
if (wordDataSubtype == layoutSubtype) {
initializePruner(false)
}
}
private val prunerCache = LruCache<Subtype, Pruner>(PRUNER_CACHE_SIZE)
@@ -123,13 +138,12 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
* 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
}
private fun initializePruner(invalidateCache: Boolean) {
val currentSubtype = this.layoutSubtype!!
val cached = prunerCache.get(currentSubtype)
val cached = when {
invalidateCache -> null
else -> prunerCache.get(currentSubtype)
}
if (cached == null) {
this.pruner = Pruner(PRUNING_LENGTH_THRESHOLD, this.words, keysByCharacter)
prunerCache.put(currentSubtype, this.pruner)
@@ -271,7 +285,7 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
* @return A list of likely words.
*/
fun pruneByExtremities(
userGesture: Gesture, keys: Iterable<Key>
userGesture: Gesture, keys: Iterable<TextKey>
): ArrayList<String> {
val remainingWords = ArrayList<String>()
val startX = userGesture.getFirstX()
@@ -338,7 +352,7 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
else -> {
val firstKey = keysByCharacter[firstBaseChar.code]
val lastKey = keysByCharacter[lastBaseChar.code]
Pair(firstKey.computedData.code, lastKey.computedData.code)
Pair(firstKey.baseCode(), lastKey.baseCode())
}
}
}
@@ -353,16 +367,21 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
* @return A list of the n closest keys.
*/
private fun findNClosestKeys(
x: Float, y: Float, n: Int, keys: Iterable<Key>
x: Float, y: Float, n: Int, keys: Iterable<TextKey>
): Iterable<Int> {
val keyDistances = HashMap<Key, Float>()
val keyDistances = HashMap<TextKey, Float>()
for (key in keys) {
val distance = Gesture.distance(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat(), x, y)
val distance = Gesture.distance(
key.visibleBounds.centerX().toFloat(),
key.visibleBounds.centerY().toFloat(),
x,
y
)
keyDistances[key] = distance
}
return keyDistances.entries.sortedWith { c1, c2 -> c1.value.compareTo(c2.value) }.take(n)
.map { (it.key as? TextKey)?.computedData?.code ?: KeyCode.UNSPECIFIED }
.map { it.key.baseCode() }
}
}
@@ -409,26 +428,39 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
if (previousLetter == lc) {
// bottom right
idealGestureWithLoops.addPoint(
key.visibleBounds.centerX() + key.visibleBounds.width() / 4.0f, key.visibleBounds.centerY() + key.visibleBounds.height() / 4.0f
key.visibleBounds.centerX() + key.visibleBounds.width() / 4.0f,
key.visibleBounds.centerY() + key.visibleBounds.height() / 4.0f
)
// top right
idealGestureWithLoops.addPoint(
key.visibleBounds.centerX() + key.visibleBounds.width() / 4.0f, key.visibleBounds.centerY() - key.visibleBounds.height() / 4.0f
key.visibleBounds.centerX() + key.visibleBounds.width() / 4.0f,
key.visibleBounds.centerY() - key.visibleBounds.height() / 4.0f
)
// top left
idealGestureWithLoops.addPoint(
key.visibleBounds.centerX() - key.visibleBounds.width() / 4.0f, key.visibleBounds.centerY() - key.visibleBounds.height() / 4.0f
key.visibleBounds.centerX() - key.visibleBounds.width() / 4.0f,
key.visibleBounds.centerY() - key.visibleBounds.height() / 4.0f
)
// bottom left
idealGestureWithLoops.addPoint(
key.visibleBounds.centerX() - key.visibleBounds.width() / 4.0f, key.visibleBounds.centerY() + key.visibleBounds.height() / 4.0f
key.visibleBounds.centerX() - key.visibleBounds.width() / 4.0f,
key.visibleBounds.centerY() + key.visibleBounds.height() / 4.0f
)
hasLoops = true
idealGesture.addPoint(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat())
idealGesture.addPoint(
key.visibleBounds.centerX().toFloat(),
key.visibleBounds.centerY().toFloat()
)
} else {
idealGesture.addPoint(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat())
idealGestureWithLoops.addPoint(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat())
idealGesture.addPoint(
key.visibleBounds.centerX().toFloat(),
key.visibleBounds.centerY().toFloat()
)
idealGestureWithLoops.addPoint(
key.visibleBounds.centerX().toFloat(),
key.visibleBounds.centerY().toFloat()
)
}
previousLetter = lc
}

View File

@@ -22,7 +22,9 @@ import android.view.VelocityTracker
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.debug.LogTopic
import dev.patrickgold.florisboard.debug.flogDebug
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import dev.patrickgold.florisboard.common.Pointer
import dev.patrickgold.florisboard.common.PointerMap
import dev.patrickgold.florisboard.common.ViewUtils
import kotlin.math.abs
import kotlin.math.atan2
@@ -67,7 +69,8 @@ abstract class SwipeGesture {
* @property listener The listener to report detected swipes to.
*/
class Detector(private val context: Context, private val listener: Listener) {
private var pointerDataMap: MutableMap<Int, PointerData> = mutableMapOf()
var isEnabled: Boolean = true
private var pointerMap: PointerMap<GesturePointer> = PointerMap { GesturePointer() }
private var thresholdSpeed: Double = numericValue(context, VelocityThreshold.NORMAL)
private var thresholdWidth: Double = numericValue(context, DistanceThreshold.NORMAL)
private var unitWidth: Double = thresholdWidth / 4.0
@@ -93,99 +96,92 @@ abstract class SwipeGesture {
* trigger, regardless of the distance from the previous event. Defaults to false.
* @return True if the given [event] is a gesture, false otherwise.
*/
fun onTouchEvent(event: MotionEvent, alwaysTriggerOnMove: Boolean = false): Boolean {
try {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN -> {
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
resetState()
velocityTracker.clear()
}
velocityTracker.addMovement(event)
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
pointerDataMap[pointerId] = PointerData().apply {
firstX = event.getX(pointerIndex)
firstY = event.getY(pointerIndex)
lastX = firstX
lastY = firstY
}
}
MotionEvent.ACTION_MOVE -> {
velocityTracker.addMovement(event)
for (pointerIndex in 0 until event.pointerCount) {
val pointerId = event.getPointerId(pointerIndex)
pointerDataMap[pointerId]?.apply {
val absDiffX = event.getX(pointerIndex) - firstX
val absDiffY = event.getY(pointerIndex) - firstY
val relDiffX = event.getX(pointerIndex) - lastX
val relDiffY = event.getY(pointerIndex) - lastY
return if (alwaysTriggerOnMove || abs(relDiffX) > (thresholdWidth / 2.0) || abs(relDiffY) > (thresholdWidth / 2.0)) {
lastX = event.getX(pointerIndex)
lastY = event.getY(pointerIndex)
val direction = detectDirection(relDiffX.toDouble(), relDiffY.toDouble())
val newAbsUnitCountX = (absDiffX / unitWidth).toInt()
val newAbsUnitCountY = (absDiffY / unitWidth).toInt()
val relUnitCountX = newAbsUnitCountX - absUnitCountX
val relUnitCountY = newAbsUnitCountY - absUnitCountY
absUnitCountX = newAbsUnitCountX
absUnitCountY = newAbsUnitCountY
listener.onSwipe(Event(
direction = direction,
type = Type.TOUCH_MOVE,
pointerId,
absUnitCountX,
absUnitCountY,
relUnitCountX,
relUnitCountY
))
} else {
false
}
}
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> {
velocityTracker.addMovement(event)
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
pointerDataMap.remove(pointerId)?.apply {
val absDiffX = event.getX(pointerIndex) - firstX
val absDiffY = event.getY(pointerIndex) - firstY
velocityTracker.computeCurrentVelocity(1000)
val velocityX = ViewLayoutUtils.convertDpToPixel(velocityTracker.getXVelocity(pointerId), context)
val velocityY = ViewLayoutUtils.convertDpToPixel(velocityTracker.getYVelocity(pointerId), context)
flogDebug(LogTopic.GESTURES) { "Velocity: $velocityX $velocityY dp/s" }
return if ((abs(absDiffX) > thresholdWidth || abs(absDiffY) > thresholdWidth) && (abs(velocityX) > thresholdSpeed || abs(velocityY) > thresholdSpeed)) {
val direction = detectDirection(absDiffX.toDouble(), absDiffY.toDouble())
absUnitCountX = (absDiffX / unitWidth).toInt()
absUnitCountY = (absDiffY / unitWidth).toInt()
listener.onSwipe(Event(
direction = direction,
type = Type.TOUCH_UP,
pointerId,
absUnitCountX,
absUnitCountY,
absUnitCountX,
absUnitCountY
))
} else {
false
}
}
return false
}
MotionEvent.ACTION_CANCEL -> {
resetState()
}
else -> return false
}
return false
} catch (e: Exception) {
return false
fun onTouchEvent(event: MotionEvent) {
if (!isEnabled) return
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
resetState()
velocityTracker.clear()
}
velocityTracker.addMovement(event)
}
fun onTouchDown(event: MotionEvent, pointer: Pointer) {
if (!isEnabled) return
pointerMap.add(pointer.id, pointer.index)?.let { gesturePointer ->
gesturePointer.firstX = event.getX(pointer.index)
gesturePointer.firstY = event.getY(pointer.index)
gesturePointer.lastX = gesturePointer.firstX
gesturePointer.lastY = gesturePointer.firstY
}
}
fun onTouchMove(event: MotionEvent, pointer: Pointer, alwaysTriggerOnMove: Boolean): Boolean {
if (!isEnabled) return false
pointerMap.findById(pointer.id)?.let { gesturePointer ->
gesturePointer.index = pointer.index
val absDiffX = event.getX(pointer.index) - gesturePointer.firstX
val absDiffY = event.getY(pointer.index) - gesturePointer.firstY
val relDiffX = event.getX(pointer.index) - gesturePointer.lastX
val relDiffY = event.getY(pointer.index) - gesturePointer.lastY
return if (alwaysTriggerOnMove || abs(relDiffX) > (thresholdWidth / 2.0) || abs(relDiffY) > (thresholdWidth / 2.0)) {
gesturePointer.lastX = event.getX(pointer.index)
gesturePointer.lastY = event.getY(pointer.index)
val direction = detectDirection(relDiffX.toDouble(), relDiffY.toDouble())
val newAbsUnitCountX = (absDiffX / unitWidth).toInt()
val newAbsUnitCountY = (absDiffY / unitWidth).toInt()
val relUnitCountX = newAbsUnitCountX - gesturePointer.absUnitCountX
val relUnitCountY = newAbsUnitCountY - gesturePointer.absUnitCountY
gesturePointer.absUnitCountX = newAbsUnitCountX
gesturePointer.absUnitCountY = newAbsUnitCountY
listener.onSwipe(Event(
direction = direction,
type = Type.TOUCH_MOVE,
pointer.id,
gesturePointer.absUnitCountX,
gesturePointer.absUnitCountY,
relUnitCountX,
relUnitCountY
))
} else {
false
}
}
return false
}
fun onTouchUp(event: MotionEvent, pointer: Pointer): Boolean {
if (!isEnabled) return false
pointerMap.findById(pointer.id)?.let { gesturePointer ->
val absDiffX = event.getX(pointer.index) - gesturePointer.firstX
val absDiffY = event.getY(pointer.index) - gesturePointer.firstY
velocityTracker.computeCurrentVelocity(1000)
val velocityX = ViewUtils.dp2px(velocityTracker.getXVelocity(pointer.id))
val velocityY = ViewUtils.dp2px(velocityTracker.getYVelocity(pointer.id))
flogDebug(LogTopic.GESTURES) { "Velocity: $velocityX $velocityY dp/s" }
pointerMap.removeById(pointer.id)
return if ((abs(absDiffX) > thresholdWidth || abs(absDiffY) > thresholdWidth) && (abs(velocityX) > thresholdSpeed || abs(velocityY) > thresholdSpeed)) {
val direction = detectDirection(absDiffX.toDouble(), absDiffY.toDouble())
gesturePointer.absUnitCountX = (absDiffX / unitWidth).toInt()
gesturePointer.absUnitCountY = (absDiffY / unitWidth).toInt()
listener.onSwipe(Event(
direction = direction,
type = Type.TOUCH_UP,
pointer.id,
gesturePointer.absUnitCountX,
gesturePointer.absUnitCountY,
gesturePointer.absUnitCountX,
gesturePointer.absUnitCountY
))
} else {
false
}
}
return false
}
fun onTouchCancel(event: MotionEvent, pointer: Pointer) {
if (!isEnabled) return
pointerMap.removeById(pointer.id)
}
/**
@@ -222,16 +218,26 @@ abstract class SwipeGesture {
* Resets the state.
*/
private fun resetState() {
pointerDataMap.clear()
pointerMap.clear()
}
class PointerData {
class GesturePointer : Pointer() {
var firstX: Float = 0.0f
var firstY: Float = 0.0f
var lastX: Float = 0.0f
var lastY: Float = 0.0f
var absUnitCountX: Int = 0
var absUnitCountY: Int = 0
override fun reset() {
super.reset()
firstX = 0.0f
firstY = 0.0f
lastX = 0.0f
lastY = 0.0f
absUnitCountX = 0
absUnitCountY = 0
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2021 ostrya
* 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
/**
* Helper class for summarizing all hint preferences in one single object.
*/
data class KeyHintConfiguration(
val symbolHintMode: KeyHintMode,
val numberHintMode: KeyHintMode,
val mergeHintPopups: Boolean
) {
companion object {
val HINTS_DISABLED = KeyHintConfiguration(KeyHintMode.DISABLED, KeyHintMode.DISABLED, false)
}
}

View File

@@ -20,15 +20,21 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
enum class KeyVariation {
enum class KeyVariation(val value: Int) {
@SerialName("all")
ALL,
ALL(0),
@SerialName("email")
EMAIL_ADDRESS,
EMAIL_ADDRESS(1),
@SerialName("normal")
NORMAL,
NORMAL(2),
@SerialName("password")
PASSWORD,
PASSWORD(3),
@SerialName("uri")
URI;
URI(4);
companion object {
fun fromInt(int: Int) = values().firstOrNull { it.value == int } ?: ALL
}
fun toInt() = value
}

View File

@@ -16,15 +16,21 @@
package dev.patrickgold.florisboard.ime.text.keyboard
enum class KeyboardMode {
CHARACTERS,
EDITING,
SYMBOLS,
SYMBOLS2,
NUMERIC,
NUMERIC_ADVANCED,
PHONE,
PHONE2,
SMARTBAR_CLIPBOARD_CURSOR_ROW,
SMARTBAR_NUMBER_ROW
enum class KeyboardMode(val value: Int) {
CHARACTERS(0),
EDITING(1),
SYMBOLS(2),
SYMBOLS2(3),
NUMERIC(4),
NUMERIC_ADVANCED(5),
PHONE(6),
PHONE2(7),
SMARTBAR_CLIPBOARD_CURSOR_ROW(8),
SMARTBAR_NUMBER_ROW(9);
companion object {
fun fromInt(int: Int) = values().firstOrNull { it.value == int } ?: CHARACTERS
}
fun toInt() = value
}

View File

@@ -19,6 +19,7 @@ package dev.patrickgold.florisboard.ime.text.keyboard
import dev.patrickgold.florisboard.ime.keyboard.Key
import dev.patrickgold.florisboard.ime.keyboard.KeyData
import dev.patrickgold.florisboard.ime.popup.MutablePopupSet
import dev.patrickgold.florisboard.ime.popup.PopupMapping
import dev.patrickgold.florisboard.ime.popup.PopupSet
import dev.patrickgold.florisboard.ime.text.key.*
@@ -26,7 +27,8 @@ class TextKey(override val data: KeyData) : Key(data) {
var computedData: TextKeyData = TextKeyData.UNSPECIFIED
private set
val computedPopups: MutablePopupSet<TextKeyData> = MutablePopupSet()
var computedHint: TextKeyData? = null
var computedSymbolHint: TextKeyData? = null
var computedNumberHint: TextKeyData? = null
fun compute(evaluator: TextComputingEvaluator) {
val keyboardMode = evaluator.getKeyboard().mode
@@ -44,10 +46,7 @@ class TextKey(override val data: KeyData) : Key(data) {
} else {
computedData = computed
computedPopups.clear()
computedPopups.hint = computedHint?.computeTextKeyData(evaluator)
if (computed is BasicTextKeyData && computed.popup != null) {
computedPopups.merge(computed.popup, evaluator)
}
mergePopups(computed, evaluator, computedPopups::merge)
if (keyboardMode == KeyboardMode.CHARACTERS || keyboardMode == KeyboardMode.NUMERIC_ADVANCED ||
keyboardMode == KeyboardMode.SYMBOLS || keyboardMode == KeyboardMode.SYMBOLS2) {
val extLabel = when (computed.groupId) {
@@ -97,6 +96,9 @@ class TextKey(override val data: KeyData) : Key(data) {
keySpecificPopupSet?.let { merge(it, evaluator) }
popupSet?.let { merge(it, evaluator) }
}
if (computed.type == KeyType.CHARACTER) {
addComputedHints(computed.code, evaluator, extendedPopups, extendedPopupsDefault)
}
}
isEnabled = evaluator.evaluateEnabled(computed)
isVisible = true
@@ -150,4 +152,55 @@ class TextKey(override val data: KeyData) : Key(data) {
}
}
}
inline fun setPressed(state: Boolean, blockIfChanged: () -> Unit) {
if (isPressed != state) {
isPressed = state
blockIfChanged()
}
}
private fun addComputedHints(
keyCode: Int,
evaluator: TextComputingEvaluator,
extendedPopups: PopupMapping?,
extendedPopupsDefault: PopupMapping?
) {
val symbolHint = computedSymbolHint
if (symbolHint != null) {
val evaluatedSymbolHint = symbolHint.computeTextKeyData(evaluator)
if (symbolHint.code != keyCode) {
computedPopups.symbolHint = evaluatedSymbolHint
mergePopups(evaluatedSymbolHint, evaluator, computedPopups::mergeSymbolHint)
val hintSpecificPopupSet =
extendedPopups?.get(KeyVariation.ALL)?.get(symbolHint.label) ?: extendedPopupsDefault?.get(
KeyVariation.ALL
)?.get(symbolHint.label)
hintSpecificPopupSet?.let { computedPopups.mergeSymbolHint(it, evaluator) }
}
}
val numericHint = computedNumberHint
if (numericHint != null) {
val evaluatedNumberHint = numericHint.computeTextKeyData(evaluator)
if (numericHint.code != keyCode) {
computedPopups.numberHint = evaluatedNumberHint
mergePopups(evaluatedNumberHint, evaluator, computedPopups::mergeNumberHint)
val hintSpecificPopupSet =
extendedPopups?.get(KeyVariation.ALL)?.get(numericHint.label) ?: extendedPopupsDefault?.get(
KeyVariation.ALL
)?.get(numericHint.label)
hintSpecificPopupSet?.let { computedPopups.mergeNumberHint(it, evaluator) }
}
}
}
private fun mergePopups(
keyData: TextKeyData?,
evaluator: TextComputingEvaluator,
merge: (popups: PopupSet<TextKeyData>, evaluator: TextComputingEvaluator) -> Unit
) {
if (keyData is BasicTextKeyData && keyData.popup != null) {
merge(keyData.popup, evaluator)
}
}
}

View File

@@ -67,202 +67,202 @@ interface TextKeyData : KeyData {
companion object {
/** Predefined key data for [KeyCode.ARROW_DOWN] */
val ARROW_DOWN = AutoTextKeyData(
val ARROW_DOWN = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_DOWN,
label = "arrow_down"
)
/** Predefined key data for [KeyCode.ARROW_LEFT] */
val ARROW_LEFT = AutoTextKeyData(
val ARROW_LEFT = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_LEFT,
label = "arrow_left"
)
/** Predefined key data for [KeyCode.ARROW_RIGHT] */
val ARROW_RIGHT = AutoTextKeyData(
val ARROW_RIGHT = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_RIGHT,
label = "arrow_right"
)
/** Predefined key data for [KeyCode.ARROW_UP] */
val ARROW_UP = AutoTextKeyData(
val ARROW_UP = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_UP,
label = "arrow_up"
)
/** Predefined key data for [KeyCode.CLIPBOARD_COPY] */
val CLIPBOARD_COPY = AutoTextKeyData(
val CLIPBOARD_COPY = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_COPY,
label = "clipboard_copy"
)
/** Predefined key data for [KeyCode.CLIPBOARD_CUT] */
val CLIPBOARD_CUT = AutoTextKeyData(
val CLIPBOARD_CUT = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_CUT,
label = "clipboard_cut"
)
/** Predefined key data for [KeyCode.CLIPBOARD_PASTE] */
val CLIPBOARD_PASTE = AutoTextKeyData(
val CLIPBOARD_PASTE = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_PASTE,
label = "clipboard_paste"
)
/** Predefined key data for [KeyCode.CLIPBOARD_SELECT] */
val CLIPBOARD_SELECT = AutoTextKeyData(
val CLIPBOARD_SELECT = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_SELECT,
label = "clipboard_select"
)
/** Predefined key data for [KeyCode.CLIPBOARD_SELECT_ALL] */
val CLIPBOARD_SELECT_ALL = AutoTextKeyData(
val CLIPBOARD_SELECT_ALL = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_SELECT_ALL,
label = "clipboard_select_all"
)
/** Predefined key data for [KeyCode.DELETE] */
val DELETE = AutoTextKeyData(
val DELETE = BasicTextKeyData(
type = KeyType.ENTER_EDITING,
code = KeyCode.DELETE,
label = "delete"
)
/** Predefined key data for [KeyCode.DELETE_WORD] */
val DELETE_WORD = AutoTextKeyData(
val DELETE_WORD = BasicTextKeyData(
type = KeyType.ENTER_EDITING,
code = KeyCode.DELETE_WORD,
label = "delete_word"
)
/** Predefined key data for [KeyCode.INTERNAL_BATCH_EDIT] */
val INTERNAL_BATCH_EDIT = AutoTextKeyData(
val INTERNAL_BATCH_EDIT = BasicTextKeyData(
type = KeyType.FUNCTION,
code = KeyCode.INTERNAL_BATCH_EDIT,
label = "internal_batch_edit"
)
/** Predefined key data for [KeyCode.MOVE_START_OF_LINE] */
val MOVE_START_OF_LINE = AutoTextKeyData(
val MOVE_START_OF_LINE = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_START_OF_LINE,
label = "move_start_of_line"
)
/** Predefined key data for [KeyCode.MOVE_END_OF_LINE] */
val MOVE_END_OF_LINE = AutoTextKeyData(
val MOVE_END_OF_LINE = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_END_OF_LINE,
label = "move_end_of_line"
)
/** Predefined key data for [KeyCode.MOVE_START_OF_PAGE] */
val MOVE_START_OF_PAGE = AutoTextKeyData(
val MOVE_START_OF_PAGE = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_START_OF_PAGE,
label = "move_start_of_page"
)
/** Predefined key data for [KeyCode.MOVE_END_OF_PAGE] */
val MOVE_END_OF_PAGE = AutoTextKeyData(
val MOVE_END_OF_PAGE = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_END_OF_PAGE,
label = "move_end_of_page"
)
/** Predefined key data for [KeyCode.REDO] */
val REDO = AutoTextKeyData(
val REDO = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.REDO,
label = "redo"
)
/** Predefined key data for [KeyCode.SHOW_INPUT_METHOD_PICKER] */
val SHOW_INPUT_METHOD_PICKER = AutoTextKeyData(
val SHOW_INPUT_METHOD_PICKER = BasicTextKeyData(
type = KeyType.FUNCTION,
code = KeyCode.SHOW_INPUT_METHOD_PICKER,
label = "show_input_method_picker"
)
/** Predefined key data for [KeyCode.SWITCH_TO_TEXT_CONTEXT] */
val SWITCH_TO_TEXT_CONTEXT = AutoTextKeyData(
val SWITCH_TO_TEXT_CONTEXT = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.SWITCH_TO_TEXT_CONTEXT,
label = "switch_to_text_context"
)
/** Predefined key data for [KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT] */
val SWITCH_TO_CLIPBOARD_CONTEXT = AutoTextKeyData(
val SWITCH_TO_CLIPBOARD_CONTEXT = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT,
label = "switch_to_clipboard_context"
)
/** Predefined key data for [KeyCode.SHIFT] */
val SHIFT = AutoTextKeyData(
val SHIFT = BasicTextKeyData(
type = KeyType.MODIFIER,
code = KeyCode.SHIFT,
label = "shift"
)
/** Predefined key data for [KeyCode.SHIFT_LOCK] */
val SHIFT_LOCK = AutoTextKeyData(
val SHIFT_LOCK = BasicTextKeyData(
type = KeyType.MODIFIER,
code = KeyCode.SHIFT_LOCK,
label = "shift_lock"
)
/** Predefined key data for [KeyCode.SPACE] */
val SPACE = AutoTextKeyData(
val SPACE = BasicTextKeyData(
type = KeyType.CHARACTER,
code = KeyCode.SPACE,
label = "space"
)
/** Predefined key data for [KeyCode.UNDO] */
val UNDO = AutoTextKeyData(
val UNDO = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.UNDO,
label = "undo"
)
/** Predefined key data for [KeyCode.UNSPECIFIED] */
val UNSPECIFIED = AutoTextKeyData(
val UNSPECIFIED = BasicTextKeyData(
type = KeyType.UNSPECIFIED,
code = KeyCode.UNSPECIFIED,
label = "unspecified"
)
/** Predefined key data for [KeyCode.VIEW_CHARACTERS] */
val VIEW_CHARACTERS = AutoTextKeyData(
val VIEW_CHARACTERS = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.VIEW_CHARACTERS,
label = "view_characters"
)
/** Predefined key data for [KeyCode.VIEW_SYMBOLS] */
val VIEW_SYMBOLS = AutoTextKeyData(
val VIEW_SYMBOLS = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.VIEW_SYMBOLS,
label = "view_symbols"
)
/** Predefined key data for [KeyCode.VIEW_SYMBOLS2] */
val VIEW_SYMBOLS2 = AutoTextKeyData(
val VIEW_SYMBOLS2 = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.VIEW_SYMBOLS2,
label = "view_symbols2"
)
/** Predefined key data for [KeyCode.VIEW_NUMERIC_ADVANCED] */
val VIEW_NUMERIC_ADVANCED = AutoTextKeyData(
val VIEW_NUMERIC_ADVANCED = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.VIEW_NUMERIC_ADVANCED,
label = "view_numeric_advanced"

View File

@@ -150,7 +150,18 @@ class LayoutManager {
val mainLayout = loadLayoutAsync(main).await().getOrNull()
val modifierToLoad = if (mainLayout?.modifier != null) {
LTN(LayoutType.CHARACTERS_MOD, mainLayout.modifier)
val layoutType = when (mainLayout.type) {
LayoutType.SYMBOLS -> {
LayoutType.SYMBOLS_MOD
}
LayoutType.SYMBOLS2 -> {
LayoutType.SYMBOLS2_MOD
}
else -> {
LayoutType.CHARACTERS_MOD
}
}
LTN(layoutType, mainLayout.modifier)
} else {
modifier
}
@@ -207,22 +218,21 @@ class LayoutManager {
// Add hints to keys
if (keyboardMode == KeyboardMode.CHARACTERS) {
val symbolsComputedArrangement = computeKeyboardAsync(KeyboardMode.SYMBOLS, subtype).await().arrangement
val minRow = if (prefs.keyboard.numberRow) { 1 } else { 0 }
// number row hint always happens on first row
if (prefs.keyboard.hintedNumberRowMode != KeyHintMode.DISABLED) {
val row = computedArrangement[0]
val symbolRow = symbolsComputedArrangement[0]
addRowHints(row, symbolRow, KeyType.NUMERIC)
}
// all other symbols are added bottom-aligned
val rOffset = computedArrangement.size - symbolsComputedArrangement.size
for ((r, row) in computedArrangement.withIndex()) {
if (r >= (3 + minRow) || r < minRow) {
if (r < rOffset) {
continue
}
val symbolRow = symbolsComputedArrangement.getOrNull(r - minRow)
val symbolRow = symbolsComputedArrangement.getOrNull(r - rOffset)
if (symbolRow != null) {
for ((k, key) in row.withIndex()) {
val symbol = symbolRow.getOrNull(k)?.data?.computeTextKeyData(DefaultTextComputingEvaluator)
val type = (key.data as? TextKeyData)?.type ?: KeyType.UNSPECIFIED
if (r == minRow && type == KeyType.CHARACTER && symbol?.type == KeyType.NUMERIC) {
key.computedHint = symbol
} else if (r > minRow && type == KeyType.CHARACTER && symbol?.type == KeyType.CHARACTER) {
key.computedHint = symbol
}
}
addRowHints(row, symbolRow, KeyType.CHARACTER)
}
}
}
@@ -240,6 +250,27 @@ class LayoutManager {
)
}
private fun addRowHints(main: Array<TextKey>, hint: Array<TextKey>, hintType: KeyType) {
for ((k,key) in main.withIndex()) {
val hintKey = hint.getOrNull(k)?.data?.computeTextKeyData(DefaultTextComputingEvaluator)
if (hintKey?.type != hintType) {
continue
}
when (hintType) {
KeyType.CHARACTER -> {
key.computedSymbolHint = hintKey
}
KeyType.NUMERIC -> {
key.computedNumberHint = hintKey
}
else -> {
// do nothing
}
}
}
}
/**
* Computes a layout for [keyboardMode] based on the given [subtype] and returns it.
*

View File

@@ -33,9 +33,11 @@ import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.OverScroller
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.ime.theme.ThemeValue
@@ -100,7 +102,7 @@ class CandidateView : View, ThemeManager.OnThemeUpdatedListener {
themeManager = ThemeManager.defaultOrNull()
themeManager?.registerOnThemeUpdatedListener(this)
florisClipboardManager = FlorisClipboardManager.getInstanceOrNull()
updateCandidates(candidates)
recomputeCandidates()
}
override fun onDetachedFromWindow() {
@@ -113,7 +115,10 @@ class CandidateView : View, ThemeManager.OnThemeUpdatedListener {
velocityTracker = null
}
fun updateCandidates(newCandidates: List<String>?) {
fun updateCandidates(newCandidates: SuggestionList?) {
if (candidates.isEmpty() && (newCandidates == null || newCandidates.isEmpty())) {
return // no need to recompute anything
}
candidates.clear()
if (newCandidates != null) {
candidates.addAll(newCandidates)
@@ -289,7 +294,9 @@ class CandidateView : View, ThemeManager.OnThemeUpdatedListener {
selectedIndex = -1
scroller.abortAnimation()
scrollTo(0, 0)
invalidate()
if (isVisible) {
invalidate()
}
}
override fun computeScroll() {
@@ -415,6 +422,7 @@ class CandidateView : View, ThemeManager.OnThemeUpdatedListener {
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (!isVisible) return
canvas ?: return
backgroundPaint.apply { color = candidateBackground.toSolidColor().color }
dividerPaint.apply { color = ColorUtils.setAlphaComponent(dividerBackground.toSolidColor().color, 64) }

View File

@@ -17,17 +17,27 @@
package dev.patrickgold.florisboard.ime.text.smartbar
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.util.Size
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InlineSuggestion
import android.widget.inline.InlineContentView
import androidx.annotation.IdRes
import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.SmartbarBinding
import dev.patrickgold.florisboard.debug.*
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.keyboard.KeyboardState
import dev.patrickgold.florisboard.ime.keyboard.updateKeyboardState
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.theme.Theme
@@ -38,6 +48,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.lang.ref.WeakReference
import java.util.*
import kotlin.math.roundToInt
/**
@@ -45,7 +56,7 @@ import kotlin.math.roundToInt
* of FlorisBoard. The view automatically tries to get the current FlorisBoard instance, which it
* needs to decide when a specific feature component is shown.
*/
class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
class SmartbarView : ConstraintLayout, KeyboardState.OnUpdateStateListener, ThemeManager.OnThemeUpdatedListener {
private val florisboard = FlorisBoard.getInstanceOrNull()
private val prefs get() = Preferences.default()
private val themeManager = ThemeManager.default()
@@ -59,11 +70,11 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
private var cachedActionEndAreaVisible: Boolean = false
@IdRes private var cachedActionEndAreaId: Int? = null
var isQuickActionsVisible: Boolean = false
set(v) {
binding.quickActionToggle.rotation = if (v) 180.0f else 0.0f
field = v
}
private val cachedState: KeyboardState = KeyboardState.new(
maskOfInterest = KeyboardState.INTEREST_TEXT
or KeyboardState.F_IS_QUICK_ACTIONS_VISIBLE
or KeyboardState.F_IS_SHOWING_INLINE_SUGGESTIONS
)
private lateinit var binding: SmartbarBinding
private var indexedActionStartArea: MutableList<Int> = mutableListOf()
@@ -142,8 +153,7 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
}
binding.quickActionToggle.setOnClickListener {
isQuickActionsVisible = !isQuickActionsVisible
updateSmartbarState()
eventListener.get()?.onSmartbarQuickActionPressed(it.id)
}
configureFeatureVisibility(
@@ -212,13 +222,26 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
}
}
/**
* Updates the Smartbar UI state by looking at the current keyboard mode, key variation, active
* editor instance, etc. Passes the evaluated attributes to [configureFeatureVisibility].
*/
fun updateSmartbarState() {
//binding.clipboardCursorRow.updateVisibility()
binding.candidates.updateDisplaySettings(prefs.suggestion.displayMode, prefs.suggestion.clipboardContentTimeout * 1_000)
override fun onInterceptUpdateKeyboardState(newState: KeyboardState): Boolean {
return true // SmartbarView is manually managing the dispatching of new states
}
override fun onUpdateKeyboardState(newState: KeyboardState) {
flogInfo(LogTopic.SMARTBAR)
if (newState != cachedState) {
cachedState.reset(newState)
if (this::binding.isInitialized) {
updateUi()
when (cachedMainAreaId) {
R.id.clipboard_cursor_row -> binding.clipboardCursorRow.updateKeyboardState(newState)
R.id.number_row -> binding.numberRow.updateKeyboardState(newState)
}
}
}
}
private fun updateUi() {
binding.quickActionToggle.rotation = if (cachedState.isQuickActionsVisible) 180.0f else 0.0f
when (florisboard) {
null -> configureFeatureVisibility(
actionStartAreaVisible = false,
@@ -228,54 +251,58 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
actionEndAreaId = null
)
else -> configureFeatureVisibility(
actionStartAreaVisible = when (florisboard.textInputManager.keyVariation) {
actionStartAreaVisible = when (cachedState.keyVariation) {
KeyVariation.PASSWORD -> false
else -> true
},
actionStartAreaId = when (florisboard.textInputManager.getActiveKeyboardMode()) {
actionStartAreaId = when (cachedState.keyboardMode) {
KeyboardMode.EDITING -> R.id.back_button
else -> R.id.quick_action_toggle
},
mainAreaId = when (florisboard.textInputManager.keyVariation) {
KeyVariation.PASSWORD -> R.id.number_row
else -> when (isQuickActionsVisible) {
true -> R.id.quick_actions
else -> when (florisboard.textInputManager.getActiveKeyboardMode()) {
KeyboardMode.EDITING -> null
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> R.id.clipboard_cursor_row
else -> when {
florisboard.activeEditorInstance.isComposingEnabled &&
florisboard.activeEditorInstance.selection.isCursorMode
-> R.id.candidates
else -> R.id.clipboard_cursor_row
}
mainAreaId = when {
cachedState.isQuickActionsVisible -> R.id.quick_actions
cachedState.isShowingInlineSuggestions -> R.id.inline_suggestions
cachedState.keyVariation == KeyVariation.PASSWORD -> {
if (!prefs.keyboard.numberRow) R.id.number_row else null
}
else -> when (florisboard.textInputManager.getActiveKeyboardMode()) {
KeyboardMode.EDITING -> null
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> R.id.clipboard_cursor_row
else -> when {
florisboard.activeEditorInstance.isComposingEnabled &&
florisboard.activeEditorInstance.selection.isCursorMode
-> R.id.candidates
else -> R.id.clipboard_cursor_row
}
}
},
actionEndAreaVisible = when (florisboard.textInputManager.keyVariation) {
actionEndAreaVisible = when (cachedState.keyVariation) {
KeyVariation.PASSWORD -> false
else -> true
},
actionEndAreaId = when {
florisboard.activeEditorInstance.isPrivateMode -> R.id.private_mode_button
cachedState.isPrivateMode -> R.id.private_mode_button
else -> null
}
)
}
binding.clipboardCursorRow.notifyStateChanged()
binding.numberRow.notifyStateChanged()
}
fun sync() {
binding.numberRow.sync()
binding.clipboardCursorRow.sync()
binding.candidates.updateDisplaySettings(prefs.suggestion.displayMode, prefs.suggestion.clipboardContentTimeout * 1_000)
}
fun onPrimaryClipChanged() {
if (prefs.suggestion.enabled && prefs.suggestion.clipboardContentEnabled && florisboard?.activeEditorInstance?.isPrivateMode == false ) {
florisboard.florisClipboardManager?.primaryClip?.let { binding.candidates.updateClipboardItem(it) }
updateSmartbarState()
if (prefs.suggestion.enabled && prefs.suggestion.clipboardContentEnabled && !cachedState.isPrivateMode) {
florisboard?.florisClipboardManager?.primaryClip?.let { binding.candidates.updateClipboardItem(it) }
}
}
fun setCandidateSuggestionWords(suggestionInitDate: Long, suggestions: List<String>?) {
fun setCandidateSuggestionWords(suggestionInitDate: Long, suggestions: SuggestionList?) {
if (suggestionInitDate > lastSuggestionInitDate) {
lastSuggestionInitDate = suggestionInitDate
binding.candidates.updateCandidates(suggestions)
@@ -286,6 +313,69 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
//
}
/**
* Clears the inline suggestions and triggers thw Smartbar update cycle.
*/
@RequiresApi(Build.VERSION_CODES.R)
fun clearInlineSuggestions() {
updateInlineSuggestionStrip(listOf())
}
/**
* Inflates the given inline suggestions. Once all provided views are ready, the suggestions
* strip is updated and the Smartbar update cycle is triggered.
*
* @param inlineSuggestions A collection of inline suggestions to be inflated and shown.
*/
@RequiresApi(Build.VERSION_CODES.R)
fun showInlineSuggestions(inlineSuggestions: Collection<InlineSuggestion>) {
if (inlineSuggestions.isEmpty()) {
updateInlineSuggestionStrip(listOf())
} else {
val suggestionMap: TreeMap<Int, InlineContentView?> = TreeMap()
for ((i, inlineSuggestion) in inlineSuggestions.withIndex()) {
val size = Size(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
try {
inlineSuggestion.inflate(context, size, context.mainExecutor) { suggestionView ->
flogDebug { "New inline suggestion view ready" }
suggestionMap[i] = suggestionView
if (suggestionMap.size >= inlineSuggestions.size) {
updateInlineSuggestionStrip(suggestionMap.values)
}
}
} catch (e: Throwable) {
flogWarning { "Failed to inflate inline suggestion: $e" }
}
}
}
}
/**
* Updates the suggestion strip with given inline content views and triggers the Smartbar
* update cycle.
*
* @param suggestionViews A collection of inline content views to show.
*/
@RequiresApi(Build.VERSION_CODES.R)
private fun updateInlineSuggestionStrip(suggestionViews: Collection<InlineContentView?>) {
flogDebug { "Updating the inline suggestion strip with ${suggestionViews.size} items" }
binding.inlineSuggestionsStrip.removeAllViews()
val florisboard = florisboard ?: return
if (suggestionViews.isEmpty()) {
florisboard.activeState.isQuickActionsVisible = false
return
} else {
for (suggestionView in suggestionViews) {
if (suggestionView == null) {
continue
}
binding.inlineSuggestionsStrip.addView(suggestionView)
}
florisboard.activeState.isQuickActionsVisible = true
}
updateKeyboardState(florisboard.activeState)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec).toFloat()

View File

@@ -16,14 +16,26 @@
package dev.patrickgold.florisboard.ime.theme
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.Icon
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.common.ImageViewStyle
import androidx.autofill.inline.common.TextViewStyle
import androidx.autofill.inline.common.ViewStyle
import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.ime.extension.AssetManager
import dev.patrickgold.florisboard.ime.extension.AssetRef
@@ -305,6 +317,82 @@ class ThemeManager private constructor(
}
}
/**
* Creates a new inline suggestion UI bundle based on the attributes of the given [theme].
*
* @param context The context of the parent view/controller.
* @param theme The theme from which the color attributes should be fetched. Defaults to [activeTheme].
*
* @return A bundle containing all necessary attributes for the inline suggestion views to properly display.
*/
@SuppressLint("RestrictedApi")
@RequiresApi(Build.VERSION_CODES.R)
fun createInlineSuggestionUiStyleBundle(context: Context, theme: Theme = activeTheme): Bundle {
val bgColor = theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_BACKGROUND).toSolidColor().color
val fgColor = theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_FOREGROUND).toSolidColor().color
val bgDrawableId = R.drawable.chip_background
val stylesBuilder = UiVersions.newStylesBuilder()
val style = InlineSuggestionUi.newStyleBuilder()
.setSingleIconChipStyle(
ViewStyle.Builder()
.setBackground(
Icon.createWithResource(context, bgDrawableId).setTint(bgColor)
)
.setPadding(0, 0, 0, 0)
.build()
)
.setChipStyle(
ViewStyle.Builder()
.setBackground(
Icon.createWithResource(context, bgDrawableId).setTint(bgColor)
)
.setPadding(
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_start).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_top).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_end).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_bottom).toInt(),
)
.build()
)
.setStartIconStyle(
ImageViewStyle.Builder()
.setLayoutMargin(0, 0, 0, 0)
.build()
)
.setTitleStyle(
TextViewStyle.Builder()
.setLayoutMargin(
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_start).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_top).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_end).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_bottom).toInt(),
)
.setTextColor(fgColor)
.setTextSize(16f)
.build()
)
.setSubtitleStyle(
TextViewStyle.Builder()
.setLayoutMargin(
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_start).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_top).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_end).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_bottom).toInt(),
)
.setTextColor(ColorUtils.setAlphaComponent(fgColor, 150))
.setTextSize(14f)
.build()
)
.setEndIconStyle(
ImageViewStyle.Builder()
.setLayoutMargin(0, 0, 0, 0)
.build()
)
.build()
stylesBuilder.addStyle(style)
return stylesBuilder.build()
}
data class RemoteColors(
val packageName: String,
val colorPrimary: ThemeValue.SolidColor?,

View File

@@ -17,17 +17,23 @@
package dev.patrickgold.florisboard.settings
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.MenuItem
import android.webkit.WebView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.AboutActivityBinding
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
import dev.patrickgold.florisboard.util.AppVersionUtils
import dev.patrickgold.florisboard.util.checkIfImeIsSelected
class AboutActivity : AppCompatActivity() {
private lateinit var binding: AboutActivityBinding
@@ -43,7 +49,22 @@ class AboutActivity : AppCompatActivity() {
setSupportActionBar(toolbar)
// Set app version string
binding.appVersion.text = "v" + AppVersionUtils.getRawVersionName(this)
val appVersion = "v" + AppVersionUtils.getRawVersionName(this)
binding.appVersion.text = appVersion
// Set setOnLongClickListener for copying the version string
binding.headArea.setOnLongClickListener {
val isImeSelected = checkIfImeIsSelected(this)
if (isImeSelected) {
FlorisClipboardManager.getInstance().addNewPlaintext(appVersion)
} else {
val clipboard: ClipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Florisboard version", appVersion)
clipboard.setPrimaryClip(clip)
}
Toast.makeText(this, R.string.about__version_copied__title, Toast.LENGTH_LONG).show()
true
}
// Set onClickListeners for buttons
binding.privacyPolicyButton.setOnClickListener {

View File

@@ -32,7 +32,7 @@ import androidx.appcompat.widget.Toolbar
import androidx.core.view.forEach
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.ThemeManagerActivityBinding
import dev.patrickgold.florisboard.ime.core.FlorisActivity
import dev.patrickgold.florisboard.common.FlorisActivity
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.extension.AssetManager
@@ -45,7 +45,7 @@ import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.ime.theme.ThemeMetaOnly
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import dev.patrickgold.florisboard.common.ViewUtils
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -383,8 +383,8 @@ class ThemeManagerActivity : FlorisActivity<ThemeManagerActivityBinding>() {
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
val marginV = ViewLayoutUtils.convertDpToPixel(8.0f, context).toInt()
val marginH = ViewLayoutUtils.convertDpToPixel(16.0f, context).toInt()
val marginV = ViewUtils.dp2px(8.0f).toInt()
val marginH = ViewUtils.dp2px(16.0f).toInt()
setMargins(marginH, marginV, marginH, marginV)
setPadding(marginV, 0, 0, 0)
}

View File

@@ -34,7 +34,7 @@ import androidx.recyclerview.widget.RecyclerView
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.UdmActivityBinding
import dev.patrickgold.florisboard.databinding.UdmEntryDialogBinding
import dev.patrickgold.florisboard.ime.core.FlorisActivity
import dev.patrickgold.florisboard.common.FlorisActivity
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
import dev.patrickgold.florisboard.ime.dictionary.FREQUENCY_DEFAULT
import dev.patrickgold.florisboard.ime.dictionary.FREQUENCY_MAX

View File

@@ -36,17 +36,17 @@ import dev.patrickgold.florisboard.databinding.ThemeEditorAttrDialogBinding
import dev.patrickgold.florisboard.databinding.ThemeEditorAttrViewBinding
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeValue
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import dev.patrickgold.florisboard.common.ViewUtils
import dev.patrickgold.florisboard.util.getActivity
class ThemeAttrView : LinearLayout {
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private lateinit var binding: ThemeEditorAttrViewBinding
private val previewDrawableSolid = GradientDrawable().apply {
cornerRadius = ViewLayoutUtils.convertDpToPixel(6.0f, context)
cornerRadius = ViewUtils.dp2px(6.0f)
}
private val previewDrawableGradient = GradientDrawable().apply {
cornerRadius = ViewLayoutUtils.convertDpToPixel(6.0f, context)
cornerRadius = ViewUtils.dp2px(6.0f)
}
var themeAttrGroupView: ThemeAttrGroupView? = null

View File

@@ -98,6 +98,9 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
dialogView.languageSpinner.setOnSelectedListener { pos ->
val selectedCode = subtypeManager.imeConfig.defaultSubtypesLanguageCodes[pos]
val defaultSubtype = subtypeManager.getDefaultSubtypeForLocale(LocaleUtils.stringToLocale(selectedCode)) ?: return@setOnSelectedListener
dialogView.composerSpinner.setSelection(
subtypeManager.imeConfig.composerNames.indexOf(defaultSubtype.composerName).coerceAtLeast(0)
)
dialogView.currencySetSpinner.setSelection(
subtypeManager.imeConfig.currencySetNames.indexOf(defaultSubtype.currencySetName).coerceAtLeast(0)
)
@@ -107,6 +110,9 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
)
}
}
dialogView.composerSpinner.initItems(
labels = subtypeManager.imeConfig.composerLabels
)
dialogView.currencySetSpinner.initItems(
labels = subtypeManager.imeConfig.currencySetLabels
)
@@ -129,6 +135,7 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
// already exists.
activeDialogWindow?.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
val languageCode = subtypeManager.imeConfig.defaultSubtypesLanguageCodes[dialogView.languageSpinner.selectedItemPosition]
val composerName = subtypeManager.imeConfig.composerNames[dialogView.composerSpinner.selectedItemPosition]
val currencySetName = subtypeManager.imeConfig.currencySetNames[dialogView.currencySetSpinner.selectedItemPosition]
val layoutMap = SubtypeLayoutMap(
characters = layoutManager.getMetaNameListFor(LayoutType.CHARACTERS)[dialogView.charactersLayoutSpinner.selectedItemPosition],
@@ -140,7 +147,7 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
phone = layoutManager.getMetaNameListFor(LayoutType.PHONE)[dialogView.phoneLayoutSpinner.selectedItemPosition],
phone2 = layoutManager.getMetaNameListFor(LayoutType.PHONE2)[dialogView.phone2LayoutSpinner.selectedItemPosition],
)
val success = subtypeManager.addSubtype(LocaleUtils.stringToLocale(languageCode), currencySetName, layoutMap)
val success = subtypeManager.addSubtype(LocaleUtils.stringToLocale(languageCode), composerName, currencySetName, layoutMap)
if (!success) {
dialogView.errorBox.setText(R.string.settings__localization__subtype_error_already_exists)
dialogView.errorBox.visibility = View.VISIBLE
@@ -161,6 +168,11 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
keys = subtypeManager.imeConfig.defaultSubtypesLanguageCodes,
defaultSelectedKey = subtype.locale.toString()
)
dialogView.composerSpinner.initItems(
labels = subtypeManager.imeConfig.composerLabels,
keys = subtypeManager.imeConfig.composerNames,
defaultSelectedKey = subtype.composerName
)
dialogView.currencySetSpinner.initItems(
labels = subtypeManager.imeConfig.currencySetLabels,
keys = subtypeManager.imeConfig.currencySetNames,
@@ -180,6 +192,7 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
setView(dialogView.root)
setPositiveButton(R.string.settings__localization__subtype_apply) { _, _ ->
val languageCode = subtypeManager.imeConfig.defaultSubtypesLanguageCodes[dialogView.languageSpinner.selectedItemPosition]
val composerName = subtypeManager.imeConfig.composerNames[dialogView.composerSpinner.selectedItemPosition]
val currencySetName = subtypeManager.imeConfig.currencySetNames[dialogView.currencySetSpinner.selectedItemPosition]
val layoutMap = SubtypeLayoutMap(
characters = layoutManager.getMetaNameListFor(LayoutType.CHARACTERS)[dialogView.charactersLayoutSpinner.selectedItemPosition],
@@ -194,6 +207,7 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
subtypeManager.modifySubtypeWithSameId(Subtype(
id = subtype.id,
locale = LocaleUtils.stringToLocale(languageCode),
composerName = composerName,
currencySetName = currencySetName,
layoutMap = layoutMap
))

View File

@@ -17,6 +17,7 @@
package dev.patrickgold.florisboard.settings.fragments
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
@@ -27,6 +28,9 @@ import dev.patrickgold.florisboard.settings.UdmActivity
class TypingInnerFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.prefs_typing)
findPreference<Preference>(Preferences.Suggestion.API30_INLINE_SUGGESTIONS_ENABLED)?.let {
it.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
}
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {

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