Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e581d6cbc4 | ||
|
|
ec13d008fb | ||
|
|
edfea1afcb | ||
|
|
25fc23d721 | ||
|
|
c701141be2 | ||
|
|
e5b956857e | ||
|
|
67236ef58d | ||
|
|
2da17a0654 | ||
|
|
1f3221a886 | ||
|
|
47f80d00c4 | ||
|
|
d648c480b5 | ||
|
|
9e26720674 | ||
|
|
a20c6bf148 | ||
|
|
d2df5cfcdf | ||
|
|
93b5503dfc | ||
|
|
4d4b54074a | ||
|
|
904fd9b85a | ||
|
|
e4f5fcf74b | ||
|
|
15f0316839 | ||
|
|
93654c4f88 | ||
|
|
62fc549ea9 | ||
|
|
d0dbd1cd4e | ||
|
|
af28f84b69 | ||
|
|
db7ee52029 | ||
|
|
7343617792 | ||
|
|
5898d7006b | ||
|
|
058be7a169 | ||
|
|
e6f2a25021 | ||
|
|
3a485a1574 | ||
|
|
0ee0f24119 | ||
|
|
004e999259 | ||
|
|
11775c4619 | ||
|
|
177bad95b3 | ||
|
|
610526d845 | ||
|
|
55e489bc07 | ||
|
|
589063be61 | ||
|
|
aa73ac706a | ||
|
|
91cbbe74a3 | ||
|
|
637d7fe503 | ||
|
|
6a5e5a1708 | ||
|
|
22fad5ba0b | ||
|
|
f3d2c8257a | ||
|
|
bc89675269 | ||
|
|
2603eb2b52 | ||
|
|
38baac1af9 | ||
|
|
7e56094f5c | ||
|
|
af6ee13855 | ||
|
|
edb8d87fa0 | ||
|
|
ff35372945 | ||
|
|
b6edbf76d0 | ||
|
|
1bde28e288 | ||
|
|
aafb02cb68 | ||
|
|
a07c91f089 | ||
|
|
1af3c1a210 | ||
|
|
c13ec8aca9 | ||
|
|
4a826cc0a3 | ||
|
|
1c9e4c0b4c | ||
|
|
6cbbca5658 | ||
|
|
a1b8550fe2 | ||
|
|
ab1007175d | ||
|
|
4cf8b4af58 | ||
|
|
2b001d9eb8 | ||
|
|
286ddd9971 | ||
|
|
d4c6411e1a | ||
|
|
915bcec0ee | ||
|
|
93eb731bf0 | ||
|
|
7a02f1c958 | ||
|
|
160d31beb0 | ||
|
|
6d389b9a7f | ||
|
|
3ea620a22e | ||
|
|
94f9f3f3e7 | ||
|
|
8f28d0e81a | ||
|
|
07ce0c83fa | ||
|
|
c95244cc06 | ||
|
|
3c2d427b1d | ||
|
|
85da2141cb | ||
|
|
cc9688a2dd | ||
|
|
cd048af114 | ||
|
|
4382dfc869 | ||
|
|
a622749b7b | ||
|
|
1c2596147a | ||
|
|
d355143ba1 | ||
|
|
a9eb4c0eec | ||
|
|
487a37bc66 | ||
|
|
85a54e701e | ||
|
|
2666acd4ae | ||
|
|
ba72e6274f | ||
|
|
e20ce07957 | ||
|
|
765a12537e | ||
|
|
4845ce55b5 | ||
|
|
47cd655d76 | ||
|
|
d3edd3d332 | ||
|
|
2d32364123 | ||
|
|
509308ec82 | ||
|
|
db65af5ea5 | ||
|
|
9a46cf9dff | ||
|
|
2591eaa49d | ||
|
|
57350b422d | ||
|
|
fe8efa8496 | ||
|
|
c5ce9ba252 | ||
|
|
4e39273812 | ||
|
|
43995f1ac5 | ||
|
|
3688f8e8dc | ||
|
|
5cbbbc2295 | ||
|
|
af08947929 | ||
|
|
3e8a227320 | ||
|
|
adb69dc365 | ||
|
|
c2998c9a2e | ||
|
|
f801c31ebb | ||
|
|
6b8652bcd9 | ||
|
|
65b5d252b6 | ||
|
|
c5ae916ece | ||
|
|
de3fcceeaf | ||
|
|
f06ea18e89 | ||
|
|
9d7754b8db | ||
|
|
2be1a328b6 | ||
|
|
9cd7931b3e | ||
|
|
03f9014b7c | ||
|
|
62abefc36e | ||
|
|
f5d79e8556 | ||
|
|
e68428ef11 | ||
|
|
1cfde9c2b9 | ||
|
|
765a596eb2 | ||
|
|
a27035a81b | ||
|
|
380eaffb08 | ||
|
|
0e7eac2796 | ||
|
|
6da344fd6c | ||
|
|
7787af69fd | ||
|
|
288bd61fb4 | ||
|
|
fe69c0f3e1 | ||
|
|
766c5efa95 | ||
|
|
35bd6e7c8d | ||
|
|
23f14ab57d | ||
|
|
ac688a38ab | ||
|
|
a2e393d7dd | ||
|
|
ba8ebaf231 | ||
|
|
a0e381ed93 | ||
|
|
09833a3369 | ||
|
|
f014b010d8 | ||
|
|
9512eb32f0 | ||
|
|
82f99bd721 | ||
|
|
1d710dfb85 | ||
|
|
1328eb1f05 | ||
|
|
1cda0662ae | ||
|
|
11cacb25c8 | ||
|
|
c0207fd84e | ||
|
|
56d3acfc67 | ||
|
|
a3e5ae9337 | ||
|
|
7e84f71464 | ||
|
|
eb88fbc981 | ||
|
|
96320e6b06 | ||
|
|
fee9c2a0ac | ||
|
|
c74a5841ec | ||
|
|
aab7a6e33a | ||
|
|
0ea59cf2ed | ||
|
|
1be6ce1ae8 | ||
|
|
8d06bea6bb | ||
|
|
4b1a0c9972 | ||
|
|
3d50ea59af | ||
|
|
83bef3aee9 | ||
|
|
c365acb800 | ||
|
|
ef075151fb | ||
|
|
5316e46ba6 | ||
|
|
467fb62067 | ||
|
|
75c59366a9 | ||
|
|
6df6e25b15 | ||
|
|
81d89cad8c | ||
|
|
99954237f7 | ||
|
|
b5e56642fc | ||
|
|
343525f259 | ||
|
|
a5adad714a | ||
|
|
67214cbdf1 | ||
|
|
3a0284f355 | ||
|
|
eecada3118 | ||
|
|
68061f1aac | ||
|
|
c4ac4abd63 |
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{*.har,*.json}]
|
||||
indent_size = 2
|
||||
|
||||
[*.kt]
|
||||
ij_kotlin_name_count_to_use_star_import = 99
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 99
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1 +1,2 @@
|
||||
* text=auto eol=lf
|
||||
* text=auto eol=lf
|
||||
*.bat text=auto eol=crlf
|
||||
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -15,18 +15,21 @@ assignees: ''
|
||||
- Thank you for your help in making FlorisBoard better!
|
||||
-->
|
||||
|
||||
**Environment information**
|
||||
- FlorisBoard Version: <!-- e.g. 0.1.0 -->
|
||||
- Install Source: <!-- Google PlayStore/F-Droid/GitHub/? -->
|
||||
- Device: <!-- e.g. OnePlus 7T -->
|
||||
- Android version, ROM: <!-- e.g. 10, Stock -->
|
||||
#### Short description
|
||||
Describe the bug in a short but concise way.
|
||||
|
||||
**Steps to reproduce**
|
||||
#### Steps to reproduce
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
#### Environment information
|
||||
- FlorisBoard Version: <!-- e.g. 0.3.6 -->
|
||||
- Install Source: <!-- Google PlayStore/F-Droid/GitHub/? -->
|
||||
- Device: <!-- e.g. OnePlus 7T -->
|
||||
- Android version, ROM: <!-- e.g. 10, Stock -->
|
||||
|
||||
<!-- (remove this line if you paste a log)
|
||||
```
|
||||
If applicable, paste the captured debug log here.
|
||||
|
||||
@@ -32,7 +32,7 @@ free to ask for help at any time!
|
||||
|
||||
## Adding a new keyboard layout / dictionary for locale
|
||||
|
||||
You can now oficially add layouts to FlorisBoard as described below.
|
||||
You can now officially add layouts to FlorisBoard as described below.
|
||||
FlorisBoard's core has stabilized enough that adding new content is
|
||||
safe, although there will be some changes in the future.
|
||||
|
||||
|
||||
44
README.md
44
README.md
@@ -62,11 +62,9 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
|
||||
* [x] Landscape orientation support (needs tweaks)
|
||||
|
||||
### Layouts
|
||||
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish,
|
||||
Norwegian, Swedish/Finnish, Icelandic, Danish, Hungarian,
|
||||
Croatian, Polish, Romanian); more coming in future versions
|
||||
* [x] Non-latin character layouts (Arabic, Persian, Greek, Russian
|
||||
(JCUKEN))
|
||||
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish, Norwegian, Swedish/Finnish, Icelandic, Danish,
|
||||
Hungarian, Croatian, Polish, Romanian, Colemak, Dvorak, ...)
|
||||
* [x] Non-latin character layouts (Arabic, Persian, Greek, Russian (JCUKEN))
|
||||
* [x] Adapt to situation in app (password, url, text, etc. )
|
||||
* [x] Special character layout(s)
|
||||
* [x] Numeric layout
|
||||
@@ -106,7 +104,7 @@ most likely be delayed back, even though I'm eager to stick to these as
|
||||
close as possible.
|
||||
|
||||
### [v0.4.0](https://github.com/florisboard/florisboard/milestone/4)
|
||||
- Module A: Smartbar rework (Implemented with #91)
|
||||
- Module A: Smartbar rework (Implemented with [#91])
|
||||
- Ability to enable/disable Smartbar (features below thus only work if
|
||||
Smartbar is enabled)
|
||||
- Dynamic switching between clipboard tools and word suggestions
|
||||
@@ -115,14 +113,13 @@ close as possible.
|
||||
- Complete rework of the Smartbar code base and the Smartbar layout
|
||||
definition in XML
|
||||
|
||||
- Module B: Composing suggestions
|
||||
- Module B: Composing suggestions (Phase 1: [#329])
|
||||
- Auto-suggestion of words based of precompiled dictionaries
|
||||
- Management of custom dictionary entries
|
||||
- Opt-in only: Learning of often typed word pais to better predict next
|
||||
words over time. Data collected here is stored locally and never leaves
|
||||
- Next-word suggestions by training language models. Data collected here is stored locally and never leaves
|
||||
the user's device.
|
||||
|
||||
- Module C: Extension packs (base implementation with #162)
|
||||
- Module C: Extension packs (base implementation with [#162])
|
||||
- Ability to load dictionaries (and later potentially other cool
|
||||
features too) only if needed to keep the core APK size small
|
||||
- Currently unclear how exactly this will work, but this is definitely
|
||||
@@ -132,13 +129,14 @@ close as possible.
|
||||
- Swiping over the characters will automatically convert this to a word
|
||||
- Possibly also add improvements based on the Flow keyboard
|
||||
|
||||
- Module E: Theme rework (Implemented with #162)
|
||||
- Module E: Theme rework (Implemented with [#162])
|
||||
- Themes are now based on the Asset schema
|
||||
- Dynamic theme creation
|
||||
- Different theme modes (`Always day`, `Always dark`, `Follow system`
|
||||
- Different theme modes (`Always day`, `Always night`, `Follow system`
|
||||
and `Follow time`)
|
||||
- Define a separate theme both for day and night theme
|
||||
- Adapt to app theme if possible
|
||||
- Theme import/export
|
||||
|
||||
### [v0.5.0](https://github.com/florisboard/florisboard/milestone/5)
|
||||
There's no exact roadmap yet but it is planned that the media part of
|
||||
@@ -151,9 +149,12 @@ passes...
|
||||
|
||||
Backlog (currently not assigned to any milestone):
|
||||
|
||||
- Theme import/export
|
||||
- Floating keyboard
|
||||
|
||||
[#91]: https://github.com/florisboard/florisboard/pull/91
|
||||
[#162]: https://github.com/florisboard/florisboard/pull/162
|
||||
[#329]: https://github.com/florisboard/florisboard/pull/329
|
||||
|
||||
## Contributing
|
||||
Wanna contribute to FlorisBoard? That's great to hear! There are lots of
|
||||
different ways to help out. Bug reporting, making pull requests,
|
||||
@@ -176,11 +177,24 @@ to get more information on this topic.
|
||||
[Jared Rummler](https://github.com/jaredrummler)
|
||||
* [Timber](https://github.com/JakeWharton/timber) by
|
||||
[JakeWharton](https://github.com/JakeWharton)
|
||||
* [kotlin-result](https://github.com/michaelbull/kotlin-result) by
|
||||
[Michael Bull](https://github.com/michaelbull)
|
||||
* [expandable-fab](https://github.com/nambicompany/expandable-fab) by
|
||||
[Nambi](https://github.com/nambicompany)
|
||||
|
||||
## Usage notes for included binary dictionary files
|
||||
All binary dictionaries included within this project in
|
||||
(this)[app/src/main/assets/ime/dict) asset folder are built from various
|
||||
sources, as stated below.
|
||||
|
||||
### Source 1: [wordfreq library by LuminosoInsight](https://github.com/LuminosoInsight/wordfreq):
|
||||
`wordfreq` is a repository which provides both a Python library and raw
|
||||
data (the wordlists). Only the data has been extracted in order to build
|
||||
binary dictionary files from it. `wordfreq`'s data is licensed under the
|
||||
Creative Commons Attribution-ShareAlike 4.0 license
|
||||
(https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
|
||||
For further information on what wordfreq's data depends on, see
|
||||
(https://github.com/LuminosoInsight/wordfreq#license).
|
||||
|
||||
## License
|
||||
```
|
||||
Copyright 2020 Patrick Goldinger
|
||||
|
||||
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,76 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion "29.0.2"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "dev.patrickgold.florisboard"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 29
|
||||
versionCode 22
|
||||
versionName "0.3.3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
resValue "string", "app_name", "FlorisBoard Debug"
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
resValue "string", "app_name", "FlorisBoard"
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'androidx.test:core:1.3.0'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
testImplementation 'org.mockito:mockito-inline:2.13.0'
|
||||
testImplementation 'org.robolectric:robolectric:4.4'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
implementation 'com.google.android:flexbox:2.0.1'
|
||||
implementation "com.squareup.moshi:moshi-kotlin:1.9.2"
|
||||
implementation 'com.squareup.moshi:moshi-adapters:1.9.2'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
|
||||
implementation 'com.jaredrummler:colorpicker:1.1.0'
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
implementation "com.michael-bull.kotlin-result:kotlin-result:1.1.9"
|
||||
implementation 'com.nambimobile.widgets:expandable-fab:1.0.2'
|
||||
}
|
||||
97
app/build.gradle.kts
Normal file
97
app/build.gradle.kts
Normal file
@@ -0,0 +1,97 @@
|
||||
plugins {
|
||||
id("com.android.application") version "4.1.2"
|
||||
kotlin("android") version "1.4.30"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion(30)
|
||||
buildToolsVersion("30.0.3")
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
freeCompilerArgs = listOf("-Xallow-result-return-type") // enables use of kotlin.Result
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "dev.patrickgold.florisboard"
|
||||
minSdkVersion(23)
|
||||
targetSdkVersion(30)
|
||||
versionCode(28)
|
||||
versionName("0.3.9")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
named("debug").configure {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
|
||||
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_debug")
|
||||
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_debug_round")
|
||||
resValue("string", "floris_app_name", "FlorisBoard Debug")
|
||||
}
|
||||
|
||||
create("beta") // Needed because by default the "beta" BuildType does not exist
|
||||
named("beta").configure {
|
||||
applicationIdSuffix = ".beta"
|
||||
versionNameSuffix = "-beta"
|
||||
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
|
||||
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_beta")
|
||||
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_beta_round")
|
||||
resValue("string", "floris_app_name", "FlorisBoard Beta")
|
||||
}
|
||||
|
||||
named("release").configure {
|
||||
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
|
||||
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_release")
|
||||
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_release_round")
|
||||
resValue("string", "floris_app_name", "@string/app_name")
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
isAbortOnError = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.activity", "activity-ktx", "1.2.1")
|
||||
implementation("androidx.appcompat", "appcompat", "1.2.0")
|
||||
implementation("androidx.core", "core-ktx", "1.3.2")
|
||||
implementation("androidx.fragment", "fragment-ktx", "1.3.0")
|
||||
implementation("androidx.preference", "preference-ktx", "1.1.1")
|
||||
implementation("androidx.constraintlayout", "constraintlayout", "2.0.4")
|
||||
implementation("androidx.lifecycle", "lifecycle-service", "2.2.0")
|
||||
implementation("com.google.android", "flexbox", "2.0.1") // requires jcenter as of version 2.0.1
|
||||
implementation("com.squareup.moshi", "moshi-kotlin", "1.11.0")
|
||||
implementation("com.squareup.moshi", "moshi-adapters", "1.11.0")
|
||||
implementation("com.google.android.material", "material", "1.3.0")
|
||||
implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-android", "1.4.2")
|
||||
implementation("com.jaredrummler", "colorpicker", "1.1.0")
|
||||
implementation("com.jakewharton.timber", "timber", "4.7.1")
|
||||
implementation("com.nambimobile.widgets", "expandable-fab", "1.0.2")
|
||||
|
||||
testImplementation("junit", "junit", "4.13.1")
|
||||
testImplementation("org.mockito", "mockito-inline", "3.7.7")
|
||||
testImplementation("org.robolectric", "robolectric", "4.5.1")
|
||||
androidTestImplementation("androidx.test.ext", "junit", "1.1.2")
|
||||
androidTestImplementation("androidx.test.espresso", "espresso-core", "3.3.0")
|
||||
}
|
||||
21
app/proguard-rules.pro
vendored
21
app/proguard-rules.pro
vendored
@@ -1,21 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -1,13 +1,11 @@
|
||||
package dev.patrickgold.florisboard
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -23,17 +23,16 @@
|
||||
<application
|
||||
android:name=".ime.core.FlorisApplication"
|
||||
android:allowBackup="false"
|
||||
android:extractNativeLibs="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/floris_app_name"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/SettingsTheme">
|
||||
|
||||
<!-- IME service -->
|
||||
<service
|
||||
android:name="dev.patrickgold.florisboard.ime.core.FlorisBoard"
|
||||
android:label="@string/app_name"
|
||||
android:label="@string/floris_app_name"
|
||||
android:permission="android.permission.BIND_INPUT_METHOD">
|
||||
<meta-data
|
||||
android:name="android.view.im"
|
||||
@@ -47,19 +46,19 @@
|
||||
<!-- Settings Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.SettingsMainActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__title"
|
||||
android:launchMode="singleTask"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Using an activity alias to disable/enable the app icon in the launcher -->
|
||||
<activity-alias
|
||||
android:name="dev.patrickgold.florisboard.SettingsLauncherAlias"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/floris_app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:targetActivity="dev.patrickgold.florisboard.setup.SetupActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
@@ -70,45 +69,45 @@
|
||||
<!-- Theme Selector Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.ThemeManagerActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__title"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Theme Editor Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.ThemeEditorActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__theme_editor__title"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- About Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.AboutActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/about__title"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Advanced Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.AdvancedActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__advanced__title"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Setup Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.setup.SetupActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/setup__title"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Crash Dialog Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.crashutility.CrashDialogActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/crash_dialog__title"
|
||||
android:theme="@style/CrashDialogTheme"/>
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
"qwerty": "QWERTY",
|
||||
"qwertz": "QWERTZ",
|
||||
"azerty": "AZERTY",
|
||||
"bepo": "BÉPO",
|
||||
"bulgarian_bds": "Bulgarian (BDS)",
|
||||
"bulgarian_phonetic": "Bulgarian (Phonetic)",
|
||||
"spanish": "Spanish (QWERTY)",
|
||||
"norwegian": "Norwegian (QWERTY)",
|
||||
"swedish_finnish": "Swedish/Finnish (QWERTY)",
|
||||
@@ -21,7 +24,11 @@
|
||||
"dvorak": "Dvorak",
|
||||
"jcuken_russian": "Russian (JCUKEN)",
|
||||
"canadian_french": "Canadian French (QWERTY)",
|
||||
"greek": "Ελληνικά"
|
||||
"greek": "Ελληνικά",
|
||||
"hebrew": "עברית",
|
||||
"serbian_latin": "Serbian (QWERTZ)",
|
||||
"serbian_cyrillic": "Serbian (ЉЊЕРТЗ)",
|
||||
"kurdish": "کوردی"
|
||||
},
|
||||
"defaultSubtypes": [
|
||||
{
|
||||
@@ -183,6 +190,36 @@
|
||||
"id": 1601,
|
||||
"languageTag": "pl",
|
||||
"preferredLayout": "qwerty"
|
||||
},
|
||||
{
|
||||
"id": 1701,
|
||||
"languageTag": "bg-bg",
|
||||
"preferredLayout": "bulgarian_phonetic"
|
||||
},
|
||||
{
|
||||
"id": 1801,
|
||||
"languageTag": "tr",
|
||||
"preferredLayout": "qwerty"
|
||||
},
|
||||
{
|
||||
"id": 1901,
|
||||
"languageTag": "iw-IL",
|
||||
"preferredLayout": "hebrew"
|
||||
},
|
||||
{
|
||||
"id": 2001,
|
||||
"languageTag": "ckb",
|
||||
"preferredLayout": "kurdish"
|
||||
},
|
||||
{
|
||||
"id": 2101,
|
||||
"languageTag": "sr-RS",
|
||||
"preferredLayout": "serbian_cyrillic"
|
||||
},
|
||||
{
|
||||
"id": 2201,
|
||||
"languageTag": "lv-LV",
|
||||
"preferredLayout": "qwerty"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BIN
app/src/main/assets/ime/dict/en.flict
Normal file
BIN
app/src/main/assets/ime/dict/en.flict
Normal file
Binary file not shown.
53
app/src/main/assets/ime/text/characters/bepo.json
Normal file
53
app/src/main/assets/ime/text/characters/bepo.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "bepo",
|
||||
"authors": [ "salamandar" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 98, "label": "b" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 112, "label": "p" },
|
||||
{ "code": 111, "label": "o" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 118, "label": "v" },
|
||||
{ "code": 100, "label": "d" },
|
||||
{ "code": 108, "label": "l" },
|
||||
{ "code": 106, "label": "j" },
|
||||
{ "code": 122, "label": "z" },
|
||||
{ "code": 119, "label": "w" }
|
||||
],
|
||||
[
|
||||
{ "code": 97, "label": "a" },
|
||||
{ "code": 117, "label": "u" },
|
||||
{ "code": 105, "label": "i" },
|
||||
{ "code": 101, "label": "e" },
|
||||
{ "code": 99, "label": "c" },
|
||||
{ "code": 116, "label": "t" },
|
||||
{ "code": 115, "label": "s" },
|
||||
{ "code": 114, "label": "r" },
|
||||
{ "code": 110, "label": "n" },
|
||||
{ "code": 109, "label": "m" },
|
||||
{ "code": 231, "label": "ç" }
|
||||
],
|
||||
[
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 121, "label": "y" },
|
||||
{ "code": 120, "label": "x" },
|
||||
{ "code": 107, "label": "k" },
|
||||
{ "code": 113, "label": "q", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 8218, "label": "‚" },
|
||||
{ "code": 8216, "label": "‘" },
|
||||
{ "code": 8217, "label": "’" },
|
||||
{ "code": 8249, "label": "‹" },
|
||||
{ "code": 8250, "label": "›" }
|
||||
]
|
||||
} },
|
||||
{ "code": 103, "label": "g" },
|
||||
{ "code": 104, "label": "h" },
|
||||
{ "code": 102, "label": "f" }
|
||||
]
|
||||
]
|
||||
}
|
||||
46
app/src/main/assets/ime/text/characters/bulgarian_bds.json
Normal file
46
app/src/main/assets/ime/text/characters/bulgarian_bds.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "bulgarian_bds",
|
||||
"authors": [ "iorvethe" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 1099, "label": "ы" },
|
||||
{ "code": 1091, "label": "у" },
|
||||
{ "code": 1077, "label": "е" },
|
||||
{ "code": 1080, "label": "и" },
|
||||
{ "code": 1096, "label": "ш" },
|
||||
{ "code": 1097, "label": "щ" },
|
||||
{ "code": 1082, "label": "к" },
|
||||
{ "code": 1089, "label": "с" },
|
||||
{ "code": 1076, "label": "д" },
|
||||
{ "code": 1079, "label": "з" },
|
||||
{ "code": 1094, "label": "ц" }
|
||||
],
|
||||
[
|
||||
{ "code": 1100, "label": "ь" },
|
||||
{ "code": 1103, "label": "я" },
|
||||
{ "code": 1072, "label": "а" },
|
||||
{ "code": 1086, "label": "о" },
|
||||
{ "code": 1078, "label": "ж" },
|
||||
{ "code": 1075, "label": "г" },
|
||||
{ "code": 1090, "label": "т" },
|
||||
{ "code": 1085, "label": "н" },
|
||||
{ "code": 1074, "label": "в" },
|
||||
{ "code": 1084, "label": "м" },
|
||||
{ "code": 1095, "label": "ч" }
|
||||
],
|
||||
[
|
||||
{ "code": 1102, "label": "ю" },
|
||||
{ "code": 1081, "label": "й" },
|
||||
{ "code": 1098, "label": "ъ" },
|
||||
{ "code": 1101, "label": "э" },
|
||||
{ "code": 1092, "label": "ф" },
|
||||
{ "code": 1093, "label": "х" },
|
||||
{ "code": 1087, "label": "п" },
|
||||
{ "code": 1088, "label": "р" },
|
||||
{ "code": 1083, "label": "л" },
|
||||
{ "code": 1073, "label": "б" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "bulgarian_phonetic",
|
||||
"authors": [ "iorvethe" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 1103, "label": "я" },
|
||||
{ "code": 1074, "label": "в" },
|
||||
{ "code": 1077, "label": "е" },
|
||||
{ "code": 1088, "label": "р" },
|
||||
{ "code": 1090, "label": "т" },
|
||||
{ "code": 1098, "label": "ъ" },
|
||||
{ "code": 1091, "label": "у" },
|
||||
{ "code": 1080, "label": "и" },
|
||||
{ "code": 1086, "label": "о" },
|
||||
{ "code": 1087, "label": "п" },
|
||||
{ "code": 1095, "label": "ч" }
|
||||
],
|
||||
[
|
||||
{ "code": 1072, "label": "а" },
|
||||
{ "code": 1089, "label": "с" },
|
||||
{ "code": 1076, "label": "д" },
|
||||
{ "code": 1092, "label": "ф" },
|
||||
{ "code": 1075, "label": "г" },
|
||||
{ "code": 1093, "label": "х" },
|
||||
{ "code": 1081, "label": "й" },
|
||||
{ "code": 1082, "label": "к" },
|
||||
{ "code": 1083, "label": "л" },
|
||||
{ "code": 1096, "label": "ш" },
|
||||
{ "code": 1097, "label": "щ" }
|
||||
],
|
||||
[
|
||||
{ "code": 1079, "label": "з" },
|
||||
{ "code": 1100, "label": "ь" },
|
||||
{ "code": 1094, "label": "ц" },
|
||||
{ "code": 1078, "label": "ж" },
|
||||
{ "code": 1073, "label": "б" },
|
||||
{ "code": 1085, "label": "н" },
|
||||
{ "code": 1084, "label": "м" },
|
||||
{ "code": 1102, "label": "ю" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"relevant": [
|
||||
{ "code": 58, "label": ":" }
|
||||
]
|
||||
} }
|
||||
}, "shift": { "code": 58, "label": ":" } }
|
||||
],
|
||||
[
|
||||
{ "code": 97, "label": "a" },
|
||||
|
||||
@@ -12,25 +12,25 @@
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 34, "label": "\"" }
|
||||
]
|
||||
} },
|
||||
}, "shift": { "code": 34, "label": "\"" } },
|
||||
{ "code": 39, "label": "'", "groupId": 101, "variation": "password", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 34, "label": "\"" }
|
||||
]
|
||||
} },
|
||||
}, "shift": { "code": 34, "label": "\"" } },
|
||||
{ "code": 47, "label": "/", "groupId": 101, "variation": "uri" },
|
||||
{ "code": 44, "label": ",", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 60, "label": "<" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
} },
|
||||
}, "shift": { "code": 60, "label": "<" } },
|
||||
{ "code": 46, "label": ".", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 62, "label": ">" }
|
||||
]
|
||||
} },
|
||||
}, "shift": { "code": 62, "label": ">" } },
|
||||
{ "code": 112, "label": "p" },
|
||||
{ "code": 121, "label": "y" },
|
||||
{ "code": 102, "label": "f" },
|
||||
|
||||
@@ -5,18 +5,19 @@
|
||||
"mapping": {
|
||||
"all": {
|
||||
"~enter": {
|
||||
"main": { "code": -215, "label": "toggle_one_handed_mode", "type": "system_gui" },
|
||||
"main": { "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
|
||||
"relevant": [
|
||||
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" }
|
||||
{ "code": -216, "label": "toggle_one_handed_mode_right", "type": "system_gui" }
|
||||
]
|
||||
},
|
||||
"~left": {
|
||||
"main": { "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
|
||||
"relevant": [
|
||||
{ "code": -215, "label": "toggle_one_handed_mode", "type": "system_gui" },
|
||||
{ "code": -215, "label": "toggle_one_handed_mode_left", "type": "system_gui" },
|
||||
{ "code": -100, "label": "settings", "type": "system_gui" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "bg",
|
||||
"authors": [ "iorvethe" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"и": {
|
||||
"relevant": [
|
||||
{ "code": 1117, "label": "ѝ" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".bg" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
167
app/src/main/assets/ime/text/characters/extended_popups/ckb.json
Normal file
167
app/src/main/assets/ime/text/characters/extended_popups/ckb.json
Normal file
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "ckb",
|
||||
"authors": [ "GoRaN" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"ق": {
|
||||
"relevant": [
|
||||
{ "code": 1647, "label": "ٯ" }
|
||||
]
|
||||
},
|
||||
"ئ": {
|
||||
"relevant": [
|
||||
{"code": 1569, "label": "ء" }
|
||||
]
|
||||
},
|
||||
"ە": {
|
||||
"relevant": [
|
||||
{ "code": 1577, "label": "ة" },
|
||||
{ "code": 1729, "label": "ـہ" }
|
||||
]
|
||||
},
|
||||
"ر": {
|
||||
"relevant": [
|
||||
{ "code": 1685, "label": "ڕ" },
|
||||
{ "code": 1682, "label": "ڒ" }
|
||||
]
|
||||
},
|
||||
"ف": {
|
||||
"relevant": [
|
||||
{ "code": 1701, "label": "ڥ" },
|
||||
{ "code": 1698, "label": "ڢ" },
|
||||
{ "code": 1700, "label": "ڤ" },
|
||||
{ "code": 1697, "label": "ڡ" }
|
||||
]
|
||||
},
|
||||
"": {
|
||||
"relevant": [
|
||||
{ "code": 65163, "label": "ﺋ" },
|
||||
{ "code": 1569, "label": "ء" },
|
||||
{ "code": 65139, "label": "ﹳ" }
|
||||
]
|
||||
},
|
||||
"ع": {
|
||||
"relevant": [
|
||||
{ "code": 1551, "label": "؏" },
|
||||
{ "code": 1594, "label": "غ" }
|
||||
]
|
||||
},
|
||||
"د": {
|
||||
"relevant": [
|
||||
{ "code": 1676, "label": "ڌ" },
|
||||
{ "code": 64390, "label": "ﮆ" },
|
||||
{ "code": 1584, "label": "ذ" },
|
||||
{ "code": 1774, "label": "ۮ" }
|
||||
]
|
||||
},
|
||||
"ه": {
|
||||
"relevant": [
|
||||
{ "code": 1726, "label": "ھ" }
|
||||
]
|
||||
},
|
||||
"خ": {
|
||||
"relevant": [
|
||||
{ "code": 1567, "label": "؟" }
|
||||
]
|
||||
},
|
||||
"س": {
|
||||
"relevant": [
|
||||
{ "code": 1589, "label": "ص" }
|
||||
]
|
||||
},
|
||||
"ش": {
|
||||
"relevant": [
|
||||
{ "code": 1590, "label": "ض" }
|
||||
]
|
||||
},
|
||||
"ب": {
|
||||
"relevant": [
|
||||
{ "code": 65010, "label": "ﷲ" },
|
||||
{ "code": 65021, "label": "﷽" },
|
||||
{ "code": 65019, "label": "ﷻ" }
|
||||
]
|
||||
},
|
||||
"م": {
|
||||
"relevant": [
|
||||
{ "code": 65018, "label": "ﷺ" },
|
||||
{ "code": 65012, "label": "ﷴ" }
|
||||
]
|
||||
},
|
||||
"ل": {
|
||||
"relevant": [
|
||||
{ "code": 1718, "label": "ڶ" },
|
||||
{ "code": 1719, "label": "ڷ" },
|
||||
{ "code": 1717, "label": "ڵ" },
|
||||
{ "code": 1720, "label": "ڸ" }
|
||||
]
|
||||
},
|
||||
"ا": {
|
||||
"relevant": [
|
||||
{ "code": 1571, "label": "أ" },
|
||||
{ "code": 1573, "label": "إ" },
|
||||
{ "code": 1570, "label": "آ" },
|
||||
{ "code": 1649, "label": "ٱ" }
|
||||
]
|
||||
},
|
||||
"ک": {
|
||||
"relevant": [
|
||||
{ "code": 1706, "label": "ڪ" },
|
||||
{ "code": 1603, "label": "ك"}
|
||||
]
|
||||
},
|
||||
"ی": {
|
||||
"relevant": [
|
||||
{ "code": 1746, "label": "ے" },
|
||||
{ "code": 1610, "label": "ي" },
|
||||
{ "code": 1744, "label": "ې" },
|
||||
{ "code": 1741, "label": "ۍ" },
|
||||
{ "code": 1742, "label": "ێ" },
|
||||
{ "code": 1597, "label": "ؽ" }
|
||||
]
|
||||
},
|
||||
"ۆ": {
|
||||
"relevant": [
|
||||
{ "code": 1743, "label": "ۏ" },
|
||||
{ "code": 1735, "label": "ۇ" },
|
||||
{ "code": 1737, "label": "ۉ" },
|
||||
{ "code": 1738, "label": "ۊ" },
|
||||
{ "code": 1572, "label": "ؤ" },
|
||||
{ "code": 1739, "label": "ۋ" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 1567, "label": "؟" },
|
||||
"relevant": [
|
||||
{ "code": 1600, "label": "ــ" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 1548, "label": "،" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 1549, "label": "؍" },
|
||||
{ "code": 1563, "label": "؛" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 42, "label": "*" },
|
||||
{ "code": 95, "label": "_" },
|
||||
{ "code": 45, "label": "-" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".krd"},
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" },
|
||||
{ "code": -255, "label": ".iq" },
|
||||
{ "code": -255, "label": ".tv" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,23 +26,23 @@
|
||||
]
|
||||
},
|
||||
"e": {
|
||||
"main": { "code": 233, "label": "é" },
|
||||
"relevant": [
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 234, "label": "ê" }
|
||||
]
|
||||
},
|
||||
"i": {
|
||||
"main": { "code": 237, "label": "í" },
|
||||
"relevant": [
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 239, "label": "ï" }
|
||||
]
|
||||
},
|
||||
@@ -71,8 +71,8 @@
|
||||
]
|
||||
},
|
||||
"u": {
|
||||
"main": { "code": 250, "label": "ú" },
|
||||
"relevant": [
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
@@ -104,9 +104,9 @@
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com.es" },
|
||||
{ "code": -255, "label": ".es" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"mapping": {
|
||||
"all": {
|
||||
"a": {
|
||||
"main": { "code": 224, "label": "à" },
|
||||
"relevant": [
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
@@ -18,8 +18,8 @@
|
||||
]
|
||||
},
|
||||
"e": {
|
||||
"main": { "code": 232, "label": "è" },
|
||||
"relevant": [
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
@@ -29,8 +29,8 @@
|
||||
]
|
||||
},
|
||||
"i": {
|
||||
"main": { "code": 236, "label": "ì" },
|
||||
"relevant": [
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 239, "label": "ï" },
|
||||
{ "code": 303, "label": "į" },
|
||||
@@ -45,8 +45,8 @@
|
||||
]
|
||||
},
|
||||
"o": {
|
||||
"main": { "code": 242, "label": "ò" },
|
||||
"relevant": [
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 186, "label": "º" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
@@ -58,8 +58,8 @@
|
||||
]
|
||||
},
|
||||
"u": {
|
||||
"main": { "code": 249, "label": "ù" },
|
||||
"relevant": [
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
@@ -91,10 +91,10 @@
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".it" },
|
||||
{ "code": -255, "label": ".gov.it" },
|
||||
{ "code": -255, "label": ".edu.it" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".it" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "iw",
|
||||
"authors": [ "Antony" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" },
|
||||
{ "code": -255, "label": ".co.il" },
|
||||
{ "code": -255, "label": ".gov.il" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
112
app/src/main/assets/ime/text/characters/extended_popups/lv.json
Normal file
112
app/src/main/assets/ime/text/characters/extended_popups/lv.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "lv",
|
||||
"authors": [ "patrickgold", "eandersons" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"a": {
|
||||
"relevant": [
|
||||
{ "code": 257, "label": "ā" }
|
||||
]
|
||||
},
|
||||
"c": {
|
||||
"relevant": [
|
||||
{ "code": 269, "label": "č" }
|
||||
]
|
||||
},
|
||||
"e": {
|
||||
"relevant": [
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 8364, "label": "€" }
|
||||
]
|
||||
},
|
||||
"g": {
|
||||
"relevant": [
|
||||
{ "code": 291, "label": "ģ" }
|
||||
]
|
||||
},
|
||||
"i": {
|
||||
"relevant": [
|
||||
{ "code": 299, "label": "ī" }
|
||||
]
|
||||
},
|
||||
"k": {
|
||||
"relevant": [
|
||||
{ "code": 311, "label": "ķ" }
|
||||
]
|
||||
},
|
||||
"l": {
|
||||
"relevant": [
|
||||
{ "code": 316, "label": "ļ" }
|
||||
]
|
||||
},
|
||||
"n": {
|
||||
"relevant": [
|
||||
{ "code": 326, "label": "ņ" }
|
||||
]
|
||||
},
|
||||
"o": {
|
||||
"relevant": [
|
||||
{ "code": 333, "label": "ō" }
|
||||
]
|
||||
},
|
||||
"r": {
|
||||
"relevant": [
|
||||
{ "code": 343, "label": "ŗ" }
|
||||
]
|
||||
},
|
||||
"s": {
|
||||
"relevant": [
|
||||
{ "code": 353, "label": "š" }
|
||||
]
|
||||
},
|
||||
"u": {
|
||||
"relevant": [
|
||||
{ "code": 363, "label": "ū" }
|
||||
]
|
||||
},
|
||||
"z": {
|
||||
"relevant": [
|
||||
{ "code": 382, "label": "ž" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 8212, "label": "—" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".lv" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".eu" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov.lv" },
|
||||
{ "code": -255, "label": ".edu.lv" },
|
||||
{ "code": -255, "label": ".org.lv" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "pt-BR",
|
||||
"authors": [ "rickym7" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"a": {
|
||||
"relevant": [
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 170, "label": "ª" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 226, "label": "â" }
|
||||
]
|
||||
},
|
||||
"c": {
|
||||
"relevant": [
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 231, "label": "ç" },
|
||||
{ "code": 263, "label": "ć" }
|
||||
]
|
||||
},
|
||||
"e": {
|
||||
"relevant": [
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" }
|
||||
]
|
||||
},
|
||||
"i": {
|
||||
"relevant": [
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 239, "label": "ï" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 237, "label": "í" }
|
||||
]
|
||||
},
|
||||
"n": {
|
||||
"relevant": [
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
]
|
||||
},
|
||||
"o": {
|
||||
"relevant": [
|
||||
{ "code": 186, "label": "º" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 243, "label": "ó" }
|
||||
]
|
||||
},
|
||||
"u": {
|
||||
"relevant": [
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 250, "label": "ú" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".br" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "sr",
|
||||
"authors": [ "hedidnothingwrong", "GrbavaCigla" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"c": {
|
||||
"relevant": [
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 263, "label": "ć" }
|
||||
]
|
||||
},
|
||||
"d": {
|
||||
"relevant": [
|
||||
{ "code": 273, "label": "đ" }
|
||||
]
|
||||
},
|
||||
"s": {
|
||||
"relevant": [
|
||||
{ "code": 353, "label": "š" }
|
||||
]
|
||||
},
|
||||
"z": {
|
||||
"relevant": [
|
||||
{ "code": 382, "label": "ž" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".eu" },
|
||||
{ "code": -255, "label": ".rs" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "tr",
|
||||
"authors": [ "kisekinopureya" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"a": {
|
||||
"relevant": [
|
||||
{ "code": 226, "label": "â" }
|
||||
]
|
||||
},
|
||||
"c": {
|
||||
"relevant": [
|
||||
{ "code": 231, "label": "ç" }
|
||||
]
|
||||
},
|
||||
"g": {
|
||||
"relevant": [
|
||||
{ "code": 287, "label": "ğ" }
|
||||
]
|
||||
},
|
||||
"i": {
|
||||
"relevant": [
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 305, "label": "ı" }
|
||||
]
|
||||
},
|
||||
"o": {
|
||||
"relevant": [
|
||||
{ "code": 246, "label": "ö" }
|
||||
]
|
||||
},
|
||||
"s": {
|
||||
"relevant": [
|
||||
{ "code": 351, "label": "ş" }
|
||||
]
|
||||
},
|
||||
"u": {
|
||||
"relevant": [
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 251, "label": "û" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".tr" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
app/src/main/assets/ime/text/characters/hebrew.json
Normal file
53
app/src/main/assets/ime/text/characters/hebrew.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "hebrew",
|
||||
"authors": [ "Antony" ],
|
||||
"direction": "rtl",
|
||||
"modifier": "hebrew",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 39, "label": "'", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 96, "label": "`" }
|
||||
]
|
||||
} },
|
||||
{ "code": 45, "label": "-", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 95, "label": "_" }
|
||||
]
|
||||
} },
|
||||
{ "code": 1511, "label": "ק" },
|
||||
{ "code": 1512, "label": "ר" },
|
||||
{ "code": 1488, "label": "א" },
|
||||
{ "code": 1496, "label": "ט" },
|
||||
{ "code": 1493, "label": "ו" },
|
||||
{ "code": 1503, "label": "ן" },
|
||||
{ "code": 1501, "label": "ם" },
|
||||
{ "code": 1508, "label": "פ" }
|
||||
],
|
||||
[
|
||||
{ "code": 1513, "label": "ף" },
|
||||
{ "code": 1491, "label": "ך" },
|
||||
{ "code": 1490, "label": "ל" },
|
||||
{ "code": 1499, "label": "ח" },
|
||||
{ "code": 1506, "label": "י" },
|
||||
{ "code": 1497, "label": "ע" },
|
||||
{ "code": 1495, "label": "כ" },
|
||||
{ "code": 1500, "label": "ג" },
|
||||
{ "code": 1498, "label": "ד" },
|
||||
{ "code": 1507, "label": "ש" }
|
||||
],
|
||||
[
|
||||
{ "code": 1494, "label": "ץ" },
|
||||
{ "code": 1505, "label": "ת" },
|
||||
{ "code": 1489, "label": "צ" },
|
||||
{ "code": 1492, "label": "מ" },
|
||||
{ "code": 1504, "label": "נ" },
|
||||
{ "code": 1502, "label": "ה" },
|
||||
{ "code": 1510, "label": "ב" },
|
||||
{ "code": 1514, "label": "ס" },
|
||||
{ "code": 1509, "label": "ז" }
|
||||
]
|
||||
]
|
||||
}
|
||||
57
app/src/main/assets/ime/text/characters/kurdish.json
Normal file
57
app/src/main/assets/ime/text/characters/kurdish.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "kurdish",
|
||||
"authors": [ "GoRaN" ],
|
||||
"direction": "rtl",
|
||||
"modifier": "kurdish",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 1602, "label": "ق", "popup": {
|
||||
"main": { "code": 1647, "label": "ٯ" }
|
||||
} },
|
||||
{ "code": 1608, "label": "و", "popup": {
|
||||
"main": { "code": -255, "label": "وو" }
|
||||
} },
|
||||
{ "code": 1749, "label": "ﻪ", "popup": {
|
||||
"main": { "code": 1577, "label": "ة" }
|
||||
} },
|
||||
{ "code": 1585, "label": "ر" },
|
||||
{ "code": 1578, "label": "ت", "popup": {
|
||||
"main": { "code": 1591, "label": "ط" }
|
||||
} },
|
||||
{ "code": 1740, "label": "ی" },
|
||||
{ "code": 1574, "label": "ﺋ", "popup": {
|
||||
"main": { "code": 1569, "label": "ء" }
|
||||
} },
|
||||
{ "code": 1593, "label": "ع" },
|
||||
{ "code": 1734, "label": "ۆ" },
|
||||
{ "code": 1662, "label": "پ", "popup": {
|
||||
"main": { "code": 1579, "label": "ث" }
|
||||
} }
|
||||
],
|
||||
[
|
||||
{ "code": 1575, "label": "ا" },
|
||||
{ "code": 1587, "label": "س" },
|
||||
{ "code": 1588, "label": "ش" },
|
||||
{ "code": 1583, "label": "د" },
|
||||
{ "code": 1601, "label": "ف" },
|
||||
{ "code": 1607, "label": "ھ" },
|
||||
{ "code": 1688, "label": "ژ" },
|
||||
{ "code": 1604, "label": "ل" },
|
||||
{ "code": 1705, "label": "ک" },
|
||||
{ "code": 1711, "label": "گ" }
|
||||
],
|
||||
[
|
||||
{ "code": 1586, "label": "ز", "popup": {
|
||||
"main": {"code": 1592, "label": "ظ" }
|
||||
} },
|
||||
{ "code": 1582, "label": "خ" },
|
||||
{ "code": 1580, "label": "ج" },
|
||||
{ "code": 1670, "label": "چ" },
|
||||
{ "code": 1581, "label": "ح" },
|
||||
{ "code": 1576, "label": "ب" },
|
||||
{ "code": 1606, "label": "ن" },
|
||||
{ "code": 1605, "label": "م" }
|
||||
]
|
||||
]
|
||||
}
|
||||
27
app/src/main/assets/ime/text/characters/mod/hebrew.json
Normal file
27
app/src/main/assets/ime/text/characters/mod/hebrew.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"type": "characters/mod",
|
||||
"name": "hebrew",
|
||||
"authors": [ "Antony" ],
|
||||
"direction": "rtl",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 0, "type": "placeholder" },
|
||||
{ "code": -5, "label": "delete", "type": "enter_editing" }
|
||||
],
|
||||
[
|
||||
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
|
||||
{ "code": 64, "label": "@", "groupId": 1, "variation": "email_address" },
|
||||
{ "code": 44, "label": ",", "groupId": 1, "variation": "normal" },
|
||||
{ "code": 44, "label": ",", "groupId": 1, "variation": "password" },
|
||||
{ "code": 47, "label": "/", "groupId": 1, "variation": "uri" },
|
||||
{ "code": -210, "label": "language_switch", "type": "system_gui" },
|
||||
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
|
||||
{ "code": 32, "label": "space" },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "email_address" },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "normal" },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "password" },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "uri" },
|
||||
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
|
||||
]
|
||||
]
|
||||
}
|
||||
27
app/src/main/assets/ime/text/characters/mod/kurdish.json
Normal file
27
app/src/main/assets/ime/text/characters/mod/kurdish.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"type": "characters/mod",
|
||||
"name": "kurdish",
|
||||
"authors": [ "GoRaN" ],
|
||||
"direction": "rtl",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 0, "type": "placeholder" },
|
||||
{ "code": -5, "label": "delete", "type": "enter_editing" }
|
||||
],
|
||||
[
|
||||
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
|
||||
{ "code": 64, "label": "@", "groupId": 1, "variation": "email_address" },
|
||||
{ "code": 1567, "label": "؟", "groupId": 1, "variation": "normal" },
|
||||
{ "code": 1548, "label": "،", "groupId": 1, "variation": "password" },
|
||||
{ "code": 47, "label": "/", "groupId": 1, "variation": "uri" },
|
||||
{ "code": -210, "label": "language_switch", "type": "system_gui" },
|
||||
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
|
||||
{ "code": 32, "label": " " },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "email_address" },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "normal" },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "password" },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "uri" },
|
||||
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "serbian_cyrillic",
|
||||
"authors": ["GrbavaCigla"],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 1113, "label": "љ" },
|
||||
{ "code": 1114, "label": "њ" },
|
||||
{ "code": 1077, "label": "е" },
|
||||
{ "code": 1088, "label": "р" },
|
||||
{ "code": 1090, "label": "т" },
|
||||
{ "code": 1079, "label": "з" },
|
||||
{ "code": 1091, "label": "у" },
|
||||
{ "code": 1080, "label": "и" },
|
||||
{ "code": 1086, "label": "о" },
|
||||
{ "code": 1087, "label": "п" },
|
||||
{ "code": 1096, "label": "ш" }
|
||||
],
|
||||
[
|
||||
{ "code": 1072, "label": "а" },
|
||||
{ "code": 1089, "label": "с" },
|
||||
{ "code": 1076, "label": "д" },
|
||||
{ "code": 1092, "label": "ф" },
|
||||
{ "code": 1075, "label": "г" },
|
||||
{ "code": 1093, "label": "х" },
|
||||
{ "code": 1112, "label": "ј" },
|
||||
{ "code": 1082, "label": "к" },
|
||||
{ "code": 1083, "label": "л" },
|
||||
{ "code": 1095, "label": "ч" },
|
||||
{ "code": 1115, "label": "ћ" }
|
||||
],
|
||||
[
|
||||
{ "code": 1119, "label": "џ" },
|
||||
{ "code": 1094, "label": "ц" },
|
||||
{ "code": 1074, "label": "в" },
|
||||
{ "code": 1073, "label": "б" },
|
||||
{ "code": 1085, "label": "н" },
|
||||
{ "code": 1084, "label": "м" },
|
||||
{ "code": 1106, "label": "ђ" },
|
||||
{ "code": 1078, "label": "ж" }
|
||||
]
|
||||
]
|
||||
}
|
||||
45
app/src/main/assets/ime/text/characters/serbian_latin.json
Normal file
45
app/src/main/assets/ime/text/characters/serbian_latin.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "serbian_latin",
|
||||
"authors": ["GrbavaCigla"],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 113, "label": "q" },
|
||||
{ "code": 119, "label": "w" },
|
||||
{ "code": 101, "label": "e" },
|
||||
{ "code": 114, "label": "r" },
|
||||
{ "code": 116, "label": "t" },
|
||||
{ "code": 122, "label": "z" },
|
||||
{ "code": 117, "label": "u" },
|
||||
{ "code": 105, "label": "i" },
|
||||
{ "code": 111, "label": "o" },
|
||||
{ "code": 112, "label": "p" },
|
||||
{ "code": 353, "label": "š" }
|
||||
],
|
||||
[
|
||||
{ "code": 97, "label": "a" },
|
||||
{ "code": 115, "label": "s" },
|
||||
{ "code": 100, "label": "d" },
|
||||
{ "code": 102, "label": "f" },
|
||||
{ "code": 103, "label": "g" },
|
||||
{ "code": 104, "label": "h" },
|
||||
{ "code": 106, "label": "j" },
|
||||
{ "code": 107, "label": "k" },
|
||||
{ "code": 108, "label": "l" },
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 263, "label": "ć" }
|
||||
],
|
||||
[
|
||||
{ "code": 121, "label": "y" },
|
||||
{ "code": 120, "label": "x" },
|
||||
{ "code": 99, "label": "c" },
|
||||
{ "code": 118, "label": "v" },
|
||||
{ "code": 98, "label": "b" },
|
||||
{ "code": 110, "label": "n" },
|
||||
{ "code": 109, "label": "m" },
|
||||
{ "code": 273, "label": "đ" },
|
||||
{ "code": 382, "label": "ž" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -48,7 +48,7 @@
|
||||
{ "code": 48, "label": "0", "type": "numeric" },
|
||||
{ "code": 61, "label": "=" },
|
||||
{ "code": 46, "label": "." },
|
||||
{ "code": 10, "label": "enter", "type": "enter_editing" }
|
||||
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,10 +11,24 @@
|
||||
],
|
||||
[
|
||||
{ "code": -201, "label": "view_characters", "type": "system_gui" },
|
||||
{ "code": 60, "label": "<" },
|
||||
{ "code": 60, "label": "<", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 171, "label": "«" },
|
||||
{ "code": 8804, "label": "≤" },
|
||||
{ "code": 8249, "label": "‹" },
|
||||
{ "code":10216, "label": "⟨" }
|
||||
]
|
||||
} },
|
||||
{ "code": -205, "label": "view_numeric_advanced", "type": "system_gui" },
|
||||
{ "code": 32, "label": "space" },
|
||||
{ "code": 62, "label": ">" },
|
||||
{ "code": 62, "label": ">", "popup": {
|
||||
"relevant": [
|
||||
{ "code":10217, "label": "⟩" },
|
||||
{ "code": 8250, "label": "›" },
|
||||
{ "code": 8805, "label": "≥" },
|
||||
{ "code": 187, "label": "»" }
|
||||
]
|
||||
} },
|
||||
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
|
||||
]
|
||||
]
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
{ "code": 960, "label": "π", "popup": {
|
||||
"main": { "code": 928, "label": "Π" },
|
||||
"relevant": [
|
||||
{ "code": 969, "label": "ω" },
|
||||
{ "code": 945, "label": "α" },
|
||||
{ "code": 946, "label": "β" },
|
||||
{ "code": 937, "label": "Ω" },
|
||||
{ "code": 956, "label": "μ" }
|
||||
]
|
||||
|
||||
73
app/src/main/assets/ime/theme/floris_black.json
Normal file
73
app/src/main/assets/ime/theme/floris_black.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
|
||||
"name": "floris_black",
|
||||
"label": "Floris Black",
|
||||
"authors": [ "serebit" ],
|
||||
"isNightTheme": true,
|
||||
"attributes": {
|
||||
"window": {
|
||||
"colorPrimary": "#388E3C",
|
||||
"colorPrimaryDark": "#306D32",
|
||||
"colorAccent": "#FF9800",
|
||||
"navigationBarColor": "@keyboard/background",
|
||||
"navigationBarLight": "false",
|
||||
"semiTransparentColor": "#20FFFFFF",
|
||||
"textColor": "#EEEEEE"
|
||||
},
|
||||
"keyboard": {
|
||||
"background": "#000000"
|
||||
},
|
||||
"key": {
|
||||
"background": "#212121",
|
||||
"backgroundPressed": "#3D3D3D",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundPressed": "@window/textColor",
|
||||
"showBorder": "true"
|
||||
},
|
||||
"key:enter": {
|
||||
"background": "@window/colorPrimary",
|
||||
"backgroundPressed": "@window/colorPrimaryDark",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundPressed": "@window/textColor"
|
||||
},
|
||||
"key:shift:capslock": {
|
||||
"foreground": "@window/colorAccent",
|
||||
"foregroundPressed": "@window/colorAccent"
|
||||
},
|
||||
"media": {
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#BDBDBD"
|
||||
},
|
||||
"oneHanded": {
|
||||
"background": "#000000",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"popup": {
|
||||
"background": "#424242",
|
||||
"backgroundActive": "#707070",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"privateMode": {
|
||||
"background": "#7800BF",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"smartbar": {
|
||||
"background": "transparent",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"smartbarButton": {
|
||||
"background": "@key/background",
|
||||
"foreground": "@key/foreground"
|
||||
},
|
||||
"extractEditLayout": {
|
||||
"background": "#282828",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"extractActionButton": {
|
||||
"background": "@smartbarButton/background",
|
||||
"foreground": "@smartbarButton/foreground"
|
||||
}
|
||||
}
|
||||
}
|
||||
76
app/src/main/assets/ime/theme/floris_black_borderless.json
Normal file
76
app/src/main/assets/ime/theme/floris_black_borderless.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
|
||||
"name": "floris_black_borderless",
|
||||
"label": "Floris Black Borderless",
|
||||
"authors": [ "serebit" ],
|
||||
"isNightTheme": true,
|
||||
"attributes": {
|
||||
"window": {
|
||||
"colorPrimary": "#388E3C",
|
||||
"colorPrimaryDark": "#306D32",
|
||||
"colorAccent": "#FF9800",
|
||||
"navigationBarColor": "@keyboard/background",
|
||||
"navigationBarLight": "false",
|
||||
"semiTransparentColor": "#20FFFFFF",
|
||||
"textColor": "#EEEEEE"
|
||||
},
|
||||
"keyboard": {
|
||||
"background": "#000000"
|
||||
},
|
||||
"key": {
|
||||
"background": "transparent",
|
||||
"backgroundPressed": "#7F616161",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundPressed": "@window/textColor",
|
||||
"showBorder": "false"
|
||||
},
|
||||
"key:enter": {
|
||||
"background": "@window/colorPrimary",
|
||||
"backgroundPressed": "@window/colorPrimaryDark",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundPressed": "@window/textColor"
|
||||
},
|
||||
"key:shift:capslock": {
|
||||
"foreground": "@window/colorAccent",
|
||||
"foregroundPressed": "@window/colorAccent"
|
||||
},
|
||||
"key:space": {
|
||||
"background": "#46616161"
|
||||
},
|
||||
"media": {
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#BDBDBD"
|
||||
},
|
||||
"oneHanded": {
|
||||
"background": "#000000",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"popup": {
|
||||
"background": "#363636",
|
||||
"backgroundActive": "#5F5F5F",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"privateMode": {
|
||||
"background": "#7800BF",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"smartbar": {
|
||||
"background": "transparent",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"smartbarButton": {
|
||||
"background": "#212121",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"extractEditLayout": {
|
||||
"background": "#282828",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"extractActionButton": {
|
||||
"background": "@smartbarButton/background",
|
||||
"foreground": "@smartbarButton/foreground"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,15 @@
|
||||
"smartbarButton": {
|
||||
"background": "@key/background",
|
||||
"foreground": "@key/foreground"
|
||||
},
|
||||
"extractEditLayout": {
|
||||
"background": "#E8E8E8",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#8A8A8A"
|
||||
},
|
||||
"extractActionButton": {
|
||||
"background": "@smartbarButton/background",
|
||||
"foreground": "@smartbarButton/foreground"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,15 @@
|
||||
"smartbarButton": {
|
||||
"background": "#FFFFFF",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"extractEditLayout": {
|
||||
"background": "#E8E8E8",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#8A8A8A"
|
||||
},
|
||||
"extractActionButton": {
|
||||
"background": "@smartbarButton/background",
|
||||
"foreground": "@smartbarButton/foreground"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,15 @@
|
||||
"smartbarButton": {
|
||||
"background": "@key/background",
|
||||
"foreground": "@key/foreground"
|
||||
},
|
||||
"extractEditLayout": {
|
||||
"background": "#282828",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"extractActionButton": {
|
||||
"background": "@smartbarButton/background",
|
||||
"foreground": "@smartbarButton/foreground"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,15 @@
|
||||
"smartbarButton": {
|
||||
"background": "#424242",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"extractEditLayout": {
|
||||
"background": "#282828",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"extractActionButton": {
|
||||
"background": "@smartbarButton/background",
|
||||
"foreground": "@smartbarButton/foreground"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
64
app/src/main/assets/ime/theme/gboard_day.json
Normal file
64
app/src/main/assets/ime/theme/gboard_day.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
|
||||
"name": "gboard_day",
|
||||
"label": "Gboard Day",
|
||||
"authors": [ "patrickgold", "itskareem" ],
|
||||
"isNightTheme": false,
|
||||
"attributes": {
|
||||
"window": {
|
||||
"colorPrimary": "#0479ed",
|
||||
"colorPrimaryDark": "#0467c9",
|
||||
"colorAccent": "#FF9800",
|
||||
"navigationBarColor": "@keyboard/background",
|
||||
"navigationBarLight": "true",
|
||||
"semiTransparentColor": "#20000000",
|
||||
"textColor": "#000000"
|
||||
},
|
||||
"keyboard": {
|
||||
"background": "#D1D6DC"
|
||||
},
|
||||
"key": {
|
||||
"background": "#FCFFFF",
|
||||
"backgroundPressed": "#F5F5F5",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundPressed": "@window/textColor",
|
||||
"showBorder": "true"
|
||||
},
|
||||
"key:enter": {
|
||||
"background": "@window/colorPrimary",
|
||||
"backgroundPressed": "@window/colorPrimaryDark",
|
||||
"foreground": "#FFFFFF",
|
||||
"foregroundPressed": "#FFFFFF"
|
||||
},
|
||||
"key:shift:capslock": {
|
||||
"foreground": "@window/colorAccent",
|
||||
"foregroundPressed": "@window/colorAccent"
|
||||
},
|
||||
"media": {
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#757575"
|
||||
},
|
||||
"oneHanded": {
|
||||
"background": "@keyboard/background",
|
||||
"foreground": "#424242"
|
||||
},
|
||||
"popup": {
|
||||
"background": "#EEEEEE",
|
||||
"backgroundActive": "#BDBDBD",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"privateMode": {
|
||||
"background": "#A000FF",
|
||||
"foreground": "#FFFFFF"
|
||||
},
|
||||
"smartbar": {
|
||||
"background": "@keyboard/background",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#8A8A8A"
|
||||
},
|
||||
"smartbarButton": {
|
||||
"background": "@key/background",
|
||||
"foreground": "@key/foreground"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -800,6 +800,384 @@ shall not be used in advertising or otherwise to promote the sale,
|
||||
use or other dealings in these Data Files or Software without prior
|
||||
written authorization of the copyright holder.
|
||||
</pre>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Dictionary Source 1: wordfreq data</h3>
|
||||
<span>Copyright (c) 2015 Luminoso Technologies, Inc.</span>
|
||||
<pre>
|
||||
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||
License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||
License"). To the extent this Public License may be interpreted as a
|
||||
contract, You are granted the Licensed Rights in consideration of Your
|
||||
acceptance of these terms and conditions, and the Licensor grants You
|
||||
such rights in consideration of benefits the Licensor receives from
|
||||
making the Licensed Material available under these terms and
|
||||
conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-SA Compatible License means a license listed at
|
||||
creativecommons.org/compatiblelicenses, approved by Creative
|
||||
Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name
|
||||
of a Creative Commons Public License. The License Elements of this
|
||||
Public License are Attribution and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
k. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
l. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
m. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. Additional offer from the Licensor -- Adapted Material.
|
||||
Every recipient of Adapted Material from You
|
||||
automatically receives an offer from the Licensor to
|
||||
exercise the Licensed Rights in the Adapted Material
|
||||
under the conditions of the Adapter's License You apply.
|
||||
|
||||
c. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share
|
||||
Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons
|
||||
license with the same License Elements, this version or
|
||||
later, or a BY-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the
|
||||
Adapter's License You apply. You may satisfy this condition
|
||||
in any reasonable manner based on the medium, means, and
|
||||
context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms
|
||||
or conditions on, or apply any Effective Technological
|
||||
Measures to, Adapted Material that restrict exercise of the
|
||||
rights granted under the Adapter's License You apply.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material,
|
||||
|
||||
including for purposes of Section 3(b); and
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
</pre>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
BIN
app/src/main/ic_app_icon_beta-playstore.png
Normal file
BIN
app/src/main/ic_app_icon_beta-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
app/src/main/ic_app_icon_debug-playstore.png
Normal file
BIN
app/src/main/ic_app_icon_debug-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
app/src/main/ic_app_icon_release-playstore.png
Normal file
BIN
app/src/main/ic_app_icon_release-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dev.patrickgold.florisboard.R
|
||||
|
||||
abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity() {
|
||||
private var _binding: V? = null
|
||||
protected val binding: V
|
||||
get() = _binding!!
|
||||
|
||||
private var _prefs: PrefHelper? = null
|
||||
protected val prefs: PrefHelper
|
||||
get() = _prefs!!
|
||||
|
||||
private var errorDialog: AlertDialog? = null
|
||||
private var errorSnackbar: Snackbar? = null
|
||||
private var errorThrowable: Throwable? = null
|
||||
private var messageSnackbar: Snackbar? = null
|
||||
|
||||
protected abstract fun onCreateBinding(): V
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
_prefs = PrefHelper.getDefaultInstance(applicationContext)
|
||||
onCreateBinding().let {
|
||||
_binding = it
|
||||
setContentView(it.root)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
_prefs = null
|
||||
errorDialog?.dismiss()
|
||||
errorDialog = null
|
||||
errorSnackbar?.dismiss()
|
||||
errorSnackbar = null
|
||||
errorThrowable = null
|
||||
messageSnackbar?.dismiss()
|
||||
messageSnackbar = null
|
||||
}
|
||||
|
||||
protected fun showMessage(@StringRes snackbarMessageResId: Int) {
|
||||
val snackbarMessage = resources.getString(snackbarMessageResId)
|
||||
showMessage(snackbarMessage)
|
||||
}
|
||||
|
||||
protected fun showMessage(snackbarMessage: String) {
|
||||
messageSnackbar?.dismiss()
|
||||
messageSnackbar = Snackbar.make(binding.root, snackbarMessage, Snackbar.LENGTH_LONG).apply {
|
||||
setAction(android.R.string.ok) {
|
||||
messageSnackbar?.dismiss()
|
||||
}
|
||||
show() }
|
||||
}
|
||||
|
||||
protected fun showError(throwable: Throwable) {
|
||||
val snackbarMessage = resources.getString(R.string.assets__error__snackbar_message)
|
||||
showError(snackbarMessage, throwable)
|
||||
}
|
||||
|
||||
protected fun showError(@StringRes snackbarMessageResId: Int, throwable: Throwable) {
|
||||
val snackbarMessage = resources.getString(snackbarMessageResId)
|
||||
showError(snackbarMessage, throwable)
|
||||
}
|
||||
|
||||
protected fun showError(snackbarMessage: String, throwable: Throwable) {
|
||||
errorDialog?.dismiss()
|
||||
errorDialog = null
|
||||
errorSnackbar?.dismiss()
|
||||
errorSnackbar = Snackbar.make(binding.root, snackbarMessage, Snackbar.LENGTH_LONG).apply {
|
||||
setAction(R.string.assets__error__details) {
|
||||
errorDialog?.dismiss()
|
||||
errorDialog = AlertDialog.Builder(this@FlorisActivity).run {
|
||||
setTitle(R.string.assets__error__details)
|
||||
setMessage(errorThrowable.toString())
|
||||
setPositiveButton(android.R.string.ok, null)
|
||||
setNeutralButton(R.string.crash_dialog__copy_to_clipboard) { _, _ ->
|
||||
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE)
|
||||
if (clipboardManager != null && clipboardManager is ClipboardManager) {
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(errorThrowable.toString(), errorThrowable.toString()))
|
||||
}
|
||||
}
|
||||
create()
|
||||
show()
|
||||
}
|
||||
}
|
||||
show()
|
||||
}
|
||||
errorThrowable = throwable
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package dev.patrickgold.florisboard.ime.core
|
||||
import android.app.Application
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.crashutility.CrashUtility
|
||||
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetManager
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import timber.log.Timber
|
||||
@@ -32,6 +33,7 @@ class FlorisApplication : Application() {
|
||||
CrashUtility.install(this)
|
||||
val prefHelper = PrefHelper.getDefaultInstance(this)
|
||||
val assetManager = AssetManager.init(this)
|
||||
DictionaryManager.init(this)
|
||||
ThemeManager.init(this, assetManager, prefHelper)
|
||||
prefHelper.initDefaultPreferences()
|
||||
}
|
||||
|
||||
@@ -20,23 +20,31 @@ import android.annotation.SuppressLint
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.inputmethodservice.ExtractEditText
|
||||
import android.inputmethodservice.InputMethodService
|
||||
import android.media.AudioManager
|
||||
import android.os.*
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.provider.Settings
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.*
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputConnection
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.lifecycle.*
|
||||
import com.squareup.moshi.Json
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
|
||||
import dev.patrickgold.florisboard.ime.media.MediaInputManager
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupLayerView
|
||||
import dev.patrickgold.florisboard.ime.text.TextInputManager
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
@@ -45,10 +53,11 @@ import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.settings.SettingsMainActivity
|
||||
import dev.patrickgold.florisboard.setup.SetupActivity
|
||||
import dev.patrickgold.florisboard.util.*
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* Variable which holds the current [FlorisBoard] instance. To get this instance from another
|
||||
@@ -60,27 +69,42 @@ private var florisboardInstance: FlorisBoard? = null
|
||||
* Core class responsible to link together both the text and media input managers as well as
|
||||
* managing the one-handed UI.
|
||||
*/
|
||||
class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedListener,
|
||||
class FlorisBoard : InputMethodService(), LifecycleOwner, ClipboardManager.OnPrimaryClipChangedListener,
|
||||
ThemeManager.OnThemeUpdatedListener {
|
||||
|
||||
lateinit var prefs: PrefHelper
|
||||
private set
|
||||
|
||||
val context: Context
|
||||
get() = inputWindowView?.context ?: this
|
||||
private val serviceLifecycleDispatcher: ServiceLifecycleDispatcher = ServiceLifecycleDispatcher(this)
|
||||
|
||||
private var extractEditLayout: WeakReference<ViewGroup?> = WeakReference(null)
|
||||
var inputView: InputView? = null
|
||||
private set
|
||||
private var inputWindowView: InputWindowView? = null
|
||||
var popupLayerView: PopupLayerView? = null
|
||||
private set
|
||||
private var inputWindowView: InputWindowView? = null
|
||||
private var eventListeners: MutableList<WeakReference<EventListener?>?> = mutableListOf()
|
||||
private var eventListeners: CopyOnWriteArrayList<EventListener> = CopyOnWriteArrayList()
|
||||
|
||||
private var audioManager: AudioManager? = null
|
||||
private var imeManager:InputMethodManager? = null
|
||||
var imeManager:InputMethodManager? = null
|
||||
var clipboardManager: ClipboardManager? = null
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
private var vibrator: Vibrator? = null
|
||||
private val osHandler = Handler()
|
||||
|
||||
private var internalBatchNestingLevel: Int = 0
|
||||
private val internalSelectionCache = object {
|
||||
var selectionCatchCount: Int = 0
|
||||
var oldSelStart: Int = -1
|
||||
var oldSelEnd: Int = -1
|
||||
var newSelStart: Int = -1
|
||||
var newSelEnd: Int = -1
|
||||
var candidatesStart: Int = -1
|
||||
var candidatesEnd: Int = -1
|
||||
}
|
||||
|
||||
var activeEditorInstance: EditorInstance = EditorInstance.default()
|
||||
|
||||
lateinit var subtypeManager: SubtypeManager
|
||||
@@ -88,6 +112,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
private var currentThemeIsNight: Boolean = false
|
||||
private var currentThemeResId: Int = 0
|
||||
private var isNumberRowVisible: Boolean = false
|
||||
private var isWindowShown: Boolean = false
|
||||
|
||||
val textInputManager: TextInputManager
|
||||
val mediaInputManager: MediaInputManager
|
||||
@@ -101,18 +126,22 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
|
||||
companion object {
|
||||
private const val IME_ID: String = "dev.patrickgold.florisboard/.ime.core.FlorisBoard"
|
||||
private const val IME_ID_BETA: String = "dev.patrickgold.florisboard.beta/dev.patrickgold.florisboard.ime.core.FlorisBoard"
|
||||
private const val IME_ID_DEBUG: String = "dev.patrickgold.florisboard.debug/dev.patrickgold.florisboard.ime.core.FlorisBoard"
|
||||
|
||||
fun checkIfImeIsEnabled(context: Context): Boolean {
|
||||
val activeImeIds = Settings.Secure.getString(
|
||||
context.contentResolver,
|
||||
Settings.Secure.ENABLED_INPUT_METHODS
|
||||
)
|
||||
) ?: "(none)"
|
||||
Timber.i("List of active IMEs: $activeImeIds")
|
||||
return when {
|
||||
BuildConfig.DEBUG -> {
|
||||
activeImeIds.split(":").contains(IME_ID_DEBUG)
|
||||
}
|
||||
context.packageName.endsWith(".beta") -> {
|
||||
activeImeIds.split(":").contains(IME_ID_BETA)
|
||||
}
|
||||
else -> {
|
||||
activeImeIds.split(":").contains(IME_ID)
|
||||
}
|
||||
@@ -123,12 +152,15 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
val selectedImeId = Settings.Secure.getString(
|
||||
context.contentResolver,
|
||||
Settings.Secure.DEFAULT_INPUT_METHOD
|
||||
)
|
||||
) ?: "(none)"
|
||||
Timber.i("Selected IME: $selectedImeId")
|
||||
return when {
|
||||
BuildConfig.DEBUG -> {
|
||||
selectedImeId == IME_ID_DEBUG
|
||||
}
|
||||
context.packageName.endsWith(".beta") -> {
|
||||
selectedImeId.split(":").contains(IME_ID_BETA)
|
||||
}
|
||||
else -> {
|
||||
selectedImeId == IME_ID
|
||||
}
|
||||
@@ -153,8 +185,12 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLifecycle(): Lifecycle {
|
||||
return serviceLifecycleDispatcher.lifecycle
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
/*if (BuildConfig.DEBUG) {
|
||||
StrictMode.setThreadPolicy(
|
||||
StrictMode.ThreadPolicy.Builder()
|
||||
.detectDiskReads()
|
||||
@@ -171,14 +207,16 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
.penaltyDeath()
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}*/
|
||||
Timber.i("onCreate()")
|
||||
|
||||
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
serviceLifecycleDispatcher.onServicePreSuperOnCreate()
|
||||
|
||||
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
audioManager = getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
||||
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||
clipboardManager?.addPrimaryClipChangedListener(this)
|
||||
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
||||
prefs = PrefHelper.getDefaultInstance(this)
|
||||
prefs.initDefaultPreferences()
|
||||
prefs.sync()
|
||||
@@ -194,7 +232,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
|
||||
|
||||
super.onCreate()
|
||||
eventListeners.toList().forEach { it?.get()?.onCreate() }
|
||||
eventListeners.toList().forEach { it?.onCreate() }
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
@@ -203,25 +241,41 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
|
||||
baseContext.setTheme(currentThemeResId)
|
||||
|
||||
inputWindowView = layoutInflater.inflate(R.layout.florisboard, null) as InputWindowView
|
||||
popupLayerView = inputWindowView?.findViewById(R.id.popup_layer)
|
||||
inputWindowView = layoutInflater.inflate(R.layout.florisboard, null) as? InputWindowView
|
||||
inputWindowView?.isHapticFeedbackEnabled = true
|
||||
|
||||
eventListeners.toList().forEach { it?.get()?.onCreateInputView() }
|
||||
eventListeners.toList().forEach { it?.onCreateInputView() }
|
||||
|
||||
return inputWindowView
|
||||
}
|
||||
|
||||
fun registerInputView(inputView: InputView) {
|
||||
Timber.i("registerInputView($inputView)")
|
||||
/**
|
||||
* Disable the default candidates view.
|
||||
*/
|
||||
override fun onCreateCandidatesView(): View? {
|
||||
return null
|
||||
}
|
||||
|
||||
this.inputView = inputView
|
||||
initializeOneHandedEnvironment()
|
||||
updateSoftInputWindowLayoutParameters()
|
||||
updateOneHandedPanelVisibility()
|
||||
themeManager.notifyCallbackReceivers()
|
||||
setActiveInput(R.id.text_input)
|
||||
|
||||
eventListeners.toList().forEach { it?.get()?.onRegisterInputView(inputView) }
|
||||
@SuppressLint("InflateParams")
|
||||
override fun onCreateExtractTextView(): View? {
|
||||
val eel = super.onCreateExtractTextView()
|
||||
if (eel !is ViewGroup) {
|
||||
return null
|
||||
}
|
||||
extractEditLayout = WeakReference(eel)
|
||||
eel.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
extractEditLayout.get()?.let { eel ->
|
||||
eel.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
eel.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT
|
||||
).also {
|
||||
it.setMargins(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return eel
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -232,16 +286,63 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
florisboardInstance = null
|
||||
|
||||
eventListeners.toList().forEach { it?.get()?.onDestroy() }
|
||||
serviceLifecycleDispatcher.onServicePreSuperOnDestroy()
|
||||
|
||||
eventListeners.toList().forEach { it?.onDestroy() }
|
||||
eventListeners.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onEvaluateFullscreenMode(): Boolean {
|
||||
return resources?.configuration?.let { config ->
|
||||
if (config.orientation != Configuration.ORIENTATION_LANDSCAPE) {
|
||||
false
|
||||
} else {
|
||||
when (prefs.keyboard.landscapeInputUiMode) {
|
||||
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeEditorInstance.imeOptions.flagNoFullscreen && !activeEditorInstance.imeOptions.flagNoExtractUi
|
||||
LandscapeInputUiMode.NEVER_SHOW -> false
|
||||
LandscapeInputUiMode.ALWAYS_SHOW -> true
|
||||
}
|
||||
}
|
||||
} ?: false
|
||||
}
|
||||
|
||||
override fun updateFullscreenMode() {
|
||||
super.updateFullscreenMode()
|
||||
updateSoftInputWindowLayoutParameters()
|
||||
}
|
||||
|
||||
override fun onUpdateExtractingVisibility(ei: EditorInfo?) {
|
||||
isExtractViewShown = !activeEditorInstance.isRawInputEditor && when (prefs.keyboard.landscapeInputUiMode) {
|
||||
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeEditorInstance.imeOptions.flagNoExtractUi
|
||||
LandscapeInputUiMode.NEVER_SHOW -> false
|
||||
LandscapeInputUiMode.ALWAYS_SHOW -> true
|
||||
}
|
||||
}
|
||||
|
||||
fun registerInputView(inputView: InputView) {
|
||||
Timber.i("registerInputView($inputView)")
|
||||
|
||||
window?.window?.findViewById<View>(android.R.id.content)?.let { content ->
|
||||
popupLayerView = PopupLayerView(content.context)
|
||||
if (content is ViewGroup) {
|
||||
content.addView(popupLayerView)
|
||||
}
|
||||
}
|
||||
this.inputView = inputView
|
||||
updateSoftInputWindowLayoutParameters()
|
||||
updateOneHandedPanelVisibility()
|
||||
themeManager.notifyCallbackReceivers()
|
||||
setActiveInput(R.id.text_input)
|
||||
|
||||
eventListeners.toList().forEach { it?.onRegisterInputView(inputView) }
|
||||
}
|
||||
|
||||
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
|
||||
Timber.i("onStartInput($attribute, $restarting)")
|
||||
|
||||
super.onStartInput(attribute, restarting)
|
||||
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE)
|
||||
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
|
||||
}
|
||||
|
||||
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
|
||||
@@ -252,7 +353,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
activeEditorInstance = EditorInstance.from(info, this)
|
||||
themeManager.updateRemoteColorValues(activeEditorInstance.packageName)
|
||||
eventListeners.toList().forEach {
|
||||
it?.get()?.onStartInputView(activeEditorInstance, restarting)
|
||||
it?.onStartInputView(activeEditorInstance, restarting)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,7 +365,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
|
||||
super.onFinishInputView(finishingInput)
|
||||
eventListeners.toList().forEach { it?.get()?.onFinishInputView(finishingInput) }
|
||||
eventListeners.toList().forEach { it?.onFinishInputView(finishingInput) }
|
||||
}
|
||||
|
||||
override fun onFinishInput() {
|
||||
@@ -275,7 +376,13 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
Timber.i("onWindowShown()")
|
||||
if (isWindowShown) {
|
||||
Timber.i("Ignoring onWindowShown()")
|
||||
return
|
||||
} else {
|
||||
Timber.i("onWindowShown()")
|
||||
}
|
||||
isWindowShown = true
|
||||
|
||||
prefs.sync()
|
||||
val newIsNumberRowVisible = prefs.keyboard.numberRow
|
||||
@@ -290,14 +397,20 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
setActiveInput(R.id.text_input)
|
||||
|
||||
super.onWindowShown()
|
||||
eventListeners.toList().forEach { it?.get()?.onWindowShown() }
|
||||
eventListeners.toList().forEach { it?.onWindowShown() }
|
||||
}
|
||||
|
||||
override fun onWindowHidden() {
|
||||
Timber.i("onWindowHidden()")
|
||||
if (!isWindowShown) {
|
||||
Timber.i("Ignoring onWindowHidden()")
|
||||
return
|
||||
} else {
|
||||
Timber.i("onWindowHidden()")
|
||||
}
|
||||
isWindowShown = false
|
||||
|
||||
super.onWindowHidden()
|
||||
eventListeners.toList().forEach { it?.get()?.onWindowHidden() }
|
||||
eventListeners.toList().forEach { it?.onWindowHidden() }
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
@@ -309,23 +422,74 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins a FlorisBoard internal batch edit. This enables the application to continue sending selection updates
|
||||
* (some apps need to to this else they absolutely refuse to give visual feedback on cursor movement etc.). The
|
||||
* selection update is then caught if [internalBatchNestingLevel] is greater than 0, thus not delegating the
|
||||
* update to the editor instance. This is needed because else the UI stutters when too many updates arrive in a
|
||||
* row.
|
||||
*/
|
||||
fun beginInternalBatchEdit() {
|
||||
internalBatchNestingLevel++
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends an internal batch edit, if [internalBatchNestingLevel] is <= 1 and calls [onUpdateSelection] with the
|
||||
* corresponding reported selection values. This call is not caught and the editor instance and other classes are
|
||||
* able to update the UI. Resets the internal selection cache and is ready for the next batch edit.
|
||||
*/
|
||||
fun endInternalBatchEdit() {
|
||||
internalBatchNestingLevel = (internalBatchNestingLevel - 1).coerceAtLeast(0)
|
||||
if (internalBatchNestingLevel == 0) {
|
||||
internalSelectionCache.apply {
|
||||
if (selectionCatchCount > 0) {
|
||||
onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd,
|
||||
candidatesStart, candidatesEnd
|
||||
)
|
||||
selectionCatchCount = 0
|
||||
oldSelStart = -1
|
||||
oldSelEnd = -1
|
||||
newSelStart = -1
|
||||
newSelEnd = -1
|
||||
candidatesStart = -1
|
||||
candidatesEnd = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUpdateSelection(
|
||||
oldSelStart: Int, oldSelEnd: Int,
|
||||
newSelStart: Int, newSelEnd: Int,
|
||||
candidatesStart: Int, candidatesEnd: Int
|
||||
) {
|
||||
Timber.i("onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
|
||||
|
||||
super.onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd,
|
||||
candidatesStart, candidatesEnd
|
||||
)
|
||||
activeEditorInstance.onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd
|
||||
)
|
||||
eventListeners.toList().forEach { it?.get()?.onUpdateSelection() }
|
||||
|
||||
if (internalBatchNestingLevel == 0) {
|
||||
Timber.i("onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
|
||||
activeEditorInstance.onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd,
|
||||
candidatesStart, candidatesEnd
|
||||
)
|
||||
eventListeners.toList().forEach { it?.onUpdateSelection() }
|
||||
} else {
|
||||
Timber.i("onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd): caught due to internal batch level of $internalBatchNestingLevel!")
|
||||
if (internalSelectionCache.selectionCatchCount++ == 0) {
|
||||
internalSelectionCache.oldSelStart = oldSelStart
|
||||
internalSelectionCache.oldSelEnd = oldSelEnd
|
||||
}
|
||||
internalSelectionCache.newSelStart = newSelStart
|
||||
internalSelectionCache.newSelEnd = newSelEnd
|
||||
internalSelectionCache.candidatesStart = candidatesStart
|
||||
internalSelectionCache.candidatesEnd = candidatesEnd
|
||||
}
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
@@ -367,15 +531,36 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
|
||||
// Update InputView theme
|
||||
inputView?.setBackgroundColor(theme.getAttr(Theme.Attr.KEYBOARD_BACKGROUND).toSolidColor().color)
|
||||
inputView?.oneHandedCtrlPanelStart?.setBackgroundColor(theme.getAttr(Theme.Attr.ONE_HANDED_BACKGROUND).toSolidColor().color)
|
||||
inputView?.oneHandedCtrlPanelEnd?.setBackgroundColor(theme.getAttr(Theme.Attr.ONE_HANDED_BACKGROUND).toSolidColor().color)
|
||||
ColorStateList.valueOf(theme.getAttr(Theme.Attr.ONE_HANDED_FOREGROUND).toSolidColor().color).also {
|
||||
inputView?.oneHandedCtrlMoveStart?.imageTintList = it
|
||||
inputView?.oneHandedCtrlMoveEnd?.imageTintList = it
|
||||
inputView?.oneHandedCtrlCloseStart?.imageTintList = it
|
||||
inputView?.oneHandedCtrlCloseEnd?.imageTintList = it
|
||||
inputView?.invalidate()
|
||||
|
||||
// Update ExtractTextView theme and attributes
|
||||
extractEditLayout.get()?.let { eel ->
|
||||
val p = resources.getDimension(R.dimen.landscapeInputUi_padding).toInt()
|
||||
eel.setPadding(p, p, 0, p)
|
||||
eel.setBackgroundColor(theme.getAttr(Theme.Attr.EXTRACT_EDIT_LAYOUT_BACKGROUND).toSolidColor().color)
|
||||
eel.findViewById<ExtractEditText>(android.R.id.inputExtractEditText)?.let { eet ->
|
||||
val p2 = resources.getDimension(R.dimen.landscapeInputUi_editText_padding).toInt()
|
||||
eet.setPadding(p2, p2, p2, p2)
|
||||
eet.background = ContextCompat.getDrawable(this, R.drawable.edit_text_background)?.also { d ->
|
||||
DrawableCompat.setTint(d, theme.getAttr(Theme.Attr.WINDOW_COLOR_PRIMARY).toSolidColor().color)
|
||||
}
|
||||
eet.setTextColor(theme.getAttr(Theme.Attr.EXTRACT_EDIT_LAYOUT_FOREGROUND).toSolidColor().color)
|
||||
eet.setHintTextColor(theme.getAttr(Theme.Attr.EXTRACT_EDIT_LAYOUT_FOREGROUND_ALT).toSolidColor().color)
|
||||
eet.highlightColor = theme.getAttr(Theme.Attr.WINDOW_COLOR_PRIMARY).toSolidColor().color
|
||||
}
|
||||
eel.findViewWithType(FrameLayout::class)?.let { fra ->
|
||||
fra.background = null
|
||||
}
|
||||
eel.findViewWithType(Button::class)?.let { btn ->
|
||||
btn.background = ContextCompat.getDrawable(this, R.drawable.shape_rect_rounded)?.also { d ->
|
||||
DrawableCompat.setTint(d, theme.getAttr(Theme.Attr.EXTRACT_ACTION_BUTTON_BACKGROUND).toSolidColor().color)
|
||||
}
|
||||
btn.setTextColor(theme.getAttr(Theme.Attr.EXTRACT_ACTION_BUTTON_FOREGROUND).toSolidColor().color)
|
||||
}
|
||||
eel.invalidate()
|
||||
}
|
||||
eventListeners.toList().forEach { it?.get()?.onApplyThemeAttributes() }
|
||||
|
||||
eventListeners.toList().forEach { it?.onApplyThemeAttributes() }
|
||||
}
|
||||
|
||||
override fun onComputeInsets(outInsets: Insets?) {
|
||||
@@ -393,11 +578,6 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
outInsets?.visibleTopInsets = visibleTopY
|
||||
}
|
||||
|
||||
override fun updateFullscreenMode() {
|
||||
super.updateFullscreenMode()
|
||||
updateSoftInputWindowLayoutParameters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the layout params of the window and input view.
|
||||
*/
|
||||
@@ -425,7 +605,12 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
if (prefs.keyboard.vibrationEnabled) {
|
||||
var vibrationStrength = prefs.keyboard.vibrationStrength
|
||||
if (vibrationStrength == -1 && prefs.keyboard.vibrationEnabledSystem) {
|
||||
vibrationStrength = 36
|
||||
val hapticsPerformed =
|
||||
inputWindowView?.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
|
||||
if (hapticsPerformed == false) {
|
||||
vibrationStrength = 36
|
||||
}
|
||||
}
|
||||
if (vibrationStrength > 0) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -477,15 +662,15 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the IME and launches [SettingsMainActivity].
|
||||
* Hides the IME and launches [SetupActivity].
|
||||
*/
|
||||
fun launchSettings() {
|
||||
requestHideSelf(0)
|
||||
val i = Intent(this, SettingsMainActivity::class.java)
|
||||
val i = Intent(this, SetupActivity::class.java)
|
||||
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
startActivity(i)
|
||||
applicationContext.startActivity(i)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -501,6 +686,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
switchToPreviousInputMethod()
|
||||
} else {
|
||||
window.window?.let { window ->
|
||||
@Suppress("DEPRECATION")
|
||||
imeManager?.switchToLastInputMethod(window.attributes.token)
|
||||
}
|
||||
}
|
||||
@@ -510,6 +696,22 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
}
|
||||
|
||||
fun switchToNextKeyboard(){
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
switchToNextInputMethod(false)
|
||||
} else {
|
||||
window.window?.let { window ->
|
||||
@Suppress("DEPRECATION")
|
||||
imeManager?.switchToNextInputMethod(window.attributes.token, false)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e,"Unable to switch to the next IME")
|
||||
imeManager?.showInputMethodPicker()
|
||||
}
|
||||
}
|
||||
|
||||
fun switchToPrevSubtype() {
|
||||
activeSubtype = subtypeManager.switchToPrevSubtype() ?: Subtype.DEFAULT
|
||||
onSubtypeChanged(activeSubtype)
|
||||
@@ -536,58 +738,29 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeOneHandedEnvironment() {
|
||||
{ v:View -> onOneHandedPanelButtonClick(v) }.also {
|
||||
inputView?.oneHandedCtrlMoveStart?.setOnClickListener(it)
|
||||
inputView?.oneHandedCtrlMoveEnd?.setOnClickListener(it)
|
||||
inputView?.oneHandedCtrlCloseStart?.setOnClickListener(it)
|
||||
inputView?.oneHandedCtrlCloseEnd?.setOnClickListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOneHandedPanelButtonClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.one_handed_ctrl_move_start -> {
|
||||
prefs.keyboard.oneHandedMode = "start"
|
||||
}
|
||||
R.id.one_handed_ctrl_move_end -> {
|
||||
prefs.keyboard.oneHandedMode = "end"
|
||||
}
|
||||
R.id.one_handed_ctrl_close_start,
|
||||
R.id.one_handed_ctrl_close_end -> {
|
||||
prefs.keyboard.oneHandedMode = "off"
|
||||
}
|
||||
fun toggleOneHandedMode(isRight: Boolean) {
|
||||
prefs.keyboard.oneHandedMode = when (prefs.keyboard.oneHandedMode) {
|
||||
OneHandedMode.OFF -> if (isRight) { OneHandedMode.END } else { OneHandedMode.START }
|
||||
else -> OneHandedMode.OFF
|
||||
}
|
||||
updateOneHandedPanelVisibility()
|
||||
}
|
||||
|
||||
fun toggleOneHandedMode() {
|
||||
when (prefs.keyboard.oneHandedMode) {
|
||||
"off" -> {
|
||||
prefs.keyboard.oneHandedMode = "end"
|
||||
}
|
||||
else -> {
|
||||
prefs.keyboard.oneHandedMode = "off"
|
||||
}
|
||||
}
|
||||
updateOneHandedPanelVisibility()
|
||||
}
|
||||
|
||||
private fun updateOneHandedPanelVisibility() {
|
||||
fun updateOneHandedPanelVisibility() {
|
||||
if (resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
|
||||
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
|
||||
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
|
||||
} else {
|
||||
when (prefs.keyboard.oneHandedMode) {
|
||||
"off" -> {
|
||||
OneHandedMode.OFF -> {
|
||||
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
|
||||
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
|
||||
}
|
||||
"start" -> {
|
||||
OneHandedMode.START -> {
|
||||
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
|
||||
inputView?.oneHandedCtrlPanelEnd?.visibility = View.VISIBLE
|
||||
}
|
||||
"end" -> {
|
||||
OneHandedMode.END -> {
|
||||
inputView?.oneHandedCtrlPanelStart?.visibility = View.VISIBLE
|
||||
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
|
||||
}
|
||||
@@ -600,7 +773,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
|
||||
override fun onPrimaryClipChanged() {
|
||||
eventListeners.toList().forEach { it?.get()?.onPrimaryClipChanged() }
|
||||
eventListeners.toList().forEach { it?.onPrimaryClipChanged() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -610,7 +783,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
* @return True if the listener has been added successfully, false otherwise.
|
||||
*/
|
||||
fun addEventListener(listener: EventListener): Boolean {
|
||||
return eventListeners.add(WeakReference(listener))
|
||||
return eventListeners.add(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -623,12 +796,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
* value may also indicate that the [listener] was not added previously.
|
||||
*/
|
||||
fun removeEventListener(listener: EventListener): Boolean {
|
||||
eventListeners.toList().forEach {
|
||||
if (it?.get() == listener) {
|
||||
return eventListeners.remove(it)
|
||||
}
|
||||
}
|
||||
return false
|
||||
return eventListeners.remove(listener)
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
@@ -673,14 +841,21 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
val defaultSubtypesLanguageNames: List<String>
|
||||
|
||||
init {
|
||||
val tmpCodes = mutableListOf<String>()
|
||||
val tmpNames = mutableListOf<String>()
|
||||
val tmpList = mutableListOf<Pair<String, String>>()
|
||||
for (defaultSubtype in defaultSubtypes) {
|
||||
tmpCodes.add(defaultSubtype.locale.toString())
|
||||
tmpNames.add(defaultSubtype.locale.displayName)
|
||||
tmpList.add(Pair(defaultSubtype.locale.toString(), defaultSubtype.locale.displayName))
|
||||
}
|
||||
defaultSubtypesLanguageCodes = tmpCodes.toList()
|
||||
defaultSubtypesLanguageNames = tmpNames.toList()
|
||||
// Sort language list alphabetically by the display name of a language
|
||||
tmpList.sortBy { it.second }
|
||||
// Move selected English variants to the top of the list
|
||||
for (languageCode in listOf("en_CA", "en_AU", "en_UK", "en_US")) {
|
||||
val index: Int = tmpList.indexOfFirst { it.first == languageCode }
|
||||
if (index > 0) {
|
||||
tmpList.add(0, tmpList.removeAt(index))
|
||||
}
|
||||
}
|
||||
defaultSubtypesLanguageCodes = tmpList.map { it.first }.toList()
|
||||
defaultSubtypesLanguageNames = tmpList.map { it.second }.toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ package dev.patrickgold.florisboard.ime.core
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ViewFlipper
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
/**
|
||||
* Custom ViewFlipper class used to prevent an unnecessary exception to be thrown when it is
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import android.os.SystemClock
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* The main logic point of processing input events and delegating them to the registered event receivers. Currently,
|
||||
* only [InputKeyEvent]s are supported, but in the future this class is thought to be the single point where input
|
||||
* events can be dispatched.
|
||||
*/
|
||||
class InputEventDispatcher private constructor(
|
||||
parentScope: CoroutineScope,
|
||||
channelCapacity: Int,
|
||||
private val mainDispatcher: CoroutineDispatcher,
|
||||
private val defaultDispatcher: CoroutineDispatcher,
|
||||
private val repeatableKeyCodes: IntArray
|
||||
) : InputKeyEventSender {
|
||||
private val channel: Channel<InputKeyEvent> = Channel(channelCapacity)
|
||||
private val scope: CoroutineScope = CoroutineScope(parentScope.coroutineContext)
|
||||
private val pressedKeys: HashMap<Int, PressedKeyInfo> = hashMapOf()
|
||||
var lastKeyEventDown: InputKeyEvent? = null
|
||||
private set
|
||||
var lastKeyEventUp: InputKeyEvent? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* The input key event register. If null, the dispatcher will still process input, but won't dispatch them to an
|
||||
* event receiver.
|
||||
*/
|
||||
var keyEventReceiver: InputKeyEventReceiver? = null
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The default input event channel capacity to be used in [new].
|
||||
*/
|
||||
private const val DEFAULT_CHANNEL_CAPACITY: Int = 32
|
||||
|
||||
/**
|
||||
* Creates a new [InputEventDispatcher] instance from given arguments and returns it.
|
||||
*
|
||||
* @param parentScope The parent coroutine scope which this dispatcher will attach its own scope to.
|
||||
* @param channelCapacity The capacity of this input channel, defaults to [DEFAULT_CHANNEL_CAPACITY].
|
||||
* @param mainDispatcher The main dispatcher used to switch the context to call the receiver callbacks.
|
||||
* Defaults to [Dispatchers.Main].
|
||||
* @param defaultDispatcher The default dispatcher used to switch the context to call the receiver callbacks.
|
||||
* Defaults to [Dispatchers.Default].
|
||||
* @param repeatableKeyCodes An int array of all key codes which are repeatable while being pressed down.
|
||||
*
|
||||
* @return A new [InputEventDispatcher] instance initialized with given arguments.
|
||||
*/
|
||||
fun new(
|
||||
parentScope: CoroutineScope,
|
||||
channelCapacity: Int = DEFAULT_CHANNEL_CAPACITY,
|
||||
mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
|
||||
defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
repeatableKeyCodes: IntArray = intArrayOf()
|
||||
): InputEventDispatcher = InputEventDispatcher(
|
||||
parentScope, channelCapacity, mainDispatcher, defaultDispatcher, repeatableKeyCodes.clone()
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
scope.launch(defaultDispatcher) {
|
||||
for (ev in channel) {
|
||||
if (!isActive) break
|
||||
val startTime = System.nanoTime()
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.d(ev.toString())
|
||||
}
|
||||
when (ev.action) {
|
||||
InputKeyEvent.Action.DOWN -> {
|
||||
if (pressedKeys.containsKey(ev.data.code)) continue
|
||||
pressedKeys[ev.data.code] = PressedKeyInfo(
|
||||
eventTimeDown = ev.eventTime,
|
||||
repeatKeyPressJob = if (!repeatableKeyCodes.contains(ev.data.code)) { null } else {
|
||||
scope.launch(defaultDispatcher) {
|
||||
delay(600)
|
||||
while (isActive) {
|
||||
channel.send(InputKeyEvent.repeat(ev.data))
|
||||
delay(50)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
withContext(mainDispatcher) {
|
||||
keyEventReceiver?.onInputKeyDown(ev)
|
||||
}
|
||||
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
|
||||
lastKeyEventDown = ev
|
||||
}
|
||||
}
|
||||
InputKeyEvent.Action.DOWN_UP -> {
|
||||
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
|
||||
withContext(mainDispatcher) {
|
||||
keyEventReceiver?.onInputKeyDown(ev)
|
||||
keyEventReceiver?.onInputKeyUp(ev)
|
||||
}
|
||||
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
|
||||
lastKeyEventDown = ev
|
||||
lastKeyEventUp = ev
|
||||
}
|
||||
}
|
||||
InputKeyEvent.Action.UP -> {
|
||||
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
|
||||
withContext(mainDispatcher) {
|
||||
keyEventReceiver?.onInputKeyUp(ev)
|
||||
}
|
||||
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
|
||||
lastKeyEventUp = ev
|
||||
}
|
||||
}
|
||||
InputKeyEvent.Action.REPEAT -> {
|
||||
if (pressedKeys.containsKey(ev.data.code)) {
|
||||
withContext(mainDispatcher) {
|
||||
keyEventReceiver?.onInputKeyRepeat(ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
InputKeyEvent.Action.CANCEL -> {
|
||||
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
|
||||
withContext(mainDispatcher) {
|
||||
keyEventReceiver?.onInputKeyCancel(ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.d("Time elapsed: ${(System.nanoTime() - startTime) / 1_000_000}")
|
||||
}
|
||||
}
|
||||
val pressedKeysIterator = pressedKeys.iterator()
|
||||
while (pressedKeysIterator.hasNext()) {
|
||||
pressedKeysIterator.next().value.repeatKeyPressJob?.cancel()
|
||||
pressedKeysIterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun send(ev: InputKeyEvent) {
|
||||
scope.launch(mainDispatcher) {
|
||||
channel.send(ev)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's currently a key down with given [code].
|
||||
*
|
||||
* @param code The key code to check for.
|
||||
*
|
||||
* @return True if the given [code] is currently down, false otherwise.
|
||||
*/
|
||||
fun isPressed(code: Int): Boolean {
|
||||
return pressedKeys.containsKey(code)
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this dispatcher and cancels the local coroutine scope.
|
||||
*/
|
||||
fun close() {
|
||||
keyEventReceiver = null
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
data class PressedKeyInfo(
|
||||
val eventTimeDown: Long,
|
||||
val repeatKeyPressJob: Job?
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class representing a single input key event.
|
||||
*
|
||||
* @property eventTime The exact event time when this event occurred, measured in milliseconds since a static point in
|
||||
* the past. The exact point is irrelevant, but while this input dispatcher is active, the point must not change in
|
||||
* order for difference time calculation to succeed.
|
||||
* @property action The action of this event.
|
||||
* @property data The data of this event.
|
||||
* @property count The count how often this event occurred. Is only respected by other methods if the [action] of this
|
||||
* event is [Action.DOWN_UP] or [Action.REPEAT], else always 1 is assumed.
|
||||
*/
|
||||
data class InputKeyEvent(
|
||||
val eventTime: Long,
|
||||
val action: Action,
|
||||
val data: KeyData,
|
||||
val count: Int
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Creates a new input key event with given [keyData] and sets the action to [Action.DOWN].
|
||||
*
|
||||
* @param keyData The key data of the input key event event to create.
|
||||
*
|
||||
* @return The created input key event.
|
||||
*/
|
||||
fun down(keyData: KeyData): InputKeyEvent {
|
||||
return InputKeyEvent(
|
||||
eventTime = SystemClock.uptimeMillis(),
|
||||
action = Action.DOWN,
|
||||
data = keyData,
|
||||
count = 1
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new input key event with given [keyData] and sets the action to [Action.DOWN_UP].
|
||||
*
|
||||
* @param keyData The key data of the input key event event to create.
|
||||
* @param count How often this event occurred. Must be grater or equal to 1, defaults to 1.
|
||||
*
|
||||
* @return The created input key event.
|
||||
*/
|
||||
fun downUp(keyData: KeyData, count: Int = 1): InputKeyEvent {
|
||||
return InputKeyEvent(
|
||||
eventTime = SystemClock.uptimeMillis(),
|
||||
action = Action.DOWN_UP,
|
||||
data = keyData,
|
||||
count = count
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new input key event with given [keyData] and sets the action to [Action.UP].
|
||||
*
|
||||
* @param keyData The key data of the input key event event to create.
|
||||
*
|
||||
* @return The created input key event.
|
||||
*/
|
||||
fun up(keyData: KeyData): InputKeyEvent {
|
||||
return InputKeyEvent(
|
||||
eventTime = SystemClock.uptimeMillis(),
|
||||
action = Action.UP,
|
||||
data = keyData,
|
||||
count = 1
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new input key event with given [keyData] and sets the action to [Action.REPEAT].
|
||||
*
|
||||
* @param keyData The key data of the input key event event to create.
|
||||
* @param count How often this event occurred. Must be grater or equal to 1, defaults to 1.
|
||||
*
|
||||
* @return The created input key event.
|
||||
*/
|
||||
fun repeat(keyData: KeyData, count: Int = 1): InputKeyEvent {
|
||||
return InputKeyEvent(
|
||||
eventTime = SystemClock.uptimeMillis(),
|
||||
action = Action.REPEAT,
|
||||
data = keyData,
|
||||
count = count
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new input key event with given [keyData] and sets the action to [Action.CANCEL].
|
||||
*
|
||||
* @param keyData The key data of the input key event event to create.
|
||||
*
|
||||
* @return The created input key event.
|
||||
*/
|
||||
fun cancel(keyData: KeyData): InputKeyEvent {
|
||||
return InputKeyEvent(
|
||||
eventTime = SystemClock.uptimeMillis(),
|
||||
action = Action.CANCEL,
|
||||
data = keyData,
|
||||
count = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the [other] input key event is a consecutive event while respecting [maxEventTimeDiff].
|
||||
*
|
||||
* @param other The other input key event to compare with this one.
|
||||
* @param maxEventTimeDiff The maximum event time diff between this event and [other], in milliseconds.
|
||||
*
|
||||
* @return True if this event is a consecutive event of [other], false otherwise.
|
||||
*/
|
||||
fun isConsecutiveEventOf(other: InputKeyEvent?, maxEventTimeDiff: Long): Boolean {
|
||||
return other != null && data.code == other.data.code && eventTime - other.eventTime <= maxEventTimeDiff
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this input key event.
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return "FlorisKeyEvent { eventTime=${eventTime}ms, action=$action, data=$data, count=$count }"
|
||||
}
|
||||
|
||||
/**
|
||||
* The action of an input key event.
|
||||
*/
|
||||
enum class Action {
|
||||
DOWN,
|
||||
DOWN_UP,
|
||||
UP,
|
||||
REPEAT,
|
||||
CANCEL,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface which represents an input key event sender.
|
||||
*/
|
||||
interface InputKeyEventSender {
|
||||
/**
|
||||
* Sends given input key event [ev] to the underlying input channel, awaiting to be processed.
|
||||
*
|
||||
* @param ev The input key event to send.
|
||||
*/
|
||||
fun send(ev: InputKeyEvent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface which represents an input key event receiver.
|
||||
*/
|
||||
interface InputKeyEventReceiver {
|
||||
/**
|
||||
* Event method which gets called when a key went down.
|
||||
*
|
||||
* @param ev The associated input key event.
|
||||
*/
|
||||
fun onInputKeyDown(ev: InputKeyEvent)
|
||||
|
||||
/**
|
||||
* Event method which gets called when a key went up.
|
||||
*
|
||||
* @param ev The associated input key event.
|
||||
*/
|
||||
fun onInputKeyUp(ev: InputKeyEvent)
|
||||
|
||||
/**
|
||||
* Event method which gets called when a key is called repeatedly while being pressed down.
|
||||
*
|
||||
* @param ev The associated input key event.
|
||||
*/
|
||||
fun onInputKeyRepeat(ev: InputKeyEvent)
|
||||
|
||||
/**
|
||||
* Event method which gets called when a key press is cancelled.
|
||||
*
|
||||
* @param ev The associated input key event.
|
||||
*/
|
||||
fun onInputKeyCancel(ev: InputKeyEvent)
|
||||
}
|
||||
@@ -20,12 +20,12 @@ import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
@@ -50,17 +50,9 @@ class InputView : LinearLayout {
|
||||
|
||||
var mainViewFlipper: ViewFlipper? = null
|
||||
private set
|
||||
var oneHandedCtrlPanelStart: LinearLayout? = null
|
||||
var oneHandedCtrlPanelStart: ViewGroup? = null
|
||||
private set
|
||||
var oneHandedCtrlPanelEnd: LinearLayout? = null
|
||||
private set
|
||||
var oneHandedCtrlMoveStart: ImageButton? = null
|
||||
private set
|
||||
var oneHandedCtrlMoveEnd: ImageButton? = null
|
||||
private set
|
||||
var oneHandedCtrlCloseStart: ImageButton? = null
|
||||
private set
|
||||
var oneHandedCtrlCloseEnd: ImageButton? = null
|
||||
var oneHandedCtrlPanelEnd: ViewGroup? = null
|
||||
private set
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
@@ -79,10 +71,6 @@ class InputView : LinearLayout {
|
||||
mainViewFlipper = findViewById(R.id.main_view_flipper)
|
||||
oneHandedCtrlPanelStart = findViewById(R.id.one_handed_ctrl_panel_start)
|
||||
oneHandedCtrlPanelEnd = findViewById(R.id.one_handed_ctrl_panel_end)
|
||||
oneHandedCtrlMoveStart = findViewById(R.id.one_handed_ctrl_move_start)
|
||||
oneHandedCtrlMoveEnd = findViewById(R.id.one_handed_ctrl_move_end)
|
||||
oneHandedCtrlCloseStart = findViewById(R.id.one_handed_ctrl_close_start)
|
||||
oneHandedCtrlCloseEnd = findViewById(R.id.one_handed_ctrl_close_end)
|
||||
|
||||
florisboard.registerInputView(this)
|
||||
}
|
||||
@@ -90,8 +78,8 @@ class InputView : LinearLayout {
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val heightFactor = when (resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> 1.0f
|
||||
else -> if (prefs.keyboard.oneHandedMode != "off") {
|
||||
0.9f
|
||||
else -> if (prefs.keyboard.oneHandedMode != OneHandedMode.OFF) {
|
||||
prefs.keyboard.oneHandedModeScaleFactor / 100.0f
|
||||
} else {
|
||||
1.0f
|
||||
}
|
||||
@@ -135,7 +123,11 @@ class InputView : LinearLayout {
|
||||
// Add bottom offset for curved screens here. As the desired heights have already been set,
|
||||
// adding a value to the height now will result in a bottom padding (aka offset).
|
||||
baseHeight += ViewLayoutUtils.convertDpToPixel(
|
||||
florisboard.prefs.keyboard.bottomOffset.toFloat(),
|
||||
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
florisboard.prefs.keyboard.bottomOffsetLandscape.toFloat()
|
||||
} else {
|
||||
florisboard.prefs.keyboard.bottomOffsetPortrait.toFloat()
|
||||
},
|
||||
context
|
||||
)
|
||||
|
||||
@@ -169,5 +161,4 @@ class InputView : LinearLayout {
|
||||
resources.getDimension(R.dimen.inputView_baseHeight)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,12 +18,7 @@ package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
|
||||
/**
|
||||
* Root view of the keyboard.
|
||||
|
||||
@@ -21,14 +21,16 @@ import android.content.SharedPreferences
|
||||
import android.provider.Settings
|
||||
import androidx.preference.PreferenceManager
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.DistanceThreshold
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.VelocityThreshold
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
|
||||
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeMode
|
||||
import dev.patrickgold.florisboard.util.TimeUtil
|
||||
import dev.patrickgold.florisboard.util.VersionName
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
/**
|
||||
* Helper class for an organized access to the shared preferences.
|
||||
@@ -215,6 +217,7 @@ class PrefHelper(
|
||||
const val SWIPE_DOWN = "gestures__swipe_down"
|
||||
const val SWIPE_LEFT = "gestures__swipe_left"
|
||||
const val SWIPE_RIGHT = "gestures__swipe_right"
|
||||
const val SPACE_BAR_LONG_PRESS = "gestures__space_bar_long_press"
|
||||
const val SPACE_BAR_SWIPE_LEFT = "gestures__space_bar_swipe_left"
|
||||
const val SPACE_BAR_SWIPE_RIGHT = "gestures__space_bar_swipe_right"
|
||||
const val SPACE_BAR_SWIPE_UP = "gestures__space_bar_swipe_up"
|
||||
@@ -235,6 +238,9 @@ class PrefHelper(
|
||||
var swipeRight: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_RIGHT, "no_action"))
|
||||
set(v) = prefHelper.setPref(SWIPE_RIGHT, v)
|
||||
var spaceBarLongPress: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_LONG_PRESS, "no_action"))
|
||||
set(v) = prefHelper.setPref(SPACE_BAR_LONG_PRESS, v)
|
||||
var spaceBarSwipeUp: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_SWIPE_UP, "no_action"))
|
||||
set(v) = prefHelper.setPref(SPACE_BAR_SWIPE_UP, v)
|
||||
@@ -303,25 +309,35 @@ class PrefHelper(
|
||||
*/
|
||||
class Keyboard(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val BOTTOM_OFFSET = "keyboard__bottom_offset"
|
||||
const val BOTTOM_OFFSET_PORTRAIT = "keyboard__bottom_offset_portrait"
|
||||
const val BOTTOM_OFFSET_LANDSCAPE = "keyboard__bottom_offset_landscape"
|
||||
const val FONT_SIZE_MULTIPLIER_PORTRAIT = "keyboard__font_size_multiplier_portrait"
|
||||
const val FONT_SIZE_MULTIPLIER_LANDSCAPE = "keyboard__font_size_multiplier_landscape"
|
||||
const val HEIGHT_FACTOR = "keyboard__height_factor"
|
||||
const val HEIGHT_FACTOR_CUSTOM = "keyboard__height_factor_custom"
|
||||
const val HINTED_NUMBER_ROW_MODE = "keyboard__hinted_number_row_mode"
|
||||
const val HINTED_SYMBOLS_MODE = "keyboard__hinted_symbols_mode"
|
||||
const val KEY_SPACING_HORIZONTAL = "keyboard__key_spacing_horizontal"
|
||||
const val KEY_SPACING_VERTICAL = "keyboard__key_spacing_vertical"
|
||||
const val LANDSCAPE_INPUT_UI_MODE = "keyboard__landscape_input_ui_mode"
|
||||
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
|
||||
const val NUMBER_ROW = "keyboard__number_row"
|
||||
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
|
||||
const val ONE_HANDED_MODE_SCALE_FACTOR = "keyboard__one_handed_mode_scale_factor"
|
||||
const val POPUP_ENABLED = "keyboard__popup_enabled"
|
||||
const val SOUND_ENABLED = "keyboard__sound_enabled"
|
||||
const val SOUND_VOLUME = "keyboard__sound_volume"
|
||||
const val UTILITY_KEY_ACTION = "keyboard__utility_key_action"
|
||||
const val UTILITY_KEY_ENABLED = "keyboard__utility_key_enabled"
|
||||
const val VIBRATION_ENABLED = "keyboard__vibration_enabled"
|
||||
const val VIBRATION_STRENGTH = "keyboard__vibration_strength"
|
||||
}
|
||||
|
||||
var bottomOffset: Int = 0
|
||||
get() = prefHelper.getPref(BOTTOM_OFFSET, 0)
|
||||
var bottomOffsetPortrait: Int = 0
|
||||
get() = prefHelper.getPref(BOTTOM_OFFSET_PORTRAIT, 0)
|
||||
private set
|
||||
var bottomOffsetLandscape: Int = 0
|
||||
get() = prefHelper.getPref(BOTTOM_OFFSET_LANDSCAPE, 0)
|
||||
private set
|
||||
var fontSizeMultiplierPortrait: Int
|
||||
get() = prefHelper.getPref(FONT_SIZE_MULTIPLIER_PORTRAIT, 100)
|
||||
@@ -341,6 +357,15 @@ class PrefHelper(
|
||||
var hintedSymbolsMode: KeyHintMode
|
||||
get() = KeyHintMode.fromString(prefHelper.getPref(HINTED_SYMBOLS_MODE, KeyHintMode.ENABLED_ACCENT_PRIORITY.toString()))
|
||||
set(v) = prefHelper.setPref(HINTED_SYMBOLS_MODE, v)
|
||||
var keySpacingHorizontal: Float = 2f
|
||||
get() = prefHelper.getPref(KEY_SPACING_HORIZONTAL, 4) / 2f
|
||||
private set
|
||||
var keySpacingVertical: Float = 5f
|
||||
get() = prefHelper.getPref(KEY_SPACING_VERTICAL, 10) / 2f
|
||||
private set
|
||||
var landscapeInputUiMode: LandscapeInputUiMode
|
||||
get() = LandscapeInputUiMode.fromString(prefHelper.getPref(LANDSCAPE_INPUT_UI_MODE, LandscapeInputUiMode.DYNAMICALLY_SHOW.toString()))
|
||||
set(v) = prefHelper.setPref(LANDSCAPE_INPUT_UI_MODE, v)
|
||||
var longPressDelay: Int = 0
|
||||
get() = prefHelper.getPref(LONG_PRESS_DELAY, 300)
|
||||
private set
|
||||
@@ -348,8 +373,11 @@ class PrefHelper(
|
||||
get() = prefHelper.getPref(NUMBER_ROW, false)
|
||||
set(v) = prefHelper.setPref(NUMBER_ROW, v)
|
||||
var oneHandedMode: String
|
||||
get() = prefHelper.getPref(ONE_HANDED_MODE, "off")
|
||||
get() = prefHelper.getPref(ONE_HANDED_MODE, OneHandedMode.OFF)
|
||||
set(value) = prefHelper.setPref(ONE_HANDED_MODE, value)
|
||||
var oneHandedModeScaleFactor: Int
|
||||
get() = prefHelper.getPref(ONE_HANDED_MODE_SCALE_FACTOR, 87)
|
||||
set(v) = prefHelper.setPref(ONE_HANDED_MODE_SCALE_FACTOR, v)
|
||||
var popupEnabled: Boolean = false
|
||||
get() = prefHelper.getPref(POPUP_ENABLED, true)
|
||||
private set
|
||||
@@ -360,6 +388,12 @@ class PrefHelper(
|
||||
var soundVolume: Int = 0
|
||||
get() = prefHelper.getPref(SOUND_VOLUME, -1)
|
||||
private set
|
||||
var utilityKeyAction: UtilityKeyAction
|
||||
get() = UtilityKeyAction.fromString(prefHelper.getPref(UTILITY_KEY_ACTION, UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS.toString()))
|
||||
set(v) = prefHelper.setPref(UTILITY_KEY_ACTION, v)
|
||||
var utilityKeyEnabled: Boolean
|
||||
get() = prefHelper.getPref(UTILITY_KEY_ENABLED, true)
|
||||
set(v) = prefHelper.setPref(UTILITY_KEY_ENABLED, v)
|
||||
var vibrationEnabled: Boolean = false
|
||||
get() = prefHelper.getPref(VIBRATION_ENABLED, true)
|
||||
private set
|
||||
@@ -404,11 +438,15 @@ class PrefHelper(
|
||||
*/
|
||||
class Suggestion(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val BLOCK_POSSIBLY_OFFENSIVE = "suggestion__block_possibly_offensive"
|
||||
const val ENABLED = "suggestion__enabled"
|
||||
const val SUGGEST_CLIPBOARD_CONTENT = "suggestion__suggest_clipboard_content"
|
||||
const val USE_PREV_WORDS = "suggestion__use_prev_words"
|
||||
}
|
||||
|
||||
var blockPossiblyOffensive: Boolean
|
||||
get() = prefHelper.getPref(BLOCK_POSSIBLY_OFFENSIVE, true)
|
||||
set(v) = prefHelper.setPref(BLOCK_POSSIBLY_OFFENSIVE, v)
|
||||
var enabled: Boolean
|
||||
get() = prefHelper.getPref(ENABLED, true)
|
||||
set(v) = prefHelper.setPref(ENABLED, v)
|
||||
|
||||
@@ -20,7 +20,10 @@ import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dev.patrickgold.florisboard.util.LocaleUtils
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.dictionary
|
||||
|
||||
import dev.patrickgold.florisboard.ime.extension.Asset
|
||||
import dev.patrickgold.florisboard.ime.nlp.LanguageModel
|
||||
import dev.patrickgold.florisboard.ime.nlp.MutableLanguageModel
|
||||
import dev.patrickgold.florisboard.ime.nlp.Token
|
||||
import dev.patrickgold.florisboard.ime.nlp.WeightedToken
|
||||
|
||||
/**
|
||||
* Standardized dictionary interface for interacting with dictionaries.
|
||||
*/
|
||||
interface Dictionary<T : Any, F : Comparable<F>> : Asset {
|
||||
val languageModel: LanguageModel<T, F>
|
||||
|
||||
/**
|
||||
* Gets token predictions based on the given [precedingTokens] and the [currentToken]. The
|
||||
* length of the returned list is limited to [maxSuggestionCount]. Note that the returned list
|
||||
* may at any time give back less items than [maxSuggestionCount] indicates.
|
||||
*/
|
||||
fun getTokenPredictions(
|
||||
precedingTokens: List<Token<T>>,
|
||||
currentToken: Token<T>?,
|
||||
maxSuggestionCount: Int,
|
||||
allowPossiblyOffensive: Boolean
|
||||
): List<WeightedToken<T, F>>
|
||||
|
||||
fun getDate(): Long
|
||||
|
||||
fun getVersion(): Int
|
||||
}
|
||||
|
||||
interface MutableDictionary<T : Any, F : Comparable<F>> : Dictionary<T, F> {
|
||||
override val languageModel: MutableLanguageModel<T, F>
|
||||
|
||||
fun trainTokenPredictions(
|
||||
precedingTokens: List<Token<T>>,
|
||||
lastToken: Token<T>
|
||||
)
|
||||
|
||||
fun setDate(date: Int)
|
||||
|
||||
fun setVersion(version: Int)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.dictionary
|
||||
|
||||
import android.content.Context
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* TODO: document
|
||||
*/
|
||||
class DictionaryManager private constructor(private val applicationContext: Context) {
|
||||
private val dictionaryCache: MutableMap<String, Dictionary<String, Int>> = mutableMapOf()
|
||||
|
||||
companion object {
|
||||
private var defaultInstance: DictionaryManager? = null
|
||||
|
||||
fun init(applicationContext: Context): DictionaryManager {
|
||||
val instance = DictionaryManager(applicationContext)
|
||||
defaultInstance = instance
|
||||
return instance
|
||||
}
|
||||
|
||||
fun default(): DictionaryManager {
|
||||
val instance = defaultInstance
|
||||
if (instance != null) {
|
||||
return instance
|
||||
} else {
|
||||
throw UninitializedPropertyAccessException(
|
||||
"${DictionaryManager::class.simpleName} has not been initialized previously. Make sure to call init(applicationContext) before using default()."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDictionary(ref: AssetRef): Result<Dictionary<String, Int>> {
|
||||
dictionaryCache[ref.toString()]?.let {
|
||||
return Result.success(it)
|
||||
}
|
||||
if (ref.path.endsWith(".flict")) {
|
||||
// Assume this is a Flictionary
|
||||
Flictionary.load(applicationContext, ref).onSuccess { flict ->
|
||||
dictionaryCache[ref.toString()] = flict
|
||||
return Result.success(flict)
|
||||
}.onFailure { err ->
|
||||
Timber.i(err)
|
||||
return Result.failure(err)
|
||||
}
|
||||
} else {
|
||||
return Result.failure(Exception("Unable to determine supported type for given AssetRef!"))
|
||||
}
|
||||
return Result.failure(Exception("If this message is ever thrown, something is completely broken..."))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.dictionary
|
||||
|
||||
import android.content.Context
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetSource
|
||||
import dev.patrickgold.florisboard.ime.nlp.*
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
/**
|
||||
* Class Flictionary which takes care of loading the binary asset as well as providing words for
|
||||
* queries.
|
||||
*
|
||||
* This class accepts binary dictionary files of the type "flict" as defined in here:
|
||||
* https://github.com/florisboard/dictionary-tools/blob/main/flictionary.md
|
||||
*/
|
||||
class Flictionary private constructor(
|
||||
override val name: String,
|
||||
override val label: String,
|
||||
override val authors: List<String>,
|
||||
private val date: Long,
|
||||
private val version: Int,
|
||||
private val headerStr: String,
|
||||
override val languageModel: LanguageModel<String, Int>
|
||||
) : Dictionary<String, Int> {
|
||||
companion object {
|
||||
private const val VERSION_0 = 0x0
|
||||
|
||||
private const val MASK_BEGIN_PTREE_NODE = 0x80
|
||||
private const val CMDB_BEGIN_PTREE_NODE = 0x00
|
||||
private const val ATTR_PTREE_NODE_ORDER = 0x70
|
||||
private const val ATTR_PTREE_NODE_TYPE = 0x0C
|
||||
private const val ATTR_PTREE_NODE_TYPE_CHAR = 0
|
||||
private const val ATTR_PTREE_NODE_TYPE_WORD_FILLER = 1
|
||||
private const val ATTR_PTREE_NODE_TYPE_WORD = 2
|
||||
private const val ATTR_PTREE_NODE_TYPE_SHORTCUT = 3
|
||||
private const val ATTR_PTREE_NODE_SIZE = 0x03
|
||||
|
||||
private const val MASK_END = 0xC0
|
||||
private const val CMDB_END = 0x80
|
||||
private const val ATTR_END_COUNT = 0x3F
|
||||
|
||||
private const val MASK_BEGIN_HEADER = 0xE0
|
||||
private const val CMDB_BEGIN_HEADER = 0xC0
|
||||
private const val ATTR_HEADER_VERSION = 0x1F
|
||||
|
||||
private const val MASK_DEFINE_SHORTCUT = 0xF0
|
||||
private const val CMDB_DEFINE_SHORTCUT = 0xE0
|
||||
|
||||
/**
|
||||
* Loads a Flictionary binary asset from given [assetRef] and returns a result containing
|
||||
* either the parsed dictionary or an exception giving information about the error which
|
||||
* occurred.
|
||||
*/
|
||||
fun load(context: Context, assetRef: AssetRef): Result<Flictionary> {
|
||||
val buffer = ByteArray(5000) { 0 }
|
||||
val inputStream: InputStream
|
||||
if (assetRef.source == AssetSource.Assets) {
|
||||
inputStream = context.assets.open(assetRef.path)
|
||||
} else {
|
||||
return Result.failure(Exception("Only AssetSource.Assets is currently supported!"))
|
||||
}
|
||||
|
||||
var headerStr: String? = null
|
||||
var date: Long = 0
|
||||
var version = 0
|
||||
val ngramTree = NgramTree()
|
||||
|
||||
var pos = 0
|
||||
val ngramOrderStack = mutableListOf<Int>()
|
||||
val ngramTreeStack = mutableListOf<NgramNode>()
|
||||
|
||||
while (true) {
|
||||
if (inputStream.readNext(buffer, 0, 1) <= 0) {
|
||||
break
|
||||
}
|
||||
val cmd = buffer[0].toInt() and 0xFF
|
||||
when {
|
||||
(cmd and MASK_BEGIN_PTREE_NODE) == CMDB_BEGIN_PTREE_NODE -> {
|
||||
if (pos == 0) {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_CMD_BEGIN_PTREE_NODE,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
val order = ((cmd and ATTR_PTREE_NODE_ORDER) shr 4) + 1
|
||||
val type = ((cmd and ATTR_PTREE_NODE_TYPE) shr 2)
|
||||
val size = (cmd and ATTR_PTREE_NODE_SIZE) + 1
|
||||
val freq: Int
|
||||
val freqSize: Int
|
||||
when (type) {
|
||||
ATTR_PTREE_NODE_TYPE_CHAR -> {
|
||||
freq = NgramNode.FREQ_CHARACTER
|
||||
freqSize = 0
|
||||
}
|
||||
ATTR_PTREE_NODE_TYPE_WORD_FILLER -> {
|
||||
freq = NgramNode.FREQ_WORD_FILLER
|
||||
freqSize = 0
|
||||
}
|
||||
ATTR_PTREE_NODE_TYPE_WORD -> {
|
||||
if (inputStream.readNext(buffer, 1, 1) > 0) {
|
||||
freq = buffer[1].toInt() and 0xFF
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
freqSize = 1
|
||||
}
|
||||
else -> return Result.failure(Exception("TODO: shortcut not supported"))
|
||||
}
|
||||
if (inputStream.readNext(buffer, freqSize + 1, size) > 0) {
|
||||
val char = String(buffer, freqSize + 1, size, Charsets.UTF_8)[0]
|
||||
val node = NgramNode(order, char, freq)
|
||||
val lastOrder = ngramOrderStack.lastOrNull()
|
||||
if (lastOrder == null) {
|
||||
ngramTree.higherOrderChildren.add(node)
|
||||
} else {
|
||||
if (lastOrder == order) {
|
||||
ngramTreeStack.last().sameOrderChildren.add(node)
|
||||
} else {
|
||||
ngramTreeStack.last().higherOrderChildren.add(node)
|
||||
}
|
||||
}
|
||||
ngramOrderStack.add(order)
|
||||
ngramTreeStack.add(node)
|
||||
pos += (freqSize + 1 + size)
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
(cmd and MASK_BEGIN_HEADER) == CMDB_BEGIN_HEADER -> {
|
||||
if (pos != 0) {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_CMD_BEGIN_HEADER,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
version = cmd and ATTR_HEADER_VERSION
|
||||
if (version != VERSION_0) {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNSUPPORTED_FLICTIONARY_VERSION,
|
||||
address = pos,
|
||||
cmdByte = cmd.toByte(),
|
||||
absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
if (inputStream.readNext(buffer, 1, 9) > 0) {
|
||||
val size = (buffer[1].toInt() and 0xFF)
|
||||
date =
|
||||
((buffer[2].toInt() and 0xFF).toLong() shl 56) +
|
||||
((buffer[3].toInt() and 0xFF).toLong() shl 48) +
|
||||
((buffer[4].toInt() and 0xFF).toLong() shl 40) +
|
||||
((buffer[5].toInt() and 0xFF).toLong() shl 32) +
|
||||
((buffer[6].toInt() and 0xFF).toLong() shl 24) +
|
||||
((buffer[7].toInt() and 0xFF).toLong() shl 16) +
|
||||
((buffer[8].toInt() and 0xFF).toLong() shl 8) +
|
||||
((buffer[9].toInt() and 0xFF).toLong() shl 0)
|
||||
if (inputStream.readNext(buffer, 10, size) > 0) {
|
||||
headerStr = String(buffer, 10, size, Charsets.UTF_8)
|
||||
ngramOrderStack.add(-1)
|
||||
ngramTreeStack.add(NgramTree())
|
||||
pos += (10 + size)
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
(cmd and MASK_END) == CMDB_END -> {
|
||||
if (pos == 0) {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_CMD_END,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
val n = (cmd and ATTR_END_COUNT)
|
||||
if (n > 0) {
|
||||
if (n <= ngramTreeStack.size) {
|
||||
for (c in 0 until n) {
|
||||
ngramOrderStack.removeLast()
|
||||
ngramTreeStack.removeLast()
|
||||
}
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size - n
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_CMD_END_ZERO_VALUE,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
pos += 1
|
||||
}
|
||||
else -> {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.INVALID_CMD_BYTE_PROVIDED,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
inputStream.close()
|
||||
|
||||
if (ngramTreeStack.size != 0) {
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF,
|
||||
address = pos, cmdByte = 0x00.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return Result.success(
|
||||
Flictionary(
|
||||
name = "flict",
|
||||
label = "flict",
|
||||
authors = listOf(),
|
||||
headerStr = headerStr ?: "",
|
||||
date = date,
|
||||
version = version,
|
||||
languageModel = FlorisLanguageModel(ngramTree)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDate(): Long = date
|
||||
|
||||
override fun getVersion(): Int = version
|
||||
|
||||
// TODO: preceding tokens are currently ignored
|
||||
override fun getTokenPredictions(
|
||||
precedingTokens: List<Token<String>>,
|
||||
currentToken: Token<String>?,
|
||||
maxSuggestionCount: Int,
|
||||
allowPossiblyOffensive: Boolean
|
||||
): List<WeightedToken<String, Int>> {
|
||||
currentToken ?: return listOf()
|
||||
|
||||
return if (currentToken.data.isNotEmpty()) {
|
||||
val retList = languageModel.matchAllNgrams(
|
||||
ngram = Ngram(
|
||||
_tokens = listOf(Token(currentToken.data.toLowerCase(Locale.ENGLISH))),
|
||||
_freq = -1
|
||||
),
|
||||
maxEditDistance = 2,
|
||||
maxTokenCount = maxSuggestionCount,
|
||||
allowPossiblyOffensive = allowPossiblyOffensive
|
||||
)
|
||||
retList
|
||||
} else {
|
||||
listOf()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A parse exception to be used by [Flictionary] to indicate where the parsing of a binary file
|
||||
* failed, while also providing some additional information.
|
||||
*/
|
||||
class ParseException(
|
||||
private val errorType: ErrorType,
|
||||
private val address: Int,
|
||||
private val cmdByte: Byte,
|
||||
private val absoluteDepth: Int
|
||||
) : Exception() {
|
||||
enum class ErrorType {
|
||||
UNSUPPORTED_FLICTIONARY_VERSION,
|
||||
UNEXPECTED_CMD_BEGIN_HEADER,
|
||||
UNEXPECTED_CMD_BEGIN_PTREE_NODE,
|
||||
UNEXPECTED_CMD_DEFINE_SHORTCUT,
|
||||
UNEXPECTED_CMD_END,
|
||||
UNEXPECTED_CMD_END_ZERO_VALUE,
|
||||
UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO,
|
||||
UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF,
|
||||
UNEXPECTED_EOF,
|
||||
INVALID_CMD_BYTE_PROVIDED,
|
||||
}
|
||||
|
||||
override val message: String
|
||||
get() = toString()
|
||||
override fun toString(): String {
|
||||
return StringBuilder().run {
|
||||
append(
|
||||
when (errorType) {
|
||||
ErrorType.UNSUPPORTED_FLICTIONARY_VERSION -> {
|
||||
"Unexpected Flictionary version: ${(cmdByte.toInt() and 0xFF) and ATTR_HEADER_VERSION}"
|
||||
}
|
||||
ErrorType.UNEXPECTED_CMD_BEGIN_HEADER -> {
|
||||
"Unexpected command: BEGIN_HEADER"
|
||||
}
|
||||
ErrorType.UNEXPECTED_CMD_BEGIN_PTREE_NODE -> {
|
||||
"Unexpected command: BEGIN_PTREE_NODE"
|
||||
}
|
||||
ErrorType.UNEXPECTED_CMD_DEFINE_SHORTCUT -> {
|
||||
"Unexpected command: DEFINE_SHORTCUT"
|
||||
}
|
||||
ErrorType.UNEXPECTED_CMD_END -> {
|
||||
"Unexpected command: END"
|
||||
}
|
||||
ErrorType.UNEXPECTED_CMD_END_ZERO_VALUE -> {
|
||||
"Unexpected zero value provided for command END"
|
||||
}
|
||||
ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO -> {
|
||||
"Unexpected decrease in absolute depth: cannot go below zero"
|
||||
}
|
||||
ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF -> {
|
||||
"Unexpected non-zero value in absolute depth at end of file"
|
||||
}
|
||||
ErrorType.UNEXPECTED_EOF -> {
|
||||
"Unexpected end of file while try to do look-ahead"
|
||||
}
|
||||
ErrorType.INVALID_CMD_BYTE_PROVIDED -> {
|
||||
"Invalid command byte provided"
|
||||
}
|
||||
}
|
||||
)
|
||||
append(
|
||||
String.format(
|
||||
"\n at address 0x%08X where cmd_byte=0x%02X and section_depth=%d",
|
||||
address,
|
||||
cmdByte,
|
||||
absoluteDepth
|
||||
)
|
||||
)
|
||||
toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next [len] bytes from the input stream into the given byte array [b]. This method guarantees to either
|
||||
* read the full length requested or if an EOF file is encountered, -1 is returned. The first byte written is at
|
||||
* `b[off]`, the second byte at `b[off+1]` and so on.
|
||||
*
|
||||
* @param b The byte array to read the next [len] bytes into.
|
||||
* @param off The offset of the first byte written in the byte array [b]. Must be non-negative.
|
||||
* @param len The number of bytes to read. Must be non-negative.
|
||||
*
|
||||
* @return The number of bytes read, always matching [len] or -1 if EOF was encountered.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if either [off] or [len] is negative or the byte array has insufficient space to
|
||||
* write the request [len] bytes into it.
|
||||
*/
|
||||
@Throws(IndexOutOfBoundsException::class)
|
||||
fun InputStream.readNext(b: ByteArray, off: Int, len: Int): Int {
|
||||
if (off < 0 || len < 0 || len > b.size - off) {
|
||||
throw IndexOutOfBoundsException()
|
||||
} else if (len == 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
var lenRead = 0
|
||||
while (lenRead < len) {
|
||||
val c = read()
|
||||
if (c == -1) {
|
||||
return -1
|
||||
} else {
|
||||
b[off + lenRead++] = c.toByte()
|
||||
}
|
||||
}
|
||||
return lenRead
|
||||
}
|
||||
@@ -17,8 +17,6 @@
|
||||
package dev.patrickgold.florisboard.ime.extension
|
||||
|
||||
import android.content.Context
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Result
|
||||
|
||||
/**
|
||||
* Interface for an Asset to use within FlorisBoard. An asset is everything from a dictionary to a
|
||||
@@ -61,6 +59,6 @@ interface Asset {
|
||||
/**
|
||||
* Loads an Asset of type [T] from the specified path.
|
||||
*/
|
||||
fun fromFile(context: Context, path: String): Result<T, Throwable> = Err(NotImplementedError())
|
||||
fun fromFile(context: Context, path: String): Result<T> = Result.failure(NotImplementedError())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,17 +17,15 @@
|
||||
package dev.patrickgold.florisboard.ime.extension
|
||||
|
||||
import android.content.Context
|
||||
import com.github.michaelbull.result.*
|
||||
import android.net.Uri
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupExtension
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyTypeAdapter
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariationAdapter
|
||||
import dev.patrickgold.florisboard.ime.text.layout.LayoutTypeAdapter
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class AssetManager private constructor(private val applicationContext: Context) {
|
||||
private val moshi: Moshi = Moshi.Builder()
|
||||
@@ -62,22 +60,22 @@ class AssetManager private constructor(private val applicationContext: Context)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAsset(ref: AssetRef): Result<Nothing?, Throwable> {
|
||||
fun deleteAsset(ref: AssetRef): Result<Unit> {
|
||||
return when (ref.source) {
|
||||
AssetSource.Internal -> {
|
||||
val file = File(applicationContext.filesDir.absolutePath + "/" + ref.path)
|
||||
if (file.isFile) {
|
||||
val success = file.delete()
|
||||
if (success) {
|
||||
Ok(null)
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Err(Exception("Could not delete file."))
|
||||
Result.failure(Exception("Could not delete file."))
|
||||
}
|
||||
} else {
|
||||
Err(Exception("Provided reference is not a file."))
|
||||
Result.failure(Exception("Provided reference is not a file."))
|
||||
}
|
||||
}
|
||||
else -> Err(Exception("Can not delete an asset in source '${ref.source}'"))
|
||||
else -> Result.failure(Exception("Can not delete an asset in source '${ref.source}'"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +98,7 @@ class AssetManager private constructor(private val applicationContext: Context)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T: Asset> listAssets(ref: AssetRef, assetClass: Class<T>): Result<Map<AssetRef, T>, Throwable> {
|
||||
fun <T : Asset> listAssets(ref: AssetRef, assetClass: KClass<T>): Result<Map<AssetRef, T>> {
|
||||
val retMap = mutableMapOf<AssetRef, T>()
|
||||
return when (ref.source) {
|
||||
AssetSource.Assets -> {
|
||||
@@ -117,9 +115,9 @@ class AssetManager private constructor(private val applicationContext: Context)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(retMap.toMap())
|
||||
Result.success(retMap.toMap())
|
||||
} catch (e: Exception) {
|
||||
Err(e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
AssetSource.Internal -> {
|
||||
@@ -139,19 +137,19 @@ class AssetManager private constructor(private val applicationContext: Context)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(retMap.toMap())
|
||||
Result.success(retMap.toMap())
|
||||
}
|
||||
else -> Ok(retMap.toMap())
|
||||
else -> Result.success(retMap.toMap())
|
||||
}
|
||||
}
|
||||
|
||||
fun <T: Asset> loadAsset(ref: AssetRef, assetClass: Class<T>): Result<T, Throwable> {
|
||||
fun <T : Asset> loadAsset(ref: AssetRef, assetClass: KClass<T>): Result<T> {
|
||||
val rawJsonData = when (ref.source) {
|
||||
is AssetSource.Assets -> {
|
||||
try {
|
||||
applicationContext.assets.open(ref.path).bufferedReader().use { it.readText() }
|
||||
} catch (e: Exception) {
|
||||
return Err(e)
|
||||
return Result.failure(e)
|
||||
}
|
||||
}
|
||||
is AssetSource.Internal -> {
|
||||
@@ -166,28 +164,68 @@ class AssetManager private constructor(private val applicationContext: Context)
|
||||
else -> "{}"
|
||||
}
|
||||
return try {
|
||||
val adapter = moshi.adapter(assetClass)
|
||||
val adapter = moshi.adapter(assetClass.java)
|
||||
val asset = adapter.fromJson(rawJsonData)
|
||||
if (asset != null) {
|
||||
Ok(asset)
|
||||
Result.success(asset)
|
||||
} else {
|
||||
Err(NullPointerException("Asset failed to load!"))
|
||||
Result.failure(NullPointerException("Asset failed to load!"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Err(e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T: Asset> writeAsset(ref: AssetRef, assetClass: Class<T>, asset: T): Result<Boolean, Throwable> {
|
||||
fun <T : Asset> loadAsset(uri: Uri, assetClass: KClass<T>): Result<T> {
|
||||
val rawJsonData = ExternalContentUtils.readTextFromUri(applicationContext, uri).onFailure {
|
||||
return Result.failure(it)
|
||||
}
|
||||
return try {
|
||||
val adapter = moshi.adapter(assetClass.java)
|
||||
val asset = adapter.fromJson(rawJsonData.getOrNull()!!)
|
||||
if (asset != null) {
|
||||
Result.success(asset)
|
||||
} else {
|
||||
Result.failure(NullPointerException("Asset failed to load!"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun loadAssetRaw(ref: AssetRef): Result<String> {
|
||||
return when (ref.source) {
|
||||
is AssetSource.Assets -> {
|
||||
try {
|
||||
Result.success(applicationContext.assets.open(ref.path).bufferedReader().use { it.readText() })
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
is AssetSource.Internal -> {
|
||||
val file = File(applicationContext.filesDir.absolutePath + "/" + ref.path)
|
||||
val contents = readFile(file)
|
||||
if (contents.isBlank()) {
|
||||
Result.failure(Exception("File is blank!"))
|
||||
} else {
|
||||
Result.success(contents)
|
||||
}
|
||||
}
|
||||
else -> Result.failure(Exception("Unsupported asset ref!"))
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Asset> writeAsset(ref: AssetRef, assetClass: KClass<T>, asset: T): Result<Unit> {
|
||||
return when (ref.source) {
|
||||
AssetSource.Internal -> {
|
||||
val adapter = moshi.adapter(assetClass)
|
||||
val adapter = moshi.adapter(assetClass.java)
|
||||
val rawJson = adapter.toJson(asset)
|
||||
val file = File(applicationContext.filesDir.absolutePath + "/" + ref.path)
|
||||
writeToFile(file, rawJson)
|
||||
Ok(true)
|
||||
Result.success(Unit)
|
||||
}
|
||||
else -> Err(Exception("Can not write an asset in source '${ref.source}'"))
|
||||
else -> Result.failure(Exception("Can not write an asset in source '${ref.source}'"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.extension
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.github.michaelbull.result.getOrElse
|
||||
|
||||
/**
|
||||
* Data class which is a reference to an asset file. It indicates in which storage medium the asset
|
||||
* is as well as the relative path to it.
|
||||
@@ -36,15 +31,15 @@ data class AssetRef(
|
||||
companion object {
|
||||
private const val DELIMITER: String = ":"
|
||||
|
||||
fun fromString(str: String): Result<AssetRef, String> {
|
||||
fun fromString(str: String): Result<AssetRef> {
|
||||
val items = str.split(DELIMITER)
|
||||
if (items.size != 2) {
|
||||
return Err("Unexpected length of given asset ref. Make sure that the asset ref string contains exactly 2 items separated by '$DELIMITER'!")
|
||||
return Result.failure(Exception("Unexpected length of given asset ref. Make sure that the asset ref string contains exactly 2 items separated by '$DELIMITER'!"))
|
||||
}
|
||||
val retSource = AssetSource.fromString(items[0]).getOrElse {
|
||||
return Err(it)
|
||||
return Result.failure(Exception(it))
|
||||
}
|
||||
return Ok(AssetRef(retSource, items[1]))
|
||||
return Result.success(AssetRef(retSource, items[1]))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.extension
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -52,16 +49,16 @@ sealed class AssetSource {
|
||||
companion object {
|
||||
private val externalRegex: Regex = """^external\\(([a-z]+\\.)*[a-z]+\\)\$""".toRegex()
|
||||
|
||||
fun fromString(str: String): Result<AssetSource, String> {
|
||||
fun fromString(str: String): Result<AssetSource> {
|
||||
return when (val string = str.toLowerCase(Locale.ENGLISH)) {
|
||||
"assets" -> Ok(Assets)
|
||||
"internal" -> Ok(Internal)
|
||||
"assets" -> Result.success(Assets)
|
||||
"internal" -> Result.success(Internal)
|
||||
else -> {
|
||||
if (string.matches(externalRegex)) {
|
||||
val packageName = string.substring(9, string.length - 1)
|
||||
Ok(External(packageName))
|
||||
Result.success(External(packageName))
|
||||
} else {
|
||||
Err("'$str' is not a valid AssetSource.")
|
||||
Result.failure(Exception("'$str' is not a valid AssetSource."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.extension
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
|
||||
class ExternalContentUtils private constructor() {
|
||||
companion object {
|
||||
fun readTextFromUri(context: Context, uri: Uri): Result<String> {
|
||||
val contentResolver = context.contentResolver
|
||||
?: return Result.failure(NullPointerException("System content resolver not available"))
|
||||
val inputStream = contentResolver.openInputStream(uri)
|
||||
?: return Result.failure(NullPointerException("Cannot open input stream for given uri '$uri'"))
|
||||
val rawText = inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
|
||||
return Result.success(rawText)
|
||||
}
|
||||
|
||||
fun writeTextToUri(context: Context, uri: Uri, text: String): Result<Unit> {
|
||||
val contentResolver = context.contentResolver
|
||||
?: return Result.failure(NullPointerException("System content resolver not available"))
|
||||
// Must use "rwt" mode to ensure destination file length is truncated after writing.
|
||||
val outputStream = contentResolver.openOutputStream(uri, "rwt")
|
||||
?: return Result.failure(NullPointerException("Cannot open output stream for given uri '$uri'"))
|
||||
outputStream.bufferedWriter(Charsets.UTF_8).use { it.flush(); it.write(text) }
|
||||
return Result.success(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.landscapeinput
|
||||
|
||||
import java.util.*
|
||||
|
||||
enum class LandscapeInputUiMode {
|
||||
DYNAMICALLY_SHOW,
|
||||
NEVER_SHOW,
|
||||
ALWAYS_SHOW;
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String): LandscapeInputUiMode {
|
||||
return valueOf(string.toUpperCase(Locale.ENGLISH))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
package dev.patrickgold.florisboard.ime.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Handler
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
@@ -25,16 +24,13 @@ import com.google.android.material.tabs.TabLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.EditorInstance
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
|
||||
import dev.patrickgold.florisboard.ime.core.InputView
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyboardView
|
||||
import dev.patrickgold.florisboard.ime.media.emoticon.EmoticonKeyData
|
||||
import dev.patrickgold.florisboard.ime.media.emoticon.EmoticonKeyboardView
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyType
|
||||
import dev.patrickgold.florisboard.util.cancelAll
|
||||
import dev.patrickgold.florisboard.util.postAtScheduledRate
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
@@ -58,11 +54,10 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
|
||||
private var activeTab: Tab? = null
|
||||
private var mediaViewFlipper: ViewFlipper? = null
|
||||
private var repeatedKeyPressHandler: Handler? = null
|
||||
private var tabLayout: TabLayout? = null
|
||||
private val tabViews = EnumMap<Tab, LinearLayout>(Tab::class.java)
|
||||
|
||||
var mediaViewGroup: LinearLayout? = null
|
||||
private var mediaViewGroup: LinearLayout? = null
|
||||
|
||||
companion object {
|
||||
private var instance: MediaInputManager? = null
|
||||
@@ -80,11 +75,6 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
florisboard.addEventListener(this)
|
||||
}
|
||||
|
||||
override fun onCreateInputView() {
|
||||
super.onCreateInputView()
|
||||
repeatedKeyPressHandler = Handler(florisboard.context.mainLooper)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new input view has been registered. Used to initialize all media-relevant
|
||||
* views and layouts.
|
||||
@@ -145,30 +135,21 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
private fun onBottomButtonEvent(view: View, event: MotionEvent?): Boolean {
|
||||
event ?: return false
|
||||
val data = when (view.id) {
|
||||
R.id.media_input_switch_to_text_input_button -> {
|
||||
KeyData(code = KeyCode.SWITCH_TO_TEXT_CONTEXT)
|
||||
}
|
||||
R.id.media_input_backspace_button -> {
|
||||
KeyData(code = KeyCode.DELETE, type = KeyType.ENTER_EDITING)
|
||||
}
|
||||
else -> null
|
||||
R.id.media_input_switch_to_text_input_button -> KeyData.SWITCH_TO_TEXT_CONTEXT
|
||||
R.id.media_input_backspace_button -> KeyData.DELETE
|
||||
else -> return false
|
||||
}
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
florisboard.keyPressVibrate()
|
||||
florisboard.keyPressSound(data)
|
||||
if (data?.code == KeyCode.DELETE && data.type == KeyType.ENTER_EDITING) {
|
||||
val delayMillis = florisboard.prefs.keyboard.longPressDelay.toLong()
|
||||
repeatedKeyPressHandler?.postAtScheduledRate(delayMillis, 25) {
|
||||
florisboard.textInputManager.sendKeyPress(data)
|
||||
}
|
||||
}
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.down(data))
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
repeatedKeyPressHandler?.cancelAll()
|
||||
if (event.actionMasked != MotionEvent.ACTION_CANCEL && data != null) {
|
||||
florisboard.textInputManager.sendKeyPress(data)
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.up(data))
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
}
|
||||
// MUST return false here so the background selector for showing a transparent bg works
|
||||
|
||||
@@ -43,12 +43,11 @@ class MediaInputView : LinearLayout, FlorisBoard.EventListener,
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
florisboard?.addEventListener(this)
|
||||
}
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
florisboard?.addEventListener(this)
|
||||
themeManager.registerOnThemeUpdatedListener(this)
|
||||
tabLayout = findViewById(R.id.media_input_tabs)
|
||||
switchToTextInputButton = findViewById(R.id.media_input_switch_to_text_input_button)
|
||||
@@ -57,8 +56,9 @@ class MediaInputView : LinearLayout, FlorisBoard.EventListener,
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
florisboard?.removeEventListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.media.emoji
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Enum for emoji category.
|
||||
@@ -38,9 +38,8 @@ enum class EmojiCategory {
|
||||
}
|
||||
|
||||
companion object {
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun fromString(string: String): EmojiCategory {
|
||||
return valueOf(string.replace(" & ", "_").toUpperCase())
|
||||
return valueOf(string.replace(" & ", "_").toUpperCase(Locale.ENGLISH))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
|
||||
/**
|
||||
* View class for managing the rendering and the events of a single emoji keyboard key.
|
||||
@@ -46,7 +48,7 @@ import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
class EmojiKeyView(
|
||||
private val emojiKeyboardView: EmojiKeyboardView,
|
||||
val data: EmojiKeyData
|
||||
) : androidx.appcompat.widget.AppCompatTextView(emojiKeyboardView.context),
|
||||
) : androidx.appcompat.widget.AppCompatTextView(emojiKeyboardView.context), CoroutineScope by MainScope(),
|
||||
FlorisBoard.EventListener, ThemeManager.OnThemeUpdatedListener {
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
@@ -64,13 +66,16 @@ class EmojiKeyView(
|
||||
triangleDrawable = ContextCompat.getDrawable(context, R.drawable.triangle_bottom_right)
|
||||
|
||||
text = data.getCodePointsAsString()
|
||||
|
||||
florisboard?.addEventListener(this)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
onApplyThemeAttributes()
|
||||
florisboard?.addEventListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
florisboard?.removeEventListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -101,11 +101,11 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener,
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||
})
|
||||
addView(tabLayout)
|
||||
florisboard?.addEventListener(this)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
florisboard?.addEventListener(this)
|
||||
themeManager.registerOnThemeUpdatedListener(this)
|
||||
mainScope.launch {
|
||||
layouts.await()
|
||||
@@ -116,8 +116,9 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener,
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
florisboard?.removeEventListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,13 +19,11 @@ package dev.patrickgold.florisboard.ime.media.emoji
|
||||
import android.content.Context
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import android.util.Log
|
||||
import androidx.core.graphics.PaintCompat
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.lang.Exception
|
||||
import java.util.*
|
||||
|
||||
private const val GROUP_IDENTIFIER = "# group: "
|
||||
@@ -71,6 +69,8 @@ private fun listStringToListInt(list: List<String>): List<Int> {
|
||||
return ret.toList()
|
||||
}
|
||||
|
||||
private var cachedEmojiLayoutMap: EmojiLayoutDataMap? = null
|
||||
|
||||
/**
|
||||
* Reads the emoji list at the given [path] and returns an parsed [EmojiLayoutDataMap]. If the
|
||||
* given file path does not exist, an empty [EmojiLayoutDataMap] is returned.
|
||||
@@ -84,6 +84,7 @@ private fun listStringToListInt(list: List<String>): List<Int> {
|
||||
fun parseRawEmojiSpecsFile(
|
||||
context: Context, path: String
|
||||
): EmojiLayoutDataMap {
|
||||
cachedEmojiLayoutMap?.let { return it }
|
||||
val layouts = EmojiLayoutDataMap(EmojiCategory::class.java)
|
||||
for (category in EmojiCategory.values()) {
|
||||
layouts[category] = mutableListOf()
|
||||
@@ -182,5 +183,6 @@ fun parseRawEmojiSpecsFile(
|
||||
}
|
||||
}
|
||||
}
|
||||
cachedEmojiLayoutMap = layouts
|
||||
return layouts
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
/**
|
||||
* Represents the root node to a n-gram tree.
|
||||
*/
|
||||
open class NgramTree(
|
||||
sameOrderChildren: MutableList<NgramNode> = mutableListOf(),
|
||||
higherOrderChildren: MutableList<NgramNode> = mutableListOf()
|
||||
) : NgramNode(0, '?', -1, sameOrderChildren, higherOrderChildren)
|
||||
|
||||
/**
|
||||
* A node of a n-gram tree, which holds the character it represents, the corresponding frequency,
|
||||
* a pre-computed string representing all parent characters and the current one as well as child
|
||||
* nodes, one for the same order n-gram nodes and one for the higher order n-gram nodes.
|
||||
*/
|
||||
open class NgramNode(
|
||||
val order: Int,
|
||||
val char: Char,
|
||||
val freq: Int,
|
||||
val sameOrderChildren: MutableList<NgramNode> = mutableListOf(),
|
||||
val higherOrderChildren: MutableList<NgramNode> = mutableListOf()
|
||||
) {
|
||||
companion object {
|
||||
const val FREQ_CHARACTER = -1
|
||||
const val FREQ_WORD_MIN = 0
|
||||
const val FREQ_WORD_MAX = 255
|
||||
const val FREQ_WORD_FILLER = -2
|
||||
const val FREQ_IS_POSSIBLY_OFFENSIVE = 0
|
||||
}
|
||||
|
||||
val isCharacter: Boolean
|
||||
get() = freq == FREQ_CHARACTER
|
||||
|
||||
val isWord: Boolean
|
||||
get() = freq in FREQ_WORD_MIN..FREQ_WORD_MAX
|
||||
|
||||
val isWordFiller: Boolean
|
||||
get() = freq == FREQ_WORD_FILLER
|
||||
|
||||
val isPossiblyOffensive: Boolean
|
||||
get() = freq == FREQ_IS_POSSIBLY_OFFENSIVE
|
||||
|
||||
fun findWord(word: String): NgramNode? {
|
||||
var currentNode = this
|
||||
for ((pos, char) in word.withIndex()) {
|
||||
val childNode = if (pos == 0) {
|
||||
currentNode.higherOrderChildren.find { it.char == char }
|
||||
} else {
|
||||
currentNode.sameOrderChildren.find { it.char == char }
|
||||
}
|
||||
if (childNode != null) {
|
||||
currentNode = childNode
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return if (currentNode.isWord || currentNode.isWordFiller) {
|
||||
currentNode
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function allows to search for a given [input] word with a given [maxEditDistance] and
|
||||
* adds all matches in the trie to the [list].
|
||||
*/
|
||||
fun listSimilarWords(
|
||||
input: String,
|
||||
list: StagedSuggestionList<String, Int>,
|
||||
word: StringBuilder,
|
||||
allowPossiblyOffensive: Boolean,
|
||||
maxEditDistance: Int,
|
||||
deletionCost: Int = 0,
|
||||
insertionCost: Int = 0,
|
||||
substitutionCost: Int = 0,
|
||||
pos: Int = -1
|
||||
) {
|
||||
if (pos > -1) {
|
||||
word.append(char)
|
||||
}
|
||||
val costSum = deletionCost + insertionCost + substitutionCost
|
||||
if (pos > -1 && (pos + 1 == input.length) && isWord && ((isPossiblyOffensive && allowPossiblyOffensive)
|
||||
|| !isPossiblyOffensive)) {
|
||||
// Using shift right instead of divide by 2^(costSum) as it is mathematically the
|
||||
// same but faster.
|
||||
if (list.canAdd(freq shr costSum)) {
|
||||
list.add(word.toString(), freq shr costSum)
|
||||
}
|
||||
}
|
||||
if (pos <= -1) {
|
||||
for (childNode in higherOrderChildren) {
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance, 0, 0, 0, 0
|
||||
)
|
||||
}
|
||||
} else if (maxEditDistance == costSum) {
|
||||
if (pos + 1 < input.length) {
|
||||
sameOrderChildren.find { it.char == input[pos + 1] }?.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost, substitutionCost, pos + 1
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Delete
|
||||
if (pos + 2 < input.length) {
|
||||
sameOrderChildren.find { it.char == input[pos + 2] }?.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost + 1, insertionCost, substitutionCost, pos + 2
|
||||
)
|
||||
}
|
||||
for (childNode in sameOrderChildren) {
|
||||
if (pos + 1 < input.length && childNode.char == input[pos + 1]) {
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost, substitutionCost, pos + 1
|
||||
)
|
||||
} else {
|
||||
// Insert
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost + 1, substitutionCost, pos
|
||||
)
|
||||
if (pos + 1 < input.length) {
|
||||
// Substitute
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost, substitutionCost + 1, pos + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pos > -1) {
|
||||
word.deleteAt(word.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun listAllSameOrderWords(list: StagedSuggestionList<String, Int>, word: StringBuilder, allowPossiblyOffensive: Boolean) {
|
||||
word.append(char)
|
||||
if (isWord && ((isPossiblyOffensive && allowPossiblyOffensive) || !isPossiblyOffensive)) {
|
||||
if (list.canAdd(freq)) {
|
||||
list.add(word.toString(), freq)
|
||||
}
|
||||
}
|
||||
for (childNode in sameOrderChildren) {
|
||||
childNode.listAllSameOrderWords(list, word, allowPossiblyOffensive)
|
||||
}
|
||||
word.deleteAt(word.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
open class FlorisLanguageModel(
|
||||
initTreeObj: NgramTree? = null
|
||||
) : LanguageModel<String, Int> {
|
||||
protected val ngramTree: NgramTree = initTreeObj ?: NgramTree()
|
||||
|
||||
override fun getNgram(vararg tokens: String): Ngram<String, Int> {
|
||||
val ngramOut = getNgramOrNull(*tokens)
|
||||
if (ngramOut != null) {
|
||||
return ngramOut
|
||||
} else {
|
||||
throw NullPointerException("No n-gram found matching the given tokens: $tokens")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNgram(ngram: Ngram<String, Int>): Ngram<String, Int> {
|
||||
val ngramOut = getNgramOrNull(ngram)
|
||||
if (ngramOut != null) {
|
||||
return ngramOut
|
||||
} else {
|
||||
throw NullPointerException("No n-gram found matching the given ngram: $ngram")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNgramOrNull(vararg tokens: String): Ngram<String, Int>? {
|
||||
var currentNode: NgramNode = ngramTree
|
||||
for (token in tokens) {
|
||||
val childNode = currentNode.findWord(token)
|
||||
if (childNode != null) {
|
||||
currentNode = childNode
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return Ngram(tokens.toList().map { Token(it) }, currentNode.freq)
|
||||
}
|
||||
|
||||
override fun getNgramOrNull(ngram: Ngram<String, Int>): Ngram<String, Int>? {
|
||||
return getNgramOrNull(*ngram.tokens.toStringList().toTypedArray())
|
||||
}
|
||||
|
||||
override fun hasNgram(ngram: Ngram<String, Int>, doMatchFreq: Boolean): Boolean {
|
||||
val result = getNgramOrNull(ngram)
|
||||
return if (result != null) {
|
||||
if (doMatchFreq) {
|
||||
ngram.freq == result.freq
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun matchAllNgrams(
|
||||
ngram: Ngram<String, Int>,
|
||||
maxEditDistance: Int,
|
||||
maxTokenCount: Int,
|
||||
allowPossiblyOffensive: Boolean
|
||||
): List<WeightedToken<String, Int>> {
|
||||
val ngramList = mutableListOf<WeightedToken<String, Int>>()
|
||||
var currentNode: NgramNode = ngramTree
|
||||
for ((t, token) in ngram.tokens.withIndex()) {
|
||||
val word = token.data
|
||||
if (t + 1 >= ngram.tokens.size) {
|
||||
if (word.isNotEmpty()) {
|
||||
// The last word is not complete, so find all possible words and sort
|
||||
val splitWord = mutableListOf<Char>()
|
||||
var splitNode: NgramNode? = currentNode
|
||||
for ((pos, char) in word.withIndex()) {
|
||||
val node = if (pos == 0) {
|
||||
splitNode?.higherOrderChildren?.find { it.char == char }
|
||||
} else {
|
||||
splitNode?.sameOrderChildren?.find { it.char == char }
|
||||
}
|
||||
splitWord.add(char)
|
||||
splitNode = node
|
||||
if (node == null) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (splitNode != null) {
|
||||
// Input thus far is valid
|
||||
val wordNodes = StagedSuggestionList<String, Int>(maxTokenCount)
|
||||
val strBuilder = StringBuilder().append(word.substring(0, word.length - 1))
|
||||
splitNode.listAllSameOrderWords(wordNodes, strBuilder, allowPossiblyOffensive)
|
||||
ngramList.addAll(wordNodes)
|
||||
}
|
||||
if (ngramList.size < maxTokenCount) {
|
||||
val wordNodes = StagedSuggestionList<String, Int>(maxTokenCount)
|
||||
val strBuilder = StringBuilder()
|
||||
currentNode.listSimilarWords(word, wordNodes, strBuilder, allowPossiblyOffensive, maxEditDistance)
|
||||
ngramList.addAll(wordNodes)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val node = currentNode.findWord(word)
|
||||
if (node == null) {
|
||||
return ngramList
|
||||
} else {
|
||||
currentNode = node
|
||||
}
|
||||
}
|
||||
}
|
||||
return ngramList
|
||||
}
|
||||
|
||||
fun toFlorisMutableLanguageModel(): FlorisMutableLanguageModel = FlorisMutableLanguageModel(ngramTree)
|
||||
}
|
||||
|
||||
open class FlorisMutableLanguageModel(
|
||||
initTreeObj: NgramTree? = null
|
||||
) : MutableLanguageModel<String, Int>, FlorisLanguageModel(initTreeObj) {
|
||||
override fun deleteNgram(ngram: Ngram<String, Int>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun insertNgram(ngram: Ngram<String, Int>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun updateNgram(ngram: Ngram<String, Int>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
fun toFlorisLanguageModel(): FlorisLanguageModel = FlorisLanguageModel(ngramTree)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
/**
|
||||
* Abstract interface for a language model. Can house any n-grams with a minimum order of one.
|
||||
*/
|
||||
interface LanguageModel<T : Any, F : Comparable<F>> {
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [tokens]. Throws a NPE if no match could be found.
|
||||
*/
|
||||
@Throws(NullPointerException::class)
|
||||
fun getNgram(vararg tokens: T): Ngram<T, F>
|
||||
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [ngram], whereas the frequency is ignored while
|
||||
* searching. Throws a NPE if no match could be found.
|
||||
*/
|
||||
@Throws(NullPointerException::class)
|
||||
fun getNgram(ngram: Ngram<T, F>): Ngram<T, F>
|
||||
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [tokens]. Returns null if no match could be found.
|
||||
*/
|
||||
fun getNgramOrNull(vararg tokens: T): Ngram<T, F>?
|
||||
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [ngram], whereas the frequency is ignored while
|
||||
* searching. Returns null if no match could be found.
|
||||
*/
|
||||
fun getNgramOrNull(ngram: Ngram<T, F>): Ngram<T, F>?
|
||||
|
||||
/**
|
||||
* Checks if a given [ngram] exists within this model. If [doMatchFreq] is set to true, the
|
||||
* frequency is also matched.
|
||||
*/
|
||||
fun hasNgram(ngram: Ngram<T, F>, doMatchFreq: Boolean = false): Boolean
|
||||
|
||||
/**
|
||||
* Matches all n-grams which match the given [ngram], whereas the last item in the n-gram is
|
||||
* is used to search for predictions.
|
||||
*/
|
||||
fun matchAllNgrams(
|
||||
ngram: Ngram<T, F>,
|
||||
maxEditDistance: Int,
|
||||
maxTokenCount: Int,
|
||||
allowPossiblyOffensive: Boolean
|
||||
): List<WeightedToken<T, F>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutable version of [LanguageModel].
|
||||
*/
|
||||
interface MutableLanguageModel<T : Any, F : Comparable<F>> : LanguageModel<T, F> {
|
||||
fun deleteNgram(ngram: Ngram<T, F>)
|
||||
|
||||
fun insertNgram(ngram: Ngram<T, F>)
|
||||
|
||||
fun updateNgram(ngram: Ngram<T, F>)
|
||||
}
|
||||
129
app/src/main/java/dev/patrickgold/florisboard/ime/nlp/Ngram.kt
Normal file
129
app/src/main/java/dev/patrickgold/florisboard/ime/nlp/Ngram.kt
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
/**
|
||||
* Abstract interface representing a n-gram of tokens. Each n-gram instance can be assigned a
|
||||
* unique frequency [freq].
|
||||
*/
|
||||
open class Ngram<T : Any, F : Comparable<F>>(_tokens: List<Token<T>>, _freq: F) {
|
||||
companion object {
|
||||
/** Constant order value for unigrams. */
|
||||
const val ORDER_UNIGRAM: Int = 1
|
||||
|
||||
/** Constant order value for bigrams. */
|
||||
const val ORDER_BIGRAM: Int = 2
|
||||
|
||||
/** Constant order value for trigrams. */
|
||||
const val ORDER_TRIGRAM: Int = 3
|
||||
}
|
||||
|
||||
init {
|
||||
if (_tokens.size < ORDER_UNIGRAM) {
|
||||
throw Exception("A n-gram must contain at least 1 token!")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of tokens for this n-gram. The length of this list is guaranteed to be matching
|
||||
* [order].
|
||||
*/
|
||||
val tokens: List<Token<T>> = _tokens
|
||||
|
||||
/**
|
||||
* The frequency value of this n-gram.
|
||||
*/
|
||||
val freq: F = _freq
|
||||
|
||||
/**
|
||||
* The order of this n-gram (1, 2, 3, ...).
|
||||
*/
|
||||
val order: Int
|
||||
get() = tokens.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract interface representing a token used in [Ngram].
|
||||
*/
|
||||
open class Token<T : Any>(_data: T) {
|
||||
/**
|
||||
* The data of this token.
|
||||
*/
|
||||
val data: T = _data
|
||||
|
||||
override fun toString(): String {
|
||||
return "Token(\"$data\")"
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return data.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Token<*>
|
||||
|
||||
if (data != other.data) return false
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as [Token] but allows to add a frequency value [freq].
|
||||
*/
|
||||
open class WeightedToken<T : Any, F : Comparable<F>>(_data: T, _freq: F) : Token<T>(_data) {
|
||||
/**
|
||||
* The frequency of this weighed token.
|
||||
*/
|
||||
val freq: F = _freq
|
||||
|
||||
override fun toString(): String {
|
||||
return "WeightedToken(\"$data\", $freq)"
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return data.hashCode() + 31 * freq.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as WeightedToken<*, *>
|
||||
|
||||
if (data != other.data || freq != other.freq) return false
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of tokens carrying [CharSequence] data to a list of [CharSequence].
|
||||
*/
|
||||
fun List<Token<CharSequence>>.toCharSequenceList(): List<CharSequence> {
|
||||
return this.map { it.data }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of tokens carrying [String] data to a list of [String].
|
||||
*/
|
||||
fun List<Token<String>>.toStringList(): List<String> {
|
||||
return this.map { it.data }
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
class StagedSuggestionList<T : Any, F : Comparable<F>>(
|
||||
private val maxSize: Int
|
||||
) : Collection<WeightedToken<T, F>> {
|
||||
private val internalArray: Array<WeightedToken<T, F>?> = Array(maxSize) { null }
|
||||
private var internalSize: Int = 0
|
||||
|
||||
override val size: Int
|
||||
get() = internalSize
|
||||
|
||||
fun add(token: T, freq: F): Boolean {
|
||||
if (internalSize < maxSize) {
|
||||
internalArray[internalSize++] = WeightedToken(token, freq)
|
||||
internalArray.sortByDescending { it?.freq }
|
||||
return true
|
||||
} else {
|
||||
if (internalArray.last()!!.freq < freq) {
|
||||
internalArray[internalArray.lastIndex] = WeightedToken(token, freq)
|
||||
internalArray.sortByDescending { it?.freq }
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun canAdd(freq: F): Boolean {
|
||||
return internalSize < maxSize || internalArray.last()!!.freq < freq
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
for (n in internalArray.indices) {
|
||||
internalArray[n] = null
|
||||
}
|
||||
internalSize = 0
|
||||
}
|
||||
|
||||
override fun contains(element: WeightedToken<T, F>): Boolean = internalArray.contains(element)
|
||||
|
||||
override fun containsAll(elements: Collection<WeightedToken<T, F>>): Boolean {
|
||||
elements.forEach { if (!contains(it)) return false }
|
||||
return true
|
||||
}
|
||||
|
||||
@Throws(IndexOutOfBoundsException::class)
|
||||
operator fun get(index: Int): WeightedToken<T, F> {
|
||||
val element = getOrNull(index)
|
||||
if (element == null) {
|
||||
throw IndexOutOfBoundsException("The specified index $index is not within the bounds of this list!")
|
||||
} else {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrNull(index: Int): WeightedToken<T, F>? {
|
||||
return internalArray.getOrNull(index)
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean = internalSize <= 0
|
||||
|
||||
override fun iterator(): Iterator<WeightedToken<T, F>> {
|
||||
return StagedIterator(this)
|
||||
}
|
||||
|
||||
class StagedIterator<T : Any, F : Comparable<F>> internal constructor (
|
||||
private val stagedSuggestionList: StagedSuggestionList<T, F>
|
||||
) : Iterator<WeightedToken<T, F>> {
|
||||
var index = 0
|
||||
|
||||
override fun next(): WeightedToken<T, F> = stagedSuggestionList[index++]
|
||||
|
||||
override fun hasNext(): Boolean = stagedSuggestionList.getOrNull(index) != null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
class TextProcessor {
|
||||
data class Word(
|
||||
val word: String,
|
||||
val isPossiblyOffensive: Boolean = false
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.onehanded
|
||||
|
||||
/**
|
||||
* Static object which contains all possible one-handed mode strings.
|
||||
*/
|
||||
object OneHandedMode {
|
||||
const val OFF: String = "off"
|
||||
const val START: String = "start"
|
||||
const val END: String = "end"
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.onehanded
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
|
||||
class OneHandedPanel : LinearLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
private var florisboard: FlorisBoard? = null
|
||||
private var themeManager: ThemeManager? = null
|
||||
|
||||
private var closeBtn: ImageButton? = null
|
||||
private var moveBtn: ImageButton? = null
|
||||
|
||||
private val panelSide: String
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
context.obtainStyledAttributes(attrs, R.styleable.OneHandedPanel).apply {
|
||||
panelSide = getString(R.styleable.OneHandedPanel_panelSide) ?: OneHandedMode.START
|
||||
recycle()
|
||||
}
|
||||
orientation = VERTICAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
florisboard = FlorisBoard.getInstanceOrNull()
|
||||
themeManager = ThemeManager.defaultOrNull()
|
||||
|
||||
closeBtn = findViewWithTag("one_handed_ctrl_close")
|
||||
closeBtn?.setOnClickListener {
|
||||
florisboard?.let {
|
||||
it.prefs.keyboard.oneHandedMode = OneHandedMode.OFF
|
||||
it.updateOneHandedPanelVisibility()
|
||||
}
|
||||
}
|
||||
moveBtn = findViewWithTag("one_handed_ctrl_move")
|
||||
moveBtn?.setOnClickListener {
|
||||
florisboard?.let {
|
||||
it.prefs.keyboard.oneHandedMode = panelSide
|
||||
it.updateOneHandedPanelVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
themeManager?.registerOnThemeUpdatedListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
florisboard = null
|
||||
themeManager?.unregisterOnThemeUpdatedListener(this)
|
||||
themeManager = null
|
||||
|
||||
closeBtn?.setOnClickListener(null)
|
||||
closeBtn = null
|
||||
moveBtn?.setOnClickListener(null)
|
||||
moveBtn = null
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
setBackgroundColor(theme.getAttr(Theme.Attr.ONE_HANDED_BACKGROUND).toSolidColor().color)
|
||||
ColorStateList.valueOf(theme.getAttr(Theme.Attr.ONE_HANDED_FOREGROUND).toSolidColor().color).also {
|
||||
closeBtn?.imageTintList = it
|
||||
moveBtn?.imageTintList = it
|
||||
}
|
||||
closeBtn?.invalidate()
|
||||
moveBtn?.invalidate()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val florisboard = florisboard ?: return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
val width = (florisboard.inputView?.measuredWidth ?: 0) *
|
||||
((100 - florisboard.prefs.keyboard.oneHandedModeScaleFactor) / 100.0f)
|
||||
super.onMeasure(MeasureSpec.makeMeasureSpec(width.toInt(), MeasureSpec.EXACTLY), heightMeasureSpec)
|
||||
}
|
||||
}
|
||||
@@ -35,8 +35,12 @@ import kotlin.math.min
|
||||
class PopupExtendedView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
|
||||
private val activeBackgroundDrawable: PaintDrawable = PaintDrawable()
|
||||
private var backgroundDrawable: PaintDrawable = PaintDrawable()
|
||||
private val activeBackgroundDrawable: PaintDrawable = PaintDrawable().apply {
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
}
|
||||
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
}
|
||||
private val labelPaint: Paint = Paint().apply {
|
||||
alpha = 255
|
||||
color = 0
|
||||
@@ -80,6 +84,7 @@ class PopupExtendedView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
init {
|
||||
visibility = GONE
|
||||
background = backgroundDrawable
|
||||
elevation = ViewLayoutUtils.convertDpToPixel(4.0f, context)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
@@ -88,20 +93,17 @@ class PopupExtendedView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
activeBackgroundDrawable.apply {
|
||||
setTint(theme.getAttr(Theme.Attr.POPUP_BACKGROUND_ACTIVE).toSolidColor().color)
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
}
|
||||
backgroundDrawable.apply {
|
||||
setTint(theme.getAttr(Theme.Attr.POPUP_BACKGROUND).toSolidColor().color)
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
}
|
||||
elevation = ViewLayoutUtils.convertDpToPixel(4.0f, context)
|
||||
labelPaint.color = theme.getAttr(Theme.Attr.POPUP_FOREGROUND).toSolidColor().color
|
||||
tldPaint.color = theme.getAttr(Theme.Attr.POPUP_FOREGROUND).toSolidColor().color
|
||||
if (isShowing) {
|
||||
|
||||
@@ -16,17 +16,9 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.popup
|
||||
|
||||
import android.content.Context
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dev.patrickgold.florisboard.ime.extension.Asset
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyTypeAdapter
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariationAdapter
|
||||
|
||||
/**
|
||||
* An object which maps each base key to its extended popups. This can be done for each
|
||||
@@ -48,29 +40,5 @@ class PopupExtension(
|
||||
) : Asset {
|
||||
companion object : Asset.Companion<PopupExtension> {
|
||||
override fun empty() = PopupExtension("", "", listOf(), mapOf())
|
||||
|
||||
override fun fromFile(context: Context, path: String): Result<PopupExtension, Throwable> {
|
||||
return try {
|
||||
val raw = context.assets.open(path).bufferedReader().use { it.readText() }
|
||||
val asset = fromJsonString(raw)
|
||||
if (asset != null) {
|
||||
Ok(asset)
|
||||
} else {
|
||||
Err(NullPointerException())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun fromJsonString(json: String): PopupExtension? {
|
||||
val moshi = Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.add(KeyTypeAdapter())
|
||||
.add(KeyVariationAdapter())
|
||||
.build()
|
||||
val layoutAdapter = moshi.adapter(PopupExtension::class.java)
|
||||
return layoutAdapter.fromJson(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,14 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
|
||||
/**
|
||||
* Basic helper view class which acts as a non-interactive layer view, which sits above the whole
|
||||
* input UI. Automatically rejects any touch events and passes it through to the View below.
|
||||
*/
|
||||
class PopupLayerView : FrameLayout {
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
@@ -34,6 +36,9 @@ class PopupLayerView : FrameLayout {
|
||||
background = null
|
||||
isClickable = false
|
||||
isFocusable = false
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
package dev.patrickgold.florisboard.ime.popup
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.view.*
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat.getDrawable
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
|
||||
@@ -64,6 +66,10 @@ class PopupManager<T_KBD: View, T_KV: View>(
|
||||
val isShowingExtendedPopup: Boolean
|
||||
get() = popupViewExt.isShowing
|
||||
|
||||
companion object {
|
||||
const val POPUP_EXTENSION_PATH_REL: String = "ime/text/characters/extended_popups"
|
||||
}
|
||||
|
||||
init {
|
||||
keyPopupWidth = keyboardView.resources.getDimension(R.dimen.key_width).toInt()
|
||||
keyPopupHeight = keyboardView.resources.getDimension(R.dimen.key_height).toInt()
|
||||
@@ -107,7 +113,8 @@ class PopupManager<T_KBD: View, T_KV: View>(
|
||||
keyView.data.popup[adjustedIndex].label, adjustedIndex
|
||||
)
|
||||
}
|
||||
KeyCode.TOGGLE_ONE_HANDED_MODE -> {
|
||||
KeyCode.TOGGLE_ONE_HANDED_MODE_LEFT,
|
||||
KeyCode.TOGGLE_ONE_HANDED_MODE_RIGHT -> {
|
||||
getDrawable(keyView.context, R.drawable.ic_smartphone)?.let {
|
||||
PopupExtendedView.Element.Icon(it, adjustedIndex)
|
||||
} ?: PopupExtendedView.Element.Undefined
|
||||
|
||||
@@ -34,7 +34,9 @@ import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
class PopupView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
|
||||
private var backgroundDrawable: PaintDrawable = PaintDrawable()
|
||||
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
}
|
||||
private val labelPaint: Paint = Paint().apply {
|
||||
alpha = 255
|
||||
color = 0
|
||||
@@ -86,7 +88,6 @@ class PopupView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
backgroundDrawable.apply {
|
||||
setTint(theme.getAttr(Theme.Attr.POPUP_BACKGROUND).toSolidColor().color)
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
}
|
||||
elevation = ViewLayoutUtils.convertDpToPixel(4.0f, context)
|
||||
threeDotsDrawable?.apply {
|
||||
|
||||
@@ -16,21 +16,24 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.*
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import android.widget.ViewFlipper
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.*
|
||||
import dev.patrickgold.florisboard.ime.dictionary.Dictionary
|
||||
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetSource
|
||||
import dev.patrickgold.florisboard.ime.nlp.Token
|
||||
import dev.patrickgold.florisboard.ime.nlp.toStringList
|
||||
import dev.patrickgold.florisboard.ime.text.editing.EditingKeyboardView
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyType
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
|
||||
import dev.patrickgold.florisboard.ime.text.key.*
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
|
||||
import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
|
||||
@@ -38,6 +41,7 @@ import dev.patrickgold.florisboard.ime.text.smartbar.SmartbarView
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
/**
|
||||
* TextInputManager is responsible for managing everything which is related to text input. All of
|
||||
@@ -50,7 +54,7 @@ import java.util.*
|
||||
* TextInputManager is also the hub in the communication between the system, the active editor
|
||||
* instance and the Smartbar.
|
||||
*/
|
||||
class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
class TextInputManager private constructor() : CoroutineScope by MainScope(), InputKeyEventReceiver,
|
||||
FlorisBoard.EventListener, SmartbarView.EventListener {
|
||||
|
||||
private val florisboard = FlorisBoard.getInstance()
|
||||
@@ -58,28 +62,41 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
get() = florisboard.activeEditorInstance
|
||||
|
||||
private var activeKeyboardMode: KeyboardMode? = null
|
||||
private var animator: ObjectAnimator? = null
|
||||
private val keyboardViews = EnumMap<KeyboardMode, KeyboardView>(KeyboardMode::class.java)
|
||||
private var editingKeyboardView: EditingKeyboardView? = null
|
||||
private val osHandler = Handler()
|
||||
private var loadingPlaceholderKeyboard: KeyboardView? = null
|
||||
private var textViewFlipper: ViewFlipper? = null
|
||||
var textViewGroup: LinearLayout? = null
|
||||
private var textViewGroup: LinearLayout? = null
|
||||
private val dictionaryManager: DictionaryManager = DictionaryManager.default()
|
||||
private var activeDictionary: Dictionary<String, Int>? = null
|
||||
val inputEventDispatcher: InputEventDispatcher = InputEventDispatcher.new(
|
||||
parentScope = this,
|
||||
repeatableKeyCodes = intArrayOf(
|
||||
KeyCode.ARROW_DOWN,
|
||||
KeyCode.ARROW_LEFT,
|
||||
KeyCode.ARROW_RIGHT,
|
||||
KeyCode.ARROW_UP,
|
||||
KeyCode.DELETE,
|
||||
KeyCode.FORWARD_DELETE
|
||||
)
|
||||
)
|
||||
|
||||
var keyVariation: KeyVariation = KeyVariation.NORMAL
|
||||
val layoutManager = LayoutManager(florisboard)
|
||||
private var smartbarView: SmartbarView? = null
|
||||
|
||||
// Caps/Space related properties
|
||||
// Caps/Shift related properties
|
||||
var caps: Boolean = false
|
||||
private set
|
||||
var capsLock: Boolean = false
|
||||
private set
|
||||
private var hasCapsRecentlyChanged: Boolean = false
|
||||
private var hasSpaceRecentlyPressed: Boolean = false
|
||||
private var newCapsState: Boolean = false
|
||||
|
||||
// Composing text related properties
|
||||
var isManualSelectionMode: Boolean = false
|
||||
private var isManualSelectionModeLeft: Boolean = false
|
||||
private var isManualSelectionModeRight: Boolean = false
|
||||
private var isManualSelectionModeStart: Boolean = false
|
||||
private var isManualSelectionModeEnd: Boolean = false
|
||||
|
||||
companion object {
|
||||
private var instance: TextInputManager? = null
|
||||
@@ -104,6 +121,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
override fun onCreate() {
|
||||
Timber.i("onCreate()")
|
||||
|
||||
inputEventDispatcher.keyEventReceiver = this
|
||||
var subtypes = florisboard.subtypeManager.subtypes
|
||||
if (subtypes.isEmpty()) {
|
||||
subtypes = listOf(Subtype.DEFAULT)
|
||||
@@ -115,6 +133,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateInputView() {
|
||||
keyboardViews.clear()
|
||||
}
|
||||
|
||||
private suspend fun addKeyboardView(mode: KeyboardMode) {
|
||||
val keyboardView = KeyboardView(florisboard.context)
|
||||
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(mode, florisboard.activeSubtype, florisboard.prefs).await()
|
||||
@@ -128,14 +150,38 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
override fun onRegisterInputView(inputView: InputView) {
|
||||
Timber.i("onRegisterInputView(inputView)")
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
textViewGroup = inputView.findViewById(R.id.text_input)
|
||||
textViewFlipper = inputView.findViewById(R.id.text_input_view_flipper)
|
||||
editingKeyboardView = inputView.findViewById(R.id.editing)
|
||||
textViewGroup = inputView.findViewById(R.id.text_input)
|
||||
textViewFlipper = inputView.findViewById(R.id.text_input_view_flipper)
|
||||
editingKeyboardView = inputView.findViewById(R.id.editing)
|
||||
loadingPlaceholderKeyboard = inputView.findViewById(R.id.keyboard_preview)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
textViewGroup?.let {
|
||||
animator = ObjectAnimator.ofFloat(it, "alpha", 0.9f, 1.0f).apply {
|
||||
duration = 125
|
||||
repeatCount = ValueAnimator.INFINITE
|
||||
repeatMode = ValueAnimator.REVERSE
|
||||
start()
|
||||
launch {
|
||||
delay(duration)
|
||||
try {
|
||||
duration = 500
|
||||
setFloatValues(1.0f, 0.4f)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
val activeKeyboardMode = getActiveKeyboardMode()
|
||||
addKeyboardView(activeKeyboardMode)
|
||||
setActiveKeyboardMode(activeKeyboardMode)
|
||||
animator?.cancel()
|
||||
textViewGroup?.let {
|
||||
animator = ObjectAnimator.ofFloat(it, "alpha", it.alpha, 1.0f).apply {
|
||||
duration = (((1.0f - it.alpha) / 0.6f) * 125f).roundToLong()
|
||||
repeatCount = 0
|
||||
start()
|
||||
}
|
||||
}
|
||||
for (mode in KeyboardMode.values()) {
|
||||
if (mode != activeKeyboardMode && mode != KeyboardMode.SMARTBAR_NUMBER_ROW) {
|
||||
addKeyboardView(mode)
|
||||
@@ -149,14 +195,21 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
smartbarView?.setEventListener(this)
|
||||
}
|
||||
|
||||
fun unregisterSmartbarView(view: SmartbarView) {
|
||||
if (smartbarView == view) {
|
||||
smartbarView = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all coroutines and cleans up.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
Timber.i("onDestroy()")
|
||||
|
||||
inputEventDispatcher.keyEventReceiver = null
|
||||
inputEventDispatcher.close()
|
||||
cancel()
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
layoutManager.onDestroy()
|
||||
instance = null
|
||||
}
|
||||
@@ -246,24 +299,31 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
/**
|
||||
* Sets [activeKeyboardMode] and updates the [SmartbarView.isQuickActionsVisible] state.
|
||||
*/
|
||||
fun setActiveKeyboardMode(mode: KeyboardMode) {
|
||||
private fun setActiveKeyboardMode(mode: KeyboardMode) {
|
||||
textViewFlipper?.displayedChild = textViewFlipper?.indexOfChild(when (mode) {
|
||||
KeyboardMode.EDITING -> editingKeyboardView
|
||||
else -> keyboardViews[mode]
|
||||
}) ?: 0
|
||||
})?.coerceAtLeast(0) ?: 0
|
||||
keyboardViews[mode]?.updateVisibility()
|
||||
keyboardViews[mode]?.requestLayout()
|
||||
keyboardViews[mode]?.requestLayoutAllKeys()
|
||||
activeKeyboardMode = mode
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = false
|
||||
smartbarView?.isQuickActionsVisible = false
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
override fun onSubtypeChanged(newSubtype: Subtype) {
|
||||
launch {
|
||||
if (activeEditorInstance.isComposingEnabled) {
|
||||
withContext(Dispatchers.IO) {
|
||||
dictionaryManager.loadDictionary(AssetRef(AssetSource.Assets,"ime/dict/en.flict")).let {
|
||||
activeDictionary = it.getOrDefault(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
val keyboardView = keyboardViews[KeyboardMode.CHARACTERS]
|
||||
keyboardView?.computedLayout = layoutManager.fetchComputedLayoutAsync(KeyboardMode.CHARACTERS, newSubtype, florisboard.prefs).await()
|
||||
keyboardView?.updateVisibility()
|
||||
@@ -275,13 +335,39 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* and passing this info on to the [SmartbarView] to turn it into candidate suggestions.
|
||||
*/
|
||||
override fun onUpdateSelection() {
|
||||
if (!activeEditorInstance.isNewSelectionInBoundsOfOld) {
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
if (!inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
|
||||
updateCapsState()
|
||||
}
|
||||
updateCapsState()
|
||||
smartbarView?.updateSmartbarState()
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.i("current word: ${activeEditorInstance.cachedInput.currentWord.text}")
|
||||
}
|
||||
if (activeEditorInstance.isComposingEnabled && !inputEventDispatcher.isPressed(KeyCode.DELETE)) {
|
||||
if (activeEditorInstance.shouldReevaluateComposingSuggestions) {
|
||||
activeEditorInstance.shouldReevaluateComposingSuggestions = false
|
||||
activeDictionary?.let {
|
||||
launch(Dispatchers.Default) {
|
||||
val startTime = System.nanoTime()
|
||||
val suggestions = it.getTokenPredictions(
|
||||
precedingTokens = listOf(),
|
||||
currentToken = Token(activeEditorInstance.cachedInput.currentWord.text),
|
||||
maxSuggestionCount = 3,
|
||||
allowPossiblyOffensive = !florisboard.prefs.suggestion.blockPossiblyOffensive
|
||||
).toStringList()
|
||||
if (BuildConfig.DEBUG) {
|
||||
val elapsed = (System.nanoTime() - startTime) / 1000.0
|
||||
Timber.i("sugg fetch time: $elapsed us")
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
smartbarView?.setCandidateSuggestionWords(startTime, suggestions)
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), listOf())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrimaryClipChanged() {
|
||||
@@ -296,9 +382,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
if (!capsLock) {
|
||||
caps = florisboard.prefs.correction.autoCapitalization &&
|
||||
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
|
||||
launch(Dispatchers.Main) {
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
}
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,14 +391,23 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* class.
|
||||
*/
|
||||
fun executeSwipeAction(swipeAction: SwipeAction) {
|
||||
when (swipeAction) {
|
||||
SwipeAction.DELETE_WORD -> handleDeleteWord()
|
||||
SwipeAction.MOVE_CURSOR_DOWN -> handleArrow(KeyCode.ARROW_DOWN)
|
||||
SwipeAction.MOVE_CURSOR_UP -> handleArrow(KeyCode.ARROW_UP)
|
||||
SwipeAction.MOVE_CURSOR_LEFT -> handleArrow(KeyCode.ARROW_LEFT)
|
||||
SwipeAction.MOVE_CURSOR_RIGHT -> handleArrow(KeyCode.ARROW_RIGHT)
|
||||
SwipeAction.SHIFT -> handleShift()
|
||||
else -> {}
|
||||
val keyData = when (swipeAction) {
|
||||
SwipeAction.DELETE_WORD -> KeyData.DELETE_WORD
|
||||
SwipeAction.INSERT_SPACE -> KeyData.SPACE
|
||||
SwipeAction.MOVE_CURSOR_DOWN -> KeyData.ARROW_DOWN
|
||||
SwipeAction.MOVE_CURSOR_UP -> KeyData.ARROW_UP
|
||||
SwipeAction.MOVE_CURSOR_LEFT -> KeyData.ARROW_LEFT
|
||||
SwipeAction.MOVE_CURSOR_RIGHT -> KeyData.ARROW_RIGHT
|
||||
SwipeAction.MOVE_CURSOR_START_OF_LINE -> KeyData.MOVE_START_OF_LINE
|
||||
SwipeAction.MOVE_CURSOR_END_OF_LINE -> KeyData.MOVE_END_OF_LINE
|
||||
SwipeAction.MOVE_CURSOR_START_OF_PAGE -> KeyData.MOVE_START_OF_PAGE
|
||||
SwipeAction.MOVE_CURSOR_END_OF_PAGE -> KeyData.MOVE_END_OF_PAGE
|
||||
SwipeAction.SHIFT -> KeyData.SHIFT
|
||||
SwipeAction.SHOW_INPUT_METHOD_PICKER -> KeyData.SHOW_INPUT_METHOD_PICKER
|
||||
else -> null
|
||||
}
|
||||
if (keyData != null) {
|
||||
inputEventDispatcher.send(InputKeyEvent.downUp(keyData))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,6 +415,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
setActiveKeyboardMode(KeyboardMode.CHARACTERS)
|
||||
}
|
||||
|
||||
override fun onSmartbarCandidatePressed(word: String) {
|
||||
activeEditorInstance.commitCompletion(word)
|
||||
}
|
||||
|
||||
override fun onSmartbarPrivateModeButtonClicked() {
|
||||
Toast.makeText(florisboard.context, R.string.private_mode_dialog__title, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
@@ -337,13 +434,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
R.id.quick_action_switch_to_media_context -> florisboard.setActiveInput(R.id.media_input)
|
||||
R.id.quick_action_open_settings -> florisboard.launchSettings()
|
||||
R.id.quick_action_one_handed_toggle -> florisboard.toggleOneHandedMode()
|
||||
R.id.quick_action_one_handed_toggle -> florisboard.toggleOneHandedMode(isRight = true)
|
||||
R.id.quick_action_undo -> {
|
||||
handleUndo()
|
||||
activeEditorInstance.performUndo()
|
||||
return
|
||||
}
|
||||
R.id.quick_action_redo -> {
|
||||
handleRedo()
|
||||
activeEditorInstance.performRedo()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -351,21 +448,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
private fun handleUndo(){
|
||||
activeEditorInstance.performUndo()
|
||||
}
|
||||
|
||||
private fun handleRedo(){
|
||||
activeEditorInstance.performRedo()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.DELETE] event.
|
||||
*/
|
||||
private fun handleDelete() {
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = false
|
||||
activeEditorInstance.deleteBackwards()
|
||||
}
|
||||
|
||||
@@ -374,8 +463,8 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
*/
|
||||
private fun handleDeleteWord() {
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = false
|
||||
activeEditorInstance.deleteWordsBeforeCursor(1)
|
||||
}
|
||||
|
||||
@@ -401,44 +490,79 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] event.
|
||||
* Handles a [KeyCode.LANGUAGE_SWITCH] event. Also handles if the language switch should cycle
|
||||
* FlorisBoard internal or system-wide.
|
||||
*/
|
||||
private fun handleShift() {
|
||||
if (hasCapsRecentlyChanged) {
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
private fun handleLanguageSwitch() {
|
||||
when (florisboard.prefs.keyboard.utilityKeyAction) {
|
||||
UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS,
|
||||
UtilityKeyAction.SWITCH_LANGUAGE -> florisboard.switchToNextSubtype()
|
||||
else -> florisboard.switchToNextKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] down event.
|
||||
*/
|
||||
private fun handleShiftDown(ev: InputKeyEvent) {
|
||||
if (ev.isConsecutiveEventOf(inputEventDispatcher.lastKeyEventDown, florisboard.prefs.keyboard.longPressDelay.toLong())) {
|
||||
newCapsState = true
|
||||
caps = true
|
||||
capsLock = true
|
||||
hasCapsRecentlyChanged = false
|
||||
} else {
|
||||
caps = !caps
|
||||
newCapsState = !caps
|
||||
caps = true
|
||||
capsLock = false
|
||||
hasCapsRecentlyChanged = true
|
||||
osHandler.postDelayed({
|
||||
hasCapsRecentlyChanged = false
|
||||
}, 300)
|
||||
}
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] up event.
|
||||
*/
|
||||
private fun handleShiftUp() {
|
||||
caps = newCapsState
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] cancel event.
|
||||
*/
|
||||
private fun handleShiftCancel() {
|
||||
caps = false
|
||||
capsLock = false
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] up event.
|
||||
*/
|
||||
private fun handleShiftLock() {
|
||||
val lastKeyEvent = inputEventDispatcher.lastKeyEventDown ?: return
|
||||
if (lastKeyEvent.data.code == KeyCode.SHIFT && lastKeyEvent.action == InputKeyEvent.Action.DOWN) {
|
||||
newCapsState = true
|
||||
caps = true
|
||||
capsLock = true
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SPACE] event. Also handles the auto-correction of two space taps if
|
||||
* enabled by the user.
|
||||
*/
|
||||
private fun handleSpace() {
|
||||
private fun handleSpace(ev: InputKeyEvent) {
|
||||
if (florisboard.prefs.correction.doubleSpacePeriod) {
|
||||
if (hasSpaceRecentlyPressed) {
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
if (ev.isConsecutiveEventOf(inputEventDispatcher.lastKeyEventUp, florisboard.prefs.keyboard.longPressDelay.toLong())) {
|
||||
val text = activeEditorInstance.getTextBeforeCursor(2)
|
||||
if (text.length == 2 && !text.matches("""[.!?‽\s][\s]""".toRegex())) {
|
||||
activeEditorInstance.deleteBackwards()
|
||||
activeEditorInstance.commitText(".")
|
||||
}
|
||||
hasSpaceRecentlyPressed = false
|
||||
} else {
|
||||
hasSpaceRecentlyPressed = true
|
||||
osHandler.postDelayed({
|
||||
hasSpaceRecentlyPressed = false
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
activeEditorInstance.commitText(KeyCode.SPACE.toChar().toString())
|
||||
@@ -447,114 +571,64 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
/**
|
||||
* Handles [KeyCode] arrow and move events, behaves differently depending on text selection.
|
||||
*/
|
||||
private fun handleArrow(code: Int) = activeEditorInstance.apply {
|
||||
val selectionStartMin = 0
|
||||
val selectionEndMax = cachedText.length
|
||||
if (selection.isSelectionMode && isManualSelectionMode) {
|
||||
// Text is selected and it is manual selection -> Expand selection depending on started
|
||||
// direction.
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
if (isManualSelectionModeLeft) {
|
||||
setSelection(
|
||||
(selection.start - 1).coerceAtLeast(selectionStartMin),
|
||||
selection.end
|
||||
)
|
||||
} else {
|
||||
setSelection(selection.start, selection.end - 1)
|
||||
}
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
if (isManualSelectionModeRight) {
|
||||
setSelection(
|
||||
selection.start,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
} else {
|
||||
setSelection(selection.start + 1, selection.end)
|
||||
}
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
if (isManualSelectionModeLeft) {
|
||||
setSelection(selectionStartMin, selection.end)
|
||||
} else {
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
}
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
if (isManualSelectionModeRight) {
|
||||
setSelection(selection.start, selectionEndMax)
|
||||
} else {
|
||||
setSelection(selection.end, selectionEndMax)
|
||||
}
|
||||
private fun handleArrow(code: Int, count: Int) = activeEditorInstance.apply {
|
||||
val isShiftPressed = isManualSelectionMode || inputEventDispatcher.isPressed(KeyCode.SHIFT)
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = true
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, meta(shift = isShiftPressed), count)
|
||||
}
|
||||
} else if (selection.isSelectionMode && !isManualSelectionMode) {
|
||||
// Text is selected but no manual selection mode -> arrows behave as if selection was
|
||||
// started in manual left mode
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
setSelection(selection.start, selection.end - 1)
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
setSelection(
|
||||
selection.start,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
setSelection(selection.start, selectionEndMax)
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = true
|
||||
isManualSelectionModeEnd = false
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, meta(shift = isShiftPressed), count)
|
||||
}
|
||||
} else if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
// No text is selected but manual selection mode is active, user wants to start a new
|
||||
// selection. Must set manual selection direction.
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
setSelection(
|
||||
(selection.start - 1).coerceAtLeast(selectionStartMin),
|
||||
selection.start
|
||||
)
|
||||
isManualSelectionModeLeft = true
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
setSelection(
|
||||
selection.end,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = true
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
isManualSelectionModeLeft = true
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
setSelection(selection.end, selectionEndMax)
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = true
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = true
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, meta(shift = isShiftPressed), count)
|
||||
}
|
||||
} else {
|
||||
// No selection and no manual selection mode -> move cursor around
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
KeyCode.ARROW_LEFT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT)
|
||||
KeyCode.ARROW_RIGHT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT)
|
||||
KeyCode.ARROW_UP -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_UP)
|
||||
KeyCode.MOVE_HOME -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_UP)
|
||||
KeyCode.MOVE_END -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
KeyCode.ARROW_UP -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = true
|
||||
isManualSelectionModeEnd = false
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_UP, meta(shift = isShiftPressed), count)
|
||||
}
|
||||
KeyCode.MOVE_START_OF_PAGE -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = true
|
||||
isManualSelectionModeEnd = false
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_UP, meta(alt = true, shift = isShiftPressed), count)
|
||||
}
|
||||
KeyCode.MOVE_END_OF_PAGE -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = true
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, meta(alt = true, shift = isShiftPressed), count)
|
||||
}
|
||||
KeyCode.MOVE_START_OF_LINE -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = true
|
||||
isManualSelectionModeEnd = false
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, meta(alt = true, shift = isShiftPressed), count)
|
||||
}
|
||||
KeyCode.MOVE_END_OF_LINE -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = true
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, meta(alt = true, shift = isShiftPressed), count)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -564,10 +638,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
*/
|
||||
private fun handleClipboardSelect() = activeEditorInstance.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
if (isManualSelectionMode && isManualSelectionModeLeft) {
|
||||
setSelection(selection.start, selection.start)
|
||||
if (isManualSelectionMode && isManualSelectionModeStart) {
|
||||
selection.updateAndNotify(selection.start, selection.start)
|
||||
} else {
|
||||
setSelection(selection.end, selection.end)
|
||||
selection.updateAndNotify(selection.end, selection.end)
|
||||
}
|
||||
isManualSelectionMode = false
|
||||
} else {
|
||||
@@ -578,27 +652,40 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.CLIPBOARD_SELECT_ALL] event.
|
||||
* Adjusts a given key data for caps state and returns the correct reference.
|
||||
*/
|
||||
private fun handleClipboardSelectAll() {
|
||||
activeEditorInstance.setSelection(0, activeEditorInstance.cachedText.length)
|
||||
private fun getAdjustedKeyData(keyData: KeyData): KeyData {
|
||||
return if (caps && keyData is FlorisKeyData && keyData.shift != null) { keyData.shift!! } else { keyData }
|
||||
}
|
||||
|
||||
/**
|
||||
* Main logic point for sending a key press. Different actions may occur depending on the given
|
||||
* [KeyData]. This method handles all key press send events, which are text based. For media
|
||||
* input send events see MediaInputManager.
|
||||
*
|
||||
* @param keyData The [KeyData] object which should be sent.
|
||||
*/
|
||||
fun sendKeyPress(keyData: KeyData) {
|
||||
when (keyData.code) {
|
||||
override fun onInputKeyDown(ev: InputKeyEvent) {
|
||||
val data = getAdjustedKeyData(ev.data)
|
||||
when (data.code) {
|
||||
KeyCode.INTERNAL_BATCH_EDIT -> {
|
||||
florisboard.beginInternalBatchEdit()
|
||||
return
|
||||
}
|
||||
KeyCode.SHIFT -> {
|
||||
handleShiftDown(ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInputKeyUp(ev: InputKeyEvent) {
|
||||
val data = getAdjustedKeyData(ev.data)
|
||||
when (data.code) {
|
||||
KeyCode.ARROW_DOWN,
|
||||
KeyCode.ARROW_LEFT,
|
||||
KeyCode.ARROW_RIGHT,
|
||||
KeyCode.ARROW_UP,
|
||||
KeyCode.MOVE_HOME,
|
||||
KeyCode.MOVE_END -> handleArrow(keyData.code)
|
||||
KeyCode.MOVE_START_OF_PAGE,
|
||||
KeyCode.MOVE_END_OF_PAGE,
|
||||
KeyCode.MOVE_START_OF_LINE,
|
||||
KeyCode.MOVE_END_OF_LINE -> if (ev.action == InputKeyEvent.Action.DOWN_UP || ev.action == InputKeyEvent.Action.REPEAT) {
|
||||
handleArrow(data.code, ev.count)
|
||||
} else {
|
||||
handleArrow(data.code, 1)
|
||||
}
|
||||
KeyCode.CLIPBOARD_CUT -> activeEditorInstance.performClipboardCut()
|
||||
KeyCode.CLIPBOARD_COPY -> activeEditorInstance.performClipboardCopy()
|
||||
KeyCode.CLIPBOARD_PASTE -> {
|
||||
@@ -606,26 +693,36 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
KeyCode.CLIPBOARD_SELECT -> handleClipboardSelect()
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> handleClipboardSelectAll()
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> activeEditorInstance.performClipboardSelectAll()
|
||||
KeyCode.DELETE -> {
|
||||
handleDelete()
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
if (ev.action == InputKeyEvent.Action.DOWN_UP || ev.action == InputKeyEvent.Action.UP) {
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
}
|
||||
KeyCode.DELETE_WORD -> {
|
||||
handleDeleteWord()
|
||||
if (ev.action == InputKeyEvent.Action.DOWN_UP || ev.action == InputKeyEvent.Action.UP) {
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
}
|
||||
KeyCode.ENTER -> {
|
||||
handleEnter()
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
KeyCode.LANGUAGE_SWITCH -> florisboard.switchToNextSubtype()
|
||||
KeyCode.SETTINGS -> florisboard.launchSettings()
|
||||
KeyCode.SHIFT -> handleShift()
|
||||
KeyCode.SHOW_INPUT_METHOD_PICKER -> {
|
||||
val im =
|
||||
florisboard.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
im.showInputMethodPicker()
|
||||
KeyCode.INTERNAL_BATCH_EDIT -> {
|
||||
florisboard.endInternalBatchEdit()
|
||||
return
|
||||
}
|
||||
KeyCode.LANGUAGE_SWITCH -> handleLanguageSwitch()
|
||||
KeyCode.SETTINGS -> florisboard.launchSettings()
|
||||
KeyCode.SHIFT -> handleShiftUp()
|
||||
KeyCode.SHIFT_LOCK -> handleShiftLock()
|
||||
KeyCode.SHOW_INPUT_METHOD_PICKER -> florisboard.imeManager?.showInputMethodPicker()
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> florisboard.setActiveInput(R.id.media_input)
|
||||
KeyCode.SWITCH_TO_TEXT_CONTEXT -> florisboard.setActiveInput(R.id.text_input)
|
||||
KeyCode.TOGGLE_ONE_HANDED_MODE -> florisboard.toggleOneHandedMode()
|
||||
KeyCode.TOGGLE_ONE_HANDED_MODE_LEFT -> florisboard.toggleOneHandedMode(isRight = false)
|
||||
KeyCode.TOGGLE_ONE_HANDED_MODE_RIGHT -> florisboard.toggleOneHandedMode(isRight = true)
|
||||
KeyCode.VIEW_CHARACTERS -> setActiveKeyboardMode(KeyboardMode.CHARACTERS)
|
||||
KeyCode.VIEW_NUMERIC -> setActiveKeyboardMode(KeyboardMode.NUMERIC)
|
||||
KeyCode.VIEW_NUMERIC_ADVANCED -> setActiveKeyboardMode(KeyboardMode.NUMERIC_ADVANCED)
|
||||
@@ -638,50 +735,59 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.NUMERIC_ADVANCED,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> when (keyData.type) {
|
||||
KeyboardMode.PHONE2 -> when (data.type) {
|
||||
KeyType.CHARACTER,
|
||||
KeyType.NUMERIC -> {
|
||||
val text = keyData.code.toChar().toString()
|
||||
val text = data.code.toChar().toString()
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
else -> when (keyData.code) {
|
||||
else -> when (data.code) {
|
||||
KeyCode.PHONE_PAUSE,
|
||||
KeyCode.PHONE_WAIT -> {
|
||||
val text = keyData.code.toChar().toString()
|
||||
val text = data.code.toChar().toString()
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> when (keyData.type) {
|
||||
KeyType.CHARACTER, KeyType.NUMERIC -> when (keyData.code) {
|
||||
KeyCode.SPACE -> handleSpace()
|
||||
else -> when (data.type) {
|
||||
KeyType.CHARACTER, KeyType.NUMERIC -> when (data.code) {
|
||||
KeyCode.SPACE -> handleSpace(ev)
|
||||
KeyCode.URI_COMPONENT_TLD -> {
|
||||
val tld = when (caps) {
|
||||
true -> keyData.label.toUpperCase(Locale.getDefault())
|
||||
false -> keyData.label.toLowerCase(Locale.getDefault())
|
||||
}
|
||||
val tld = data.label.toLowerCase(Locale.ENGLISH)
|
||||
activeEditorInstance.commitText(tld)
|
||||
}
|
||||
else -> {
|
||||
var text = keyData.code.toChar().toString()
|
||||
text = when (caps) {
|
||||
true -> text.toUpperCase(Locale.getDefault())
|
||||
false -> text.toLowerCase(Locale.getDefault())
|
||||
var text = data.code.toChar().toString()
|
||||
val locale = if (florisboard.activeSubtype.locale.language == "el") { Locale.getDefault() } else { florisboard.activeSubtype.locale }
|
||||
text = when (caps && activeKeyboardMode == KeyboardMode.CHARACTERS) {
|
||||
true -> text.toUpperCase(locale)
|
||||
false -> text
|
||||
}
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Timber.e("sendKeyPress(keyData): Received unknown key: $keyData")
|
||||
Timber.e("sendKeyPress(keyData): Received unknown key: $data")
|
||||
}
|
||||
}
|
||||
}
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
}
|
||||
if (keyData.code != KeyCode.SHIFT && !capsLock) {
|
||||
if (data.code != KeyCode.SHIFT && !capsLock && !inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
|
||||
updateCapsState()
|
||||
}
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
override fun onInputKeyRepeat(ev: InputKeyEvent) {
|
||||
onInputKeyUp(ev)
|
||||
}
|
||||
|
||||
override fun onInputKeyCancel(ev: InputKeyEvent) {
|
||||
val data = getAdjustedKeyData(ev.data)
|
||||
when (data.code) {
|
||||
KeyCode.SHIFT -> handleShiftCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import android.widget.Button
|
||||
import androidx.appcompat.widget.AppCompatImageButton
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
@@ -46,10 +47,25 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
private val data: KeyData
|
||||
private val data: KeyData = when (id) {
|
||||
R.id.arrow_down -> KeyData.ARROW_DOWN
|
||||
R.id.arrow_left -> KeyData.ARROW_LEFT
|
||||
R.id.arrow_right -> KeyData.ARROW_RIGHT
|
||||
R.id.arrow_up -> KeyData.ARROW_UP
|
||||
R.id.backspace -> KeyData.DELETE
|
||||
R.id.clipboard_copy -> KeyData.CLIPBOARD_COPY
|
||||
R.id.clipboard_cut -> KeyData.CLIPBOARD_CUT
|
||||
R.id.clipboard_paste -> KeyData.CLIPBOARD_PASTE
|
||||
R.id.move_start_of_line -> KeyData.MOVE_START_OF_LINE
|
||||
R.id.move_end_of_line -> KeyData.MOVE_END_OF_LINE
|
||||
R.id.select -> KeyData.CLIPBOARD_SELECT
|
||||
R.id.select_all -> KeyData.CLIPBOARD_SELECT_ALL
|
||||
else -> KeyData.UNSPECIFIED
|
||||
}
|
||||
private var isKeyPressed: Boolean = false
|
||||
private val repeatedKeyPressHandler: Handler = Handler(context.mainLooper)
|
||||
|
||||
private val defaultTextSize: Float = Button(context).textSize
|
||||
private var label: String? = null
|
||||
private var labelPaint: Paint = Paint().apply {
|
||||
alpha = 255
|
||||
@@ -57,7 +73,7 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
|
||||
isAntiAlias = true
|
||||
isFakeBoldText = false
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = Button(context).textSize
|
||||
textSize = defaultTextSize
|
||||
typeface = Typeface.DEFAULT
|
||||
}
|
||||
|
||||
@@ -71,22 +87,6 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, R.style.TextEditingButton)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
val code = when (id) {
|
||||
R.id.arrow_down -> KeyCode.ARROW_DOWN
|
||||
R.id.arrow_left -> KeyCode.ARROW_LEFT
|
||||
R.id.arrow_right -> KeyCode.ARROW_RIGHT
|
||||
R.id.arrow_up -> KeyCode.ARROW_UP
|
||||
R.id.backspace -> KeyCode.DELETE
|
||||
R.id.clipboard_copy -> KeyCode.CLIPBOARD_COPY
|
||||
R.id.clipboard_cut -> KeyCode.CLIPBOARD_CUT
|
||||
R.id.clipboard_paste -> KeyCode.CLIPBOARD_PASTE
|
||||
R.id.move_home -> KeyCode.MOVE_HOME
|
||||
R.id.move_end -> KeyCode.MOVE_END
|
||||
R.id.select -> KeyCode.CLIPBOARD_SELECT
|
||||
R.id.select_all -> KeyCode.CLIPBOARD_SELECT_ALL
|
||||
else -> 0
|
||||
}
|
||||
data = KeyData(code = code)
|
||||
context.obtainStyledAttributes(attrs, R.styleable.EditingKeyView).apply {
|
||||
label = getString(R.styleable.EditingKeyView_android_text)
|
||||
recycle()
|
||||
@@ -123,7 +123,7 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
|
||||
val delayMillis = prefs.keyboard.longPressDelay.toLong()
|
||||
repeatedKeyPressHandler.postAtScheduledRate(delayMillis, 25) {
|
||||
if (isKeyPressed) {
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.downUp(data))
|
||||
} else {
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
}
|
||||
@@ -135,7 +135,7 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
|
||||
isKeyPressed = false
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
if (event.actionMasked != MotionEvent.ACTION_CANCEL) {
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.downUp(data))
|
||||
}
|
||||
}
|
||||
else -> return false
|
||||
@@ -173,8 +173,10 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
|
||||
}
|
||||
val isPortrait =
|
||||
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
if (!isPortrait) {
|
||||
labelPaint.textSize *= 0.9f
|
||||
labelPaint.textSize = if (isPortrait) {
|
||||
defaultTextSize
|
||||
} else {
|
||||
defaultTextSize * 0.9f
|
||||
}
|
||||
val centerX = measuredWidth / 2.0f
|
||||
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2
|
||||
|
||||
@@ -35,8 +35,6 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
|
||||
private var arrowUpKey: EditingKeyView? = null
|
||||
private var arrowDownKey: EditingKeyView? = null
|
||||
private var selectKey: EditingKeyView? = null
|
||||
private var selectAllKey: EditingKeyView? = null
|
||||
private var cutKey: EditingKeyView? = null
|
||||
@@ -45,16 +43,13 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
florisboard?.addEventListener(this)
|
||||
}
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
florisboard?.addEventListener(this)
|
||||
themeManager.registerOnThemeUpdatedListener(this)
|
||||
|
||||
arrowUpKey = findViewById(R.id.arrow_up)
|
||||
arrowDownKey = findViewById(R.id.arrow_down)
|
||||
selectKey = findViewById(R.id.select)
|
||||
selectAllKey = findViewById(R.id.select_all)
|
||||
cutKey = findViewById(R.id.clipboard_cut)
|
||||
@@ -63,8 +58,9 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
florisboard?.removeEventListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
@@ -74,8 +70,6 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
|
||||
override fun onUpdateSelection() {
|
||||
val isSelectionActive = florisboard?.activeEditorInstance?.selection?.isSelectionMode ?: false
|
||||
val isSelectionMode = florisboard?.textInputManager?.isManualSelectionMode ?: false
|
||||
arrowUpKey?.isEnabled = !(isSelectionActive || isSelectionMode)
|
||||
arrowDownKey?.isEnabled = !(isSelectionActive || isSelectionMode)
|
||||
selectKey?.isHighlighted = isSelectionActive || isSelectionMode
|
||||
selectAllKey?.visibility = when {
|
||||
isSelectionActive -> View.GONE
|
||||
|
||||
@@ -27,11 +27,17 @@ enum class SwipeAction {
|
||||
DELETE_WORD,
|
||||
DELETE_WORDS_PRECISELY,
|
||||
HIDE_KEYBOARD,
|
||||
INSERT_SPACE,
|
||||
MOVE_CURSOR_UP,
|
||||
MOVE_CURSOR_DOWN,
|
||||
MOVE_CURSOR_LEFT,
|
||||
MOVE_CURSOR_RIGHT,
|
||||
MOVE_CURSOR_START_OF_LINE,
|
||||
MOVE_CURSOR_END_OF_LINE,
|
||||
MOVE_CURSOR_START_OF_PAGE,
|
||||
MOVE_CURSOR_END_OF_PAGE,
|
||||
SHIFT,
|
||||
SHOW_INPUT_METHOD_PICKER,
|
||||
SWITCH_TO_PREV_SUBTYPE,
|
||||
SWITCH_TO_NEXT_SUBTYPE,
|
||||
SWITCH_TO_PREV_KEYBOARD;
|
||||
|
||||
@@ -19,13 +19,44 @@ package dev.patrickgold.florisboard.ime.text.gestures
|
||||
import android.content.Context
|
||||
import android.view.MotionEvent
|
||||
import dev.patrickgold.florisboard.R
|
||||
import java.lang.Exception
|
||||
import kotlin.math.*
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.atan
|
||||
|
||||
/**
|
||||
* Wrapper class which holds all enums, interfaces and classes for detecting a swipe gesture.
|
||||
*/
|
||||
abstract class SwipeGesture {
|
||||
companion object {
|
||||
/**
|
||||
* Returns a numeric value for a given [DistanceThreshold], based on the values defined in
|
||||
* the resources dimens.xml file.
|
||||
*/
|
||||
fun numericValue(context: Context, of: DistanceThreshold): Double {
|
||||
return when (of) {
|
||||
DistanceThreshold.VERY_SHORT -> context.resources.getDimension(R.dimen.gesture_distance_threshold_very_short)
|
||||
DistanceThreshold.SHORT -> context.resources.getDimension(R.dimen.gesture_distance_threshold_short)
|
||||
DistanceThreshold.NORMAL -> context.resources.getDimension(R.dimen.gesture_distance_threshold_normal)
|
||||
DistanceThreshold.LONG -> context.resources.getDimension(R.dimen.gesture_distance_threshold_long)
|
||||
DistanceThreshold.VERY_LONG -> context.resources.getDimension(R.dimen.gesture_distance_threshold_very_long)
|
||||
}.toDouble()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a numeric value for a given [VelocityThreshold], based on the values defined in
|
||||
* the resources dimens.xml file.
|
||||
*/
|
||||
fun numericValue(context: Context, of: VelocityThreshold): Double {
|
||||
return when (of) {
|
||||
VelocityThreshold.VERY_SLOW -> context.resources.getInteger(R.integer.gesture_velocity_threshold_very_slow)
|
||||
VelocityThreshold.SLOW -> context.resources.getInteger(R.integer.gesture_velocity_threshold_slow)
|
||||
VelocityThreshold.NORMAL -> context.resources.getInteger(R.integer.gesture_velocity_threshold_normal)
|
||||
VelocityThreshold.FAST -> context.resources.getInteger(R.integer.gesture_velocity_threshold_fast)
|
||||
VelocityThreshold.VERY_FAST -> context.resources.getInteger(R.integer.gesture_velocity_threshold_very_fast)
|
||||
}.toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which detects swipes based on given [MotionEvent]s. Only supports single-finger swipes
|
||||
* and ignores additional pointers provided, if any.
|
||||
@@ -33,63 +64,116 @@ abstract class SwipeGesture {
|
||||
* @property listener The listener to report detected swipes to.
|
||||
*/
|
||||
class Detector(private val context: Context, private val listener: Listener) {
|
||||
private val eventList: MutableList<MotionEvent> = mutableListOf()
|
||||
private var indexFirst: Int = 0
|
||||
private var indexLastMoveRecognized: Int = 0
|
||||
private var pointerDataMap: MutableMap<Int, PointerData> = mutableMapOf()
|
||||
private var thresholdWidth: Double = numericValue(context, DistanceThreshold.NORMAL)
|
||||
private var unitWidth: Double = thresholdWidth / 4.0
|
||||
|
||||
var distanceThreshold: DistanceThreshold = DistanceThreshold.NORMAL
|
||||
set(value) {
|
||||
field = value
|
||||
thresholdWidth = numericValue(context, value)
|
||||
unitWidth = thresholdWidth / 4.0
|
||||
}
|
||||
var velocityThreshold: VelocityThreshold = VelocityThreshold.NORMAL
|
||||
|
||||
fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
/**
|
||||
* Method which evaluates if a given [event] is a gesture.
|
||||
*
|
||||
* @param event The MotionEvent which should be checked for a gesture.
|
||||
* @param alwaysTriggerOnMove Set to true if the moving detection algorithm should always
|
||||
* trigger, regardless of the distance from the previous event. Defaults to false.
|
||||
* @return True if the given [event] is a gesture, false otherwise.
|
||||
*/
|
||||
fun onTouchEvent(event: MotionEvent, alwaysTriggerOnMove: Boolean = false): Boolean {
|
||||
try {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN,
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
clearEventList()
|
||||
eventList.add(MotionEvent.obtainNoHistory(event))
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
resetState()
|
||||
}
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
pointerDataMap[pointerId] = PointerData().apply {
|
||||
firstX = event.getX(pointerIndex)
|
||||
firstY = event.getY(pointerIndex)
|
||||
lastX = firstX
|
||||
lastY = firstY
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
eventList.add(MotionEvent.obtainNoHistory(event))
|
||||
val lastEvent = eventList[indexLastMoveRecognized]
|
||||
val diffX = event.x - lastEvent.x
|
||||
val diffY = event.y - lastEvent.y
|
||||
val distanceThresholdNV = numericValue(distanceThreshold) / 4.0f
|
||||
return if (abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) {
|
||||
indexLastMoveRecognized = eventList.size - 1
|
||||
val direction = detectDirection(diffX.toDouble(), diffY.toDouble())
|
||||
listener.onSwipe(direction, Type.TOUCH_MOVE)
|
||||
} else {
|
||||
false
|
||||
for (pointerIndex in 0 until event.pointerCount) {
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
pointerDataMap[pointerId]?.apply {
|
||||
val absDiffX = event.getX(pointerIndex) - firstX
|
||||
val absDiffY = event.getY(pointerIndex) - firstY
|
||||
val relDiffX = event.getX(pointerIndex) - lastX
|
||||
val relDiffY = event.getY(pointerIndex) - lastY
|
||||
return if (alwaysTriggerOnMove || abs(relDiffX) > (thresholdWidth / 2.0) || abs(relDiffY) > (thresholdWidth / 2.0)) {
|
||||
lastX = event.getX(pointerIndex)
|
||||
lastY = event.getY(pointerIndex)
|
||||
val direction = detectDirection(relDiffX.toDouble(), relDiffY.toDouble())
|
||||
val newAbsUnitCountX = (absDiffX / unitWidth).toInt()
|
||||
val newAbsUnitCountY = (absDiffY / unitWidth).toInt()
|
||||
val relUnitCountX = newAbsUnitCountX - absUnitCountX
|
||||
val relUnitCountY = newAbsUnitCountY - absUnitCountY
|
||||
absUnitCountX = newAbsUnitCountX
|
||||
absUnitCountY = newAbsUnitCountY
|
||||
listener.onSwipe(Event(
|
||||
direction = direction,
|
||||
type = Type.TOUCH_MOVE,
|
||||
pointerId,
|
||||
absUnitCountX,
|
||||
absUnitCountY,
|
||||
relUnitCountX,
|
||||
relUnitCountY
|
||||
))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_POINTER_UP -> {
|
||||
val firstEvent = eventList[indexFirst]
|
||||
val diffX = event.x - firstEvent.x
|
||||
val diffY = event.y - firstEvent.y
|
||||
val distanceThresholdNV = numericValue(distanceThreshold)
|
||||
/*val velocityThresholdNV = numericValue(velocityThreshold)
|
||||
val velocity =
|
||||
((convertPixelsToDp(
|
||||
sqrt(diffX.pow(2) + diffY.pow(2)),
|
||||
context
|
||||
) / event.downTime) * 10.0f.pow(8)).toInt()*/
|
||||
clearEventList()
|
||||
// return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
|
||||
return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV)) {
|
||||
val direction = detectDirection(diffX.toDouble(), diffY.toDouble())
|
||||
listener.onSwipe(direction, Type.TOUCH_UP)
|
||||
} else {
|
||||
false
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
pointerDataMap.remove(pointerId)?.apply {
|
||||
val absDiffX = event.getX(pointerIndex) - firstX
|
||||
val absDiffY = event.getY(pointerIndex) - firstY
|
||||
/*val velocityThresholdNV = numericValue(velocityThreshold)
|
||||
val velocity =
|
||||
((convertPixelsToDp(
|
||||
sqrt(diffX.pow(2) + diffY.pow(2)),
|
||||
context
|
||||
) / event.downTime) * 10.0f.pow(8)).toInt()*/
|
||||
// return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
|
||||
return if ((abs(absDiffX) > thresholdWidth || abs(absDiffY) > thresholdWidth)) {
|
||||
val direction = detectDirection(absDiffX.toDouble(), absDiffY.toDouble())
|
||||
absUnitCountX = (absDiffX / unitWidth).toInt()
|
||||
absUnitCountY = (absDiffY / unitWidth).toInt()
|
||||
listener.onSwipe(Event(
|
||||
direction = direction,
|
||||
type = Type.TOUCH_UP,
|
||||
pointerId,
|
||||
absUnitCountX,
|
||||
absUnitCountY,
|
||||
absUnitCountX,
|
||||
absUnitCountY
|
||||
))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
clearEventList()
|
||||
resetState()
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
return false
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -134,50 +218,53 @@ abstract class SwipeGesture {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up and clears the event list.
|
||||
* Resets the state.
|
||||
*/
|
||||
private fun clearEventList() {
|
||||
for (event in eventList) {
|
||||
event.recycle()
|
||||
}
|
||||
eventList.clear()
|
||||
indexFirst = 0
|
||||
indexLastMoveRecognized = 0
|
||||
private fun resetState() {
|
||||
pointerDataMap.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a numeric value for a given [DistanceThreshold], based on the values defined in
|
||||
* the resources dimens.xml file.
|
||||
*/
|
||||
private fun numericValue(of: DistanceThreshold): Double {
|
||||
return when (of) {
|
||||
DistanceThreshold.VERY_SHORT -> context.resources.getDimension(R.dimen.gesture_distance_threshold_very_short)
|
||||
DistanceThreshold.SHORT -> context.resources.getDimension(R.dimen.gesture_distance_threshold_short)
|
||||
DistanceThreshold.NORMAL -> context.resources.getDimension(R.dimen.gesture_distance_threshold_normal)
|
||||
DistanceThreshold.LONG -> context.resources.getDimension(R.dimen.gesture_distance_threshold_long)
|
||||
DistanceThreshold.VERY_LONG -> context.resources.getDimension(R.dimen.gesture_distance_threshold_very_long)
|
||||
}.toDouble()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a numeric value for a given [VelocityThreshold], based on the values defined in
|
||||
* the resources dimens.xml file.
|
||||
*/
|
||||
private fun numericValue(of: VelocityThreshold): Double {
|
||||
return when (of) {
|
||||
VelocityThreshold.VERY_SLOW -> context.resources.getInteger(R.integer.gesture_velocity_threshold_very_slow)
|
||||
VelocityThreshold.SLOW -> context.resources.getInteger(R.integer.gesture_velocity_threshold_slow)
|
||||
VelocityThreshold.NORMAL -> context.resources.getInteger(R.integer.gesture_velocity_threshold_normal)
|
||||
VelocityThreshold.FAST -> context.resources.getInteger(R.integer.gesture_velocity_threshold_fast)
|
||||
VelocityThreshold.VERY_FAST -> context.resources.getInteger(R.integer.gesture_velocity_threshold_very_fast)
|
||||
}.toDouble()
|
||||
class PointerData {
|
||||
var firstX: Float = 0.0f
|
||||
var firstY: Float = 0.0f
|
||||
var lastX: Float = 0.0f
|
||||
var lastY: Float = 0.0f
|
||||
var absUnitCountX: Int = 0
|
||||
var absUnitCountY: Int = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface which provides an abstract callback function, which will be called for any
|
||||
* detected swipe event.
|
||||
*/
|
||||
interface Listener {
|
||||
fun onSwipe(direction: Direction, type: Type): Boolean
|
||||
fun onSwipe(event: Event): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class which describes a single gesture event.
|
||||
*/
|
||||
data class Event(
|
||||
/** The direction of the swipe. */
|
||||
val direction: Direction,
|
||||
/** The type of the swipe. */
|
||||
val type: Type,
|
||||
/** The pointer ID of this event, corresponds to the value reported by the original MotionEvent. */
|
||||
val pointerId: Int,
|
||||
/** The unit count on the x-axis, measured from the first event (ACTION_DOWN). */
|
||||
val absUnitCountX: Int,
|
||||
/** The unit count on the y-axis, measured from the first event (ACTION_DOWN). */
|
||||
val absUnitCountY: Int,
|
||||
/** The unit count on the x-axis, measured from the last event (ACTION_MOVE). */
|
||||
val relUnitCountX: Int,
|
||||
/** The unit count on the y-axis, measured from the last event (ACTION_MOVE). */
|
||||
val relUnitCountY: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* ENum which defines the direction of the detected swipe.
|
||||
*/
|
||||
enum class Direction {
|
||||
UP_LEFT,
|
||||
UP,
|
||||
@@ -189,6 +276,9 @@ abstract class SwipeGesture {
|
||||
LEFT,
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum which defines the type of the gesture.
|
||||
*/
|
||||
enum class Type {
|
||||
TOUCH_UP,
|
||||
TOUCH_MOVE;
|
||||
|
||||
@@ -40,15 +40,17 @@ object KeyCode {
|
||||
const val ARROW_RIGHT = -21
|
||||
const val ARROW_UP = -22
|
||||
const val ARROW_DOWN = -23
|
||||
const val MOVE_HOME = -24
|
||||
const val MOVE_END = -25
|
||||
const val MOVE_START_OF_PAGE = -24
|
||||
const val MOVE_END_OF_PAGE = -25
|
||||
const val MOVE_START_OF_LINE = -26
|
||||
const val MOVE_END_OF_LINE = -27
|
||||
|
||||
const val SETTINGS = -100
|
||||
const val CANCEL = -3
|
||||
const val CLEAR_INPUT = -13
|
||||
const val VOICE_INPUT = -4
|
||||
|
||||
const val DISABLED = 0
|
||||
const val UNSPECIFIED = 0
|
||||
|
||||
const val SPLIT_LAYOUT = -110
|
||||
const val MERGE_LAYOUT = -111
|
||||
@@ -83,9 +85,12 @@ object KeyCode {
|
||||
const val SWITCH_TO_TEXT_CONTEXT = -212
|
||||
const val SWITCH_TO_MEDIA_CONTEXT = -213
|
||||
const val SWITCH_TO_CLIPBOARD_CONTEXT = -214
|
||||
const val TOGGLE_ONE_HANDED_MODE = -215
|
||||
const val TOGGLE_ONE_HANDED_MODE_LEFT = -215
|
||||
const val TOGGLE_ONE_HANDED_MODE_RIGHT =-216
|
||||
const val URI_COMPONENT_TLD = -255
|
||||
|
||||
const val INTERNAL_BATCH_EDIT = -901
|
||||
|
||||
const val KESHIDA = 1600
|
||||
const val HALF_SPACE = 8204
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupSet
|
||||
import dev.patrickgold.florisboard.ime.text.key.FlorisKeyData.Companion.GROUP_DEFAULT
|
||||
|
||||
/**
|
||||
* Data class which describes a single key and its attributes.
|
||||
@@ -33,7 +34,167 @@ open class KeyData(
|
||||
var type: KeyType = KeyType.CHARACTER,
|
||||
var code: Int = 0,
|
||||
var label: String = ""
|
||||
)
|
||||
) {
|
||||
companion object {
|
||||
/** Predefined key data for [KeyCode.ARROW_DOWN] */
|
||||
val ARROW_DOWN = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.ARROW_DOWN,
|
||||
label = "arrow_down"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.ARROW_LEFT] */
|
||||
val ARROW_LEFT = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.ARROW_LEFT,
|
||||
label = "arrow_left"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.ARROW_RIGHT] */
|
||||
val ARROW_RIGHT = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.ARROW_RIGHT,
|
||||
label = "arrow_right"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.ARROW_UP] */
|
||||
val ARROW_UP = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.ARROW_UP,
|
||||
label = "arrow_up"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_COPY] */
|
||||
val CLIPBOARD_COPY = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_COPY,
|
||||
label = "clipboard_copy"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_CUT] */
|
||||
val CLIPBOARD_CUT = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_CUT,
|
||||
label = "clipboard_cut"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_PASTE] */
|
||||
val CLIPBOARD_PASTE = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_PASTE,
|
||||
label = "clipboard_paste"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_SELECT] */
|
||||
val CLIPBOARD_SELECT = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_SELECT,
|
||||
label = "clipboard_select"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_SELECT_ALL] */
|
||||
val CLIPBOARD_SELECT_ALL = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_SELECT_ALL,
|
||||
label = "clipboard_select_all"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.DELETE] */
|
||||
val DELETE = KeyData(
|
||||
type = KeyType.ENTER_EDITING,
|
||||
code = KeyCode.DELETE,
|
||||
label = "delete"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.DELETE_WORD] */
|
||||
val DELETE_WORD = KeyData(
|
||||
type = KeyType.ENTER_EDITING,
|
||||
code = KeyCode.DELETE_WORD,
|
||||
label = "delete_word"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.INTERNAL_BATCH_EDIT] */
|
||||
val INTERNAL_BATCH_EDIT = KeyData(
|
||||
type = KeyType.FUNCTION,
|
||||
code = KeyCode.INTERNAL_BATCH_EDIT,
|
||||
label = "internal_batch_edit"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.MOVE_START_OF_LINE] */
|
||||
val MOVE_START_OF_LINE = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.MOVE_START_OF_LINE,
|
||||
label = "move_start_of_line"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.MOVE_END_OF_LINE] */
|
||||
val MOVE_END_OF_LINE = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.MOVE_END_OF_LINE,
|
||||
label = "move_end_of_line"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.MOVE_START_OF_PAGE] */
|
||||
val MOVE_START_OF_PAGE = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.MOVE_START_OF_PAGE,
|
||||
label = "move_start_of_page"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.MOVE_END_OF_PAGE] */
|
||||
val MOVE_END_OF_PAGE = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.MOVE_END_OF_PAGE,
|
||||
label = "move_end_of_page"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SHOW_INPUT_METHOD_PICKER] */
|
||||
val SHOW_INPUT_METHOD_PICKER = KeyData(
|
||||
type = KeyType.FUNCTION,
|
||||
code = KeyCode.SHOW_INPUT_METHOD_PICKER,
|
||||
label = "show_input_method_picker"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SWITCH_TO_TEXT_CONTEXT] */
|
||||
val SWITCH_TO_TEXT_CONTEXT = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.SWITCH_TO_TEXT_CONTEXT,
|
||||
label = "switch_to_text_context"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SHIFT] */
|
||||
val SHIFT = KeyData(
|
||||
type = KeyType.MODIFIER,
|
||||
code = KeyCode.SHIFT,
|
||||
label = "shift"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SHIFT_LOCK] */
|
||||
val SHIFT_LOCK = KeyData(
|
||||
type = KeyType.MODIFIER,
|
||||
code = KeyCode.SHIFT_LOCK,
|
||||
label = "shift_lock"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SPACE] */
|
||||
val SPACE = KeyData(
|
||||
type = KeyType.CHARACTER,
|
||||
code = KeyCode.SPACE,
|
||||
label = "space"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.UNSPECIFIED] */
|
||||
val UNSPECIFIED = KeyData(
|
||||
type = KeyType.UNSPECIFIED,
|
||||
code = KeyCode.UNSPECIFIED,
|
||||
label = "unspecified"
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "KeyData { type=$type code=$code label=\"$label\" }"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class which describes a single key and its attributes, while also providing additional
|
||||
@@ -46,6 +207,10 @@ open class KeyData(
|
||||
* or if the key should always be visible. Defaults to [KeyVariation.ALL].
|
||||
* @property popup List of keys which will be accessible while long pressing the key. Defaults to
|
||||
* an empty set (no extended popup).
|
||||
* @property shift An alternative key to use when the keyboard caps state is true. Useful for layouts
|
||||
* such as Colemak and Dvorak. Defaults to null (don't override base uppercase key). This override
|
||||
* property should only be used to provide an uppercase variant of two else not related variants, but
|
||||
* should not be used for providing an uppercase letter (e.g. 'a' -> 'A').
|
||||
*/
|
||||
class FlorisKeyData(
|
||||
type: KeyType = KeyType.CHARACTER,
|
||||
@@ -53,7 +218,8 @@ class FlorisKeyData(
|
||||
label: String = "",
|
||||
var groupId: Int = GROUP_DEFAULT,
|
||||
var variation: KeyVariation = KeyVariation.ALL,
|
||||
var popup: PopupSet<KeyData> = PopupSet()
|
||||
var popup: PopupSet<KeyData> = PopupSet(),
|
||||
var shift: KeyData? = null
|
||||
) : KeyData(type, code, label) {
|
||||
companion object {
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.squareup.moshi.FromJson
|
||||
import java.util.*
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.squareup.moshi.FromJson
|
||||
import java.util.*
|
||||
|
||||
enum class KeyVariation {
|
||||
ALL,
|
||||
@@ -27,9 +27,8 @@ enum class KeyVariation {
|
||||
URI;
|
||||
|
||||
companion object {
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun fromString(string: String): KeyVariation {
|
||||
return valueOf(string.toUpperCase())
|
||||
return valueOf(string.toUpperCase(Locale.ENGLISH))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,11 @@ package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.*
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Outline
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.PaintDrawable
|
||||
import android.os.Handler
|
||||
@@ -32,7 +36,10 @@ import com.google.android.flexbox.FlexboxLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.ImeOptions
|
||||
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupManager
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
@@ -40,8 +47,11 @@ import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeValue
|
||||
import dev.patrickgold.florisboard.util.*
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
import dev.patrickgold.florisboard.util.cancelAll
|
||||
import dev.patrickgold.florisboard.util.postDelayed
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* View class for managing the rendering and the events of a single keyboard key.
|
||||
@@ -54,17 +64,20 @@ import java.util.*
|
||||
@SuppressLint("ViewConstructor")
|
||||
class KeyView(
|
||||
private val keyboardView: KeyboardView,
|
||||
val data: FlorisKeyData
|
||||
val data: FlorisKeyData,
|
||||
private val florisboard: FlorisBoard?
|
||||
) : View(keyboardView.context), SwipeGesture.Listener, ThemeManager.OnThemeUpdatedListener {
|
||||
private var isKeyPressed: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
updateKeyPressedBackground()
|
||||
}
|
||||
private var initSelectionStart: Int = 0
|
||||
private var initSelectionEnd: Int = 0
|
||||
private var hasTriggeredGestureMove: Boolean = false
|
||||
private var keyHintMode: KeyHintMode = KeyHintMode.DISABLED
|
||||
private val longKeyPressHandler: Handler = Handler(context.mainLooper)
|
||||
private val repeatedKeyPressHandler: Handler = Handler(context.mainLooper)
|
||||
val popupManager = PopupManager<KeyboardView, KeyView>(keyboardView, florisboard?.popupLayerView)
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private var shouldBlockNextKeyCode: Boolean = false
|
||||
|
||||
@@ -95,24 +108,34 @@ class KeyView(
|
||||
isFakeBoldText = false
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = resources.getDimension(R.dimen.key_textHintSize)
|
||||
typeface = Typeface.DEFAULT
|
||||
typeface = Typeface.MONOSPACE
|
||||
}
|
||||
private val tempRect: Rect = Rect()
|
||||
private var themeValueCache: ThemeValueCache = ThemeValueCache()
|
||||
|
||||
var florisboard: FlorisBoard? = null
|
||||
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
|
||||
val swipeGestureDetector = SwipeGesture.Detector(context, this)
|
||||
var touchHitBox: Rect = Rect(-1, -1, -1, -1)
|
||||
|
||||
init {
|
||||
layoutParams = FlexboxLayout.LayoutParams(
|
||||
FlexboxLayout.LayoutParams.WRAP_CONTENT, FlexboxLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
val keyMarginH: Int
|
||||
val keyMarginV: Int
|
||||
|
||||
if (keyboardView.isSmartbarKeyboardView) {
|
||||
keyMarginH = resources.getDimension(R.dimen.key_marginH).toInt()
|
||||
keyMarginV = resources.getDimension(R.dimen.key_marginV).toInt()
|
||||
} else {
|
||||
keyMarginV = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingVertical, context).toInt()
|
||||
keyMarginH = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
|
||||
}
|
||||
|
||||
setMargins(
|
||||
resources.getDimension((R.dimen.key_marginH)).toInt(),
|
||||
resources.getDimension(R.dimen.key_marginV).toInt(),
|
||||
resources.getDimension((R.dimen.key_marginH)).toInt(),
|
||||
resources.getDimension(R.dimen.key_marginV).toInt()
|
||||
keyMarginH,
|
||||
keyMarginV,
|
||||
keyMarginH,
|
||||
keyMarginV
|
||||
)
|
||||
flexShrink = when (keyboardView.computedLayout?.mode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
@@ -146,13 +169,13 @@ class KeyView(
|
||||
setPadding(0, 0, 0, 0)
|
||||
|
||||
background = backgroundDrawable
|
||||
elevation = if(themeValueCache.shouldShowBorder) 4.0f else 0.0f
|
||||
elevation = if (themeValueCache.shouldShowBorder) 4.0f else 0.0f
|
||||
|
||||
if (prefs.keyboard.hintedNumberRowMode != KeyHintMode.DISABLED && data.popup.hint?.type == KeyType.NUMERIC) {
|
||||
keyHintMode = prefs.keyboard.hintedNumberRowMode
|
||||
}
|
||||
if (prefs.keyboard.hintedSymbolsMode != KeyHintMode.DISABLED && data.popup.hint?.type == KeyType.CHARACTER) {
|
||||
keyHintMode = prefs.keyboard.hintedNumberRowMode
|
||||
keyHintMode = prefs.keyboard.hintedSymbolsMode
|
||||
}
|
||||
|
||||
updateKeyPressedBackground()
|
||||
@@ -162,20 +185,34 @@ class KeyView(
|
||||
* Creates a label text from the given [keyData].
|
||||
*
|
||||
* @param keyData Optional. The key data to generate the label from. Defaults to [data].
|
||||
* @return The generated label.
|
||||
* @param caps If the generated text should be uppercase (true) or in lowercase (false).
|
||||
* Defaults to FlorisBoard's TextInputManager's caps state or false. Ignored when the passed
|
||||
* [keyData] is a TLD, in which case always the lower case variant is returned.
|
||||
* @param subtype The subtype for which this label should be created. Defaults to
|
||||
* [Subtype.DEFAULT]. Ignored when the passed [keyData] is a TLD.
|
||||
* @return The generated label ready for usage in the front-end UI.
|
||||
*/
|
||||
fun getComputedLetter(keyData: KeyData = data): String {
|
||||
if (keyData.code == KeyCode.URI_COMPONENT_TLD) {
|
||||
return when (florisboard?.textInputManager?.caps) {
|
||||
true -> keyData.label.toUpperCase(Locale.getDefault())
|
||||
else -> keyData.label.toLowerCase(Locale.getDefault())
|
||||
fun getComputedLetter(
|
||||
keyData: KeyData = data,
|
||||
caps: Boolean = florisboard?.textInputManager?.caps ?: false,
|
||||
subtype: Subtype = florisboard?.activeSubtype ?: Subtype.DEFAULT
|
||||
): String {
|
||||
return if (caps && keyData is FlorisKeyData && keyData.shift != null) {
|
||||
(keyData.shift!!.code.toChar()).toString()
|
||||
} else {
|
||||
when (data.code) {
|
||||
KeyCode.URI_COMPONENT_TLD -> keyData.label.toLowerCase(Locale.ENGLISH)
|
||||
else -> {
|
||||
val labelText = (keyData.code.toChar()).toString()
|
||||
val locale = if (subtype.locale.language == "el") { Locale.getDefault() } else { subtype.locale }
|
||||
if (caps) {
|
||||
labelText.toUpperCase(locale)
|
||||
} else {
|
||||
labelText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val label = (keyData.code.toChar()).toString()
|
||||
return when {
|
||||
florisboard?.textInputManager?.caps ?: false -> label.toUpperCase(Locale.getDefault())
|
||||
else -> label
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,69 +241,80 @@ class KeyView(
|
||||
*/
|
||||
fun onFlorisTouchEvent(event: MotionEvent?): Boolean {
|
||||
if (event == null || !isEnabled) return false
|
||||
if (swipeGestureDetector.onTouchEvent(event)) {
|
||||
val florisboard = florisboard ?: return false
|
||||
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
|
||||
val alwaysTriggerOnMove = (hasTriggeredGestureMove
|
||||
&& (data.code == KeyCode.DELETE && prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_CHARACTERS_PRECISELY
|
||||
|| data.code == KeyCode.SPACE))
|
||||
if (swipeGestureDetector.onTouchEvent(event, alwaysTriggerOnMove)) {
|
||||
isKeyPressed = false
|
||||
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
longKeyPressHandler.cancelAll()
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
keyboardView.popupManager.hide()
|
||||
popupManager.hide()
|
||||
return true
|
||||
}
|
||||
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (data.code == KeyCode.SHIFT) {
|
||||
isKeyPressed = true
|
||||
florisboard?.keyPressVibrate()
|
||||
florisboard?.keyPressSound(data)
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
} else {
|
||||
val delayMillis = prefs.keyboard.longPressDelay.toLong()
|
||||
hasTriggeredGestureMove = false
|
||||
shouldBlockNextKeyCode = false
|
||||
florisboard?.prefs?.keyboard?.let {
|
||||
if (it.popupEnabled){
|
||||
keyboardView.popupManager.show(this, keyHintMode)
|
||||
}
|
||||
}
|
||||
isKeyPressed = true
|
||||
florisboard?.keyPressVibrate()
|
||||
florisboard?.keyPressSound(data)
|
||||
when (data.code) {
|
||||
KeyCode.ARROW_DOWN,
|
||||
KeyCode.ARROW_LEFT,
|
||||
KeyCode.ARROW_RIGHT,
|
||||
KeyCode.ARROW_UP,
|
||||
KeyCode.DELETE -> {
|
||||
repeatedKeyPressHandler.postAtScheduledRate(delayMillis, 25) {
|
||||
if (isKeyPressed) {
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
} else {
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.down(data))
|
||||
isKeyPressed = true
|
||||
val delayMillis = prefs.keyboard.longPressDelay.toLong()
|
||||
hasTriggeredGestureMove = false
|
||||
shouldBlockNextKeyCode = false
|
||||
if (florisboard.prefs.keyboard.popupEnabled) {
|
||||
popupManager.show(this, keyHintMode)
|
||||
}
|
||||
isKeyPressed = true
|
||||
florisboard.keyPressVibrate()
|
||||
florisboard.keyPressSound(data)
|
||||
|
||||
when (data.code) {
|
||||
KeyCode.SPACE -> {
|
||||
initSelectionStart = florisboard.activeEditorInstance.selection.start
|
||||
initSelectionEnd = florisboard.activeEditorInstance.selection.end
|
||||
longKeyPressHandler.postDelayed((delayMillis * 2.5f).toLong()) {
|
||||
when (prefs.gestures.spaceBarLongPress) {
|
||||
SwipeAction.NO_ACTION,
|
||||
SwipeAction.INSERT_SPACE -> {
|
||||
}
|
||||
else -> {
|
||||
this.florisboard.executeSwipeAction(prefs.gestures.spaceBarLongPress)
|
||||
shouldBlockNextKeyCode = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
longKeyPressHandler.postDelayed(delayMillis) {
|
||||
if (data.popup.isNotEmpty()) {
|
||||
keyboardView.popupManager.extend(this, keyHintMode)
|
||||
KeyCode.SHIFT -> {
|
||||
longKeyPressHandler.postDelayed((delayMillis * 2.5).toLong()) {
|
||||
this.florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.SHIFT_LOCK))
|
||||
}
|
||||
if (data.code == KeyCode.SPACE) {
|
||||
florisboard?.textInputManager?.sendKeyPress(
|
||||
KeyData(
|
||||
type = KeyType.FUNCTION,
|
||||
code = KeyCode.SHOW_INPUT_METHOD_PICKER,
|
||||
)
|
||||
)
|
||||
}
|
||||
KeyCode.LANGUAGE_SWITCH -> {
|
||||
longKeyPressHandler.postDelayed((delayMillis * 2.0).toLong()) {
|
||||
shouldBlockNextKeyCode = true
|
||||
this.florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.SHOW_INPUT_METHOD_PICKER))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
longKeyPressHandler.postDelayed(delayMillis) {
|
||||
if (data.popup.isNotEmpty()) {
|
||||
popupManager.extend(this@KeyView, keyHintMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (keyboardView.popupManager.isShowingExtendedPopup) {
|
||||
if (popupManager.isShowingExtendedPopup) {
|
||||
val isPointerWithinBounds =
|
||||
keyboardView.popupManager.propagateMotionEvent(this, event)
|
||||
popupManager.propagateMotionEvent(this, event)
|
||||
if (!isPointerWithinBounds && !shouldBlockNextKeyCode) {
|
||||
keyboardView.dismissActiveKeyViewReference()
|
||||
keyboardView.dismissActiveKeyViewReference(pointerId)
|
||||
}
|
||||
} else {
|
||||
val parent = parent as ViewGroup
|
||||
@@ -276,32 +324,50 @@ class KeyView(
|
||||
|| event.y > 1.35f * measuredHeight
|
||||
) {
|
||||
if (!shouldBlockNextKeyCode) {
|
||||
keyboardView.dismissActiveKeyViewReference()
|
||||
keyboardView.dismissActiveKeyViewReference(pointerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
MotionEvent.ACTION_UP -> {
|
||||
longKeyPressHandler.cancelAll()
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
if (data.code != KeyCode.SHIFT) {
|
||||
if (hasTriggeredGestureMove && data.code == KeyCode.DELETE) {
|
||||
hasTriggeredGestureMove = false
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
deleteBackwards()
|
||||
if (hasTriggeredGestureMove && data.code == KeyCode.DELETE) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
florisboard.activeEditorInstance.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
deleteBackwards()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val retData = popupManager.getActiveKeyData(this)
|
||||
if (!shouldBlockNextKeyCode && retData != null) {
|
||||
if (retData == data) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.up(data))
|
||||
} else {
|
||||
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(retData))
|
||||
}
|
||||
} else {
|
||||
val retData = keyboardView.popupManager.getActiveKeyData(this)
|
||||
if (event.actionMasked != MotionEvent.ACTION_CANCEL && !shouldBlockNextKeyCode && retData != null) {
|
||||
florisboard?.textInputManager?.sendKeyPress(retData)
|
||||
} else {
|
||||
shouldBlockNextKeyCode = false
|
||||
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
keyboardView.popupManager.hide()
|
||||
}
|
||||
popupManager.hide()
|
||||
}
|
||||
shouldBlockNextKeyCode = false
|
||||
hasTriggeredGestureMove = false
|
||||
isKeyPressed = false
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
longKeyPressHandler.cancelAll()
|
||||
if (data.code != KeyCode.SHIFT) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
popupManager.hide()
|
||||
shouldBlockNextKeyCode = false
|
||||
hasTriggeredGestureMove = false
|
||||
isKeyPressed = false
|
||||
}
|
||||
else -> return false
|
||||
@@ -313,47 +379,33 @@ class KeyView(
|
||||
* Swipe event handler. Listens to touch_move left/right swipes and triggers the swipe action
|
||||
* defined in the prefs.
|
||||
*/
|
||||
override fun onSwipe(direction: SwipeGesture.Direction, type: SwipeGesture.Type): Boolean {
|
||||
override fun onSwipe(event: SwipeGesture.Event): Boolean {
|
||||
val florisboard = florisboard ?: return false
|
||||
return when (data.code) {
|
||||
KeyCode.DELETE -> when (type) {
|
||||
SwipeGesture.Type.TOUCH_MOVE -> when (direction) {
|
||||
SwipeGesture.Direction.LEFT -> when (prefs.gestures.deleteKeySwipeLeft) {
|
||||
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
setSelection(
|
||||
if (selection.start > 0) { selection.start - 1 } else { selection.start },
|
||||
selection.end
|
||||
)
|
||||
}
|
||||
hasTriggeredGestureMove = true
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
KeyCode.DELETE -> when (event.type) {
|
||||
SwipeGesture.Type.TOUCH_MOVE -> when (prefs.gestures.deleteKeySwipeLeft) {
|
||||
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
|
||||
florisboard.activeEditorInstance.apply {
|
||||
selection.updateAndNotify(
|
||||
(selection.end + event.absUnitCountX + 1).coerceIn(0, selection.end),
|
||||
selection.end
|
||||
)
|
||||
}
|
||||
SwipeAction.DELETE_WORDS_PRECISELY -> {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
hasTriggeredGestureMove = true
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
SwipeAction.DELETE_WORDS_PRECISELY -> when (event.direction) {
|
||||
SwipeGesture.Direction.LEFT -> {
|
||||
florisboard.activeEditorInstance.apply {
|
||||
leftAppendWordToSelection()
|
||||
}
|
||||
|
||||
hasTriggeredGestureMove = true
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
SwipeGesture.Direction.RIGHT -> when (prefs.gestures.deleteKeySwipeLeft) {
|
||||
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
setSelection(
|
||||
if (selection.start < selection.end) { selection.start + 1 } else { selection.start },
|
||||
selection.end
|
||||
)
|
||||
}
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
|
||||
SwipeAction.DELETE_WORDS_PRECISELY -> {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
SwipeGesture.Direction.RIGHT -> {
|
||||
florisboard.activeEditorInstance.apply {
|
||||
leftPopWordFromSelection()
|
||||
}
|
||||
shouldBlockNextKeyCode = true
|
||||
@@ -365,20 +417,45 @@ class KeyView(
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
KeyCode.SPACE -> when (type) {
|
||||
SwipeGesture.Type.TOUCH_MOVE -> when (direction) {
|
||||
KeyCode.SPACE -> when (event.type) {
|
||||
SwipeGesture.Type.TOUCH_MOVE -> when (event.direction) {
|
||||
SwipeGesture.Direction.UP -> {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeUp)
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
if (event.absUnitCountY.times(-1) >= 6) {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeUp)
|
||||
hasTriggeredGestureMove = true
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
SwipeGesture.Direction.LEFT -> {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
|
||||
if (prefs.gestures.spaceBarSwipeLeft == SwipeAction.MOVE_CURSOR_LEFT) {
|
||||
abs(event.relUnitCountX).let {
|
||||
val count = if (!hasTriggeredGestureMove) { it - 1 } else { it }
|
||||
if (count > 0) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.ARROW_LEFT, count))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
|
||||
}
|
||||
hasTriggeredGestureMove = true
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
SwipeGesture.Direction.RIGHT -> {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
|
||||
if (prefs.gestures.spaceBarSwipeRight == SwipeAction.MOVE_CURSOR_RIGHT) {
|
||||
abs(event.relUnitCountX).let {
|
||||
val count = if (!hasTriggeredGestureMove) { it - 1 } else { it }
|
||||
if (count > 0) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.ARROW_RIGHT, count))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
|
||||
}
|
||||
hasTriggeredGestureMove = true
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
@@ -397,25 +474,49 @@ class KeyView(
|
||||
* by Devunwired
|
||||
*/
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
desiredWidth = when (keyboardView.computedLayout?.mode) {
|
||||
|
||||
val keyMarginH: Int
|
||||
val keyMarginV: Int
|
||||
|
||||
if (keyboardView.isSmartbarKeyboardView) {
|
||||
keyMarginH = resources.getDimension(R.dimen.key_marginH).toInt()
|
||||
keyMarginV = resources.getDimension(R.dimen.key_marginV).toInt()
|
||||
} else {
|
||||
keyMarginV = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingVertical, context).toInt()
|
||||
keyMarginH = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
|
||||
}
|
||||
|
||||
(layoutParams as ViewGroup.MarginLayoutParams).setMargins(
|
||||
keyMarginH,
|
||||
keyMarginV,
|
||||
keyMarginH,
|
||||
keyMarginV
|
||||
)
|
||||
|
||||
desiredWidth = (keyboardView.desiredKeyWidth * when (keyboardView.computedLayout?.mode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> (keyboardView.desiredKeyWidth * 2.68f).toInt()
|
||||
KeyboardMode.PHONE2 -> 2.68f
|
||||
KeyboardMode.NUMERIC_ADVANCED -> when (data.code) {
|
||||
44, 46 -> keyboardView.desiredKeyWidth
|
||||
KeyCode.VIEW_SYMBOLS, 61 -> (keyboardView.desiredKeyWidth * 1.34f).toInt()
|
||||
else -> (keyboardView.desiredKeyWidth * 1.56f).toInt()
|
||||
44, 46 -> 1.00f
|
||||
KeyCode.VIEW_SYMBOLS, 61 -> 1.34f
|
||||
else -> 1.56f
|
||||
}
|
||||
else -> when (data.code) {
|
||||
KeyCode.SHIFT,
|
||||
KeyCode.DELETE ->
|
||||
if ((keyboardView.computedLayout?.arrangement?.get(2)?.size ?: 0) > 10) {
|
||||
1.12f
|
||||
} else {
|
||||
1.56f
|
||||
}
|
||||
KeyCode.VIEW_CHARACTERS,
|
||||
KeyCode.VIEW_SYMBOLS,
|
||||
KeyCode.VIEW_SYMBOLS2,
|
||||
KeyCode.DELETE,
|
||||
KeyCode.ENTER -> (keyboardView.desiredKeyWidth * 1.56f).toInt()
|
||||
else -> keyboardView.desiredKeyWidth
|
||||
KeyCode.ENTER -> 1.56f
|
||||
else -> 1.00f
|
||||
}
|
||||
}
|
||||
}).toInt()
|
||||
desiredHeight = keyboardView.desiredKeyHeight
|
||||
|
||||
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
|
||||
@@ -478,10 +579,9 @@ class KeyView(
|
||||
private fun updateEnabledState() {
|
||||
isEnabled = when (data.code) {
|
||||
KeyCode.CLIPBOARD_COPY,
|
||||
KeyCode.CLIPBOARD_CUT -> {
|
||||
florisboard?.activeEditorInstance?.selection?.isSelectionMode == true &&
|
||||
florisboard?.activeEditorInstance?.isRawInputEditor == false
|
||||
}
|
||||
KeyCode.CLIPBOARD_CUT -> (florisboard != null
|
||||
&& florisboard.activeEditorInstance.selection.isSelectionMode
|
||||
&& !florisboard.activeEditorInstance.isRawInputEditor)
|
||||
KeyCode.CLIPBOARD_PASTE -> florisboard?.clipboardManager?.hasPrimaryClip() == true
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> {
|
||||
florisboard?.activeEditorInstance?.isRawInputEditor == false
|
||||
@@ -494,35 +594,53 @@ class KeyView(
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
if (keyboardView.isSmartbarKeyboardView) {
|
||||
themeValueCache.apply {
|
||||
keyBackground = theme.getAttr(Theme.Attr.SMARTBAR_BACKGROUND)
|
||||
keyBackgroundPressed = theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_BACKGROUND)
|
||||
keyForeground = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND)
|
||||
keyForegroundAlt = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND_ALT)
|
||||
keyForegroundPressed = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND)
|
||||
shouldShowBorder = false
|
||||
}
|
||||
} else {
|
||||
val label = data.label
|
||||
val capsSpecific = when {
|
||||
florisboard?.textInputManager?.capsLock == true -> {
|
||||
"capslock"
|
||||
}
|
||||
florisboard?.textInputManager?.caps == true -> {
|
||||
"caps"
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
when {
|
||||
keyboardView.isLoadingPlaceholderKeyboard -> {
|
||||
val label = data.label
|
||||
themeValueCache.apply {
|
||||
shouldShowBorder = theme.getAttr(Theme.Attr.KEY_SHOW_BORDER, label).toOnOff().state
|
||||
keyBackground = if (shouldShowBorder) {
|
||||
theme.getAttr(Theme.Attr.KEY_BACKGROUND, label)
|
||||
} else {
|
||||
theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_BACKGROUND, label)
|
||||
}
|
||||
keyBackgroundPressed = theme.getAttr(Theme.Attr.KEY_BACKGROUND_PRESSED, label)
|
||||
keyForeground = keyBackground
|
||||
keyForegroundAlt = ThemeValue.SolidColor(0)
|
||||
keyForegroundPressed = keyBackgroundPressed
|
||||
}
|
||||
}
|
||||
themeValueCache.apply {
|
||||
keyBackground = theme.getAttr(Theme.Attr.KEY_BACKGROUND, label, capsSpecific)
|
||||
keyBackgroundPressed = theme.getAttr(Theme.Attr.KEY_BACKGROUND_PRESSED, label, capsSpecific)
|
||||
keyForeground = theme.getAttr(Theme.Attr.KEY_FOREGROUND, label, capsSpecific)
|
||||
keyForegroundAlt = ThemeValue.SolidColor(0)
|
||||
keyForegroundPressed = theme.getAttr(Theme.Attr.KEY_FOREGROUND_PRESSED, label, capsSpecific)
|
||||
shouldShowBorder = theme.getAttr(Theme.Attr.KEY_SHOW_BORDER, label, capsSpecific).toOnOff().state
|
||||
keyboardView.isSmartbarKeyboardView -> {
|
||||
themeValueCache.apply {
|
||||
keyBackground = theme.getAttr(Theme.Attr.SMARTBAR_BACKGROUND)
|
||||
keyBackgroundPressed = theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_BACKGROUND)
|
||||
keyForeground = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND)
|
||||
keyForegroundAlt = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND_ALT)
|
||||
keyForegroundPressed = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND)
|
||||
shouldShowBorder = false
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val label = data.label
|
||||
val capsSpecific = when {
|
||||
florisboard?.textInputManager?.capsLock == true -> {
|
||||
"capslock"
|
||||
}
|
||||
florisboard?.textInputManager?.caps == true -> {
|
||||
"caps"
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
themeValueCache.apply {
|
||||
keyBackground = theme.getAttr(Theme.Attr.KEY_BACKGROUND, label, capsSpecific)
|
||||
keyBackgroundPressed = theme.getAttr(Theme.Attr.KEY_BACKGROUND_PRESSED, label, capsSpecific)
|
||||
keyForeground = theme.getAttr(Theme.Attr.KEY_FOREGROUND, label, capsSpecific)
|
||||
keyForegroundAlt = ThemeValue.SolidColor(0)
|
||||
keyForegroundPressed = theme.getAttr(Theme.Attr.KEY_FOREGROUND_PRESSED, label, capsSpecific)
|
||||
shouldShowBorder = theme.getAttr(Theme.Attr.KEY_SHOW_BORDER, label, capsSpecific).toOnOff().state
|
||||
}
|
||||
}
|
||||
}
|
||||
updateKeyPressedBackground()
|
||||
@@ -549,8 +667,17 @@ class KeyView(
|
||||
touchHitBox.set(-1, -1, -1, -1)
|
||||
} else {
|
||||
val parent = parent as ViewGroup
|
||||
val keyMarginH = resources.getDimension((R.dimen.key_marginH)).toInt()
|
||||
val keyMarginV = resources.getDimension((R.dimen.key_marginV)).toInt()
|
||||
|
||||
val keyMarginH: Int
|
||||
val keyMarginV: Int
|
||||
|
||||
if (keyboardView.isSmartbarKeyboardView) {
|
||||
keyMarginH = resources.getDimension(R.dimen.key_marginH).toInt()
|
||||
keyMarginV = resources.getDimension(R.dimen.key_marginV).toInt()
|
||||
} else {
|
||||
keyMarginV = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingVertical, context).toInt()
|
||||
keyMarginH = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
|
||||
}
|
||||
|
||||
touchHitBox.apply {
|
||||
left = when (this@KeyView) {
|
||||
@@ -576,22 +703,48 @@ class KeyView(
|
||||
when (data.code) {
|
||||
KeyCode.SWITCH_TO_TEXT_CONTEXT,
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
|
||||
visibility = if (florisboard?.shouldShowLanguageSwitch() == true) {
|
||||
GONE
|
||||
} else {
|
||||
VISIBLE
|
||||
val tempUtilityKeyAction = when {
|
||||
prefs.keyboard.utilityKeyEnabled -> prefs.keyboard.utilityKeyAction
|
||||
else -> UtilityKeyAction.DISABLED
|
||||
}
|
||||
visibility = when (tempUtilityKeyAction) {
|
||||
UtilityKeyAction.DISABLED,
|
||||
UtilityKeyAction.SWITCH_LANGUAGE,
|
||||
UtilityKeyAction.SWITCH_KEYBOARD_APP -> GONE
|
||||
UtilityKeyAction.SWITCH_TO_EMOJIS -> VISIBLE
|
||||
UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS ->
|
||||
if (florisboard?.shouldShowLanguageSwitch() == true) {
|
||||
GONE
|
||||
} else {
|
||||
VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode.LANGUAGE_SWITCH -> {
|
||||
visibility = if (florisboard?.shouldShowLanguageSwitch() == true) {
|
||||
VISIBLE
|
||||
} else {
|
||||
GONE
|
||||
val tempUtilityKeyAction = when {
|
||||
prefs.keyboard.utilityKeyEnabled -> prefs.keyboard.utilityKeyAction
|
||||
else -> UtilityKeyAction.DISABLED
|
||||
}
|
||||
visibility = when (tempUtilityKeyAction) {
|
||||
UtilityKeyAction.DISABLED,
|
||||
UtilityKeyAction.SWITCH_TO_EMOJIS -> GONE
|
||||
UtilityKeyAction.SWITCH_LANGUAGE,
|
||||
UtilityKeyAction.SWITCH_KEYBOARD_APP -> VISIBLE
|
||||
UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS ->
|
||||
if (florisboard?.shouldShowLanguageSwitch() == true) {
|
||||
VISIBLE
|
||||
} else {
|
||||
GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> if (data.variation != KeyVariation.ALL) {
|
||||
val keyVariation = florisboard?.textInputManager?.keyVariation ?: KeyVariation.NORMAL
|
||||
visibility = if (data.variation == keyVariation) { VISIBLE } else { GONE }
|
||||
visibility = if (data.variation == keyVariation) {
|
||||
VISIBLE
|
||||
} else {
|
||||
GONE
|
||||
}
|
||||
updateTouchHitBox()
|
||||
}
|
||||
}
|
||||
@@ -631,13 +784,9 @@ class KeyView(
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the key label / drawable.
|
||||
* Computes the labels and drawables needed to draw the key.
|
||||
*/
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
canvas ?: return
|
||||
|
||||
private fun computeLabelsAndDrawables() {
|
||||
if (data.type == KeyType.CHARACTER && data.code != KeyCode.SPACE
|
||||
&& data.code != KeyCode.HALF_SPACE && data.code != KeyCode.KESHIDA || data.type == KeyType.NUMERIC
|
||||
) {
|
||||
@@ -711,7 +860,8 @@ class KeyView(
|
||||
KeyboardMode.CHARACTERS -> {
|
||||
label = florisboard?.activeSubtype?.locale?.displayName
|
||||
}
|
||||
else -> {}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
|
||||
@@ -745,6 +895,17 @@ class KeyView(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the key label / drawable.
|
||||
*/
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
canvas ?: return
|
||||
|
||||
computeLabelsAndDrawables()
|
||||
|
||||
// Draw drawable
|
||||
val drawable = drawable
|
||||
@@ -780,11 +941,11 @@ class KeyView(
|
||||
}
|
||||
else -> when {
|
||||
(data.type == KeyType.CHARACTER || data.type == KeyType.NUMERIC) &&
|
||||
data.code != KeyCode.SPACE -> {
|
||||
data.code != KeyCode.SPACE -> {
|
||||
val cachedTextSize = setTextSizeFor(
|
||||
labelPaint,
|
||||
desiredWidth - (2.6f * drawablePaddingH),
|
||||
desiredHeight - (3.4f * drawablePaddingV),
|
||||
measuredWidth - (2.6f * drawablePaddingH),
|
||||
measuredHeight - (3.4f * drawablePaddingV),
|
||||
// Note: taking a "X" here because it is one of the biggest letters and
|
||||
// the keys must have the same base character for calculation, else
|
||||
// they will all look different and weird...
|
||||
@@ -799,7 +960,7 @@ class KeyView(
|
||||
else -> 1.0f
|
||||
}
|
||||
)
|
||||
keyboardView.popupManager.keyPopupTextSize = cachedTextSize
|
||||
popupManager.keyPopupTextSize = cachedTextSize
|
||||
}
|
||||
else -> {
|
||||
setTextSizeFor(
|
||||
@@ -816,9 +977,17 @@ class KeyView(
|
||||
}
|
||||
}
|
||||
}
|
||||
labelPaint.color = themeValueCache.keyForeground.toSolidColor().color
|
||||
labelPaint.color = if (isKeyPressed && isEnabled) {
|
||||
themeValueCache.keyForegroundPressed.toSolidColor().color
|
||||
} else {
|
||||
themeValueCache.keyForeground.toSolidColor().color
|
||||
}
|
||||
labelPaint.alpha = if (keyboardView.computedLayout?.mode == KeyboardMode.CHARACTERS &&
|
||||
data.code == KeyCode.SPACE) { 120 } else { 255 }
|
||||
data.code == KeyCode.SPACE) {
|
||||
120
|
||||
} else {
|
||||
255
|
||||
}
|
||||
val centerX = measuredWidth / 2.0f
|
||||
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2
|
||||
if (label.contains("\n")) {
|
||||
@@ -836,15 +1005,15 @@ class KeyView(
|
||||
if (hintedLabel != null) {
|
||||
setTextSizeFor(
|
||||
hintedLabelPaint,
|
||||
desiredWidth * 1.0f / 6.0f,
|
||||
desiredHeight * 1.0f / 6.0f,
|
||||
desiredWidth * 1.0f / 5.0f,
|
||||
desiredHeight * 1.0f / 5.0f,
|
||||
// Note: taking a "X" here because it is one of the biggest letters and
|
||||
// the keys must have the same base character for calculation, else
|
||||
// they will all look different and weird...
|
||||
"X"
|
||||
)
|
||||
hintedLabelPaint.color = labelPaint.color
|
||||
hintedLabelPaint.alpha = 120
|
||||
hintedLabelPaint.alpha = 170
|
||||
val centerX = measuredWidth * 5.0f / 6.0f
|
||||
val centerY = measuredHeight * 1.0f / 6.0f + (hintedLabelPaint.textSize - hintedLabelPaint.descent()) / 2
|
||||
canvas.drawText(hintedLabel, centerX, centerY, hintedLabelPaint)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Enum for declaring the utility key actions.
|
||||
*/
|
||||
enum class UtilityKeyAction {
|
||||
SWITCH_TO_EMOJIS,
|
||||
SWITCH_LANGUAGE,
|
||||
SWITCH_KEYBOARD_APP,
|
||||
DYNAMIC_SWITCH_LANGUAGE_EMOJIS,
|
||||
DISABLED;
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String): UtilityKeyAction {
|
||||
return valueOf(string.toUpperCase(Locale.ENGLISH))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,17 +23,25 @@ import com.google.android.flexbox.FlexDirection
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
|
||||
/**
|
||||
* This class' sole purpose is to manage the layout within a row of [KeyboardView]. No logic is
|
||||
* handled in this class.
|
||||
*/
|
||||
class KeyboardRowView(context: Context) : FlexboxLayout(context) {
|
||||
class KeyboardRowView(context: Context, val keyboardView: KeyboardView) : FlexboxLayout(context) {
|
||||
init {
|
||||
val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
val keyMarginH = if (keyboardView.isSmartbarKeyboardView){
|
||||
resources.getDimension(R.dimen.key_marginH).toInt()
|
||||
}else{
|
||||
ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
|
||||
}
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
|
||||
setMargins(
|
||||
resources.getDimension(R.dimen.keyboard_row_marginH).toInt(), 0,
|
||||
resources.getDimension(R.dimen.keyboard_row_marginH).toInt(), 0
|
||||
keyMarginH, 0,
|
||||
keyMarginH, 0
|
||||
)
|
||||
}
|
||||
flexDirection = FlexDirection.ROW
|
||||
@@ -49,4 +57,19 @@ class KeyboardRowView(context: Context) : FlexboxLayout(context) {
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
val keyMarginH = if (keyboardView.isSmartbarKeyboardView){
|
||||
resources.getDimension(R.dimen.key_marginH).toInt()
|
||||
}else{
|
||||
ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
|
||||
}
|
||||
(layoutParams as MarginLayoutParams).setMargins(
|
||||
keyMarginH, 0,
|
||||
keyMarginH, 0
|
||||
)
|
||||
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,15 +27,17 @@ import androidx.core.view.children
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupManager
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyView
|
||||
import dev.patrickgold.florisboard.ime.text.layout.ComputedLayoutData
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyView
|
||||
import dev.patrickgold.florisboard.ime.text.layout.ComputedLayoutData
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
@@ -48,10 +50,8 @@ import kotlin.math.roundToInt
|
||||
*/
|
||||
class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Listener,
|
||||
ThemeManager.OnThemeUpdatedListener {
|
||||
private var activeKeyView: KeyView? = null
|
||||
private var activePointerId: Int? = null
|
||||
private var activeX: Float = 0.0f
|
||||
private var activeY: Float = 0.0f
|
||||
private var activeKeyViews: MutableMap<Int, KeyView> = mutableMapOf()
|
||||
private var initialKeyCodes: MutableMap<Int, Int> = mutableMapOf()
|
||||
|
||||
var computedLayout: ComputedLayoutData? = null
|
||||
set(v) {
|
||||
@@ -61,10 +61,9 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
var desiredKeyWidth: Int = resources.getDimension(R.dimen.key_width).toInt()
|
||||
var desiredKeyHeight: Int = resources.getDimension(R.dimen.key_height).toInt()
|
||||
var florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private var initialKeyCode: Int = 0
|
||||
private val isPreviewMode: Boolean
|
||||
val isSmartbarKeyboardView: Boolean
|
||||
var popupManager = PopupManager<KeyboardView, KeyView>(this, florisboard?.popupLayerView)
|
||||
val isLoadingPlaceholderKeyboard: Boolean
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
|
||||
@@ -75,6 +74,7 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
context.obtainStyledAttributes(attrs, R.styleable.KeyboardView).apply {
|
||||
isPreviewMode = getBoolean(R.styleable.KeyboardView_isPreviewKeyboard, false)
|
||||
isSmartbarKeyboardView = getBoolean(R.styleable.KeyboardView_isSmartbarKeyboard, false)
|
||||
isLoadingPlaceholderKeyboard = getBoolean(R.styleable.KeyboardView_isLoadingPlaceholderKeyboard, false)
|
||||
recycle()
|
||||
}
|
||||
orientation = VERTICAL
|
||||
@@ -82,8 +82,10 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
florisboard?.addEventListener(this)
|
||||
onWindowShown()
|
||||
if (isLoadingPlaceholderKeyboard) {
|
||||
computedLayout = ComputedLayoutData.PRE_GENERATED_LOADING_KEYBOARD
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,16 +95,16 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
destroyLayout()
|
||||
val computedLayout = computedLayout ?: return
|
||||
for (row in computedLayout.arrangement) {
|
||||
val rowView = KeyboardRowView(context)
|
||||
val rowView = KeyboardRowView(context, this)
|
||||
for (key in row) {
|
||||
val keyView = KeyView(this, key)
|
||||
keyView.florisboard = florisboard
|
||||
val keyView = KeyView(this, key, florisboard)
|
||||
rowView.addView(keyView)
|
||||
}
|
||||
addView(rowView)
|
||||
}
|
||||
if (!isPreviewMode) {
|
||||
themeManager.requestThemeUpdate(this)
|
||||
onWindowShown()
|
||||
} else {
|
||||
updateVisibility()
|
||||
}
|
||||
@@ -117,6 +119,7 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
florisboard?.addEventListener(this)
|
||||
if (!isPreviewMode) {
|
||||
themeManager.registerOnThemeUpdatedListener(this)
|
||||
}
|
||||
@@ -126,11 +129,11 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
* Dismisses all shown key popups when keyboard is detached from window.
|
||||
*/
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
popupManager.dismissAllPopups()
|
||||
if (!isPreviewMode) {
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
}
|
||||
florisboard?.removeEventListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
@@ -138,6 +141,18 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
distanceThreshold = prefs.gestures.swipeDistanceThreshold
|
||||
velocityThreshold = prefs.gestures.swipeVelocityThreshold
|
||||
}
|
||||
for (row in children) {
|
||||
if (row is ViewGroup) {
|
||||
for (keyView in row.children) {
|
||||
if (keyView is KeyView) {
|
||||
keyView.swipeGestureDetector.apply {
|
||||
distanceThreshold = prefs.gestures.swipeDistanceThreshold
|
||||
velocityThreshold = prefs.gestures.swipeVelocityThreshold
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,102 +167,103 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
event ?: return false
|
||||
if (isPreviewMode) {
|
||||
return false
|
||||
}
|
||||
val eventFloris = MotionEvent.obtainNoHistory(event)
|
||||
if (event == null || isPreviewMode || isLoadingPlaceholderKeyboard) return false
|
||||
|
||||
if (!isSmartbarKeyboardView && swipeGestureDetector.onTouchEvent(event)) {
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_CANCEL)
|
||||
activeKeyView = null
|
||||
activePointerId = null
|
||||
for (pointerIndex in 0 until event.pointerCount) {
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_CANCEL)
|
||||
activeKeyViews.remove(pointerId)
|
||||
}
|
||||
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(KeyData.INTERNAL_BATCH_EDIT))
|
||||
}
|
||||
return true
|
||||
}
|
||||
val pointerIndex = event.actionIndex
|
||||
var pointerId = event.getPointerId(pointerIndex)
|
||||
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN,
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.down(KeyData.INTERNAL_BATCH_EDIT))
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
searchForActiveKeyView(event, pointerIndex, pointerId)
|
||||
initialKeyCodes[pointerId] = activeKeyViews[pointerId]?.data?.code ?: 0
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
|
||||
}
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
if (activePointerId == null) {
|
||||
activePointerId = pointerId
|
||||
activeX = event.getX(pointerIndex)
|
||||
activeY = event.getY(pointerIndex)
|
||||
searchForActiveKeyView()
|
||||
initialKeyCode = activeKeyView?.data?.code ?: 0
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
|
||||
} else if (activePointerId != pointerId) {
|
||||
// New pointer arrived. Send ACTION_UP to current active view and move on
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_UP)
|
||||
activePointerId = pointerId
|
||||
activeX = event.getX(pointerIndex)
|
||||
activeY = event.getY(pointerIndex)
|
||||
searchForActiveKeyView()
|
||||
initialKeyCode = activeKeyView?.data?.code ?: 0
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
|
||||
}
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
searchForActiveKeyView(event, pointerIndex, pointerId)
|
||||
initialKeyCodes[pointerId] = activeKeyViews[pointerId]?.data?.code ?: 0
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
for (index in 0 until event.pointerCount) {
|
||||
pointerId = event.getPointerId(index)
|
||||
if (activePointerId == pointerId) {
|
||||
activeX = event.getX(index)
|
||||
activeY = event.getY(index)
|
||||
if (activeKeyView == null) {
|
||||
searchForActiveKeyView()
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
|
||||
} else {
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_MOVE)
|
||||
}
|
||||
for (pointerIndex in 0 until event.pointerCount) {
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
if (!activeKeyViews.containsKey(pointerId)) {
|
||||
searchForActiveKeyView(event, pointerIndex, pointerId)
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
|
||||
} else {
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_MOVE)
|
||||
}
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL,
|
||||
MotionEvent.ACTION_POINTER_UP -> {
|
||||
if (activePointerId == pointerId) {
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_UP)
|
||||
activeKeyView = null
|
||||
activePointerId = null
|
||||
}
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, event.actionMasked)
|
||||
activeKeyViews.remove(pointerId)
|
||||
}
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, event.actionMasked)
|
||||
activeKeyViews.remove(pointerId)
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(KeyData.INTERNAL_BATCH_EDIT))
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
eventFloris.recycle()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a touch [event] to [activeKeyView] with action set to [actionParam]. Normalizes passed
|
||||
* actions (ACTION_POINTER_* will be converted to ACTION_*). Translates the absolute coords of
|
||||
* a passed [event] to relative ones so the [activeKeyView] can work with it.
|
||||
* Sends a touch [event] to the active key view which is associated with given [pointerId]. The action of the
|
||||
* event is set to [actionParam]. Normalizes passed actions (ACTION_POINTER_* will be converted to ACTION_*).
|
||||
* Translates the absolute coords of a passed [event] to relative ones so the active key view can work with it.
|
||||
*
|
||||
* @param event The event to pass to [activeKeyView].
|
||||
* @param event The event to pass to the active key view.
|
||||
* @param pointerIndex The index of the pointer, used for getting coordinates.
|
||||
* @param pointerId The unique ID of the pointer, used to reference the active key view.
|
||||
* @param actionParam The action to set the [event] to.
|
||||
*/
|
||||
private fun sendFlorisTouchEvent(event: MotionEvent, actionParam: Int) {
|
||||
val keyView = activeKeyView ?: return
|
||||
val keyViewParent = keyView.parent as ViewGroup
|
||||
keyView.onFlorisTouchEvent(event.apply {
|
||||
action = when (actionParam) {
|
||||
private fun sendFlorisTouchEvent(event: MotionEvent, pointerIndex: Int, pointerId: Int, actionParam: Int) {
|
||||
val keyView = activeKeyViews[pointerId] ?: return
|
||||
val keyViewParent = keyView.parent as? ViewGroup ?: return
|
||||
val eventToSend = MotionEvent.obtain(
|
||||
event.downTime,
|
||||
event.eventTime,
|
||||
when (actionParam) {
|
||||
MotionEvent.ACTION_POINTER_DOWN -> MotionEvent.ACTION_DOWN
|
||||
MotionEvent.ACTION_POINTER_UP -> MotionEvent.ACTION_UP
|
||||
else -> actionParam
|
||||
}
|
||||
setLocation(
|
||||
activeX - keyViewParent.x - keyView.x,
|
||||
activeY - keyViewParent.y - keyView.y
|
||||
)
|
||||
})
|
||||
},
|
||||
event.getX(pointerIndex) - keyViewParent.x - keyView.x,
|
||||
event.getY(pointerIndex) - keyViewParent.y - keyView.y,
|
||||
0
|
||||
)
|
||||
keyView.onFlorisTouchEvent(eventToSend)
|
||||
eventToSend.recycle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Swipe event handler. Listens to touch_up swipes and executes the swipe action defined for it
|
||||
* in the prefs.
|
||||
*/
|
||||
override fun onSwipe(direction: SwipeGesture.Direction, type: SwipeGesture.Type): Boolean {
|
||||
override fun onSwipe(event: SwipeGesture.Event): Boolean {
|
||||
return when {
|
||||
initialKeyCode == KeyCode.DELETE -> {
|
||||
if (type == SwipeGesture.Type.TOUCH_UP && direction == SwipeGesture.Direction.LEFT &&
|
||||
initialKeyCodes[event.pointerId] == KeyCode.DELETE -> {
|
||||
if (event.type == SwipeGesture.Type.TOUCH_UP && event.direction == SwipeGesture.Direction.LEFT &&
|
||||
prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_WORD) {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.deleteKeySwipeLeft)
|
||||
true
|
||||
@@ -255,10 +271,19 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
false
|
||||
}
|
||||
}
|
||||
initialKeyCode > KeyCode.SPACE && !popupManager.isShowingExtendedPopup -> when {
|
||||
!prefs.glide.enabled -> when (type) {
|
||||
initialKeyCodes[event.pointerId] == KeyCode.SHIFT && activeKeyViews[event.pointerId]?.data?.code != KeyCode.SHIFT &&
|
||||
event.type == SwipeGesture.Type.TOUCH_UP -> {
|
||||
activeKeyViews[event.pointerId]?.let {
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(it.popupManager.getActiveKeyData(it) ?: it.data))
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.cancel(KeyData.SHIFT))
|
||||
}
|
||||
true
|
||||
}
|
||||
initialKeyCodes[event.pointerId] ?: 0 > KeyCode.SPACE &&
|
||||
activeKeyViews[event.pointerId]?.popupManager?.isShowingExtendedPopup == false -> when {
|
||||
!prefs.glide.enabled -> when (event.type) {
|
||||
SwipeGesture.Type.TOUCH_UP -> {
|
||||
val swipeAction = when (direction) {
|
||||
val swipeAction = when (event.direction) {
|
||||
SwipeGesture.Direction.UP -> prefs.gestures.swipeUp
|
||||
SwipeGesture.Direction.DOWN -> prefs.gestures.swipeDown
|
||||
SwipeGesture.Direction.LEFT -> prefs.gestures.swipeLeft
|
||||
@@ -281,15 +306,17 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for an active key view at [activeX]/[activeY].
|
||||
* Searches for an active key view at the passed pointer location.
|
||||
*/
|
||||
private fun searchForActiveKeyView() {
|
||||
private fun searchForActiveKeyView(event: MotionEvent, pointerIndex: Int, pointerId: Int) {
|
||||
val activeX = event.getX(pointerIndex)
|
||||
val activeY = event.getY(pointerIndex)
|
||||
loop@ for (row in children) {
|
||||
if (row is FlexboxLayout) {
|
||||
for (keyView in row.children) {
|
||||
if (keyView is KeyView) {
|
||||
if (keyView.touchHitBox.contains(activeX.toInt(), activeY.toInt())) {
|
||||
activeKeyView = keyView
|
||||
activeKeyViews[pointerId] = keyView
|
||||
break@loop
|
||||
}
|
||||
}
|
||||
@@ -299,22 +326,28 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the current [activeKeyView] and sends a [MotionEvent.ACTION_CANCEL] to indicate
|
||||
* the loss of focus.
|
||||
* Invalidates the current active key view and sends a [MotionEvent.ACTION_CANCEL] to indicate the loss of focus.
|
||||
*/
|
||||
fun dismissActiveKeyViewReference() {
|
||||
activeKeyView?.onFlorisTouchEvent(MotionEvent.obtain(
|
||||
fun dismissActiveKeyViewReference(pointerId: Int) {
|
||||
activeKeyViews.remove(pointerId)?.onFlorisTouchEvent(MotionEvent.obtain(
|
||||
0, 0, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0
|
||||
))
|
||||
activeKeyView = null
|
||||
}
|
||||
|
||||
/**
|
||||
* The desired key heights/widths are being calculated here.
|
||||
*/
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val keyMarginH = resources.getDimension((R.dimen.key_marginH)).toInt()
|
||||
val keyMarginV = resources.getDimension((R.dimen.key_marginV)).toInt()
|
||||
val keyMarginH: Int
|
||||
val keyMarginV: Int
|
||||
|
||||
if (isSmartbarKeyboardView){
|
||||
keyMarginH = resources.getDimension(R.dimen.key_marginH).toInt()
|
||||
keyMarginV = resources.getDimension(R.dimen.key_marginV).toInt()
|
||||
}else {
|
||||
keyMarginV = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingVertical, context).toInt()
|
||||
keyMarginH = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
|
||||
}
|
||||
|
||||
val desiredWidth = MeasureSpec.getSize(widthMeasureSpec).toFloat()
|
||||
desiredKeyWidth = if (isSmartbarKeyboardView) {
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package dev.patrickgold.florisboard.ime.text.layout
|
||||
|
||||
import dev.patrickgold.florisboard.ime.text.key.FlorisKeyData
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyType
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
|
||||
typealias LayoutDataArrangement = List<List<FlorisKeyData>>
|
||||
@@ -52,4 +54,56 @@ data class ComputedLayoutData(
|
||||
val name: String,
|
||||
val direction: String,
|
||||
val arrangement: ComputedLayoutDataArrangement = mutableListOf()
|
||||
)
|
||||
) {
|
||||
companion object {
|
||||
val PRE_GENERATED_LOADING_KEYBOARD = ComputedLayoutData(
|
||||
mode = KeyboardMode.CHARACTERS,
|
||||
name = "__loading_keyboard__",
|
||||
direction = "ltr",
|
||||
arrangement = mutableListOf(
|
||||
mutableListOf(
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0)
|
||||
),
|
||||
mutableListOf(
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0)
|
||||
),
|
||||
mutableListOf(
|
||||
FlorisKeyData(code = KeyCode.SHIFT, type = KeyType.MODIFIER, label = "shift"),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = KeyCode.DELETE, type = KeyType.ENTER_EDITING, label = "delete")
|
||||
),
|
||||
mutableListOf(
|
||||
FlorisKeyData(code = KeyCode.VIEW_SYMBOLS, type = KeyType.SYSTEM_GUI, label = "view_symbols"),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = KeyCode.SPACE, label = "space"),
|
||||
FlorisKeyData(code = 0),
|
||||
FlorisKeyData(code = KeyCode.ENTER, type = KeyType.ENTER_EDITING, label = "enter")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,15 @@
|
||||
package dev.patrickgold.florisboard.ime.text.layout
|
||||
|
||||
import android.content.Context
|
||||
import com.github.michaelbull.result.getOr
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetManager
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetSource
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupExtension
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupManager
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupSet
|
||||
import dev.patrickgold.florisboard.ime.text.key.*
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
@@ -36,6 +39,7 @@ private typealias KMS = Pair<KeyboardMode, Subtype>
|
||||
* Class which manages layout loading and caching.
|
||||
*/
|
||||
class LayoutManager(private val context: Context) : CoroutineScope by MainScope() {
|
||||
private val assetManager: AssetManager = AssetManager.default()
|
||||
private val computedLayoutCache: HashMap<KMS, Deferred<ComputedLayoutData>> = hashMapOf()
|
||||
|
||||
/**
|
||||
@@ -65,9 +69,21 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
}
|
||||
|
||||
private fun loadExtendedPopups(subtype: Subtype? = null): PopupExtension {
|
||||
val lang = subtype?.locale?.language ?: "\$default"
|
||||
val map = PopupExtension.fromFile(context, "ime/text/characters/extended_popups/$lang.json")
|
||||
return map.getOr(PopupExtension.empty())
|
||||
val langTagRef = AssetRef(
|
||||
source = AssetSource.Assets,
|
||||
path = PopupManager.POPUP_EXTENSION_PATH_REL + "/" + (subtype?.locale?.toLanguageTag() ?: "\$default") + ".json"
|
||||
)
|
||||
val langRef = AssetRef(
|
||||
source = AssetSource.Assets,
|
||||
path = PopupManager.POPUP_EXTENSION_PATH_REL + "/" + (subtype?.locale?.language ?: "\$default") + ".json"
|
||||
)
|
||||
assetManager.loadAsset(langTagRef, PopupExtension::class).onSuccess {
|
||||
return it
|
||||
}
|
||||
assetManager.loadAsset(langRef, PopupExtension::class).onSuccess {
|
||||
return it
|
||||
}
|
||||
return PopupExtension.empty()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,7 +162,8 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
}
|
||||
|
||||
// Add popup to keys
|
||||
if (keyboardMode == KeyboardMode.CHARACTERS) {
|
||||
if (keyboardMode == KeyboardMode.CHARACTERS || keyboardMode == KeyboardMode.NUMERIC_ADVANCED ||
|
||||
keyboardMode == KeyboardMode.SYMBOLS || keyboardMode == KeyboardMode.SYMBOLS2) {
|
||||
val extendedPopupsDefault = loadExtendedPopups()
|
||||
val extendedPopups = loadExtendedPopups(subtype)
|
||||
for (row in computedArrangement) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user