Compare commits
97 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 |
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.
|
||||
|
||||
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,74 +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 25
|
||||
versionName "0.3.6"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">FlorisBoard Debug</string>
|
||||
</resources>
|
||||
@@ -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"/>
|
||||
|
||||
|
||||
@@ -215,6 +215,11 @@
|
||||
"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.
@@ -31,6 +31,8 @@
|
||||
{ "code": 231, "label": "ç" }
|
||||
],
|
||||
[
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 121, "label": "y" },
|
||||
{ "code": 120, "label": "x" },
|
||||
{ "code": 107, "label": "k" },
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
"~enter": {
|
||||
"main": { "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
|
||||
"relevant": [
|
||||
{ "code": -215, "label": "toggle_one_handed_mode", "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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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": "μ" }
|
||||
]
|
||||
|
||||
@@ -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,26 +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.ViewGroup
|
||||
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
|
||||
@@ -48,7 +53,7 @@ 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
|
||||
@@ -64,14 +69,17 @@ 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 var extractEditLayout: WeakReference<View?> = WeakReference(null)
|
||||
private val serviceLifecycleDispatcher: ServiceLifecycleDispatcher = ServiceLifecycleDispatcher(this)
|
||||
|
||||
private var extractEditLayout: WeakReference<ViewGroup?> = WeakReference(null)
|
||||
var inputView: InputView? = null
|
||||
private set
|
||||
private var inputWindowView: InputWindowView? = null
|
||||
@@ -80,12 +88,23 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
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
|
||||
@@ -93,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
|
||||
@@ -106,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)
|
||||
}
|
||||
@@ -128,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
|
||||
}
|
||||
@@ -158,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()
|
||||
@@ -176,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()
|
||||
@@ -208,7 +241,9 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
|
||||
baseContext.setTheme(currentThemeResId)
|
||||
|
||||
inputWindowView = layoutInflater.inflate(R.layout.florisboard, null) as InputWindowView
|
||||
inputWindowView = layoutInflater.inflate(R.layout.florisboard, null) as? InputWindowView
|
||||
inputWindowView?.isHapticFeedbackEnabled = true
|
||||
|
||||
eventListeners.toList().forEach { it?.onCreateInputView() }
|
||||
|
||||
return inputWindowView
|
||||
@@ -224,10 +259,67 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
@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() {
|
||||
Timber.i("onDestroy()")
|
||||
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
clipboardManager?.removePrimaryClipChangedListener(this)
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
florisboardInstance = null
|
||||
|
||||
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)")
|
||||
|
||||
@@ -238,7 +330,6 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
}
|
||||
this.inputView = inputView
|
||||
initializeOneHandedEnvironment()
|
||||
updateSoftInputWindowLayoutParameters()
|
||||
updateOneHandedPanelVisibility()
|
||||
themeManager.notifyCallbackReceivers()
|
||||
@@ -247,24 +338,11 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
eventListeners.toList().forEach { it?.onRegisterInputView(inputView) }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Timber.i("onDestroy()")
|
||||
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
clipboardManager?.removePrimaryClipChangedListener(this)
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
florisboardInstance = null
|
||||
|
||||
eventListeners.toList().forEach { it?.onDestroy() }
|
||||
eventListeners.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -298,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
|
||||
@@ -317,7 +401,13 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
|
||||
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?.onWindowHidden() }
|
||||
@@ -332,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?.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) {
|
||||
@@ -390,31 +531,33 @@ 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
|
||||
// Update ExtractTextView theme and attributes
|
||||
extractEditLayout.get()?.let { eel ->
|
||||
if (eel is ViewGroup) {
|
||||
eel.setBackgroundColor(theme.getAttr(Theme.Attr.EXTRACT_EDIT_LAYOUT_BACKGROUND).toSolidColor().color)
|
||||
eel.findViewById<ExtractEditText>(android.R.id.inputExtractEditText)?.let { textView ->
|
||||
textView.background?.setTint(theme.getAttr(Theme.Attr.WINDOW_COLOR_PRIMARY).toSolidColor().color)
|
||||
textView.setTextColor(theme.getAttr(Theme.Attr.EXTRACT_EDIT_LAYOUT_FOREGROUND).toSolidColor().color)
|
||||
textView.setHintTextColor(theme.getAttr(Theme.Attr.EXTRACT_EDIT_LAYOUT_FOREGROUND_ALT).toSolidColor().color)
|
||||
textView.highlightColor = theme.getAttr(Theme.Attr.WINDOW_COLOR_PRIMARY).toSolidColor().color
|
||||
}
|
||||
eel.findViewWithType(Button::class)?.let { actionButton ->
|
||||
actionButton.setBackgroundColor(theme.getAttr(Theme.Attr.EXTRACT_ACTION_BUTTON_BACKGROUND).toSolidColor().color)
|
||||
actionButton.setTextColor(theme.getAttr(Theme.Attr.EXTRACT_ACTION_BUTTON_FOREGROUND).toSolidColor().color)
|
||||
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?.onApplyThemeAttributes() }
|
||||
@@ -435,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.
|
||||
*/
|
||||
@@ -467,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) {
|
||||
@@ -519,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)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -595,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
|
||||
}
|
||||
@@ -727,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,10 +20,12 @@ import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
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
|
||||
@@ -48,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)
|
||||
@@ -77,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)
|
||||
}
|
||||
@@ -88,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
|
||||
}
|
||||
@@ -171,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,6 +21,8 @@ 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
|
||||
@@ -29,7 +31,6 @@ 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.
|
||||
@@ -316,9 +317,13 @@ class PrefHelper(
|
||||
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"
|
||||
@@ -352,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
|
||||
@@ -359,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
|
||||
@@ -421,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: "
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -111,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
|
||||
|
||||
@@ -18,16 +18,19 @@ package dev.patrickgold.florisboard.ime.text
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
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.*
|
||||
@@ -51,7 +54,7 @@ import kotlin.math.roundToLong
|
||||
* 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()
|
||||
@@ -63,26 +66,37 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
private val keyboardViews = EnumMap<KeyboardMode, KeyboardView>(KeyboardMode::class.java)
|
||||
private var editingKeyboardView: EditingKeyboardView? = null
|
||||
private var loadingPlaceholderKeyboard: KeyboardView? = null
|
||||
private val osHandler = Handler()
|
||||
private var textViewFlipper: ViewFlipper? = 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
|
||||
@@ -107,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)
|
||||
@@ -124,7 +139,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
|
||||
private suspend fun addKeyboardView(mode: KeyboardMode) {
|
||||
val keyboardView = KeyboardView(florisboard.context)
|
||||
keyboardView.id = View.generateViewId()
|
||||
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(mode, florisboard.activeSubtype, florisboard.prefs).await()
|
||||
keyboardViews[mode] = keyboardView
|
||||
textViewFlipper?.addView(keyboardView)
|
||||
@@ -193,8 +207,9 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
override fun onDestroy() {
|
||||
Timber.i("onDestroy()")
|
||||
|
||||
inputEventDispatcher.keyEventReceiver = null
|
||||
inputEventDispatcher.close()
|
||||
cancel()
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
layoutManager.onDestroy()
|
||||
instance = null
|
||||
}
|
||||
@@ -294,14 +309,21 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
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()
|
||||
@@ -313,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() {
|
||||
@@ -334,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,20 +391,23 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* class.
|
||||
*/
|
||||
fun executeSwipeAction(swipeAction: SwipeAction) {
|
||||
when (swipeAction) {
|
||||
SwipeAction.DELETE_WORD -> handleDeleteWord()
|
||||
SwipeAction.INSERT_SPACE -> handleSpace()
|
||||
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.MOVE_CURSOR_START_OF_LINE -> handleArrow(KeyCode.MOVE_HOME)
|
||||
SwipeAction.MOVE_CURSOR_END_OF_LINE -> handleArrow(KeyCode.MOVE_END)
|
||||
SwipeAction.SHIFT -> handleShift()
|
||||
SwipeAction.SHOW_INPUT_METHOD_PICKER -> sendKeyPress(
|
||||
KeyData(type = KeyType.FUNCTION, code = KeyCode.SHOW_INPUT_METHOD_PICKER)
|
||||
)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,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()
|
||||
}
|
||||
@@ -381,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
|
||||
}
|
||||
}
|
||||
@@ -395,23 +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() {
|
||||
hasCapsRecentlyChanged = false
|
||||
hasSpaceRecentlyPressed = false
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = false
|
||||
activeEditorInstance.deleteBackwards()
|
||||
}
|
||||
|
||||
@@ -419,11 +462,9 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Handles a [KeyCode.DELETE_WORD] event.
|
||||
*/
|
||||
private fun handleDeleteWord() {
|
||||
hasCapsRecentlyChanged = false
|
||||
hasSpaceRecentlyPressed = false
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = false
|
||||
activeEditorInstance.deleteWordsBeforeCursor(1)
|
||||
}
|
||||
|
||||
@@ -431,8 +472,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Handles a [KeyCode.ENTER] event.
|
||||
*/
|
||||
private fun handleEnter() {
|
||||
hasCapsRecentlyChanged = false
|
||||
hasSpaceRecentlyPressed = false
|
||||
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
|
||||
activeEditorInstance.performEnter()
|
||||
} else {
|
||||
@@ -463,44 +502,67 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] event.
|
||||
* Handles a [KeyCode.SHIFT] down event.
|
||||
*/
|
||||
private fun handleShift() {
|
||||
if (hasCapsRecentlyChanged) {
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
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())
|
||||
@@ -509,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -626,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 {
|
||||
@@ -640,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 -> {
|
||||
@@ -668,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.INTERNAL_BATCH_EDIT -> {
|
||||
florisboard.endInternalBatchEdit()
|
||||
return
|
||||
}
|
||||
KeyCode.LANGUAGE_SWITCH -> handleLanguageSwitch()
|
||||
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.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)
|
||||
@@ -700,49 +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 = keyData.label.toLowerCase(Locale.ENGLISH)
|
||||
val tld = data.label.toLowerCase(Locale.ENGLISH)
|
||||
activeEditorInstance.commitText(tld)
|
||||
}
|
||||
else -> {
|
||||
hasCapsRecentlyChanged = false
|
||||
hasSpaceRecentlyPressed = false
|
||||
var text = keyData.code.toChar().toString()
|
||||
text = when (caps) {
|
||||
true -> text.toUpperCase(florisboard.activeSubtype.locale)
|
||||
false -> text.toLowerCase(florisboard.activeSubtype.locale)
|
||||
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
|
||||
@@ -52,8 +50,6 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
|
||||
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)
|
||||
@@ -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
|
||||
|
||||
@@ -34,6 +34,8 @@ enum class SwipeAction {
|
||||
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,
|
||||
|
||||
@@ -19,8 +19,9 @@ 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.
|
||||
@@ -63,10 +64,7 @@ abstract class SwipeGesture {
|
||||
* @property listener The listener to report detected swipes to.
|
||||
*/
|
||||
class Detector(private val context: Context, private val listener: Listener) {
|
||||
private var firstMotionEvent: MotionEvent? = null
|
||||
private var lastMotionEvent: MotionEvent? = null
|
||||
private var absUnitCountX: Int = 0
|
||||
private var absUnitCountY: Int = 0
|
||||
private var pointerDataMap: MutableMap<Int, PointerData> = mutableMapOf()
|
||||
private var thresholdWidth: Double = numericValue(context, DistanceThreshold.NORMAL)
|
||||
private var unitWidth: Double = thresholdWidth / 4.0
|
||||
|
||||
@@ -91,67 +89,83 @@ abstract class SwipeGesture {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN,
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
resetState()
|
||||
firstMotionEvent = MotionEvent.obtainNoHistory(event)
|
||||
lastMotionEvent = firstMotionEvent
|
||||
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 -> {
|
||||
val firstEvent = firstMotionEvent ?: return false
|
||||
val absDiffX = event.x - firstEvent.x
|
||||
val absDiffY = event.y - firstEvent.y
|
||||
val lastEvent = lastMotionEvent ?: return false
|
||||
val relDiffX = event.x - lastEvent.x
|
||||
val relDiffY = event.y - lastEvent.y
|
||||
return if (alwaysTriggerOnMove || abs(relDiffX) > (thresholdWidth / 2.0) || abs(relDiffY) > (thresholdWidth / 2.0)) {
|
||||
lastMotionEvent = MotionEvent.obtainNoHistory(event)
|
||||
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,
|
||||
absUnitCountX,
|
||||
absUnitCountY,
|
||||
relUnitCountX,
|
||||
relUnitCountY
|
||||
))
|
||||
} 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 = firstMotionEvent ?: return false
|
||||
val absDiffX = event.x - firstEvent.x
|
||||
val absDiffY = event.y - firstEvent.y
|
||||
/*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) {
|
||||
val ret = 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,
|
||||
absUnitCountX,
|
||||
absUnitCountY,
|
||||
absUnitCountX,
|
||||
absUnitCountY
|
||||
))
|
||||
} 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
|
||||
}
|
||||
}
|
||||
resetState()
|
||||
return ret
|
||||
return false
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
resetState()
|
||||
@@ -159,7 +173,7 @@ abstract class SwipeGesture {
|
||||
else -> return false
|
||||
}
|
||||
return false
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -207,10 +221,16 @@ abstract class SwipeGesture {
|
||||
* Resets the state.
|
||||
*/
|
||||
private fun resetState() {
|
||||
firstMotionEvent = null
|
||||
lastMotionEvent = null
|
||||
absUnitCountX = 0
|
||||
absUnitCountY = 0
|
||||
pointerDataMap.clear()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +238,7 @@ abstract class SwipeGesture {
|
||||
* An interface which provides an abstract callback function, which will be called for any
|
||||
* detected swipe event.
|
||||
*/
|
||||
fun interface Listener {
|
||||
interface Listener {
|
||||
fun onSwipe(event: Event): Boolean
|
||||
}
|
||||
|
||||
@@ -230,6 +250,8 @@ abstract class SwipeGesture {
|
||||
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). */
|
||||
|
||||
@@ -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.*
|
||||
|
||||
|
||||
@@ -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,8 +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
|
||||
@@ -41,7 +47,9 @@ 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
|
||||
|
||||
@@ -69,7 +77,7 @@ class KeyView(
|
||||
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
|
||||
|
||||
@@ -112,11 +120,22 @@ class KeyView(
|
||||
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,
|
||||
@@ -150,7 +169,7 @@ 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
|
||||
@@ -178,14 +197,19 @@ class KeyView(
|
||||
caps: Boolean = florisboard?.textInputManager?.caps ?: false,
|
||||
subtype: Subtype = florisboard?.activeSubtype ?: Subtype.DEFAULT
|
||||
): String {
|
||||
return when (data.code) {
|
||||
KeyCode.URI_COMPONENT_TLD -> keyData.label.toLowerCase(Locale.ENGLISH)
|
||||
else -> {
|
||||
val labelText = (keyData.code.toChar()).toString()
|
||||
if (caps) {
|
||||
labelText.toUpperCase(subtype.locale)
|
||||
} else {
|
||||
labelText.toLowerCase(subtype.locale)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,79 +241,80 @@ class KeyView(
|
||||
*/
|
||||
fun onFlorisTouchEvent(event: MotionEvent?): Boolean {
|
||||
if (event == null || !isEnabled) return false
|
||||
val florisboard = florisboard ?: return false
|
||||
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
|
||||
val alwaysTriggerOnMove = (hasTriggeredGestureMove
|
||||
&& florisboard?.activeEditorInstance?.isRawInputEditor == false
|
||||
&& (data.code == KeyCode.DELETE && prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_CHARACTERS_PRECISELY
|
||||
|| data.code == KeyCode.SPACE))
|
||||
&& (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 * 2.0f).toLong(), 25) {
|
||||
if (isKeyPressed) {
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
} else {
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.code == KeyCode.SPACE) {
|
||||
initSelectionStart = florisboard?.activeEditorInstance?.selection?.start ?: 0
|
||||
initSelectionEnd = florisboard?.activeEditorInstance?.selection?.end ?: 0
|
||||
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 -> {}
|
||||
SwipeAction.INSERT_SPACE -> {
|
||||
}
|
||||
else -> {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.spaceBarLongPress)
|
||||
this.florisboard.executeSwipeAction(prefs.gestures.spaceBarLongPress)
|
||||
shouldBlockNextKeyCode = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
KeyCode.SHIFT -> {
|
||||
longKeyPressHandler.postDelayed((delayMillis * 2.5).toLong()) {
|
||||
this.florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.SHIFT_LOCK))
|
||||
}
|
||||
}
|
||||
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()) {
|
||||
keyboardView.popupManager.extend(this, keyHintMode)
|
||||
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
|
||||
@@ -299,31 +324,49 @@ 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) {
|
||||
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
|
||||
}
|
||||
@@ -343,8 +386,8 @@ class KeyView(
|
||||
SwipeGesture.Type.TOUCH_MOVE -> when (prefs.gestures.deleteKeySwipeLeft) {
|
||||
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
|
||||
florisboard.activeEditorInstance.apply {
|
||||
setSelection(
|
||||
(selection.end + event.absUnitCountX).coerceIn(0, selection.end),
|
||||
selection.updateAndNotify(
|
||||
(selection.end + event.absUnitCountX + 1).coerceIn(0, selection.end),
|
||||
selection.end
|
||||
)
|
||||
}
|
||||
@@ -388,12 +431,10 @@ class KeyView(
|
||||
}
|
||||
SwipeGesture.Direction.LEFT -> {
|
||||
if (prefs.gestures.spaceBarSwipeLeft == SwipeAction.MOVE_CURSOR_LEFT) {
|
||||
if (!florisboard.activeEditorInstance.isRawInputEditor) {
|
||||
val s = (initSelectionEnd + event.absUnitCountX).coerceIn(0, florisboard.activeEditorInstance.cachedText.length)
|
||||
florisboard.activeEditorInstance.setSelection(s, s)
|
||||
} else {
|
||||
for (n in 0 until abs(event.relUnitCountX)) {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
|
||||
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 {
|
||||
@@ -405,12 +446,10 @@ class KeyView(
|
||||
}
|
||||
SwipeGesture.Direction.RIGHT -> {
|
||||
if (prefs.gestures.spaceBarSwipeRight == SwipeAction.MOVE_CURSOR_RIGHT) {
|
||||
if (!florisboard.activeEditorInstance.isRawInputEditor) {
|
||||
val s = (initSelectionEnd + event.absUnitCountX).coerceIn(0, florisboard.activeEditorInstance.cachedText.length)
|
||||
florisboard.activeEditorInstance.setSelection(s, s)
|
||||
} else {
|
||||
for (n in 0 until abs(event.relUnitCountX)) {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
|
||||
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 {
|
||||
@@ -435,6 +474,25 @@ class KeyView(
|
||||
* by Devunwired
|
||||
*/
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
|
||||
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,
|
||||
@@ -447,11 +505,11 @@ class KeyView(
|
||||
else -> when (data.code) {
|
||||
KeyCode.SHIFT,
|
||||
KeyCode.DELETE ->
|
||||
if ((keyboardView.computedLayout?.arrangement?.get(2)?.size ?: 0) > 10) {
|
||||
1.12f
|
||||
} else {
|
||||
1.56f
|
||||
}
|
||||
if ((keyboardView.computedLayout?.arrangement?.get(2)?.size ?: 0) > 10) {
|
||||
1.12f
|
||||
} else {
|
||||
1.56f
|
||||
}
|
||||
KeyCode.VIEW_CHARACTERS,
|
||||
KeyCode.VIEW_SYMBOLS,
|
||||
KeyCode.VIEW_SYMBOLS2,
|
||||
@@ -522,8 +580,8 @@ class KeyView(
|
||||
isEnabled = when (data.code) {
|
||||
KeyCode.CLIPBOARD_COPY,
|
||||
KeyCode.CLIPBOARD_CUT -> (florisboard != null
|
||||
&& florisboard.activeEditorInstance.selection.isSelectionMode
|
||||
&& florisboard.activeEditorInstance.isRawInputEditor)
|
||||
&& florisboard.activeEditorInstance.selection.isSelectionMode
|
||||
&& !florisboard.activeEditorInstance.isRawInputEditor)
|
||||
KeyCode.CLIPBOARD_PASTE -> florisboard?.clipboardManager?.hasPrimaryClip() == true
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> {
|
||||
florisboard?.activeEditorInstance?.isRawInputEditor == false
|
||||
@@ -609,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) {
|
||||
@@ -673,7 +740,11 @@ class KeyView(
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -789,7 +860,8 @@ class KeyView(
|
||||
KeyboardMode.CHARACTERS -> {
|
||||
label = florisboard?.activeSubtype?.locale?.displayName
|
||||
}
|
||||
else -> {}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
|
||||
@@ -869,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...
|
||||
@@ -888,7 +960,7 @@ class KeyView(
|
||||
else -> 1.0f
|
||||
}
|
||||
)
|
||||
keyboardView.popupManager.keyPopupTextSize = cachedTextSize
|
||||
popupManager.keyPopupTextSize = cachedTextSize
|
||||
}
|
||||
else -> {
|
||||
setTextSizeFor(
|
||||
@@ -911,7 +983,11 @@ class KeyView(
|
||||
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")) {
|
||||
|
||||
@@ -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,11 +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
|
||||
val isLoadingPlaceholderKeyboard: Boolean
|
||||
var popupManager = PopupManager<KeyboardView, KeyView>(this, florisboard?.popupLayerView)
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
|
||||
@@ -97,7 +95,7 @@ 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, florisboard)
|
||||
rowView.addView(keyView)
|
||||
@@ -131,7 +129,6 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
* Dismisses all shown key popups when keyboard is detached from window.
|
||||
*/
|
||||
override fun onDetachedFromWindow() {
|
||||
popupManager.dismissAllPopups()
|
||||
if (!isPreviewMode) {
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
}
|
||||
@@ -170,92 +167,93 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
event ?: return false
|
||||
if (isPreviewMode || isLoadingPlaceholderKeyboard) {
|
||||
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()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,7 +262,7 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
*/
|
||||
override fun onSwipe(event: SwipeGesture.Event): Boolean {
|
||||
return when {
|
||||
initialKeyCode == KeyCode.DELETE -> {
|
||||
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)
|
||||
@@ -273,7 +271,16 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
false
|
||||
}
|
||||
}
|
||||
initialKeyCode > KeyCode.SPACE && !popupManager.isShowingExtendedPopup -> when {
|
||||
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 (event.direction) {
|
||||
@@ -299,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
|
||||
}
|
||||
}
|
||||
@@ -317,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) {
|
||||
|
||||
@@ -18,7 +18,6 @@ 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.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyType
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
package dev.patrickgold.florisboard.ime.text.layout
|
||||
|
||||
import android.content.Context
|
||||
import com.github.michaelbull.result.getOr
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
@@ -79,10 +77,10 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
source = AssetSource.Assets,
|
||||
path = PopupManager.POPUP_EXTENSION_PATH_REL + "/" + (subtype?.locale?.language ?: "\$default") + ".json"
|
||||
)
|
||||
assetManager.loadAsset(langTagRef, PopupExtension::class.java).onSuccess {
|
||||
assetManager.loadAsset(langTagRef, PopupExtension::class).onSuccess {
|
||||
return it
|
||||
}
|
||||
assetManager.loadAsset(langRef, PopupExtension::class.java).onSuccess {
|
||||
assetManager.loadAsset(langRef, PopupExtension::class).onSuccess {
|
||||
return it
|
||||
}
|
||||
return PopupExtension.empty()
|
||||
@@ -164,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) {
|
||||
|
||||
@@ -53,6 +53,7 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
private val themeManager = ThemeManager.default()
|
||||
private var eventListener: WeakReference<EventListener?>? = null
|
||||
private val mainScope = MainScope()
|
||||
private var lastSuggestionInitDate: Long = 0
|
||||
|
||||
private var cachedActionStartAreaVisible: Boolean = false
|
||||
@IdRes private var cachedActionStartAreaId: Int? = null
|
||||
@@ -121,6 +122,22 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
}
|
||||
}
|
||||
|
||||
binding.candidate0.setOnClickListener {
|
||||
if (it is Button) {
|
||||
eventListener?.get()?.onSmartbarCandidatePressed(it.text.toString())
|
||||
}
|
||||
}
|
||||
binding.candidate1.setOnClickListener {
|
||||
if (it is Button) {
|
||||
eventListener?.get()?.onSmartbarCandidatePressed(it.text.toString())
|
||||
}
|
||||
}
|
||||
binding.candidate2.setOnClickListener {
|
||||
if (it is Button) {
|
||||
eventListener?.get()?.onSmartbarCandidatePressed(it.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
binding.clipboardSuggestion.setOnClickListener {
|
||||
florisboard?.activeEditorInstance?.performClipboardPaste()
|
||||
shouldSuggestClipboardContents = false
|
||||
@@ -259,7 +276,8 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
KeyboardMode.PHONE2 -> null
|
||||
else -> when {
|
||||
florisboard.activeEditorInstance.isComposingEnabled &&
|
||||
shouldSuggestClipboardContents
|
||||
shouldSuggestClipboardContents &&
|
||||
florisboard.activeEditorInstance.selection.isCursorMode
|
||||
-> R.id.clipboard_suggestion_row
|
||||
florisboard.activeEditorInstance.isComposingEnabled &&
|
||||
florisboard.activeEditorInstance.selection.isCursorMode
|
||||
@@ -306,6 +324,28 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
updateSmartbarState()
|
||||
}
|
||||
|
||||
fun setCandidateSuggestionWords(suggestionInitDate: Long, suggestions: List<String>) {
|
||||
if (suggestionInitDate > lastSuggestionInitDate) {
|
||||
lastSuggestionInitDate = suggestionInitDate
|
||||
binding.candidate1.text = suggestions.getOrNull(0) ?: ""
|
||||
binding.candidate0.text = suggestions.getOrNull(1) ?: ""
|
||||
binding.candidate2.text = suggestions.getOrNull(2) ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCandidateSuggestionCapsState() {
|
||||
val tim = florisboard?.textInputManager ?: return
|
||||
if (tim.capsLock) {
|
||||
binding.candidate0.text = binding.candidate0.text.toString().toUpperCase(florisboard.activeSubtype.locale)
|
||||
binding.candidate1.text = binding.candidate1.text.toString().toUpperCase(florisboard.activeSubtype.locale)
|
||||
binding.candidate2.text = binding.candidate2.text.toString().toUpperCase(florisboard.activeSubtype.locale)
|
||||
} else {
|
||||
binding.candidate0.text = binding.candidate0.text.toString().toLowerCase(florisboard.activeSubtype.locale)
|
||||
binding.candidate1.text = binding.candidate1.text.toString().toLowerCase(florisboard.activeSubtype.locale)
|
||||
binding.candidate2.text = binding.candidate2.text.toString().toLowerCase(florisboard.activeSubtype.locale)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
|
||||
val heightSize = MeasureSpec.getSize(heightMeasureSpec).toFloat()
|
||||
@@ -348,7 +388,7 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
*/
|
||||
interface EventListener {
|
||||
fun onSmartbarBackButtonPressed() {}
|
||||
//fun onSmartbarCandidatePressed() {}
|
||||
fun onSmartbarCandidatePressed(word: String) {}
|
||||
//fun onSmartbarCandidateLongPressed() {}
|
||||
fun onSmartbarPrivateModeButtonClicked() {}
|
||||
fun onSmartbarQuickActionPressed(@IdRes quickActionId: Int) {}
|
||||
|
||||
@@ -22,7 +22,7 @@ import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import com.github.michaelbull.result.*
|
||||
import android.net.Uri
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetManager
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
@@ -85,6 +85,8 @@ class ThemeManager private constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultOrNull(): ThemeManager? = defaultInstance
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -102,7 +104,7 @@ class ThemeManager private constructor(
|
||||
activeTheme = AdaptiveThemeOverlay(this, if (ref == null) {
|
||||
Theme.BASE_THEME
|
||||
} else {
|
||||
loadTheme(ref).getOr(Theme.BASE_THEME)
|
||||
loadTheme(ref).getOrDefault(Theme.BASE_THEME)
|
||||
})
|
||||
Timber.i(activeTheme.label)
|
||||
notifyCallbackReceivers()
|
||||
@@ -170,7 +172,7 @@ class ThemeManager private constructor(
|
||||
ThemeValue.SolidColor(a.getColor(1, defColor))
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
remoteColorPrimary
|
||||
}
|
||||
}
|
||||
remoteColorPrimaryVariant = when {
|
||||
@@ -184,7 +186,7 @@ class ThemeManager private constructor(
|
||||
ThemeValue.SolidColor(a.getColor(4, defColor))
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
remoteColorPrimaryVariant
|
||||
}
|
||||
}
|
||||
remoteColorSecondary = when {
|
||||
@@ -198,7 +200,7 @@ class ThemeManager private constructor(
|
||||
ThemeValue.SolidColor(a.getColor(7, defColor))
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
remoteColorSecondary
|
||||
}
|
||||
}
|
||||
a.recycle()
|
||||
@@ -216,6 +218,7 @@ class ThemeManager private constructor(
|
||||
remoteColorSecondary?.let {
|
||||
remoteColorSecondary = ThemeValue.SolidColor(it.color or Color.BLACK)
|
||||
}
|
||||
notifyCallbackReceivers()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,23 +249,34 @@ class ThemeManager private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteTheme(ref: AssetRef): Result<Nothing?, Throwable> {
|
||||
fun deleteTheme(ref: AssetRef): Result<Unit> {
|
||||
return assetManager.deleteAsset(ref)
|
||||
}
|
||||
|
||||
fun loadTheme(ref: AssetRef): Result<Theme, Throwable> {
|
||||
assetManager.loadAsset(ref, ThemeJson::class.java).onSuccess { themeJson ->
|
||||
fun loadTheme(ref: AssetRef): Result<Theme> {
|
||||
assetManager.loadAsset(ref, ThemeJson::class).onSuccess { themeJson ->
|
||||
val theme = themeJson.toTheme()
|
||||
return Ok(theme)
|
||||
return Result.success(theme)
|
||||
}.onFailure {
|
||||
Timber.e(it.toString())
|
||||
return Err(it)
|
||||
return Result.failure(it)
|
||||
}
|
||||
return Err(Exception("Unreachable code"))
|
||||
return Result.failure(Exception("Unreachable code"))
|
||||
}
|
||||
|
||||
fun writeTheme(ref: AssetRef, theme: Theme): Result<Boolean, Throwable> {
|
||||
return assetManager.writeAsset(ref, ThemeJson::class.java, ThemeJson.fromTheme(theme))
|
||||
fun loadTheme(uri: Uri): Result<Theme> {
|
||||
assetManager.loadAsset(uri, ThemeJson::class).onSuccess { themeJson ->
|
||||
val theme = themeJson.toTheme()
|
||||
return Result.success(theme)
|
||||
}.onFailure {
|
||||
Timber.e(it.toString())
|
||||
return Result.failure(it)
|
||||
}
|
||||
return Result.failure(Exception("Unreachable code"))
|
||||
}
|
||||
|
||||
fun writeTheme(ref: AssetRef, theme: Theme): Result<Unit> {
|
||||
return assetManager.writeAsset(ref, ThemeJson::class, ThemeJson.fromTheme(theme))
|
||||
}
|
||||
|
||||
private fun evaluateActiveThemeRef(): AssetRef? {
|
||||
@@ -299,7 +313,7 @@ class ThemeManager private constructor(
|
||||
prefs.theme.dayThemeRef
|
||||
}
|
||||
}
|
||||
}).onFailure { Timber.e(it) }.getOr(null)
|
||||
}).onFailure { Timber.e(it) }.getOrDefault(null)
|
||||
}
|
||||
|
||||
private fun indexThemeRefs() {
|
||||
@@ -307,7 +321,7 @@ class ThemeManager private constructor(
|
||||
indexedNightThemeRefs.clear()
|
||||
assetManager.listAssets(
|
||||
AssetRef(AssetSource.Assets, THEME_PATH_REL),
|
||||
ThemeMetaOnly::class.java
|
||||
ThemeMetaOnly::class
|
||||
).onSuccess {
|
||||
for ((ref, themeMetaOnly) in it) {
|
||||
if (themeMetaOnly.isNightTheme) {
|
||||
@@ -321,7 +335,7 @@ class ThemeManager private constructor(
|
||||
}
|
||||
assetManager.listAssets(
|
||||
AssetRef(AssetSource.Internal, THEME_PATH_REL),
|
||||
ThemeMetaOnly::class.java
|
||||
ThemeMetaOnly::class
|
||||
).onSuccess {
|
||||
for ((ref, themeMetaOnly) in it) {
|
||||
if (themeMetaOnly.isNightTheme) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package dev.patrickgold.florisboard.settings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
@@ -32,6 +33,7 @@ class AboutActivity : AppCompatActivity() {
|
||||
private lateinit var binding: AboutActivityBinding
|
||||
private var licensesAlertDialog: AlertDialog? = null
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = AboutActivityBinding.inflate(layoutInflater)
|
||||
@@ -41,7 +43,7 @@ class AboutActivity : AppCompatActivity() {
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
// Set app version string
|
||||
binding.appVersion.text = AppVersionUtils.getRawVersionName(this)
|
||||
binding.appVersion.text = "v" + AppVersionUtils.getRawVersionName(this)
|
||||
|
||||
// Set onClickListeners for buttons
|
||||
binding.privacyPolicyButton.setOnClickListener {
|
||||
|
||||
@@ -90,7 +90,7 @@ class SettingsMainActivity : AppCompatActivity(),
|
||||
R.id.settings__navigation__home -> {
|
||||
supportActionBar?.title = String.format(
|
||||
resources.getString(R.string.settings__home__title),
|
||||
resources.getString(R.string.app_name)
|
||||
resources.getString(R.string.floris_app_name)
|
||||
)
|
||||
loadFragment(HomeFragment())
|
||||
true
|
||||
|
||||
@@ -28,10 +28,10 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.forEach
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.ThemeEditorActivityBinding
|
||||
import dev.patrickgold.florisboard.databinding.ThemeEditorGroupViewBinding
|
||||
import dev.patrickgold.florisboard.databinding.ThemeEditorMetaDialogBinding
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
@@ -60,6 +60,12 @@ class ThemeEditorActivity : AppCompatActivity() {
|
||||
private var editedThemeRef: AssetRef? = null
|
||||
private var isSaved: Boolean = false
|
||||
|
||||
private var themeLabel: String = ""
|
||||
set(v) {
|
||||
field = v
|
||||
binding.themeNameLabel.text = v
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Constant code for a theme saved activity result. */
|
||||
const val RESULT_CODE_THEME_EDIT_SAVED: Int = 0xFBADC1
|
||||
@@ -85,6 +91,8 @@ class ThemeEditorActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
binding.themeNameEditBtn.setOnClickListener { showMetaEditDialog() }
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
@@ -129,22 +137,13 @@ class ThemeEditorActivity : AppCompatActivity() {
|
||||
R.id.add_group_btn -> addGroup()
|
||||
R.id.theme_cancel_btn -> onBackPressed()
|
||||
R.id.theme_save_btn -> {
|
||||
val themeName = binding.themeNameValue.text.toString().trim()
|
||||
if (Theme.validateField(Theme.ValidationField.THEME_LABEL, themeName)) {
|
||||
val ref = editedThemeRef
|
||||
if (ref != null) {
|
||||
themeManager.writeTheme(
|
||||
ref, editedTheme.copy(
|
||||
label = themeName
|
||||
)
|
||||
)
|
||||
isSaved = true
|
||||
finish()
|
||||
}
|
||||
} else {
|
||||
binding.themeNameLabel.error =
|
||||
resources.getString(R.string.settings__theme_editor__error_theme_label_empty)
|
||||
binding.themeNameLabel.isErrorEnabled = true
|
||||
val ref = editedThemeRef
|
||||
if (ref != null) {
|
||||
themeManager.writeTheme(
|
||||
ref, editedTheme.copy(label = themeLabel)
|
||||
)
|
||||
isSaved = true
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,7 +304,7 @@ class ThemeEditorActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
editedTheme = editedTheme.copy(
|
||||
label = binding.themeNameValue.text.toString(),
|
||||
label = themeLabel,
|
||||
attributes = tempMap.toMap()
|
||||
)
|
||||
binding.keyboardPreview.onThemeUpdated(editedTheme)
|
||||
@@ -315,7 +314,7 @@ class ThemeEditorActivity : AppCompatActivity() {
|
||||
* Builds the Ui for the current [editedTheme]. Also sorts the groups afterwards.
|
||||
*/
|
||||
private fun buildUi() {
|
||||
binding.themeNameValue.setText(editedTheme.label)
|
||||
themeLabel = editedTheme.label
|
||||
for ((groupName, groupAttrs) in editedTheme.attributes) {
|
||||
val groupView = addGroup(groupName).root
|
||||
for ((attrName, attrValue) in groupAttrs) {
|
||||
@@ -330,4 +329,29 @@ class ThemeEditorActivity : AppCompatActivity() {
|
||||
}
|
||||
sortGroups()
|
||||
}
|
||||
|
||||
private fun showMetaEditDialog() {
|
||||
val dialogView = ThemeEditorMetaDialogBinding.inflate(layoutInflater)
|
||||
dialogView.metaName.setText(editedTheme.label)
|
||||
val dialog: AlertDialog
|
||||
AlertDialog.Builder(this).apply {
|
||||
setTitle(R.string.settings__theme_editor__edit_theme_name_dialog_title)
|
||||
setCancelable(true)
|
||||
setView(dialogView.root)
|
||||
setPositiveButton(android.R.string.ok, null)
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
create()
|
||||
dialog = show()
|
||||
dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
|
||||
val tempThemeLabel = dialogView.metaName.text.toString().trim()
|
||||
if (Theme.validateField(Theme.ValidationField.THEME_LABEL, tempThemeLabel)) {
|
||||
themeLabel = tempThemeLabel
|
||||
dialog.dismiss()
|
||||
} else {
|
||||
dialogView.metaNameLabel.error = resources.getString(R.string.settings__theme_editor__error_theme_label_empty)
|
||||
dialogView.metaNameLabel.isErrorEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,26 +17,28 @@
|
||||
package dev.patrickgold.florisboard.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RadioButton
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.forEach
|
||||
import com.github.michaelbull.result.getOr
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.ThemeManagerActivityBinding
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisActivity
|
||||
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.extension.ExternalContentUtils
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
@@ -47,11 +49,9 @@ import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
class ThemeManagerActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ThemeManagerActivityBinding
|
||||
class ThemeManagerActivity : FlorisActivity<ThemeManagerActivityBinding>() {
|
||||
private lateinit var layoutManager: LayoutManager
|
||||
private val mainScope = MainScope()
|
||||
private lateinit var prefs: PrefHelper
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
|
||||
private var key: String = ""
|
||||
@@ -59,19 +59,70 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
private var selectedTheme: Theme = Theme.empty()
|
||||
private var selectedRef: AssetRef? = null
|
||||
|
||||
companion object {
|
||||
private const val EDITOR_REQ_CODE: Int = 0xFB01
|
||||
private val themeEditor = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult? ->
|
||||
if (result?.resultCode == ThemeEditorActivity.RESULT_CODE_THEME_EDIT_SAVED) {
|
||||
themeManager.update()
|
||||
buildUi()
|
||||
}
|
||||
}
|
||||
|
||||
private val importTheme = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||
// If uri is null it indicates that the selection activity was cancelled (mostly by pressing the back button,
|
||||
// so we don't display an error message here.
|
||||
if (uri == null) return@registerForActivityResult
|
||||
val toBeImportedTheme = themeManager.loadTheme(uri)
|
||||
if (toBeImportedTheme.isSuccess) {
|
||||
val newTheme = toBeImportedTheme.getOrNull()!!.copy(
|
||||
name = toBeImportedTheme.getOrNull()!!.name + "_imported",
|
||||
label = toBeImportedTheme.getOrNull()!!.label + " (Imported)"
|
||||
)
|
||||
val newAssetRef = AssetRef(
|
||||
AssetSource.Internal,
|
||||
ThemeManager.THEME_PATH_REL + "/" + newTheme.name + ".json"
|
||||
)
|
||||
themeManager.writeTheme(newAssetRef, newTheme).onSuccess {
|
||||
themeManager.update()
|
||||
selectedTheme = newTheme
|
||||
selectedRef = newAssetRef
|
||||
setThemeRefInPrefs(newAssetRef)
|
||||
buildUi()
|
||||
showMessage(R.string.settings__theme_manager__theme_import_success)
|
||||
}.onFailure {
|
||||
showError(it)
|
||||
}
|
||||
} else {
|
||||
showError(toBeImportedTheme.exceptionOrNull()!!)
|
||||
}
|
||||
}
|
||||
|
||||
private val exportTheme = registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri: Uri? ->
|
||||
// If uri is null it indicates that the selection activity was cancelled (mostly by pressing the back button,
|
||||
// so we don't display an error message here.
|
||||
if (uri == null) return@registerForActivityResult
|
||||
val selectedRef = selectedRef
|
||||
if (selectedRef != null) {
|
||||
AssetManager.default().loadAssetRaw(selectedRef).onSuccess {
|
||||
Timber.i(it)
|
||||
ExternalContentUtils.writeTextToUri(this, uri, it).onSuccess {
|
||||
showMessage(R.string.settings__theme_manager__theme_export_success)
|
||||
}.onFailure {
|
||||
showError(it)
|
||||
}
|
||||
}.onFailure {
|
||||
showError(it)
|
||||
}
|
||||
} else {
|
||||
showError(NullPointerException("selectedRef is null!"))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_KEY: String = "key"
|
||||
const val EXTRA_DEFAULT_VALUE: String = "default_value"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
prefs = PrefHelper.getDefaultInstance(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ThemeManagerActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
key = intent.getStringExtra(EXTRA_KEY) ?: ""
|
||||
defaultValue = intent.getStringExtra(EXTRA_DEFAULT_VALUE) ?: ""
|
||||
@@ -91,8 +142,10 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
|
||||
binding.fabOptionCreateEmpty.setOnClickListener { onActionClicked(it) }
|
||||
binding.fabOptionCreateFromSelected.setOnClickListener { onActionClicked(it) }
|
||||
binding.fabOptionImport.setOnClickListener { onActionClicked(it) }
|
||||
binding.themeDeleteBtn.setOnClickListener { onActionClicked(it) }
|
||||
binding.themeEditBtn.setOnClickListener { onActionClicked(it) }
|
||||
binding.themeExportBtn.setOnClickListener { onActionClicked(it) }
|
||||
|
||||
layoutManager = LayoutManager(this).apply {
|
||||
preloadComputedLayout(KeyboardMode.CHARACTERS, Subtype.DEFAULT, prefs)
|
||||
@@ -101,6 +154,10 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
buildUi()
|
||||
}
|
||||
|
||||
override fun onCreateBinding(): ThemeManagerActivityBinding {
|
||||
return ThemeManagerActivityBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
@@ -132,7 +189,7 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
PrefHelper.Theme.NIGHT_THEME_REF -> prefs.theme.nightThemeRef
|
||||
else -> ""
|
||||
}
|
||||
).getOr(null)
|
||||
).getOrDefault(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,15 +234,6 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == EDITOR_REQ_CODE) {
|
||||
themeManager.update()
|
||||
buildUi()
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
fun onActionClicked(view: View) {
|
||||
when (view.id) {
|
||||
R.id.fab_option_create_empty -> {
|
||||
@@ -202,9 +250,11 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
ThemeManager.THEME_PATH_REL + "/" + newTheme.name + ".json"
|
||||
)
|
||||
themeManager.writeTheme(newAssetRef, newTheme).onSuccess {
|
||||
startActivityForResult(Intent(this, ThemeEditorActivity::class.java).apply {
|
||||
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, newAssetRef.toString())
|
||||
}, EDITOR_REQ_CODE)
|
||||
themeEditor.launch(
|
||||
Intent(this, ThemeEditorActivity::class.java).apply {
|
||||
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, newAssetRef.toString())
|
||||
}
|
||||
)
|
||||
}.onFailure {
|
||||
Timber.e(it.toString())
|
||||
}
|
||||
@@ -227,16 +277,18 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
ThemeManager.THEME_PATH_REL + "/" + themeCopy.name + ".json"
|
||||
)
|
||||
themeManager.writeTheme(newAssetRef, themeCopy).onSuccess {
|
||||
startActivityForResult(Intent(this, ThemeEditorActivity::class.java).apply {
|
||||
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, newAssetRef.toString())
|
||||
}, EDITOR_REQ_CODE)
|
||||
themeEditor.launch(
|
||||
Intent(this, ThemeEditorActivity::class.java).apply {
|
||||
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, newAssetRef.toString())
|
||||
}
|
||||
)
|
||||
}.onFailure {
|
||||
Timber.e(it.toString())
|
||||
}
|
||||
}
|
||||
/*R.id.fab_option_import -> {
|
||||
Toast.makeText(this, "Import not yet implemented", Toast.LENGTH_SHORT).show()
|
||||
}*/
|
||||
R.id.fab_option_import -> {
|
||||
importTheme.launch("*/*")
|
||||
}
|
||||
R.id.theme_delete_btn -> {
|
||||
val deleteRef = selectedRef?.copy()
|
||||
if (deleteRef?.source == AssetSource.Internal) {
|
||||
@@ -273,9 +325,11 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
R.id.theme_edit_btn -> {
|
||||
val editRef = selectedRef
|
||||
if (editRef?.source == AssetSource.Internal) {
|
||||
startActivityForResult(Intent(this, ThemeEditorActivity::class.java).apply {
|
||||
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, editRef.toString())
|
||||
}, EDITOR_REQ_CODE)
|
||||
themeEditor.launch(
|
||||
Intent(this, ThemeEditorActivity::class.java).apply {
|
||||
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, editRef.toString())
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// This toast normally should never show, though if the edit button is enabled
|
||||
// even if it shouldn't, just show a toast so the user knows the app is
|
||||
@@ -287,6 +341,9 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
).show()
|
||||
}
|
||||
}
|
||||
R.id.theme_export_btn -> {
|
||||
exportTheme.launch("${selectedTheme.name}.json")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.SeekBar
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.Preference.OnPreferenceChangeListener
|
||||
import androidx.preference.Preference.OnPreferenceClickListener
|
||||
import androidx.preference.PreferenceManager
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.SeekBarDialogBinding
|
||||
@@ -161,4 +163,4 @@ class DialogSeekBarPreference : Preference {
|
||||
private fun seekBarProgressToActualValue(progress: Int): Int {
|
||||
return (progress * step) + min
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.Preference.OnPreferenceClickListener
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
@@ -104,4 +104,4 @@ class ThemeSelectorPreference : Preference, SharedPreferences.OnSharedPreference
|
||||
i.putExtra(ThemeManagerActivity.EXTRA_DEFAULT_VALUE, defaultValue)
|
||||
context.startActivity(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import android.app.TimePickerDialog
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.Preference.OnPreferenceChangeListener
|
||||
import androidx.preference.Preference.OnPreferenceClickListener
|
||||
import androidx.preference.PreferenceManager
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.util.TimeUtil
|
||||
@@ -82,4 +84,4 @@ class TimePickerDialogPreference : Preference {
|
||||
}, time.hour, time.minute, true)
|
||||
timePickerDialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@ import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.settings.components.DialogSeekBarPreference
|
||||
|
||||
class KeyboardFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private var heightFactorCustom: DialogSeekBarPreference? = null
|
||||
private var oneHandedModeScaleFactor: DialogSeekBarPreference? = null
|
||||
private var utilityKeyAction: ListPreference? = null
|
||||
private var sharedPrefs: SharedPreferences? = null
|
||||
|
||||
@@ -36,8 +38,10 @@ class KeyboardFragment : PreferenceFragmentCompat(),
|
||||
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
heightFactorCustom = findPreference(PrefHelper.Keyboard.HEIGHT_FACTOR_CUSTOM)
|
||||
oneHandedModeScaleFactor = findPreference(PrefHelper.Keyboard.ONE_HANDED_MODE_SCALE_FACTOR)
|
||||
utilityKeyAction = findPreference(PrefHelper.Keyboard.UTILITY_KEY_ACTION)
|
||||
onSharedPreferenceChanged(null, PrefHelper.Keyboard.HEIGHT_FACTOR)
|
||||
onSharedPreferenceChanged(null, PrefHelper.Keyboard.ONE_HANDED_MODE)
|
||||
onSharedPreferenceChanged(null, PrefHelper.Keyboard.UTILITY_KEY_ENABLED)
|
||||
}
|
||||
|
||||
@@ -52,10 +56,16 @@ class KeyboardFragment : PreferenceFragmentCompat(),
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
if (key == PrefHelper.Keyboard.HEIGHT_FACTOR) {
|
||||
heightFactorCustom?.isVisible = sharedPrefs?.getString(key, "") == "custom"
|
||||
} else if (key == PrefHelper.Keyboard.UTILITY_KEY_ENABLED) {
|
||||
utilityKeyAction?.isVisible = sharedPrefs?.getBoolean(key, false) == true
|
||||
when (key) {
|
||||
PrefHelper.Keyboard.HEIGHT_FACTOR -> {
|
||||
heightFactorCustom?.isVisible = sharedPrefs?.getString(key, "") == "custom"
|
||||
}
|
||||
PrefHelper.Keyboard.ONE_HANDED_MODE -> {
|
||||
oneHandedModeScaleFactor?.isEnabled = sharedPrefs?.getString(key, "") != OneHandedMode.OFF
|
||||
}
|
||||
PrefHelper.Keyboard.UTILITY_KEY_ENABLED -> {
|
||||
utilityKeyAction?.isVisible = sharedPrefs?.getBoolean(key, false) == true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import dev.patrickgold.florisboard.databinding.SetupFragmentFinishBinding
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
|
||||
class FinishFragment : Fragment() {
|
||||
private lateinit var binding: SetupFragmentFinishBinding
|
||||
|
||||
@@ -18,7 +18,6 @@ package dev.patrickgold.florisboard.util
|
||||
|
||||
import android.content.Context
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import java.lang.Exception
|
||||
|
||||
object AppVersionUtils {
|
||||
fun getRawVersionName(context: Context): String {
|
||||
@@ -69,12 +68,12 @@ data class VersionName(
|
||||
if (list.size == 3) {
|
||||
return VersionName(list[0], list[1], list[2])
|
||||
}
|
||||
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[.][0-9]+""".toRegex())) {
|
||||
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[-][0-9]+""".toRegex())) {
|
||||
val list = raw.split(".").map { it.toInt() }
|
||||
if (list.size == 4) {
|
||||
return VersionName(list[0], list[1], list[2], null, list[3])
|
||||
}
|
||||
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[.][a-zA-Z]+""".toRegex())) {
|
||||
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[-][a-zA-Z]+""".toRegex())) {
|
||||
val list = raw.split(".")
|
||||
if (list.size == 4) {
|
||||
return VersionName(
|
||||
@@ -82,7 +81,7 @@ data class VersionName(
|
||||
list[3], null
|
||||
)
|
||||
}
|
||||
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[.][a-zA-Z]+[0-9]+""".toRegex())) {
|
||||
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[-][a-zA-Z]+[0-9]+""".toRegex())) {
|
||||
val list = raw.split(".")
|
||||
if (list.size == 4) {
|
||||
val extraName = list[3].split("""[0-9]+""".toRegex())[0]
|
||||
|
||||
7
app/src/main/res/drawable/edit_text_background.xml
Normal file
7
app/src/main/res/drawable/edit_text_background.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle" android:tintMode="multiply">
|
||||
<solid android:color="@android:color/transparent"/>
|
||||
<corners android:radius="@dimen/landscapeInputUi_editText_cornerRadius"/>
|
||||
<stroke android:width="@dimen/landscapeInputUi_editText_borderWidth" android:color="@android:color/white"/>
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/ic_open_with.xml
Normal file
5
app/src/main/res/drawable/ic_open_with.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?android:attr/textColorPrimary" android:pathData="M10,9h4L14,6h3l-5,-5 -5,5h3v3zM9,10L6,10L6,7l-5,5 5,5v-3h3v-4zM23,12l-5,-5v3h-3v4h3v3l5,-5zM14,15h-4v3L7,18l5,5 5,-5h-3v-3z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_share.xml
Normal file
5
app/src/main/res/drawable/ic_share.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?android:attr/textColorPrimary" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
||||
@@ -20,17 +20,18 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/app_icon"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
android:background="@mipmap/floris_app_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:elevation="8dp"
|
||||
android:contentDescription="@string/about__app_icon_content_description"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:text="@string/floris_app_name"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
app:layout_constraintHeight_percent="0.75"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/move_home"
|
||||
app:layout_constraintBottom_toTopOf="@+id/move_start_of_line"
|
||||
android:src="@drawable/ic_keyboard_arrow_left"/>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
@@ -51,12 +51,12 @@
|
||||
app:layout_constraintWidth_percent="0.2"
|
||||
app:layout_constraintHeight_percent="0.75"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/move_end"
|
||||
app:layout_constraintBottom_toTopOf="@+id/move_end_of_line"
|
||||
app:layout_constraintLeft_toRightOf="@+id/select"
|
||||
android:src="@drawable/ic_keyboard_arrow_right"/>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
android:id="@+id/move_home"
|
||||
android:id="@+id/move_start_of_line"
|
||||
style="@style/TextEditingButton"
|
||||
app:layout_constraintWidth_percent="0.35"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
@@ -65,11 +65,11 @@
|
||||
android:src="@drawable/ic_first_page"/>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
android:id="@+id/move_end"
|
||||
android:id="@+id/move_end_of_line"
|
||||
style="@style/TextEditingButton"
|
||||
app:layout_constraintWidth_percent="0.35"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
app:layout_constraintLeft_toRightOf="@+id/move_home"
|
||||
app:layout_constraintLeft_toRightOf="@+id/move_start_of_line"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:src="@drawable/ic_last_page"/>
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
app:layout_constraintTop_toBottomOf="@+id/clipboard_paste"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/move_end"
|
||||
app:layout_constraintLeft_toRightOf="@+id/move_end_of_line"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:src="@drawable/ic_backspace"/>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<dev.patrickgold.florisboard.ime.core.InputWindowView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/florisboard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -18,29 +19,32 @@
|
||||
android:background="?inputView_bgColorFallback"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<LinearLayout
|
||||
<dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel
|
||||
android:id="@+id/one_handed_ctrl_panel_start"
|
||||
style="@style/OneHandedPanel"
|
||||
android:visibility="gone">
|
||||
android:layout_width="@dimen/one_handed_width"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="0"
|
||||
android:visibility="gone"
|
||||
app:panelSide="start">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_close_start"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:src="@drawable/ic_zoom_out_map"
|
||||
android:contentDescription="@string/one_handed__close_btn_content_description"/>
|
||||
|
||||
<View
|
||||
android:tag="one_handed_ctrl_close"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/one_handed_button_height"
|
||||
android:visibility="invisible"/>
|
||||
android:layout_marginBottom="@dimen/one_handed_button_height"
|
||||
android:src="@drawable/ic_zoom_out_map"
|
||||
android:background="@drawable/button_transparent_bg_on_press"
|
||||
android:contentDescription="@string/one_handed__close_btn_content_description"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_move_start"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:tag="one_handed_ctrl_move"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/one_handed_button_height"
|
||||
android:src="@drawable/ic_keyboard_arrow_left"
|
||||
android:background="@drawable/button_transparent_bg_on_press"
|
||||
android:contentDescription="@string/one_handed__move_start_btn_content_description"/>
|
||||
|
||||
</LinearLayout>
|
||||
</dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.core.FlorisViewFlipper
|
||||
android:id="@+id/main_view_flipper"
|
||||
@@ -55,29 +59,32 @@
|
||||
|
||||
</dev.patrickgold.florisboard.ime.core.FlorisViewFlipper>
|
||||
|
||||
<LinearLayout
|
||||
<dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel
|
||||
android:id="@+id/one_handed_ctrl_panel_end"
|
||||
style="@style/OneHandedPanel"
|
||||
android:visibility="gone">
|
||||
android:layout_width="@dimen/one_handed_width"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="0"
|
||||
android:visibility="gone"
|
||||
app:panelSide="end">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_close_end"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:src="@drawable/ic_zoom_out_map"
|
||||
android:contentDescription="@string/one_handed__close_btn_content_description"/>
|
||||
|
||||
<View
|
||||
android:tag="one_handed_ctrl_close"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/one_handed_button_height"
|
||||
android:visibility="invisible"/>
|
||||
android:layout_marginBottom="@dimen/one_handed_button_height"
|
||||
android:src="@drawable/ic_zoom_out_map"
|
||||
android:background="@drawable/button_transparent_bg_on_press"
|
||||
android:contentDescription="@string/one_handed__close_btn_content_description"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_move_end"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:tag="one_handed_ctrl_move"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/one_handed_button_height"
|
||||
android:src="@drawable/ic_keyboard_arrow_right"
|
||||
android:contentDescription="@string/one_handed__move_end_btn_content_description"/>
|
||||
android:background="@drawable/button_transparent_bg_on_press"
|
||||
android:contentDescription="@string/one_handed__move_start_btn_content_description"/>
|
||||
|
||||
</LinearLayout>
|
||||
</dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel>
|
||||
|
||||
</dev.patrickgold.florisboard.ime.core.InputView>
|
||||
|
||||
|
||||
@@ -39,6 +39,16 @@
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
style="@style/SettingsCardView">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="This version includes experimental support for composing suggestions, currently regardless of your selected subtype the language is English (US). Note that currently only suggestions are displayed if at least one character of a word is typed in. Next-word suggestions are planned for the next version. If you want to give feedback on this feature, you can do so on the GitHub repository, feedback is always highly appreciated :)"/>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
style="@style/SettingsCardView">
|
||||
|
||||
|
||||
@@ -45,22 +45,19 @@
|
||||
|
||||
<Button
|
||||
android:id="@+id/candidate0"
|
||||
style="@style/SmartbarCandidate"
|
||||
android:text="suggestions"/>
|
||||
style="@style/SmartbarCandidate"/>
|
||||
|
||||
<View style="@style/SmartbarDivider"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/candidate1"
|
||||
style="@style/SmartbarCandidate"
|
||||
android:text="not yet"/>
|
||||
style="@style/SmartbarCandidate"/>
|
||||
|
||||
<View style="@style/SmartbarDivider"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/candidate2"
|
||||
style="@style/SmartbarCandidate"
|
||||
android:text="implemented"/>
|
||||
style="@style/SmartbarCandidate"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -110,12 +107,11 @@
|
||||
android:contentDescription="@string/smartbar__quick_action__one_handed_mode"
|
||||
android:src="@drawable/ic_smartphone"/>
|
||||
|
||||
<!-- TODO: find better icon for editing -->
|
||||
<dev.patrickgold.florisboard.ime.text.smartbar.SmartbarQuickActionButton
|
||||
android:id="@+id/quick_action_switch_to_editing_context"
|
||||
style="@style/SmartbarQuickAction"
|
||||
android:contentDescription="@string/smartbar__quick_action__switch_to_editing_context"
|
||||
android:src="@drawable/ic_format_italic"/>
|
||||
android:src="@drawable/ic_open_with"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -42,28 +42,31 @@
|
||||
android:layout_margin="8dp"
|
||||
app:isPreviewKeyboard="true"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/theme_name_label"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:hint="@string/settings__theme_editor__name_label"
|
||||
app:boxBackgroundMode="outline"
|
||||
app:boxBackgroundColor="?android:windowBackground"
|
||||
app:boxStrokeColor="?colorAccent"
|
||||
app:boxStrokeErrorColor="?colorError"
|
||||
app:boxStrokeWidth="1dp">
|
||||
android:orientation="horizontal"
|
||||
android:layout_margin="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/theme_name_value"
|
||||
android:layout_width="match_parent"
|
||||
<TextView
|
||||
android:id="@+id/theme_name_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textFilter"
|
||||
android:imeOptions="flagForceAscii|flagNoExtractUi"/>
|
||||
android:layout_weight="1.0"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
tools:text="GroupName"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<ImageButton
|
||||
android:id="@+id/theme_name_edit_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_edit"
|
||||
android:tint="?android:textColorPrimary"
|
||||
android:contentDescription="@string/settings__theme_editor__edit_group_dialog_title"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
31
app/src/main/res/layout/theme_editor_meta_dialog.xml
Normal file
31
app/src/main/res/layout/theme_editor_meta_dialog.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingTop="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/meta_name_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/settings__theme_editor__name_label"
|
||||
app:boxBackgroundMode="outline"
|
||||
app:boxBackgroundColor="?android:windowBackground"
|
||||
app:boxStrokeColor="?colorAccent"
|
||||
app:boxStrokeErrorColor="?colorError"
|
||||
app:boxStrokeWidth="1dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/meta_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textFilter"
|
||||
android:imeOptions="flagForceAscii|flagNoExtractUi"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -112,15 +112,30 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end">
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/theme_export_btn"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAllCaps="false"
|
||||
android:text="@string/assets__action__export"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:drawableStart="@drawable/ic_share"
|
||||
android:drawablePadding="8dp"
|
||||
android:drawableTint="?colorAccent"/>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1.0"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/theme_delete_btn"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textAllCaps="false"
|
||||
android:text="@string/assets__action__delete"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
@@ -133,7 +148,6 @@
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textAllCaps="false"
|
||||
android:text="@string/assets__action__edit"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
@@ -160,7 +174,8 @@
|
||||
|
||||
<com.nambimobile.widgets.efab.ExpandableFabLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
app:layout_dodgeInsetEdges="bottom">
|
||||
|
||||
<com.nambimobile.widgets.efab.Overlay
|
||||
android:layout_width="match_parent"
|
||||
@@ -189,13 +204,13 @@
|
||||
app:fab_color="?colorPrimaryDark"
|
||||
app:fab_icon="@drawable/ic_file"
|
||||
app:label_text="@string/settings__theme_manager__create_from_selected"/>
|
||||
<!--<com.nambimobile.widgets.efab.FabOption
|
||||
<com.nambimobile.widgets.efab.FabOption
|
||||
android:id="@+id/fab_option_import"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:fab_color="?colorPrimaryDark"
|
||||
app:fab_icon="@drawable/ic_input"
|
||||
app:label_text="@string/assets__action__import"/>-->
|
||||
app:label_text="@string/assets__action__import"/>
|
||||
|
||||
</com.nambimobile.widgets.efab.ExpandableFabLayout>
|
||||
|
||||
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_app_icon_beta.xml
Normal file
5
app/src/main/res/mipmap-anydpi-v26/ic_app_icon_beta.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_app_icon_beta_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_app_icon_beta_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_app_icon_beta_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_app_icon_beta_foreground"/>
|
||||
</adaptive-icon>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_app_icon_debug.xml
Normal file
5
app/src/main/res/mipmap-anydpi-v26/ic_app_icon_debug.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_app_icon_debug_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_app_icon_debug_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_app_icon_debug_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_app_icon_debug_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_app_icon_release_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_app_icon_release_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_app_icon_release_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_app_icon_release_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user