Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91cbbe74a3 | ||
|
|
637d7fe503 | ||
|
|
6a5e5a1708 | ||
|
|
22fad5ba0b | ||
|
|
f3d2c8257a | ||
|
|
bc89675269 | ||
|
|
2603eb2b52 | ||
|
|
38baac1af9 | ||
|
|
7e56094f5c | ||
|
|
af6ee13855 | ||
|
|
edb8d87fa0 | ||
|
|
ff35372945 | ||
|
|
b6edbf76d0 | ||
|
|
1bde28e288 | ||
|
|
aafb02cb68 | ||
|
|
a07c91f089 | ||
|
|
1af3c1a210 | ||
|
|
c13ec8aca9 | ||
|
|
4a826cc0a3 | ||
|
|
1c9e4c0b4c | ||
|
|
6cbbca5658 | ||
|
|
a1b8550fe2 | ||
|
|
ab1007175d | ||
|
|
4cf8b4af58 | ||
|
|
2b001d9eb8 | ||
|
|
286ddd9971 | ||
|
|
d4c6411e1a | ||
|
|
915bcec0ee | ||
|
|
93eb731bf0 | ||
|
|
7a02f1c958 | ||
|
|
160d31beb0 | ||
|
|
6d389b9a7f | ||
|
|
3ea620a22e | ||
|
|
94f9f3f3e7 | ||
|
|
8f28d0e81a | ||
|
|
07ce0c83fa | ||
|
|
c95244cc06 | ||
|
|
3c2d427b1d | ||
|
|
85da2141cb | ||
|
|
cc9688a2dd | ||
|
|
cd048af114 | ||
|
|
4382dfc869 | ||
|
|
a622749b7b | ||
|
|
1c2596147a | ||
|
|
d355143ba1 | ||
|
|
a9eb4c0eec | ||
|
|
487a37bc66 | ||
|
|
85a54e701e | ||
|
|
2666acd4ae | ||
|
|
ba72e6274f | ||
|
|
e20ce07957 | ||
|
|
765a12537e | ||
|
|
4845ce55b5 | ||
|
|
47cd655d76 | ||
|
|
d3edd3d332 | ||
|
|
2d32364123 | ||
|
|
509308ec82 | ||
|
|
db65af5ea5 | ||
|
|
9a46cf9dff | ||
|
|
2591eaa49d | ||
|
|
57350b422d | ||
|
|
fe8efa8496 | ||
|
|
c5ce9ba252 | ||
|
|
4e39273812 | ||
|
|
43995f1ac5 | ||
|
|
3688f8e8dc | ||
|
|
5cbbbc2295 | ||
|
|
af08947929 | ||
|
|
3e8a227320 | ||
|
|
adb69dc365 | ||
|
|
c2998c9a2e | ||
|
|
f801c31ebb | ||
|
|
6b8652bcd9 | ||
|
|
65b5d252b6 | ||
|
|
c5ae916ece | ||
|
|
de3fcceeaf | ||
|
|
f06ea18e89 | ||
|
|
9d7754b8db | ||
|
|
2be1a328b6 | ||
|
|
9cd7931b3e | ||
|
|
03f9014b7c | ||
|
|
62abefc36e | ||
|
|
f5d79e8556 | ||
|
|
e68428ef11 | ||
|
|
1cfde9c2b9 | ||
|
|
765a596eb2 | ||
|
|
a27035a81b | ||
|
|
380eaffb08 | ||
|
|
0e7eac2796 | ||
|
|
6da344fd6c | ||
|
|
7787af69fd | ||
|
|
288bd61fb4 | ||
|
|
fe69c0f3e1 | ||
|
|
766c5efa95 | ||
|
|
35bd6e7c8d | ||
|
|
23f14ab57d | ||
|
|
ac688a38ab | ||
|
|
a2e393d7dd | ||
|
|
ba8ebaf231 | ||
|
|
a0e381ed93 | ||
|
|
09833a3369 | ||
|
|
f014b010d8 | ||
|
|
9512eb32f0 | ||
|
|
82f99bd721 | ||
|
|
1d710dfb85 | ||
|
|
1328eb1f05 |
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1 +1,2 @@
|
||||
* text=auto eol=lf
|
||||
* text=auto eol=lf
|
||||
*.bat text=auto eol=crlf
|
||||
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -15,18 +15,21 @@ assignees: ''
|
||||
- Thank you for your help in making FlorisBoard better!
|
||||
-->
|
||||
|
||||
**Environment information**
|
||||
- FlorisBoard Version: <!-- e.g. 0.1.0 -->
|
||||
- Install Source: <!-- Google PlayStore/F-Droid/GitHub/? -->
|
||||
- Device: <!-- e.g. OnePlus 7T -->
|
||||
- Android version, ROM: <!-- e.g. 10, Stock -->
|
||||
#### Short description
|
||||
Describe the bug in a short but concise way.
|
||||
|
||||
**Steps to reproduce**
|
||||
#### Steps to reproduce
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
#### Environment information
|
||||
- FlorisBoard Version: <!-- e.g. 0.3.6 -->
|
||||
- Install Source: <!-- Google PlayStore/F-Droid/GitHub/? -->
|
||||
- Device: <!-- e.g. OnePlus 7T -->
|
||||
- Android version, ROM: <!-- e.g. 10, Stock -->
|
||||
|
||||
<!-- (remove this line if you paste a log)
|
||||
```
|
||||
If applicable, paste the captured debug log here.
|
||||
|
||||
@@ -32,7 +32,7 @@ free to ask for help at any time!
|
||||
|
||||
## Adding a new keyboard layout / dictionary for locale
|
||||
|
||||
You can now oficially add layouts to FlorisBoard as described below.
|
||||
You can now officially add layouts to FlorisBoard as described below.
|
||||
FlorisBoard's core has stabilized enough that adding new content is
|
||||
safe, although there will be some changes in the future.
|
||||
|
||||
|
||||
34
README.md
34
README.md
@@ -64,7 +64,7 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
|
||||
### Layouts
|
||||
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish,
|
||||
Norwegian, Swedish/Finnish, Icelandic, Danish, Hungarian,
|
||||
Croatian, Polish, Romanian); more coming in future versions
|
||||
Croatian, Polish, Romanian, Colemak, Dvorak); more coming in future versions
|
||||
* [x] Non-latin character layouts (Arabic, Persian, Greek, Russian
|
||||
(JCUKEN))
|
||||
* [x] Adapt to situation in app (password, url, text, etc. )
|
||||
@@ -106,7 +106,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 +115,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,10 +131,10 @@ 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
|
||||
@@ -154,6 +153,10 @@ 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,
|
||||
@@ -181,6 +184,21 @@ to get more information on this topic.
|
||||
* [expandable-fab](https://github.com/nambicompany/expandable-fab) by
|
||||
[Nambi](https://github.com/nambicompany)
|
||||
|
||||
## Usage notes for included binary dictionary files
|
||||
All binary dictionaries included within this project in
|
||||
(this)[app/src/main/assets/ime/dict) asset folder are built from various
|
||||
sources, as stated below.
|
||||
|
||||
### Source 1: [wordfreq library by LuminosoInsight](https://github.com/LuminosoInsight/wordfreq):
|
||||
`wordfreq` is a repository which provides both a Python library and raw
|
||||
data (the wordlists). Only the data has been extracted in order to build
|
||||
binary dictionary files from it. `wordfreq`'s data is licensed under the
|
||||
Creative Commons Attribution-ShareAlike 4.0 license
|
||||
(https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
|
||||
For further information on what wordfreq's data depends on, see
|
||||
(https://github.com/LuminosoInsight/wordfreq#license).
|
||||
|
||||
## License
|
||||
```
|
||||
Copyright 2020 Patrick Goldinger
|
||||
|
||||
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,76 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion "29.0.2"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "dev.patrickgold.florisboard"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 29
|
||||
versionCode 24
|
||||
versionName "0.3.5"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
resValue "string", "app_name", "FlorisBoard Debug"
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
resValue "string", "app_name", "FlorisBoard"
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'androidx.test:core:1.3.0'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
testImplementation 'org.mockito:mockito-inline:2.13.0'
|
||||
testImplementation 'org.robolectric:robolectric:4.4'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
implementation 'com.google.android:flexbox:2.0.1'
|
||||
implementation "com.squareup.moshi:moshi-kotlin:1.9.2"
|
||||
implementation 'com.squareup.moshi:moshi-adapters:1.9.2'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
|
||||
implementation 'com.jaredrummler:colorpicker:1.1.0'
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
implementation "com.michael-bull.kotlin-result:kotlin-result:1.1.9"
|
||||
implementation 'com.nambimobile.widgets:expandable-fab:1.0.2'
|
||||
}
|
||||
77
app/build.gradle.kts
Normal file
77
app/build.gradle.kts
Normal file
@@ -0,0 +1,77 @@
|
||||
plugins {
|
||||
id("com.android.application") version "4.1.2"
|
||||
kotlin("android") version "1.4.30"
|
||||
}
|
||||
|
||||
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()
|
||||
freeCompilerArgs = listOf("-Xallow-result-return-type") // enables use of kotlin.Result
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "dev.patrickgold.florisboard"
|
||||
minSdkVersion(23)
|
||||
targetSdkVersion(29)
|
||||
versionCode(27)
|
||||
versionName("0.3.8")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
named("debug").configure {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "floris_app_name", "FlorisBoard Debug")
|
||||
}
|
||||
|
||||
named("release").configure {
|
||||
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
resValue("string", "floris_app_name", "@string/app_name")
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
isAbortOnError = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
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")
|
||||
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.michael-bull.kotlin-result", "kotlin-result", "1.1.10")
|
||||
implementation("com.nambimobile.widgets", "expandable-fab", "1.0.2")
|
||||
|
||||
testImplementation("junit", "junit", "4.13.1")
|
||||
testImplementation("org.mockito", "mockito-inline", "3.7.7")
|
||||
testImplementation("org.robolectric", "robolectric", "4.5.1")
|
||||
androidTestImplementation("androidx.test.ext", "junit", "1.1.2")
|
||||
androidTestImplementation("androidx.test.espresso", "espresso-core", "3.3.0")
|
||||
}
|
||||
21
app/proguard-rules.pro
vendored
21
app/proguard-rules.pro
vendored
@@ -1,21 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -1,13 +1,11 @@
|
||||
package dev.patrickgold.florisboard
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -23,9 +23,8 @@
|
||||
<application
|
||||
android:name=".ime.core.FlorisApplication"
|
||||
android:allowBackup="false"
|
||||
android:extractNativeLibs="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:label="@string/floris_app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/SettingsTheme">
|
||||
@@ -33,7 +32,7 @@
|
||||
<!-- 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"
|
||||
@@ -57,7 +56,7 @@
|
||||
<activity-alias
|
||||
android:name="dev.patrickgold.florisboard.SettingsLauncherAlias"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:label="@string/floris_app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:targetActivity="dev.patrickgold.florisboard.setup.SetupActivity">
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"qwertz": "QWERTZ",
|
||||
"azerty": "AZERTY",
|
||||
"bepo": "BÉPO",
|
||||
"bulgarian_bds": "Bulgarian (BDS)",
|
||||
"bulgarian_phonetic": "Bulgarian (Phonetic)",
|
||||
"spanish": "Spanish (QWERTY)",
|
||||
"norwegian": "Norwegian (QWERTY)",
|
||||
"swedish_finnish": "Swedish/Finnish (QWERTY)",
|
||||
@@ -22,7 +24,11 @@
|
||||
"dvorak": "Dvorak",
|
||||
"jcuken_russian": "Russian (JCUKEN)",
|
||||
"canadian_french": "Canadian French (QWERTY)",
|
||||
"greek": "Ελληνικά"
|
||||
"greek": "Ελληνικά",
|
||||
"hebrew": "עברית",
|
||||
"serbian_latin": "Serbian (QWERTZ)",
|
||||
"serbian_cyrillic": "Serbian (ЉЊЕРТЗ)",
|
||||
"kurdish": "کوردی"
|
||||
},
|
||||
"defaultSubtypes": [
|
||||
{
|
||||
@@ -184,6 +190,36 @@
|
||||
"id": 1601,
|
||||
"languageTag": "pl",
|
||||
"preferredLayout": "qwerty"
|
||||
},
|
||||
{
|
||||
"id": 1701,
|
||||
"languageTag": "bg-bg",
|
||||
"preferredLayout": "bulgarian_phonetic"
|
||||
},
|
||||
{
|
||||
"id": 1801,
|
||||
"languageTag": "tr",
|
||||
"preferredLayout": "qwerty"
|
||||
},
|
||||
{
|
||||
"id": 1901,
|
||||
"languageTag": "iw-IL",
|
||||
"preferredLayout": "hebrew"
|
||||
},
|
||||
{
|
||||
"id": 2001,
|
||||
"languageTag": "ckb",
|
||||
"preferredLayout": "kurdish"
|
||||
},
|
||||
{
|
||||
"id": 2101,
|
||||
"languageTag": "sr-RS",
|
||||
"preferredLayout": "serbian_cyrillic"
|
||||
},
|
||||
{
|
||||
"id": 2201,
|
||||
"languageTag": "lv-LV",
|
||||
"preferredLayout": "qwerty"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BIN
app/src/main/assets/ime/dict/en.flict
Normal file
BIN
app/src/main/assets/ime/dict/en.flict
Normal file
Binary file not shown.
@@ -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" },
|
||||
|
||||
46
app/src/main/assets/ime/text/characters/bulgarian_bds.json
Normal file
46
app/src/main/assets/ime/text/characters/bulgarian_bds.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "bulgarian_bds",
|
||||
"authors": [ "iorvethe" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 1099, "label": "ы" },
|
||||
{ "code": 1091, "label": "у" },
|
||||
{ "code": 1077, "label": "е" },
|
||||
{ "code": 1080, "label": "и" },
|
||||
{ "code": 1096, "label": "ш" },
|
||||
{ "code": 1097, "label": "щ" },
|
||||
{ "code": 1082, "label": "к" },
|
||||
{ "code": 1089, "label": "с" },
|
||||
{ "code": 1076, "label": "д" },
|
||||
{ "code": 1079, "label": "з" },
|
||||
{ "code": 1094, "label": "ц" }
|
||||
],
|
||||
[
|
||||
{ "code": 1100, "label": "ь" },
|
||||
{ "code": 1103, "label": "я" },
|
||||
{ "code": 1072, "label": "а" },
|
||||
{ "code": 1086, "label": "о" },
|
||||
{ "code": 1078, "label": "ж" },
|
||||
{ "code": 1075, "label": "г" },
|
||||
{ "code": 1090, "label": "т" },
|
||||
{ "code": 1085, "label": "н" },
|
||||
{ "code": 1074, "label": "в" },
|
||||
{ "code": 1084, "label": "м" },
|
||||
{ "code": 1095, "label": "ч" }
|
||||
],
|
||||
[
|
||||
{ "code": 1102, "label": "ю" },
|
||||
{ "code": 1081, "label": "й" },
|
||||
{ "code": 1098, "label": "ъ" },
|
||||
{ "code": 1101, "label": "э" },
|
||||
{ "code": 1092, "label": "ф" },
|
||||
{ "code": 1093, "label": "х" },
|
||||
{ "code": 1087, "label": "п" },
|
||||
{ "code": 1088, "label": "р" },
|
||||
{ "code": 1083, "label": "л" },
|
||||
{ "code": 1073, "label": "б" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "bulgarian_phonetic",
|
||||
"authors": [ "iorvethe" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 1103, "label": "я" },
|
||||
{ "code": 1074, "label": "в" },
|
||||
{ "code": 1077, "label": "е" },
|
||||
{ "code": 1088, "label": "р" },
|
||||
{ "code": 1090, "label": "т" },
|
||||
{ "code": 1098, "label": "ъ" },
|
||||
{ "code": 1091, "label": "у" },
|
||||
{ "code": 1080, "label": "и" },
|
||||
{ "code": 1086, "label": "о" },
|
||||
{ "code": 1087, "label": "п" },
|
||||
{ "code": 1095, "label": "ч" }
|
||||
],
|
||||
[
|
||||
{ "code": 1072, "label": "а" },
|
||||
{ "code": 1089, "label": "с" },
|
||||
{ "code": 1076, "label": "д" },
|
||||
{ "code": 1092, "label": "ф" },
|
||||
{ "code": 1075, "label": "г" },
|
||||
{ "code": 1093, "label": "х" },
|
||||
{ "code": 1081, "label": "й" },
|
||||
{ "code": 1082, "label": "к" },
|
||||
{ "code": 1083, "label": "л" },
|
||||
{ "code": 1096, "label": "ш" },
|
||||
{ "code": 1097, "label": "щ" }
|
||||
],
|
||||
[
|
||||
{ "code": 1079, "label": "з" },
|
||||
{ "code": 1100, "label": "ь" },
|
||||
{ "code": 1094, "label": "ц" },
|
||||
{ "code": 1078, "label": "ж" },
|
||||
{ "code": 1073, "label": "б" },
|
||||
{ "code": 1085, "label": "н" },
|
||||
{ "code": 1084, "label": "м" },
|
||||
{ "code": 1102, "label": "ю" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -5,18 +5,19 @@
|
||||
"mapping": {
|
||||
"all": {
|
||||
"~enter": {
|
||||
"main": { "code": -215, "label": "toggle_one_handed_mode", "type": "system_gui" },
|
||||
"main": { "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
|
||||
"relevant": [
|
||||
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" }
|
||||
{ "code": -216, "label": "toggle_one_handed_mode_right", "type": "system_gui" }
|
||||
]
|
||||
},
|
||||
"~left": {
|
||||
"main": { "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
|
||||
"relevant": [
|
||||
{ "code": -215, "label": "toggle_one_handed_mode", "type": "system_gui" },
|
||||
{ "code": -215, "label": "toggle_one_handed_mode_left", "type": "system_gui" },
|
||||
{ "code": -100, "label": "settings", "type": "system_gui" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "bg",
|
||||
"authors": [ "iorvethe" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"и": {
|
||||
"relevant": [
|
||||
{ "code": 1117, "label": "ѝ" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".bg" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
167
app/src/main/assets/ime/text/characters/extended_popups/ckb.json
Normal file
167
app/src/main/assets/ime/text/characters/extended_popups/ckb.json
Normal file
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "ckb",
|
||||
"authors": [ "GoRaN" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"ق": {
|
||||
"relevant": [
|
||||
{ "code": 1647, "label": "ٯ" }
|
||||
]
|
||||
},
|
||||
"ئ": {
|
||||
"relevant": [
|
||||
{"code": 1569, "label": "ء" }
|
||||
]
|
||||
},
|
||||
"ە": {
|
||||
"relevant": [
|
||||
{ "code": 1577, "label": "ة" },
|
||||
{ "code": 1729, "label": "ـہ" }
|
||||
]
|
||||
},
|
||||
"ر": {
|
||||
"relevant": [
|
||||
{ "code": 1685, "label": "ڕ" },
|
||||
{ "code": 1682, "label": "ڒ" }
|
||||
]
|
||||
},
|
||||
"ف": {
|
||||
"relevant": [
|
||||
{ "code": 1701, "label": "ڥ" },
|
||||
{ "code": 1698, "label": "ڢ" },
|
||||
{ "code": 1700, "label": "ڤ" },
|
||||
{ "code": 1697, "label": "ڡ" }
|
||||
]
|
||||
},
|
||||
"": {
|
||||
"relevant": [
|
||||
{ "code": 65163, "label": "ﺋ" },
|
||||
{ "code": 1569, "label": "ء" },
|
||||
{ "code": 65139, "label": "ﹳ" }
|
||||
]
|
||||
},
|
||||
"ع": {
|
||||
"relevant": [
|
||||
{ "code": 1551, "label": "؏" },
|
||||
{ "code": 1594, "label": "غ" }
|
||||
]
|
||||
},
|
||||
"د": {
|
||||
"relevant": [
|
||||
{ "code": 1676, "label": "ڌ" },
|
||||
{ "code": 64390, "label": "ﮆ" },
|
||||
{ "code": 1584, "label": "ذ" },
|
||||
{ "code": 1774, "label": "ۮ" }
|
||||
]
|
||||
},
|
||||
"ه": {
|
||||
"relevant": [
|
||||
{ "code": 1726, "label": "ھ" }
|
||||
]
|
||||
},
|
||||
"خ": {
|
||||
"relevant": [
|
||||
{ "code": 1567, "label": "؟" }
|
||||
]
|
||||
},
|
||||
"س": {
|
||||
"relevant": [
|
||||
{ "code": 1589, "label": "ص" }
|
||||
]
|
||||
},
|
||||
"ش": {
|
||||
"relevant": [
|
||||
{ "code": 1590, "label": "ض" }
|
||||
]
|
||||
},
|
||||
"ب": {
|
||||
"relevant": [
|
||||
{ "code": 65010, "label": "ﷲ" },
|
||||
{ "code": 65021, "label": "﷽" },
|
||||
{ "code": 65019, "label": "ﷻ" }
|
||||
]
|
||||
},
|
||||
"م": {
|
||||
"relevant": [
|
||||
{ "code": 65018, "label": "ﷺ" },
|
||||
{ "code": 65012, "label": "ﷴ" }
|
||||
]
|
||||
},
|
||||
"ل": {
|
||||
"relevant": [
|
||||
{ "code": 1718, "label": "ڶ" },
|
||||
{ "code": 1719, "label": "ڷ" },
|
||||
{ "code": 1717, "label": "ڵ" },
|
||||
{ "code": 1720, "label": "ڸ" }
|
||||
]
|
||||
},
|
||||
"ا": {
|
||||
"relevant": [
|
||||
{ "code": 1571, "label": "أ" },
|
||||
{ "code": 1573, "label": "إ" },
|
||||
{ "code": 1570, "label": "آ" },
|
||||
{ "code": 1649, "label": "ٱ" }
|
||||
]
|
||||
},
|
||||
"ک": {
|
||||
"relevant": [
|
||||
{ "code": 1706, "label": "ڪ" },
|
||||
{ "code": 1603, "label": "ك"}
|
||||
]
|
||||
},
|
||||
"ی": {
|
||||
"relevant": [
|
||||
{ "code": 1746, "label": "ے" },
|
||||
{ "code": 1610, "label": "ي" },
|
||||
{ "code": 1744, "label": "ې" },
|
||||
{ "code": 1741, "label": "ۍ" },
|
||||
{ "code": 1742, "label": "ێ" },
|
||||
{ "code": 1597, "label": "ؽ" }
|
||||
]
|
||||
},
|
||||
"ۆ": {
|
||||
"relevant": [
|
||||
{ "code": 1743, "label": "ۏ" },
|
||||
{ "code": 1735, "label": "ۇ" },
|
||||
{ "code": 1737, "label": "ۉ" },
|
||||
{ "code": 1738, "label": "ۊ" },
|
||||
{ "code": 1572, "label": "ؤ" },
|
||||
{ "code": 1739, "label": "ۋ" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 1567, "label": "؟" },
|
||||
"relevant": [
|
||||
{ "code": 1600, "label": "ــ" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 1548, "label": "،" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 1549, "label": "؍" },
|
||||
{ "code": 1563, "label": "؛" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 42, "label": "*" },
|
||||
{ "code": 95, "label": "_" },
|
||||
{ "code": 45, "label": "-" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".krd"},
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" },
|
||||
{ "code": -255, "label": ".iq" },
|
||||
{ "code": -255, "label": ".tv" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,23 +26,23 @@
|
||||
]
|
||||
},
|
||||
"e": {
|
||||
"main": { "code": 233, "label": "é" },
|
||||
"relevant": [
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 234, "label": "ê" }
|
||||
]
|
||||
},
|
||||
"i": {
|
||||
"main": { "code": 237, "label": "í" },
|
||||
"relevant": [
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 239, "label": "ï" }
|
||||
]
|
||||
},
|
||||
@@ -71,8 +71,8 @@
|
||||
]
|
||||
},
|
||||
"u": {
|
||||
"main": { "code": 250, "label": "ú" },
|
||||
"relevant": [
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
@@ -104,9 +104,9 @@
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com.es" },
|
||||
{ "code": -255, "label": ".es" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"mapping": {
|
||||
"all": {
|
||||
"a": {
|
||||
"main": { "code": 224, "label": "à" },
|
||||
"relevant": [
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
@@ -18,8 +18,8 @@
|
||||
]
|
||||
},
|
||||
"e": {
|
||||
"main": { "code": 232, "label": "è" },
|
||||
"relevant": [
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
@@ -29,8 +29,8 @@
|
||||
]
|
||||
},
|
||||
"i": {
|
||||
"main": { "code": 236, "label": "ì" },
|
||||
"relevant": [
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 239, "label": "ï" },
|
||||
{ "code": 303, "label": "į" },
|
||||
@@ -45,8 +45,8 @@
|
||||
]
|
||||
},
|
||||
"o": {
|
||||
"main": { "code": 242, "label": "ò" },
|
||||
"relevant": [
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 186, "label": "º" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
@@ -58,8 +58,8 @@
|
||||
]
|
||||
},
|
||||
"u": {
|
||||
"main": { "code": 249, "label": "ù" },
|
||||
"relevant": [
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
@@ -91,10 +91,10 @@
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".it" },
|
||||
{ "code": -255, "label": ".gov.it" },
|
||||
{ "code": -255, "label": ".edu.it" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".it" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "iw",
|
||||
"authors": [ "Antony" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" },
|
||||
{ "code": -255, "label": ".co.il" },
|
||||
{ "code": -255, "label": ".gov.il" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
112
app/src/main/assets/ime/text/characters/extended_popups/lv.json
Normal file
112
app/src/main/assets/ime/text/characters/extended_popups/lv.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "lv",
|
||||
"authors": [ "patrickgold", "eandersons" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"a": {
|
||||
"relevant": [
|
||||
{ "code": 257, "label": "ā" }
|
||||
]
|
||||
},
|
||||
"c": {
|
||||
"relevant": [
|
||||
{ "code": 269, "label": "č" }
|
||||
]
|
||||
},
|
||||
"e": {
|
||||
"relevant": [
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 8364, "label": "€" }
|
||||
]
|
||||
},
|
||||
"g": {
|
||||
"relevant": [
|
||||
{ "code": 291, "label": "ģ" }
|
||||
]
|
||||
},
|
||||
"i": {
|
||||
"relevant": [
|
||||
{ "code": 299, "label": "ī" }
|
||||
]
|
||||
},
|
||||
"k": {
|
||||
"relevant": [
|
||||
{ "code": 311, "label": "ķ" }
|
||||
]
|
||||
},
|
||||
"l": {
|
||||
"relevant": [
|
||||
{ "code": 316, "label": "ļ" }
|
||||
]
|
||||
},
|
||||
"n": {
|
||||
"relevant": [
|
||||
{ "code": 326, "label": "ņ" }
|
||||
]
|
||||
},
|
||||
"o": {
|
||||
"relevant": [
|
||||
{ "code": 333, "label": "ō" }
|
||||
]
|
||||
},
|
||||
"r": {
|
||||
"relevant": [
|
||||
{ "code": 343, "label": "ŗ" }
|
||||
]
|
||||
},
|
||||
"s": {
|
||||
"relevant": [
|
||||
{ "code": 353, "label": "š" }
|
||||
]
|
||||
},
|
||||
"u": {
|
||||
"relevant": [
|
||||
{ "code": 363, "label": "ū" }
|
||||
]
|
||||
},
|
||||
"z": {
|
||||
"relevant": [
|
||||
{ "code": 382, "label": "ž" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 8212, "label": "—" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".lv" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".eu" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov.lv" },
|
||||
{ "code": -255, "label": ".edu.lv" },
|
||||
{ "code": -255, "label": ".org.lv" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "sr",
|
||||
"authors": [ "hedidnothingwrong", "GrbavaCigla" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"c": {
|
||||
"relevant": [
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 263, "label": "ć" }
|
||||
]
|
||||
},
|
||||
"d": {
|
||||
"relevant": [
|
||||
{ "code": 273, "label": "đ" }
|
||||
]
|
||||
},
|
||||
"s": {
|
||||
"relevant": [
|
||||
{ "code": 353, "label": "š" }
|
||||
]
|
||||
},
|
||||
"z": {
|
||||
"relevant": [
|
||||
{ "code": 382, "label": "ž" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".eu" },
|
||||
{ "code": -255, "label": ".rs" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "tr",
|
||||
"authors": [ "kisekinopureya" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"a": {
|
||||
"relevant": [
|
||||
{ "code": 226, "label": "â" }
|
||||
]
|
||||
},
|
||||
"c": {
|
||||
"relevant": [
|
||||
{ "code": 231, "label": "ç" }
|
||||
]
|
||||
},
|
||||
"g": {
|
||||
"relevant": [
|
||||
{ "code": 287, "label": "ğ" }
|
||||
]
|
||||
},
|
||||
"i": {
|
||||
"relevant": [
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 305, "label": "ı" }
|
||||
]
|
||||
},
|
||||
"o": {
|
||||
"relevant": [
|
||||
{ "code": 246, "label": "ö" }
|
||||
]
|
||||
},
|
||||
"s": {
|
||||
"relevant": [
|
||||
{ "code": 351, "label": "ş" }
|
||||
]
|
||||
},
|
||||
"u": {
|
||||
"relevant": [
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 251, "label": "û" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".tr" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
app/src/main/assets/ime/text/characters/hebrew.json
Normal file
53
app/src/main/assets/ime/text/characters/hebrew.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "hebrew",
|
||||
"authors": [ "Antony" ],
|
||||
"direction": "rtl",
|
||||
"modifier": "hebrew",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 39, "label": "'", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 96, "label": "`" }
|
||||
]
|
||||
} },
|
||||
{ "code": 45, "label": "-", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 95, "label": "_" }
|
||||
]
|
||||
} },
|
||||
{ "code": 1511, "label": "ק" },
|
||||
{ "code": 1512, "label": "ר" },
|
||||
{ "code": 1488, "label": "א" },
|
||||
{ "code": 1496, "label": "ט" },
|
||||
{ "code": 1493, "label": "ו" },
|
||||
{ "code": 1503, "label": "ן" },
|
||||
{ "code": 1501, "label": "ם" },
|
||||
{ "code": 1508, "label": "פ" }
|
||||
],
|
||||
[
|
||||
{ "code": 1513, "label": "ף" },
|
||||
{ "code": 1491, "label": "ך" },
|
||||
{ "code": 1490, "label": "ל" },
|
||||
{ "code": 1499, "label": "ח" },
|
||||
{ "code": 1506, "label": "י" },
|
||||
{ "code": 1497, "label": "ע" },
|
||||
{ "code": 1495, "label": "כ" },
|
||||
{ "code": 1500, "label": "ג" },
|
||||
{ "code": 1498, "label": "ד" },
|
||||
{ "code": 1507, "label": "ש" }
|
||||
],
|
||||
[
|
||||
{ "code": 1494, "label": "ץ" },
|
||||
{ "code": 1505, "label": "ת" },
|
||||
{ "code": 1489, "label": "צ" },
|
||||
{ "code": 1492, "label": "מ" },
|
||||
{ "code": 1504, "label": "נ" },
|
||||
{ "code": 1502, "label": "ה" },
|
||||
{ "code": 1510, "label": "ב" },
|
||||
{ "code": 1514, "label": "ס" },
|
||||
{ "code": 1509, "label": "ז" }
|
||||
]
|
||||
]
|
||||
}
|
||||
57
app/src/main/assets/ime/text/characters/kurdish.json
Normal file
57
app/src/main/assets/ime/text/characters/kurdish.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "kurdish",
|
||||
"authors": [ "GoRaN" ],
|
||||
"direction": "rtl",
|
||||
"modifier": "kurdish",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 1602, "label": "ق", "popup": {
|
||||
"main": { "code": 1647, "label": "ٯ" }
|
||||
} },
|
||||
{ "code": 1608, "label": "و", "popup": {
|
||||
"main": { "code": -255, "label": "وو" }
|
||||
} },
|
||||
{ "code": 1749, "label": "ﻪ", "popup": {
|
||||
"main": { "code": 1577, "label": "ة" }
|
||||
} },
|
||||
{ "code": 1585, "label": "ر" },
|
||||
{ "code": 1578, "label": "ت", "popup": {
|
||||
"main": { "code": 1591, "label": "ط" }
|
||||
} },
|
||||
{ "code": 1740, "label": "ی" },
|
||||
{ "code": 1574, "label": "ﺋ", "popup": {
|
||||
"main": { "code": 1569, "label": "ء" }
|
||||
} },
|
||||
{ "code": 1593, "label": "ع" },
|
||||
{ "code": 1734, "label": "ۆ" },
|
||||
{ "code": 1662, "label": "پ", "popup": {
|
||||
"main": { "code": 1579, "label": "ث" }
|
||||
} }
|
||||
],
|
||||
[
|
||||
{ "code": 1575, "label": "ا" },
|
||||
{ "code": 1587, "label": "س" },
|
||||
{ "code": 1588, "label": "ش" },
|
||||
{ "code": 1583, "label": "د" },
|
||||
{ "code": 1601, "label": "ف" },
|
||||
{ "code": 1607, "label": "ھ" },
|
||||
{ "code": 1688, "label": "ژ" },
|
||||
{ "code": 1604, "label": "ل" },
|
||||
{ "code": 1705, "label": "ک" },
|
||||
{ "code": 1711, "label": "گ" }
|
||||
],
|
||||
[
|
||||
{ "code": 1586, "label": "ز", "popup": {
|
||||
"main": {"code": 1592, "label": "ظ" }
|
||||
} },
|
||||
{ "code": 1582, "label": "خ" },
|
||||
{ "code": 1580, "label": "ج" },
|
||||
{ "code": 1670, "label": "چ" },
|
||||
{ "code": 1581, "label": "ح" },
|
||||
{ "code": 1576, "label": "ب" },
|
||||
{ "code": 1606, "label": "ن" },
|
||||
{ "code": 1605, "label": "م" }
|
||||
]
|
||||
]
|
||||
}
|
||||
27
app/src/main/assets/ime/text/characters/mod/hebrew.json
Normal file
27
app/src/main/assets/ime/text/characters/mod/hebrew.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"type": "characters/mod",
|
||||
"name": "hebrew",
|
||||
"authors": [ "Antony" ],
|
||||
"direction": "rtl",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 0, "type": "placeholder" },
|
||||
{ "code": -5, "label": "delete", "type": "enter_editing" }
|
||||
],
|
||||
[
|
||||
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
|
||||
{ "code": 64, "label": "@", "groupId": 1, "variation": "email_address" },
|
||||
{ "code": 44, "label": ",", "groupId": 1, "variation": "normal" },
|
||||
{ "code": 44, "label": ",", "groupId": 1, "variation": "password" },
|
||||
{ "code": 47, "label": "/", "groupId": 1, "variation": "uri" },
|
||||
{ "code": -210, "label": "language_switch", "type": "system_gui" },
|
||||
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
|
||||
{ "code": 32, "label": "space" },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "email_address" },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "normal" },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "password" },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "uri" },
|
||||
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
|
||||
]
|
||||
]
|
||||
}
|
||||
27
app/src/main/assets/ime/text/characters/mod/kurdish.json
Normal file
27
app/src/main/assets/ime/text/characters/mod/kurdish.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"type": "characters/mod",
|
||||
"name": "kurdish",
|
||||
"authors": [ "GoRaN" ],
|
||||
"direction": "rtl",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 0, "type": "placeholder" },
|
||||
{ "code": -5, "label": "delete", "type": "enter_editing" }
|
||||
],
|
||||
[
|
||||
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
|
||||
{ "code": 64, "label": "@", "groupId": 1, "variation": "email_address" },
|
||||
{ "code": 1567, "label": "؟", "groupId": 1, "variation": "normal" },
|
||||
{ "code": 1548, "label": "،", "groupId": 1, "variation": "password" },
|
||||
{ "code": 47, "label": "/", "groupId": 1, "variation": "uri" },
|
||||
{ "code": -210, "label": "language_switch", "type": "system_gui" },
|
||||
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
|
||||
{ "code": 32, "label": " " },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "email_address" },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "normal" },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "password" },
|
||||
{ "code": 46, "label": ".", "groupId": 2, "variation": "uri" },
|
||||
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "serbian_cyrillic",
|
||||
"authors": ["GrbavaCigla"],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 1113, "label": "љ" },
|
||||
{ "code": 1114, "label": "њ" },
|
||||
{ "code": 1077, "label": "е" },
|
||||
{ "code": 1088, "label": "р" },
|
||||
{ "code": 1090, "label": "т" },
|
||||
{ "code": 1079, "label": "з" },
|
||||
{ "code": 1091, "label": "у" },
|
||||
{ "code": 1080, "label": "и" },
|
||||
{ "code": 1086, "label": "о" },
|
||||
{ "code": 1087, "label": "п" },
|
||||
{ "code": 1096, "label": "ш" }
|
||||
],
|
||||
[
|
||||
{ "code": 1072, "label": "а" },
|
||||
{ "code": 1089, "label": "с" },
|
||||
{ "code": 1076, "label": "д" },
|
||||
{ "code": 1092, "label": "ф" },
|
||||
{ "code": 1075, "label": "г" },
|
||||
{ "code": 1093, "label": "х" },
|
||||
{ "code": 1112, "label": "ј" },
|
||||
{ "code": 1082, "label": "к" },
|
||||
{ "code": 1083, "label": "л" },
|
||||
{ "code": 1095, "label": "ч" },
|
||||
{ "code": 1115, "label": "ћ" }
|
||||
],
|
||||
[
|
||||
{ "code": 1119, "label": "џ" },
|
||||
{ "code": 1094, "label": "ц" },
|
||||
{ "code": 1074, "label": "в" },
|
||||
{ "code": 1073, "label": "б" },
|
||||
{ "code": 1085, "label": "н" },
|
||||
{ "code": 1084, "label": "м" },
|
||||
{ "code": 1106, "label": "ђ" },
|
||||
{ "code": 1078, "label": "ж" }
|
||||
]
|
||||
]
|
||||
}
|
||||
45
app/src/main/assets/ime/text/characters/serbian_latin.json
Normal file
45
app/src/main/assets/ime/text/characters/serbian_latin.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "serbian_latin",
|
||||
"authors": ["GrbavaCigla"],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 113, "label": "q" },
|
||||
{ "code": 119, "label": "w" },
|
||||
{ "code": 101, "label": "e" },
|
||||
{ "code": 114, "label": "r" },
|
||||
{ "code": 116, "label": "t" },
|
||||
{ "code": 122, "label": "z" },
|
||||
{ "code": 117, "label": "u" },
|
||||
{ "code": 105, "label": "i" },
|
||||
{ "code": 111, "label": "o" },
|
||||
{ "code": 112, "label": "p" },
|
||||
{ "code": 353, "label": "š" }
|
||||
],
|
||||
[
|
||||
{ "code": 97, "label": "a" },
|
||||
{ "code": 115, "label": "s" },
|
||||
{ "code": 100, "label": "d" },
|
||||
{ "code": 102, "label": "f" },
|
||||
{ "code": 103, "label": "g" },
|
||||
{ "code": 104, "label": "h" },
|
||||
{ "code": 106, "label": "j" },
|
||||
{ "code": 107, "label": "k" },
|
||||
{ "code": 108, "label": "l" },
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 263, "label": "ć" }
|
||||
],
|
||||
[
|
||||
{ "code": 121, "label": "y" },
|
||||
{ "code": 120, "label": "x" },
|
||||
{ "code": 99, "label": "c" },
|
||||
{ "code": 118, "label": "v" },
|
||||
{ "code": 98, "label": "b" },
|
||||
{ "code": 110, "label": "n" },
|
||||
{ "code": 109, "label": "m" },
|
||||
{ "code": 273, "label": "đ" },
|
||||
{ "code": 382, "label": "ž" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -48,7 +48,7 @@
|
||||
{ "code": 48, "label": "0", "type": "numeric" },
|
||||
{ "code": 61, "label": "=" },
|
||||
{ "code": 46, "label": "." },
|
||||
{ "code": 10, "label": "enter", "type": "enter_editing" }
|
||||
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
{ "code": 960, "label": "π", "popup": {
|
||||
"main": { "code": 928, "label": "Π" },
|
||||
"relevant": [
|
||||
{ "code": 969, "label": "ω" },
|
||||
{ "code": 945, "label": "α" },
|
||||
{ "code": 946, "label": "β" },
|
||||
{ "code": 937, "label": "Ω" },
|
||||
{ "code": 956, "label": "μ" }
|
||||
]
|
||||
|
||||
73
app/src/main/assets/ime/theme/floris_black.json
Normal file
73
app/src/main/assets/ime/theme/floris_black.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
|
||||
"name": "floris_black",
|
||||
"label": "Floris Black",
|
||||
"authors": [ "serebit" ],
|
||||
"isNightTheme": true,
|
||||
"attributes": {
|
||||
"window": {
|
||||
"colorPrimary": "#388E3C",
|
||||
"colorPrimaryDark": "#306D32",
|
||||
"colorAccent": "#FF9800",
|
||||
"navigationBarColor": "@keyboard/background",
|
||||
"navigationBarLight": "false",
|
||||
"semiTransparentColor": "#20FFFFFF",
|
||||
"textColor": "#EEEEEE"
|
||||
},
|
||||
"keyboard": {
|
||||
"background": "#000000"
|
||||
},
|
||||
"key": {
|
||||
"background": "#212121",
|
||||
"backgroundPressed": "#3D3D3D",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundPressed": "@window/textColor",
|
||||
"showBorder": "true"
|
||||
},
|
||||
"key:enter": {
|
||||
"background": "@window/colorPrimary",
|
||||
"backgroundPressed": "@window/colorPrimaryDark",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundPressed": "@window/textColor"
|
||||
},
|
||||
"key:shift:capslock": {
|
||||
"foreground": "@window/colorAccent",
|
||||
"foregroundPressed": "@window/colorAccent"
|
||||
},
|
||||
"media": {
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#BDBDBD"
|
||||
},
|
||||
"oneHanded": {
|
||||
"background": "#000000",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"popup": {
|
||||
"background": "#424242",
|
||||
"backgroundActive": "#707070",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"privateMode": {
|
||||
"background": "#7800BF",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"smartbar": {
|
||||
"background": "transparent",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"smartbarButton": {
|
||||
"background": "@key/background",
|
||||
"foreground": "@key/foreground"
|
||||
},
|
||||
"extractEditLayout": {
|
||||
"background": "#282828",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"extractActionButton": {
|
||||
"background": "@smartbarButton/background",
|
||||
"foreground": "@smartbarButton/foreground"
|
||||
}
|
||||
}
|
||||
}
|
||||
76
app/src/main/assets/ime/theme/floris_black_borderless.json
Normal file
76
app/src/main/assets/ime/theme/floris_black_borderless.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
|
||||
"name": "floris_black_borderless",
|
||||
"label": "Floris Black Borderless",
|
||||
"authors": [ "serebit" ],
|
||||
"isNightTheme": true,
|
||||
"attributes": {
|
||||
"window": {
|
||||
"colorPrimary": "#388E3C",
|
||||
"colorPrimaryDark": "#306D32",
|
||||
"colorAccent": "#FF9800",
|
||||
"navigationBarColor": "@keyboard/background",
|
||||
"navigationBarLight": "false",
|
||||
"semiTransparentColor": "#20FFFFFF",
|
||||
"textColor": "#EEEEEE"
|
||||
},
|
||||
"keyboard": {
|
||||
"background": "#000000"
|
||||
},
|
||||
"key": {
|
||||
"background": "transparent",
|
||||
"backgroundPressed": "#7F616161",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundPressed": "@window/textColor",
|
||||
"showBorder": "false"
|
||||
},
|
||||
"key:enter": {
|
||||
"background": "@window/colorPrimary",
|
||||
"backgroundPressed": "@window/colorPrimaryDark",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundPressed": "@window/textColor"
|
||||
},
|
||||
"key:shift:capslock": {
|
||||
"foreground": "@window/colorAccent",
|
||||
"foregroundPressed": "@window/colorAccent"
|
||||
},
|
||||
"key:space": {
|
||||
"background": "#46616161"
|
||||
},
|
||||
"media": {
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#BDBDBD"
|
||||
},
|
||||
"oneHanded": {
|
||||
"background": "#000000",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"popup": {
|
||||
"background": "#363636",
|
||||
"backgroundActive": "#5F5F5F",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"privateMode": {
|
||||
"background": "#7800BF",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"smartbar": {
|
||||
"background": "transparent",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"smartbarButton": {
|
||||
"background": "#212121",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"extractEditLayout": {
|
||||
"background": "#282828",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"extractActionButton": {
|
||||
"background": "@smartbarButton/background",
|
||||
"foreground": "@smartbarButton/foreground"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,15 @@
|
||||
"smartbarButton": {
|
||||
"background": "@key/background",
|
||||
"foreground": "@key/foreground"
|
||||
},
|
||||
"extractEditLayout": {
|
||||
"background": "#E8E8E8",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#8A8A8A"
|
||||
},
|
||||
"extractActionButton": {
|
||||
"background": "@smartbarButton/background",
|
||||
"foreground": "@smartbarButton/foreground"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,15 @@
|
||||
"smartbarButton": {
|
||||
"background": "#FFFFFF",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"extractEditLayout": {
|
||||
"background": "#E8E8E8",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#8A8A8A"
|
||||
},
|
||||
"extractActionButton": {
|
||||
"background": "@smartbarButton/background",
|
||||
"foreground": "@smartbarButton/foreground"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,15 @@
|
||||
"smartbarButton": {
|
||||
"background": "@key/background",
|
||||
"foreground": "@key/foreground"
|
||||
},
|
||||
"extractEditLayout": {
|
||||
"background": "#282828",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"extractActionButton": {
|
||||
"background": "@smartbarButton/background",
|
||||
"foreground": "@smartbarButton/foreground"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,15 @@
|
||||
"smartbarButton": {
|
||||
"background": "#424242",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"extractEditLayout": {
|
||||
"background": "#282828",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"extractActionButton": {
|
||||
"background": "@smartbarButton/background",
|
||||
"foreground": "@smartbarButton/foreground"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
64
app/src/main/assets/ime/theme/gboard_day.json
Normal file
64
app/src/main/assets/ime/theme/gboard_day.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
|
||||
"name": "gboard_day",
|
||||
"label": "Gboard Day",
|
||||
"authors": [ "patrickgold", "itskareem" ],
|
||||
"isNightTheme": false,
|
||||
"attributes": {
|
||||
"window": {
|
||||
"colorPrimary": "#0479ed",
|
||||
"colorPrimaryDark": "#0467c9",
|
||||
"colorAccent": "#FF9800",
|
||||
"navigationBarColor": "@keyboard/background",
|
||||
"navigationBarLight": "true",
|
||||
"semiTransparentColor": "#20000000",
|
||||
"textColor": "#000000"
|
||||
},
|
||||
"keyboard": {
|
||||
"background": "#D1D6DC"
|
||||
},
|
||||
"key": {
|
||||
"background": "#FCFFFF",
|
||||
"backgroundPressed": "#F5F5F5",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundPressed": "@window/textColor",
|
||||
"showBorder": "true"
|
||||
},
|
||||
"key:enter": {
|
||||
"background": "@window/colorPrimary",
|
||||
"backgroundPressed": "@window/colorPrimaryDark",
|
||||
"foreground": "#FFFFFF",
|
||||
"foregroundPressed": "#FFFFFF"
|
||||
},
|
||||
"key:shift:capslock": {
|
||||
"foreground": "@window/colorAccent",
|
||||
"foregroundPressed": "@window/colorAccent"
|
||||
},
|
||||
"media": {
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#757575"
|
||||
},
|
||||
"oneHanded": {
|
||||
"background": "@keyboard/background",
|
||||
"foreground": "#424242"
|
||||
},
|
||||
"popup": {
|
||||
"background": "#EEEEEE",
|
||||
"backgroundActive": "#BDBDBD",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"privateMode": {
|
||||
"background": "#A000FF",
|
||||
"foreground": "#FFFFFF"
|
||||
},
|
||||
"smartbar": {
|
||||
"background": "@keyboard/background",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#8A8A8A"
|
||||
},
|
||||
"smartbarButton": {
|
||||
"background": "@key/background",
|
||||
"foreground": "@key/foreground"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -800,6 +800,384 @@ shall not be used in advertising or otherwise to promote the sale,
|
||||
use or other dealings in these Data Files or Software without prior
|
||||
written authorization of the copyright holder.
|
||||
</pre>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Dictionary Source 1: wordfreq data</h3>
|
||||
<span>Copyright (c) 2015 Luminoso Technologies, Inc.</span>
|
||||
<pre>
|
||||
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||
License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||
License"). To the extent this Public License may be interpreted as a
|
||||
contract, You are granted the Licensed Rights in consideration of Your
|
||||
acceptance of these terms and conditions, and the Licensor grants You
|
||||
such rights in consideration of benefits the Licensor receives from
|
||||
making the Licensed Material available under these terms and
|
||||
conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-SA Compatible License means a license listed at
|
||||
creativecommons.org/compatiblelicenses, approved by Creative
|
||||
Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name
|
||||
of a Creative Commons Public License. The License Elements of this
|
||||
Public License are Attribution and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
k. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
l. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
m. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. Additional offer from the Licensor -- Adapted Material.
|
||||
Every recipient of Adapted Material from You
|
||||
automatically receives an offer from the Licensor to
|
||||
exercise the Licensed Rights in the Adapted Material
|
||||
under the conditions of the Adapter's License You apply.
|
||||
|
||||
c. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share
|
||||
Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons
|
||||
license with the same License Elements, this version or
|
||||
later, or a BY-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the
|
||||
Adapter's License You apply. You may satisfy this condition
|
||||
in any reasonable manner based on the medium, means, and
|
||||
context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms
|
||||
or conditions on, or apply any Effective Technological
|
||||
Measures to, Adapted Material that restrict exercise of the
|
||||
rights granted under the Adapter's License You apply.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material,
|
||||
|
||||
including for purposes of Section 3(b); and
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
</pre>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
}
|
||||
|
||||
@@ -23,19 +23,26 @@ import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.inputmethodservice.ExtractEditText
|
||||
import android.inputmethodservice.InputMethodService
|
||||
import android.media.AudioManager
|
||||
import android.os.*
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.provider.Settings
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.*
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputConnection
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import 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.popup.PopupLayerView
|
||||
import dev.patrickgold.florisboard.ime.text.TextInputManager
|
||||
@@ -45,10 +52,11 @@ import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.settings.SettingsMainActivity
|
||||
import dev.patrickgold.florisboard.setup.SetupActivity
|
||||
import dev.patrickgold.florisboard.util.*
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* Variable which holds the current [FlorisBoard] instance. To get this instance from another
|
||||
@@ -67,15 +75,16 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
|
||||
val context: Context
|
||||
get() = inputWindowView?.context ?: this
|
||||
private var extractEditLayout: WeakReference<ViewGroup?> = WeakReference(null)
|
||||
var inputView: InputView? = null
|
||||
private set
|
||||
private var inputWindowView: InputWindowView? = null
|
||||
var popupLayerView: PopupLayerView? = null
|
||||
private set
|
||||
private var inputWindowView: InputWindowView? = null
|
||||
private var eventListeners: MutableList<WeakReference<EventListener?>?> = mutableListOf()
|
||||
private var eventListeners: CopyOnWriteArrayList<EventListener> = CopyOnWriteArrayList()
|
||||
|
||||
private var audioManager: AudioManager? = null
|
||||
private var imeManager:InputMethodManager? = null
|
||||
var imeManager:InputMethodManager? = null
|
||||
var clipboardManager: ClipboardManager? = null
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
private var vibrator: Vibrator? = null
|
||||
@@ -154,7 +163,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
/*if (BuildConfig.DEBUG) {
|
||||
StrictMode.setThreadPolicy(
|
||||
StrictMode.ThreadPolicy.Builder()
|
||||
.detectDiskReads()
|
||||
@@ -171,14 +180,14 @@ 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
|
||||
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
audioManager = getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
||||
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||
clipboardManager?.addPrimaryClipChangedListener(this)
|
||||
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
||||
prefs = PrefHelper.getDefaultInstance(this)
|
||||
prefs.initDefaultPreferences()
|
||||
prefs.sync()
|
||||
@@ -194,7 +203,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
|
||||
|
||||
super.onCreate()
|
||||
eventListeners.toList().forEach { it?.get()?.onCreate() }
|
||||
eventListeners.toList().forEach { it?.onCreate() }
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
@@ -203,17 +212,79 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
|
||||
baseContext.setTheme(currentThemeResId)
|
||||
|
||||
inputWindowView = layoutInflater.inflate(R.layout.florisboard, null) as InputWindowView
|
||||
popupLayerView = inputWindowView?.findViewById(R.id.popup_layer)
|
||||
inputWindowView = layoutInflater.inflate(R.layout.florisboard, null) as? InputWindowView
|
||||
inputWindowView?.isHapticFeedbackEnabled = true
|
||||
|
||||
eventListeners.toList().forEach { it?.get()?.onCreateInputView() }
|
||||
eventListeners.toList().forEach { it?.onCreateInputView() }
|
||||
|
||||
return inputWindowView
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the default candidates view.
|
||||
*/
|
||||
override fun onCreateCandidatesView(): View? {
|
||||
return null
|
||||
}
|
||||
|
||||
@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 onEvaluateFullscreenMode(): Boolean {
|
||||
return resources?.configuration?.let { config ->
|
||||
if (config.orientation != Configuration.ORIENTATION_LANDSCAPE) {
|
||||
false
|
||||
} else {
|
||||
when (prefs.keyboard.landscapeInputUiMode) {
|
||||
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeEditorInstance.imeOptions.flagNoFullscreen && !activeEditorInstance.imeOptions.flagNoExtractUi
|
||||
LandscapeInputUiMode.NEVER_SHOW -> false
|
||||
LandscapeInputUiMode.ALWAYS_SHOW -> true
|
||||
}
|
||||
}
|
||||
} ?: false
|
||||
}
|
||||
|
||||
override fun updateFullscreenMode() {
|
||||
super.updateFullscreenMode()
|
||||
updateSoftInputWindowLayoutParameters()
|
||||
}
|
||||
|
||||
override fun onUpdateExtractingVisibility(ei: EditorInfo?) {
|
||||
isExtractViewShown = !activeEditorInstance.isRawInputEditor && when (prefs.keyboard.landscapeInputUiMode) {
|
||||
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeEditorInstance.imeOptions.flagNoExtractUi
|
||||
LandscapeInputUiMode.NEVER_SHOW -> false
|
||||
LandscapeInputUiMode.ALWAYS_SHOW -> true
|
||||
}
|
||||
}
|
||||
|
||||
fun registerInputView(inputView: InputView) {
|
||||
Timber.i("registerInputView($inputView)")
|
||||
|
||||
window?.window?.findViewById<View>(android.R.id.content)?.let { content ->
|
||||
popupLayerView = PopupLayerView(content.context)
|
||||
if (content is ViewGroup) {
|
||||
content.addView(popupLayerView)
|
||||
}
|
||||
}
|
||||
this.inputView = inputView
|
||||
initializeOneHandedEnvironment()
|
||||
updateSoftInputWindowLayoutParameters()
|
||||
@@ -221,7 +292,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
themeManager.notifyCallbackReceivers()
|
||||
setActiveInput(R.id.text_input)
|
||||
|
||||
eventListeners.toList().forEach { it?.get()?.onRegisterInputView(inputView) }
|
||||
eventListeners.toList().forEach { it?.onRegisterInputView(inputView) }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -232,7 +303,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
florisboardInstance = null
|
||||
|
||||
eventListeners.toList().forEach { it?.get()?.onDestroy() }
|
||||
eventListeners.toList().forEach { it?.onDestroy() }
|
||||
eventListeners.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
@@ -252,7 +323,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
activeEditorInstance = EditorInstance.from(info, this)
|
||||
themeManager.updateRemoteColorValues(activeEditorInstance.packageName)
|
||||
eventListeners.toList().forEach {
|
||||
it?.get()?.onStartInputView(activeEditorInstance, restarting)
|
||||
it?.onStartInputView(activeEditorInstance, restarting)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,7 +335,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
|
||||
super.onFinishInputView(finishingInput)
|
||||
eventListeners.toList().forEach { it?.get()?.onFinishInputView(finishingInput) }
|
||||
eventListeners.toList().forEach { it?.onFinishInputView(finishingInput) }
|
||||
}
|
||||
|
||||
override fun onFinishInput() {
|
||||
@@ -290,14 +361,14 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
setActiveInput(R.id.text_input)
|
||||
|
||||
super.onWindowShown()
|
||||
eventListeners.toList().forEach { it?.get()?.onWindowShown() }
|
||||
eventListeners.toList().forEach { it?.onWindowShown() }
|
||||
}
|
||||
|
||||
override fun onWindowHidden() {
|
||||
Timber.i("onWindowHidden()")
|
||||
|
||||
super.onWindowHidden()
|
||||
eventListeners.toList().forEach { it?.get()?.onWindowHidden() }
|
||||
eventListeners.toList().forEach { it?.onWindowHidden() }
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
@@ -323,9 +394,10 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
)
|
||||
activeEditorInstance.onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd
|
||||
newSelStart, newSelEnd,
|
||||
candidatesStart, candidatesEnd
|
||||
)
|
||||
eventListeners.toList().forEach { it?.get()?.onUpdateSelection() }
|
||||
eventListeners.toList().forEach { it?.onUpdateSelection() }
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
@@ -375,7 +447,36 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
inputView?.oneHandedCtrlCloseStart?.imageTintList = it
|
||||
inputView?.oneHandedCtrlCloseEnd?.imageTintList = it
|
||||
}
|
||||
eventListeners.toList().forEach { it?.get()?.onApplyThemeAttributes() }
|
||||
inputView?.invalidate()
|
||||
|
||||
// Update ExtractTextView theme and attributes
|
||||
extractEditLayout.get()?.let { eel ->
|
||||
val p = resources.getDimension(R.dimen.landscapeInputUi_padding).toInt()
|
||||
eel.setPadding(p, p, 0, p)
|
||||
eel.setBackgroundColor(theme.getAttr(Theme.Attr.EXTRACT_EDIT_LAYOUT_BACKGROUND).toSolidColor().color)
|
||||
eel.findViewById<ExtractEditText>(android.R.id.inputExtractEditText)?.let { eet ->
|
||||
val p2 = resources.getDimension(R.dimen.landscapeInputUi_editText_padding).toInt()
|
||||
eet.setPadding(p2, p2, p2, p2)
|
||||
eet.background = ContextCompat.getDrawable(this, R.drawable.edit_text_background)?.also { d ->
|
||||
DrawableCompat.setTint(d, theme.getAttr(Theme.Attr.WINDOW_COLOR_PRIMARY).toSolidColor().color)
|
||||
}
|
||||
eet.setTextColor(theme.getAttr(Theme.Attr.EXTRACT_EDIT_LAYOUT_FOREGROUND).toSolidColor().color)
|
||||
eet.setHintTextColor(theme.getAttr(Theme.Attr.EXTRACT_EDIT_LAYOUT_FOREGROUND_ALT).toSolidColor().color)
|
||||
eet.highlightColor = theme.getAttr(Theme.Attr.WINDOW_COLOR_PRIMARY).toSolidColor().color
|
||||
}
|
||||
eel.findViewWithType(FrameLayout::class)?.let { fra ->
|
||||
fra.background = null
|
||||
}
|
||||
eel.findViewWithType(Button::class)?.let { btn ->
|
||||
btn.background = ContextCompat.getDrawable(this, R.drawable.shape_rect_rounded)?.also { d ->
|
||||
DrawableCompat.setTint(d, theme.getAttr(Theme.Attr.EXTRACT_ACTION_BUTTON_BACKGROUND).toSolidColor().color)
|
||||
}
|
||||
btn.setTextColor(theme.getAttr(Theme.Attr.EXTRACT_ACTION_BUTTON_FOREGROUND).toSolidColor().color)
|
||||
}
|
||||
eel.invalidate()
|
||||
}
|
||||
|
||||
eventListeners.toList().forEach { it?.onApplyThemeAttributes() }
|
||||
}
|
||||
|
||||
override fun onComputeInsets(outInsets: Insets?) {
|
||||
@@ -393,11 +494,6 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
outInsets?.visibleTopInsets = visibleTopY
|
||||
}
|
||||
|
||||
override fun updateFullscreenMode() {
|
||||
super.updateFullscreenMode()
|
||||
updateSoftInputWindowLayoutParameters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the layout params of the window and input view.
|
||||
*/
|
||||
@@ -425,7 +521,12 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
if (prefs.keyboard.vibrationEnabled) {
|
||||
var vibrationStrength = prefs.keyboard.vibrationStrength
|
||||
if (vibrationStrength == -1 && prefs.keyboard.vibrationEnabledSystem) {
|
||||
vibrationStrength = 36
|
||||
val hapticsPerformed =
|
||||
inputWindowView?.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
|
||||
if (hapticsPerformed == false) {
|
||||
vibrationStrength = 36
|
||||
}
|
||||
}
|
||||
if (vibrationStrength > 0) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -477,11 +578,11 @@ 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
|
||||
@@ -578,10 +679,10 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
updateOneHandedPanelVisibility()
|
||||
}
|
||||
|
||||
fun toggleOneHandedMode() {
|
||||
fun toggleOneHandedMode(isRight: Boolean) {
|
||||
when (prefs.keyboard.oneHandedMode) {
|
||||
"off" -> {
|
||||
prefs.keyboard.oneHandedMode = "end"
|
||||
prefs.keyboard.oneHandedMode = if (isRight) { "end" } else { "start" }
|
||||
}
|
||||
else -> {
|
||||
prefs.keyboard.oneHandedMode = "off"
|
||||
@@ -617,7 +718,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
|
||||
override fun onPrimaryClipChanged() {
|
||||
eventListeners.toList().forEach { it?.get()?.onPrimaryClipChanged() }
|
||||
eventListeners.toList().forEach { it?.onPrimaryClipChanged() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -627,7 +728,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
* @return True if the listener has been added successfully, false otherwise.
|
||||
*/
|
||||
fun addEventListener(listener: EventListener): Boolean {
|
||||
return eventListeners.add(WeakReference(listener))
|
||||
return eventListeners.add(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -640,12 +741,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
* value may also indicate that the [listener] was not added previously.
|
||||
*/
|
||||
fun removeEventListener(listener: EventListener): Boolean {
|
||||
eventListeners.toList().forEach {
|
||||
if (it?.get() == listener) {
|
||||
return eventListeners.remove(it)
|
||||
}
|
||||
}
|
||||
return false
|
||||
return eventListeners.remove(listener)
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
@@ -690,14 +786,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
|
||||
|
||||
@@ -20,11 +20,9 @@ import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
@@ -135,7 +133,11 @@ class InputView : LinearLayout {
|
||||
// Add bottom offset for curved screens here. As the desired heights have already been set,
|
||||
// adding a value to the height now will result in a bottom padding (aka offset).
|
||||
baseHeight += ViewLayoutUtils.convertDpToPixel(
|
||||
florisboard.prefs.keyboard.bottomOffset.toFloat(),
|
||||
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
florisboard.prefs.keyboard.bottomOffsetLandscape.toFloat()
|
||||
} else {
|
||||
florisboard.prefs.keyboard.bottomOffsetPortrait.toFloat()
|
||||
},
|
||||
context
|
||||
)
|
||||
|
||||
|
||||
@@ -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,15 +21,15 @@ 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.text.gestures.DistanceThreshold
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.VelocityThreshold
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
|
||||
import dev.patrickgold.florisboard.ime.text.key.SwitchKeyMode
|
||||
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.
|
||||
@@ -308,26 +308,34 @@ class PrefHelper(
|
||||
*/
|
||||
class Keyboard(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val BOTTOM_OFFSET = "keyboard__bottom_offset"
|
||||
const val BOTTOM_OFFSET_PORTRAIT = "keyboard__bottom_offset_portrait"
|
||||
const val BOTTOM_OFFSET_LANDSCAPE = "keyboard__bottom_offset_landscape"
|
||||
const val FONT_SIZE_MULTIPLIER_PORTRAIT = "keyboard__font_size_multiplier_portrait"
|
||||
const val FONT_SIZE_MULTIPLIER_LANDSCAPE = "keyboard__font_size_multiplier_landscape"
|
||||
const val HEIGHT_FACTOR = "keyboard__height_factor"
|
||||
const val HEIGHT_FACTOR_CUSTOM = "keyboard__height_factor_custom"
|
||||
const val HINTED_NUMBER_ROW_MODE = "keyboard__hinted_number_row_mode"
|
||||
const val HINTED_SYMBOLS_MODE = "keyboard__hinted_symbols_mode"
|
||||
const val KEY_SPACING_HORIZONTAL = "keyboard__key_spacing_horizontal"
|
||||
const val KEY_SPACING_VERTICAL = "keyboard__key_spacing_vertical"
|
||||
const val LANDSCAPE_INPUT_UI_MODE = "keyboard__landscape_input_ui_mode"
|
||||
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
|
||||
const val NUMBER_ROW = "keyboard__number_row"
|
||||
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
|
||||
const val POPUP_ENABLED = "keyboard__popup_enabled"
|
||||
const val SOUND_ENABLED = "keyboard__sound_enabled"
|
||||
const val SOUND_VOLUME = "keyboard__sound_volume"
|
||||
const val SWITCH_KEY_MODE = "keyboard__switch_key_mode"
|
||||
const val UTILITY_KEY_ACTION = "keyboard__utility_key_action"
|
||||
const val UTILITY_KEY_ENABLED = "keyboard__utility_key_enabled"
|
||||
const val VIBRATION_ENABLED = "keyboard__vibration_enabled"
|
||||
const val VIBRATION_STRENGTH = "keyboard__vibration_strength"
|
||||
}
|
||||
|
||||
var bottomOffset: Int = 0
|
||||
get() = prefHelper.getPref(BOTTOM_OFFSET, 0)
|
||||
var bottomOffsetPortrait: Int = 0
|
||||
get() = prefHelper.getPref(BOTTOM_OFFSET_PORTRAIT, 0)
|
||||
private set
|
||||
var bottomOffsetLandscape: Int = 0
|
||||
get() = prefHelper.getPref(BOTTOM_OFFSET_LANDSCAPE, 0)
|
||||
private set
|
||||
var fontSizeMultiplierPortrait: Int
|
||||
get() = prefHelper.getPref(FONT_SIZE_MULTIPLIER_PORTRAIT, 100)
|
||||
@@ -347,6 +355,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
|
||||
@@ -366,9 +383,12 @@ class PrefHelper(
|
||||
var soundVolume: Int = 0
|
||||
get() = prefHelper.getPref(SOUND_VOLUME, -1)
|
||||
private set
|
||||
var switchKeyMode: SwitchKeyMode
|
||||
get() = SwitchKeyMode.fromString(prefHelper.getPref(SWITCH_KEY_MODE, SwitchKeyMode.DYNAMIC_LANGUAGE_EMOJI.toString()))
|
||||
set(v) = prefHelper.setPref(SWITCH_KEY_MODE, v)
|
||||
var utilityKeyAction: UtilityKeyAction
|
||||
get() = UtilityKeyAction.fromString(prefHelper.getPref(UTILITY_KEY_ACTION, UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS.toString()))
|
||||
set(v) = prefHelper.setPref(UTILITY_KEY_ACTION, v)
|
||||
var utilityKeyEnabled: Boolean
|
||||
get() = prefHelper.getPref(UTILITY_KEY_ENABLED, true)
|
||||
set(v) = prefHelper.setPref(UTILITY_KEY_ENABLED, v)
|
||||
var vibrationEnabled: Boolean = false
|
||||
get() = prefHelper.getPref(VIBRATION_ENABLED, true)
|
||||
private set
|
||||
@@ -413,11 +433,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,69 @@
|
||||
/*
|
||||
* 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 com.github.michaelbull.result.*
|
||||
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>, Throwable> {
|
||||
dictionaryCache[ref.toString()]?.let {
|
||||
return Ok(it)
|
||||
}
|
||||
if (ref.path.endsWith(".flict")) {
|
||||
// Assume this is a Flictionary
|
||||
Flictionary.load(applicationContext, ref).onSuccess { flict ->
|
||||
dictionaryCache[ref.toString()] = flict
|
||||
return Ok(flict)
|
||||
}.onFailure { err ->
|
||||
Timber.i(err)
|
||||
return Err(err)
|
||||
}
|
||||
} else {
|
||||
return Err(Exception("Unable to determine supported type for given AssetRef!"))
|
||||
}
|
||||
return Err(Exception("If this message is ever thrown, something is completely broken..."))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
/*
|
||||
* 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 com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
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, Exception> {
|
||||
val buffer = ByteArray(5000) { 0 }
|
||||
val inputStream: InputStream
|
||||
if (assetRef.source == AssetSource.Assets) {
|
||||
inputStream = context.assets.open(assetRef.path)
|
||||
} else {
|
||||
return Err(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 Err(
|
||||
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 Err(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
freqSize = 1
|
||||
}
|
||||
else -> return Err(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 Err(
|
||||
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 Err(
|
||||
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 Err(
|
||||
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 Err(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Err(
|
||||
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 Err(
|
||||
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 Err(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size - n
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Err(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_CMD_END_ZERO_VALUE,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
pos += 1
|
||||
}
|
||||
else -> {
|
||||
inputStream.close()
|
||||
return Err(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.INVALID_CMD_BYTE_PROVIDED,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
inputStream.close()
|
||||
|
||||
if (ngramTreeStack.size != 0) {
|
||||
return Err(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF,
|
||||
address = pos, cmdByte = 0x00.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return Ok(
|
||||
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
|
||||
}
|
||||
@@ -19,13 +19,10 @@ package dev.patrickgold.florisboard.ime.extension
|
||||
import android.content.Context
|
||||
import com.github.michaelbull.result.*
|
||||
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
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,12 +43,11 @@ class MediaInputView : LinearLayout, FlorisBoard.EventListener,
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
florisboard?.addEventListener(this)
|
||||
}
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
florisboard?.addEventListener(this)
|
||||
themeManager.registerOnThemeUpdatedListener(this)
|
||||
tabLayout = findViewById(R.id.media_input_tabs)
|
||||
switchToTextInputButton = findViewById(R.id.media_input_switch_to_text_input_button)
|
||||
@@ -57,8 +56,9 @@ class MediaInputView : LinearLayout, FlorisBoard.EventListener,
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
florisboard?.removeEventListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.media.emoji
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Enum for emoji category.
|
||||
@@ -38,9 +38,8 @@ enum class EmojiCategory {
|
||||
}
|
||||
|
||||
companion object {
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun fromString(string: String): EmojiCategory {
|
||||
return valueOf(string.replace(" & ", "_").toUpperCase())
|
||||
return valueOf(string.replace(" & ", "_").toUpperCase(Locale.ENGLISH))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,13 +64,16 @@ class EmojiKeyView(
|
||||
triangleDrawable = ContextCompat.getDrawable(context, R.drawable.triangle_bottom_right)
|
||||
|
||||
text = data.getCodePointsAsString()
|
||||
|
||||
florisboard?.addEventListener(this)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
onApplyThemeAttributes()
|
||||
florisboard?.addEventListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
florisboard?.removeEventListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -101,11 +101,11 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener,
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||
})
|
||||
addView(tabLayout)
|
||||
florisboard?.addEventListener(this)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
florisboard?.addEventListener(this)
|
||||
themeManager.registerOnThemeUpdatedListener(this)
|
||||
mainScope.launch {
|
||||
layouts.await()
|
||||
@@ -116,8 +116,9 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener,
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
florisboard?.removeEventListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,13 +19,11 @@ package dev.patrickgold.florisboard.ime.media.emoji
|
||||
import android.content.Context
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import android.util.Log
|
||||
import androidx.core.graphics.PaintCompat
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.lang.Exception
|
||||
import java.util.*
|
||||
|
||||
private const val GROUP_IDENTIFIER = "# group: "
|
||||
@@ -71,6 +69,8 @@ private fun listStringToListInt(list: List<String>): List<Int> {
|
||||
return ret.toList()
|
||||
}
|
||||
|
||||
private var cachedEmojiLayoutMap: EmojiLayoutDataMap? = null
|
||||
|
||||
/**
|
||||
* Reads the emoji list at the given [path] and returns an parsed [EmojiLayoutDataMap]. If the
|
||||
* given file path does not exist, an empty [EmojiLayoutDataMap] is returned.
|
||||
@@ -84,6 +84,7 @@ private fun listStringToListInt(list: List<String>): List<Int> {
|
||||
fun parseRawEmojiSpecsFile(
|
||||
context: Context, path: String
|
||||
): EmojiLayoutDataMap {
|
||||
cachedEmojiLayoutMap?.let { return it }
|
||||
val layouts = EmojiLayoutDataMap(EmojiCategory::class.java)
|
||||
for (category in EmojiCategory.values()) {
|
||||
layouts[category] = mutableListOf()
|
||||
@@ -182,5 +183,6 @@ fun parseRawEmojiSpecsFile(
|
||||
}
|
||||
}
|
||||
}
|
||||
cachedEmojiLayoutMap = layouts
|
||||
return layouts
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
/**
|
||||
* Represents the root node to a n-gram tree.
|
||||
*/
|
||||
open class NgramTree(
|
||||
sameOrderChildren: MutableList<NgramNode> = mutableListOf(),
|
||||
higherOrderChildren: MutableList<NgramNode> = mutableListOf()
|
||||
) : NgramNode(0, '?', -1, sameOrderChildren, higherOrderChildren)
|
||||
|
||||
/**
|
||||
* A node of a n-gram tree, which holds the character it represents, the corresponding frequency,
|
||||
* a pre-computed string representing all parent characters and the current one as well as child
|
||||
* nodes, one for the same order n-gram nodes and one for the higher order n-gram nodes.
|
||||
*/
|
||||
open class NgramNode(
|
||||
val order: Int,
|
||||
val char: Char,
|
||||
val freq: Int,
|
||||
val sameOrderChildren: MutableList<NgramNode> = mutableListOf(),
|
||||
val higherOrderChildren: MutableList<NgramNode> = mutableListOf()
|
||||
) {
|
||||
companion object {
|
||||
const val FREQ_CHARACTER = -1
|
||||
const val FREQ_WORD_MIN = 0
|
||||
const val FREQ_WORD_MAX = 255
|
||||
const val FREQ_WORD_FILLER = -2
|
||||
const val FREQ_IS_POSSIBLY_OFFENSIVE = 0
|
||||
}
|
||||
|
||||
val isCharacter: Boolean
|
||||
get() = freq == FREQ_CHARACTER
|
||||
|
||||
val isWord: Boolean
|
||||
get() = freq in FREQ_WORD_MIN..FREQ_WORD_MAX
|
||||
|
||||
val isWordFiller: Boolean
|
||||
get() = freq == FREQ_WORD_FILLER
|
||||
|
||||
val isPossiblyOffensive: Boolean
|
||||
get() = freq == FREQ_IS_POSSIBLY_OFFENSIVE
|
||||
|
||||
fun findWord(word: String): NgramNode? {
|
||||
var currentNode = this
|
||||
for ((pos, char) in word.withIndex()) {
|
||||
val childNode = if (pos == 0) {
|
||||
currentNode.higherOrderChildren.find { it.char == char }
|
||||
} else {
|
||||
currentNode.sameOrderChildren.find { it.char == char }
|
||||
}
|
||||
if (childNode != null) {
|
||||
currentNode = childNode
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return if (currentNode.isWord || currentNode.isWordFiller) {
|
||||
currentNode
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function allows to search for a given [input] word with a given [maxEditDistance] and
|
||||
* adds all matches in the trie to the [list].
|
||||
*/
|
||||
fun listSimilarWords(
|
||||
input: String,
|
||||
list: StagedSuggestionList<String, Int>,
|
||||
word: StringBuilder,
|
||||
allowPossiblyOffensive: Boolean,
|
||||
maxEditDistance: Int,
|
||||
deletionCost: Int = 0,
|
||||
insertionCost: Int = 0,
|
||||
substitutionCost: Int = 0,
|
||||
pos: Int = -1
|
||||
) {
|
||||
if (pos > -1) {
|
||||
word.append(char)
|
||||
}
|
||||
val costSum = deletionCost + insertionCost + substitutionCost
|
||||
if (pos > -1 && (pos + 1 == input.length) && isWord && ((isPossiblyOffensive && allowPossiblyOffensive)
|
||||
|| !isPossiblyOffensive)) {
|
||||
// Using shift right instead of divide by 2^(costSum) as it is mathematically the
|
||||
// same but faster.
|
||||
if (list.canAdd(freq shr costSum)) {
|
||||
list.add(word.toString(), freq shr costSum)
|
||||
}
|
||||
}
|
||||
if (pos <= -1) {
|
||||
for (childNode in higherOrderChildren) {
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance, 0, 0, 0, 0
|
||||
)
|
||||
}
|
||||
} else if (maxEditDistance == costSum) {
|
||||
if (pos + 1 < input.length) {
|
||||
sameOrderChildren.find { it.char == input[pos + 1] }?.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost, substitutionCost, pos + 1
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Delete
|
||||
if (pos + 2 < input.length) {
|
||||
sameOrderChildren.find { it.char == input[pos + 2] }?.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost + 1, insertionCost, substitutionCost, pos + 2
|
||||
)
|
||||
}
|
||||
for (childNode in sameOrderChildren) {
|
||||
if (pos + 1 < input.length && childNode.char == input[pos + 1]) {
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost, substitutionCost, pos + 1
|
||||
)
|
||||
} else {
|
||||
// Insert
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost + 1, substitutionCost, pos
|
||||
)
|
||||
if (pos + 1 < input.length) {
|
||||
// Substitute
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost, substitutionCost + 1, pos + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pos > -1) {
|
||||
word.deleteAt(word.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun listAllSameOrderWords(list: StagedSuggestionList<String, Int>, word: StringBuilder, allowPossiblyOffensive: Boolean) {
|
||||
word.append(char)
|
||||
if (isWord && ((isPossiblyOffensive && allowPossiblyOffensive) || !isPossiblyOffensive)) {
|
||||
if (list.canAdd(freq)) {
|
||||
list.add(word.toString(), freq)
|
||||
}
|
||||
}
|
||||
for (childNode in sameOrderChildren) {
|
||||
childNode.listAllSameOrderWords(list, word, allowPossiblyOffensive)
|
||||
}
|
||||
word.deleteAt(word.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
open class FlorisLanguageModel(
|
||||
initTreeObj: NgramTree? = null
|
||||
) : LanguageModel<String, Int> {
|
||||
protected val ngramTree: NgramTree = initTreeObj ?: NgramTree()
|
||||
|
||||
override fun getNgram(vararg tokens: String): Ngram<String, Int> {
|
||||
val ngramOut = getNgramOrNull(*tokens)
|
||||
if (ngramOut != null) {
|
||||
return ngramOut
|
||||
} else {
|
||||
throw NullPointerException("No n-gram found matching the given tokens: $tokens")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNgram(ngram: Ngram<String, Int>): Ngram<String, Int> {
|
||||
val ngramOut = getNgramOrNull(ngram)
|
||||
if (ngramOut != null) {
|
||||
return ngramOut
|
||||
} else {
|
||||
throw NullPointerException("No n-gram found matching the given ngram: $ngram")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNgramOrNull(vararg tokens: String): Ngram<String, Int>? {
|
||||
var currentNode: NgramNode = ngramTree
|
||||
for (token in tokens) {
|
||||
val childNode = currentNode.findWord(token)
|
||||
if (childNode != null) {
|
||||
currentNode = childNode
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return Ngram(tokens.toList().map { Token(it) }, currentNode.freq)
|
||||
}
|
||||
|
||||
override fun getNgramOrNull(ngram: Ngram<String, Int>): Ngram<String, Int>? {
|
||||
return getNgramOrNull(*ngram.tokens.toStringList().toTypedArray())
|
||||
}
|
||||
|
||||
override fun hasNgram(ngram: Ngram<String, Int>, doMatchFreq: Boolean): Boolean {
|
||||
val result = getNgramOrNull(ngram)
|
||||
return if (result != null) {
|
||||
if (doMatchFreq) {
|
||||
ngram.freq == result.freq
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun matchAllNgrams(
|
||||
ngram: Ngram<String, Int>,
|
||||
maxEditDistance: Int,
|
||||
maxTokenCount: Int,
|
||||
allowPossiblyOffensive: Boolean
|
||||
): List<WeightedToken<String, Int>> {
|
||||
val ngramList = mutableListOf<WeightedToken<String, Int>>()
|
||||
var currentNode: NgramNode = ngramTree
|
||||
for ((t, token) in ngram.tokens.withIndex()) {
|
||||
val word = token.data
|
||||
if (t + 1 >= ngram.tokens.size) {
|
||||
if (word.isNotEmpty()) {
|
||||
// The last word is not complete, so find all possible words and sort
|
||||
val splitWord = mutableListOf<Char>()
|
||||
var splitNode: NgramNode? = currentNode
|
||||
for ((pos, char) in word.withIndex()) {
|
||||
val node = if (pos == 0) {
|
||||
splitNode?.higherOrderChildren?.find { it.char == char }
|
||||
} else {
|
||||
splitNode?.sameOrderChildren?.find { it.char == char }
|
||||
}
|
||||
splitWord.add(char)
|
||||
splitNode = node
|
||||
if (node == null) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (splitNode != null) {
|
||||
// Input thus far is valid
|
||||
val wordNodes = StagedSuggestionList<String, Int>(maxTokenCount)
|
||||
val strBuilder = StringBuilder().append(word.substring(0, word.length - 1))
|
||||
splitNode.listAllSameOrderWords(wordNodes, strBuilder, allowPossiblyOffensive)
|
||||
ngramList.addAll(wordNodes)
|
||||
}
|
||||
if (ngramList.size < maxTokenCount) {
|
||||
val wordNodes = StagedSuggestionList<String, Int>(maxTokenCount)
|
||||
val strBuilder = StringBuilder()
|
||||
currentNode.listSimilarWords(word, wordNodes, strBuilder, allowPossiblyOffensive, maxEditDistance)
|
||||
ngramList.addAll(wordNodes)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val node = currentNode.findWord(word)
|
||||
if (node == null) {
|
||||
return ngramList
|
||||
} else {
|
||||
currentNode = node
|
||||
}
|
||||
}
|
||||
}
|
||||
return ngramList
|
||||
}
|
||||
|
||||
fun toFlorisMutableLanguageModel(): FlorisMutableLanguageModel = FlorisMutableLanguageModel(ngramTree)
|
||||
}
|
||||
|
||||
open class FlorisMutableLanguageModel(
|
||||
initTreeObj: NgramTree? = null
|
||||
) : MutableLanguageModel<String, Int>, FlorisLanguageModel(initTreeObj) {
|
||||
override fun deleteNgram(ngram: Ngram<String, Int>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun insertNgram(ngram: Ngram<String, Int>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun updateNgram(ngram: Ngram<String, Int>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
fun toFlorisLanguageModel(): FlorisLanguageModel = FlorisLanguageModel(ngramTree)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
/**
|
||||
* Abstract interface for a language model. Can house any n-grams with a minimum order of one.
|
||||
*/
|
||||
interface LanguageModel<T : Any, F : Comparable<F>> {
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [tokens]. Throws a NPE if no match could be found.
|
||||
*/
|
||||
@Throws(NullPointerException::class)
|
||||
fun getNgram(vararg tokens: T): Ngram<T, F>
|
||||
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [ngram], whereas the frequency is ignored while
|
||||
* searching. Throws a NPE if no match could be found.
|
||||
*/
|
||||
@Throws(NullPointerException::class)
|
||||
fun getNgram(ngram: Ngram<T, F>): Ngram<T, F>
|
||||
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [tokens]. Returns null if no match could be found.
|
||||
*/
|
||||
fun getNgramOrNull(vararg tokens: T): Ngram<T, F>?
|
||||
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [ngram], whereas the frequency is ignored while
|
||||
* searching. Returns null if no match could be found.
|
||||
*/
|
||||
fun getNgramOrNull(ngram: Ngram<T, F>): Ngram<T, F>?
|
||||
|
||||
/**
|
||||
* Checks if a given [ngram] exists within this model. If [doMatchFreq] is set to true, the
|
||||
* frequency is also matched.
|
||||
*/
|
||||
fun hasNgram(ngram: Ngram<T, F>, doMatchFreq: Boolean = false): Boolean
|
||||
|
||||
/**
|
||||
* Matches all n-grams which match the given [ngram], whereas the last item in the n-gram is
|
||||
* is used to search for predictions.
|
||||
*/
|
||||
fun matchAllNgrams(
|
||||
ngram: Ngram<T, F>,
|
||||
maxEditDistance: Int,
|
||||
maxTokenCount: Int,
|
||||
allowPossiblyOffensive: Boolean
|
||||
): List<WeightedToken<T, F>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutable version of [LanguageModel].
|
||||
*/
|
||||
interface MutableLanguageModel<T : Any, F : Comparable<F>> : LanguageModel<T, F> {
|
||||
fun deleteNgram(ngram: Ngram<T, F>)
|
||||
|
||||
fun insertNgram(ngram: Ngram<T, F>)
|
||||
|
||||
fun updateNgram(ngram: Ngram<T, F>)
|
||||
}
|
||||
129
app/src/main/java/dev/patrickgold/florisboard/ime/nlp/Ngram.kt
Normal file
129
app/src/main/java/dev/patrickgold/florisboard/ime/nlp/Ngram.kt
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
/**
|
||||
* Abstract interface representing a n-gram of tokens. Each n-gram instance can be assigned a
|
||||
* unique frequency [freq].
|
||||
*/
|
||||
open class Ngram<T : Any, F : Comparable<F>>(_tokens: List<Token<T>>, _freq: F) {
|
||||
companion object {
|
||||
/** Constant order value for unigrams. */
|
||||
const val ORDER_UNIGRAM: Int = 1
|
||||
|
||||
/** Constant order value for bigrams. */
|
||||
const val ORDER_BIGRAM: Int = 2
|
||||
|
||||
/** Constant order value for trigrams. */
|
||||
const val ORDER_TRIGRAM: Int = 3
|
||||
}
|
||||
|
||||
init {
|
||||
if (_tokens.size < ORDER_UNIGRAM) {
|
||||
throw Exception("A n-gram must contain at least 1 token!")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of tokens for this n-gram. The length of this list is guaranteed to be matching
|
||||
* [order].
|
||||
*/
|
||||
val tokens: List<Token<T>> = _tokens
|
||||
|
||||
/**
|
||||
* The frequency value of this n-gram.
|
||||
*/
|
||||
val freq: F = _freq
|
||||
|
||||
/**
|
||||
* The order of this n-gram (1, 2, 3, ...).
|
||||
*/
|
||||
val order: Int
|
||||
get() = tokens.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract interface representing a token used in [Ngram].
|
||||
*/
|
||||
open class Token<T : Any>(_data: T) {
|
||||
/**
|
||||
* The data of this token.
|
||||
*/
|
||||
val data: T = _data
|
||||
|
||||
override fun toString(): String {
|
||||
return "Token(\"$data\")"
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return data.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Token<*>
|
||||
|
||||
if (data != other.data) return false
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as [Token] but allows to add a frequency value [freq].
|
||||
*/
|
||||
open class WeightedToken<T : Any, F : Comparable<F>>(_data: T, _freq: F) : Token<T>(_data) {
|
||||
/**
|
||||
* The frequency of this weighed token.
|
||||
*/
|
||||
val freq: F = _freq
|
||||
|
||||
override fun toString(): String {
|
||||
return "WeightedToken(\"$data\", $freq)"
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return data.hashCode() + 31 * freq.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as WeightedToken<*, *>
|
||||
|
||||
if (data != other.data || freq != other.freq) return false
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of tokens carrying [CharSequence] data to a list of [CharSequence].
|
||||
*/
|
||||
fun List<Token<CharSequence>>.toCharSequenceList(): List<CharSequence> {
|
||||
return this.map { it.data }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of tokens carrying [String] data to a list of [String].
|
||||
*/
|
||||
fun List<Token<String>>.toStringList(): List<String> {
|
||||
return this.map { it.data }
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
class StagedSuggestionList<T : Any, F : Comparable<F>>(
|
||||
private val maxSize: Int
|
||||
) : Collection<WeightedToken<T, F>> {
|
||||
private val internalArray: Array<WeightedToken<T, F>?> = Array(maxSize) { null }
|
||||
private var internalSize: Int = 0
|
||||
|
||||
override val size: Int
|
||||
get() = internalSize
|
||||
|
||||
fun add(token: T, freq: F): Boolean {
|
||||
if (internalSize < maxSize) {
|
||||
internalArray[internalSize++] = WeightedToken(token, freq)
|
||||
internalArray.sortByDescending { it?.freq }
|
||||
return true
|
||||
} else {
|
||||
if (internalArray.last()!!.freq < freq) {
|
||||
internalArray[internalArray.lastIndex] = WeightedToken(token, freq)
|
||||
internalArray.sortByDescending { it?.freq }
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun canAdd(freq: F): Boolean {
|
||||
return internalSize < maxSize || internalArray.last()!!.freq < freq
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
for (n in internalArray.indices) {
|
||||
internalArray[n] = null
|
||||
}
|
||||
internalSize = 0
|
||||
}
|
||||
|
||||
override fun contains(element: WeightedToken<T, F>): Boolean = internalArray.contains(element)
|
||||
|
||||
override fun containsAll(elements: Collection<WeightedToken<T, F>>): Boolean {
|
||||
elements.forEach { if (!contains(it)) return false }
|
||||
return true
|
||||
}
|
||||
|
||||
@Throws(IndexOutOfBoundsException::class)
|
||||
operator fun get(index: Int): WeightedToken<T, F> {
|
||||
val element = getOrNull(index)
|
||||
if (element == null) {
|
||||
throw IndexOutOfBoundsException("The specified index $index is not within the bounds of this list!")
|
||||
} else {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrNull(index: Int): WeightedToken<T, F>? {
|
||||
return internalArray.getOrNull(index)
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean = internalSize <= 0
|
||||
|
||||
override fun iterator(): Iterator<WeightedToken<T, F>> {
|
||||
return StagedIterator(this)
|
||||
}
|
||||
|
||||
class StagedIterator<T : Any, F : Comparable<F>> internal constructor (
|
||||
private val stagedSuggestionList: StagedSuggestionList<T, F>
|
||||
) : Iterator<WeightedToken<T, F>> {
|
||||
var index = 0
|
||||
|
||||
override fun next(): WeightedToken<T, F> = stagedSuggestionList[index++]
|
||||
|
||||
override fun hasNext(): Boolean = stagedSuggestionList.getOrNull(index) != null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
class TextProcessor {
|
||||
data class Word(
|
||||
val word: String,
|
||||
val isPossiblyOffensive: Boolean = false
|
||||
)
|
||||
}
|
||||
@@ -35,8 +35,12 @@ import kotlin.math.min
|
||||
class PopupExtendedView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
|
||||
private val activeBackgroundDrawable: PaintDrawable = PaintDrawable()
|
||||
private var backgroundDrawable: PaintDrawable = PaintDrawable()
|
||||
private val activeBackgroundDrawable: PaintDrawable = PaintDrawable().apply {
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
}
|
||||
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
}
|
||||
private val labelPaint: Paint = Paint().apply {
|
||||
alpha = 255
|
||||
color = 0
|
||||
@@ -80,6 +84,7 @@ class PopupExtendedView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
init {
|
||||
visibility = GONE
|
||||
background = backgroundDrawable
|
||||
elevation = ViewLayoutUtils.convertDpToPixel(4.0f, context)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
@@ -88,20 +93,17 @@ class PopupExtendedView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
activeBackgroundDrawable.apply {
|
||||
setTint(theme.getAttr(Theme.Attr.POPUP_BACKGROUND_ACTIVE).toSolidColor().color)
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
}
|
||||
backgroundDrawable.apply {
|
||||
setTint(theme.getAttr(Theme.Attr.POPUP_BACKGROUND).toSolidColor().color)
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
}
|
||||
elevation = ViewLayoutUtils.convertDpToPixel(4.0f, context)
|
||||
labelPaint.color = theme.getAttr(Theme.Attr.POPUP_FOREGROUND).toSolidColor().color
|
||||
tldPaint.color = theme.getAttr(Theme.Attr.POPUP_FOREGROUND).toSolidColor().color
|
||||
if (isShowing) {
|
||||
|
||||
@@ -20,12 +20,14 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
|
||||
/**
|
||||
* Basic helper view class which acts as a non-interactive layer view, which sits above the whole
|
||||
* input UI. Automatically rejects any touch events and passes it through to the View below.
|
||||
*/
|
||||
class PopupLayerView : FrameLayout {
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
@@ -34,6 +36,9 @@ class PopupLayerView : FrameLayout {
|
||||
background = null
|
||||
isClickable = false
|
||||
isFocusable = false
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
package dev.patrickgold.florisboard.ime.popup
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.view.*
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat.getDrawable
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
|
||||
@@ -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
|
||||
|
||||
@@ -34,7 +34,9 @@ import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
class PopupView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
|
||||
private var backgroundDrawable: PaintDrawable = PaintDrawable()
|
||||
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
}
|
||||
private val labelPaint: Paint = Paint().apply {
|
||||
alpha = 255
|
||||
color = 0
|
||||
@@ -86,7 +88,6 @@ class PopupView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
backgroundDrawable.apply {
|
||||
setTint(theme.getAttr(Theme.Attr.POPUP_BACKGROUND).toSolidColor().color)
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
}
|
||||
elevation = ViewLayoutUtils.convertDpToPixel(4.0f, context)
|
||||
threeDotsDrawable?.apply {
|
||||
|
||||
@@ -18,16 +18,21 @@ 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 com.github.michaelbull.result.getOr
|
||||
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.*
|
||||
@@ -66,6 +71,8 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
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
|
||||
|
||||
var keyVariation: KeyVariation = KeyVariation.NORMAL
|
||||
val layoutManager = LayoutManager(florisboard)
|
||||
@@ -124,7 +131,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)
|
||||
@@ -181,6 +187,12 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
smartbarView?.setEventListener(this)
|
||||
}
|
||||
|
||||
fun unregisterSmartbarView(view: SmartbarView) {
|
||||
if (smartbarView == view) {
|
||||
smartbarView = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all coroutines and cleans up.
|
||||
*/
|
||||
@@ -296,6 +308,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
|
||||
override fun onSubtypeChanged(newSubtype: Subtype) {
|
||||
launch {
|
||||
if (activeEditorInstance.isComposingEnabled) {
|
||||
withContext(Dispatchers.IO) {
|
||||
dictionaryManager.loadDictionary(AssetRef(AssetSource.Assets,"ime/dict/en.flict")).let {
|
||||
activeDictionary = it.getOr(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
val keyboardView = keyboardViews[KeyboardMode.CHARACTERS]
|
||||
keyboardView?.computedLayout = layoutManager.fetchComputedLayoutAsync(KeyboardMode.CHARACTERS, newSubtype, florisboard.prefs).await()
|
||||
keyboardView?.updateVisibility()
|
||||
@@ -314,6 +333,35 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
updateCapsState()
|
||||
smartbarView?.updateSmartbarState()
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.i("current word: ${activeEditorInstance.cachedInput.currentWord.text}")
|
||||
}
|
||||
if (activeEditorInstance.isComposingEnabled) {
|
||||
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() {
|
||||
@@ -346,8 +394,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
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.MOVE_CURSOR_START_OF_LINE -> handleArrow(KeyCode.MOVE_START_OF_LINE)
|
||||
SwipeAction.MOVE_CURSOR_END_OF_LINE -> handleArrow(KeyCode.MOVE_END_OF_LINE)
|
||||
SwipeAction.MOVE_CURSOR_START_OF_PAGE -> handleArrow(KeyCode.MOVE_START_OF_PAGE)
|
||||
SwipeAction.MOVE_CURSOR_END_OF_PAGE -> handleArrow(KeyCode.MOVE_END_OF_PAGE)
|
||||
SwipeAction.SHIFT -> handleShift()
|
||||
SwipeAction.SHOW_INPUT_METHOD_PICKER -> sendKeyPress(
|
||||
KeyData(type = KeyType.FUNCTION, code = KeyCode.SHOW_INPUT_METHOD_PICKER)
|
||||
@@ -360,6 +410,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()
|
||||
}
|
||||
@@ -375,7 +429,7 @@ 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()
|
||||
return
|
||||
@@ -449,9 +503,9 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* FlorisBoard internal or system-wide.
|
||||
*/
|
||||
private fun handleLanguageSwitch() {
|
||||
when (florisboard.prefs.keyboard.switchKeyMode) {
|
||||
SwitchKeyMode.DYNAMIC_LANGUAGE_EMOJI,
|
||||
SwitchKeyMode.ALWAYS_LANGUAGE_INTERNAL -> florisboard.switchToNextSubtype()
|
||||
when (florisboard.prefs.keyboard.utilityKeyAction) {
|
||||
UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS,
|
||||
UtilityKeyAction.SWITCH_LANGUAGE -> florisboard.switchToNextSubtype()
|
||||
else -> florisboard.switchToNextKeyboard()
|
||||
}
|
||||
}
|
||||
@@ -474,6 +528,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}, 300)
|
||||
}
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -503,9 +558,9 @@ 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 {
|
||||
private fun handleArrow(code: Int) = activeEditorInstance.run {
|
||||
val selectionStartMin = 0
|
||||
val selectionEndMax = cachedText.length
|
||||
val selectionEndMax = cachedInput.expectedMaxLength
|
||||
if (selection.isSelectionMode && isManualSelectionMode) {
|
||||
// Text is selected and it is manual selection -> Expand selection depending on started
|
||||
// direction.
|
||||
@@ -513,37 +568,37 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
if (isManualSelectionModeLeft) {
|
||||
setSelection(
|
||||
selection.updateAndNotify(
|
||||
(selection.start - 1).coerceAtLeast(selectionStartMin),
|
||||
selection.end
|
||||
)
|
||||
} else {
|
||||
setSelection(selection.start, selection.end - 1)
|
||||
selection.updateAndNotify(selection.start, selection.end - 1)
|
||||
}
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
if (isManualSelectionModeRight) {
|
||||
setSelection(
|
||||
selection.updateAndNotify(
|
||||
selection.start,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
} else {
|
||||
setSelection(selection.start + 1, selection.end)
|
||||
selection.updateAndNotify(selection.start + 1, selection.end)
|
||||
}
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
KeyCode.MOVE_START_OF_LINE -> {
|
||||
if (isManualSelectionModeLeft) {
|
||||
setSelection(selectionStartMin, selection.end)
|
||||
selection.updateAndNotify(selectionStartMin, selection.end)
|
||||
} else {
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
selection.updateAndNotify(selectionStartMin, selection.start)
|
||||
}
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
KeyCode.MOVE_END_OF_LINE -> {
|
||||
if (isManualSelectionModeRight) {
|
||||
setSelection(selection.start, selectionEndMax)
|
||||
selection.updateAndNotify(selection.start, selectionEndMax)
|
||||
} else {
|
||||
setSelection(selection.end, selectionEndMax)
|
||||
selection.updateAndNotify(selection.end, selectionEndMax)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -553,20 +608,20 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
setSelection(selection.start, selection.end - 1)
|
||||
selection.updateAndNotify(selection.start, selection.end - 1)
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
setSelection(
|
||||
selection.updateAndNotify(
|
||||
selection.start,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
KeyCode.MOVE_START_OF_LINE -> {
|
||||
selection.updateAndNotify(selectionStartMin, selection.start)
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
setSelection(selection.start, selectionEndMax)
|
||||
KeyCode.MOVE_END_OF_LINE -> {
|
||||
selection.updateAndNotify(selection.start, selectionEndMax)
|
||||
}
|
||||
}
|
||||
} else if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
@@ -575,7 +630,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
setSelection(
|
||||
selection.updateAndNotify(
|
||||
(selection.start - 1).coerceAtLeast(selectionStartMin),
|
||||
selection.start
|
||||
)
|
||||
@@ -583,7 +638,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
setSelection(
|
||||
selection.updateAndNotify(
|
||||
selection.end,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
@@ -591,13 +646,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
isManualSelectionModeRight = true
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
KeyCode.MOVE_START_OF_LINE -> {
|
||||
selection.updateAndNotify(selectionStartMin, selection.start)
|
||||
isManualSelectionModeLeft = true
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
setSelection(selection.end, selectionEndMax)
|
||||
KeyCode.MOVE_END_OF_LINE -> {
|
||||
selection.updateAndNotify(selection.end, selectionEndMax)
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = true
|
||||
}
|
||||
@@ -609,10 +664,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
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.MOVE_START_OF_PAGE -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_UP)
|
||||
KeyCode.MOVE_END_OF_PAGE -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
KeyCode.MOVE_START_OF_LINE -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_LEFT)
|
||||
KeyCode.MOVE_END_OF_LINE -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_RIGHT)
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -621,9 +679,9 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
private fun handleClipboardSelect() = activeEditorInstance.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
if (isManualSelectionMode && isManualSelectionModeLeft) {
|
||||
setSelection(selection.start, selection.start)
|
||||
selection.updateAndNotify(selection.start, selection.start)
|
||||
} else {
|
||||
setSelection(selection.end, selection.end)
|
||||
selection.updateAndNotify(selection.end, selection.end)
|
||||
}
|
||||
isManualSelectionMode = false
|
||||
} else {
|
||||
@@ -633,13 +691,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.CLIPBOARD_SELECT_ALL] event.
|
||||
*/
|
||||
private fun handleClipboardSelectAll() {
|
||||
activeEditorInstance.setSelection(0, activeEditorInstance.cachedText.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -653,8 +704,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
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 -> handleArrow(keyData.code)
|
||||
KeyCode.CLIPBOARD_CUT -> activeEditorInstance.performClipboardCut()
|
||||
KeyCode.CLIPBOARD_COPY -> activeEditorInstance.performClipboardCopy()
|
||||
KeyCode.CLIPBOARD_PASTE -> {
|
||||
@@ -662,7 +715,7 @@ 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()
|
||||
@@ -674,14 +727,11 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
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.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)
|
||||
@@ -712,19 +762,16 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
KeyType.CHARACTER, KeyType.NUMERIC -> when (keyData.code) {
|
||||
KeyCode.SPACE -> handleSpace()
|
||||
KeyCode.URI_COMPONENT_TLD -> {
|
||||
val tld = when (caps) {
|
||||
true -> keyData.label.toUpperCase(Locale.getDefault())
|
||||
false -> keyData.label.toLowerCase(Locale.getDefault())
|
||||
}
|
||||
val tld = keyData.label.toLowerCase(Locale.ENGLISH)
|
||||
activeEditorInstance.commitText(tld)
|
||||
}
|
||||
else -> {
|
||||
hasCapsRecentlyChanged = false
|
||||
hasSpaceRecentlyPressed = false
|
||||
var text = keyData.code.toChar().toString()
|
||||
text = when (caps) {
|
||||
true -> text.toUpperCase(Locale.getDefault())
|
||||
false -> text.toLowerCase(Locale.getDefault())
|
||||
text = when (caps && activeKeyboardMode == KeyboardMode.CHARACTERS) {
|
||||
true -> text.toUpperCase(florisboard.activeSubtype.locale)
|
||||
false -> text
|
||||
}
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
|
||||
@@ -80,8 +80,8 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
|
||||
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.move_start_of_line -> KeyCode.MOVE_START_OF_LINE
|
||||
R.id.move_end_of_line -> KeyCode.MOVE_END_OF_LINE
|
||||
R.id.select -> KeyCode.CLIPBOARD_SELECT
|
||||
R.id.select_all -> KeyCode.CLIPBOARD_SELECT_ALL
|
||||
else -> 0
|
||||
|
||||
@@ -45,12 +45,11 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
florisboard?.addEventListener(this)
|
||||
}
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
florisboard?.addEventListener(this)
|
||||
themeManager.registerOnThemeUpdatedListener(this)
|
||||
|
||||
arrowUpKey = findViewById(R.id.arrow_up)
|
||||
@@ -63,8 +62,9 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
florisboard?.removeEventListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
|
||||
@@ -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,36 +64,61 @@ abstract class SwipeGesture {
|
||||
* @property listener The listener to report detected swipes to.
|
||||
*/
|
||||
class Detector(private val context: Context, private val listener: Listener) {
|
||||
private val eventList: MutableList<MotionEvent> = mutableListOf()
|
||||
private var indexFirst: Int = 0
|
||||
private var indexLastMoveRecognized: Int = 0
|
||||
private var firstMotionEvent: MotionEvent? = null
|
||||
private var lastMotionEvent: MotionEvent? = null
|
||||
private var absUnitCountX: Int = 0
|
||||
private var absUnitCountY: Int = 0
|
||||
private var thresholdWidth: Double = numericValue(context, DistanceThreshold.NORMAL)
|
||||
private var unitWidth: Double = thresholdWidth / 4.0
|
||||
|
||||
var distanceThreshold: DistanceThreshold = DistanceThreshold.NORMAL
|
||||
set(value) {
|
||||
field = value
|
||||
thresholdWidth = numericValue(context, value)
|
||||
unitWidth = thresholdWidth / 4.0
|
||||
}
|
||||
var velocityThreshold: VelocityThreshold = VelocityThreshold.NORMAL
|
||||
|
||||
fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
/**
|
||||
* Method which evaluates if a given [event] is a gesture.
|
||||
*
|
||||
* @param event The MotionEvent which should be checked for a gesture.
|
||||
* @param alwaysTriggerOnMove Set to true if the moving detection algorithm should always
|
||||
* trigger, regardless of the distance from the previous event. Defaults to false.
|
||||
* @return True if the given [event] is a gesture, false otherwise.
|
||||
*/
|
||||
fun onTouchEvent(event: MotionEvent, alwaysTriggerOnMove: Boolean = false): Boolean {
|
||||
try {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN,
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
clearEventList()
|
||||
eventList.add(MotionEvent.obtainNoHistory(event))
|
||||
resetState()
|
||||
firstMotionEvent = MotionEvent.obtainNoHistory(event)
|
||||
lastMotionEvent = firstMotionEvent
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
eventList.add(MotionEvent.obtainNoHistory(event))
|
||||
val firstEvent = eventList[indexFirst]
|
||||
val lastEvent = eventList[indexLastMoveRecognized]
|
||||
val diffX = event.x - lastEvent.x
|
||||
val diffY = event.y - lastEvent.y
|
||||
val distanceThresholdNV = numericValue(context, distanceThreshold) / 4.0f
|
||||
return if (abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) {
|
||||
indexLastMoveRecognized = eventList.size - 1
|
||||
val direction = detectDirection(diffX.toDouble(), diffY.toDouble())
|
||||
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,
|
||||
diffX = event.x - firstEvent.x,
|
||||
diffY = event.y - firstEvent.y
|
||||
absUnitCountX,
|
||||
absUnitCountY,
|
||||
relUnitCountX,
|
||||
relUnitCountY
|
||||
))
|
||||
} else {
|
||||
false
|
||||
@@ -100,32 +126,36 @@ abstract class SwipeGesture {
|
||||
}
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_POINTER_UP -> {
|
||||
val firstEvent = eventList[indexFirst]
|
||||
val diffX = event.x - firstEvent.x
|
||||
val diffY = event.y - firstEvent.y
|
||||
val distanceThresholdNV = numericValue(context, distanceThreshold)
|
||||
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()*/
|
||||
clearEventList()
|
||||
// return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
|
||||
return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV)) {
|
||||
val direction = detectDirection(diffX.toDouble(), diffY.toDouble())
|
||||
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,
|
||||
diffX = diffX,
|
||||
diffY = diffY
|
||||
absUnitCountX,
|
||||
absUnitCountY,
|
||||
absUnitCountX,
|
||||
absUnitCountY
|
||||
))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
resetState()
|
||||
return ret
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
clearEventList()
|
||||
resetState()
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
@@ -175,22 +205,45 @@ abstract class SwipeGesture {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up and clears the event list.
|
||||
* Resets the state.
|
||||
*/
|
||||
private fun clearEventList() {
|
||||
for (event in eventList) {
|
||||
event.recycle()
|
||||
}
|
||||
eventList.clear()
|
||||
indexFirst = 0
|
||||
indexLastMoveRecognized = 0
|
||||
private fun resetState() {
|
||||
firstMotionEvent = null
|
||||
lastMotionEvent = null
|
||||
absUnitCountX = 0
|
||||
absUnitCountY = 0
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
/**
|
||||
* An interface which provides an abstract callback function, which will be called for any
|
||||
* detected swipe event.
|
||||
*/
|
||||
fun interface Listener {
|
||||
fun onSwipe(event: Event): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class which describes a single gesture event.
|
||||
*/
|
||||
data class Event(
|
||||
/** The direction of the swipe. */
|
||||
val direction: Direction,
|
||||
/** The type of the swipe. */
|
||||
val type: Type,
|
||||
/** The unit count on the x-axis, measured from the first event (ACTION_DOWN). */
|
||||
val absUnitCountX: Int,
|
||||
/** The unit count on the y-axis, measured from the first event (ACTION_DOWN). */
|
||||
val absUnitCountY: Int,
|
||||
/** The unit count on the x-axis, measured from the last event (ACTION_MOVE). */
|
||||
val relUnitCountX: Int,
|
||||
/** The unit count on the y-axis, measured from the last event (ACTION_MOVE). */
|
||||
val relUnitCountY: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* ENum which defines the direction of the detected swipe.
|
||||
*/
|
||||
enum class Direction {
|
||||
UP_LEFT,
|
||||
UP,
|
||||
@@ -202,13 +255,9 @@ abstract class SwipeGesture {
|
||||
LEFT,
|
||||
}
|
||||
|
||||
data class Event(
|
||||
val direction: Direction,
|
||||
val type: Type,
|
||||
val diffX: Float,
|
||||
val diffY: Float
|
||||
)
|
||||
|
||||
/**
|
||||
* Enum which defines the type of the gesture.
|
||||
*/
|
||||
enum class Type {
|
||||
TOUCH_UP,
|
||||
TOUCH_MOVE;
|
||||
|
||||
@@ -40,8 +40,10 @@ 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
|
||||
@@ -83,7 +85,8 @@ 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 KESHIDA = 1600
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.squareup.moshi.FromJson
|
||||
import java.util.*
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.squareup.moshi.FromJson
|
||||
import java.util.*
|
||||
|
||||
enum class KeyVariation {
|
||||
ALL,
|
||||
@@ -27,9 +27,8 @@ enum class KeyVariation {
|
||||
URI;
|
||||
|
||||
companion object {
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun fromString(string: String): KeyVariation {
|
||||
return valueOf(string.toUpperCase())
|
||||
return valueOf(string.toUpperCase(Locale.ENGLISH))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ 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.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
@@ -42,6 +43,7 @@ import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeValue
|
||||
import dev.patrickgold.florisboard.util.*
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* View class for managing the rendering and the events of a single keyboard key.
|
||||
@@ -54,13 +56,16 @@ import java.util.*
|
||||
@SuppressLint("ViewConstructor")
|
||||
class KeyView(
|
||||
private val keyboardView: KeyboardView,
|
||||
val data: FlorisKeyData
|
||||
val data: FlorisKeyData,
|
||||
private val florisboard: FlorisBoard?
|
||||
) : View(keyboardView.context), SwipeGesture.Listener, ThemeManager.OnThemeUpdatedListener {
|
||||
private var isKeyPressed: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
updateKeyPressedBackground()
|
||||
}
|
||||
private var initSelectionStart: Int = 0
|
||||
private var initSelectionEnd: Int = 0
|
||||
private var hasTriggeredGestureMove: Boolean = false
|
||||
private var keyHintMode: KeyHintMode = KeyHintMode.DISABLED
|
||||
private val longKeyPressHandler: Handler = Handler(context.mainLooper)
|
||||
@@ -95,12 +100,11 @@ class KeyView(
|
||||
isFakeBoldText = false
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = resources.getDimension(R.dimen.key_textHintSize)
|
||||
typeface = Typeface.DEFAULT
|
||||
typeface = Typeface.MONOSPACE
|
||||
}
|
||||
private val tempRect: Rect = Rect()
|
||||
private var themeValueCache: ThemeValueCache = ThemeValueCache()
|
||||
|
||||
var florisboard: FlorisBoard? = null
|
||||
val swipeGestureDetector = SwipeGesture.Detector(context, this)
|
||||
var touchHitBox: Rect = Rect(-1, -1, -1, -1)
|
||||
|
||||
@@ -108,11 +112,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,
|
||||
@@ -162,20 +177,29 @@ class KeyView(
|
||||
* Creates a label text from the given [keyData].
|
||||
*
|
||||
* @param keyData Optional. The key data to generate the label from. Defaults to [data].
|
||||
* @return The generated label.
|
||||
* @param caps If the generated text should be uppercase (true) or in lowercase (false).
|
||||
* Defaults to FlorisBoard's TextInputManager's caps state or false. Ignored when the passed
|
||||
* [keyData] is a TLD, in which case always the lower case variant is returned.
|
||||
* @param subtype The subtype for which this label should be created. Defaults to
|
||||
* [Subtype.DEFAULT]. Ignored when the passed [keyData] is a TLD.
|
||||
* @return The generated label ready for usage in the front-end UI.
|
||||
*/
|
||||
fun getComputedLetter(keyData: KeyData = data): String {
|
||||
if (keyData.code == KeyCode.URI_COMPONENT_TLD) {
|
||||
return when (florisboard?.textInputManager?.caps) {
|
||||
true -> keyData.label.toUpperCase(Locale.getDefault())
|
||||
else -> keyData.label.toLowerCase(Locale.getDefault())
|
||||
fun getComputedLetter(
|
||||
keyData: KeyData = data,
|
||||
caps: Boolean = florisboard?.textInputManager?.caps ?: false && florisboard?.textInputManager?.getActiveKeyboardMode() == KeyboardMode.CHARACTERS,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
val label = (keyData.code.toChar()).toString()
|
||||
return when {
|
||||
florisboard?.textInputManager?.caps ?: false -> label.toUpperCase(Locale.getDefault())
|
||||
else -> label
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,7 +228,10 @@ class KeyView(
|
||||
*/
|
||||
fun onFlorisTouchEvent(event: MotionEvent?): Boolean {
|
||||
if (event == null || !isEnabled) return false
|
||||
if (swipeGestureDetector.onTouchEvent(event)) {
|
||||
val alwaysTriggerOnMove = (hasTriggeredGestureMove
|
||||
&& (data.code == KeyCode.DELETE && prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_CHARACTERS_PRECISELY
|
||||
|| data.code == KeyCode.SPACE))
|
||||
if (swipeGestureDetector.onTouchEvent(event, alwaysTriggerOnMove)) {
|
||||
isKeyPressed = false
|
||||
longKeyPressHandler.cancelAll()
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
@@ -236,7 +263,7 @@ class KeyView(
|
||||
KeyCode.ARROW_RIGHT,
|
||||
KeyCode.ARROW_UP,
|
||||
KeyCode.DELETE -> {
|
||||
repeatedKeyPressHandler.postAtScheduledRate(delayMillis, 25) {
|
||||
repeatedKeyPressHandler.postAtScheduledRate((delayMillis * 2.0f).toLong(), 25) {
|
||||
if (isKeyPressed) {
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
} else {
|
||||
@@ -246,6 +273,8 @@ class KeyView(
|
||||
}
|
||||
}
|
||||
if (data.code == KeyCode.SPACE) {
|
||||
initSelectionStart = florisboard?.activeEditorInstance?.selection?.start ?: 0
|
||||
initSelectionEnd = florisboard?.activeEditorInstance?.selection?.end ?: 0
|
||||
longKeyPressHandler.postDelayed((delayMillis * 2.5f).toLong()) {
|
||||
when (prefs.gestures.spaceBarLongPress) {
|
||||
SwipeAction.NO_ACTION,
|
||||
@@ -290,7 +319,6 @@ class KeyView(
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
if (data.code != KeyCode.SHIFT) {
|
||||
if (hasTriggeredGestureMove && data.code == KeyCode.DELETE) {
|
||||
hasTriggeredGestureMove = false
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
deleteBackwards()
|
||||
@@ -306,6 +334,7 @@ class KeyView(
|
||||
keyboardView.popupManager.hide()
|
||||
}
|
||||
}
|
||||
hasTriggeredGestureMove = false
|
||||
isKeyPressed = false
|
||||
}
|
||||
else -> return false
|
||||
@@ -318,14 +347,14 @@ class KeyView(
|
||||
* defined in the prefs.
|
||||
*/
|
||||
override fun onSwipe(event: SwipeGesture.Event): Boolean {
|
||||
val florisboard = florisboard ?: return false
|
||||
return when (data.code) {
|
||||
KeyCode.DELETE -> when (event.type) {
|
||||
SwipeGesture.Type.TOUCH_MOVE -> when (prefs.gestures.deleteKeySwipeLeft) {
|
||||
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
|
||||
val charWidth = SwipeGesture.numericValue(context, swipeGestureDetector.distanceThreshold) / 4.0f
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
setSelection(
|
||||
(selection.end - (event.diffX.times(-1) / charWidth).toInt()).coerceIn(0, selection.end),
|
||||
florisboard.activeEditorInstance.apply {
|
||||
selection.updateAndNotify(
|
||||
(selection.end + event.absUnitCountX).coerceIn(0, selection.end),
|
||||
selection.end
|
||||
)
|
||||
}
|
||||
@@ -335,7 +364,7 @@ class KeyView(
|
||||
}
|
||||
SwipeAction.DELETE_WORDS_PRECISELY -> when (event.direction) {
|
||||
SwipeGesture.Direction.LEFT -> {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
florisboard.activeEditorInstance.apply {
|
||||
leftAppendWordToSelection()
|
||||
}
|
||||
hasTriggeredGestureMove = true
|
||||
@@ -343,7 +372,7 @@ class KeyView(
|
||||
true
|
||||
}
|
||||
SwipeGesture.Direction.RIGHT -> {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
florisboard.activeEditorInstance.apply {
|
||||
leftPopWordFromSelection()
|
||||
}
|
||||
shouldBlockNextKeyCode = true
|
||||
@@ -358,17 +387,46 @@ class KeyView(
|
||||
KeyCode.SPACE -> when (event.type) {
|
||||
SwipeGesture.Type.TOUCH_MOVE -> when (event.direction) {
|
||||
SwipeGesture.Direction.UP -> {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeUp)
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
if (event.absUnitCountY.times(-1) >= 6) {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeUp)
|
||||
hasTriggeredGestureMove = true
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
SwipeGesture.Direction.LEFT -> {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
|
||||
if (prefs.gestures.spaceBarSwipeLeft == SwipeAction.MOVE_CURSOR_LEFT) {
|
||||
if (!florisboard.activeEditorInstance.isRawInputEditor) {
|
||||
val s = (initSelectionEnd + event.absUnitCountX).coerceIn(0, florisboard.activeEditorInstance.cachedInput.expectedMaxLength)
|
||||
florisboard.activeEditorInstance.selection.updateAndNotify(s, s)
|
||||
} else {
|
||||
for (n in 0 until abs(event.relUnitCountX)) {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
|
||||
}
|
||||
hasTriggeredGestureMove = true
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
SwipeGesture.Direction.RIGHT -> {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
|
||||
if (prefs.gestures.spaceBarSwipeRight == SwipeAction.MOVE_CURSOR_RIGHT) {
|
||||
if (!florisboard.activeEditorInstance.isRawInputEditor) {
|
||||
val s = (initSelectionEnd + event.absUnitCountX).coerceIn(0, florisboard.activeEditorInstance.cachedInput.expectedMaxLength)
|
||||
florisboard.activeEditorInstance.selection.updateAndNotify(s, s)
|
||||
} else {
|
||||
for (n in 0 until abs(event.relUnitCountX)) {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
|
||||
}
|
||||
hasTriggeredGestureMove = true
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
@@ -387,6 +445,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,
|
||||
@@ -473,10 +550,9 @@ class KeyView(
|
||||
private fun updateEnabledState() {
|
||||
isEnabled = when (data.code) {
|
||||
KeyCode.CLIPBOARD_COPY,
|
||||
KeyCode.CLIPBOARD_CUT -> {
|
||||
florisboard?.activeEditorInstance?.selection?.isSelectionMode == true &&
|
||||
florisboard?.activeEditorInstance?.isRawInputEditor == false
|
||||
}
|
||||
KeyCode.CLIPBOARD_CUT -> (florisboard != null
|
||||
&& florisboard.activeEditorInstance.selection.isSelectionMode
|
||||
&& !florisboard.activeEditorInstance.isRawInputEditor)
|
||||
KeyCode.CLIPBOARD_PASTE -> florisboard?.clipboardManager?.hasPrimaryClip() == true
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> {
|
||||
florisboard?.activeEditorInstance?.isRawInputEditor == false
|
||||
@@ -562,8 +638,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) {
|
||||
@@ -589,12 +674,16 @@ class KeyView(
|
||||
when (data.code) {
|
||||
KeyCode.SWITCH_TO_TEXT_CONTEXT,
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
|
||||
visibility = when (prefs.keyboard.switchKeyMode) {
|
||||
SwitchKeyMode.ALWAYS_LANGUAGE_INTERNAL,
|
||||
SwitchKeyMode.ALWAYS_LANGUAGE_SYSTEM,
|
||||
SwitchKeyMode.NEVER_SHOW -> GONE
|
||||
SwitchKeyMode.ALWAYS_EMOJI -> VISIBLE
|
||||
SwitchKeyMode.DYNAMIC_LANGUAGE_EMOJI ->
|
||||
val tempUtilityKeyAction = when {
|
||||
prefs.keyboard.utilityKeyEnabled -> prefs.keyboard.utilityKeyAction
|
||||
else -> UtilityKeyAction.DISABLED
|
||||
}
|
||||
visibility = when (tempUtilityKeyAction) {
|
||||
UtilityKeyAction.DISABLED,
|
||||
UtilityKeyAction.SWITCH_LANGUAGE,
|
||||
UtilityKeyAction.SWITCH_KEYBOARD_APP -> GONE
|
||||
UtilityKeyAction.SWITCH_TO_EMOJIS -> VISIBLE
|
||||
UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS ->
|
||||
if (florisboard?.shouldShowLanguageSwitch() == true) {
|
||||
GONE
|
||||
} else {
|
||||
@@ -603,12 +692,16 @@ class KeyView(
|
||||
}
|
||||
}
|
||||
KeyCode.LANGUAGE_SWITCH -> {
|
||||
visibility = when (prefs.keyboard.switchKeyMode) {
|
||||
SwitchKeyMode.ALWAYS_EMOJI,
|
||||
SwitchKeyMode.NEVER_SHOW -> GONE
|
||||
SwitchKeyMode.ALWAYS_LANGUAGE_INTERNAL,
|
||||
SwitchKeyMode.ALWAYS_LANGUAGE_SYSTEM -> VISIBLE
|
||||
SwitchKeyMode.DYNAMIC_LANGUAGE_EMOJI ->
|
||||
val tempUtilityKeyAction = when {
|
||||
prefs.keyboard.utilityKeyEnabled -> prefs.keyboard.utilityKeyAction
|
||||
else -> UtilityKeyAction.DISABLED
|
||||
}
|
||||
visibility = when (tempUtilityKeyAction) {
|
||||
UtilityKeyAction.DISABLED,
|
||||
UtilityKeyAction.SWITCH_TO_EMOJIS -> GONE
|
||||
UtilityKeyAction.SWITCH_LANGUAGE,
|
||||
UtilityKeyAction.SWITCH_KEYBOARD_APP -> VISIBLE
|
||||
UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS ->
|
||||
if (florisboard?.shouldShowLanguageSwitch() == true) {
|
||||
VISIBLE
|
||||
} else {
|
||||
@@ -658,13 +751,9 @@ class KeyView(
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the key label / drawable.
|
||||
* Computes the labels and drawables needed to draw the key.
|
||||
*/
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
canvas ?: return
|
||||
|
||||
private fun computeLabelsAndDrawables() {
|
||||
if (data.type == KeyType.CHARACTER && data.code != KeyCode.SPACE
|
||||
&& data.code != KeyCode.HALF_SPACE && data.code != KeyCode.KESHIDA || data.type == KeyType.NUMERIC
|
||||
) {
|
||||
@@ -772,6 +861,17 @@ class KeyView(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the key label / drawable.
|
||||
*/
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
canvas ?: return
|
||||
|
||||
computeLabelsAndDrawables()
|
||||
|
||||
// Draw drawable
|
||||
val drawable = drawable
|
||||
@@ -810,8 +910,8 @@ class KeyView(
|
||||
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...
|
||||
@@ -867,15 +967,15 @@ class KeyView(
|
||||
if (hintedLabel != null) {
|
||||
setTextSizeFor(
|
||||
hintedLabelPaint,
|
||||
desiredWidth * 1.0f / 6.0f,
|
||||
desiredHeight * 1.0f / 6.0f,
|
||||
desiredWidth * 1.0f / 5.0f,
|
||||
desiredHeight * 1.0f / 5.0f,
|
||||
// Note: taking a "X" here because it is one of the biggest letters and
|
||||
// the keys must have the same base character for calculation, else
|
||||
// they will all look different and weird...
|
||||
"X"
|
||||
)
|
||||
hintedLabelPaint.color = labelPaint.color
|
||||
hintedLabelPaint.alpha = 120
|
||||
hintedLabelPaint.alpha = 170
|
||||
val centerX = measuredWidth * 5.0f / 6.0f
|
||||
val centerY = measuredHeight * 1.0f / 6.0f + (hintedLabelPaint.textSize - hintedLabelPaint.descent()) / 2
|
||||
canvas.drawText(hintedLabel, centerX, centerY, hintedLabelPaint)
|
||||
|
||||
@@ -19,17 +19,17 @@ package dev.patrickgold.florisboard.ime.text.key
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Enum for declaring the switch key modes.
|
||||
* Enum for declaring the utility key actions.
|
||||
*/
|
||||
enum class SwitchKeyMode {
|
||||
ALWAYS_EMOJI,
|
||||
ALWAYS_LANGUAGE_INTERNAL,
|
||||
ALWAYS_LANGUAGE_SYSTEM,
|
||||
DYNAMIC_LANGUAGE_EMOJI,
|
||||
NEVER_SHOW;
|
||||
enum class UtilityKeyAction {
|
||||
SWITCH_TO_EMOJIS,
|
||||
SWITCH_LANGUAGE,
|
||||
SWITCH_KEYBOARD_APP,
|
||||
DYNAMIC_SWITCH_LANGUAGE_EMOJIS,
|
||||
DISABLED;
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String): SwitchKeyMode {
|
||||
fun fromString(string: String): UtilityKeyAction {
|
||||
return valueOf(string.toUpperCase(Locale.ENGLISH))
|
||||
}
|
||||
}
|
||||
@@ -23,17 +23,25 @@ import com.google.android.flexbox.FlexDirection
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
|
||||
/**
|
||||
* This class' sole purpose is to manage the layout within a row of [KeyboardView]. No logic is
|
||||
* handled in this class.
|
||||
*/
|
||||
class KeyboardRowView(context: Context) : FlexboxLayout(context) {
|
||||
class KeyboardRowView(context: Context, val keyboardView: KeyboardView) : FlexboxLayout(context) {
|
||||
init {
|
||||
val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
val keyMarginH = if (keyboardView.isSmartbarKeyboardView){
|
||||
resources.getDimension(R.dimen.key_marginH).toInt()
|
||||
}else{
|
||||
ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
|
||||
}
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
|
||||
setMargins(
|
||||
resources.getDimension(R.dimen.keyboard_row_marginH).toInt(), 0,
|
||||
resources.getDimension(R.dimen.keyboard_row_marginH).toInt(), 0
|
||||
keyMarginH, 0,
|
||||
keyMarginH, 0
|
||||
)
|
||||
}
|
||||
flexDirection = FlexDirection.ROW
|
||||
@@ -49,4 +57,19 @@ class KeyboardRowView(context: Context) : FlexboxLayout(context) {
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
val keyMarginH = if (keyboardView.isSmartbarKeyboardView){
|
||||
resources.getDimension(R.dimen.key_marginH).toInt()
|
||||
}else{
|
||||
ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
|
||||
}
|
||||
(layoutParams as MarginLayoutParams).setMargins(
|
||||
keyMarginH, 0,
|
||||
keyMarginH, 0
|
||||
)
|
||||
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +30,13 @@ import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
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.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
|
||||
|
||||
/**
|
||||
@@ -84,13 +85,9 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
florisboard?.addEventListener(this)
|
||||
onWindowShown()
|
||||
if (isLoadingPlaceholderKeyboard) {
|
||||
computedLayout = ComputedLayoutData.PRE_GENERATED_LOADING_KEYBOARD
|
||||
/*for ((i, row) in children.withIndex()) {
|
||||
row.alpha = (i + 1) * 0.25f
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,10 +98,9 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
destroyLayout()
|
||||
val computedLayout = computedLayout ?: return
|
||||
for (row in computedLayout.arrangement) {
|
||||
val rowView = KeyboardRowView(context)
|
||||
val rowView = KeyboardRowView(context, this)
|
||||
for (key in row) {
|
||||
val keyView = KeyView(this, key)
|
||||
keyView.florisboard = florisboard
|
||||
val keyView = KeyView(this, key, florisboard)
|
||||
rowView.addView(keyView)
|
||||
}
|
||||
addView(rowView)
|
||||
@@ -126,6 +122,7 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
florisboard?.addEventListener(this)
|
||||
if (!isPreviewMode) {
|
||||
themeManager.registerOnThemeUpdatedListener(this)
|
||||
}
|
||||
@@ -135,11 +132,12 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
* Dismisses all shown key popups when keyboard is detached from window.
|
||||
*/
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
popupManager.dismissAllPopups()
|
||||
if (!isPreviewMode) {
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
}
|
||||
florisboard?.removeEventListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
@@ -334,8 +332,16 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
* 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,7 +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
|
||||
@@ -164,7 +163,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) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package dev.patrickgold.florisboard.ime.text.layout
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.squareup.moshi.FromJson
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Defines the type of the layout.
|
||||
@@ -19,15 +19,13 @@ enum class LayoutType {
|
||||
SYMBOLS2,
|
||||
SYMBOLS2_MOD;
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
override fun toString(): String {
|
||||
return super.toString().replace("_", "/").toLowerCase()
|
||||
return super.toString().replace("_", "/").toLowerCase(Locale.ENGLISH)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun fromString(string: String): LayoutType {
|
||||
return valueOf(string.replace("/", "_").toUpperCase())
|
||||
return valueOf(string.replace("/", "_").toUpperCase(Locale.ENGLISH))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@ class SmartbarQuickActionButton : androidx.appcompat.widget.AppCompatImageButton
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,7 @@ import dev.patrickgold.florisboard.util.setDrawableTintColor2
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.math.roundToInt
|
||||
@@ -52,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
|
||||
@@ -113,13 +115,29 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
Subtype.DEFAULT,
|
||||
prefs
|
||||
).await()
|
||||
launch(Dispatchers.Main) {
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.clipboardCursorRow.computedLayout = layout
|
||||
binding.clipboardCursorRow.updateVisibility()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -133,7 +151,7 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
Subtype.DEFAULT,
|
||||
prefs
|
||||
).await()
|
||||
launch(Dispatchers.Main) {
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.numberRow.computedLayout = layout
|
||||
binding.numberRow.updateVisibility()
|
||||
}
|
||||
@@ -146,7 +164,9 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
|
||||
for (quickAction in binding.quickActions.children) {
|
||||
if (quickAction is SmartbarQuickActionButton) {
|
||||
quickAction.setOnClickListener { eventListener?.get()?.onSmartbarQuickActionPressed(quickAction.id) }
|
||||
quickAction.id.let { quickActionId ->
|
||||
quickAction.setOnClickListener { eventListener?.get()?.onSmartbarQuickActionPressed(quickActionId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,8 +189,10 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
eventListener = null
|
||||
florisboard?.textInputManager?.unregisterSmartbarView(this)
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,6 +323,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()
|
||||
@@ -330,6 +374,7 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
for (view in candidateViewList) {
|
||||
view.setTextColor(theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND).toSolidColor().color)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun setEventListener(listener: EventListener) {
|
||||
@@ -342,7 +387,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) {}
|
||||
|
||||
@@ -80,20 +80,20 @@ open class Theme(
|
||||
*/
|
||||
fun getUiAttrNameString(context: Context, attrName: String): String {
|
||||
val strId = when (attrName) {
|
||||
"background" -> R.string.settings__theme__attr_background
|
||||
"backgroundActive" -> R.string.settings__theme__attr_backgroundActive
|
||||
"backgroundPressed" -> R.string.settings__theme__attr_backgroundPressed
|
||||
"foreground" -> R.string.settings__theme__attr_foreground
|
||||
"foregroundAlt" -> R.string.settings__theme__attr_foregroundAlt
|
||||
"foregroundPressed" -> R.string.settings__theme__attr_foregroundPressed
|
||||
"showBorder" -> R.string.settings__theme__attr_showBorder
|
||||
"colorPrimary" -> R.string.settings__theme__attr_colorPrimary
|
||||
"colorPrimaryDark" -> R.string.settings__theme__attr_colorPrimaryDark
|
||||
"colorAccent" -> R.string.settings__theme__attr_colorAccent
|
||||
"navigationBarColor" -> R.string.settings__theme__attr_navBarColor
|
||||
"navigationBarLight" -> R.string.settings__theme__attr_navBarLight
|
||||
"semiTransparentColor" -> R.string.settings__theme__attr_semiTransparentColor
|
||||
"textColor" -> R.string.settings__theme__attr_textColor
|
||||
"background" -> R.string.settings__theme__attr_background
|
||||
"backgroundActive" -> R.string.settings__theme__attr_backgroundActive
|
||||
"backgroundPressed" -> R.string.settings__theme__attr_backgroundPressed
|
||||
"foreground" -> R.string.settings__theme__attr_foreground
|
||||
"foregroundAlt" -> R.string.settings__theme__attr_foregroundAlt
|
||||
"foregroundPressed" -> R.string.settings__theme__attr_foregroundPressed
|
||||
"showBorder" -> R.string.settings__theme__attr_showBorder
|
||||
"colorPrimary" -> R.string.settings__theme__attr_colorPrimary
|
||||
"colorPrimaryDark" -> R.string.settings__theme__attr_colorPrimaryDark
|
||||
"colorAccent" -> R.string.settings__theme__attr_colorAccent
|
||||
"navigationBarColor" -> R.string.settings__theme__attr_navBarColor
|
||||
"navigationBarLight" -> R.string.settings__theme__attr_navBarLight
|
||||
"semiTransparentColor" -> R.string.settings__theme__attr_semiTransparentColor
|
||||
"textColor" -> R.string.settings__theme__attr_textColor
|
||||
else -> null
|
||||
}
|
||||
return if (strId != null) {
|
||||
@@ -121,15 +121,17 @@ open class Theme(
|
||||
)
|
||||
else -> {
|
||||
val strId = when (groupName) {
|
||||
"window" -> R.string.settings__theme__group_window
|
||||
"keyboard" -> R.string.settings__theme__group_keyboard
|
||||
"key" -> R.string.settings__theme__group_key
|
||||
"media" -> R.string.settings__theme__group_media
|
||||
"oneHanded" -> R.string.settings__theme__group_oneHanded
|
||||
"popup" -> R.string.settings__theme__group_popup
|
||||
"privateMode" -> R.string.settings__theme__group_privateMode
|
||||
"smartbar" -> R.string.settings__theme__group_smartbar
|
||||
"smartbarButton" -> R.string.settings__theme__group_smartbarButton
|
||||
"window" -> R.string.settings__theme__group_window
|
||||
"keyboard" -> R.string.settings__theme__group_keyboard
|
||||
"key" -> R.string.settings__theme__group_key
|
||||
"media" -> R.string.settings__theme__group_media
|
||||
"oneHanded" -> R.string.settings__theme__group_oneHanded
|
||||
"popup" -> R.string.settings__theme__group_popup
|
||||
"privateMode" -> R.string.settings__theme__group_privateMode
|
||||
"smartbar" -> R.string.settings__theme__group_smartbar
|
||||
"smartbarButton" -> R.string.settings__theme__group_smartbarButton
|
||||
"extractEditLayout" -> R.string.settings__theme__group_extractEditLayout
|
||||
"extractActionButton" -> R.string.settings__theme__group_extractActionButton
|
||||
else -> null
|
||||
}
|
||||
if (strId != null) {
|
||||
@@ -210,6 +212,15 @@ open class Theme(
|
||||
Pair("smartbarButton", mapOf(
|
||||
Pair("background", ThemeValue.fromString("@key/background")),
|
||||
Pair("foreground", ThemeValue.fromString("@key/foreground")),
|
||||
)),
|
||||
Pair("extractEditLayout", mapOf(
|
||||
Pair("background", bgColor),
|
||||
Pair("foreground", ThemeValue.fromString("@window/textColor")),
|
||||
Pair("foregroundAlt", ThemeValue.fromString("#73FFFFFF")),
|
||||
)),
|
||||
Pair("extractActionButton", mapOf(
|
||||
Pair("background", ThemeValue.fromString("@smartbarButton/background")),
|
||||
Pair("foreground", ThemeValue.fromString("@smartbarButton/foreground")),
|
||||
))
|
||||
)
|
||||
)
|
||||
@@ -306,7 +317,7 @@ open class Theme(
|
||||
getAttrOrNull(ref.copy(group = "${ref.group}::$s2"))?.let { return it }
|
||||
}
|
||||
getAttrOrNull(ref)?.let { return it }
|
||||
return ThemeValue.SolidColor(0)
|
||||
return BASE_THEME.getAttrOrNull(ref) ?: ThemeValue.SolidColor(0)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -367,6 +378,13 @@ open class Theme(
|
||||
|
||||
val SMARTBAR_BUTTON_BACKGROUND = ThemeValue.Reference("smartbarButton", "background")
|
||||
val SMARTBAR_BUTTON_FOREGROUND = ThemeValue.Reference("smartbarButton", "foreground")
|
||||
|
||||
val EXTRACT_EDIT_LAYOUT_BACKGROUND = ThemeValue.Reference("extractEditLayout", "background")
|
||||
val EXTRACT_EDIT_LAYOUT_FOREGROUND = ThemeValue.Reference("extractEditLayout", "foreground")
|
||||
val EXTRACT_EDIT_LAYOUT_FOREGROUND_ALT = ThemeValue.Reference("extractEditLayout", "foregroundAlt")
|
||||
|
||||
val EXTRACT_ACTION_BUTTON_BACKGROUND = ThemeValue.Reference("extractActionButton", "background")
|
||||
val EXTRACT_ACTION_BUTTON_FOREGROUND = ThemeValue.Reference("extractActionButton", "foreground")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ class ThemeManager private constructor(
|
||||
ThemeValue.SolidColor(a.getColor(1, defColor))
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
remoteColorPrimary
|
||||
}
|
||||
}
|
||||
remoteColorPrimaryVariant = when {
|
||||
@@ -184,7 +184,7 @@ class ThemeManager private constructor(
|
||||
ThemeValue.SolidColor(a.getColor(4, defColor))
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
remoteColorPrimaryVariant
|
||||
}
|
||||
}
|
||||
remoteColorSecondary = when {
|
||||
@@ -198,7 +198,7 @@ class ThemeManager private constructor(
|
||||
ThemeValue.SolidColor(a.getColor(7, defColor))
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
remoteColorSecondary
|
||||
}
|
||||
}
|
||||
a.recycle()
|
||||
@@ -216,6 +216,7 @@ class ThemeManager private constructor(
|
||||
remoteColorSecondary?.let {
|
||||
remoteColorSecondary = ThemeValue.SolidColor(it.color or Color.BLACK)
|
||||
}
|
||||
notifyCallbackReceivers()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -261,19 +261,24 @@ class ThemeEditorActivity : AppCompatActivity() {
|
||||
val sortedMap = baseMap.toList().sortedBy { (_, v) -> v }.toMap().toMutableMap()
|
||||
val groupIds = sortedMap.keys.toMutableList()
|
||||
val groupNames = sortedMap.values.toMutableList()
|
||||
if (groupNames.contains("keyboard")) {
|
||||
val windowGroupId = groupIds[groupNames.indexOf("keyboard")]
|
||||
groupIds.remove(windowGroupId)
|
||||
groupNames.remove("keyboard")
|
||||
groupIds.add(0, windowGroupId)
|
||||
groupNames.add(0, "keyboard")
|
||||
}
|
||||
if (groupNames.contains("window")) {
|
||||
val windowGroupId = groupIds[groupNames.indexOf("window")]
|
||||
groupIds.remove(windowGroupId)
|
||||
groupNames.remove("window")
|
||||
groupIds.add(0, windowGroupId)
|
||||
groupNames.add(0, "window")
|
||||
listOf(
|
||||
Pair("keyboard", true),
|
||||
Pair("window", true),
|
||||
Pair("extractEditLayout", false),
|
||||
Pair("extractActionButton", false),
|
||||
).forEach { (groupName, addFirst) ->
|
||||
if (groupNames.contains(groupName)) {
|
||||
val groupId = groupIds[groupNames.indexOf(groupName)]
|
||||
groupIds.remove(groupId)
|
||||
groupNames.remove(groupName)
|
||||
if (addFirst) {
|
||||
groupIds.add(0, groupId)
|
||||
groupNames.add(0, groupName)
|
||||
} else {
|
||||
groupIds.add(groupId)
|
||||
groupNames.add(groupName)
|
||||
}
|
||||
}
|
||||
}
|
||||
for ((n, groupId) in groupIds.withIndex()) {
|
||||
binding.themeAttributes.findViewById<ThemeAttrGroupView>(groupId)?.let { groupView ->
|
||||
|
||||
@@ -89,6 +89,11 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
binding.fabOptionCreateEmpty.setOnClickListener { onActionClicked(it) }
|
||||
binding.fabOptionCreateFromSelected.setOnClickListener { onActionClicked(it) }
|
||||
binding.themeDeleteBtn.setOnClickListener { onActionClicked(it) }
|
||||
binding.themeEditBtn.setOnClickListener { onActionClicked(it) }
|
||||
|
||||
layoutManager = LayoutManager(this).apply {
|
||||
preloadComputedLayout(KeyboardMode.CHARACTERS, Subtype.DEFAULT, prefs)
|
||||
}
|
||||
|
||||
@@ -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,6 +22,7 @@ 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
|
||||
@@ -104,4 +105,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package dev.patrickgold.florisboard.settings.fragments
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import dev.patrickgold.florisboard.R
|
||||
@@ -27,6 +28,7 @@ import dev.patrickgold.florisboard.settings.components.DialogSeekBarPreference
|
||||
class KeyboardFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private var heightFactorCustom: DialogSeekBarPreference? = null
|
||||
private var utilityKeyAction: ListPreference? = null
|
||||
private var sharedPrefs: SharedPreferences? = null
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
@@ -34,7 +36,9 @@ class KeyboardFragment : PreferenceFragmentCompat(),
|
||||
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
heightFactorCustom = findPreference(PrefHelper.Keyboard.HEIGHT_FACTOR_CUSTOM)
|
||||
utilityKeyAction = findPreference(PrefHelper.Keyboard.UTILITY_KEY_ACTION)
|
||||
onSharedPreferenceChanged(null, PrefHelper.Keyboard.HEIGHT_FACTOR)
|
||||
onSharedPreferenceChanged(null, PrefHelper.Keyboard.UTILITY_KEY_ENABLED)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -50,6 +54,8 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package dev.patrickgold.florisboard.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.res.ColorStateList
|
||||
@@ -11,6 +10,7 @@ import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.children
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
fun getColorFromAttr(
|
||||
context: Context,
|
||||
@@ -73,6 +73,18 @@ fun refreshLayoutOf(view: View?) {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : View> ViewGroup.findViewWithType(type: KClass<T>): T? {
|
||||
for (child in this.children) {
|
||||
if (type.isInstance(child)) {
|
||||
return child as T
|
||||
} else if (child is ViewGroup) {
|
||||
child.findViewWithType(type)?.let { return it }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Context extension function to get the Activity from the Context. Originally written by Vlad as
|
||||
* an SO answer. Modified to return an AppCompatActivity, as FlorisBoard relies on some compat
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:color="#FFFFFFFF"/>
|
||||
<item android:color="#A2FFFFFF"/>
|
||||
</selector>
|
||||
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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user