Compare commits
110 Commits
v0.3.11-be
...
v0.3.13-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a317372b2 | ||
|
|
402f7bd267 | ||
|
|
e8eb6e3068 | ||
|
|
3dd9c45777 | ||
|
|
7255229361 | ||
|
|
4d2fa29886 | ||
|
|
ef90faf98b | ||
|
|
82caa8365e | ||
|
|
391257e9e9 | ||
|
|
b082253167 | ||
|
|
8df701e3fe | ||
|
|
9f232f5dbf | ||
|
|
7017726dcb | ||
|
|
b48ca8fd1e | ||
|
|
88d5e15a5e | ||
|
|
e9537cbd1d | ||
|
|
8e216bf3ac | ||
|
|
63352cc615 | ||
|
|
e9e2563739 | ||
|
|
87bb098445 | ||
|
|
da1944bedf | ||
|
|
d4a92e0d46 | ||
|
|
0fa6c1f235 | ||
|
|
260b1ba5ca | ||
|
|
f0799a6a0e | ||
|
|
155238946a | ||
|
|
45f91cf40c | ||
|
|
94f5b56b6a | ||
|
|
46db467073 | ||
|
|
17dde536d9 | ||
|
|
be67bf4b84 | ||
|
|
8f142548fe | ||
|
|
a68f439f39 | ||
|
|
7a0892bb36 | ||
|
|
8457390156 | ||
|
|
72be3898c1 | ||
|
|
d35bf5af63 | ||
|
|
04d3af6484 | ||
|
|
26920e4a98 | ||
|
|
7419966b51 | ||
|
|
58b832c6c3 | ||
|
|
99f2ec1879 | ||
|
|
4249f9ef86 | ||
|
|
60107ae299 | ||
|
|
6a95a865fa | ||
|
|
9e32589af5 | ||
|
|
6133e225e1 | ||
|
|
348c143d92 | ||
|
|
ce00785ffe | ||
|
|
78cdce750d | ||
|
|
f3f95ae282 | ||
|
|
018885eb30 | ||
|
|
c6c8a76dd6 | ||
|
|
3cae8b7230 | ||
|
|
814c8de0c2 | ||
|
|
32fe175b48 | ||
|
|
b901f6de8d | ||
|
|
fe9ba3246c | ||
|
|
71a39f0fc1 | ||
|
|
f7556898e1 | ||
|
|
578539f5d0 | ||
|
|
7c28c7fbea | ||
|
|
88bcadff81 | ||
|
|
25e25dfbf0 | ||
|
|
ba3dc0178d | ||
|
|
91e7f424bb | ||
|
|
b89f791eb0 | ||
|
|
ad3a0425ab | ||
|
|
7cf52ecf3e | ||
|
|
b1ef18f4fd | ||
|
|
b74af5bbe9 | ||
|
|
b8aa4bbfc4 | ||
|
|
e024ac9272 | ||
|
|
c5fa027a8e | ||
|
|
b6ec2b25be | ||
|
|
a756b59c60 | ||
|
|
8687ce55ed | ||
|
|
1ac6985dd0 | ||
|
|
986b4a878f | ||
|
|
1ef38fe7f3 | ||
|
|
bcad0af35e | ||
|
|
b5b89fde4f | ||
|
|
be1fc710ed | ||
|
|
aa55fd3070 | ||
|
|
a132462466 | ||
|
|
df393ff607 | ||
|
|
64040f0407 | ||
|
|
0c1abdd507 | ||
|
|
53594e3343 | ||
|
|
c6c06b87c5 | ||
|
|
ae6eb5d72d | ||
|
|
bbce53fdf4 | ||
|
|
88a6f436ef | ||
|
|
ee8f44d816 | ||
|
|
0308ec355f | ||
|
|
3ac14f8a2a | ||
|
|
2b087b76dc | ||
|
|
1715e5ddfa | ||
|
|
6cc17161a5 | ||
|
|
5d1c20617b | ||
|
|
d9efa48c9c | ||
|
|
dedd4cb7f0 | ||
|
|
42b147b656 | ||
|
|
47ce490d6c | ||
|
|
5563a1cadd | ||
|
|
7beb2e5ef6 | ||
|
|
f00da13cba | ||
|
|
bfed1747f7 | ||
|
|
abb4b104fa | ||
|
|
b69b1caa72 |
@@ -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]
|
||||
|
||||
4
.github/workflows/android.yml
vendored
4
.github/workflows/android.yml
vendored
@@ -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
3
.gitignore
vendored
@@ -41,5 +41,8 @@ captures/
|
||||
*.jks
|
||||
crowdin.properties
|
||||
|
||||
# C++
|
||||
.cxx/
|
||||
|
||||
# AndroidX Room schema JSONs
|
||||
/app/schemas/
|
||||
|
||||
28
README.md
28
README.md
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
61
app/src/main/assets/ime/text/characters/bone.json
Normal file
61
app/src/main/assets/ime/text/characters/bone.json
Normal 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" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "ϋ" }
|
||||
]
|
||||
},
|
||||
"ω": {
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": "ץ" }
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
62
app/src/main/assets/ime/text/characters/korean.json
Normal file
62
app/src/main/assets/ime/text/characters/korean.json
Normal 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": "ㅡ"}
|
||||
]
|
||||
]
|
||||
}
|
||||
53
app/src/main/assets/ime/text/characters/mod/neo2.json
Normal file
53
app/src/main/assets/ime/text/characters/mod/neo2.json
Normal 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" }
|
||||
]
|
||||
]
|
||||
}
|
||||
61
app/src/main/assets/ime/text/characters/neo2.json
Normal file
61
app/src/main/assets/ime/text/characters/neo2.json
Normal 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" }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/bengali.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/bengali.json
Normal 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": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/devanagari.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/devanagari.json
Normal 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": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/gujarati.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/gujarati.json
Normal 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": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/gurmukhi.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/gurmukhi.json
Normal 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": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/kannada.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/kannada.json
Normal 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": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/malayalam.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/malayalam.json
Normal 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": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
80
app/src/main/assets/ime/text/numeric/row/neo2.json
Normal file
80
app/src/main/assets/ime/text/numeric/row/neo2.json
Normal 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": "—" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/oriya.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/oriya.json
Normal 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": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/tamil.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/tamil.json
Normal 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": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/telugu.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/telugu.json
Normal 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": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
22
app/src/main/assets/ime/text/symbols/mod/neo2.json
Normal file
22
app/src/main/assets/ime/text/symbols/mod/neo2.json
Normal 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" }
|
||||
]
|
||||
]
|
||||
}
|
||||
46
app/src/main/assets/ime/text/symbols/neo2.json
Normal file
46
app/src/main/assets/ime/text/symbols/neo2.json
Normal 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": ";" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
38
app/src/main/cpp/CMakeLists.txt
Normal file
38
app/src/main/cpp/CMakeLists.txt
Normal 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
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
0
app/src/main/cpp/ime/dummy
Normal file
0
app/src/main/cpp/ime/dummy
Normal file
13
app/src/main/cpp/ime/nlp/CMakeLists.txt
Normal file
13
app/src/main/cpp/ime/nlp/CMakeLists.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
add_library(
|
||||
# Name
|
||||
ime-nlp
|
||||
|
||||
# Headers
|
||||
nlp.h
|
||||
token.h
|
||||
suggestion_list.h
|
||||
|
||||
# Sources
|
||||
token.cpp
|
||||
suggestion_list.cpp
|
||||
)
|
||||
32
app/src/main/cpp/ime/nlp/nlp.h
Normal file
32
app/src/main/cpp/ime/nlp/nlp.h
Normal 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
|
||||
98
app/src/main/cpp/ime/nlp/suggestion_list.cpp
Normal file
98
app/src/main/cpp/ime/nlp/suggestion_list.cpp
Normal 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;
|
||||
}
|
||||
51
app/src/main/cpp/ime/nlp/suggestion_list.h
Normal file
51
app/src/main/cpp/ime/nlp/suggestion_list.h
Normal 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
|
||||
61
app/src/main/cpp/ime/nlp/token.cpp
Normal file
61
app/src/main/cpp/ime/nlp/token.cpp
Normal 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
|
||||
51
app/src/main/cpp/ime/nlp/token.h
Normal file
51
app/src/main/cpp/ime/nlp/token.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
186
app/src/main/java/dev/patrickgold/florisboard/common/Pointer.kt
Normal file
186
app/src/main/java/dev/patrickgold/florisboard/common/Pointer.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>)
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
))
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user