Compare commits
164 Commits
v0.3.11-be
...
v0.3.13-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37bb4cea43 | ||
|
|
79d608feea | ||
|
|
54573de3e3 | ||
|
|
2fba2d3b4a | ||
|
|
fd0cbbdcb1 | ||
|
|
b6e3deedf4 | ||
|
|
4c74bf1b4a | ||
|
|
2a4e3c8c58 | ||
|
|
e34e5b4260 | ||
|
|
ae2df7dfe4 | ||
|
|
1b3d0a5cf2 | ||
|
|
4c94329071 | ||
|
|
6ffcf2f865 | ||
|
|
e2c9a66880 | ||
|
|
e9bc25ebc7 | ||
|
|
6379e63669 | ||
|
|
70a0763e7f | ||
|
|
863080e6ce | ||
|
|
3ef454b8bd | ||
|
|
2bbdfc71d0 | ||
|
|
d1c783dde1 | ||
|
|
644da67601 | ||
|
|
b8d99efd29 | ||
|
|
4067d92a44 | ||
|
|
13a17f3a6b | ||
|
|
57c679e500 | ||
|
|
f70f45dab6 | ||
|
|
8d8f723d66 | ||
|
|
7c3c6a7ad7 | ||
|
|
d7a1c9377a | ||
|
|
2a317372b2 | ||
|
|
402f7bd267 | ||
|
|
e8eb6e3068 | ||
|
|
3dd9c45777 | ||
|
|
7255229361 | ||
|
|
4d2fa29886 | ||
|
|
ef90faf98b | ||
|
|
82caa8365e | ||
|
|
391257e9e9 | ||
|
|
b082253167 | ||
|
|
8df701e3fe | ||
|
|
9f232f5dbf | ||
|
|
7017726dcb | ||
|
|
b48ca8fd1e | ||
|
|
88d5e15a5e | ||
|
|
e9537cbd1d | ||
|
|
8e216bf3ac | ||
|
|
63352cc615 | ||
|
|
e9e2563739 | ||
|
|
87bb098445 | ||
|
|
da1944bedf | ||
|
|
d4a92e0d46 | ||
|
|
0fa6c1f235 | ||
|
|
260b1ba5ca | ||
|
|
f0799a6a0e | ||
|
|
155238946a | ||
|
|
45f91cf40c | ||
|
|
94f5b56b6a | ||
|
|
46db467073 | ||
|
|
17dde536d9 | ||
|
|
be67bf4b84 | ||
|
|
8f142548fe | ||
|
|
a68f439f39 | ||
|
|
7a0892bb36 | ||
|
|
8457390156 | ||
|
|
72be3898c1 | ||
|
|
d35bf5af63 | ||
|
|
04d3af6484 | ||
|
|
26920e4a98 | ||
|
|
7419966b51 | ||
|
|
58b832c6c3 | ||
|
|
99f2ec1879 | ||
|
|
4249f9ef86 | ||
|
|
60107ae299 | ||
|
|
6a95a865fa | ||
|
|
9e32589af5 | ||
|
|
6133e225e1 | ||
|
|
348c143d92 | ||
|
|
ce00785ffe | ||
|
|
78cdce750d | ||
|
|
f3f95ae282 | ||
|
|
018885eb30 | ||
|
|
c6c8a76dd6 | ||
|
|
3cae8b7230 | ||
|
|
814c8de0c2 | ||
|
|
32fe175b48 | ||
|
|
b901f6de8d | ||
|
|
fe9ba3246c | ||
|
|
71a39f0fc1 | ||
|
|
f7556898e1 | ||
|
|
578539f5d0 | ||
|
|
7c28c7fbea | ||
|
|
88bcadff81 | ||
|
|
25e25dfbf0 | ||
|
|
ba3dc0178d | ||
|
|
91e7f424bb | ||
|
|
b89f791eb0 | ||
|
|
ad3a0425ab | ||
|
|
7cf52ecf3e | ||
|
|
b1ef18f4fd | ||
|
|
b74af5bbe9 | ||
|
|
b8aa4bbfc4 | ||
|
|
e024ac9272 | ||
|
|
c5fa027a8e | ||
|
|
b6ec2b25be | ||
|
|
a756b59c60 | ||
|
|
8687ce55ed | ||
|
|
1ac6985dd0 | ||
|
|
986b4a878f | ||
|
|
1ef38fe7f3 | ||
|
|
bcad0af35e | ||
|
|
b5b89fde4f | ||
|
|
be1fc710ed | ||
|
|
aa55fd3070 | ||
|
|
a132462466 | ||
|
|
df393ff607 | ||
|
|
64040f0407 | ||
|
|
0c1abdd507 | ||
|
|
53594e3343 | ||
|
|
c6c06b87c5 | ||
|
|
ae6eb5d72d | ||
|
|
bbce53fdf4 | ||
|
|
fdd7e60c1d | ||
|
|
3b9a489d5c | ||
|
|
de40ccb759 | ||
|
|
a04d584402 | ||
|
|
a14a6a798b | ||
|
|
636d329dba | ||
|
|
d1e97dac57 | ||
|
|
41fbca8f65 | ||
|
|
535b48e5b4 | ||
|
|
d3e8d35e5d | ||
|
|
da8073141e | ||
|
|
030665732a | ||
|
|
cc042dd77c | ||
|
|
773624769d | ||
|
|
0b1d0c74fe | ||
|
|
760d307478 | ||
|
|
084c2abfc2 | ||
|
|
df6b08024f | ||
|
|
25498695ef | ||
|
|
5c81179d60 | ||
|
|
58d150bb03 | ||
|
|
2b1951ea5f | ||
|
|
5a5089c413 | ||
|
|
dcd20e4b73 | ||
|
|
88a6f436ef | ||
|
|
ee8f44d816 | ||
|
|
0308ec355f | ||
|
|
3ac14f8a2a | ||
|
|
2b087b76dc | ||
|
|
1715e5ddfa | ||
|
|
6cc17161a5 | ||
|
|
5d1c20617b | ||
|
|
d9efa48c9c | ||
|
|
dedd4cb7f0 | ||
|
|
42b147b656 | ||
|
|
47ce490d6c | ||
|
|
5563a1cadd | ||
|
|
7beb2e5ef6 | ||
|
|
f00da13cba | ||
|
|
bfed1747f7 | ||
|
|
abb4b104fa | ||
|
|
b69b1caa72 |
@@ -9,7 +9,7 @@ insert_final_newline = true
|
||||
max_line_length = 120
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{*.har,*.json}]
|
||||
[{*.har,*.json,*yml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.kt]
|
||||
|
||||
4
.github/workflows/android.yml
vendored
4
.github/workflows/android.yml
vendored
@@ -16,6 +16,8 @@ jobs:
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Setup CMake and Ninja
|
||||
uses: lukka/get-cmake@v3.20.1
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
@@ -25,7 +27,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean assemble
|
||||
run: ./gradlew clean assembleDebug
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-debug.apk
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -39,4 +39,10 @@ captures/
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
crowdin.properties
|
||||
crowdin.properties
|
||||
|
||||
# C++
|
||||
.cxx/
|
||||
|
||||
# AndroidX Room schema JSONs
|
||||
/app/schemas/
|
||||
|
||||
34
README.md
34
README.md
@@ -45,7 +45,11 @@ _A. IzzySoft's repo for F-Droid_:
|
||||
|
||||
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" height="64" alt="IzzySoft repo badge">](https://apt.izzysoft.de/fdroid/index/apk/dev.patrickgold.florisboard.beta)
|
||||
|
||||
_B. Use the APK provided in the release section of this repo_
|
||||
_B. Google Play_:
|
||||
|
||||
Follow the same steps as for the stable track, the app can then be accessed [here](https://play.google.com/store/apps/details?id=dev.patrickgold.florisboard.beta).
|
||||
|
||||
_C. Use the APK provided in the release section of this repo_
|
||||
|
||||
### Giving feedback
|
||||
If you want to give feedback to FlorisBoard, there are several ways to
|
||||
@@ -74,8 +78,8 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
|
||||
|
||||
### Layouts
|
||||
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish, Norwegian, Swedish/Finnish, Icelandic, Danish,
|
||||
Hungarian, Croatian, Polish, Romanian, Colemak, Dvorak, Turkish-Q, Turkish-F, ...)
|
||||
* [x] Non-latin character layouts (Arabic, Persian, Kurdish, Greek, Russian (JCUKEN))
|
||||
Hungarian, Croatian, Polish, Romanian, Colemak, Dvorak, Turkish-Q, Turkish-F, and more...)
|
||||
* [x] Non-latin character layouts (Arabic, Persian, Kurdish, Greek, Russian (JCUKEN), and more...)
|
||||
* [x] Adapt to situation in app (password, url, text, etc. )
|
||||
* [x] Special character layout(s)
|
||||
* [x] Numeric layout
|
||||
@@ -93,13 +97,17 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
|
||||
* [x] Subtype selection (language/layout)
|
||||
* [x] Keyboard behaviour preferences
|
||||
* [x] Gesture preferences
|
||||
* [x] User dictionary manager (system and internal)
|
||||
|
||||
### Other useful features
|
||||
* [x] Support for Android 11+ inline autofill API
|
||||
* [x] One-handed mode
|
||||
* [x] Clipboard/cursor tools
|
||||
* [x] Clipboard manager/history
|
||||
* [x] Integrated number row / symbols in character layouts
|
||||
* [x] Gesture support
|
||||
* [x] Full support for the system user dictionary (shared dictionary
|
||||
between all keyboards) and a private, internal user dictionary
|
||||
* [x] Full integration in IME service list of Android (xml/method)
|
||||
(integration is internal-only, because Android's default subtype
|
||||
implementation not really allows for dynamic language/layout
|
||||
@@ -131,13 +139,14 @@ close as possible.
|
||||
- Next-word suggestions by training language models. Data collected here is stored locally and never leaves
|
||||
the user's device.
|
||||
|
||||
- Module C: Extension packs (base implementation with [#162])
|
||||
- Module C: Extension packs (Implemented with [#162], reworked several times and still not stable)
|
||||
- Ability to load dictionaries (and later potentially other cool
|
||||
features too) only if needed to keep the core APK size small
|
||||
- Currently unclear how exactly this will work, but this is definitely
|
||||
a must-have feature
|
||||
- A full implementation may come only in v0.5.0
|
||||
|
||||
- Module D: Glide typing
|
||||
- Module D: Glide typing (Implemented with [#544])
|
||||
- Swiping over the characters will automatically convert this to a word
|
||||
- Possibly also add improvements based on the Flow keyboard
|
||||
|
||||
@@ -151,9 +160,11 @@ close as possible.
|
||||
- Theme import/export
|
||||
|
||||
### [v0.5.0](https://github.com/florisboard/florisboard/milestone/5)
|
||||
There's no exact roadmap yet but it is planned that the media part of
|
||||
FlorisBoard (emojis, emoticons, kaomoji) gets a rework. Also as an extension
|
||||
(requires v0.4.0/Module C) GIF support is planned.
|
||||
There's no exact roadmap yet, but these are the most important points:
|
||||
- Full layout customization in runtime
|
||||
- Extensive rework and customization of the media input (emojis, emoticons, kaomoji)
|
||||
- Better Smartbar customization
|
||||
- As an extension GIF support
|
||||
|
||||
### > v0.5.0
|
||||
This is completely open as of now and will gather planned features as time
|
||||
@@ -166,6 +177,7 @@ Backlog (currently not assigned to any milestone):
|
||||
[#91]: https://github.com/florisboard/florisboard/pull/91
|
||||
[#162]: https://github.com/florisboard/florisboard/pull/162
|
||||
[#329]: https://github.com/florisboard/florisboard/pull/329
|
||||
[#544]: https://github.com/florisboard/florisboard/pull/544
|
||||
|
||||
## Contributing
|
||||
Wanna contribute to FlorisBoard? That's great to hear! There are lots of
|
||||
@@ -183,8 +195,8 @@ to get more information on this topic.
|
||||
by [google](https://github.com/google)
|
||||
* [Google Material icons](https://github.com/google/material-design-icons) by
|
||||
[google](https://github.com/google)
|
||||
* [Moshi JSON library](https://github.com/square/moshi) by
|
||||
[square](https://github.com/square)
|
||||
* [KotlinX serialization library](https://github.com/Kotlin/kotlinx.serialization) by
|
||||
[Kotlin](https://github.com/Kotlin)
|
||||
* [ColorPicker preference](https://github.com/jaredrummler/ColorPicker) by
|
||||
[Jared Rummler](https://github.com/jaredrummler)
|
||||
* [Timber](https://github.com/JakeWharton/timber) by
|
||||
@@ -194,7 +206,7 @@ to get more information on this topic.
|
||||
|
||||
## Usage notes for included binary dictionary files
|
||||
All binary dictionaries included within this project in
|
||||
(this)[app/src/main/assets/ime/dict) asset folder are built from various
|
||||
(this)[app/src/main/assets/ime/dict] asset folder are built from various
|
||||
sources, as stated below.
|
||||
|
||||
### Source 1: [wordfreq library by LuminosoInsight](https://github.com/LuminosoInsight/wordfreq):
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
plugins {
|
||||
id("com.android.application") version "4.1.3"
|
||||
kotlin("android") version "1.5.0-RC"
|
||||
kotlin("kapt") version "1.5.0-RC"
|
||||
kotlin("plugin.serialization") version "1.5.0-RC"
|
||||
id("com.android.application") version "4.2.1"
|
||||
kotlin("android") version "1.5.0"
|
||||
kotlin("kapt") version "1.5.0"
|
||||
kotlin("plugin.serialization") version "1.5.0"
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -24,21 +24,47 @@ android {
|
||||
applicationId = "dev.patrickgold.florisboard"
|
||||
minSdkVersion(23)
|
||||
targetSdkVersion(30)
|
||||
versionCode(39)
|
||||
versionName("0.3.11")
|
||||
versionCode(47)
|
||||
versionName("0.3.13")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments += mapOf(
|
||||
Pair("room.schemaLocation", "$projectDir/schemas"),
|
||||
Pair("room.incremental", "true"),
|
||||
Pair("room.expandProjection", "true")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cppFlags("-std=c++17", "-fexceptions", "-frtti")
|
||||
arguments("-DANDROID_STL=c++_static")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path("src/main/cpp/CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
named("debug").configure {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
|
||||
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_debug")
|
||||
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_debug_round")
|
||||
resValue("string", "floris_app_name", "FlorisBoard Debug")
|
||||
@@ -79,12 +105,13 @@ android {
|
||||
dependencies {
|
||||
implementation("androidx.activity", "activity-ktx", "1.2.1")
|
||||
implementation("androidx.appcompat", "appcompat", "1.2.0")
|
||||
implementation("androidx.autofill", "autofill", "1.1.0")
|
||||
implementation("androidx.core", "core-ktx", "1.3.2")
|
||||
implementation("androidx.fragment", "fragment-ktx", "1.3.0")
|
||||
implementation("androidx.preference", "preference-ktx", "1.1.1")
|
||||
implementation("androidx.constraintlayout", "constraintlayout", "2.0.4")
|
||||
implementation("androidx.lifecycle", "lifecycle-service", "2.2.0")
|
||||
implementation("com.google.android", "flexbox", "2.0.1") // requires jcenter as of version 2.0.1
|
||||
implementation("com.google.android", "flexbox", "2.0.1")
|
||||
implementation("com.google.android.material", "material", "1.3.0")
|
||||
implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-android", "1.4.2")
|
||||
implementation("org.jetbrains.kotlinx", "kotlinx-serialization-json", "1.1.0")
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
|
||||
<application
|
||||
android:name=".ime.core.FlorisApplication"
|
||||
android:name="dev.patrickgold.florisboard.FlorisApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/floris_app_name"
|
||||
@@ -31,16 +31,13 @@
|
||||
|
||||
<!-- IME service -->
|
||||
<service
|
||||
android:name="dev.patrickgold.florisboard.ime.core.FlorisBoard"
|
||||
android:name="dev.patrickgold.florisboard.FlorisImeService"
|
||||
android:label="@string/floris_app_name"
|
||||
android:permission="android.permission.BIND_INPUT_METHOD">
|
||||
<meta-data
|
||||
android:name="android.view.im"
|
||||
android:resource="@xml/method"/>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.view.InputMethod"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.view.im" android:resource="@xml/method"/>
|
||||
</service>
|
||||
|
||||
<!-- Settings Activity -->
|
||||
@@ -66,6 +63,13 @@
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<!-- User Dictionary Manager Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.UdmActivity"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__title"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Theme Selector Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.ThemeManagerActivity"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"package": "dev.patrickgold.florisboard",
|
||||
"composers": [
|
||||
{ "$": "appender" },
|
||||
{ "$": "hangul-unicode" }
|
||||
],
|
||||
"currencySets": [
|
||||
{
|
||||
"name": "azerbaijani_manat",
|
||||
@@ -246,6 +250,7 @@
|
||||
{
|
||||
"id": 101,
|
||||
"languageTag": "en-US",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "qwerty"
|
||||
@@ -254,6 +259,7 @@
|
||||
{
|
||||
"id": 102,
|
||||
"languageTag": "en-UK",
|
||||
"composer": "appender",
|
||||
"currencySet": "pound",
|
||||
"preferred": {
|
||||
"characters": "qwerty"
|
||||
@@ -262,6 +268,7 @@
|
||||
{
|
||||
"id": 103,
|
||||
"languageTag": "en-CA",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "qwerty"
|
||||
@@ -270,6 +277,7 @@
|
||||
{
|
||||
"id": 104,
|
||||
"languageTag": "en-AU",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "qwerty"
|
||||
@@ -278,6 +286,7 @@
|
||||
{
|
||||
"id": 201,
|
||||
"languageTag": "de-DE",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "qwertz"
|
||||
@@ -286,6 +295,7 @@
|
||||
{
|
||||
"id": 202,
|
||||
"languageTag": "de-AT",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "qwertz"
|
||||
@@ -294,14 +304,27 @@
|
||||
{
|
||||
"id": 203,
|
||||
"languageTag": "de-CH",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "swiss_german"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 204,
|
||||
"languageTag": "de-DE-neobone",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "neo2",
|
||||
"symbols": "neo2",
|
||||
"numericRow": "neo2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 301,
|
||||
"languageTag": "fr-FR",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "azerty"
|
||||
@@ -310,6 +333,7 @@
|
||||
{
|
||||
"id": 302,
|
||||
"languageTag": "fr-CA",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "canadian_french"
|
||||
@@ -318,6 +342,7 @@
|
||||
{
|
||||
"id": 303,
|
||||
"languageTag": "fr-CH",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "swiss_french"
|
||||
@@ -326,6 +351,7 @@
|
||||
{
|
||||
"id": 401,
|
||||
"languageTag": "it-IT",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "qwerty"
|
||||
@@ -334,6 +360,7 @@
|
||||
{
|
||||
"id": 402,
|
||||
"languageTag": "it-CH",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "swiss_italian"
|
||||
@@ -342,6 +369,7 @@
|
||||
{
|
||||
"id": 501,
|
||||
"languageTag": "es-ES",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "spanish"
|
||||
@@ -350,6 +378,7 @@
|
||||
{
|
||||
"id": 502,
|
||||
"languageTag": "es-US",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "spanish"
|
||||
@@ -358,6 +387,7 @@
|
||||
{
|
||||
"id": 503,
|
||||
"languageTag": "es-419",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "spanish"
|
||||
@@ -366,6 +396,7 @@
|
||||
{
|
||||
"id": 601,
|
||||
"languageTag": "pt-PT",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "qwerty"
|
||||
@@ -374,6 +405,7 @@
|
||||
{
|
||||
"id": 602,
|
||||
"languageTag": "pt-BR",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "qwerty"
|
||||
@@ -382,6 +414,7 @@
|
||||
{
|
||||
"id": 701,
|
||||
"languageTag": "nb-NO",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "norwegian"
|
||||
@@ -390,6 +423,7 @@
|
||||
{
|
||||
"id": 702,
|
||||
"languageTag": "nn-NO",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "norwegian"
|
||||
@@ -398,6 +432,7 @@
|
||||
{
|
||||
"id": 711,
|
||||
"languageTag": "sv-SE",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "swedish_finnish"
|
||||
@@ -406,6 +441,7 @@
|
||||
{
|
||||
"id": 721,
|
||||
"languageTag": "fi-FI",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "swedish_finnish"
|
||||
@@ -414,6 +450,7 @@
|
||||
{
|
||||
"id": 731,
|
||||
"languageTag": "da-DK",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "danish"
|
||||
@@ -422,6 +459,7 @@
|
||||
{
|
||||
"id": 741,
|
||||
"languageTag": "is-IS",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "icelandic"
|
||||
@@ -430,6 +468,7 @@
|
||||
{
|
||||
"id": 751,
|
||||
"languageTag": "fo",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "faroese"
|
||||
@@ -438,6 +477,7 @@
|
||||
{
|
||||
"id": 801,
|
||||
"languageTag": "fa-FA",
|
||||
"composer": "appender",
|
||||
"currencySet": "iranian_rial",
|
||||
"preferred": {
|
||||
"characters": "persian",
|
||||
@@ -449,6 +489,7 @@
|
||||
{
|
||||
"id": 901,
|
||||
"languageTag": "ar",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "arabic",
|
||||
@@ -460,6 +501,7 @@
|
||||
{
|
||||
"id": 1001,
|
||||
"languageTag": "hu",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "hungarian"
|
||||
@@ -468,6 +510,7 @@
|
||||
{
|
||||
"id": 1101,
|
||||
"languageTag": "eo",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "esperanto"
|
||||
@@ -476,6 +519,7 @@
|
||||
{
|
||||
"id": 1201,
|
||||
"languageTag": "hr",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "qwertz"
|
||||
@@ -484,6 +528,7 @@
|
||||
{
|
||||
"id": 1301,
|
||||
"languageTag": "ru",
|
||||
"composer": "appender",
|
||||
"currencySet": "russian_ruble",
|
||||
"preferred": {
|
||||
"characters": "jcuken_russian"
|
||||
@@ -492,6 +537,7 @@
|
||||
{
|
||||
"id": 1351,
|
||||
"languageTag": "uk",
|
||||
"composer": "appender",
|
||||
"currencySet": "ukrainian_hryvnia",
|
||||
"preferred": {
|
||||
"characters": "jcuken_ukrainian"
|
||||
@@ -500,6 +546,7 @@
|
||||
{
|
||||
"id": 1401,
|
||||
"languageTag": "el",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "greek"
|
||||
@@ -508,6 +555,7 @@
|
||||
{
|
||||
"id": 1501,
|
||||
"languageTag": "ro",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "qwerty"
|
||||
@@ -516,6 +564,7 @@
|
||||
{
|
||||
"id": 1601,
|
||||
"languageTag": "pl",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "qwerty"
|
||||
@@ -524,6 +573,7 @@
|
||||
{
|
||||
"id": 1701,
|
||||
"languageTag": "bg-bg",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "bulgarian_phonetic"
|
||||
@@ -532,6 +582,7 @@
|
||||
{
|
||||
"id": 1801,
|
||||
"languageTag": "tr",
|
||||
"composer": "appender",
|
||||
"currencySet": "turkish_lira",
|
||||
"preferred": {
|
||||
"characters": "qwerty"
|
||||
@@ -540,6 +591,7 @@
|
||||
{
|
||||
"id": 1901,
|
||||
"languageTag": "iw-IL",
|
||||
"composer": "appender",
|
||||
"currencySet": "israeli_new_shekel",
|
||||
"preferred": {
|
||||
"characters": "hebrew"
|
||||
@@ -548,6 +600,7 @@
|
||||
{
|
||||
"id": 2001,
|
||||
"languageTag": "ckb",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "kurdish",
|
||||
@@ -559,6 +612,7 @@
|
||||
{
|
||||
"id": 2101,
|
||||
"languageTag": "sr-RS",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "serbian_cyrillic"
|
||||
@@ -567,6 +621,7 @@
|
||||
{
|
||||
"id": 2201,
|
||||
"languageTag": "lv-LV",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "qwerty"
|
||||
@@ -575,6 +630,7 @@
|
||||
{
|
||||
"id": 2301,
|
||||
"languageTag": "ku",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "kurdish_kurmanci"
|
||||
@@ -583,6 +639,7 @@
|
||||
{
|
||||
"id": 2501,
|
||||
"languageTag": "ca",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "catalan"
|
||||
@@ -591,6 +648,7 @@
|
||||
{
|
||||
"id": 2601,
|
||||
"languageTag": "IPA-IPA",
|
||||
"composer": "appender",
|
||||
"currencySet": "dollar",
|
||||
"preferred": {
|
||||
"characters": "ipa",
|
||||
@@ -601,6 +659,7 @@
|
||||
{
|
||||
"id": 2701,
|
||||
"languageTag": "sk",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "qwertz"
|
||||
@@ -609,10 +668,20 @@
|
||||
{
|
||||
"id": 2801,
|
||||
"languageTag": "cs",
|
||||
"composer": "appender",
|
||||
"currencySet": "euro",
|
||||
"preferred": {
|
||||
"characters": "qwertz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2900,
|
||||
"languageTag": "ko",
|
||||
"composer": "hangul-unicode",
|
||||
"currencySet": "south_korean_won",
|
||||
"preferred": {
|
||||
"characters": "korean"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
61
app/src/main/assets/ime/text/characters/bone.json
Normal file
61
app/src/main/assets/ime/text/characters/bone.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "bone",
|
||||
"label": "Bone",
|
||||
"authors": [ "ostrya" ],
|
||||
"direction": "ltr",
|
||||
"modifier": "neo2",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 106, "label": "j" },
|
||||
{ "$": "auto_text_key", "code": 100, "label": "d" },
|
||||
{ "$": "auto_text_key", "code": 117, "label": "u" },
|
||||
{ "$": "auto_text_key", "code": 97, "label": "a" },
|
||||
{ "$": "auto_text_key", "code": 120, "label": "x" },
|
||||
{ "$": "auto_text_key", "code": 112, "label": "p" },
|
||||
{ "$": "auto_text_key", "code": 104, "label": "h" },
|
||||
{ "$": "auto_text_key", "code": 108, "label": "l" },
|
||||
{ "$": "auto_text_key", "code": 109, "label": "m" },
|
||||
{ "$": "auto_text_key", "code": 119, "label": "w" },
|
||||
{ "$": "case_selector",
|
||||
"lower": {
|
||||
"code": 223, "label": "ß", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 180, "label": "´" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"upper": {
|
||||
"code": 7838, "label": "ẞ", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 180, "label": "´" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 99, "label": "c" },
|
||||
{ "$": "auto_text_key", "code": 116, "label": "t" },
|
||||
{ "$": "auto_text_key", "code": 105, "label": "i" },
|
||||
{ "$": "auto_text_key", "code": 101, "label": "e" },
|
||||
{ "$": "auto_text_key", "code": 111, "label": "o" },
|
||||
{ "$": "auto_text_key", "code": 98, "label": "b" },
|
||||
{ "$": "auto_text_key", "code": 110, "label": "n" },
|
||||
{ "$": "auto_text_key", "code": 114, "label": "r" },
|
||||
{ "$": "auto_text_key", "code": 115, "label": "s" },
|
||||
{ "$": "auto_text_key", "code": 103, "label": "g" },
|
||||
{ "$": "auto_text_key", "code": 113, "label": "q" }
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 102, "label": "f" },
|
||||
{ "$": "auto_text_key", "code": 118, "label": "v" },
|
||||
{ "$": "auto_text_key", "code": 252, "label": "ü" },
|
||||
{ "$": "auto_text_key", "code": 228, "label": "ä" },
|
||||
{ "$": "auto_text_key", "code": 246, "label": "ö" },
|
||||
{ "$": "auto_text_key", "code": 121, "label": "y" },
|
||||
{ "$": "auto_text_key", "code": 122, "label": "z" },
|
||||
{ "$": "auto_text_key", "code": 107, "label": "k" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -4,28 +4,22 @@
|
||||
"authors": [ "GoRaN" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
|
||||
"ﻪ": {
|
||||
"relevant": [
|
||||
{ "code": 1577, "label": "ة" },
|
||||
{ "code": 1729, "label": "ـہ" }
|
||||
]
|
||||
},
|
||||
"ر": {
|
||||
"relevant": [
|
||||
{ "code": 1685, "label": "ڕ" },
|
||||
{ "code": 1682, "label": "ڒ" }
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
"ی": {
|
||||
"relevant": [
|
||||
{ "code": 1746, "label": "ے" },
|
||||
{ "code": 1610, "label": "ي" },
|
||||
{ "code": 1744, "label": "ې" },
|
||||
{ "code": 1741, "label": "ۍ" },
|
||||
{ "code": 1742, "label": "ێ" },
|
||||
{ "code": 1744, "label": "ې" },
|
||||
{ "code": 1610, "label": "ي" },
|
||||
{ "code": 1597, "label": "ؽ" }
|
||||
]
|
||||
},
|
||||
@@ -34,10 +28,15 @@
|
||||
"ﺋ": {
|
||||
"relevant": [
|
||||
{ "code": 65163, "label": "ﺋ" },
|
||||
{ "code": 1569, "label": "ء" },
|
||||
{ "code": 65139, "label": "ﹳ" }
|
||||
]
|
||||
},
|
||||
"ح": {
|
||||
"relevant": [
|
||||
{ "code": 65010, "label": "ﷲ" },
|
||||
{ "code": 65019, "label": "ﷻ" }
|
||||
]
|
||||
},
|
||||
|
||||
"ع": {
|
||||
"relevant": [
|
||||
@@ -56,12 +55,9 @@
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
"ف": {
|
||||
"relevant": [
|
||||
{ "code": 1701, "label": "ڥ" },
|
||||
{ "code": 1700, "label": "ڤ" },
|
||||
{ "code": 1698, "label": "ڢ" },
|
||||
{ "code": 1697, "label": "ڡ" }
|
||||
]
|
||||
@@ -70,7 +66,6 @@
|
||||
"د": {
|
||||
"relevant": [
|
||||
{ "code": 1676, "label": "ڌ" },
|
||||
{ "code": 1584, "label": "ذ" },
|
||||
{ "code": 64390, "label": "ﮆ" },
|
||||
{ "code": 1774, "label": "ۮ" }
|
||||
]
|
||||
@@ -93,9 +88,7 @@
|
||||
},
|
||||
"ب": {
|
||||
"relevant": [
|
||||
{ "code": 65010, "label": "ﷲ" },
|
||||
{ "code": 65021, "label": "﷽" },
|
||||
{ "code": 65019, "label": "ﷻ" }
|
||||
{ "code": 65021, "label": "﷽" }
|
||||
]
|
||||
},
|
||||
"م": {
|
||||
@@ -108,7 +101,6 @@
|
||||
"relevant": [
|
||||
{ "code": 1718, "label": "ڶ" },
|
||||
{ "code": 1719, "label": "ڷ" },
|
||||
{ "code": 1717, "label": "ڵ" },
|
||||
{ "code": 1720, "label": "ڸ" }
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "de-DE-neobone",
|
||||
"authors": [ "ostrya" ],
|
||||
"mapping": {
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".ch" },
|
||||
{ "code": -255, "label": ".de" },
|
||||
{ "code": -255, "label": ".at" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
]
|
||||
},
|
||||
"ι": {
|
||||
"main": { "$": "auto_text_key", "code": 943, "label": "ί" },
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 912, "label": "ΐ" },
|
||||
{ "$": "auto_text_key", "code": 970, "label": "ϊ" },
|
||||
{ "$": "auto_text_key", "code": 943, "label": "ί" }
|
||||
{ "$": "auto_text_key", "code": 970, "label": "ϊ" }
|
||||
]
|
||||
},
|
||||
"ο": {
|
||||
@@ -32,10 +32,10 @@
|
||||
]
|
||||
},
|
||||
"υ": {
|
||||
"main": { "$": "auto_text_key", "code": 973, "label": "ύ" },
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 944, "label": "ΰ" },
|
||||
{ "$": "auto_text_key", "code": 971, "label": "ϋ" },
|
||||
{ "$": "auto_text_key", "code": 973, "label": "ύ" }
|
||||
{ "$": "auto_text_key", "code": 971, "label": "ϋ" }
|
||||
]
|
||||
},
|
||||
"ω": {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "ko",
|
||||
"authors": [ "patrickgold", "Hayleia" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"ㅂ": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 12611, "label": "ㅃ" }
|
||||
]
|
||||
},
|
||||
"ㅈ": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 12617, "label": "ㅉ" }
|
||||
]
|
||||
},
|
||||
"ㄷ": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 12600, "label": "ㄸ" }
|
||||
]
|
||||
},
|
||||
"ㄱ": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 12594, "label": "ㄲ" }
|
||||
]
|
||||
},
|
||||
"ㅅ": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 12614, "label": "ㅆ" }
|
||||
]
|
||||
},
|
||||
"ㅐ": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 12626, "label": "ㅒ" }
|
||||
]
|
||||
},
|
||||
"ㅔ": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 12630, "label": "ㅖ" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
77
app/src/main/assets/ime/text/characters/halmak.json
Normal file
77
app/src/main/assets/ime/text/characters/halmak.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "halmak",
|
||||
"label": "Halmak",
|
||||
"authors": [ "dessalines" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 119, "label": "w" },
|
||||
{ "$": "auto_text_key", "code": 108, "label": "l" },
|
||||
{ "$": "auto_text_key", "code": 114, "label": "r" },
|
||||
{ "$": "auto_text_key", "code": 98, "label": "b" },
|
||||
{ "$": "auto_text_key", "code": 122, "label": "z" },
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 59, "label": ";", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 58, "label": ":" }
|
||||
]
|
||||
} },
|
||||
"upper": { "code": 58, "label": ":", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 59, "label": ";" }
|
||||
]
|
||||
} }
|
||||
},
|
||||
{ "$": "auto_text_key", "code": 113, "label": "q" },
|
||||
{ "$": "auto_text_key", "code": 117, "label": "u" },
|
||||
{ "$": "auto_text_key", "code": 100, "label": "d" },
|
||||
{ "$": "auto_text_key", "code": 106, "label": "j" }
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 115, "label": "s" },
|
||||
{ "$": "auto_text_key", "code": 104, "label": "h" },
|
||||
{ "$": "auto_text_key", "code": 110, "label": "n" },
|
||||
{ "$": "auto_text_key", "code": 116, "label": "t" },
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 44, "label": ",", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 40, "label": "(" }
|
||||
]
|
||||
} },
|
||||
"upper": { "code": 40, "label": "(", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 44, "label": "," }
|
||||
]
|
||||
} }
|
||||
},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 46, "label": ".", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 41, "label": ")" }
|
||||
]
|
||||
} },
|
||||
"upper": { "code": 41, "label": ")", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 46, "label": "." }
|
||||
]
|
||||
} }
|
||||
},
|
||||
{ "$": "auto_text_key", "code": 97, "label": "a" },
|
||||
{ "$": "auto_text_key", "code": 101, "label": "e" },
|
||||
{ "$": "auto_text_key", "code": 111, "label": "o" },
|
||||
{ "$": "auto_text_key", "code": 105, "label": "i" }
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 102, "label": "f" },
|
||||
{ "$": "auto_text_key", "code": 109, "label": "m" },
|
||||
{ "$": "auto_text_key", "code": 118, "label": "v" },
|
||||
{ "$": "auto_text_key", "code": 99, "label": "c" },
|
||||
{ "$": "auto_text_key", "code": 103, "label": "g" },
|
||||
{ "$": "auto_text_key", "code": 112, "label": "p" },
|
||||
{ "$": "auto_text_key", "code": 120, "label": "x" },
|
||||
{ "$": "auto_text_key", "code": 107, "label": "k" },
|
||||
{ "$": "auto_text_key", "code": 121, "label": "y" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -28,27 +28,27 @@
|
||||
{ "code": 1508, "label": "פ" }
|
||||
],
|
||||
[
|
||||
{ "code": 1513, "label": "ף" },
|
||||
{ "code": 1491, "label": "ך" },
|
||||
{ "code": 1490, "label": "ל" },
|
||||
{ "code": 1499, "label": "ח" },
|
||||
{ "code": 1506, "label": "י" },
|
||||
{ "code": 1497, "label": "ע" },
|
||||
{ "code": 1495, "label": "כ" },
|
||||
{ "code": 1500, "label": "ג" },
|
||||
{ "code": 1498, "label": "ד" },
|
||||
{ "code": 1507, "label": "ש" }
|
||||
{ "code": 1513, "label": "ש" },
|
||||
{ "code": 1491, "label": "ד" },
|
||||
{ "code": 1490, "label": "ג" },
|
||||
{ "code": 1499, "label": "כ" },
|
||||
{ "code": 1506, "label": "ע" },
|
||||
{ "code": 1497, "label": "י" },
|
||||
{ "code": 1495, "label": "ח" },
|
||||
{ "code": 1500, "label": "ל" },
|
||||
{ "code": 1498, "label": "ך" },
|
||||
{ "code": 1507, "label": "ף" }
|
||||
],
|
||||
[
|
||||
{ "code": 1494, "label": "ץ" },
|
||||
{ "code": 1505, "label": "ת" },
|
||||
{ "code": 1489, "label": "צ" },
|
||||
{ "code": 1492, "label": "מ" },
|
||||
{ "code": 1494, "label": "ז" },
|
||||
{ "code": 1505, "label": "ס" },
|
||||
{ "code": 1489, "label": "ב" },
|
||||
{ "code": 1492, "label": "ה" },
|
||||
{ "code": 1504, "label": "נ" },
|
||||
{ "code": 1502, "label": "ה" },
|
||||
{ "code": 1510, "label": "ב" },
|
||||
{ "code": 1514, "label": "ס" },
|
||||
{ "code": 1509, "label": "ז" }
|
||||
{ "code": 1502, "label": "מ" },
|
||||
{ "code": 1510, "label": "צ" },
|
||||
{ "code": 1514, "label": "ת" },
|
||||
{ "code": 1509, "label": "ץ" }
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
62
app/src/main/assets/ime/text/characters/korean.json
Normal file
62
app/src/main/assets/ime/text/characters/korean.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "korean",
|
||||
"label": "South Korean standard",
|
||||
"authors": [ "patrickgold", "Hayleia" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 12610, "label": "ㅂ" },
|
||||
"upper": { "code": 12611, "label": "ㅃ" }
|
||||
},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 12616, "label": "ㅈ" },
|
||||
"upper": { "code": 12617, "label": "ㅉ" }
|
||||
},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 12599, "label": "ㄷ" },
|
||||
"upper": { "code": 12600, "label": "ㄸ" }
|
||||
},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 12593, "label": "ㄱ" },
|
||||
"upper": { "code": 12594, "label": "ㄲ" }
|
||||
},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 12613, "label": "ㅅ" },
|
||||
"upper": { "code": 12614, "label": "ㅆ" }
|
||||
},
|
||||
{ "$": "auto_text_key", "code": 12635, "label": "ㅛ"},
|
||||
{ "$": "auto_text_key", "code": 12629, "label": "ㅕ"},
|
||||
{ "$": "auto_text_key", "code": 12625, "label": "ㅑ"},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 12624, "label": "ㅐ" },
|
||||
"upper": { "code": 12626, "label": "ㅒ" }
|
||||
},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 12628, "label": "ㅔ" },
|
||||
"upper": { "code": 12630, "label": "ㅖ" }
|
||||
}
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 12609, "label": "ㅁ"},
|
||||
{ "$": "auto_text_key", "code": 12596, "label": "ㄴ"},
|
||||
{ "$": "auto_text_key", "code": 12615, "label": "ㅇ"},
|
||||
{ "$": "auto_text_key", "code": 12601, "label": "ㄹ"},
|
||||
{ "$": "auto_text_key", "code": 12622, "label": "ㅎ"},
|
||||
{ "$": "auto_text_key", "code": 12631, "label": "ㅗ"},
|
||||
{ "$": "auto_text_key", "code": 12627, "label": "ㅓ"},
|
||||
{ "$": "auto_text_key", "code": 12623, "label": "ㅏ"},
|
||||
{ "$": "auto_text_key", "code": 12643, "label": "ㅣ"}
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 12619, "label": "ㅋ"},
|
||||
{ "$": "auto_text_key", "code": 12620, "label": "ㅌ"},
|
||||
{ "$": "auto_text_key", "code": 12618, "label": "ㅊ"},
|
||||
{ "$": "auto_text_key", "code": 12621, "label": "ㅍ"},
|
||||
{ "$": "auto_text_key", "code": 12640, "label": "ㅠ"},
|
||||
{ "$": "auto_text_key", "code": 12636, "label": "ㅜ"},
|
||||
{ "$": "auto_text_key", "code": 12641, "label": "ㅡ"}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "kurdish",
|
||||
"label": "کوردی",
|
||||
"label": "کوردی (قوەرتی نوێ)",
|
||||
"authors": [ "GoRaN" ],
|
||||
"direction": "rtl",
|
||||
"modifier": "kurdish",
|
||||
@@ -13,34 +13,46 @@
|
||||
{ "code": 1608, "label": "و", "popup": {
|
||||
"main": { "code": -255, "label": "وو" }
|
||||
} },
|
||||
{ "code": 1749, "label": "ﻪ" },
|
||||
{ "code": 1585, "label": "ر" },
|
||||
|
||||
{ "code": 1749, "label": "ﻪ", "popup": {
|
||||
"main": { "code": 1577, "label": "ة" }
|
||||
} },
|
||||
{ "code": 1585, "label": "ر", "popup": {
|
||||
"main": { "code": 1685, "label": "ڕ" }
|
||||
} },
|
||||
{ "code": 1578, "label": "ت", "popup": {
|
||||
"main": { "code": 1591, "label": "ط" }
|
||||
} },
|
||||
{ "code": 1740, "label": "ی" },
|
||||
|
||||
{ "code": 1574, "label": "ﺋ"},
|
||||
|
||||
{ "code": 1740, "label": "ی", "popup": {
|
||||
"main": { "code": 1742, "label": "ێ" }
|
||||
} },
|
||||
{ "code": 1574, "label": "ﺋ", "popup": {
|
||||
"main": { "code": 1569, "label": "ء" }
|
||||
} },
|
||||
{ "code": 1593, "label": "ع", "popup": {
|
||||
"main": { "code": 1594, "label": "غ" }
|
||||
} },
|
||||
{ "code": 1734, "label": "ۆ" },
|
||||
|
||||
{ "code": 1662, "label": "پ", "popup": {
|
||||
"main": { "code": 1579, "label": "ث" }
|
||||
} }
|
||||
],
|
||||
[
|
||||
{ "code": 1575, "label": "ا" },
|
||||
{"code": 1575, "label": "ا"},
|
||||
{ "code": 1587, "label": "س" },
|
||||
{ "code": 1588, "label": "ش" },
|
||||
{ "code": 1583, "label": "د" },
|
||||
{ "code": 1601, "label": "ف" },
|
||||
{ "code": 1583, "label": "د", "popup": {
|
||||
"main": {"code": 1584, "label": "ذ" }
|
||||
} },
|
||||
{ "code": 1601, "label": "ف" , "popup": {
|
||||
"main": {"code": 1700, "label": "ڤ" }
|
||||
} },
|
||||
{ "code": 1607, "label": "ھ" },
|
||||
{ "code": 1688, "label": "ژ" },
|
||||
{ "code": 1604, "label": "ل" },
|
||||
{ "code": 1688, "label": "ژ", "popup": {
|
||||
"main": { "code": 1600, "label": "▬" }
|
||||
} },
|
||||
{ "code": 1604, "label": "ل", "popup": {
|
||||
"main": { "code": 1717, "label": "ڵ" }
|
||||
} },
|
||||
{ "code": 1705, "label": "ک" },
|
||||
{ "code": 1711, "label": "گ" }
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "kurdish_standard",
|
||||
"label": "کوردی - ستاندارد",
|
||||
"label": "کوردی (قڤفغ)",
|
||||
"authors": [ "GoRaN" ],
|
||||
"direction": "rtl",
|
||||
"modifier": "kurdish",
|
||||
@@ -10,16 +10,14 @@
|
||||
{ "code": 1602, "label": "ق", "popup": {
|
||||
"main": { "code": 1647, "label": "ٯ" }
|
||||
} },
|
||||
{ "code": 1700, "label": "ڤ", "popup": {
|
||||
"main": { "code": 1701, "label": "ڥ" }
|
||||
} },
|
||||
{ "code": 1601, "label": "ف", "popup": {
|
||||
"main": { "code": 1698, "label": "ڢ" }
|
||||
} },
|
||||
{ "code": 1700, "label": "ڤ" },
|
||||
{ "code": 1601, "label": "ف" },
|
||||
{ "code": 1594, "label": "غ" },
|
||||
{ "code": 1593, "label": "ع"},
|
||||
{ "code": 1607, "label": "ھ" },
|
||||
{ "code": 1749, "label": "ﻪ" },
|
||||
{ "code": 1749, "label": "ﻪ", "popup": {
|
||||
"main": { "code": 1577, "label": "ة" }
|
||||
} },
|
||||
|
||||
{ "code": 1578, "label": "ت", "popup": {
|
||||
"main": { "code": 1591, "label": "ط" }
|
||||
@@ -46,7 +44,9 @@
|
||||
} },
|
||||
{ "code": 1585, "label": "ر" },
|
||||
{ "code": 1685, "label": "ڕ" },
|
||||
{ "code": 1583, "label": "د" },
|
||||
{ "code": 1583, "label": "د", "popup": {
|
||||
"main": {"code": 1584, "label": "ذ" }
|
||||
} },
|
||||
{ "code": -255, "label": "وو" },
|
||||
{ "code": 1608, "label": "و" },
|
||||
{ "code": 1734, "label": "ۆ" },
|
||||
@@ -55,8 +55,10 @@
|
||||
|
||||
],
|
||||
[
|
||||
{ "code": 1600, "label": "kashida", "variation": "normal" },
|
||||
{ "code": 1574, "label": "ﺋ"},
|
||||
|
||||
{ "code": 1574, "label": "ﺋ", "popup": {
|
||||
"main": { "code": 1569, "label": "ء" }
|
||||
} },
|
||||
|
||||
{ "code": 1662, "label": "پ", "popup": {
|
||||
"main": { "code": 1579, "label": "ث" }
|
||||
|
||||
@@ -6,14 +6,17 @@
|
||||
"direction": "rtl",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 1600, "label": "kashida", "popup":
|
||||
{ "main": { "code": 8204, "label": "half_space" }
|
||||
} },
|
||||
{ "code": 0, "type": "placeholder" },
|
||||
{ "code": -5, "label": "delete", "type": "enter_editing" }
|
||||
],
|
||||
[
|
||||
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
|
||||
{ "$": "variation_selector",
|
||||
"default": { "code": 1567, "label": "؟", "groupId": 1 },
|
||||
"password": { "code": 1548, "label": "،", "groupId": 1 },
|
||||
"default": { "code": 1548, "label": "،", "groupId": 1 },
|
||||
"password": { "code": 35, "label": "#", "groupId": 1 },
|
||||
"email": { "code": 64, "label": "@", "groupId": 1 },
|
||||
"uri": { "code": 47, "label": "/", "groupId": 1 }
|
||||
},
|
||||
|
||||
53
app/src/main/assets/ime/text/characters/mod/neo2.json
Normal file
53
app/src/main/assets/ime/text/characters/mod/neo2.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"type": "characters/mod",
|
||||
"name": "neo2",
|
||||
"label": "Neo2",
|
||||
"authors": [ "ostrya" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": -1, "label": "shift", "type": "modifier" },
|
||||
{ "code": 0, "type": "placeholder" },
|
||||
{ "code": -5, "label": "delete", "type": "enter_editing" }
|
||||
],
|
||||
[
|
||||
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
|
||||
{ "code": -210, "label": "language_switch", "type": "system_gui" },
|
||||
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
|
||||
{ "code": 32, "label": "space" },
|
||||
{ "$": "variation_selector",
|
||||
"default": { "code": 44, "label": ",", "groupId": 1,
|
||||
"popup": {
|
||||
"main": { "code": 34, "label": "\"" },
|
||||
"relevant": [
|
||||
{ "code": 8211, "label": "–" }
|
||||
]
|
||||
} },
|
||||
"email": { "code": 64, "label": "@", "groupId": 1,
|
||||
"popup": {
|
||||
"relevant": [
|
||||
{ "code": 44, "label": "," }
|
||||
]
|
||||
} },
|
||||
"uri": { "code": 47, "label": "/", "groupId": 1,
|
||||
"popup": {
|
||||
"relevant": [
|
||||
{ "code": 44, "label": "," }
|
||||
]
|
||||
} }
|
||||
},
|
||||
{ "$": "variation_selector",
|
||||
"default": { "code": 46, "label": ".", "groupId": 2,
|
||||
"popup": {
|
||||
"relevant": [
|
||||
{ "code": 183, "label": "·" },
|
||||
{ "code": 39, "label": "'" }
|
||||
]
|
||||
} },
|
||||
"email": { "code": 46, "label": ".", "groupId": 2 },
|
||||
"uri": { "code": 46, "label": ".", "groupId": 2 }
|
||||
},
|
||||
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
|
||||
]
|
||||
]
|
||||
}
|
||||
61
app/src/main/assets/ime/text/characters/neo2.json
Normal file
61
app/src/main/assets/ime/text/characters/neo2.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "neo2",
|
||||
"label": "Neo2",
|
||||
"authors": [ "ostrya" ],
|
||||
"direction": "ltr",
|
||||
"modifier": "neo2",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 120, "label": "x" },
|
||||
{ "$": "auto_text_key", "code": 118, "label": "v" },
|
||||
{ "$": "auto_text_key", "code": 108, "label": "l" },
|
||||
{ "$": "auto_text_key", "code": 99, "label": "c" },
|
||||
{ "$": "auto_text_key", "code": 119, "label": "w" },
|
||||
{ "$": "auto_text_key", "code": 107, "label": "k" },
|
||||
{ "$": "auto_text_key", "code": 104, "label": "h" },
|
||||
{ "$": "auto_text_key", "code": 103, "label": "g" },
|
||||
{ "$": "auto_text_key", "code": 102, "label": "f" },
|
||||
{ "$": "auto_text_key", "code": 113, "label": "q" },
|
||||
{ "$": "case_selector",
|
||||
"lower": {
|
||||
"code": 223, "label": "ß", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 180, "label": "´" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"upper": {
|
||||
"code": 7838, "label": "ẞ", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 180, "label": "´" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 117, "label": "u" },
|
||||
{ "$": "auto_text_key", "code": 105, "label": "i" },
|
||||
{ "$": "auto_text_key", "code": 97, "label": "a" },
|
||||
{ "$": "auto_text_key", "code": 101, "label": "e" },
|
||||
{ "$": "auto_text_key", "code": 111, "label": "o" },
|
||||
{ "$": "auto_text_key", "code": 115, "label": "s" },
|
||||
{ "$": "auto_text_key", "code": 110, "label": "n" },
|
||||
{ "$": "auto_text_key", "code": 114, "label": "r" },
|
||||
{ "$": "auto_text_key", "code": 116, "label": "t" },
|
||||
{ "$": "auto_text_key", "code": 100, "label": "d" },
|
||||
{ "$": "auto_text_key", "code": 121, "label": "y" }
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 252, "label": "ü" },
|
||||
{ "$": "auto_text_key", "code": 246, "label": "ö" },
|
||||
{ "$": "auto_text_key", "code": 228, "label": "ä" },
|
||||
{ "$": "auto_text_key", "code": 112, "label": "p" },
|
||||
{ "$": "auto_text_key", "code": 122, "label": "z" },
|
||||
{ "$": "auto_text_key", "code": 98, "label": "b" },
|
||||
{ "$": "auto_text_key", "code": 109, "label": "m" },
|
||||
{ "$": "auto_text_key", "code": 106, "label": "j" }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/bengali.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/bengali.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"type": "numeric_row",
|
||||
"name": "bengali",
|
||||
"label": "Bengali",
|
||||
"authors": [ "yashpalgoyal1304" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 2535, "label": "১", "type": "numeric", "popup": {
|
||||
"main": { "code": 49, "label": "1" },
|
||||
"relevant": [
|
||||
{ "code": 8537, "label": "⅙" },
|
||||
{ "code": 8528, "label": "⅐" },
|
||||
{ "code": 8539, "label": "⅛" },
|
||||
{ "code": 8529, "label": "⅑" },
|
||||
{ "code": 8530, "label": "⅒" },
|
||||
{ "code": 185, "label": "¹" },
|
||||
{ "code": 189, "label": "½" },
|
||||
{ "code": 8531, "label": "⅓" },
|
||||
{ "code": 188, "label": "¼" },
|
||||
{ "code": 8533, "label": "⅕" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2536, "label": "২", "type": "numeric", "popup": {
|
||||
"main": { "code": 50, "label": "2" },
|
||||
"relevant": [
|
||||
{ "code": 8532, "label": "⅔" },
|
||||
{ "code": 178, "label": "²" },
|
||||
{ "code": 8534, "label": "⅖" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2537, "label": "৩", "type": "numeric", "popup": {
|
||||
"main": { "code": 51, "label": "3" },
|
||||
"relevant": [
|
||||
{ "code": 8535, "label": "⅗" },
|
||||
{ "code": 190, "label": "¾" },
|
||||
{ "code": 179, "label": "³" },
|
||||
{ "code": 8540, "label": "⅜" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2538, "label": "৪", "type": "numeric", "popup": {
|
||||
"main": { "code": 52, "label": "4" },
|
||||
"relevant": [
|
||||
{ "code": 8536, "label": "⅘" },
|
||||
{ "code": 8308, "label": "⁴" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2539, "label": "৫", "type": "numeric", "popup": {
|
||||
"main": { "code": 53, "label": "5" },
|
||||
"relevant": [
|
||||
{ "code": 8538, "label": "⅚" },
|
||||
{ "code": 8309, "label": "⁵" },
|
||||
{ "code": 8541, "label": "⅝" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2540, "label": "৬", "type": "numeric", "popup": {
|
||||
"main": { "code": 54, "label": "6" },
|
||||
"relevant": [
|
||||
{ "code": 8310, "label": "⁶" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2541, "label": "৭", "type": "numeric", "popup": {
|
||||
"main": { "code": 55, "label": "7" },
|
||||
"relevant": [
|
||||
{ "code": 8542, "label": "⅞" },
|
||||
{ "code": 8311, "label": "⁷" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2542, "label": "৮", "type": "numeric", "popup": {
|
||||
"main": { "code": 56, "label": "8" },
|
||||
"relevant": [
|
||||
{ "code": 8312, "label": "⁸" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2543, "label": "৯", "type": "numeric", "popup": {
|
||||
"main": { "code": 57, "label": "9" },
|
||||
"relevant": [
|
||||
{ "code": 8313, "label": "⁹" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2534, "label": "০", "type": "numeric", "popup": {
|
||||
"main": { "code": 48, "label": "0" },
|
||||
"relevant": [
|
||||
{ "code": 8319, "label": "ⁿ" },
|
||||
{ "code": 8709, "label": "∅" },
|
||||
{ "code": 8304, "label": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/devanagari.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/devanagari.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"type": "numeric_row",
|
||||
"name": "devanagari",
|
||||
"label": "Devanagari",
|
||||
"authors": [ "yashpalgoyal1304" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 2407, "label": "१", "type": "numeric", "popup": {
|
||||
"main": { "code": 49, "label": "1" },
|
||||
"relevant": [
|
||||
{ "code": 8537, "label": "⅙" },
|
||||
{ "code": 8528, "label": "⅐" },
|
||||
{ "code": 8539, "label": "⅛" },
|
||||
{ "code": 8529, "label": "⅑" },
|
||||
{ "code": 8530, "label": "⅒" },
|
||||
{ "code": 185, "label": "¹" },
|
||||
{ "code": 189, "label": "½" },
|
||||
{ "code": 8531, "label": "⅓" },
|
||||
{ "code": 188, "label": "¼" },
|
||||
{ "code": 8533, "label": "⅕" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2408, "label": "२", "type": "numeric", "popup": {
|
||||
"main": { "code": 50, "label": "2" },
|
||||
"relevant": [
|
||||
{ "code": 8532, "label": "⅔" },
|
||||
{ "code": 178, "label": "²" },
|
||||
{ "code": 8534, "label": "⅖" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2409, "label": "३", "type": "numeric", "popup": {
|
||||
"main": { "code": 51, "label": "3" },
|
||||
"relevant": [
|
||||
{ "code": 8535, "label": "⅗" },
|
||||
{ "code": 190, "label": "¾" },
|
||||
{ "code": 179, "label": "³" },
|
||||
{ "code": 8540, "label": "⅜" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2410, "label": "४", "type": "numeric", "popup": {
|
||||
"main": { "code": 52, "label": "4" },
|
||||
"relevant": [
|
||||
{ "code": 8536, "label": "⅘" },
|
||||
{ "code": 8308, "label": "⁴" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2411, "label": "५", "type": "numeric", "popup": {
|
||||
"main": { "code": 53, "label": "5" },
|
||||
"relevant": [
|
||||
{ "code": 8538, "label": "⅚" },
|
||||
{ "code": 8309, "label": "⁵" },
|
||||
{ "code": 8541, "label": "⅝" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2412, "label": "६", "type": "numeric", "popup": {
|
||||
"main": { "code": 54, "label": "6" },
|
||||
"relevant": [
|
||||
{ "code": 8310, "label": "⁶" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2413, "label": "७", "type": "numeric", "popup": {
|
||||
"main": { "code": 55, "label": "7" },
|
||||
"relevant": [
|
||||
{ "code": 8542, "label": "⅞" },
|
||||
{ "code": 8311, "label": "⁷" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2414, "label": "८", "type": "numeric", "popup": {
|
||||
"main": { "code": 56, "label": "8" },
|
||||
"relevant": [
|
||||
{ "code": 8312, "label": "⁸" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2415, "label": "९", "type": "numeric", "popup": {
|
||||
"main": { "code": 57, "label": "9" },
|
||||
"relevant": [
|
||||
{ "code": 8313, "label": "⁹" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2406, "label": "०", "type": "numeric", "popup": {
|
||||
"main": { "code": 48, "label": "0" },
|
||||
"relevant": [
|
||||
{ "code": 8319, "label": "ⁿ" },
|
||||
{ "code": 8709, "label": "∅" },
|
||||
{ "code": 8304, "label": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/gujarati.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/gujarati.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"type": "numeric_row",
|
||||
"name": "gujarati",
|
||||
"label": "Gujarati",
|
||||
"authors": [ "yashpalgoyal1304" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 2791, "label": "૧", "type": "numeric", "popup": {
|
||||
"main": { "code": 49, "label": "1" },
|
||||
"relevant": [
|
||||
{ "code": 8537, "label": "⅙" },
|
||||
{ "code": 8528, "label": "⅐" },
|
||||
{ "code": 8539, "label": "⅛" },
|
||||
{ "code": 8529, "label": "⅑" },
|
||||
{ "code": 8530, "label": "⅒" },
|
||||
{ "code": 185, "label": "¹" },
|
||||
{ "code": 189, "label": "½" },
|
||||
{ "code": 8531, "label": "⅓" },
|
||||
{ "code": 188, "label": "¼" },
|
||||
{ "code": 8533, "label": "⅕" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2792, "label": "૨", "type": "numeric", "popup": {
|
||||
"main": { "code": 50, "label": "2" },
|
||||
"relevant": [
|
||||
{ "code": 8532, "label": "⅔" },
|
||||
{ "code": 178, "label": "²" },
|
||||
{ "code": 8534, "label": "⅖" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2793, "label": "૩", "type": "numeric", "popup": {
|
||||
"main": { "code": 51, "label": "3" },
|
||||
"relevant": [
|
||||
{ "code": 8535, "label": "⅗" },
|
||||
{ "code": 190, "label": "¾" },
|
||||
{ "code": 179, "label": "³" },
|
||||
{ "code": 8540, "label": "⅜" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2794, "label": "૪", "type": "numeric", "popup": {
|
||||
"main": { "code": 52, "label": "4" },
|
||||
"relevant": [
|
||||
{ "code": 8536, "label": "⅘" },
|
||||
{ "code": 8308, "label": "⁴" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2795, "label": "૫", "type": "numeric", "popup": {
|
||||
"main": { "code": 53, "label": "5" },
|
||||
"relevant": [
|
||||
{ "code": 8538, "label": "⅚" },
|
||||
{ "code": 8309, "label": "⁵" },
|
||||
{ "code": 8541, "label": "⅝" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2796, "label": "૬", "type": "numeric", "popup": {
|
||||
"main": { "code": 54, "label": "6" },
|
||||
"relevant": [
|
||||
{ "code": 8310, "label": "⁶" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2797, "label": "૭", "type": "numeric", "popup": {
|
||||
"main": { "code": 55, "label": "7" },
|
||||
"relevant": [
|
||||
{ "code": 8542, "label": "⅞" },
|
||||
{ "code": 8311, "label": "⁷" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2798, "label": "૮", "type": "numeric", "popup": {
|
||||
"main": { "code": 56, "label": "8" },
|
||||
"relevant": [
|
||||
{ "code": 8312, "label": "⁸" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2799, "label": "૯", "type": "numeric", "popup": {
|
||||
"main": { "code": 57, "label": "9" },
|
||||
"relevant": [
|
||||
{ "code": 8313, "label": "⁹" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2790, "label": "૦", "type": "numeric", "popup": {
|
||||
"main": { "code": 48, "label": "0" },
|
||||
"relevant": [
|
||||
{ "code": 8319, "label": "ⁿ" },
|
||||
{ "code": 8709, "label": "∅" },
|
||||
{ "code": 8304, "label": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/gurmukhi.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/gurmukhi.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"type": "numeric_row",
|
||||
"name": "gurmukhi",
|
||||
"label": "Gurmukhi",
|
||||
"authors": [ "yashpalgoyal1304" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 2663, "label": "੧", "type": "numeric", "popup": {
|
||||
"main": { "code": 49, "label": "1" },
|
||||
"relevant": [
|
||||
{ "code": 8537, "label": "⅙" },
|
||||
{ "code": 8528, "label": "⅐" },
|
||||
{ "code": 8539, "label": "⅛" },
|
||||
{ "code": 8529, "label": "⅑" },
|
||||
{ "code": 8530, "label": "⅒" },
|
||||
{ "code": 185, "label": "¹" },
|
||||
{ "code": 189, "label": "½" },
|
||||
{ "code": 8531, "label": "⅓" },
|
||||
{ "code": 188, "label": "¼" },
|
||||
{ "code": 8533, "label": "⅕" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2664, "label": "੨", "type": "numeric", "popup": {
|
||||
"main": { "code": 50, "label": "2" },
|
||||
"relevant": [
|
||||
{ "code": 8532, "label": "⅔" },
|
||||
{ "code": 178, "label": "²" },
|
||||
{ "code": 8534, "label": "⅖" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2665, "label": "੩", "type": "numeric", "popup": {
|
||||
"main": { "code": 51, "label": "3" },
|
||||
"relevant": [
|
||||
{ "code": 8535, "label": "⅗" },
|
||||
{ "code": 190, "label": "¾" },
|
||||
{ "code": 179, "label": "³" },
|
||||
{ "code": 8540, "label": "⅜" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2666, "label": "੪", "type": "numeric", "popup": {
|
||||
"main": { "code": 52, "label": "4" },
|
||||
"relevant": [
|
||||
{ "code": 8536, "label": "⅘" },
|
||||
{ "code": 8308, "label": "⁴" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2667, "label": "੫", "type": "numeric", "popup": {
|
||||
"main": { "code": 53, "label": "5" },
|
||||
"relevant": [
|
||||
{ "code": 8538, "label": "⅚" },
|
||||
{ "code": 8309, "label": "⁵" },
|
||||
{ "code": 8541, "label": "⅝" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2668, "label": "੬", "type": "numeric", "popup": {
|
||||
"main": { "code": 54, "label": "6" },
|
||||
"relevant": [
|
||||
{ "code": 8310, "label": "⁶" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2669, "label": "੭", "type": "numeric", "popup": {
|
||||
"main": { "code": 55, "label": "7" },
|
||||
"relevant": [
|
||||
{ "code": 8542, "label": "⅞" },
|
||||
{ "code": 8311, "label": "⁷" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2670, "label": "੮", "type": "numeric", "popup": {
|
||||
"main": { "code": 56, "label": "8" },
|
||||
"relevant": [
|
||||
{ "code": 8312, "label": "⁸" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2671, "label": "੯", "type": "numeric", "popup": {
|
||||
"main": { "code": 57, "label": "9" },
|
||||
"relevant": [
|
||||
{ "code": 8313, "label": "⁹" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2662, "label": "੦", "type": "numeric", "popup": {
|
||||
"main": { "code": 48, "label": "0" },
|
||||
"relevant": [
|
||||
{ "code": 8319, "label": "ⁿ" },
|
||||
{ "code": 8709, "label": "∅" },
|
||||
{ "code": 8304, "label": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/kannada.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/kannada.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"type": "numeric_row",
|
||||
"name": "kannada",
|
||||
"label": "Kannada",
|
||||
"authors": [ "yashpalgoyal1304" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 3303, "label": "೧", "type": "numeric", "popup": {
|
||||
"main": { "code": 49, "label": "1" },
|
||||
"relevant": [
|
||||
{ "code": 8537, "label": "⅙" },
|
||||
{ "code": 8528, "label": "⅐" },
|
||||
{ "code": 8539, "label": "⅛" },
|
||||
{ "code": 8529, "label": "⅑" },
|
||||
{ "code": 8530, "label": "⅒" },
|
||||
{ "code": 185, "label": "¹" },
|
||||
{ "code": 189, "label": "½" },
|
||||
{ "code": 8531, "label": "⅓" },
|
||||
{ "code": 188, "label": "¼" },
|
||||
{ "code": 8533, "label": "⅕" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3304, "label": "೨", "type": "numeric", "popup": {
|
||||
"main": { "code": 50, "label": "2" },
|
||||
"relevant": [
|
||||
{ "code": 8532, "label": "⅔" },
|
||||
{ "code": 178, "label": "²" },
|
||||
{ "code": 8534, "label": "⅖" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3305, "label": "೩", "type": "numeric", "popup": {
|
||||
"main": { "code": 51, "label": "3" },
|
||||
"relevant": [
|
||||
{ "code": 8535, "label": "⅗" },
|
||||
{ "code": 190, "label": "¾" },
|
||||
{ "code": 179, "label": "³" },
|
||||
{ "code": 8540, "label": "⅜" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3306, "label": "೪", "type": "numeric", "popup": {
|
||||
"main": { "code": 52, "label": "4" },
|
||||
"relevant": [
|
||||
{ "code": 8536, "label": "⅘" },
|
||||
{ "code": 8308, "label": "⁴" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3307, "label": "೫", "type": "numeric", "popup": {
|
||||
"main": { "code": 53, "label": "5" },
|
||||
"relevant": [
|
||||
{ "code": 8538, "label": "⅚" },
|
||||
{ "code": 8309, "label": "⁵" },
|
||||
{ "code": 8541, "label": "⅝" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3308, "label": "೬", "type": "numeric", "popup": {
|
||||
"main": { "code": 54, "label": "6" },
|
||||
"relevant": [
|
||||
{ "code": 8310, "label": "⁶" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3309, "label": "೭", "type": "numeric", "popup": {
|
||||
"main": { "code": 55, "label": "7" },
|
||||
"relevant": [
|
||||
{ "code": 8542, "label": "⅞" },
|
||||
{ "code": 8311, "label": "⁷" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3310, "label": "೮", "type": "numeric", "popup": {
|
||||
"main": { "code": 56, "label": "8" },
|
||||
"relevant": [
|
||||
{ "code": 8312, "label": "⁸" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3311, "label": "೯", "type": "numeric", "popup": {
|
||||
"main": { "code": 57, "label": "9" },
|
||||
"relevant": [
|
||||
{ "code": 8313, "label": "⁹" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3302, "label": "೦", "type": "numeric", "popup": {
|
||||
"main": { "code": 48, "label": "0" },
|
||||
"relevant": [
|
||||
{ "code": 8319, "label": "ⁿ" },
|
||||
{ "code": 8709, "label": "∅" },
|
||||
{ "code": 8304, "label": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/malayalam.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/malayalam.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"type": "numeric_row",
|
||||
"name": "malayalam",
|
||||
"label": "Malayalam",
|
||||
"authors": [ "yashpalgoyal1304" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 3431, "label": "൧", "type": "numeric", "popup": {
|
||||
"main": { "code": 49, "label": "1" },
|
||||
"relevant": [
|
||||
{ "code": 8537, "label": "⅙" },
|
||||
{ "code": 8528, "label": "⅐" },
|
||||
{ "code": 8539, "label": "⅛" },
|
||||
{ "code": 8529, "label": "⅑" },
|
||||
{ "code": 8530, "label": "⅒" },
|
||||
{ "code": 185, "label": "¹" },
|
||||
{ "code": 189, "label": "½" },
|
||||
{ "code": 8531, "label": "⅓" },
|
||||
{ "code": 188, "label": "¼" },
|
||||
{ "code": 8533, "label": "⅕" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3432, "label": "൨", "type": "numeric", "popup": {
|
||||
"main": { "code": 50, "label": "2" },
|
||||
"relevant": [
|
||||
{ "code": 8532, "label": "⅔" },
|
||||
{ "code": 178, "label": "²" },
|
||||
{ "code": 8534, "label": "⅖" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3433, "label": "൩", "type": "numeric", "popup": {
|
||||
"main": { "code": 51, "label": "3" },
|
||||
"relevant": [
|
||||
{ "code": 8535, "label": "⅗" },
|
||||
{ "code": 190, "label": "¾" },
|
||||
{ "code": 179, "label": "³" },
|
||||
{ "code": 8540, "label": "⅜" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3434, "label": "൪", "type": "numeric", "popup": {
|
||||
"main": { "code": 52, "label": "4" },
|
||||
"relevant": [
|
||||
{ "code": 8536, "label": "⅘" },
|
||||
{ "code": 8308, "label": "⁴" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3435, "label": "൫", "type": "numeric", "popup": {
|
||||
"main": { "code": 53, "label": "5" },
|
||||
"relevant": [
|
||||
{ "code": 8538, "label": "⅚" },
|
||||
{ "code": 8309, "label": "⁵" },
|
||||
{ "code": 8541, "label": "⅝" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3436, "label": "൬", "type": "numeric", "popup": {
|
||||
"main": { "code": 54, "label": "6" },
|
||||
"relevant": [
|
||||
{ "code": 8310, "label": "⁶" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3437, "label": "൭", "type": "numeric", "popup": {
|
||||
"main": { "code": 55, "label": "7" },
|
||||
"relevant": [
|
||||
{ "code": 8542, "label": "⅞" },
|
||||
{ "code": 8311, "label": "⁷" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3438, "label": "൮", "type": "numeric", "popup": {
|
||||
"main": { "code": 56, "label": "8" },
|
||||
"relevant": [
|
||||
{ "code": 8312, "label": "⁸" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3439, "label": "൯", "type": "numeric", "popup": {
|
||||
"main": { "code": 57, "label": "9" },
|
||||
"relevant": [
|
||||
{ "code": 8313, "label": "⁹" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3430, "label": "൦", "type": "numeric", "popup": {
|
||||
"main": { "code": 48, "label": "0" },
|
||||
"relevant": [
|
||||
{ "code": 8319, "label": "ⁿ" },
|
||||
{ "code": 8709, "label": "∅" },
|
||||
{ "code": 8304, "label": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
80
app/src/main/assets/ime/text/numeric/row/neo2.json
Normal file
80
app/src/main/assets/ime/text/numeric/row/neo2.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"type": "numeric_row",
|
||||
"name": "neo2",
|
||||
"label": "Neo2",
|
||||
"authors": [ "ostrya" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 49, "label": "1", "type": "numeric", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 176, "label": "°" },
|
||||
{ "code": 185, "label": "¹" }
|
||||
]
|
||||
} },
|
||||
{ "code": 50, "label": "2", "type": "numeric", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 167, "label": "§" },
|
||||
{ "code": 178, "label": "²" }
|
||||
]
|
||||
} },
|
||||
{ "code": 51, "label": "3", "type": "numeric", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 8467, "label": "ℓ" },
|
||||
{ "code": 179, "label": "³" }
|
||||
]
|
||||
} },
|
||||
{ "code": 52, "label": "4", "type": "numeric", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 187, "label": "»" },
|
||||
{ "code": 8250, "label": "›" }
|
||||
]
|
||||
} },
|
||||
{ "code": 53, "label": "5", "type": "numeric", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 171, "label": "«" },
|
||||
{ "code": 8249, "label": "‹" }
|
||||
]
|
||||
} },
|
||||
{ "code": 54, "label": "6", "type": "numeric", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 36, "label": "$" },
|
||||
{ "code": 162, "label": "¢" }
|
||||
]
|
||||
} },
|
||||
{ "code": 55, "label": "7", "type": "numeric", "popup": {
|
||||
"main": { "code": -801, "label": "currency_slot_1" },
|
||||
"relevant": [
|
||||
{ "code": -802, "label": "currency_slot_2" },
|
||||
{ "code": -803, "label": "currency_slot_3" },
|
||||
{ "code": -804, "label": "currency_slot_4" },
|
||||
{ "code": -805, "label": "currency_slot_5" },
|
||||
{ "code": -806, "label": "currency_slot_6" }
|
||||
]
|
||||
} },
|
||||
{ "code": 56, "label": "8", "type": "numeric", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 8222, "label": "„" },
|
||||
{ "code": 8218, "label": "‚" }
|
||||
]
|
||||
} },
|
||||
{ "code": 57, "label": "9", "type": "numeric", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 8220, "label": "“" },
|
||||
{ "code": 8216, "label": "‘" }
|
||||
]
|
||||
} },
|
||||
{ "code": 48, "label": "0", "type": "numeric", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 8221, "label": "”" },
|
||||
{ "code": 8217, "label": "’" }
|
||||
]
|
||||
} },
|
||||
{ "code": 45, "label": "-", "type": "numeric", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 8212, "label": "—" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/oriya.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/oriya.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"type": "numeric_row",
|
||||
"name": "oriya",
|
||||
"label": "Odia",
|
||||
"authors": [ "yashpalgoyal1304" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 2919, "label": "୧", "type": "numeric", "popup": {
|
||||
"main": { "code": 49, "label": "1" },
|
||||
"relevant": [
|
||||
{ "code": 8537, "label": "⅙" },
|
||||
{ "code": 8528, "label": "⅐" },
|
||||
{ "code": 8539, "label": "⅛" },
|
||||
{ "code": 8529, "label": "⅑" },
|
||||
{ "code": 8530, "label": "⅒" },
|
||||
{ "code": 185, "label": "¹" },
|
||||
{ "code": 189, "label": "½" },
|
||||
{ "code": 8531, "label": "⅓" },
|
||||
{ "code": 188, "label": "¼" },
|
||||
{ "code": 8533, "label": "⅕" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2920, "label": "୨", "type": "numeric", "popup": {
|
||||
"main": { "code": 50, "label": "2" },
|
||||
"relevant": [
|
||||
{ "code": 8532, "label": "⅔" },
|
||||
{ "code": 178, "label": "²" },
|
||||
{ "code": 8534, "label": "⅖" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2921, "label": "୩", "type": "numeric", "popup": {
|
||||
"main": { "code": 51, "label": "3" },
|
||||
"relevant": [
|
||||
{ "code": 8535, "label": "⅗" },
|
||||
{ "code": 190, "label": "¾" },
|
||||
{ "code": 179, "label": "³" },
|
||||
{ "code": 8540, "label": "⅜" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2922, "label": "୪", "type": "numeric", "popup": {
|
||||
"main": { "code": 52, "label": "4" },
|
||||
"relevant": [
|
||||
{ "code": 8536, "label": "⅘" },
|
||||
{ "code": 8308, "label": "⁴" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2923, "label": "୫", "type": "numeric", "popup": {
|
||||
"main": { "code": 53, "label": "5" },
|
||||
"relevant": [
|
||||
{ "code": 8538, "label": "⅚" },
|
||||
{ "code": 8309, "label": "⁵" },
|
||||
{ "code": 8541, "label": "⅝" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2924, "label": "୬", "type": "numeric", "popup": {
|
||||
"main": { "code": 54, "label": "6" },
|
||||
"relevant": [
|
||||
{ "code": 8310, "label": "⁶" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2925, "label": "୭", "type": "numeric", "popup": {
|
||||
"main": { "code": 55, "label": "7" },
|
||||
"relevant": [
|
||||
{ "code": 8542, "label": "⅞" },
|
||||
{ "code": 8311, "label": "⁷" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2926, "label": "୮", "type": "numeric", "popup": {
|
||||
"main": { "code": 56, "label": "8" },
|
||||
"relevant": [
|
||||
{ "code": 8312, "label": "⁸" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2927, "label": "୯", "type": "numeric", "popup": {
|
||||
"main": { "code": 57, "label": "9" },
|
||||
"relevant": [
|
||||
{ "code": 8313, "label": "⁹" }
|
||||
]
|
||||
} },
|
||||
{ "code": 2918, "label": "୦", "type": "numeric", "popup": {
|
||||
"main": { "code": 48, "label": "0" },
|
||||
"relevant": [
|
||||
{ "code": 8319, "label": "ⁿ" },
|
||||
{ "code": 8709, "label": "∅" },
|
||||
{ "code": 8304, "label": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/tamil.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/tamil.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"type": "numeric_row",
|
||||
"name": "tamil",
|
||||
"label": "Tamil",
|
||||
"authors": [ "yashpalgoyal1304" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 3047, "label": "௧", "type": "numeric", "popup": {
|
||||
"main": { "code": 49, "label": "1" },
|
||||
"relevant": [
|
||||
{ "code": 8537, "label": "⅙" },
|
||||
{ "code": 8528, "label": "⅐" },
|
||||
{ "code": 8539, "label": "⅛" },
|
||||
{ "code": 8529, "label": "⅑" },
|
||||
{ "code": 8530, "label": "⅒" },
|
||||
{ "code": 185, "label": "¹" },
|
||||
{ "code": 189, "label": "½" },
|
||||
{ "code": 8531, "label": "⅓" },
|
||||
{ "code": 188, "label": "¼" },
|
||||
{ "code": 8533, "label": "⅕" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3048, "label": "௨", "type": "numeric", "popup": {
|
||||
"main": { "code": 50, "label": "2" },
|
||||
"relevant": [
|
||||
{ "code": 8532, "label": "⅔" },
|
||||
{ "code": 178, "label": "²" },
|
||||
{ "code": 8534, "label": "⅖" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3049, "label": "௩", "type": "numeric", "popup": {
|
||||
"main": { "code": 51, "label": "3" },
|
||||
"relevant": [
|
||||
{ "code": 8535, "label": "⅗" },
|
||||
{ "code": 190, "label": "¾" },
|
||||
{ "code": 179, "label": "³" },
|
||||
{ "code": 8540, "label": "⅜" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3050, "label": "௪", "type": "numeric", "popup": {
|
||||
"main": { "code": 52, "label": "4" },
|
||||
"relevant": [
|
||||
{ "code": 8536, "label": "⅘" },
|
||||
{ "code": 8308, "label": "⁴" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3051, "label": "௫", "type": "numeric", "popup": {
|
||||
"main": { "code": 53, "label": "5" },
|
||||
"relevant": [
|
||||
{ "code": 8538, "label": "⅚" },
|
||||
{ "code": 8309, "label": "⁵" },
|
||||
{ "code": 8541, "label": "⅝" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3052, "label": "௬", "type": "numeric", "popup": {
|
||||
"main": { "code": 54, "label": "6" },
|
||||
"relevant": [
|
||||
{ "code": 8310, "label": "⁶" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3053, "label": "௭", "type": "numeric", "popup": {
|
||||
"main": { "code": 55, "label": "7" },
|
||||
"relevant": [
|
||||
{ "code": 8542, "label": "⅞" },
|
||||
{ "code": 8311, "label": "⁷" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3054, "label": "௮", "type": "numeric", "popup": {
|
||||
"main": { "code": 56, "label": "8" },
|
||||
"relevant": [
|
||||
{ "code": 8312, "label": "⁸" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3055, "label": "௯", "type": "numeric", "popup": {
|
||||
"main": { "code": 57, "label": "9" },
|
||||
"relevant": [
|
||||
{ "code": 8313, "label": "⁹" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3046, "label": "௦", "type": "numeric", "popup": {
|
||||
"main": { "code": 48, "label": "0" },
|
||||
"relevant": [
|
||||
{ "code": 8319, "label": "ⁿ" },
|
||||
{ "code": 8709, "label": "∅" },
|
||||
{ "code": 8304, "label": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
91
app/src/main/assets/ime/text/numeric/row/telugu.json
Normal file
91
app/src/main/assets/ime/text/numeric/row/telugu.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"type": "numeric_row",
|
||||
"name": "telugu",
|
||||
"label": "Telugu",
|
||||
"authors": [ "yashpalgoyal1304" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 3175, "label": "౧", "type": "numeric", "popup": {
|
||||
"main": { "code": 49, "label": "1" },
|
||||
"relevant": [
|
||||
{ "code": 8537, "label": "⅙" },
|
||||
{ "code": 8528, "label": "⅐" },
|
||||
{ "code": 8539, "label": "⅛" },
|
||||
{ "code": 8529, "label": "⅑" },
|
||||
{ "code": 8530, "label": "⅒" },
|
||||
{ "code": 185, "label": "¹" },
|
||||
{ "code": 189, "label": "½" },
|
||||
{ "code": 8531, "label": "⅓" },
|
||||
{ "code": 188, "label": "¼" },
|
||||
{ "code": 8533, "label": "⅕" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3176, "label": "౨", "type": "numeric", "popup": {
|
||||
"main": { "code": 50, "label": "2" },
|
||||
"relevant": [
|
||||
{ "code": 8532, "label": "⅔" },
|
||||
{ "code": 178, "label": "²" },
|
||||
{ "code": 8534, "label": "⅖" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3177, "label": "౩", "type": "numeric", "popup": {
|
||||
"main": { "code": 51, "label": "3" },
|
||||
"relevant": [
|
||||
{ "code": 8535, "label": "⅗" },
|
||||
{ "code": 190, "label": "¾" },
|
||||
{ "code": 179, "label": "³" },
|
||||
{ "code": 8540, "label": "⅜" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3178, "label": "౪", "type": "numeric", "popup": {
|
||||
"main": { "code": 52, "label": "4" },
|
||||
"relevant": [
|
||||
{ "code": 8536, "label": "⅘" },
|
||||
{ "code": 8308, "label": "⁴" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3179, "label": "౫", "type": "numeric", "popup": {
|
||||
"main": { "code": 53, "label": "5" },
|
||||
"relevant": [
|
||||
{ "code": 8538, "label": "⅚" },
|
||||
{ "code": 8309, "label": "⁵" },
|
||||
{ "code": 8541, "label": "⅝" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3180, "label": "౬", "type": "numeric", "popup": {
|
||||
"main": { "code": 54, "label": "6" },
|
||||
"relevant": [
|
||||
{ "code": 8310, "label": "⁶" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3181, "label": "౭", "type": "numeric", "popup": {
|
||||
"main": { "code": 55, "label": "7" },
|
||||
"relevant": [
|
||||
{ "code": 8542, "label": "⅞" },
|
||||
{ "code": 8311, "label": "⁷" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3182, "label": "౮", "type": "numeric", "popup": {
|
||||
"main": { "code": 56, "label": "8" },
|
||||
"relevant": [
|
||||
{ "code": 8312, "label": "⁸" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3183, "label": "౯", "type": "numeric", "popup": {
|
||||
"main": { "code": 57, "label": "9" },
|
||||
"relevant": [
|
||||
{ "code": 8313, "label": "⁹" }
|
||||
]
|
||||
} },
|
||||
{ "code": 3174, "label": "౦", "type": "numeric", "popup": {
|
||||
"main": { "code": 48, "label": "0" },
|
||||
"relevant": [
|
||||
{ "code": 8319, "label": "ⁿ" },
|
||||
{ "code": 8709, "label": "∅" },
|
||||
{ "code": 8304, "label": "⁰" }
|
||||
]
|
||||
} }
|
||||
]
|
||||
]
|
||||
}
|
||||
22
app/src/main/assets/ime/text/symbols/mod/neo2.json
Normal file
22
app/src/main/assets/ime/text/symbols/mod/neo2.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"type": "symbols/mod",
|
||||
"name": "neo2",
|
||||
"label": "Neo2",
|
||||
"authors": [ "ostrya" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": -203, "label": "view_symbols2", "type": "system_gui" },
|
||||
{ "code": 0, "type": "placeholder" },
|
||||
{ "code": -5, "label": "delete", "type": "enter_editing" }
|
||||
],
|
||||
[
|
||||
{ "code": -201, "label": "view_characters", "type": "system_gui" },
|
||||
{ "code": -205, "label": "view_numeric_advanced", "type": "system_gui" },
|
||||
{ "code": 32, "label": "space" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
|
||||
]
|
||||
]
|
||||
}
|
||||
46
app/src/main/assets/ime/text/symbols/neo2.json
Normal file
46
app/src/main/assets/ime/text/symbols/neo2.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"type": "symbols",
|
||||
"name": "neo2",
|
||||
"label": "Neo2",
|
||||
"authors": [ "ostrya" ],
|
||||
"direction": "ltr",
|
||||
"modifier": "neo2",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 8230, "label": "…" },
|
||||
{ "code": 95, "label": "_" },
|
||||
{ "code": 91, "label": "[" },
|
||||
{ "code": 93, "label": "]" },
|
||||
{ "code": 94, "label": "^" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 60, "label": "<" },
|
||||
{ "code": 62, "label": ">" },
|
||||
{ "code": 61, "label": "=" },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 383, "label": "ſ" }
|
||||
],
|
||||
[
|
||||
{ "code": 92, "label": "\\" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 123, "label": "{" },
|
||||
{ "code": 125, "label": "}" },
|
||||
{ "code": 42, "label": "*" },
|
||||
{ "code": 63, "label": "?" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 64, "label": "@" }
|
||||
],
|
||||
[
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 36, "label": "$" },
|
||||
{ "code": 124, "label": "|" },
|
||||
{ "code": 126, "label": "~" },
|
||||
{ "code": 96, "label": "`" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 59, "label": ";" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -503,24 +503,6 @@ SOFTWARE.
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>kotlin-result</h3>
|
||||
<span>Copyright (c) 2017-2020 Michael Bull (https://www.michael-bull.com)</span>
|
||||
<pre>
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
</pre>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Material Icons</h3>
|
||||
<span>Copyright 2018 Google LLC</span>
|
||||
<pre>
|
||||
@@ -729,24 +711,6 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Moshi</h3>
|
||||
<span>Copyright 2015 Square, Inc.</span>
|
||||
<pre>
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
</pre>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Timber</h3>
|
||||
<span>Copyright 2013 Jake Wharton</span>
|
||||
<pre>
|
||||
|
||||
38
app/src/main/cpp/CMakeLists.txt
Normal file
38
app/src/main/cpp/CMakeLists.txt
Normal file
@@ -0,0 +1,38 @@
|
||||
# For more information about using CMake with Android Studio, read the
|
||||
# documentation: https://d.android.com/studio/projects/add-native-code.html
|
||||
|
||||
cmake_minimum_required(VERSION 3.10.2)
|
||||
|
||||
project("florisboard")
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
add_subdirectory(ime/nlp)
|
||||
|
||||
add_library(
|
||||
# Name
|
||||
florisboard-native
|
||||
|
||||
# Type
|
||||
SHARED
|
||||
|
||||
# Sources
|
||||
dev_patrickgold_florisboard_ime_nlp_SuggestionList.cpp
|
||||
)
|
||||
|
||||
find_library(
|
||||
# Save to var
|
||||
log-lib
|
||||
|
||||
# Original name
|
||||
log
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
# Destination
|
||||
florisboard-native
|
||||
|
||||
# Sources
|
||||
${log-lib}
|
||||
ime-nlp
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <jni.h>
|
||||
#include "ime/nlp/suggestion_list.h"
|
||||
|
||||
#pragma ide diagnostic ignored "UnusedLocalVariable"
|
||||
|
||||
using namespace ime::nlp;
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeInitialize(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jint max_size) {
|
||||
auto *suggestionList = new SuggestionList(max_size);
|
||||
return reinterpret_cast<jlong>(suggestionList);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeDispose(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr) {
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
suggestionList->clear();
|
||||
delete suggestionList;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeAdd(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr,
|
||||
jstring word,
|
||||
jint freq) {
|
||||
const char *cWord = env->GetStringUTFChars(word, nullptr);
|
||||
word_t stdWord = word_t(cWord);
|
||||
env->ReleaseStringUTFChars(word, cWord);
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
return suggestionList->add(std::move(stdWord), freq);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeClear(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr) {
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
suggestionList->clear();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeContains(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr,
|
||||
jstring element) {
|
||||
const char *cWord = env->GetStringUTFChars(element, nullptr);
|
||||
const word_t stdWord = word_t(cWord);
|
||||
env->ReleaseStringUTFChars(element, cWord);
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
return suggestionList->containsWord(stdWord);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeGetOrNull(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr,
|
||||
jint index) {
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
auto weightedToken = suggestionList->get(index);
|
||||
if (weightedToken == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return env->NewStringUTF(weightedToken->data.c_str());
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeSize(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr) {
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
return suggestionList->size();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeGetIsPrimaryTokenAutoInsert(
|
||||
JNIEnv *env, jobject thiz, jlong native_ptr) {
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
return suggestionList->isPrimaryTokenAutoInsert;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeSetIsPrimaryTokenAutoInsert(
|
||||
JNIEnv *env, jobject thiz, jlong native_ptr, jboolean v) {
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
suggestionList->isPrimaryTokenAutoInsert = v;
|
||||
}
|
||||
0
app/src/main/cpp/ime/dummy
Normal file
0
app/src/main/cpp/ime/dummy
Normal file
13
app/src/main/cpp/ime/nlp/CMakeLists.txt
Normal file
13
app/src/main/cpp/ime/nlp/CMakeLists.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
add_library(
|
||||
# Name
|
||||
ime-nlp
|
||||
|
||||
# Headers
|
||||
nlp.h
|
||||
token.h
|
||||
suggestion_list.h
|
||||
|
||||
# Sources
|
||||
token.cpp
|
||||
suggestion_list.cpp
|
||||
)
|
||||
32
app/src/main/cpp/ime/nlp/nlp.h
Normal file
32
app/src/main/cpp/ime/nlp/nlp.h
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef FLORISBOARD_NLP_H
|
||||
#define FLORISBOARD_NLP_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ime::nlp {
|
||||
|
||||
typedef std::string word_t;
|
||||
typedef uint16_t freq_t;
|
||||
|
||||
static const freq_t FREQ_VALUE_MASK = 0xFF;
|
||||
static const freq_t FREQ_POSSIBLY_OFFENSIVE = 0x01;
|
||||
|
||||
} // namespace ime::nlp
|
||||
|
||||
#endif // FLORISBOARD_NLP_H
|
||||
98
app/src/main/cpp/ime/nlp/suggestion_list.cpp
Normal file
98
app/src/main/cpp/ime/nlp/suggestion_list.cpp
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "suggestion_list.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
using namespace ime::nlp;
|
||||
|
||||
SuggestionList::SuggestionList(size_t _maxSize) :
|
||||
maxSize(_maxSize), internalSize(0), tokens(_maxSize), isPrimaryTokenAutoInsert(false)
|
||||
{ }
|
||||
|
||||
SuggestionList::~SuggestionList() = default;
|
||||
|
||||
bool SuggestionList::add(word_t &&word, freq_t &&freq) {
|
||||
auto entryIndex = indexOfWord(word);
|
||||
if (entryIndex.has_value()) {
|
||||
// Word exists already
|
||||
auto entry = tokens[entryIndex.value()];
|
||||
if (entry.freq < freq) {
|
||||
// Need to update freq
|
||||
entry.freq = freq;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (internalSize < maxSize) {
|
||||
tokens[internalSize++] = WeightedToken(std::move(word), freq);
|
||||
} else {
|
||||
auto last = tokens[internalSize - 1];
|
||||
if (last.freq < freq) {
|
||||
tokens[internalSize - 1] = WeightedToken(std::move(word), freq);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(tokens.begin(), tokens.begin() + internalSize, std::greater<>());
|
||||
return true;
|
||||
}
|
||||
|
||||
void SuggestionList::clear() {
|
||||
internalSize = 0;
|
||||
isPrimaryTokenAutoInsert = false;
|
||||
}
|
||||
|
||||
bool SuggestionList::contains(const WeightedToken &element) const {
|
||||
return indexOf(element).has_value();
|
||||
}
|
||||
|
||||
bool SuggestionList::containsWord(const word_t &word) const {
|
||||
return indexOfWord(word).has_value();
|
||||
}
|
||||
|
||||
const WeightedToken *SuggestionList::get(size_t index) const {
|
||||
if (index < 0 || index >= internalSize) return nullptr;
|
||||
return &tokens[index];
|
||||
}
|
||||
|
||||
std::optional<size_t> SuggestionList::indexOf(const WeightedToken &element) const {
|
||||
for (size_t n = 0; n < internalSize; n++) {
|
||||
if (element == tokens[n]) {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<size_t> SuggestionList::indexOfWord(const word_t &word) const {
|
||||
for (size_t n = 0; n < internalSize; n++) {
|
||||
if (word == tokens[n].data) {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool SuggestionList::isEmpty() const {
|
||||
return internalSize == 0;
|
||||
}
|
||||
|
||||
size_t SuggestionList::size() const {
|
||||
return internalSize;
|
||||
}
|
||||
51
app/src/main/cpp/ime/nlp/suggestion_list.h
Normal file
51
app/src/main/cpp/ime/nlp/suggestion_list.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef FLORISBOARD_SUGGESTION_LIST_H
|
||||
#define FLORISBOARD_SUGGESTION_LIST_H
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include "token.h"
|
||||
|
||||
namespace ime::nlp {
|
||||
|
||||
class SuggestionList {
|
||||
public:
|
||||
SuggestionList(size_t _maxSize);
|
||||
~SuggestionList();
|
||||
|
||||
bool add(word_t &&word, freq_t &&freq);
|
||||
void clear();
|
||||
bool contains(const WeightedToken &element) const;
|
||||
bool containsWord(const word_t &word) const;
|
||||
const WeightedToken *get(size_t index) const;
|
||||
std::optional<size_t> indexOf(const WeightedToken &element) const;
|
||||
std::optional<size_t> indexOfWord(const word_t &word) const;
|
||||
bool isEmpty() const;
|
||||
size_t size() const;
|
||||
|
||||
bool isPrimaryTokenAutoInsert;
|
||||
|
||||
private:
|
||||
std::vector<WeightedToken> tokens;
|
||||
size_t internalSize;
|
||||
size_t maxSize;
|
||||
};
|
||||
|
||||
} // namespace ime::nlp
|
||||
|
||||
#endif // FLORISBOARD_SUGGESTION_LIST_H
|
||||
61
app/src/main/cpp/ime/nlp/token.cpp
Normal file
61
app/src/main/cpp/ime/nlp/token.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "token.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace ime::nlp {
|
||||
|
||||
Token::Token() : data() {}
|
||||
Token::Token(word_t &&_data) : data(std::move(_data)) {}
|
||||
|
||||
bool operator==(const Token &t1, const Token &t2) {
|
||||
return t1.data == t2.data;
|
||||
}
|
||||
|
||||
bool operator!=(const Token &t1, const Token &t2) {
|
||||
return !(t1 == t2);
|
||||
}
|
||||
|
||||
WeightedToken::WeightedToken() : Token(), freq(0) {}
|
||||
WeightedToken::WeightedToken(word_t &&_data, freq_t _freq) : Token(std::move(_data)), freq(_freq) {}
|
||||
|
||||
bool operator==(const WeightedToken &t1, const WeightedToken &t2) {
|
||||
return t1.data == t2.data && t1.freq == t2.freq;
|
||||
}
|
||||
|
||||
bool operator!=(const WeightedToken &t1, const WeightedToken &t2) {
|
||||
return !(t1 == t2);
|
||||
}
|
||||
|
||||
bool operator<(const WeightedToken &t1, const WeightedToken &t2) {
|
||||
return t1.freq < t2.freq;
|
||||
}
|
||||
|
||||
bool operator<=(const WeightedToken &t1, const WeightedToken &t2) {
|
||||
return t1.freq <= t2.freq;
|
||||
}
|
||||
|
||||
bool operator>(const WeightedToken &t1, const WeightedToken &t2) {
|
||||
return t1.freq > t2.freq;
|
||||
}
|
||||
|
||||
bool operator>=(const WeightedToken &t1, const WeightedToken &t2) {
|
||||
return t1.freq >= t2.freq;
|
||||
}
|
||||
|
||||
} // namespace ime::nlp
|
||||
51
app/src/main/cpp/ime/nlp/token.h
Normal file
51
app/src/main/cpp/ime/nlp/token.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef FLORISBOARD_TOKEN_H
|
||||
#define FLORISBOARD_TOKEN_H
|
||||
|
||||
#include "nlp.h"
|
||||
#include <string>
|
||||
|
||||
namespace ime::nlp {
|
||||
|
||||
class Token {
|
||||
public:
|
||||
word_t data;
|
||||
Token();
|
||||
Token(word_t &&_data);
|
||||
|
||||
friend bool operator==(const Token &t1, const Token &t2);
|
||||
friend bool operator!=(const Token &t1, const Token &t2);
|
||||
};
|
||||
|
||||
class WeightedToken : public Token {
|
||||
public:
|
||||
freq_t freq;
|
||||
WeightedToken();
|
||||
WeightedToken(word_t &&_data, freq_t _freq);
|
||||
|
||||
friend bool operator==(const WeightedToken &t1, const WeightedToken &t2);
|
||||
friend bool operator!=(const WeightedToken &t1, const WeightedToken &t2);
|
||||
friend bool operator<(const WeightedToken &t1, const WeightedToken &t2);
|
||||
friend bool operator<=(const WeightedToken &t1, const WeightedToken &t2);
|
||||
friend bool operator>(const WeightedToken &t1, const WeightedToken &t2);
|
||||
friend bool operator>=(const WeightedToken &t1, const WeightedToken &t2);
|
||||
};
|
||||
|
||||
} // namespace ime::nlp
|
||||
|
||||
#endif // FLORISBOARD_TOKEN_H
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,22 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
package dev.patrickgold.florisboard
|
||||
|
||||
import android.app.Application
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.crashutility.CrashUtility
|
||||
import dev.patrickgold.florisboard.debug.Flog
|
||||
import dev.patrickgold.florisboard.debug.LogTopic
|
||||
import dev.patrickgold.florisboard.ime.core.Preferences
|
||||
import dev.patrickgold.florisboard.ime.core.SubtypeManager
|
||||
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetManager
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("unused")
|
||||
class FlorisApplication : Application(), CoroutineScope by MainScope() {
|
||||
class FlorisApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (BuildConfig.DEBUG) {
|
||||
@@ -43,11 +42,11 @@ class FlorisApplication : Application(), CoroutineScope by MainScope() {
|
||||
flogOutputs = Flog.OUTPUT_CONSOLE
|
||||
)
|
||||
CrashUtility.install(this)
|
||||
val prefHelper = PrefHelper.getDefaultInstance(this)
|
||||
val prefs = Preferences.initDefault(this)
|
||||
val assetManager = AssetManager.init(this)
|
||||
SubtypeManager.init(this, prefHelper)
|
||||
SubtypeManager.init(this)
|
||||
DictionaryManager.init(this)
|
||||
ThemeManager.init(this, assetManager, prefHelper)
|
||||
prefHelper.initDefaultPreferences()
|
||||
ThemeManager.init(this, assetManager)
|
||||
prefs.initDefaultPreferences()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard
|
||||
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
|
||||
/**
|
||||
* This class only exists to prevent accidental IME deactivation after an update
|
||||
* of FlorisBoard to a new version when the location of the FlorisBoard class has
|
||||
* changed. The Android Framework uses the service class path as the IME id,
|
||||
* using this extension here makes sure it won't change ever again for the system.
|
||||
*
|
||||
* Important: DO NOT PUT ANY LOGIC INTO THIS CLASS. Make the necessary changes
|
||||
* within the FlorisBoard class instead.
|
||||
*/
|
||||
class FlorisImeService : FlorisBoard()
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
package dev.patrickgold.florisboard.common
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
@@ -26,6 +26,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
@@ -34,10 +35,7 @@ abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity(), CoroutineS
|
||||
private var _binding: V? = null
|
||||
protected val binding: V
|
||||
get() = _binding!!
|
||||
|
||||
private var _prefs: PrefHelper? = null
|
||||
protected val prefs: PrefHelper
|
||||
get() = _prefs!!
|
||||
protected val prefs: Preferences get() = Preferences.default()
|
||||
|
||||
private var errorDialog: AlertDialog? = null
|
||||
private var errorSnackbar: Snackbar? = null
|
||||
@@ -48,7 +46,6 @@ abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity(), CoroutineS
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
_prefs = PrefHelper.getDefaultInstance(applicationContext)
|
||||
onCreateBinding().let {
|
||||
_binding = it
|
||||
setContentView(it.root)
|
||||
@@ -59,7 +56,6 @@ abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity(), CoroutineS
|
||||
super.onDestroy()
|
||||
cancel()
|
||||
_binding = null
|
||||
_prefs = null
|
||||
errorDialog?.dismiss()
|
||||
errorDialog = null
|
||||
errorSnackbar?.dismiss()
|
||||
@@ -102,7 +98,7 @@ abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity(), CoroutineS
|
||||
errorDialog?.dismiss()
|
||||
errorDialog = AlertDialog.Builder(this@FlorisActivity).run {
|
||||
setTitle(R.string.assets__error__details)
|
||||
setMessage(errorThrowable.toString())
|
||||
setMessage(errorThrowable?.stackTraceToString())
|
||||
setPositiveButton(android.R.string.ok, null)
|
||||
setNeutralButton(R.string.crash_dialog__copy_to_clipboard) { _, _ ->
|
||||
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE)
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
package dev.patrickgold.florisboard.common
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.common
|
||||
|
||||
/**
|
||||
* Type alias for a native pointer.
|
||||
*/
|
||||
typealias NativePtr = Long
|
||||
|
||||
/**
|
||||
* Constant value for a native null pointer.
|
||||
*/
|
||||
const val NATIVE_NULLPTR: NativePtr = 0
|
||||
|
||||
/**
|
||||
* Generic interface for a native instance object. Defines the basic
|
||||
* methods which each native instance wrapper should define and be able
|
||||
* to handle to.
|
||||
*/
|
||||
interface NativeInstanceWrapper {
|
||||
/**
|
||||
* Returns the native pointer of this instance. The returned pointer
|
||||
* is only valid if [dispose] has not been previously called.
|
||||
*
|
||||
* @return The native null pointer for this instance.
|
||||
*/
|
||||
fun nativePtr(): NativePtr
|
||||
|
||||
/**
|
||||
* Deletes the native object and frees allocated resources. After
|
||||
* invoking this method one MUST NOT touch this instance ever again.
|
||||
*/
|
||||
fun dispose()
|
||||
}
|
||||
186
app/src/main/java/dev/patrickgold/florisboard/common/Pointer.kt
Normal file
186
app/src/main/java/dev/patrickgold/florisboard/common/Pointer.kt
Normal file
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.common
|
||||
|
||||
import androidx.annotation.RestrictTo
|
||||
|
||||
/**
|
||||
* A simple helper object managing touch pointer objects. This class is designed to hold
|
||||
* at max [capacity] at once. It tries to reduce the need to recreate objects and to resize
|
||||
* arrays by creating a fixed-size list and by reusing pointers. This map supports iterating
|
||||
* over all active pointers.
|
||||
*
|
||||
* @property capacity The capacity of this map, determining the maximum number of pointers this
|
||||
* map can hold at once. This value must be greater than or equal to one. Should a smaller capacity
|
||||
* be passed, automatically the minimum capacity `1` is assumed.
|
||||
* @param init The initializer for each pointer. Note that [Pointer.reset] is called before
|
||||
* storing the new object, to ensure that this pointer is not initialized with some pointer data.
|
||||
*/
|
||||
class PointerMap<P : Pointer>(val capacity: Int = 4, init: (Int) -> P) : Iterable<P> {
|
||||
/**
|
||||
* The internal list of pointers, is not intended for public access.
|
||||
*/
|
||||
private val pointers: List<P> = List(capacity.coerceAtLeast(1)) { i ->
|
||||
init(i).also { pointer -> pointer.reset() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new pointer with given [id] and [index] and returns it. If this map is already at max
|
||||
* capacity, null is returned and the pointer could not be added.
|
||||
*
|
||||
* @param id The id of the pointer to add.
|
||||
* @param index The index of the pointer to add.
|
||||
*
|
||||
* @return The newly added pointer or null if the map is already full.
|
||||
*/
|
||||
fun add(id: Int, index: Int): P? {
|
||||
for (pointer in pointers) {
|
||||
if (pointer.isNotUsed) {
|
||||
pointer.id = id
|
||||
pointer.index = index
|
||||
return pointer
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears this map and resets all pointers.
|
||||
*/
|
||||
fun clear() {
|
||||
for (pointer in pointers) {
|
||||
pointer.reset()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a pointer by given [id].
|
||||
*
|
||||
* @param id The id of the pointer which should be found.
|
||||
*
|
||||
* @return The pointer with given [id] or null.
|
||||
*/
|
||||
fun findById(id: Int): P? {
|
||||
for (pointer in pointers) {
|
||||
if (pointer.id == id) {
|
||||
return pointer
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a pointer from the internal array based on the internal array index. This method
|
||||
* is intended to be used only by the [PointerIterator].
|
||||
*
|
||||
* @param index
|
||||
*
|
||||
* @return The pointer for given index or null, excluding unused pointers.
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
fun get(index: Int): P? {
|
||||
val pointer = pointers.getOrNull(index)
|
||||
if (pointer != null && pointer.isUsed) {
|
||||
return pointer
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<P> {
|
||||
return PointerIterator(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a pointer with given [id] and returns a boolean result.
|
||||
*
|
||||
* @param id The id of the pointer to remove. If the id is not existent, noting happens.
|
||||
*
|
||||
* @return True if a pointer was removed, false otherwise.
|
||||
*/
|
||||
fun removeById(id: Int): Boolean {
|
||||
for (pointer in pointers) {
|
||||
if (pointer.id == id) {
|
||||
pointer.reset()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of this map (only counting active pointers). This value is anywhere
|
||||
* between 0 and [capacity].
|
||||
*/
|
||||
val size: Int
|
||||
get() = pointers.count { it.isUsed }
|
||||
}
|
||||
|
||||
class PointerIterator<P : Pointer>(private val pointerMap: PointerMap<P>) : Iterator<P> {
|
||||
private var index: Int = 0
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
do {
|
||||
if (pointerMap.get(index) != null) {
|
||||
return true
|
||||
}
|
||||
} while (++index < pointerMap.capacity)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun next(): P {
|
||||
return pointerMap.get(index++)!!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract touch pointer definition.
|
||||
*/
|
||||
abstract class Pointer {
|
||||
companion object {
|
||||
const val UNUSED_P: Int = -1
|
||||
}
|
||||
|
||||
/**
|
||||
* The id of this pointer, corresponds to the motion event this pointer originated.
|
||||
*/
|
||||
var id: Int = UNUSED_P
|
||||
|
||||
/**
|
||||
* The index of this pointer, corresponds to the motion event this pointer originated.
|
||||
*/
|
||||
var index: Int = UNUSED_P
|
||||
|
||||
/**
|
||||
* True if this pointer is used and active, false otherwise.
|
||||
*/
|
||||
val isUsed: Boolean
|
||||
get() = id >= 0
|
||||
|
||||
/**
|
||||
* False if this pointer is used and active, true otherwise.
|
||||
*/
|
||||
val isNotUsed: Boolean
|
||||
get() = !isUsed
|
||||
|
||||
/**
|
||||
* Resets this pointer to be used again.
|
||||
*/
|
||||
open fun reset() {
|
||||
id = UNUSED_P
|
||||
index = UNUSED_P
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,16 +15,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.util
|
||||
package dev.patrickgold.florisboard.common
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
|
||||
|
||||
/**
|
||||
* This file has been taken from the Android LatinIME project. Following modifications were done to
|
||||
* the original source code:
|
||||
@@ -38,7 +38,7 @@ import android.widget.LinearLayout
|
||||
* The original source code can be found at the following location:
|
||||
* https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+/refs/heads/master/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java
|
||||
*/
|
||||
object ViewLayoutUtils {
|
||||
object ViewUtils {
|
||||
fun updateLayoutHeightOf(window: Window, layoutHeight: Int) {
|
||||
val params = window.attributes
|
||||
if (params != null && params.height != layoutHeight) {
|
||||
@@ -81,11 +81,10 @@ object ViewLayoutUtils {
|
||||
* Source: https://stackoverflow.com/a/9563438/6801193 (by Muhammad Nabeel Arif)
|
||||
*
|
||||
* @param dp A value in dp (density independent pixels) unit. Which we need to convert into pixels
|
||||
* @param context Context to get resources and device specific display metrics
|
||||
* @return A float value to represent px equivalent to dp depending on device density
|
||||
*/
|
||||
fun convertDpToPixel(dp: Float, context: Context): Float {
|
||||
return dp * (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
|
||||
fun dp2px(dp: Float): Float {
|
||||
return dp * (Resources.getSystem().displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,10 +93,9 @@ object ViewLayoutUtils {
|
||||
* Source: https://stackoverflow.com/a/9563438/6801193 (by Muhammad Nabeel Arif)
|
||||
*
|
||||
* @param px A value in px (pixels) unit. Which we need to convert into db
|
||||
* @param context Context to get resources and device specific display metrics
|
||||
* @return A float value to represent dp equivalent to px value
|
||||
*/
|
||||
fun convertPixelsToDp(px: Float, context: Context): Float {
|
||||
return px / (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
|
||||
fun px2dp(px: Float): Float {
|
||||
return px / (Resources.getSystem().displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,13 @@ import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.CrashDialogBinding
|
||||
import dev.patrickgold.florisboard.debug.*
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import java.util.*
|
||||
import dev.patrickgold.florisboard.ime.core.Preferences
|
||||
|
||||
class CrashDialogActivity : AppCompatActivity() {
|
||||
private lateinit var binding: CrashDialogBinding
|
||||
private var stacktraces: List<CrashUtility.Stacktrace> = listOf()
|
||||
private var errorReport: StringBuilder = StringBuilder()
|
||||
private var prefs: PrefHelper? = null
|
||||
private var prefs: Preferences? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -43,7 +42,7 @@ class CrashDialogActivity : AppCompatActivity() {
|
||||
// We secure the PrefHelper usage here because the PrefHelper could potentially be the
|
||||
// source of the crash, thus making the crash dialog unusable if not wrapped.
|
||||
try {
|
||||
prefs = PrefHelper.getDefaultInstance(this)
|
||||
prefs = Preferences.default()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
@@ -136,10 +135,10 @@ class CrashDialogActivity : AppCompatActivity() {
|
||||
private fun getDeviceName(): String {
|
||||
val manufacturer = Build.MANUFACTURER
|
||||
val model = Build.MODEL
|
||||
return if (model.toLowerCase(Locale.ENGLISH).startsWith(manufacturer.toLowerCase(Locale.ENGLISH))) {
|
||||
model.capitalize(Locale.ENGLISH)
|
||||
return if (model.lowercase().startsWith(manufacturer.lowercase())) {
|
||||
model.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
|
||||
} else {
|
||||
"${manufacturer.capitalize(Locale.ENGLISH)} $model"
|
||||
"${manufacturer.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }} $model"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ abstract class CrashUtility private constructor() {
|
||||
private const val UNHANDLED_STACKTRACE_FILE_EXT = "stacktrace"
|
||||
|
||||
private var lastActivityCreated: WeakReference<Activity?> = WeakReference(null)
|
||||
private var stagedException: Throwable? = null
|
||||
|
||||
/**
|
||||
* Installs the CrashUtility crash handler for the given package [context]. Also registers
|
||||
@@ -148,6 +149,22 @@ abstract class CrashUtility private constructor() {
|
||||
return true
|
||||
}
|
||||
|
||||
fun stageException(e: Throwable?) {
|
||||
if (stagedException == null) {
|
||||
stagedException = e
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
|
||||
fun handleStagedButUnhandledExceptions() {
|
||||
val e = stagedException ?: return
|
||||
val handler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
if (handler is UncaughtExceptionHandler) {
|
||||
stagedException = null
|
||||
handler.uncaughtException(null, e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and returns all unhandled stacktrace files.
|
||||
*
|
||||
@@ -362,7 +379,6 @@ abstract class CrashUtility private constructor() {
|
||||
flogInfo(LogTopic.CRASH_UTILITY) {
|
||||
"Detected application crash, executing custom crash handler."
|
||||
}
|
||||
thread ?: return
|
||||
throwable ?: return
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val stacktrace = Log.getStackTraceString(throwable)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalContracts::class, ExperimentalUnsignedTypes::class)
|
||||
@file:OptIn(ExperimentalContracts::class)
|
||||
|
||||
package dev.patrickgold.florisboard.debug
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalUnsignedTypes::class)
|
||||
|
||||
package dev.patrickgold.florisboard.debug
|
||||
|
||||
/**
|
||||
@@ -38,6 +36,7 @@ object LogTopic {
|
||||
const val LAYOUT_MANAGER: FlogTopic = 8u
|
||||
const val TEXT_KEYBOARD_VIEW: FlogTopic = 16u
|
||||
const val GESTURES: FlogTopic = 32u
|
||||
const val SMARTBAR: FlogTopic = 64u
|
||||
|
||||
const val GLIDE: FlogTopic = 512u
|
||||
const val CLIPBOARD: FlogTopic = 1024u
|
||||
|
||||
@@ -64,7 +64,7 @@ class ClipboardHistoryView : LinearLayout, FlorisBoard.EventListener,
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val height = florisboard?.inputView?.desiredMediaKeyboardViewHeight ?: 0.0f
|
||||
val height = florisboard?.uiBinding?.inputView?.desiredMediaKeyboardViewHeight ?: 0.0f
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height.roundToInt(), MeasureSpec.EXACTLY))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,14 @@ package dev.patrickgold.florisboard.ime.clip
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.FlorisboardBinding
|
||||
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
|
||||
import dev.patrickgold.florisboard.ime.core.InputView
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.BasicTextKeyData
|
||||
import kotlinx.coroutines.*
|
||||
@@ -43,29 +42,23 @@ class ClipboardInputManager private constructor() : CoroutineScope by MainScope(
|
||||
florisboard.addEventListener(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new input view has been registered. Used to initialize all media-relevant
|
||||
* views and layouts.
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onRegisterInputView(inputView: InputView) {
|
||||
launch(Dispatchers.Default) {
|
||||
inputView.findViewById<ImageButton>(R.id.back_to_keyboard_button)
|
||||
.setOnTouchListener { view, event -> onButtonPressEvent(view, event) }
|
||||
|
||||
inputView.findViewById<ImageButton>(R.id.clear_clipboard_history)
|
||||
.setOnTouchListener { view, event -> onButtonPressEvent(view, event) }
|
||||
|
||||
recyclerView = inputView.findViewById(R.id.clipboard_history_items)
|
||||
override fun onInitializeInputUi(uiBinding: FlorisboardBinding) {
|
||||
uiBinding.clipboard.backToKeyboardButton
|
||||
.setOnTouchListener { view, event -> onButtonPressEvent(view, event) }
|
||||
uiBinding.clipboard.clearClipboardHistory
|
||||
.setOnTouchListener { view, event -> onButtonPressEvent(view, event) }
|
||||
|
||||
recyclerView = uiBinding.clipboard.clipboardHistoryItems.also {
|
||||
if (BuildConfig.DEBUG && adapter == null) {
|
||||
error("initClipboard() not called")
|
||||
}
|
||||
|
||||
recyclerView!!.adapter = adapter
|
||||
it.adapter = adapter
|
||||
val manager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
|
||||
recyclerView!!.layoutManager = manager
|
||||
it.layoutManager = manager
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,8 +72,8 @@ class ClipboardInputManager private constructor() : CoroutineScope by MainScope(
|
||||
/**
|
||||
* Returns a reference to the [ClipboardHistoryView]
|
||||
*/
|
||||
fun getClipboardHistoryView(): ClipboardHistoryView{
|
||||
return FlorisBoard.getInstance().inputView?.mainViewFlipper?.getChildAt(2) as ClipboardHistoryView
|
||||
fun getClipboardHistoryView(): ClipboardHistoryView {
|
||||
return florisboard.uiBinding?.mainViewFlipper?.getChildAt(2) as ClipboardHistoryView
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@ import android.widget.LinearLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
import dev.patrickgold.florisboard.common.ViewUtils
|
||||
|
||||
class ClipboardPopupView: LinearLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
|
||||
@@ -23,7 +23,7 @@ class ClipboardPopupView: LinearLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
)
|
||||
|
||||
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
setCornerRadius(ViewUtils.dp2px(6.0f))
|
||||
}
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import dev.patrickgold.florisboard.ime.clip.provider.*
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.core.Preferences
|
||||
import dev.patrickgold.florisboard.util.cancelAll
|
||||
import dev.patrickgold.florisboard.util.postAtScheduledRate
|
||||
import timber.log.Timber
|
||||
@@ -49,7 +49,7 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
|
||||
private var onPrimaryClipChangedListeners: ArrayList<OnPrimaryClipChangedListener> = arrayListOf()
|
||||
private lateinit var systemClipboardManager: ClipboardManager
|
||||
private lateinit var handler: Handler
|
||||
private lateinit var prefHelper: PrefHelper
|
||||
private val prefs get() = Preferences.default()
|
||||
|
||||
data class TimedClipData(val data: ClipboardItem, val timeUTC: Long)
|
||||
|
||||
@@ -105,24 +105,47 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
|
||||
* Adds a new item to the clipboard history (if enabled).
|
||||
*/
|
||||
fun updateHistory(newData: ClipboardItem) {
|
||||
val clipboardPrefs = prefHelper.clipboard
|
||||
val clipboardPrefs = prefs.clipboard
|
||||
|
||||
if (clipboardPrefs.enableHistory) {
|
||||
if (clipboardPrefs.limitHistorySize) {
|
||||
var numRemoved = 0
|
||||
while (history.size >= clipboardPrefs.maxHistorySize) {
|
||||
numRemoved += 1
|
||||
history.removeLast().data.close()
|
||||
}
|
||||
ClipboardInputManager.getInstance().notifyItemRangeRemoved(history.size, numRemoved)
|
||||
}
|
||||
val clipboardInputManager = ClipboardInputManager.getInstance()
|
||||
|
||||
val timed = TimedClipData(newData, System.currentTimeMillis())
|
||||
history.addFirst(timed)
|
||||
ClipboardInputManager.getInstance().notifyItemInserted(pins.size)
|
||||
val historyElement = history.firstOrNull { it.data.type == ItemType.TEXT && it.data.text == newData.text }
|
||||
if (historyElement != null) {
|
||||
moveToTheBeginning(historyElement, newData, clipboardInputManager)
|
||||
} else {
|
||||
if (clipboardPrefs.limitHistorySize) {
|
||||
var numRemoved = 0
|
||||
while (history.size >= clipboardPrefs.maxHistorySize) {
|
||||
numRemoved += 1
|
||||
history.removeLast().data.close()
|
||||
}
|
||||
clipboardInputManager.notifyItemRangeRemoved(history.size, numRemoved)
|
||||
}
|
||||
|
||||
createAndAddNewTimedClipData(newData)
|
||||
clipboardInputManager.notifyItemInserted(pins.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a ClipboardItem to the beginning of the history by removing the old one and creating a new one
|
||||
*/
|
||||
private fun moveToTheBeginning(
|
||||
historyElement: TimedClipData,
|
||||
newData: ClipboardItem,
|
||||
clipboardInputManager: ClipboardInputManager
|
||||
) {
|
||||
val elementsPosition = history.indexOf(historyElement)
|
||||
history.remove(historyElement)
|
||||
|
||||
createAndAddNewTimedClipData(newData)
|
||||
|
||||
clipboardInputManager.notifyItemMoved(elementsPosition, 0)
|
||||
clipboardInputManager.notifyItemChanged(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Used so that [onPrimaryClipChanged] knows whether it was called by [changeCurrent] (and hence shouldn't update
|
||||
* history)
|
||||
@@ -133,14 +156,14 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
|
||||
* Changes current clipboard item. WITHOUT updating the history.
|
||||
*/
|
||||
fun changeCurrent(newData: ClipboardItem, closePrevious: Boolean) {
|
||||
if (prefHelper.clipboard.enableInternal) {
|
||||
if (prefs.clipboard.enableInternal) {
|
||||
if (closePrevious) current?.close()
|
||||
current = newData
|
||||
val isEqual = when (newData.type) {
|
||||
ItemType.TEXT -> newData.text == systemClipboardManager.primaryClip?.getItemAt(0)?.text
|
||||
ItemType.IMAGE -> newData.uri == systemClipboardManager.primaryClip?.getItemAt(0)?.uri
|
||||
}
|
||||
if (prefHelper.clipboard.syncToSystem && !isEqual)
|
||||
if (prefs.clipboard.syncToSystem && !isEqual)
|
||||
systemClipboardManager.setPrimaryClip(newData.toClipData())
|
||||
} else {
|
||||
shouldUpdateHistory = false
|
||||
@@ -156,7 +179,7 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
|
||||
fun addNewClip(newData: ClipboardItem) {
|
||||
updateHistory(newData)
|
||||
// If history is disabled, this new item will replace the old one and hence should be closed.
|
||||
changeCurrent(newData, !prefHelper.clipboard.enableHistory)
|
||||
changeCurrent(newData, !prefs.clipboard.enableHistory)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,7 +191,7 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
|
||||
}
|
||||
|
||||
val primaryClip: ClipboardItem?
|
||||
get() = if (prefHelper.clipboard.enableInternal) {
|
||||
get() = if (prefs.clipboard.enableInternal) {
|
||||
current
|
||||
} else {
|
||||
systemClipboardManager.primaryClip?.let { ClipboardItem.fromClipData(it, false) }
|
||||
@@ -204,13 +227,13 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
|
||||
ItemType.IMAGE -> internalPrimaryClip.uri == systemPrimaryClip.getItemAt(0)?.uri
|
||||
else -> false
|
||||
}
|
||||
if (prefHelper.clipboard.enableInternal) {
|
||||
if (prefs.clipboard.enableInternal) {
|
||||
// In the event that the internal clipboard is enabled, sync to internal clipboard is enabled
|
||||
// and the item is not already in internal clipboard, add it.
|
||||
if (prefHelper.clipboard.syncToFloris && !isEqual) {
|
||||
if (prefs.clipboard.syncToFloris && !isEqual) {
|
||||
addNewClip(ClipboardItem.fromClipData(systemPrimaryClip, true))
|
||||
}
|
||||
} else if (prefHelper.clipboard.enableHistory) {
|
||||
} else if (prefs.clipboard.enableHistory) {
|
||||
// in the event history is enabled, and it should be updated it is updated
|
||||
if (shouldUpdateHistory) {
|
||||
updateHistory(ClipboardItem.fromClipData(systemPrimaryClip, false))
|
||||
@@ -247,16 +270,14 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
|
||||
systemClipboardManager = (context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager)
|
||||
systemClipboardManager.addPrimaryClipChangedListener(this)
|
||||
|
||||
prefHelper = PrefHelper.getDefaultInstance(context)
|
||||
|
||||
val cleanUpClipboard = Runnable {
|
||||
if (!prefHelper.clipboard.cleanUpOld) {
|
||||
if (!prefs.clipboard.cleanUpOld) {
|
||||
return@Runnable
|
||||
}
|
||||
|
||||
val currentTime = System.currentTimeMillis()
|
||||
var numToPop = 0
|
||||
val expiryTime = prefHelper.clipboard.cleanUpAfter * 60 * 1000
|
||||
val expiryTime = prefs.clipboard.cleanUpAfter * 60 * 1000
|
||||
for (item in history.asReversed()) {
|
||||
if (item.timeUTC + expiryTime < currentTime) {
|
||||
numToPop += 1
|
||||
@@ -271,7 +292,7 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
|
||||
}
|
||||
FlorisBoard.getInstance().clipInputManager.initClipboard(this.history, this.pins)
|
||||
handler = Handler(Looper.getMainLooper())
|
||||
prefHelper
|
||||
prefs
|
||||
handler.postAtScheduledRate(0, INTERVAL, cleanUpClipboard)
|
||||
executor = FlorisBoard.getInstance().asyncExecutor
|
||||
executor.execute {
|
||||
@@ -333,7 +354,7 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
|
||||
val clipInputManager = FlorisBoard.getInstance().clipInputManager
|
||||
val item = pins.removeAt(adapterPos)
|
||||
|
||||
val clipboardPrefs = prefHelper.clipboard
|
||||
val clipboardPrefs = prefs.clipboard
|
||||
if (clipboardPrefs.limitHistorySize) {
|
||||
var numRemoved = 0
|
||||
while (history.size >= clipboardPrefs.maxHistorySize) {
|
||||
@@ -343,8 +364,7 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
|
||||
ClipboardInputManager.getInstance().notifyItemRangeRemoved(history.size, numRemoved)
|
||||
}
|
||||
|
||||
val timed = TimedClipData(item, System.currentTimeMillis())
|
||||
history.addFirst(timed)
|
||||
createAndAddNewTimedClipData(item)
|
||||
|
||||
clipInputManager.notifyItemMoved(adapterPos, pins.size)
|
||||
clipInputManager.notifyItemChanged(pins.size)
|
||||
@@ -354,6 +374,14 @@ class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryC
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new TimedClipData and adds it to the history
|
||||
*/
|
||||
private fun createAndAddNewTimedClipData(newData: ClipboardItem) {
|
||||
val timed = TimedClipData(newData, System.currentTimeMillis())
|
||||
history.addFirst(timed)
|
||||
}
|
||||
|
||||
fun removeClip(pos: Int) {
|
||||
when {
|
||||
pos < pins.size -> {
|
||||
|
||||
@@ -21,32 +21,33 @@ import android.content.Intent
|
||||
import android.inputmethodservice.InputMethodService
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import android.text.InputType
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyCharacterMap
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputConnection
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||
import androidx.core.view.inputmethod.InputContentInfoCompat
|
||||
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
|
||||
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
|
||||
import dev.patrickgold.florisboard.ime.clip.provider.ItemType
|
||||
import dev.patrickgold.florisboard.ime.keyboard.ImeOptions
|
||||
import dev.patrickgold.florisboard.ime.keyboard.InputAttributes
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardState
|
||||
import dev.patrickgold.florisboard.ime.text.TextInputManager
|
||||
import dev.patrickgold.florisboard.ime.text.composing.Composer
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Class which holds information relevant to an editor instance like the [cachedInput], [selection],
|
||||
* [inputAttributes], [imeOptions], etc. This class is thought to be an improved [EditorInfo]
|
||||
* Class which holds information relevant to an editor instance like the [cachedInput],
|
||||
* [selection] etc. This class is thought to be an improved [EditorInfo]
|
||||
* object which also holds the state of the currently focused input editor.
|
||||
*/
|
||||
class EditorInstance private constructor(
|
||||
private val ims: InputMethodService?,
|
||||
val imeOptions: ImeOptions,
|
||||
val inputAttributes: InputAttributes,
|
||||
val packageName: String,
|
||||
val activeState: KeyboardState,
|
||||
private val editorInfo: EditorInfo
|
||||
) {
|
||||
val cachedInput: CachedInput = CachedInput(this)
|
||||
@@ -56,25 +57,12 @@ class EditorInstance private constructor(
|
||||
get() {
|
||||
val ic = inputConnection ?: return InputAttributes.CapsMode.NONE
|
||||
return InputAttributes.CapsMode.fromFlags(
|
||||
ic.getCursorCapsMode(inputAttributes.capsMode.toFlags())
|
||||
ic.getCursorCapsMode(activeState.inputAttributes.capsMode.toFlags())
|
||||
)
|
||||
}
|
||||
val inputConnection: InputConnection?
|
||||
get() = ims?.currentInputConnection
|
||||
var isComposingEnabled: Boolean = false
|
||||
set(v) {
|
||||
field = v
|
||||
cachedInput.reevaluate()
|
||||
if (v && !isRawInputEditor) {
|
||||
markComposingRegion(cachedInput.currentWord)
|
||||
} else {
|
||||
markComposingRegion(null)
|
||||
}
|
||||
}
|
||||
var shouldReevaluateComposingSuggestions: Boolean = false
|
||||
var isPrivateMode: Boolean = false
|
||||
val isRawInputEditor: Boolean
|
||||
get() = inputAttributes.type == InputAttributes.Type.NULL
|
||||
var selection: Selection = Selection(this)
|
||||
private set
|
||||
var isPhantomSpaceActive: Boolean = false
|
||||
@@ -85,22 +73,20 @@ class EditorInstance private constructor(
|
||||
fun default(): EditorInstance {
|
||||
return EditorInstance(
|
||||
ims = null,
|
||||
imeOptions = ImeOptions.fromImeOptionsInt(EditorInfo.IME_NULL),
|
||||
inputAttributes = InputAttributes.fromInputTypeInt(InputType.TYPE_NULL),
|
||||
packageName = "undefined",
|
||||
activeState = KeyboardState.new(),
|
||||
editorInfo = EditorInfo()
|
||||
)
|
||||
}
|
||||
|
||||
fun from(editorInfo: EditorInfo?, ims: InputMethodService?): EditorInstance {
|
||||
fun from(editorInfo: EditorInfo?, ims: InputMethodService?, state: KeyboardState): EditorInstance {
|
||||
return if (editorInfo == null) {
|
||||
default()
|
||||
} else {
|
||||
EditorInstance(
|
||||
ims = ims,
|
||||
imeOptions = ImeOptions.fromImeOptionsInt(editorInfo.imeOptions),
|
||||
inputAttributes = InputAttributes.fromInputTypeInt(editorInfo.inputType),
|
||||
packageName = editorInfo.packageName,
|
||||
activeState = state,
|
||||
editorInfo = editorInfo
|
||||
).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
@@ -124,6 +110,9 @@ class EditorInstance private constructor(
|
||||
newSelStart: Int, newSelEnd: Int,
|
||||
candidatesStart: Int, candidatesEnd: Int
|
||||
) {
|
||||
if (newSelStart == oldSelStart && newSelEnd == oldSelEnd) {
|
||||
return
|
||||
}
|
||||
// The Android Framework allows that start can be greater than end in some cases. To prevent bugs in the Floris
|
||||
// input logic, we swap start and end here if this should really be the case.
|
||||
if (newSelEnd < newSelStart) {
|
||||
@@ -136,13 +125,30 @@ class EditorInstance private constructor(
|
||||
} else if (isPhantomSpaceActive && !wasPhantomSpaceActiveLastUpdate) {
|
||||
wasPhantomSpaceActiveLastUpdate = true
|
||||
}
|
||||
cachedInput.update()
|
||||
if (isComposingEnabled && candidatesStart >= 0 && candidatesEnd >= 0) {
|
||||
shouldReevaluateComposingSuggestions = true
|
||||
if (selection.isCursorMode) {
|
||||
cachedInput.update()
|
||||
if (activeState.isComposingEnabled) {
|
||||
if (candidatesStart >= 0 && candidatesEnd >= 0) {
|
||||
shouldReevaluateComposingSuggestions = true
|
||||
}
|
||||
if (activeState.isRichInputEditor && !isPhantomSpaceActive) {
|
||||
markComposingRegion(cachedInput.currentWord)
|
||||
} else if (newSelStart >= 0) {
|
||||
markComposingRegion(null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (candidatesStart >= 0 || candidatesEnd >= 0) {
|
||||
markComposingRegion(null)
|
||||
}
|
||||
}
|
||||
if (selection.isCursorMode && isComposingEnabled && !isRawInputEditor && !isPhantomSpaceActive) {
|
||||
}
|
||||
|
||||
fun composingEnabledChanged() {
|
||||
cachedInput.reevaluate()
|
||||
if (activeState.isComposingEnabled && activeState.isRichInputEditor) {
|
||||
markComposingRegion(cachedInput.currentWord)
|
||||
} else if (newSelStart >= 0) {
|
||||
} else {
|
||||
markComposingRegion(null)
|
||||
}
|
||||
}
|
||||
@@ -156,7 +162,7 @@ class EditorInstance private constructor(
|
||||
*/
|
||||
fun commitCompletion(text: String): Boolean {
|
||||
val ic = inputConnection ?: return false
|
||||
return if (isRawInputEditor) {
|
||||
return if (activeState.isRawInputEditor) {
|
||||
false
|
||||
} else {
|
||||
ic.beginBatchEdit()
|
||||
@@ -172,6 +178,26 @@ class EditorInstance private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper, replacing a call to inputConnection.commitText with text composition in mind.
|
||||
*/
|
||||
fun doCommitText(text: String): Pair<Boolean, String> {
|
||||
val ic = inputConnection ?: return Pair(false, "")
|
||||
val composer: Composer = FlorisBoard.getInstance().composer
|
||||
return if (text.length != 1) {
|
||||
Pair(ic.commitText(text, 1), text)
|
||||
} else {
|
||||
ic.beginBatchEdit()
|
||||
ic.finishComposingText()
|
||||
val previous = getTextBeforeCursor(composer.toRead)
|
||||
val (rm, finalText) = composer.getActions(previous, text[0])
|
||||
if (rm != 0) ic.deleteSurroundingText(rm, 0)
|
||||
ic.commitText(finalText, 1)
|
||||
ic.endBatchEdit()
|
||||
Pair(true, finalText)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the given [text] to this editor instance and adjusts both the cursor position and
|
||||
* composing region, if any.
|
||||
@@ -185,8 +211,8 @@ class EditorInstance private constructor(
|
||||
*/
|
||||
fun commitText(text: String): Boolean {
|
||||
val ic = inputConnection ?: return false
|
||||
return if (isRawInputEditor || selection.isSelectionMode || !isComposingEnabled) {
|
||||
ic.commitText(text, 1)
|
||||
return if (activeState.isRawInputEditor || selection.isSelectionMode || !activeState.isComposingEnabled) {
|
||||
doCommitText(text).first
|
||||
} else {
|
||||
ic.beginBatchEdit()
|
||||
val isWordComponent = CachedInput.isWordComponent(text)
|
||||
@@ -199,8 +225,8 @@ class EditorInstance private constructor(
|
||||
}
|
||||
!isPhantomSpace && isWordComponent -> {
|
||||
ic.finishComposingText()
|
||||
ic.commitText(text, 1)
|
||||
ic.setComposingRegion(cachedInput.currentWord.start, cachedInput.currentWord.end + text.length)
|
||||
val finalText = doCommitText(text).second
|
||||
ic.setComposingRegion(cachedInput.currentWord.start, cachedInput.currentWord.end + finalText.length)
|
||||
}
|
||||
else -> {
|
||||
ic.finishComposingText()
|
||||
@@ -219,7 +245,7 @@ class EditorInstance private constructor(
|
||||
*/
|
||||
fun commitGesture(text: String): Boolean {
|
||||
val ic = inputConnection ?: return false
|
||||
return if (isRawInputEditor) {
|
||||
return if (activeState.isRawInputEditor) {
|
||||
false
|
||||
} else {
|
||||
ic.beginBatchEdit()
|
||||
@@ -302,7 +328,7 @@ class EditorInstance private constructor(
|
||||
val ic = inputConnection ?: return false
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
return if (n < 1 || isRawInputEditor || !selection.isValid || !selection.isCursorMode) {
|
||||
return if (n < 1 || activeState.isRawInputEditor || !selection.isValid || !selection.isCursorMode) {
|
||||
false
|
||||
} else {
|
||||
ic.beginBatchEdit()
|
||||
@@ -340,7 +366,7 @@ class EditorInstance private constructor(
|
||||
*/
|
||||
fun getTextAfterCursor(n: Int): String {
|
||||
val ic = inputConnection
|
||||
if (ic == null || !selection.isValid || n < 1 || isRawInputEditor) {
|
||||
if (ic == null || !selection.isValid || n < 1 || activeState.isRawInputEditor) {
|
||||
return ""
|
||||
}
|
||||
return ic.getTextAfterCursor(n, 0)?.toString() ?: ""
|
||||
@@ -356,7 +382,7 @@ class EditorInstance private constructor(
|
||||
*/
|
||||
fun getTextBeforeCursor(n: Int): String {
|
||||
val ic = inputConnection
|
||||
if (ic == null || !selection.isValid || n < 1 || isRawInputEditor) {
|
||||
if (ic == null || !selection.isValid || n < 1 || activeState.isRawInputEditor) {
|
||||
return ""
|
||||
}
|
||||
return ic.getTextBeforeCursor(n.coerceAtMost(selection.start), 0)?.toString() ?: ""
|
||||
@@ -425,7 +451,7 @@ class EditorInstance private constructor(
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
private fun markComposingRegion(region: Region?): Boolean {
|
||||
fun markComposingRegion(region: Region?): Boolean {
|
||||
val ic = inputConnection ?: return false
|
||||
return if (region == null || !region.isValid) {
|
||||
ic.finishComposingText()
|
||||
@@ -486,7 +512,7 @@ class EditorInstance private constructor(
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
markComposingRegion(null)
|
||||
val ic = inputConnection ?: return false
|
||||
if (isRawInputEditor) {
|
||||
if (activeState.isRawInputEditor) {
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_A, meta(ctrl = true))
|
||||
} else {
|
||||
ic.performContextMenuAction(android.R.id.selectAll)
|
||||
@@ -502,7 +528,7 @@ class EditorInstance private constructor(
|
||||
fun performEnter(): Boolean {
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
return if (isRawInputEditor) {
|
||||
return if (activeState.isRawInputEditor) {
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER)
|
||||
} else {
|
||||
commitText("\n")
|
||||
@@ -516,7 +542,7 @@ class EditorInstance private constructor(
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performEnterAction(action: ImeOptions.Action): Boolean {
|
||||
fun performEnterAction(action: ImeOptions.EnterAction): Boolean {
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
val ic = inputConnection ?: return false
|
||||
@@ -625,26 +651,26 @@ class EditorInstance private constructor(
|
||||
val ic = inputConnection ?: return false
|
||||
ic.beginBatchEdit()
|
||||
val eventTime = SystemClock.uptimeMillis()
|
||||
if (metaState and KeyEvent.META_CTRL_ON > 0) {
|
||||
if (metaState and KeyEvent.META_CTRL_ON != 0) {
|
||||
sendDownKeyEvent(eventTime, KeyEvent.KEYCODE_CTRL_LEFT, 0)
|
||||
}
|
||||
if (metaState and KeyEvent.META_ALT_ON > 0) {
|
||||
if (metaState and KeyEvent.META_ALT_ON != 0) {
|
||||
sendDownKeyEvent(eventTime, KeyEvent.KEYCODE_ALT_LEFT, 0)
|
||||
}
|
||||
if (metaState and KeyEvent.META_SHIFT_ON > 0) {
|
||||
if (metaState and KeyEvent.META_SHIFT_ON != 0) {
|
||||
sendDownKeyEvent(eventTime, KeyEvent.KEYCODE_SHIFT_LEFT, 0)
|
||||
}
|
||||
for (n in 0 until count) {
|
||||
sendDownKeyEvent(eventTime, keyEventCode, metaState)
|
||||
sendUpKeyEvent(eventTime, keyEventCode, metaState)
|
||||
}
|
||||
if (metaState and KeyEvent.META_SHIFT_ON > 0) {
|
||||
if (metaState and KeyEvent.META_SHIFT_ON != 0) {
|
||||
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_SHIFT_LEFT, 0)
|
||||
}
|
||||
if (metaState and KeyEvent.META_ALT_ON > 0) {
|
||||
if (metaState and KeyEvent.META_ALT_ON != 0) {
|
||||
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_ALT_LEFT, 0)
|
||||
}
|
||||
if (metaState and KeyEvent.META_CTRL_ON > 0) {
|
||||
if (metaState and KeyEvent.META_CTRL_ON != 0) {
|
||||
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_CTRL_LEFT, 0)
|
||||
}
|
||||
ic.endBatchEdit()
|
||||
@@ -652,227 +678,6 @@ class EditorInstance private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which holds the same information as an [EditorInfo.imeOptions] int but more accessible and
|
||||
* readable.
|
||||
*/
|
||||
class ImeOptions private constructor(imeOptions: Int) {
|
||||
val action: Action = Action.fromInt(imeOptions)
|
||||
val flagForceAscii: Boolean = imeOptions and EditorInfo.IME_FLAG_FORCE_ASCII > 0
|
||||
val flagNavigateNext: Boolean = imeOptions and EditorInfo.IME_FLAG_NAVIGATE_NEXT > 0
|
||||
val flagNavigatePrevious: Boolean = imeOptions and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS > 0
|
||||
val flagNoAccessoryAction: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION > 0
|
||||
val flagNoEnterAction: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0
|
||||
val flagNoExtractUi: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_EXTRACT_UI > 0
|
||||
val flagNoFullscreen: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_FULLSCREEN > 0
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
val flagNoPersonalizedLearning: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING > 0
|
||||
|
||||
companion object {
|
||||
fun default(): ImeOptions {
|
||||
return fromImeOptionsInt(EditorInfo.IME_NULL)
|
||||
}
|
||||
|
||||
fun fromImeOptionsInt(imeOptions: Int): ImeOptions {
|
||||
return ImeOptions(imeOptions)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Action {
|
||||
DONE,
|
||||
GO,
|
||||
NEXT,
|
||||
NONE,
|
||||
PREVIOUS,
|
||||
SEARCH,
|
||||
SEND,
|
||||
UNSPECIFIED;
|
||||
|
||||
companion object {
|
||||
fun fromInt(raw: Int): Action {
|
||||
return when (raw and EditorInfo.IME_MASK_ACTION) {
|
||||
EditorInfo.IME_ACTION_DONE -> DONE
|
||||
EditorInfo.IME_ACTION_GO -> GO
|
||||
EditorInfo.IME_ACTION_NEXT -> NEXT
|
||||
EditorInfo.IME_ACTION_NONE -> NONE
|
||||
EditorInfo.IME_ACTION_PREVIOUS -> PREVIOUS
|
||||
EditorInfo.IME_ACTION_SEARCH -> SEARCH
|
||||
EditorInfo.IME_ACTION_SEND -> SEND
|
||||
EditorInfo.IME_ACTION_UNSPECIFIED -> UNSPECIFIED
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toInt(): Int {
|
||||
return when (this) {
|
||||
DONE -> EditorInfo.IME_ACTION_DONE
|
||||
GO -> EditorInfo.IME_ACTION_GO
|
||||
NEXT -> EditorInfo.IME_ACTION_NEXT
|
||||
NONE -> EditorInfo.IME_ACTION_NONE
|
||||
PREVIOUS -> EditorInfo.IME_ACTION_PREVIOUS
|
||||
SEARCH -> EditorInfo.IME_ACTION_SEARCH
|
||||
SEND -> EditorInfo.IME_ACTION_SEND
|
||||
UNSPECIFIED -> EditorInfo.IME_ACTION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which holds the same information as an [EditorInfo.inputType] int but more accessible and
|
||||
* readable.
|
||||
*/
|
||||
class InputAttributes private constructor(inputType: Int) {
|
||||
val type: Type
|
||||
val variation: Variation
|
||||
val capsMode: CapsMode
|
||||
var flagNumberDecimal: Boolean = false
|
||||
private set
|
||||
var flagNumberSigned: Boolean = false
|
||||
private set
|
||||
var flagTextAutoComplete: Boolean = false
|
||||
private set
|
||||
var flagTextAutoCorrect: Boolean = false
|
||||
private set
|
||||
var flagTextImeMultiLine: Boolean = false
|
||||
private set
|
||||
var flagTextMultiLine: Boolean = false
|
||||
private set
|
||||
var flagTextNoSuggestions: Boolean = false
|
||||
private set
|
||||
|
||||
init {
|
||||
when (inputType and InputType.TYPE_MASK_CLASS) {
|
||||
InputType.TYPE_NULL -> {
|
||||
type = Type.NULL
|
||||
variation = Variation.NORMAL
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
InputType.TYPE_CLASS_DATETIME -> {
|
||||
type = Type.DATETIME
|
||||
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_DATETIME_VARIATION_DATE -> Variation.DATE
|
||||
InputType.TYPE_DATETIME_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_DATETIME_VARIATION_TIME -> Variation.TIME
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
InputType.TYPE_CLASS_NUMBER -> {
|
||||
type = Type.NUMBER
|
||||
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_NUMBER_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> Variation.PASSWORD
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.NONE
|
||||
flagNumberDecimal = inputType and InputType.TYPE_NUMBER_FLAG_DECIMAL > 0
|
||||
flagNumberSigned = inputType and InputType.TYPE_NUMBER_FLAG_SIGNED > 0
|
||||
}
|
||||
InputType.TYPE_CLASS_PHONE -> {
|
||||
type = Type.PHONE
|
||||
variation = Variation.NORMAL
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
InputType.TYPE_CLASS_TEXT -> {
|
||||
type = Type.TEXT
|
||||
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> Variation.EMAIL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> Variation.EMAIL_SUBJECT
|
||||
InputType.TYPE_TEXT_VARIATION_FILTER -> Variation.FILTER
|
||||
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> Variation.LONG_MESSAGE
|
||||
InputType.TYPE_TEXT_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_TEXT_VARIATION_PASSWORD -> Variation.PASSWORD
|
||||
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> Variation.PERSON_NAME
|
||||
InputType.TYPE_TEXT_VARIATION_PHONETIC -> Variation.PHONETIC
|
||||
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> Variation.POSTAL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> Variation.SHORT_MESSAGE
|
||||
InputType.TYPE_TEXT_VARIATION_URI -> Variation.URI
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> Variation.VISIBLE_PASSWORD
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> Variation.WEB_EDIT_TEXT
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> Variation.WEB_EMAIL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> Variation.WEB_PASSWORD
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.fromFlags(inputType)
|
||||
flagTextAutoComplete = inputType and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE > 0
|
||||
flagTextAutoCorrect = inputType and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT > 0
|
||||
flagTextImeMultiLine = inputType and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE > 0
|
||||
flagTextMultiLine = inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE > 0
|
||||
flagTextNoSuggestions = inputType and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS > 0
|
||||
}
|
||||
else -> {
|
||||
type = Type.TEXT
|
||||
variation = Variation.NORMAL
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromInputTypeInt(inputType: Int): InputAttributes {
|
||||
return InputAttributes(inputType)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
DATETIME,
|
||||
NULL,
|
||||
NUMBER,
|
||||
PHONE,
|
||||
TEXT;
|
||||
}
|
||||
|
||||
enum class Variation {
|
||||
DATE,
|
||||
EMAIL_ADDRESS,
|
||||
EMAIL_SUBJECT,
|
||||
FILTER,
|
||||
LONG_MESSAGE,
|
||||
NORMAL,
|
||||
PASSWORD,
|
||||
PERSON_NAME,
|
||||
PHONETIC,
|
||||
POSTAL_ADDRESS,
|
||||
SHORT_MESSAGE,
|
||||
TIME,
|
||||
URI,
|
||||
VISIBLE_PASSWORD,
|
||||
WEB_EDIT_TEXT,
|
||||
WEB_EMAIL_ADDRESS,
|
||||
WEB_PASSWORD;
|
||||
}
|
||||
|
||||
enum class CapsMode {
|
||||
ALL,
|
||||
NONE,
|
||||
SENTENCES,
|
||||
WORDS;
|
||||
|
||||
companion object {
|
||||
fun fromFlags(flags: Int): CapsMode {
|
||||
return when {
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0 -> ALL
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0 -> SENTENCES
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0 -> WORDS
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toFlags(): Int {
|
||||
return when (this) {
|
||||
ALL -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
||||
SENTENCES -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
WORDS -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which marks a region of [CachedInput.rawText] and which provides length and
|
||||
* validation fields, as well as providing an easy way to get a [text] for this region.
|
||||
@@ -961,7 +766,7 @@ class Selection(private val editorInstance: EditorInstance) : Region(editorInsta
|
||||
* selection change.
|
||||
*/
|
||||
fun updateAndNotify(newStart: Int, newEnd: Int): Boolean {
|
||||
return super.update(newStart, newEnd) && if (!editorInstance.isRawInputEditor) {
|
||||
return super.update(newStart, newEnd) && if (editorInstance.activeState.isRichInputEditor) {
|
||||
editorInstance.inputConnection?.setSelection(newStart, newEnd) ?: false
|
||||
} else {
|
||||
false
|
||||
@@ -1001,8 +806,8 @@ class CachedInput(private val editorInstance: EditorInstance) {
|
||||
private set
|
||||
|
||||
companion object {
|
||||
private const val CACHED_TEXT_N_CHARS_BEFORE_CURSOR: Int = 192
|
||||
private const val CACHED_TEXT_N_CHARS_AFTER_CURSOR: Int = 64
|
||||
private const val CACHED_TEXT_N_CHARS_BEFORE_CURSOR: Int = 128
|
||||
private const val CACHED_TEXT_N_CHARS_AFTER_CURSOR: Int = 48
|
||||
|
||||
private val WORD_EVAL_REGEX = """[^\p{L}\']""".toRegex()
|
||||
private val WORD_SPLIT_REGEX_EN = """((?<=$WORD_EVAL_REGEX)|(?=$WORD_EVAL_REGEX))""".toRegex()
|
||||
@@ -1051,7 +856,7 @@ class CachedInput(private val editorInstance: EditorInstance) {
|
||||
*/
|
||||
fun update() = editorInstance.run {
|
||||
val ic = inputConnection
|
||||
if (ic == null) {
|
||||
if (ic == null || selection.isSelectionMode) {
|
||||
offset = 0
|
||||
rawText.clear()
|
||||
expectedMaxLength = 0
|
||||
|
||||
@@ -25,21 +25,35 @@ import android.inputmethodservice.ExtractEditText
|
||||
import android.inputmethodservice.InputMethodService
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.provider.Settings
|
||||
import android.view.*
|
||||
import android.util.Size
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.Gravity
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import android.view.inputmethod.InlineSuggestionsResponse
|
||||
import android.view.inputmethod.InputConnection
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.lifecycle.*
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.crashutility.CrashUtility
|
||||
import dev.patrickgold.florisboard.debug.*
|
||||
import dev.patrickgold.florisboard.ime.clip.ClipboardInputManager
|
||||
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
|
||||
@@ -49,15 +63,29 @@ import dev.patrickgold.florisboard.ime.media.MediaInputManager
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupLayerView
|
||||
import dev.patrickgold.florisboard.ime.text.TextInputManager
|
||||
import dev.patrickgold.florisboard.ime.text.composing.Appender
|
||||
import dev.patrickgold.florisboard.ime.text.composing.Composer
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.key.*
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.ime.text.key.CurrencySet
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.setup.SetupActivity
|
||||
import dev.patrickgold.florisboard.util.*
|
||||
import kotlinx.coroutines.*
|
||||
import dev.patrickgold.florisboard.util.AppVersionUtils
|
||||
import dev.patrickgold.florisboard.common.ViewUtils
|
||||
import dev.patrickgold.florisboard.databinding.FlorisboardBinding
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardState
|
||||
import dev.patrickgold.florisboard.ime.keyboard.updateKeyboardState
|
||||
import dev.patrickgold.florisboard.util.debugSummarize
|
||||
import dev.patrickgold.florisboard.util.findViewWithType
|
||||
import dev.patrickgold.florisboard.util.refreshLayoutOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
@@ -77,8 +105,11 @@ private var florisboardInstance: FlorisBoard? = null
|
||||
/**
|
||||
* Core class responsible to link together both the text and media input managers as well as
|
||||
* managing the one-handed UI.
|
||||
*
|
||||
* All inline suggestion code has been added based on this demo autofill IME provided by Android directly:
|
||||
* https://cs.android.com/android/platform/superproject/+/master:development/samples/AutofillKeyboard/src/com/example/android/autofillkeyboard/AutofillImeService.java
|
||||
*/
|
||||
class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager.OnPrimaryClipChangedListener,
|
||||
open class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager.OnPrimaryClipChangedListener,
|
||||
ThemeManager.OnThemeUpdatedListener {
|
||||
|
||||
private val serviceLifecycleDispatcher: ServiceLifecycleDispatcher = ServiceLifecycleDispatcher(this)
|
||||
@@ -91,16 +122,15 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
* service class should be used directly.
|
||||
*/
|
||||
private var _themeContext: Context? = null
|
||||
private val themeContext: Context
|
||||
val themeContext: Context
|
||||
get() = _themeContext ?: this
|
||||
|
||||
lateinit var prefs: PrefHelper
|
||||
private set
|
||||
private val prefs: Preferences get() = Preferences.default()
|
||||
val activeState: KeyboardState = KeyboardState.new()
|
||||
|
||||
private var extractEditLayout: WeakReference<ViewGroup?> = WeakReference(null)
|
||||
var inputView: InputView? = null
|
||||
var uiBinding: FlorisboardBinding? = null
|
||||
private set
|
||||
private var inputWindowView: InputWindowView? = null
|
||||
private var extractEditLayout: WeakReference<ViewGroup?> = WeakReference(null)
|
||||
var popupLayerView: PopupLayerView? = null
|
||||
private set
|
||||
private var eventListeners: CopyOnWriteArrayList<EventListener> = CopyOnWriteArrayList()
|
||||
@@ -125,30 +155,40 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
var activeEditorInstance: EditorInstance = EditorInstance.default()
|
||||
|
||||
val subtypeManager: SubtypeManager get() = SubtypeManager.default()
|
||||
val composer: Composer get() = subtypeManager.imeConfig.composerFromName.getValue(activeSubtype.composerName)
|
||||
lateinit var activeSubtype: Subtype
|
||||
private var currentThemeIsNight: Boolean = false
|
||||
private var currentThemeResId: Int = 0
|
||||
private var isNumberRowVisible: Boolean = false
|
||||
private var isWindowShown: Boolean = false
|
||||
|
||||
val textInputManager: TextInputManager
|
||||
val mediaInputManager: MediaInputManager
|
||||
val clipInputManager: ClipboardInputManager
|
||||
private var responseState = ResponseState.RESET
|
||||
private var pendingResponse: Runnable? = null
|
||||
private val handler: Handler = Handler(Looper.getMainLooper())
|
||||
|
||||
lateinit var textInputManager: TextInputManager
|
||||
lateinit var mediaInputManager: MediaInputManager
|
||||
lateinit var clipInputManager: ClipboardInputManager
|
||||
|
||||
var isClipboardContextMenuShown = false
|
||||
|
||||
init {
|
||||
florisboardInstance = this
|
||||
// MUST WRAP all code within Service init in try..catch to prevent any crash loops
|
||||
try {
|
||||
florisboardInstance = this
|
||||
|
||||
textInputManager = TextInputManager.getInstance()
|
||||
mediaInputManager = MediaInputManager.getInstance()
|
||||
clipInputManager = ClipboardInputManager.getInstance()
|
||||
textInputManager = TextInputManager.getInstance()
|
||||
mediaInputManager = MediaInputManager.getInstance()
|
||||
clipInputManager = ClipboardInputManager.getInstance()
|
||||
|
||||
System.loadLibrary("florisboard-native")
|
||||
} catch (e: Exception) {
|
||||
CrashUtility.stageException(e)
|
||||
}
|
||||
}
|
||||
|
||||
lateinit var asyncExecutor: ExecutorService
|
||||
|
||||
companion object {
|
||||
|
||||
@Synchronized
|
||||
fun getInstance(): FlorisBoard {
|
||||
return florisboardInstance!!
|
||||
@@ -176,67 +216,76 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
/*if (BuildConfig.DEBUG) {
|
||||
StrictMode.setThreadPolicy(
|
||||
StrictMode.ThreadPolicy.Builder()
|
||||
.detectDiskReads()
|
||||
.detectDiskWrites()
|
||||
.detectNetwork() // or .detectAll() for all detectable problems
|
||||
.penaltyLog()
|
||||
.build()
|
||||
)
|
||||
StrictMode.setVmPolicy(
|
||||
StrictMode.VmPolicy.Builder()
|
||||
.detectLeakedSqlLiteObjects()
|
||||
.detectLeakedClosableObjects()
|
||||
.penaltyLog()
|
||||
.penaltyDeath()
|
||||
.build()
|
||||
)
|
||||
}*/
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
serviceLifecycleDispatcher.onServicePreSuperOnCreate()
|
||||
// MUST WRAP all code within Service onCreate() in try..catch to prevent any crash loops
|
||||
try {
|
||||
// Additional try..catch wrapper as the event listeners chain or the super.onCreate() method could crash
|
||||
// and lead to a crash loop
|
||||
try {
|
||||
// "Main" try..catch block
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
serviceLifecycleDispatcher.onServicePreSuperOnCreate()
|
||||
|
||||
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
audioManager = getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
||||
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
||||
prefs = PrefHelper.getDefaultInstance(this)
|
||||
prefs.initDefaultPreferences()
|
||||
prefs.sync()
|
||||
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
|
||||
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
audioManager = getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
||||
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
||||
prefs.sync()
|
||||
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
|
||||
|
||||
currentThemeIsNight = themeManager.activeTheme.isNightTheme
|
||||
currentThemeResId = getDayNightBaseThemeId(currentThemeIsNight)
|
||||
isNumberRowVisible = prefs.keyboard.numberRow
|
||||
setTheme(currentThemeResId)
|
||||
themeManager.registerOnThemeUpdatedListener(this)
|
||||
currentThemeIsNight = themeManager.activeTheme.isNightTheme
|
||||
currentThemeResId = getDayNightBaseThemeId(currentThemeIsNight)
|
||||
setTheme(currentThemeResId)
|
||||
themeManager.registerOnThemeUpdatedListener(this)
|
||||
|
||||
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
|
||||
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
|
||||
|
||||
asyncExecutor = Executors.newSingleThreadExecutor()
|
||||
florisClipboardManager = FlorisClipboardManager.getInstance().also {
|
||||
it.initialize(this)
|
||||
it.addPrimaryClipChangedListener(this)
|
||||
asyncExecutor = Executors.newSingleThreadExecutor()
|
||||
florisClipboardManager = FlorisClipboardManager.getInstance().also {
|
||||
it.initialize(this)
|
||||
it.addPrimaryClipChangedListener(this)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
super.onCreate() // MUST CALL even if exception thrown or crash loop is imminent
|
||||
CrashUtility.stageException(e)
|
||||
return
|
||||
}
|
||||
// Code executed here indicates no crashes occurred, so we execute the onCreate() event as normal
|
||||
super.onCreate()
|
||||
eventListeners.toList().forEach { it?.onCreate() }
|
||||
} catch (e: Exception) {
|
||||
CrashUtility.stageException(e)
|
||||
}
|
||||
|
||||
super.onCreate()
|
||||
eventListeners.toList().forEach { it?.onCreate() }
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
override fun onCreateInputView(): View? {
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
CrashUtility.handleStagedButUnhandledExceptions()
|
||||
|
||||
updateThemeContext(currentThemeResId)
|
||||
|
||||
popupLayerView = PopupLayerView(themeContext)
|
||||
window?.window?.findViewById<View>(android.R.id.content)?.let { content ->
|
||||
if (content is ViewGroup) {
|
||||
content.addView(popupLayerView)
|
||||
}
|
||||
}
|
||||
|
||||
inputWindowView = LayoutInflater.from(themeContext).inflate(R.layout.florisboard, null) as? InputWindowView
|
||||
inputWindowView?.isHapticFeedbackEnabled = true
|
||||
uiBinding = FlorisboardBinding.inflate(LayoutInflater.from(themeContext))
|
||||
|
||||
eventListeners.toList().forEach { it?.onCreateInputView() }
|
||||
eventListeners.toList().forEach { it?.onInitializeInputUi(uiBinding!!) }
|
||||
|
||||
return inputWindowView
|
||||
return uiBinding!!.inputWindowView
|
||||
}
|
||||
|
||||
fun initWindow() {
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
|
||||
updateSoftInputWindowLayoutParameters()
|
||||
updateOneHandedPanelVisibility()
|
||||
|
||||
themeManager.requestThemeUpdate(this)
|
||||
|
||||
dispatchCurrentStateToInputUi()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -282,8 +331,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
imeManager = null
|
||||
vibrator = null
|
||||
popupLayerView = null
|
||||
inputView = null
|
||||
inputWindowView = null
|
||||
uiBinding = null
|
||||
florisboardInstance = null
|
||||
|
||||
eventListeners.toList().forEach { it?.onDestroy() }
|
||||
@@ -297,7 +345,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
false
|
||||
} else {
|
||||
when (prefs.keyboard.landscapeInputUiMode) {
|
||||
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeEditorInstance.imeOptions.flagNoFullscreen && !activeEditorInstance.imeOptions.flagNoExtractUi
|
||||
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeState.imeOptions.flagNoFullscreen && !activeState.imeOptions.flagNoExtractUi
|
||||
LandscapeInputUiMode.NEVER_SHOW -> false
|
||||
LandscapeInputUiMode.ALWAYS_SHOW -> true
|
||||
}
|
||||
@@ -311,47 +359,41 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
}
|
||||
|
||||
override fun onUpdateExtractingVisibility(ei: EditorInfo?) {
|
||||
isExtractViewShown = !activeEditorInstance.isRawInputEditor && when (prefs.keyboard.landscapeInputUiMode) {
|
||||
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeEditorInstance.imeOptions.flagNoExtractUi
|
||||
isExtractViewShown = activeState.isRichInputEditor && when (prefs.keyboard.landscapeInputUiMode) {
|
||||
LandscapeInputUiMode.DYNAMICALLY_SHOW -> !activeState.imeOptions.flagNoExtractUi
|
||||
LandscapeInputUiMode.NEVER_SHOW -> false
|
||||
LandscapeInputUiMode.ALWAYS_SHOW -> true
|
||||
}
|
||||
}
|
||||
|
||||
fun registerInputView(inputView: InputView) {
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
|
||||
window?.window?.findViewById<View>(android.R.id.content)?.let { content ->
|
||||
if (content is ViewGroup) {
|
||||
popupLayerView?.let { content.addView(it) }
|
||||
}
|
||||
}
|
||||
this.inputView = inputView
|
||||
updateSoftInputWindowLayoutParameters()
|
||||
updateOneHandedPanelVisibility()
|
||||
themeManager.notifyCallbackReceivers()
|
||||
setActiveInput(R.id.text_input)
|
||||
|
||||
eventListeners.toList().forEach { it?.onRegisterInputView(inputView) }
|
||||
}
|
||||
|
||||
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
|
||||
super.onStartInput(attribute, restarting)
|
||||
responseState = if (responseState == ResponseState.RECEIVE_RESPONSE) {
|
||||
ResponseState.START_INPUT
|
||||
} else {
|
||||
ResponseState.RESET
|
||||
}
|
||||
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
|
||||
}
|
||||
|
||||
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
|
||||
flogInfo(LogTopic.IMS_EVENTS) { "restarting=$restarting"}
|
||||
flogInfo(LogTopic.IMS_EVENTS) { "restarting=$restarting" }
|
||||
flogInfo(LogTopic.IMS_EVENTS) { info?.debugSummarize() ?: "" }
|
||||
|
||||
super.onStartInputView(info, restarting)
|
||||
activeEditorInstance = EditorInstance.from(info, this)
|
||||
prefs.sync()
|
||||
if (info != null) {
|
||||
activeState.update(info)
|
||||
activeState.isSelectionMode = (info.initialSelEnd - info.initialSelStart) != 0
|
||||
}
|
||||
activeEditorInstance = EditorInstance.from(info, this, activeState)
|
||||
themeManager.updateRemoteColorValues(activeEditorInstance.packageName)
|
||||
eventListeners.toList().forEach {
|
||||
it?.onStartInputView(activeEditorInstance, restarting)
|
||||
}
|
||||
dispatchCurrentStateToInputUi()
|
||||
}
|
||||
|
||||
override fun onFinishInputView(finishingInput: Boolean) {
|
||||
@@ -359,10 +401,15 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
|
||||
if (finishingInput) {
|
||||
activeEditorInstance = EditorInstance.default()
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
textInputManager.smartbarView?.clearInlineSuggestions()
|
||||
}
|
||||
}
|
||||
|
||||
super.onFinishInputView(finishingInput)
|
||||
eventListeners.toList().forEach { it?.onFinishInputView(finishingInput) }
|
||||
dispatchCurrentStateToInputUi()
|
||||
}
|
||||
|
||||
override fun onFinishInput() {
|
||||
@@ -372,6 +419,75 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
super.onFinishInput()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest? {
|
||||
return if (prefs.smartbar.enabled && prefs.suggestion.api30InlineSuggestionsEnabled) {
|
||||
flogInfo(LogTopic.IMS_EVENTS) {
|
||||
"Creating inline suggestions request because Smartbar and inline suggestions are enabled."
|
||||
}
|
||||
val stylesBundle = themeManager.createInlineSuggestionUiStyleBundle(themeContext)
|
||||
InlinePresentationSpec.Builder(
|
||||
Size(
|
||||
uiBinding?.inputView?.desiredInlineSuggestionsMinWidth ?: 0,
|
||||
uiBinding?.inputView?.desiredInlineSuggestionsMinHeight ?: 0
|
||||
),
|
||||
Size(
|
||||
uiBinding?.inputView?.desiredInlineSuggestionsMaxWidth ?: 0,
|
||||
uiBinding?.inputView?.desiredInlineSuggestionsMaxHeight ?: 0
|
||||
)
|
||||
).let { spec ->
|
||||
spec.setStyle(stylesBundle)
|
||||
InlineSuggestionsRequest.Builder(listOf(spec.build())).let { request ->
|
||||
request.setMaxSuggestionCount(6)
|
||||
request.build()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
flogInfo(LogTopic.IMS_EVENTS) {
|
||||
"Ignoring inline suggestions request because Smartbar and/or inline suggestions are disabled."
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean {
|
||||
flogInfo(LogTopic.IMS_EVENTS) {
|
||||
"Received inline suggestions response with ${response.inlineSuggestions.size} suggestion(s) provided."
|
||||
}
|
||||
textInputManager.smartbarView?.clearInlineSuggestions()
|
||||
postPendingResponse(response)
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun cancelPendingResponse() {
|
||||
pendingResponse?.let {
|
||||
handler.removeCallbacks(it)
|
||||
pendingResponse = null
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun postPendingResponse(response: InlineSuggestionsResponse) {
|
||||
cancelPendingResponse()
|
||||
val inlineSuggestions = response.inlineSuggestions
|
||||
responseState = ResponseState.RECEIVE_RESPONSE
|
||||
pendingResponse = Runnable {
|
||||
pendingResponse = null
|
||||
if (responseState == ResponseState.START_INPUT && inlineSuggestions.isEmpty()) {
|
||||
textInputManager.smartbarView?.clearInlineSuggestions()
|
||||
} else {
|
||||
textInputManager.smartbarView?.showInlineSuggestions(inlineSuggestions)
|
||||
}
|
||||
responseState = ResponseState.RESET
|
||||
}.also { handler.post(it) }
|
||||
}
|
||||
|
||||
fun dispatchCurrentStateToInputUi() {
|
||||
uiBinding?.inputView?.updateKeyboardState(activeState)
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
super.onWindowShown()
|
||||
if (isWindowShown) {
|
||||
@@ -383,23 +499,23 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
isWindowShown = true
|
||||
|
||||
prefs.sync()
|
||||
val newIsNumberRowVisible = prefs.keyboard.numberRow
|
||||
if (isNumberRowVisible != newIsNumberRowVisible) {
|
||||
textInputManager.keyboards.clear(KeyboardMode.CHARACTERS)
|
||||
isNumberRowVisible = newIsNumberRowVisible
|
||||
val newActiveSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
|
||||
if (newActiveSubtype != activeSubtype) {
|
||||
activeSubtype = newActiveSubtype
|
||||
onSubtypeChanged(activeSubtype, true)
|
||||
} else {
|
||||
onSubtypeChanged(activeSubtype, false)
|
||||
}
|
||||
themeManager.update()
|
||||
updateOneHandedPanelVisibility()
|
||||
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
|
||||
onSubtypeChanged(activeSubtype)
|
||||
setActiveInput(R.id.text_input)
|
||||
updateOneHandedPanelVisibility()
|
||||
themeManager.update()
|
||||
|
||||
if (prefs.devtools.enabled && prefs.devtools.showHeapMemoryStats) {
|
||||
devtoolsOverlaySyncJob?.cancel()
|
||||
devtoolsOverlaySyncJob = uiScope.launch(Dispatchers.Default) {
|
||||
while (true) {
|
||||
if (!isActive) break
|
||||
withContext(Dispatchers.Main) { inputView?.invalidate() }
|
||||
withContext(Dispatchers.Main) { uiBinding?.inputView?.invalidate() }
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
@@ -482,8 +598,9 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
candidatesStart, candidatesEnd
|
||||
)
|
||||
|
||||
activeState.isSelectionMode = (newSelEnd - newSelStart) != 0
|
||||
if (internalBatchNestingLevel == 0) {
|
||||
flogInfo(LogTopic.IMS_EVENTS) { "$oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd" }
|
||||
flogInfo(LogTopic.IMS_EVENTS) { "onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)" }
|
||||
activeEditorInstance.onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd,
|
||||
@@ -491,9 +608,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
)
|
||||
eventListeners.toList().forEach { it?.onUpdateSelection() }
|
||||
} else {
|
||||
flogInfo(LogTopic.IMS_EVENTS) {
|
||||
"$oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd: caught due to internal batch level of $internalBatchNestingLevel!"
|
||||
}
|
||||
flogInfo(LogTopic.IMS_EVENTS) { "onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd): caught due to internal batch level of $internalBatchNestingLevel!" }
|
||||
if (internalSelectionCache.selectionCatchCount++ == 0) {
|
||||
internalSelectionCache.oldSelStart = oldSelStart
|
||||
internalSelectionCache.oldSelEnd = oldSelEnd
|
||||
@@ -503,6 +618,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
internalSelectionCache.candidatesStart = candidatesStart
|
||||
internalSelectionCache.candidatesEnd = candidatesEnd
|
||||
}
|
||||
dispatchCurrentStateToInputUi()
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
@@ -543,8 +659,8 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
w.decorView.systemUiVisibility = flags
|
||||
|
||||
// Update InputView theme
|
||||
inputView?.setBackgroundColor(theme.getAttr(Theme.Attr.KEYBOARD_BACKGROUND).toSolidColor().color)
|
||||
inputView?.invalidate()
|
||||
uiBinding?.inputView?.setBackgroundColor(theme.getAttr(Theme.Attr.KEYBOARD_BACKGROUND).toSolidColor().color)
|
||||
uiBinding?.inputView?.invalidate()
|
||||
|
||||
// Update ExtractTextView theme and attributes
|
||||
extractEditLayout.get()?.let { eel ->
|
||||
@@ -578,8 +694,8 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
|
||||
override fun onComputeInsets(outInsets: Insets?) {
|
||||
super.onComputeInsets(outInsets)
|
||||
val inputView = this.inputView ?: return
|
||||
val inputWindowView = this.inputWindowView ?: return
|
||||
val inputView = uiBinding?.inputView ?: return
|
||||
val inputWindowView = uiBinding?.inputWindowView ?: return
|
||||
// TODO: Check also if the keyboard is currently suppressed by a hardware keyboard
|
||||
|
||||
if (!isInputViewShown) {
|
||||
@@ -602,8 +718,8 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
*/
|
||||
private fun updateSoftInputWindowLayoutParameters() {
|
||||
val w = window?.window ?: return
|
||||
ViewLayoutUtils.updateLayoutHeightOf(w, WindowManager.LayoutParams.MATCH_PARENT)
|
||||
val inputWindowView = this.inputWindowView
|
||||
ViewUtils.updateLayoutHeightOf(w, WindowManager.LayoutParams.MATCH_PARENT)
|
||||
val inputWindowView = uiBinding?.inputWindowView
|
||||
if (inputWindowView != null) {
|
||||
val layoutHeight = if (isFullscreenMode) {
|
||||
WindowManager.LayoutParams.WRAP_CONTENT
|
||||
@@ -611,9 +727,9 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
WindowManager.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
val inputArea = w.findViewById<View>(android.R.id.inputArea)
|
||||
ViewLayoutUtils.updateLayoutHeightOf(inputArea, layoutHeight)
|
||||
ViewLayoutUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM)
|
||||
ViewLayoutUtils.updateLayoutHeightOf(inputWindowView, layoutHeight)
|
||||
ViewUtils.updateLayoutHeightOf(inputArea, layoutHeight)
|
||||
ViewUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM)
|
||||
ViewUtils.updateLayoutHeightOf(inputWindowView, layoutHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,9 +747,9 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
|
||||
val hapticsPerformed = if (vibrationDuration < 0 && vibrationStrength < 0) {
|
||||
if (isMovingGestureEffect && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
inputWindowView?.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE)
|
||||
uiBinding?.inputWindowView?.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE)
|
||||
} else {
|
||||
inputWindowView?.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
uiBinding?.inputWindowView?.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
@@ -763,35 +879,35 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
fun switchToPrevSubtype() {
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
activeSubtype = subtypeManager.switchToPrevSubtype() ?: Subtype.DEFAULT
|
||||
onSubtypeChanged(activeSubtype)
|
||||
onSubtypeChanged(activeSubtype, true)
|
||||
}
|
||||
|
||||
fun switchToNextSubtype() {
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
activeSubtype = subtypeManager.switchToNextSubtype() ?: Subtype.DEFAULT
|
||||
onSubtypeChanged(activeSubtype)
|
||||
onSubtypeChanged(activeSubtype, true)
|
||||
}
|
||||
|
||||
private fun onSubtypeChanged(newSubtype: Subtype) {
|
||||
private fun onSubtypeChanged(newSubtype: Subtype, doRefreshLayouts: Boolean) {
|
||||
flogInfo(LogTopic.SUBTYPE_MANAGER) { "New subtype: $newSubtype" }
|
||||
textInputManager.onSubtypeChanged(newSubtype)
|
||||
mediaInputManager.onSubtypeChanged(newSubtype)
|
||||
clipInputManager.onSubtypeChanged(newSubtype)
|
||||
textInputManager.onSubtypeChanged(newSubtype, doRefreshLayouts)
|
||||
mediaInputManager.onSubtypeChanged(newSubtype, doRefreshLayouts)
|
||||
clipInputManager.onSubtypeChanged(newSubtype, doRefreshLayouts)
|
||||
}
|
||||
|
||||
fun setActiveInput(type: Int, forceSwitchToCharacters: Boolean = false) {
|
||||
when (type) {
|
||||
R.id.text_input -> {
|
||||
inputView?.mainViewFlipper?.displayedChild = 0
|
||||
uiBinding?.mainViewFlipper?.displayedChild = 0
|
||||
if (forceSwitchToCharacters) {
|
||||
textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(TextKeyData.VIEW_CHARACTERS))
|
||||
}
|
||||
}
|
||||
R.id.media_input -> {
|
||||
inputView?.mainViewFlipper?.displayedChild = 1
|
||||
uiBinding?.mainViewFlipper?.displayedChild = 1
|
||||
}
|
||||
R.id.clip_input -> {
|
||||
inputView?.mainViewFlipper?.displayedChild = 2
|
||||
uiBinding?.mainViewFlipper?.displayedChild = 2
|
||||
}
|
||||
}
|
||||
textInputManager.isGlidePostEffect = false
|
||||
@@ -807,27 +923,27 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
|
||||
fun updateOneHandedPanelVisibility() {
|
||||
if (resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
|
||||
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
|
||||
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
|
||||
uiBinding?.oneHandedCtrlPanelStart?.visibility = View.GONE
|
||||
uiBinding?.oneHandedCtrlPanelEnd?.visibility = View.GONE
|
||||
} else {
|
||||
when (prefs.keyboard.oneHandedMode) {
|
||||
OneHandedMode.OFF -> {
|
||||
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
|
||||
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
|
||||
uiBinding?.oneHandedCtrlPanelStart?.visibility = View.GONE
|
||||
uiBinding?.oneHandedCtrlPanelEnd?.visibility = View.GONE
|
||||
}
|
||||
OneHandedMode.START -> {
|
||||
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
|
||||
inputView?.oneHandedCtrlPanelEnd?.visibility = View.VISIBLE
|
||||
uiBinding?.oneHandedCtrlPanelStart?.visibility = View.GONE
|
||||
uiBinding?.oneHandedCtrlPanelEnd?.visibility = View.VISIBLE
|
||||
}
|
||||
OneHandedMode.END -> {
|
||||
inputView?.oneHandedCtrlPanelStart?.visibility = View.VISIBLE
|
||||
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
|
||||
uiBinding?.oneHandedCtrlPanelStart?.visibility = View.VISIBLE
|
||||
uiBinding?.oneHandedCtrlPanelEnd?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
// Delay execution so this function can return, then refresh the whole layout
|
||||
uiScope.launch {
|
||||
refreshLayoutOf(inputView)
|
||||
refreshLayoutOf(uiBinding?.inputView)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,8 +976,7 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
|
||||
interface EventListener {
|
||||
fun onCreate() {}
|
||||
fun onCreateInputView() {}
|
||||
fun onRegisterInputView(inputView: InputView) {}
|
||||
fun onInitializeInputUi(uiBinding: FlorisboardBinding) {}
|
||||
fun onDestroy() {}
|
||||
|
||||
fun onStartInputView(instance: EditorInstance, restarting: Boolean) {}
|
||||
@@ -874,7 +989,11 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
|
||||
fun onApplyThemeAttributes() {}
|
||||
fun onPrimaryClipChanged() {}
|
||||
fun onSubtypeChanged(newSubtype: Subtype) {}
|
||||
fun onSubtypeChanged(newSubtype: Subtype, doRefreshLayouts: Boolean) {}
|
||||
}
|
||||
|
||||
private enum class ResponseState {
|
||||
RESET, RECEIVE_RESPONSE, START_INPUT
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -895,15 +1014,35 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
|
||||
data class ImeConfig(
|
||||
@SerialName("package")
|
||||
val packageName: String,
|
||||
@SerialName("composers")
|
||||
val composers: List<Composer> = listOf(),
|
||||
@SerialName("currencySets")
|
||||
val currencySets: List<CurrencySet> = listOf(),
|
||||
@SerialName("defaultSubtypes")
|
||||
val defaultSubtypes: List<DefaultSubtype> = listOf()
|
||||
) {
|
||||
@Transient var currencySetNames: List<String> = listOf()
|
||||
@Transient var currencySetLabels: List<String> = listOf()
|
||||
@Transient var composerNames: List<String> = listOf()
|
||||
@Transient var composerLabels: List<String> = listOf()
|
||||
@Transient val composerFromName: Map<String, Composer> = composers.map { it.name to it }.toMap()
|
||||
@Transient var defaultSubtypesLanguageCodes: List<String> = listOf()
|
||||
@Transient var defaultSubtypesLanguageNames: List<String> = listOf()
|
||||
|
||||
init {
|
||||
val tmpComposerList = composers.map { Pair(it.name, it.label) }.toMutableList()
|
||||
// Sort composer list alphabetically by the label of a composer
|
||||
tmpComposerList.sortBy { it.second }
|
||||
// Move selected composers to the top of the list
|
||||
for (composerName in listOf(Appender.name)) {
|
||||
val index: Int = tmpComposerList.indexOfFirst { it.first == composerName }
|
||||
if (index > 0) {
|
||||
tmpComposerList.add(0, tmpComposerList.removeAt(index))
|
||||
}
|
||||
}
|
||||
composerNames = tmpComposerList.map { it.first }.toList()
|
||||
composerLabels = tmpComposerList.map { it.second }.toList()
|
||||
|
||||
val tmpCurrencyList = mutableListOf<Pair<String, String>>()
|
||||
for (currencySet in currencySets) {
|
||||
tmpCurrencyList.add(Pair(currencySet.name, currencySet.label))
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.util.SparseArray
|
||||
import androidx.core.util.forEach
|
||||
import androidx.core.util.set
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
|
||||
@@ -36,8 +39,9 @@ class InputEventDispatcher private constructor(
|
||||
private val repeatableKeyCodes: IntArray
|
||||
) : InputKeyEventSender {
|
||||
private val channel: Channel<InputKeyEvent> = Channel(channelCapacity)
|
||||
private val scope: CoroutineScope = CoroutineScope(defaultDispatcher + SupervisorJob())
|
||||
private val pressedKeys: HashMap<Int, PressedKeyInfo> = hashMapOf()
|
||||
private val mainScope: CoroutineScope = CoroutineScope(mainDispatcher + SupervisorJob())
|
||||
private val defaultScope: CoroutineScope = CoroutineScope(defaultDispatcher + SupervisorJob())
|
||||
private val pressedKeys: SparseArray<PressedKeyInfo> = SparseArray()
|
||||
var lastKeyEventDown: InputKeyEvent? = null
|
||||
private set
|
||||
var lastKeyEventUp: InputKeyEvent? = null
|
||||
@@ -53,7 +57,7 @@ class InputEventDispatcher private constructor(
|
||||
/**
|
||||
* The default input event channel capacity to be used in [new].
|
||||
*/
|
||||
private const val DEFAULT_CHANNEL_CAPACITY: Int = 32
|
||||
private const val DEFAULT_CHANNEL_CAPACITY: Int = 64
|
||||
|
||||
/**
|
||||
* Creates a new [InputEventDispatcher] instance from given arguments and returns it.
|
||||
@@ -69,16 +73,26 @@ class InputEventDispatcher private constructor(
|
||||
*/
|
||||
fun new(
|
||||
channelCapacity: Int = DEFAULT_CHANNEL_CAPACITY,
|
||||
mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
|
||||
mainDispatcher: CoroutineDispatcher = Dispatchers.Main.immediate,
|
||||
defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
repeatableKeyCodes: IntArray = intArrayOf()
|
||||
): InputEventDispatcher = InputEventDispatcher(
|
||||
channelCapacity, mainDispatcher, defaultDispatcher, repeatableKeyCodes.clone()
|
||||
)
|
||||
|
||||
private fun <T> SparseArray<T>.removeAndReturn(key: Int): T? {
|
||||
val elem = get(key)
|
||||
return if (elem == null) {
|
||||
null
|
||||
} else {
|
||||
remove(key)
|
||||
elem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
scope.launch(defaultDispatcher) {
|
||||
defaultScope.launch {
|
||||
for (ev in channel) {
|
||||
if (!isActive) break
|
||||
val startTime = System.nanoTime()
|
||||
@@ -87,11 +101,11 @@ class InputEventDispatcher private constructor(
|
||||
}
|
||||
when (ev.action) {
|
||||
InputKeyEvent.Action.DOWN -> {
|
||||
if (pressedKeys.containsKey(ev.data.code)) continue
|
||||
if (pressedKeys.indexOfKey(ev.data.code) >= 0) continue
|
||||
pressedKeys[ev.data.code] = PressedKeyInfo(
|
||||
eventTimeDown = ev.eventTime,
|
||||
repeatKeyPressJob = if (!repeatableKeyCodes.contains(ev.data.code)) { null } else {
|
||||
scope.launch(defaultDispatcher) {
|
||||
defaultScope.launch {
|
||||
delay(600)
|
||||
while (isActive) {
|
||||
channel.send(InputKeyEvent.repeat(ev.data))
|
||||
@@ -108,7 +122,7 @@ class InputEventDispatcher private constructor(
|
||||
}
|
||||
}
|
||||
InputKeyEvent.Action.DOWN_UP -> {
|
||||
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
|
||||
pressedKeys.removeAndReturn(ev.data.code)?.repeatKeyPressJob?.cancel()
|
||||
withContext(mainDispatcher) {
|
||||
keyEventReceiver?.onInputKeyDown(ev)
|
||||
keyEventReceiver?.onInputKeyUp(ev)
|
||||
@@ -119,7 +133,7 @@ class InputEventDispatcher private constructor(
|
||||
}
|
||||
}
|
||||
InputKeyEvent.Action.UP -> {
|
||||
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
|
||||
pressedKeys.removeAndReturn(ev.data.code)?.repeatKeyPressJob?.cancel()
|
||||
withContext(mainDispatcher) {
|
||||
keyEventReceiver?.onInputKeyUp(ev)
|
||||
}
|
||||
@@ -128,14 +142,14 @@ class InputEventDispatcher private constructor(
|
||||
}
|
||||
}
|
||||
InputKeyEvent.Action.REPEAT -> {
|
||||
if (pressedKeys.containsKey(ev.data.code)) {
|
||||
if (pressedKeys.indexOfKey(ev.data.code) >= 0) {
|
||||
withContext(mainDispatcher) {
|
||||
keyEventReceiver?.onInputKeyRepeat(ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
InputKeyEvent.Action.CANCEL -> {
|
||||
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
|
||||
pressedKeys.removeAndReturn(ev.data.code)?.repeatKeyPressJob?.cancel()
|
||||
withContext(mainDispatcher) {
|
||||
keyEventReceiver?.onInputKeyCancel(ev)
|
||||
}
|
||||
@@ -145,16 +159,13 @@ class InputEventDispatcher private constructor(
|
||||
Timber.d("Time elapsed: ${(System.nanoTime() - startTime) / 1_000_000}")
|
||||
}
|
||||
}
|
||||
val pressedKeysIterator = pressedKeys.iterator()
|
||||
while (pressedKeysIterator.hasNext()) {
|
||||
pressedKeysIterator.next().value.repeatKeyPressJob?.cancel()
|
||||
pressedKeysIterator.remove()
|
||||
}
|
||||
pressedKeys.forEach { _, value -> value.repeatKeyPressJob?.cancel() }
|
||||
pressedKeys.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun send(ev: InputKeyEvent) {
|
||||
scope.launch(mainDispatcher) {
|
||||
mainScope.launch {
|
||||
channel.send(ev)
|
||||
}
|
||||
}
|
||||
@@ -167,7 +178,7 @@ class InputEventDispatcher private constructor(
|
||||
* @return True if the given [code] is currently down, false otherwise.
|
||||
*/
|
||||
fun isPressed(code: Int): Boolean {
|
||||
return pressedKeys.containsKey(code)
|
||||
return pressedKeys.indexOfKey(code) >= 0
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,7 +186,8 @@ class InputEventDispatcher private constructor(
|
||||
*/
|
||||
fun close() {
|
||||
keyEventReceiver = null
|
||||
scope.cancel()
|
||||
mainScope.cancel()
|
||||
defaultScope.cancel()
|
||||
}
|
||||
|
||||
data class PressedKeyInfo(
|
||||
|
||||
@@ -22,26 +22,24 @@ import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import android.text.TextPaint
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
import timber.log.Timber
|
||||
import dev.patrickgold.florisboard.common.ViewUtils
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Root view of the keyboard. Notifies [FlorisBoard] when it has been attached to a window.
|
||||
* Root view of the keyboard.
|
||||
*/
|
||||
class InputView : LinearLayout {
|
||||
private var florisboard: FlorisBoard = FlorisBoard.getInstance()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private val florisboard get() = FlorisBoard.getInstance()
|
||||
private val prefs get() = Preferences.default()
|
||||
|
||||
var desiredInputViewHeight: Float = resources.getDimension(R.dimen.inputView_baseHeight)
|
||||
private set
|
||||
@@ -56,11 +54,13 @@ class InputView : LinearLayout {
|
||||
var shouldGiveAdditionalSpace: Boolean = false
|
||||
private set
|
||||
|
||||
var mainViewFlipper: ViewFlipper? = null
|
||||
var desiredInlineSuggestionsMinWidth: Int = 0
|
||||
private set
|
||||
var oneHandedCtrlPanelStart: ViewGroup? = null
|
||||
var desiredInlineSuggestionsMinHeight: Int = 0
|
||||
private set
|
||||
var oneHandedCtrlPanelEnd: ViewGroup? = null
|
||||
var desiredInlineSuggestionsMaxWidth: Int = 0
|
||||
private set
|
||||
var desiredInlineSuggestionsMaxHeight: Int = 0
|
||||
private set
|
||||
|
||||
private val overlayTextPaint: TextPaint = TextPaint().apply {
|
||||
@@ -78,18 +78,6 @@ class InputView : LinearLayout {
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
Timber.i("onAttachedToWindow()")
|
||||
|
||||
super.onAttachedToWindow()
|
||||
|
||||
mainViewFlipper = findViewById(R.id.main_view_flipper)
|
||||
oneHandedCtrlPanelStart = findViewById(R.id.one_handed_ctrl_panel_start)
|
||||
oneHandedCtrlPanelEnd = findViewById(R.id.one_handed_ctrl_panel_end)
|
||||
|
||||
florisboard.registerInputView(this)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
heightFactor = when (resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> 1.0f
|
||||
@@ -123,7 +111,7 @@ class InputView : LinearLayout {
|
||||
baseTextInputHeight += additionalHeight
|
||||
}
|
||||
val smartbarDisabled = !prefs.smartbar.enabled ||
|
||||
tim.keyVariation == KeyVariation.PASSWORD && prefs.keyboard.numberRow
|
||||
tim.activeState.keyVariation == KeyVariation.PASSWORD && prefs.keyboard.numberRow && !prefs.suggestion.api30InlineSuggestionsEnabled
|
||||
if (smartbarDisabled) {
|
||||
baseHeight = baseTextInputHeight
|
||||
baseSmartbarHeight = 0.0f
|
||||
@@ -134,15 +122,22 @@ class InputView : LinearLayout {
|
||||
desiredMediaKeyboardViewHeight = baseHeight
|
||||
// Add bottom offset for curved screens here. As the desired heights have already been set,
|
||||
// adding a value to the height now will result in a bottom padding (aka offset).
|
||||
baseHeight += ViewLayoutUtils.convertDpToPixel(
|
||||
baseHeight += ViewUtils.dp2px(
|
||||
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
florisboard.prefs.keyboard.bottomOffsetLandscape.toFloat()
|
||||
prefs.keyboard.bottomOffsetLandscape.toFloat()
|
||||
} else {
|
||||
florisboard.prefs.keyboard.bottomOffsetPortrait.toFloat()
|
||||
},
|
||||
context
|
||||
prefs.keyboard.bottomOffsetPortrait.toFloat()
|
||||
}
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val width = MeasureSpec.getSize(widthMeasureSpec)
|
||||
desiredInlineSuggestionsMinWidth = width / 3
|
||||
desiredInlineSuggestionsMinHeight = desiredSmartbarHeight.toInt()
|
||||
desiredInlineSuggestionsMaxWidth = (width / 1.5).toInt()
|
||||
desiredInlineSuggestionsMaxHeight = desiredSmartbarHeight.toInt()
|
||||
}
|
||||
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(baseHeight.roundToInt(), MeasureSpec.EXACTLY))
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,17 @@ import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
|
||||
/**
|
||||
* Root view of the keyboard.
|
||||
* Root window view of the keyboard.
|
||||
*/
|
||||
class InputWindowView : FrameLayout {
|
||||
private val florisboard get() = FlorisBoard.getInstanceOrNull()
|
||||
|
||||
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)
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
florisboard?.initWindow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.DistanceThreshold
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.VelocityThreshold
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
|
||||
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.CandidateView
|
||||
@@ -37,7 +38,7 @@ import java.lang.ref.WeakReference
|
||||
/**
|
||||
* Helper class for an organized access to the shared preferences.
|
||||
*/
|
||||
class PrefHelper(
|
||||
class Preferences(
|
||||
context: Context,
|
||||
val shared: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
) {
|
||||
@@ -51,6 +52,7 @@ class PrefHelper(
|
||||
val clipboard = Clipboard(this)
|
||||
val correction = Correction(this)
|
||||
val devtools = Devtools(this)
|
||||
val dictionary = Dictionary(this)
|
||||
val gestures = Gestures(this)
|
||||
val glide = Glide(this)
|
||||
val internal = Internal(this)
|
||||
@@ -128,15 +130,24 @@ class PrefHelper(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val OLD_SUBTYPES_REGEX = """^([\-0-9]+/[\-a-zA-Z0-9]+/[a-zA-Z_]+[;]*)+${'$'}""".toRegex()
|
||||
private var defaultInstance: PrefHelper? = null
|
||||
// old settings are id/language/layout and id/language/currencySet/layout
|
||||
// new settings have composer
|
||||
private val OLD_SUBTYPES_REGEX = """^([\-0-9]+/[\-a-zA-Z0-9]+(/[a-zA-Z_]+)?/[a-zA-Z_]+[;]*)+${'$'}""".toRegex()
|
||||
private var defaultInstance: Preferences? = null
|
||||
|
||||
@Synchronized
|
||||
fun getDefaultInstance(context: Context): PrefHelper {
|
||||
if (defaultInstance == null) {
|
||||
defaultInstance = PrefHelper(context)
|
||||
}
|
||||
return defaultInstance!!
|
||||
fun initDefault(context: Context): Preferences {
|
||||
val instance = Preferences(context.applicationContext)
|
||||
defaultInstance = instance
|
||||
return instance
|
||||
}
|
||||
|
||||
fun default(): Preferences {
|
||||
return defaultInstance
|
||||
?: throw UninitializedPropertyAccessException("""
|
||||
Default preferences not initialized! Make sure to call initDefault()
|
||||
before accessing the default preferences.
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +195,7 @@ class PrefHelper(
|
||||
/**
|
||||
* Wrapper class for advanced preferences.
|
||||
*/
|
||||
class Advanced(private val prefHelper: PrefHelper) {
|
||||
class Advanced(private val prefs: Preferences) {
|
||||
companion object {
|
||||
const val SETTINGS_THEME = "advanced__settings_theme"
|
||||
const val SHOW_APP_ICON = "advanced__show_app_icon"
|
||||
@@ -192,20 +203,20 @@ class PrefHelper(
|
||||
}
|
||||
|
||||
var settingsTheme: String = ""
|
||||
get() = prefHelper.getPref(SETTINGS_THEME, "auto")
|
||||
get() = prefs.getPref(SETTINGS_THEME, "auto")
|
||||
private set
|
||||
var showAppIcon: Boolean = false
|
||||
get() = prefHelper.getPref(SHOW_APP_ICON, true)
|
||||
get() = prefs.getPref(SHOW_APP_ICON, true)
|
||||
private set
|
||||
var forcePrivateMode: Boolean
|
||||
get() = prefHelper.getPref(FORCE_PRIVATE_MODE, false)
|
||||
set(v) = prefHelper.setPref(FORCE_PRIVATE_MODE, v)
|
||||
get() = prefs.getPref(FORCE_PRIVATE_MODE, false)
|
||||
set(v) = prefs.setPref(FORCE_PRIVATE_MODE, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for correction preferences.
|
||||
*/
|
||||
class Correction(private val prefHelper: PrefHelper) {
|
||||
class Correction(private val prefs: Preferences) {
|
||||
companion object {
|
||||
const val AUTO_CAPITALIZATION = "correction__auto_capitalization"
|
||||
const val DOUBLE_SPACE_PERIOD = "correction__double_space_period"
|
||||
@@ -213,37 +224,57 @@ class PrefHelper(
|
||||
}
|
||||
|
||||
var autoCapitalization: Boolean
|
||||
get() = prefHelper.getPref(AUTO_CAPITALIZATION, true)
|
||||
set(v) = prefHelper.setPref(AUTO_CAPITALIZATION, v)
|
||||
get() = prefs.getPref(AUTO_CAPITALIZATION, true)
|
||||
set(v) = prefs.setPref(AUTO_CAPITALIZATION, v)
|
||||
var doubleSpacePeriod: Boolean
|
||||
get() = prefHelper.getPref(DOUBLE_SPACE_PERIOD, true)
|
||||
set(v) = prefHelper.setPref(DOUBLE_SPACE_PERIOD, v)
|
||||
get() = prefs.getPref(DOUBLE_SPACE_PERIOD, true)
|
||||
set(v) = prefs.setPref(DOUBLE_SPACE_PERIOD, v)
|
||||
var rememberCapsLockState: Boolean
|
||||
get() = prefHelper.getPref(REMEMBER_CAPS_LOCK_STATE, false)
|
||||
set(v) = prefHelper.setPref(REMEMBER_CAPS_LOCK_STATE, v)
|
||||
get() = prefs.getPref(REMEMBER_CAPS_LOCK_STATE, false)
|
||||
set(v) = prefs.setPref(REMEMBER_CAPS_LOCK_STATE, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for devtools preferences.
|
||||
*/
|
||||
class Devtools(private val prefHelper: PrefHelper) {
|
||||
class Devtools(private val prefs: Preferences) {
|
||||
companion object {
|
||||
const val ENABLED = "devtools__enabled"
|
||||
const val SHOW_HEAP_MEMORY_STATS = "devtools__show_heap_memory_stats"
|
||||
const val CLEAR_UDM_INTERNAL_DATABASE = "devtools__clear_udm_internal_database"
|
||||
}
|
||||
|
||||
var enabled: Boolean
|
||||
get() = prefHelper.getPref(ENABLED, false)
|
||||
set(v) = prefHelper.setPref(ENABLED, v)
|
||||
get() = prefs.getPref(ENABLED, false)
|
||||
set(v) = prefs.setPref(ENABLED, v)
|
||||
var showHeapMemoryStats: Boolean
|
||||
get() = prefHelper.getPref(SHOW_HEAP_MEMORY_STATS, false)
|
||||
set(v) = prefHelper.setPref(SHOW_HEAP_MEMORY_STATS, v)
|
||||
get() = prefs.getPref(SHOW_HEAP_MEMORY_STATS, false)
|
||||
set(v) = prefs.setPref(SHOW_HEAP_MEMORY_STATS, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for dictionary preferences.
|
||||
*/
|
||||
class Dictionary(private val prefs: Preferences) {
|
||||
companion object {
|
||||
const val ENABLE_SYSTEM_USER_DICTIONARY = "suggestion__enable_system_user_dictionary"
|
||||
const val MANAGE_SYSTEM_USER_DICTIONARY = "suggestion__manage_system_user_dictionary"
|
||||
const val ENABLE_FLORIS_USER_DICTIONARY = "suggestion__enable_floris_user_dictionary"
|
||||
const val MANAGE_FLORIS_USER_DICTIONARY = "suggestion__manage_floris_user_dictionary"
|
||||
}
|
||||
|
||||
var enableSystemUserDictionary: Boolean
|
||||
get() = prefs.getPref(ENABLE_SYSTEM_USER_DICTIONARY, true)
|
||||
set(v) = prefs.setPref(ENABLE_SYSTEM_USER_DICTIONARY, v)
|
||||
var enableFlorisUserDictionary: Boolean
|
||||
get() = prefs.getPref(ENABLE_FLORIS_USER_DICTIONARY, true)
|
||||
set(v) = prefs.setPref(ENABLE_FLORIS_USER_DICTIONARY, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for gestures preferences.
|
||||
*/
|
||||
class Gestures(private val prefHelper: PrefHelper) {
|
||||
class Gestures(private val prefs: Preferences) {
|
||||
companion object {
|
||||
const val SWIPE_UP = "gestures__swipe_up"
|
||||
const val SWIPE_DOWN = "gestures__swipe_down"
|
||||
@@ -259,44 +290,44 @@ class PrefHelper(
|
||||
}
|
||||
|
||||
var swipeUp: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_UP, "no_action"))
|
||||
set(v) = prefHelper.setPref(SWIPE_UP, v)
|
||||
get() = SwipeAction.fromString(prefs.getPref(SWIPE_UP, "no_action"))
|
||||
set(v) = prefs.setPref(SWIPE_UP, v)
|
||||
var swipeDown: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_DOWN, "no_action"))
|
||||
set(v) = prefHelper.setPref(SWIPE_DOWN, v)
|
||||
get() = SwipeAction.fromString(prefs.getPref(SWIPE_DOWN, "no_action"))
|
||||
set(v) = prefs.setPref(SWIPE_DOWN, v)
|
||||
var swipeLeft: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_LEFT, "no_action"))
|
||||
set(v) = prefHelper.setPref(SWIPE_LEFT, v)
|
||||
get() = SwipeAction.fromString(prefs.getPref(SWIPE_LEFT, "no_action"))
|
||||
set(v) = prefs.setPref(SWIPE_LEFT, v)
|
||||
var swipeRight: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_RIGHT, "no_action"))
|
||||
set(v) = prefHelper.setPref(SWIPE_RIGHT, v)
|
||||
get() = SwipeAction.fromString(prefs.getPref(SWIPE_RIGHT, "no_action"))
|
||||
set(v) = prefs.setPref(SWIPE_RIGHT, v)
|
||||
var spaceBarLongPress: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_LONG_PRESS, "no_action"))
|
||||
set(v) = prefHelper.setPref(SPACE_BAR_LONG_PRESS, v)
|
||||
get() = SwipeAction.fromString(prefs.getPref(SPACE_BAR_LONG_PRESS, "no_action"))
|
||||
set(v) = prefs.setPref(SPACE_BAR_LONG_PRESS, v)
|
||||
var spaceBarSwipeUp: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_SWIPE_UP, "no_action"))
|
||||
set(v) = prefHelper.setPref(SPACE_BAR_SWIPE_UP, v)
|
||||
get() = SwipeAction.fromString(prefs.getPref(SPACE_BAR_SWIPE_UP, "no_action"))
|
||||
set(v) = prefs.setPref(SPACE_BAR_SWIPE_UP, v)
|
||||
var spaceBarSwipeLeft: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_SWIPE_LEFT, "no_action"))
|
||||
set(v) = prefHelper.setPref(SPACE_BAR_SWIPE_LEFT, v)
|
||||
get() = SwipeAction.fromString(prefs.getPref(SPACE_BAR_SWIPE_LEFT, "no_action"))
|
||||
set(v) = prefs.setPref(SPACE_BAR_SWIPE_LEFT, v)
|
||||
var spaceBarSwipeRight: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_SWIPE_RIGHT, "no_action"))
|
||||
set(v) = prefHelper.setPref(SPACE_BAR_SWIPE_RIGHT, v)
|
||||
get() = SwipeAction.fromString(prefs.getPref(SPACE_BAR_SWIPE_RIGHT, "no_action"))
|
||||
set(v) = prefs.setPref(SPACE_BAR_SWIPE_RIGHT, v)
|
||||
var deleteKeySwipeLeft: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(DELETE_KEY_SWIPE_LEFT, "no_action"))
|
||||
set(v) = prefHelper.setPref(DELETE_KEY_SWIPE_LEFT, v)
|
||||
get() = SwipeAction.fromString(prefs.getPref(DELETE_KEY_SWIPE_LEFT, "no_action"))
|
||||
set(v) = prefs.setPref(DELETE_KEY_SWIPE_LEFT, v)
|
||||
var swipeVelocityThreshold: VelocityThreshold
|
||||
get() = VelocityThreshold.fromString(prefHelper.getPref(SWIPE_VELOCITY_THRESHOLD, "normal"))
|
||||
set(v) = prefHelper.setPref(SWIPE_VELOCITY_THRESHOLD, v)
|
||||
get() = VelocityThreshold.fromString(prefs.getPref(SWIPE_VELOCITY_THRESHOLD, "normal"))
|
||||
set(v) = prefs.setPref(SWIPE_VELOCITY_THRESHOLD, v)
|
||||
var swipeDistanceThreshold: DistanceThreshold
|
||||
get() = DistanceThreshold.fromString(prefHelper.getPref(SWIPE_DISTANCE_THRESHOLD, "normal"))
|
||||
set(v) = prefHelper.setPref(SWIPE_DISTANCE_THRESHOLD, v)
|
||||
get() = DistanceThreshold.fromString(prefs.getPref(SWIPE_DISTANCE_THRESHOLD, "normal"))
|
||||
set(v) = prefs.setPref(SWIPE_DISTANCE_THRESHOLD, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for glide preferences.
|
||||
*/
|
||||
class Glide(private val prefHelper: PrefHelper) {
|
||||
class Glide(private val prefs: Preferences) {
|
||||
companion object {
|
||||
const val ENABLED = "glide__enabled"
|
||||
const val SHOW_TRAIL = "glide__show_trail"
|
||||
@@ -307,30 +338,30 @@ class PrefHelper(
|
||||
}
|
||||
|
||||
var enabled: Boolean
|
||||
get() = prefHelper.getPref(ENABLED, false)
|
||||
set(v) = prefHelper.setPref(ENABLED, v)
|
||||
get() = prefs.getPref(ENABLED, false)
|
||||
set(v) = prefs.setPref(ENABLED, v)
|
||||
var showTrail: Boolean
|
||||
get() = prefHelper.getPref(SHOW_TRAIL, false)
|
||||
set(v) = prefHelper.setPref(SHOW_TRAIL, v)
|
||||
get() = prefs.getPref(SHOW_TRAIL, false)
|
||||
set(v) = prefs.setPref(SHOW_TRAIL, v)
|
||||
var trailDuration: Int
|
||||
get() = prefHelper.getPref(TRAIL_DURATION, 200)
|
||||
set(v) = prefHelper.setPref(TRAIL_DURATION, v)
|
||||
get() = prefs.getPref(TRAIL_DURATION, 200)
|
||||
set(v) = prefs.setPref(TRAIL_DURATION, v)
|
||||
var showPreview: Boolean
|
||||
get() = prefHelper.getPref(SHOW_PREVIEW, true)
|
||||
set(v) = prefHelper.setPref(SHOW_PREVIEW, v)
|
||||
get() = prefs.getPref(SHOW_PREVIEW, true)
|
||||
set(v) = prefs.setPref(SHOW_PREVIEW, v)
|
||||
var previewRefreshDelay: Int
|
||||
get() = prefHelper.getPref(PREVIEW_REFRESH_DELAY, 150)
|
||||
set(v) = prefHelper.setPref(PREVIEW_REFRESH_DELAY, v)
|
||||
get() = prefs.getPref(PREVIEW_REFRESH_DELAY, 150)
|
||||
set(v) = prefs.setPref(PREVIEW_REFRESH_DELAY, v)
|
||||
var trailMaxLength: Int
|
||||
get() = prefHelper.getPref(MAX_TRAIL_LENGTH, 150)
|
||||
set(v) = prefHelper.setPref(MAX_TRAIL_LENGTH, v)
|
||||
get() = prefs.getPref(MAX_TRAIL_LENGTH, 150)
|
||||
set(v) = prefs.setPref(MAX_TRAIL_LENGTH, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for internal preferences. A preference qualifies as an internal pref if the
|
||||
* user has no ability to control this preference's value directly (via a UI pref view).
|
||||
*/
|
||||
class Internal(private val prefHelper: PrefHelper) {
|
||||
class Internal(private val prefs: Preferences) {
|
||||
companion object {
|
||||
const val IS_IME_SET_UP = "internal__is_ime_set_up"
|
||||
const val VERSION_ON_INSTALL = "internal__version_on_install"
|
||||
@@ -339,23 +370,23 @@ class PrefHelper(
|
||||
}
|
||||
|
||||
var isImeSetUp: Boolean
|
||||
get() = prefHelper.getPref(IS_IME_SET_UP, false)
|
||||
set(v) = prefHelper.setPref(IS_IME_SET_UP, v)
|
||||
get() = prefs.getPref(IS_IME_SET_UP, false)
|
||||
set(v) = prefs.setPref(IS_IME_SET_UP, v)
|
||||
var versionOnInstall: String
|
||||
get() = prefHelper.getPref(VERSION_ON_INSTALL, VersionName.DEFAULT_RAW)
|
||||
set(v) = prefHelper.setPref(VERSION_ON_INSTALL, v)
|
||||
get() = prefs.getPref(VERSION_ON_INSTALL, VersionName.DEFAULT_RAW)
|
||||
set(v) = prefs.setPref(VERSION_ON_INSTALL, v)
|
||||
var versionLastUse: String
|
||||
get() = prefHelper.getPref(VERSION_LAST_USE, VersionName.DEFAULT_RAW)
|
||||
set(v) = prefHelper.setPref(VERSION_LAST_USE, v)
|
||||
get() = prefs.getPref(VERSION_LAST_USE, VersionName.DEFAULT_RAW)
|
||||
set(v) = prefs.setPref(VERSION_LAST_USE, v)
|
||||
var versionLastChangelog: String
|
||||
get() = prefHelper.getPref(VERSION_LAST_CHANGELOG, VersionName.DEFAULT_RAW)
|
||||
set(v) = prefHelper.setPref(VERSION_LAST_CHANGELOG, v)
|
||||
get() = prefs.getPref(VERSION_LAST_CHANGELOG, VersionName.DEFAULT_RAW)
|
||||
set(v) = prefs.setPref(VERSION_LAST_CHANGELOG, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for keyboard preferences.
|
||||
*/
|
||||
class Keyboard(private val prefHelper: PrefHelper) {
|
||||
class Keyboard(private val prefs: Preferences) {
|
||||
companion object {
|
||||
const val BOTTOM_OFFSET_PORTRAIT = "keyboard__bottom_offset_portrait"
|
||||
const val BOTTOM_OFFSET_LANDSCAPE = "keyboard__bottom_offset_landscape"
|
||||
@@ -369,6 +400,7 @@ class PrefHelper(
|
||||
const val KEY_SPACING_VERTICAL = "keyboard__key_spacing_vertical"
|
||||
const val LANDSCAPE_INPUT_UI_MODE = "keyboard__landscape_input_ui_mode"
|
||||
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
|
||||
const val MERGE_HINT_POPUPS_ENABLED = "keyboard__merge_hint_popups_enabled"
|
||||
const val NUMBER_ROW = "keyboard__number_row"
|
||||
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
|
||||
const val ONE_HANDED_MODE_SCALE_FACTOR = "keyboard__one_handed_mode_scale_factor"
|
||||
@@ -384,148 +416,159 @@ class PrefHelper(
|
||||
}
|
||||
|
||||
var bottomOffsetPortrait: Int = 0
|
||||
get() = prefHelper.getPref(BOTTOM_OFFSET_PORTRAIT, 0)
|
||||
get() = prefs.getPref(BOTTOM_OFFSET_PORTRAIT, 0)
|
||||
private set
|
||||
var bottomOffsetLandscape: Int = 0
|
||||
get() = prefHelper.getPref(BOTTOM_OFFSET_LANDSCAPE, 0)
|
||||
get() = prefs.getPref(BOTTOM_OFFSET_LANDSCAPE, 0)
|
||||
private set
|
||||
var fontSizeMultiplierPortrait: Int
|
||||
get() = prefHelper.getPref(FONT_SIZE_MULTIPLIER_PORTRAIT, 100)
|
||||
set(v) = prefHelper.setPref(FONT_SIZE_MULTIPLIER_PORTRAIT, v)
|
||||
get() = prefs.getPref(FONT_SIZE_MULTIPLIER_PORTRAIT, 100)
|
||||
set(v) = prefs.setPref(FONT_SIZE_MULTIPLIER_PORTRAIT, v)
|
||||
var fontSizeMultiplierLandscape: Int
|
||||
get() = prefHelper.getPref(FONT_SIZE_MULTIPLIER_LANDSCAPE, 100)
|
||||
set(v) = prefHelper.setPref(FONT_SIZE_MULTIPLIER_LANDSCAPE, v)
|
||||
get() = prefs.getPref(FONT_SIZE_MULTIPLIER_LANDSCAPE, 100)
|
||||
set(v) = prefs.setPref(FONT_SIZE_MULTIPLIER_LANDSCAPE, v)
|
||||
var heightFactor: String = ""
|
||||
get() = prefHelper.getPref(HEIGHT_FACTOR, "normal")
|
||||
get() = prefs.getPref(HEIGHT_FACTOR, "normal")
|
||||
private set
|
||||
var heightFactorCustom: Int
|
||||
get() = prefHelper.getPref(HEIGHT_FACTOR_CUSTOM, 100)
|
||||
set(v) = prefHelper.setPref(HEIGHT_FACTOR_CUSTOM, v)
|
||||
get() = prefs.getPref(HEIGHT_FACTOR_CUSTOM, 100)
|
||||
set(v) = prefs.setPref(HEIGHT_FACTOR_CUSTOM, v)
|
||||
var hintedNumberRowMode: KeyHintMode
|
||||
get() = KeyHintMode.fromString(prefHelper.getPref(HINTED_NUMBER_ROW_MODE, KeyHintMode.ENABLED_ACCENT_PRIORITY.toString()))
|
||||
set(v) = prefHelper.setPref(HINTED_NUMBER_ROW_MODE, v)
|
||||
get() = KeyHintMode.fromString(prefs.getPref(HINTED_NUMBER_ROW_MODE, KeyHintMode.ENABLED_ACCENT_PRIORITY.toString()))
|
||||
set(v) = prefs.setPref(HINTED_NUMBER_ROW_MODE, v)
|
||||
var hintedSymbolsMode: KeyHintMode
|
||||
get() = KeyHintMode.fromString(prefHelper.getPref(HINTED_SYMBOLS_MODE, KeyHintMode.ENABLED_ACCENT_PRIORITY.toString()))
|
||||
set(v) = prefHelper.setPref(HINTED_SYMBOLS_MODE, v)
|
||||
get() = KeyHintMode.fromString(prefs.getPref(HINTED_SYMBOLS_MODE, KeyHintMode.ENABLED_ACCENT_PRIORITY.toString()))
|
||||
set(v) = prefs.setPref(HINTED_SYMBOLS_MODE, v)
|
||||
var keySpacingHorizontal: Float = 2f
|
||||
get() = prefHelper.getPref(KEY_SPACING_HORIZONTAL, 4) / 2f
|
||||
get() = prefs.getPref(KEY_SPACING_HORIZONTAL, 4) / 2f
|
||||
private set
|
||||
var keySpacingVertical: Float = 5f
|
||||
get() = prefHelper.getPref(KEY_SPACING_VERTICAL, 10) / 2f
|
||||
get() = prefs.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)
|
||||
get() = LandscapeInputUiMode.fromString(prefs.getPref(LANDSCAPE_INPUT_UI_MODE, LandscapeInputUiMode.DYNAMICALLY_SHOW.toString()))
|
||||
set(v) = prefs.setPref(LANDSCAPE_INPUT_UI_MODE, v)
|
||||
var longPressDelay: Int = 0
|
||||
get() = prefHelper.getPref(LONG_PRESS_DELAY, 300)
|
||||
get() = prefs.getPref(LONG_PRESS_DELAY, 300)
|
||||
private set
|
||||
var mergeHintPopupsEnabled: Boolean
|
||||
get() = prefs.getPref(MERGE_HINT_POPUPS_ENABLED, false)
|
||||
set(v) = prefs.setPref(MERGE_HINT_POPUPS_ENABLED, v)
|
||||
var numberRow: Boolean
|
||||
get() = prefHelper.getPref(NUMBER_ROW, false)
|
||||
set(v) = prefHelper.setPref(NUMBER_ROW, v)
|
||||
get() = prefs.getPref(NUMBER_ROW, false)
|
||||
set(v) = prefs.setPref(NUMBER_ROW, v)
|
||||
var oneHandedMode: String
|
||||
get() = prefHelper.getPref(ONE_HANDED_MODE, OneHandedMode.OFF)
|
||||
set(value) = prefHelper.setPref(ONE_HANDED_MODE, value)
|
||||
get() = prefs.getPref(ONE_HANDED_MODE, OneHandedMode.OFF)
|
||||
set(value) = prefs.setPref(ONE_HANDED_MODE, value)
|
||||
var oneHandedModeScaleFactor: Int
|
||||
get() = prefHelper.getPref(ONE_HANDED_MODE_SCALE_FACTOR, 87)
|
||||
set(v) = prefHelper.setPref(ONE_HANDED_MODE_SCALE_FACTOR, v)
|
||||
get() = prefs.getPref(ONE_HANDED_MODE_SCALE_FACTOR, 87)
|
||||
set(v) = prefs.setPref(ONE_HANDED_MODE_SCALE_FACTOR, v)
|
||||
var popupEnabled: Boolean = false
|
||||
get() = prefHelper.getPref(POPUP_ENABLED, true)
|
||||
get() = prefs.getPref(POPUP_ENABLED, true)
|
||||
private set
|
||||
var soundEnabled: Boolean = false
|
||||
get() = prefHelper.getPref(SOUND_ENABLED, true)
|
||||
get() = prefs.getPref(SOUND_ENABLED, true)
|
||||
private set
|
||||
var soundEnabledSystem: Boolean = false
|
||||
var soundVolume: Int = 0
|
||||
get() = prefHelper.getPref(SOUND_VOLUME, -1)
|
||||
get() = prefs.getPref(SOUND_VOLUME, -1)
|
||||
private set
|
||||
var spaceBarSwitchesToCharacters: Boolean
|
||||
get() = prefHelper.getPref(SPACE_BAR_SWITCHES_TO_CHARACTERS, true)
|
||||
set(v) = prefHelper.setPref(SPACE_BAR_SWITCHES_TO_CHARACTERS, v)
|
||||
get() = prefs.getPref(SPACE_BAR_SWITCHES_TO_CHARACTERS, true)
|
||||
set(v) = prefs.setPref(SPACE_BAR_SWITCHES_TO_CHARACTERS, 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)
|
||||
get() = UtilityKeyAction.fromString(prefs.getPref(UTILITY_KEY_ACTION, UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS.toString()))
|
||||
set(v) = prefs.setPref(UTILITY_KEY_ACTION, v)
|
||||
var utilityKeyEnabled: Boolean
|
||||
get() = prefHelper.getPref(UTILITY_KEY_ENABLED, true)
|
||||
set(v) = prefHelper.setPref(UTILITY_KEY_ENABLED, v)
|
||||
get() = prefs.getPref(UTILITY_KEY_ENABLED, true)
|
||||
set(v) = prefs.setPref(UTILITY_KEY_ENABLED, v)
|
||||
var vibrationEnabled: Boolean = false
|
||||
get() = prefHelper.getPref(VIBRATION_ENABLED, true)
|
||||
get() = prefs.getPref(VIBRATION_ENABLED, true)
|
||||
private set
|
||||
var vibrationEnabledSystem: Boolean = false
|
||||
var vibrationDuration: Int = 0
|
||||
get() = prefHelper.getPref(VIBRATION_DURATION, -1)
|
||||
get() = prefs.getPref(VIBRATION_DURATION, -1)
|
||||
private set
|
||||
var vibrationStrength: Int = 0
|
||||
get() = prefHelper.getPref(VIBRATION_STRENGTH, -1)
|
||||
get() = prefs.getPref(VIBRATION_STRENGTH, -1)
|
||||
private set
|
||||
|
||||
fun keyHintConfiguration(): KeyHintConfiguration {
|
||||
return KeyHintConfiguration(hintedSymbolsMode, hintedNumberRowMode, mergeHintPopupsEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for localization preferences.
|
||||
*/
|
||||
class Localization(private val prefHelper: PrefHelper) {
|
||||
class Localization(private val prefs: Preferences) {
|
||||
companion object {
|
||||
const val ACTIVE_SUBTYPE_ID = "localization__active_subtype_id"
|
||||
const val SUBTYPES = "localization__subtypes"
|
||||
}
|
||||
|
||||
var activeSubtypeId: Int
|
||||
get() = prefHelper.getPref(ACTIVE_SUBTYPE_ID, Subtype.DEFAULT.id)
|
||||
set(v) = prefHelper.setPref(ACTIVE_SUBTYPE_ID, v)
|
||||
get() = prefs.getPref(ACTIVE_SUBTYPE_ID, Subtype.DEFAULT.id)
|
||||
set(v) = prefs.setPref(ACTIVE_SUBTYPE_ID, v)
|
||||
var subtypes: String
|
||||
get() = prefHelper.getPref(SUBTYPES, "")
|
||||
set(v) = prefHelper.setPref(SUBTYPES, v)
|
||||
get() = prefs.getPref(SUBTYPES, "")
|
||||
set(v) = prefs.setPref(SUBTYPES, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for Smartbar preferences.
|
||||
*/
|
||||
class Smartbar(private val prefHelper: PrefHelper) {
|
||||
class Smartbar(private val prefs: Preferences) {
|
||||
companion object {
|
||||
const val ENABLED = "smartbar__enabled"
|
||||
}
|
||||
|
||||
var enabled: Boolean
|
||||
get() = prefHelper.getPref(ENABLED, true)
|
||||
set(v) = prefHelper.setPref(ENABLED, v)
|
||||
get() = prefs.getPref(ENABLED, true)
|
||||
set(v) = prefs.setPref(ENABLED, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for suggestion preferences.
|
||||
*/
|
||||
class Suggestion(private val prefHelper: PrefHelper) {
|
||||
class Suggestion(private val prefs: Preferences) {
|
||||
companion object {
|
||||
const val BLOCK_POSSIBLY_OFFENSIVE = "suggestion__block_possibly_offensive"
|
||||
const val CLIPBOARD_CONTENT_ENABLED = "suggestion__clipboard_content_enabled"
|
||||
const val CLIPBOARD_CONTENT_TIMEOUT = "suggestion__clipboard_content_timeout"
|
||||
const val DISPLAY_MODE = "suggestion__display_mode"
|
||||
const val ENABLED = "suggestion__enabled"
|
||||
const val USE_PREV_WORDS = "suggestion__use_prev_words"
|
||||
const val API30_INLINE_SUGGESTIONS_ENABLED = "suggestion__api30_inline_suggestions_enabled"
|
||||
const val BLOCK_POSSIBLY_OFFENSIVE = "suggestion__block_possibly_offensive"
|
||||
const val CLIPBOARD_CONTENT_ENABLED = "suggestion__clipboard_content_enabled"
|
||||
const val CLIPBOARD_CONTENT_TIMEOUT = "suggestion__clipboard_content_timeout"
|
||||
const val DISPLAY_MODE = "suggestion__display_mode"
|
||||
const val ENABLED = "suggestion__enabled"
|
||||
const val USE_PREV_WORDS = "suggestion__use_prev_words"
|
||||
}
|
||||
|
||||
var api30InlineSuggestionsEnabled: Boolean
|
||||
get() = prefs.getPref(API30_INLINE_SUGGESTIONS_ENABLED, true)
|
||||
set(v) = prefs.setPref(API30_INLINE_SUGGESTIONS_ENABLED, v)
|
||||
var blockPossiblyOffensive: Boolean
|
||||
get() = prefHelper.getPref(BLOCK_POSSIBLY_OFFENSIVE, true)
|
||||
set(v) = prefHelper.setPref(BLOCK_POSSIBLY_OFFENSIVE, v)
|
||||
get() = prefs.getPref(BLOCK_POSSIBLY_OFFENSIVE, true)
|
||||
set(v) = prefs.setPref(BLOCK_POSSIBLY_OFFENSIVE, v)
|
||||
var clipboardContentEnabled: Boolean
|
||||
get() = prefHelper.getPref(CLIPBOARD_CONTENT_ENABLED, false)
|
||||
set(v) = prefHelper.setPref(CLIPBOARD_CONTENT_ENABLED, v)
|
||||
get() = prefs.getPref(CLIPBOARD_CONTENT_ENABLED, false)
|
||||
set(v) = prefs.setPref(CLIPBOARD_CONTENT_ENABLED, v)
|
||||
var clipboardContentTimeout: Int
|
||||
get() = prefHelper.getPref(CLIPBOARD_CONTENT_TIMEOUT, 30)
|
||||
set(v) = prefHelper.setPref(CLIPBOARD_CONTENT_TIMEOUT, v)
|
||||
get() = prefs.getPref(CLIPBOARD_CONTENT_TIMEOUT, 30)
|
||||
set(v) = prefs.setPref(CLIPBOARD_CONTENT_TIMEOUT, v)
|
||||
var displayMode: CandidateView.DisplayMode
|
||||
get() = CandidateView.DisplayMode.fromString(prefHelper.getPref(DISPLAY_MODE, CandidateView.DisplayMode.DYNAMIC_SCROLLABLE.toString()))
|
||||
set(v) = prefHelper.setPref(DISPLAY_MODE, v)
|
||||
get() = CandidateView.DisplayMode.fromString(prefs.getPref(DISPLAY_MODE, CandidateView.DisplayMode.DYNAMIC_SCROLLABLE.toString()))
|
||||
set(v) = prefs.setPref(DISPLAY_MODE, v)
|
||||
var enabled: Boolean
|
||||
get() = prefHelper.getPref(ENABLED, true)
|
||||
set(v) = prefHelper.setPref(ENABLED, v)
|
||||
get() = prefs.getPref(ENABLED, true)
|
||||
set(v) = prefs.setPref(ENABLED, v)
|
||||
var usePrevWords: Boolean
|
||||
get() = prefHelper.getPref(USE_PREV_WORDS, true)
|
||||
set(v) = prefHelper.setPref(USE_PREV_WORDS, v)
|
||||
get() = prefs.getPref(USE_PREV_WORDS, true)
|
||||
set(v) = prefs.setPref(USE_PREV_WORDS, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for theme preferences.
|
||||
*/
|
||||
class Theme(private val prefHelper: PrefHelper) {
|
||||
class Theme(private val prefs: Preferences) {
|
||||
companion object {
|
||||
const val MODE = "theme__mode"
|
||||
const val DAY_THEME_REF = "theme__day_theme_ref"
|
||||
@@ -537,32 +580,32 @@ class PrefHelper(
|
||||
}
|
||||
|
||||
var mode: ThemeMode
|
||||
get() = ThemeMode.fromString(prefHelper.getPref(MODE, ThemeMode.FOLLOW_SYSTEM.toString()))
|
||||
set(v) = prefHelper.setPref(MODE, v)
|
||||
get() = ThemeMode.fromString(prefs.getPref(MODE, ThemeMode.FOLLOW_SYSTEM.toString()))
|
||||
set(v) = prefs.setPref(MODE, v)
|
||||
var dayThemeRef: String
|
||||
get() = prefHelper.getPref(DAY_THEME_REF, "assets:ime/theme/floris_day.json")
|
||||
set(v) = prefHelper.setPref(DAY_THEME_REF, v)
|
||||
get() = prefs.getPref(DAY_THEME_REF, "assets:ime/theme/floris_day.json")
|
||||
set(v) = prefs.setPref(DAY_THEME_REF, v)
|
||||
var dayThemeAdaptToApp: Boolean
|
||||
get() = prefHelper.getPref(DAY_THEME_ADAPT_TO_APP, false)
|
||||
set(v) = prefHelper.setPref(DAY_THEME_ADAPT_TO_APP, v)
|
||||
get() = prefs.getPref(DAY_THEME_ADAPT_TO_APP, false)
|
||||
set(v) = prefs.setPref(DAY_THEME_ADAPT_TO_APP, v)
|
||||
var nightThemeRef: String
|
||||
get() = prefHelper.getPref(NIGHT_THEME_REF, "assets:ime/theme/floris_night.json")
|
||||
set(v) = prefHelper.setPref(NIGHT_THEME_REF, v)
|
||||
get() = prefs.getPref(NIGHT_THEME_REF, "assets:ime/theme/floris_night.json")
|
||||
set(v) = prefs.setPref(NIGHT_THEME_REF, v)
|
||||
var nightThemeAdaptToApp: Boolean
|
||||
get() = prefHelper.getPref(NIGHT_THEME_ADAPT_TO_APP, false)
|
||||
set(v) = prefHelper.setPref(NIGHT_THEME_ADAPT_TO_APP, v)
|
||||
get() = prefs.getPref(NIGHT_THEME_ADAPT_TO_APP, false)
|
||||
set(v) = prefs.setPref(NIGHT_THEME_ADAPT_TO_APP, v)
|
||||
var sunriseTime: Int
|
||||
get() = prefHelper.getPref(SUNRISE_TIME, TimeUtil.encode(6, 0))
|
||||
set(v) = prefHelper.setPref(SUNRISE_TIME, v)
|
||||
get() = prefs.getPref(SUNRISE_TIME, TimeUtil.encode(6, 0))
|
||||
set(v) = prefs.setPref(SUNRISE_TIME, v)
|
||||
var sunsetTime: Int
|
||||
get() = prefHelper.getPref(SUNSET_TIME, TimeUtil.encode(18, 0))
|
||||
set(v) = prefHelper.setPref(SUNSET_TIME, v)
|
||||
get() = prefs.getPref(SUNSET_TIME, TimeUtil.encode(18, 0))
|
||||
set(v) = prefs.setPref(SUNSET_TIME, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for clipboard preferences
|
||||
*/
|
||||
class Clipboard(private val prefHelper: PrefHelper) {
|
||||
class Clipboard(private val prefs: Preferences) {
|
||||
companion object {
|
||||
const val ENABLE_INTERNAL = "clipboard__enable_internal"
|
||||
const val SYNC_TO_SYSTEM = "clipboard__sync_to_system"
|
||||
@@ -575,35 +618,35 @@ class PrefHelper(
|
||||
}
|
||||
|
||||
var enableInternal: Boolean
|
||||
get() = prefHelper.getPref(ENABLE_INTERNAL, false)
|
||||
set(v) = prefHelper.setPref(ENABLE_INTERNAL, v)
|
||||
get() = prefs.getPref(ENABLE_INTERNAL, false)
|
||||
set(v) = prefs.setPref(ENABLE_INTERNAL, v)
|
||||
|
||||
var syncToSystem: Boolean
|
||||
get() = prefHelper.getPref(SYNC_TO_SYSTEM, false)
|
||||
set(v) = prefHelper.setPref(SYNC_TO_SYSTEM, v)
|
||||
get() = prefs.getPref(SYNC_TO_SYSTEM, false)
|
||||
set(v) = prefs.setPref(SYNC_TO_SYSTEM, v)
|
||||
|
||||
var syncToFloris: Boolean
|
||||
get() = prefHelper.getPref(SYNC_TO_FLORIS, true)
|
||||
set(v) = prefHelper.setPref(SYNC_TO_FLORIS, v)
|
||||
get() = prefs.getPref(SYNC_TO_FLORIS, true)
|
||||
set(v) = prefs.setPref(SYNC_TO_FLORIS, v)
|
||||
|
||||
var enableHistory: Boolean
|
||||
get() = prefHelper.getPref(ENABLE_HISTORY, false)
|
||||
set(v) = prefHelper.setPref(ENABLE_HISTORY, v)
|
||||
get() = prefs.getPref(ENABLE_HISTORY, false)
|
||||
set(v) = prefs.setPref(ENABLE_HISTORY, v)
|
||||
|
||||
var cleanUpOld: Boolean
|
||||
get() = prefHelper.getPref(CLEAN_UP_OLD, false)
|
||||
set(v) = prefHelper.setPref(CLEAN_UP_OLD, v)
|
||||
get() = prefs.getPref(CLEAN_UP_OLD, false)
|
||||
set(v) = prefs.setPref(CLEAN_UP_OLD, v)
|
||||
|
||||
var limitHistorySize: Boolean
|
||||
get() = prefHelper.getPref(LIMIT_HISTORY_SIZE, true)
|
||||
set(v) = prefHelper.setPref(LIMIT_HISTORY_SIZE, v)
|
||||
get() = prefs.getPref(LIMIT_HISTORY_SIZE, true)
|
||||
set(v) = prefs.setPref(LIMIT_HISTORY_SIZE, v)
|
||||
|
||||
var cleanUpAfter: Int
|
||||
get() = prefHelper.getPref(CLEAN_UP_AFTER, 20)
|
||||
set(v) = prefHelper.setPref(CLEAN_UP_AFTER, v)
|
||||
get() = prefs.getPref(CLEAN_UP_AFTER, 20)
|
||||
set(v) = prefs.setPref(CLEAN_UP_AFTER, v)
|
||||
|
||||
var maxHistorySize: Int
|
||||
get() = prefHelper.getPref(MAX_HISTORY_SIZE, 20)
|
||||
set(v) = prefHelper.setPref(MAX_HISTORY_SIZE, v)
|
||||
get() = prefs.getPref(MAX_HISTORY_SIZE, 20)
|
||||
set(v) = prefs.setPref(MAX_HISTORY_SIZE, v)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import dev.patrickgold.florisboard.ime.text.composing.Appender
|
||||
import dev.patrickgold.florisboard.ime.text.composing.Composer
|
||||
import dev.patrickgold.florisboard.ime.text.layout.LayoutType
|
||||
import dev.patrickgold.florisboard.util.LocaleUtils
|
||||
import kotlinx.serialization.*
|
||||
@@ -32,12 +34,14 @@ import java.util.*
|
||||
* @property id The ID of this subtype. Although this can be any numeric value, its value
|
||||
* typically matches the one of the [DefaultSubtype] with the same locale.
|
||||
* @property locale The locale this subtype is bound to.
|
||||
* @property composerName The composer name to composer characters the way they should.
|
||||
* @property currencySetName The currency set name to display the correct currency symbols for this subtype.
|
||||
* @property layoutMap The layout map to properly display the correct layout for each layout type.
|
||||
*/
|
||||
data class Subtype(
|
||||
val id: Int,
|
||||
val locale: Locale,
|
||||
val composerName: String,
|
||||
val currencySetName: String,
|
||||
val layoutMap: SubtypeLayoutMap,
|
||||
) {
|
||||
@@ -50,6 +54,7 @@ data class Subtype(
|
||||
val DEFAULT = Subtype(
|
||||
id = -1,
|
||||
locale = Locale.ENGLISH,
|
||||
composerName = Appender.name,
|
||||
currencySetName = "\$default",
|
||||
layoutMap = SubtypeLayoutMap(characters = "qwerty")
|
||||
)
|
||||
@@ -67,17 +72,29 @@ data class Subtype(
|
||||
*/
|
||||
fun fromString(str: String): Subtype {
|
||||
val data = str.split("/")
|
||||
if (data.size != 4) {
|
||||
throw InvalidPropertiesFormatException(
|
||||
"Given string contains more or less than 4 properties..."
|
||||
)
|
||||
} else {
|
||||
val locale = LocaleUtils.stringToLocale(data[1])
|
||||
return Subtype(
|
||||
data[0].toInt(),
|
||||
locale,
|
||||
data[2],
|
||||
SubtypeLayoutMap.fromString(data[3])
|
||||
when (data.size) {
|
||||
4 -> {
|
||||
val locale = LocaleUtils.stringToLocale(data[1])
|
||||
return Subtype(
|
||||
data[0].toInt(),
|
||||
locale,
|
||||
Appender.name,
|
||||
data[2],
|
||||
SubtypeLayoutMap.fromString(data[3])
|
||||
)
|
||||
}
|
||||
5 -> {
|
||||
val locale = LocaleUtils.stringToLocale(data[1])
|
||||
return Subtype(
|
||||
data[0].toInt(),
|
||||
locale,
|
||||
data[2],
|
||||
data[3],
|
||||
SubtypeLayoutMap.fromString(data[4])
|
||||
)
|
||||
}
|
||||
else -> throw InvalidPropertiesFormatException(
|
||||
"Given string contains more or less than 5 properties..."
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -86,6 +103,7 @@ data class Subtype(
|
||||
init {
|
||||
var result = id
|
||||
result = 31 * result + locale.hashCode()
|
||||
result = 31 * result + composerName.hashCode()
|
||||
result = 31 * result + currencySetName.hashCode()
|
||||
result = 31 * result + layoutMap.hashCode()
|
||||
_hashCode = result
|
||||
@@ -93,11 +111,11 @@ data class Subtype(
|
||||
|
||||
/**
|
||||
* Converts this object into its string representation. Format:
|
||||
* <id>/<language_tag>/<currency_set_name>/<layout_map>
|
||||
* <id>/<language_tag>/<composer_name>/<currency_set_name>/<layout_map>
|
||||
*/
|
||||
override fun toString(): String {
|
||||
val languageTag = locale.toLanguageTag()
|
||||
return "$id/$languageTag/$currencySetName/$layoutMap"
|
||||
return "$id/$languageTag/$composerName/$currencySetName/$layoutMap"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,6 +135,7 @@ data class Subtype(
|
||||
|
||||
if (id != other.id) return false
|
||||
if (locale != other.locale) return false
|
||||
if (composerName != other.composerName) return false
|
||||
if (currencySetName != other.currencySetName) return false
|
||||
if (layoutMap != other.layoutMap) return false
|
||||
|
||||
@@ -310,6 +329,8 @@ data class DefaultSubtype(
|
||||
@Serializable(with = LocaleSerializer::class)
|
||||
@SerialName("languageTag")
|
||||
var locale: Locale,
|
||||
@SerialName("composer")
|
||||
var composerName: String,
|
||||
@SerialName("currencySet")
|
||||
var currencySetName: String,
|
||||
var preferred: SubtypeLayoutMap
|
||||
|
||||
@@ -37,10 +37,10 @@ import kotlin.collections.ArrayList
|
||||
* @property subtypes The currently active subtypes.
|
||||
*/
|
||||
class SubtypeManager(
|
||||
private val packageName: String,
|
||||
private val prefs: PrefHelper
|
||||
private val packageName: String
|
||||
) : CoroutineScope by MainScope() {
|
||||
private val assetManager get() = AssetManager.default()
|
||||
private val prefs get() = Preferences.default()
|
||||
|
||||
companion object {
|
||||
const val IME_CONFIG_FILE_PATH = "ime/config.json"
|
||||
@@ -48,8 +48,8 @@ class SubtypeManager(
|
||||
|
||||
private var instance: SubtypeManager? = null
|
||||
|
||||
fun init(context: Context, prefs: PrefHelper): SubtypeManager {
|
||||
val defaultInstance = SubtypeManager(context.packageName, prefs)
|
||||
fun init(context: Context): SubtypeManager {
|
||||
val defaultInstance = SubtypeManager(context.packageName)
|
||||
instance = defaultInstance
|
||||
return defaultInstance
|
||||
}
|
||||
@@ -112,16 +112,18 @@ class SubtypeManager(
|
||||
* list, if it does not exist.
|
||||
*
|
||||
* @param locale The locale of the subtype to be added.
|
||||
* @param composerName The composer name of the subtype to be added.
|
||||
* @param currencySetName The currency set name of the subtype to be added.
|
||||
* @param layoutMap The layout map of the subtype to be added.
|
||||
* @return True if the subtype was added, false otherwise. A return value of false indicates
|
||||
* that the subtype already exists.
|
||||
*/
|
||||
fun addSubtype(locale: Locale, currencySetName: String, layoutMap: SubtypeLayoutMap): Boolean {
|
||||
fun addSubtype(locale: Locale, composerName: String, currencySetName: String, layoutMap: SubtypeLayoutMap): Boolean {
|
||||
return addSubtype(
|
||||
Subtype(
|
||||
(locale.hashCode() + 31 * layoutMap.hashCode() + 31 * currencySetName.hashCode()),
|
||||
locale,
|
||||
composerName,
|
||||
currencySetName,
|
||||
layoutMap
|
||||
)
|
||||
|
||||
@@ -17,40 +17,35 @@
|
||||
package dev.patrickgold.florisboard.ime.dictionary
|
||||
|
||||
import dev.patrickgold.florisboard.ime.extension.Asset
|
||||
import dev.patrickgold.florisboard.ime.nlp.LanguageModel
|
||||
import dev.patrickgold.florisboard.ime.nlp.MutableLanguageModel
|
||||
import dev.patrickgold.florisboard.ime.nlp.Token
|
||||
import dev.patrickgold.florisboard.ime.nlp.WeightedToken
|
||||
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
|
||||
import dev.patrickgold.florisboard.ime.nlp.Word
|
||||
|
||||
/**
|
||||
* Standardized dictionary interface for interacting with dictionaries.
|
||||
*/
|
||||
interface Dictionary<T : Any, F : Comparable<F>> : Asset {
|
||||
val languageModel: LanguageModel<T, F>
|
||||
|
||||
interface Dictionary : Asset {
|
||||
/**
|
||||
* Gets token predictions based on the given [precedingTokens] and the [currentToken]. The
|
||||
* length of the returned list is limited to [maxSuggestionCount]. Note that the returned list
|
||||
* may at any time give back less items than [maxSuggestionCount] indicates.
|
||||
*/
|
||||
fun getTokenPredictions(
|
||||
precedingTokens: List<Token<T>>,
|
||||
currentToken: Token<T>?,
|
||||
precedingTokens: List<Word>,
|
||||
currentToken: Word?,
|
||||
maxSuggestionCount: Int,
|
||||
allowPossiblyOffensive: Boolean
|
||||
): List<WeightedToken<T, F>>
|
||||
allowPossiblyOffensive: Boolean,
|
||||
destSuggestionList: SuggestionList
|
||||
)
|
||||
|
||||
fun getDate(): Long
|
||||
|
||||
fun getVersion(): Int
|
||||
}
|
||||
|
||||
interface MutableDictionary<T : Any, F : Comparable<F>> : Dictionary<T, F> {
|
||||
override val languageModel: MutableLanguageModel<T, F>
|
||||
|
||||
interface MutableDictionary : Dictionary {
|
||||
fun trainTokenPredictions(
|
||||
precedingTokens: List<Token<T>>,
|
||||
lastToken: Token<T>
|
||||
precedingTokens: List<Word>,
|
||||
lastToken: Word
|
||||
)
|
||||
|
||||
fun setDate(date: Int)
|
||||
|
||||
@@ -17,14 +17,32 @@
|
||||
package dev.patrickgold.florisboard.ime.dictionary
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import dev.patrickgold.florisboard.ime.core.Preferences
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
|
||||
import dev.patrickgold.florisboard.ime.nlp.Word
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* TODO: document
|
||||
*/
|
||||
class DictionaryManager private constructor(private val applicationContext: Context) {
|
||||
private val dictionaryCache: MutableMap<String, Dictionary<String, Int>> = mutableMapOf()
|
||||
class DictionaryManager private constructor(
|
||||
context: Context,
|
||||
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||
) {
|
||||
private val applicationContext: WeakReference<Context> = WeakReference(context.applicationContext ?: context)
|
||||
private val prefs get() = Preferences.default()
|
||||
|
||||
private val dictionaryCache: MutableMap<String, Dictionary> = mutableMapOf()
|
||||
|
||||
private var florisUserDictionaryDatabase: FlorisUserDictionaryDatabase? = null
|
||||
private var systemUserDictionaryDatabase: SystemUserDictionaryDatabase? = null
|
||||
|
||||
companion object {
|
||||
private var defaultInstance: DictionaryManager? = null
|
||||
@@ -47,22 +65,118 @@ class DictionaryManager private constructor(private val applicationContext: Cont
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDictionary(ref: AssetRef): Result<Dictionary<String, Int>> {
|
||||
dictionaryCache[ref.toString()]?.let {
|
||||
return Result.success(it)
|
||||
inline fun suggest(
|
||||
currentWord: Word,
|
||||
preceidingWords: List<Word>,
|
||||
subtype: Subtype,
|
||||
allowPossiblyOffensive: Boolean,
|
||||
maxSuggestionCount: Int,
|
||||
block: (suggestions: SuggestionList) -> Unit
|
||||
) {
|
||||
val suggestions = SuggestionList.new(maxSuggestionCount)
|
||||
queryUserDictionary(currentWord, subtype.locale, suggestions)
|
||||
block(suggestions)
|
||||
suggestions.dispose()
|
||||
}
|
||||
|
||||
fun prepareDictionaries(subtype: Subtype) {
|
||||
// TODO: Implement this
|
||||
}
|
||||
|
||||
fun queryUserDictionary(word: Word, locale: Locale, destSuggestionList: SuggestionList) {
|
||||
val florisDao = florisUserDictionaryDao()
|
||||
val systemDao = systemUserDictionaryDao()
|
||||
if (florisDao == null && systemDao == null) {
|
||||
return
|
||||
}
|
||||
if (ref.path.endsWith(".flict")) {
|
||||
// Assume this is a Flictionary
|
||||
Flictionary.load(applicationContext, ref).onSuccess { flict ->
|
||||
dictionaryCache[ref.toString()] = flict
|
||||
return Result.success(flict)
|
||||
}.onFailure { err ->
|
||||
Timber.i(err)
|
||||
return Result.failure(err)
|
||||
if (prefs.dictionary.enableFlorisUserDictionary) {
|
||||
florisDao?.query(word, locale)?.let {
|
||||
for (entry in it) {
|
||||
destSuggestionList.add(entry.word, entry.freq)
|
||||
}
|
||||
}
|
||||
florisDao?.queryShortcut(word, locale)?.let {
|
||||
for (entry in it) {
|
||||
destSuggestionList.add(entry.word, entry.freq)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Result.failure(Exception("Unable to determine supported type for given AssetRef!"))
|
||||
}
|
||||
return Result.failure(Exception("If this message is ever thrown, something is completely broken..."))
|
||||
if (prefs.dictionary.enableSystemUserDictionary) {
|
||||
systemDao?.query(word, locale)?.let {
|
||||
for (entry in it) {
|
||||
destSuggestionList.add(entry.word, entry.freq)
|
||||
}
|
||||
}
|
||||
systemDao?.queryShortcut(word, locale)?.let {
|
||||
for (entry in it) {
|
||||
destSuggestionList.add(entry.word, entry.freq)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun florisUserDictionaryDao(): UserDictionaryDao? {
|
||||
return if (prefs.suggestion.enabled && prefs.dictionary.enableFlorisUserDictionary) {
|
||||
florisUserDictionaryDatabase?.userDictionaryDao()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun florisUserDictionaryDatabase(): FlorisUserDictionaryDatabase? {
|
||||
return if (prefs.suggestion.enabled && prefs.dictionary.enableFlorisUserDictionary) {
|
||||
florisUserDictionaryDatabase
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun systemUserDictionaryDao(): UserDictionaryDao? {
|
||||
return if (prefs.suggestion.enabled && prefs.dictionary.enableSystemUserDictionary) {
|
||||
systemUserDictionaryDatabase?.userDictionaryDao()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun systemUserDictionaryDatabase(): SystemUserDictionaryDatabase? {
|
||||
return if (prefs.suggestion.enabled && prefs.dictionary.enableSystemUserDictionary) {
|
||||
systemUserDictionaryDatabase
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun loadUserDictionariesIfNecessary() {
|
||||
val context = applicationContext.get() ?: return
|
||||
|
||||
if (prefs.suggestion.enabled) {
|
||||
if (florisUserDictionaryDatabase == null && prefs.dictionary.enableFlorisUserDictionary) {
|
||||
florisUserDictionaryDatabase = Room.databaseBuilder(
|
||||
context,
|
||||
FlorisUserDictionaryDatabase::class.java,
|
||||
FlorisUserDictionaryDatabase.DB_FILE_NAME
|
||||
).allowMainThreadQueries().build()
|
||||
}
|
||||
if (systemUserDictionaryDatabase == null && prefs.dictionary.enableSystemUserDictionary) {
|
||||
systemUserDictionaryDatabase = SystemUserDictionaryDatabase(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun unloadUserDictionariesIfNecessary() {
|
||||
if (florisUserDictionaryDatabase != null) {
|
||||
florisUserDictionaryDatabase?.close()
|
||||
florisUserDictionaryDatabase = null
|
||||
}
|
||||
if (systemUserDictionaryDatabase != null) {
|
||||
systemUserDictionaryDatabase = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ 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
|
||||
|
||||
/**
|
||||
@@ -31,15 +30,15 @@ import kotlin.jvm.Throws
|
||||
* This class accepts binary dictionary files of the type "flict" as defined in here:
|
||||
* https://github.com/florisboard/dictionary-tools/blob/main/flictionary.md
|
||||
*/
|
||||
/**
|
||||
class Flictionary private constructor(
|
||||
override val name: String,
|
||||
override val label: String,
|
||||
override val authors: List<String>,
|
||||
private val date: Long,
|
||||
private val version: Int,
|
||||
private val headerStr: String,
|
||||
override val languageModel: LanguageModel<String, Int>
|
||||
) : Dictionary<String, Int> {
|
||||
private val headerStr: String
|
||||
) : Dictionary {
|
||||
companion object {
|
||||
private const val VERSION_0 = 0x0
|
||||
|
||||
@@ -307,7 +306,7 @@ class Flictionary private constructor(
|
||||
return if (currentToken.data.isNotEmpty()) {
|
||||
val retList = languageModel.matchAllNgrams(
|
||||
ngram = Ngram(
|
||||
_tokens = listOf(Token(currentToken.data.toLowerCase(Locale.ENGLISH))),
|
||||
_tokens = listOf(Token(currentToken.data.lowercase())),
|
||||
_freq = -1
|
||||
),
|
||||
maxEditDistance = 2,
|
||||
@@ -428,3 +427,4 @@ fun InputStream.readNext(b: ByteArray, off: Int, len: Int): Int {
|
||||
}
|
||||
return lenRead
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
/*
|
||||
* 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.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.UserDictionary
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Database
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Insert
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.Update
|
||||
import dev.patrickgold.florisboard.ime.extension.ExternalContentUtils
|
||||
import dev.patrickgold.florisboard.util.LocaleUtils
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
|
||||
private const val WORDS_TABLE = "words"
|
||||
|
||||
const val FREQUENCY_MIN = 1
|
||||
const val FREQUENCY_MAX = 255
|
||||
const val FREQUENCY_DEFAULT = 128
|
||||
|
||||
private const val SORT_BY_WORD_ASC = "${UserDictionary.Words.WORD} ASC"
|
||||
private const val SORT_BY_WORD_DESC = "${UserDictionary.Words.WORD} DESC"
|
||||
private const val SORT_BY_FREQ_ASC = "${UserDictionary.Words.FREQUENCY} ASC"
|
||||
private const val SORT_BY_FREQ_DESC = "${UserDictionary.Words.FREQUENCY} DESC"
|
||||
|
||||
private val PROJECTIONS: Array<String> = arrayOf(
|
||||
UserDictionary.Words._ID,
|
||||
UserDictionary.Words.WORD,
|
||||
UserDictionary.Words.FREQUENCY,
|
||||
UserDictionary.Words.LOCALE,
|
||||
UserDictionary.Words.SHORTCUT,
|
||||
)
|
||||
|
||||
private val PROJECTIONS_LANGUAGE: Array<String> = arrayOf(
|
||||
UserDictionary.Words.LOCALE,
|
||||
)
|
||||
|
||||
@Entity(tableName = WORDS_TABLE)
|
||||
data class UserDictionaryEntry(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = UserDictionary.Words._ID, index = true)
|
||||
val id: Long,
|
||||
@ColumnInfo(name = UserDictionary.Words.WORD)
|
||||
val word: String,
|
||||
@ColumnInfo(name = UserDictionary.Words.FREQUENCY)
|
||||
val freq: Int,
|
||||
@ColumnInfo(name = UserDictionary.Words.LOCALE)
|
||||
val locale: String?,
|
||||
@ColumnInfo(name = UserDictionary.Words.SHORTCUT)
|
||||
val shortcut: String?,
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface UserDictionaryDao {
|
||||
companion object {
|
||||
private const val SELECT_ALL_FROM_WORDS =
|
||||
"SELECT * FROM $WORDS_TABLE"
|
||||
private const val LOCALE_MATCHES =
|
||||
"(${UserDictionary.Words.LOCALE} = :locale OR ${UserDictionary.Words.LOCALE} IS NULL)"
|
||||
}
|
||||
|
||||
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.WORD} LIKE '%' || :word || '%'")
|
||||
fun query(word: String): List<UserDictionaryEntry>
|
||||
|
||||
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.WORD} LIKE '%' || :word || '%' AND $LOCALE_MATCHES")
|
||||
fun query(word: String, locale: Locale?): List<UserDictionaryEntry>
|
||||
|
||||
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.SHORTCUT} = :shortcut")
|
||||
fun queryShortcut(shortcut: String): List<UserDictionaryEntry>
|
||||
|
||||
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.SHORTCUT} = :shortcut AND $LOCALE_MATCHES")
|
||||
fun queryShortcut(shortcut: String, locale: Locale?): List<UserDictionaryEntry>
|
||||
|
||||
@Query(SELECT_ALL_FROM_WORDS)
|
||||
fun queryAll(): List<UserDictionaryEntry>
|
||||
|
||||
@Query("$SELECT_ALL_FROM_WORDS WHERE (${UserDictionary.Words.LOCALE} = :locale AND :locale IS NOT NULL) OR (${UserDictionary.Words.LOCALE} IS NULL AND :locale IS NULL)")
|
||||
fun queryAll(locale: Locale?): List<UserDictionaryEntry>
|
||||
|
||||
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.WORD} = :word")
|
||||
fun queryExact(word: String): List<UserDictionaryEntry>
|
||||
|
||||
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.WORD} = :word AND (${UserDictionary.Words.LOCALE} = :locale OR (${UserDictionary.Words.LOCALE} IS NULL AND :locale IS NULL))")
|
||||
fun queryExact(word: String, locale: Locale?): List<UserDictionaryEntry>
|
||||
|
||||
@Query("SELECT DISTINCT ${UserDictionary.Words.LOCALE} FROM $WORDS_TABLE")
|
||||
fun queryLanguageList(): List<Locale?>
|
||||
|
||||
@Insert
|
||||
fun insert(entry: UserDictionaryEntry)
|
||||
|
||||
@Update
|
||||
fun update(entry: UserDictionaryEntry)
|
||||
|
||||
@Delete
|
||||
fun delete(entry: UserDictionaryEntry)
|
||||
|
||||
@Query("DELETE FROM $WORDS_TABLE")
|
||||
fun deleteAll()
|
||||
}
|
||||
|
||||
interface UserDictionaryDatabase {
|
||||
fun userDictionaryDao(): UserDictionaryDao
|
||||
|
||||
fun reset()
|
||||
|
||||
fun importCombinedList(context: Context, uri: Uri): Result<Unit> {
|
||||
return ExternalContentUtils.readFromUri(context, uri,6_192_000) { src ->
|
||||
var isFirstLine = true
|
||||
src.forEachLine { line ->
|
||||
if (isFirstLine) {
|
||||
// Ignore
|
||||
isFirstLine = false
|
||||
} else {
|
||||
var word: String? = null
|
||||
var freq: Int? = null
|
||||
var locale: String? = null
|
||||
var shortcut: String? = null
|
||||
line.split(';').forEach { property ->
|
||||
val keyValuePair = property.split('=')
|
||||
if (keyValuePair.size == 2) {
|
||||
val key = keyValuePair[0].trim().lowercase()
|
||||
val value = keyValuePair[1].trim()
|
||||
when (key) {
|
||||
"w", "word" -> word = value.ifBlank { null }
|
||||
"f", "freq" -> runCatching { value.toInt(10) }.onSuccess {
|
||||
freq = it.coerceIn(FREQUENCY_MIN, FREQUENCY_MAX)
|
||||
}
|
||||
"l", "locale" -> locale = when (value) {
|
||||
"all", "null", "" -> null
|
||||
else -> value.ifBlank { null }
|
||||
}
|
||||
"s", "shortcut" -> shortcut = value.ifBlank { null }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (word != null && freq != null) {
|
||||
val alreadyExistingEntries = userDictionaryDao().queryExact(
|
||||
word!!, locale?.let { LocaleUtils.stringToLocale(it) }
|
||||
)
|
||||
if (alreadyExistingEntries.isNotEmpty()) {
|
||||
userDictionaryDao().update(UserDictionaryEntry(alreadyExistingEntries[0].id, word!!, freq!!, locale, shortcut))
|
||||
} else {
|
||||
userDictionaryDao().insert(UserDictionaryEntry(0, word!!, freq!!, locale, shortcut))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun exportCombinedList(context: Context, uri: Uri): Result<Unit> {
|
||||
return ExternalContentUtils.writeToUri(context, uri) { dst ->
|
||||
StringBuilder().apply {
|
||||
append("dictionary=")
|
||||
append(uri.lastPathSegment)
|
||||
append(";date=")
|
||||
append(System.currentTimeMillis())
|
||||
append(";generated-by=")
|
||||
append(context.packageName)
|
||||
append(";version=1")
|
||||
appendLine()
|
||||
dst.write(toString())
|
||||
}
|
||||
for (entry in userDictionaryDao().queryAll()) {
|
||||
StringBuilder().apply {
|
||||
append(" w=")
|
||||
append(entry.word)
|
||||
append(";f=")
|
||||
append(entry.freq)
|
||||
append(";l=")
|
||||
append(entry.locale) // always append locale even if null
|
||||
if (entry.shortcut != null) {
|
||||
append(";s=")
|
||||
append(entry.shortcut)
|
||||
}
|
||||
appendLine()
|
||||
dst.write(toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Database(entities = [UserDictionaryEntry::class], version = 1)
|
||||
@TypeConverters(FlorisUserDictionaryDatabase.Converters::class)
|
||||
abstract class FlorisUserDictionaryDatabase : RoomDatabase(), UserDictionaryDatabase {
|
||||
companion object {
|
||||
const val DB_FILE_NAME = "floris_user_dictionary"
|
||||
}
|
||||
|
||||
abstract override fun userDictionaryDao(): UserDictionaryDao
|
||||
|
||||
override fun reset() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
class Converters {
|
||||
@TypeConverter
|
||||
fun localeToString(locale: Locale?): String? {
|
||||
return when (locale) {
|
||||
null -> null
|
||||
else -> locale.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun stringToLocale(string: String?): Locale? {
|
||||
return when (string) {
|
||||
null, "all", "null", "" -> null
|
||||
else -> LocaleUtils.stringToLocale(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SystemUserDictionaryDatabase(context: Context) : UserDictionaryDatabase {
|
||||
private val applicationContext: WeakReference<Context> = WeakReference(context.applicationContext ?: context)
|
||||
|
||||
private val dao = object : UserDictionaryDao {
|
||||
override fun query(word: String): List<UserDictionaryEntry> {
|
||||
return queryResolver(
|
||||
selection = "${UserDictionary.Words.WORD} LIKE ?",
|
||||
selectionArgs = arrayOf("%$word%"),
|
||||
sortOrder = SORT_BY_FREQ_DESC,
|
||||
)
|
||||
}
|
||||
|
||||
override fun query(word: String, locale: Locale?): List<UserDictionaryEntry> {
|
||||
return if (locale == null) {
|
||||
queryResolver(
|
||||
selection = "${UserDictionary.Words.WORD} LIKE ? AND ${UserDictionary.Words.LOCALE} IS NULL",
|
||||
selectionArgs = arrayOf("%$word%"),
|
||||
sortOrder = SORT_BY_FREQ_DESC,
|
||||
)
|
||||
} else {
|
||||
queryResolver(
|
||||
selection = "${UserDictionary.Words.WORD} LIKE ? AND (${UserDictionary.Words.LOCALE} = ? OR ${UserDictionary.Words.LOCALE} = ? OR ${UserDictionary.Words.LOCALE} IS NULL)",
|
||||
selectionArgs = arrayOf("%$word%", locale.toString(), locale.language.toString()),
|
||||
sortOrder = SORT_BY_FREQ_DESC,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun queryShortcut(shortcut: String): List<UserDictionaryEntry> {
|
||||
return queryResolver(
|
||||
selection = "${UserDictionary.Words.SHORTCUT} = ?",
|
||||
selectionArgs = arrayOf(shortcut),
|
||||
sortOrder = SORT_BY_FREQ_DESC,
|
||||
)
|
||||
}
|
||||
|
||||
override fun queryShortcut(shortcut: String, locale: Locale?): List<UserDictionaryEntry> {
|
||||
return if (locale == null) {
|
||||
queryResolver(
|
||||
selection = "${UserDictionary.Words.SHORTCUT} = ? AND ${UserDictionary.Words.LOCALE} IS NULL",
|
||||
selectionArgs = arrayOf(shortcut),
|
||||
sortOrder = SORT_BY_FREQ_DESC,
|
||||
)
|
||||
} else {
|
||||
queryResolver(
|
||||
selection = "${UserDictionary.Words.SHORTCUT} = ? AND (${UserDictionary.Words.LOCALE} = ? OR ${UserDictionary.Words.LOCALE} = ? OR ${UserDictionary.Words.LOCALE} IS NULL)",
|
||||
selectionArgs = arrayOf(shortcut, locale.toString(), locale.language.toString()),
|
||||
sortOrder = SORT_BY_FREQ_DESC,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun queryAll(): List<UserDictionaryEntry> {
|
||||
return queryResolver(
|
||||
selection = null,
|
||||
selectionArgs = null,
|
||||
sortOrder = SORT_BY_FREQ_DESC,
|
||||
)
|
||||
}
|
||||
|
||||
override fun queryAll(locale: Locale?): List<UserDictionaryEntry> {
|
||||
return if (locale == null) {
|
||||
queryResolver(
|
||||
selection = "${UserDictionary.Words.LOCALE} IS NULL",
|
||||
selectionArgs = null,
|
||||
sortOrder = SORT_BY_FREQ_DESC,
|
||||
)
|
||||
} else {
|
||||
queryResolver(
|
||||
selection = "${UserDictionary.Words.LOCALE} = ?",
|
||||
selectionArgs = arrayOf(locale.toString()),
|
||||
sortOrder = SORT_BY_FREQ_DESC,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun queryExact(word: String): List<UserDictionaryEntry> {
|
||||
return queryResolver(
|
||||
selection = "${UserDictionary.Words.WORD} = ?",
|
||||
selectionArgs = arrayOf(word),
|
||||
sortOrder = null,
|
||||
)
|
||||
}
|
||||
|
||||
override fun queryExact(word: String, locale: Locale?): List<UserDictionaryEntry> {
|
||||
return if (locale == null) {
|
||||
queryResolver(
|
||||
selection = "${UserDictionary.Words.WORD} = ? AND ${UserDictionary.Words.LOCALE} IS NULL",
|
||||
selectionArgs = arrayOf(word),
|
||||
sortOrder = SORT_BY_FREQ_DESC,
|
||||
)
|
||||
} else {
|
||||
queryResolver(
|
||||
selection = "${UserDictionary.Words.WORD} LIKE ? AND ${UserDictionary.Words.LOCALE} = ?",
|
||||
selectionArgs = arrayOf(word, locale.toString()),
|
||||
sortOrder = SORT_BY_FREQ_DESC,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun queryLanguageList(): List<Locale?> {
|
||||
val resolver = applicationContext.get()?.contentResolver ?: return listOf()
|
||||
val cursor = resolver.query(
|
||||
UserDictionary.Words.CONTENT_URI,
|
||||
PROJECTIONS_LANGUAGE,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
) ?: return listOf()
|
||||
if (cursor.count <= 0) {
|
||||
return listOf()
|
||||
}
|
||||
val localeIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE)
|
||||
val retList = mutableSetOf<Locale?>()
|
||||
while (cursor.moveToNext()) {
|
||||
val localeStr = cursor.getString(localeIndex)
|
||||
if (localeStr == null) {
|
||||
retList.add(null)
|
||||
} else {
|
||||
retList.add(LocaleUtils.stringToLocale(localeStr))
|
||||
}
|
||||
}
|
||||
cursor.close()
|
||||
return retList.toList()
|
||||
}
|
||||
|
||||
private fun queryResolver(selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): List<UserDictionaryEntry> {
|
||||
val resolver = applicationContext.get()?.contentResolver ?: return listOf()
|
||||
val cursor = resolver.query(
|
||||
UserDictionary.Words.CONTENT_URI,
|
||||
PROJECTIONS,
|
||||
selection,
|
||||
selectionArgs,
|
||||
sortOrder
|
||||
) ?: return listOf()
|
||||
return parseEntries(cursor).also { cursor.close() }
|
||||
}
|
||||
|
||||
private fun parseEntries(cursor: Cursor): List<UserDictionaryEntry> {
|
||||
if (cursor.count <= 0) {
|
||||
return listOf()
|
||||
}
|
||||
val idIndex = cursor.getColumnIndex(UserDictionary.Words._ID)
|
||||
val wordIndex = cursor.getColumnIndex(UserDictionary.Words.WORD)
|
||||
val freqIndex = cursor.getColumnIndex(UserDictionary.Words.FREQUENCY)
|
||||
val localeIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE)
|
||||
val shortcutIndex = cursor.getColumnIndex(UserDictionary.Words.SHORTCUT)
|
||||
val retList = mutableListOf<UserDictionaryEntry>()
|
||||
while (cursor.moveToNext()) {
|
||||
retList.add(
|
||||
UserDictionaryEntry(
|
||||
id = cursor.getLong(idIndex),
|
||||
word = cursor.getString(wordIndex),
|
||||
freq = cursor.getInt(freqIndex),
|
||||
locale = cursor.getString(localeIndex),
|
||||
shortcut = cursor.getString(shortcutIndex)
|
||||
)
|
||||
)
|
||||
}
|
||||
return retList
|
||||
}
|
||||
|
||||
override fun insert(entry: UserDictionaryEntry) {
|
||||
val resolver = applicationContext.get()?.contentResolver ?: return
|
||||
val contentValues = ContentValues(5).apply {
|
||||
put(UserDictionary.Words.WORD, entry.word)
|
||||
put(UserDictionary.Words.FREQUENCY, entry.freq)
|
||||
put(UserDictionary.Words.LOCALE, entry.locale)
|
||||
put(UserDictionary.Words.APP_ID, 0)
|
||||
put(UserDictionary.Words.SHORTCUT, entry.shortcut)
|
||||
}
|
||||
resolver.insert(UserDictionary.Words.CONTENT_URI, contentValues)
|
||||
}
|
||||
|
||||
override fun update(entry: UserDictionaryEntry) {
|
||||
val resolver = applicationContext.get()?.contentResolver ?: return
|
||||
val contentValues = ContentValues(4).apply {
|
||||
put(UserDictionary.Words.WORD, entry.word)
|
||||
put(UserDictionary.Words.FREQUENCY, entry.freq)
|
||||
put(UserDictionary.Words.LOCALE, entry.locale)
|
||||
put(UserDictionary.Words.SHORTCUT, entry.shortcut)
|
||||
}
|
||||
resolver.update(UserDictionary.Words.CONTENT_URI, contentValues, "${UserDictionary.Words._ID} = ${entry.id}", null)
|
||||
}
|
||||
|
||||
override fun delete(entry: UserDictionaryEntry) {
|
||||
val resolver = applicationContext.get()?.contentResolver ?: return
|
||||
resolver.delete(UserDictionary.Words.CONTENT_URI, "${UserDictionary.Words._ID} = ${entry.id}", null)
|
||||
}
|
||||
|
||||
override fun deleteAll() {
|
||||
// Unsupported action
|
||||
}
|
||||
}
|
||||
|
||||
override fun userDictionaryDao(): UserDictionaryDao {
|
||||
return dao
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,8 @@ import dev.patrickgold.florisboard.ime.keyboard.CaseSelector
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyData
|
||||
import dev.patrickgold.florisboard.ime.keyboard.VariationSelector
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.AutoTextKeyData
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.BasicTextKeyData
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.MultiTextKeyData
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
|
||||
import dev.patrickgold.florisboard.ime.text.composing.*
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -55,6 +53,12 @@ class AssetManager private constructor(val applicationContext: Context) {
|
||||
subclass(MultiTextKeyData::class, MultiTextKeyData.serializer())
|
||||
default { BasicTextKeyData.serializer() }
|
||||
}
|
||||
polymorphic(Composer::class) {
|
||||
subclass(Appender::class, Appender.serializer())
|
||||
subclass(HangulUnicode::class, HangulUnicode.serializer())
|
||||
subclass(WithRules::class, WithRules.serializer())
|
||||
default { Appender.serializer() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.extension
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Sealed class which specifies where an asset comes from. There are 3 different types, all of which
|
||||
* require a different approach on how to access the actual asset.
|
||||
@@ -50,7 +48,7 @@ sealed class AssetSource {
|
||||
private val externalRegex: Regex = """^external\\(([a-z]+\\.)*[a-z]+\\)\$""".toRegex()
|
||||
|
||||
fun fromString(str: String): Result<AssetSource> {
|
||||
return when (val string = str.toLowerCase(Locale.ENGLISH)) {
|
||||
return when (val string = str.lowercase()) {
|
||||
"assets" -> Result.success(Assets)
|
||||
"internal" -> Result.success(Internal)
|
||||
else -> {
|
||||
|
||||
@@ -14,35 +14,58 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalContracts::class)
|
||||
|
||||
package dev.patrickgold.florisboard.ime.extension
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import java.io.BufferedReader
|
||||
import java.io.BufferedWriter
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
class ExternalContentUtils private constructor() {
|
||||
companion object {
|
||||
fun readTextFromUri(context: Context, uri: Uri, maxSize: Int): Result<String> {
|
||||
val contentResolver = context.contentResolver
|
||||
?: return Result.failure(NullPointerException("System content resolver not available"))
|
||||
val inputStream = contentResolver.openInputStream(uri)
|
||||
?: return Result.failure(NullPointerException("Cannot open input stream for given uri '$uri'"))
|
||||
val assetFileDescriptor = contentResolver.openAssetFileDescriptor(uri, "r")
|
||||
?: return Result.failure(NullPointerException("Cannot open asset file descriptor for given uri '$uri'"))
|
||||
if (assetFileDescriptor.length > maxSize) {
|
||||
return Result.failure(Exception("Contents of given uri '$uri' exceeds maximum size of $maxSize bytes!"))
|
||||
inline fun <R> readFromUri(context: Context, uri: Uri, maxSize: Int, block: (it: BufferedReader) -> R): Result<R> {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
|
||||
}
|
||||
return runCatching {
|
||||
val contentResolver = context.contentResolver
|
||||
?: throw NullPointerException("System content resolver not available")
|
||||
val inputStream = contentResolver.openInputStream(uri)
|
||||
?: throw NullPointerException("Cannot open input stream for given uri '$uri'")
|
||||
val assetFileDescriptor = contentResolver.openAssetFileDescriptor(uri, "r")
|
||||
?: throw NullPointerException("Cannot open asset file descriptor for given uri '$uri'")
|
||||
if (assetFileDescriptor.length > maxSize) {
|
||||
throw Exception("Contents of given uri '$uri' exceeds maximum size of $maxSize bytes!")
|
||||
}
|
||||
inputStream.bufferedReader(Charsets.UTF_8).use { block(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun readTextFromUri(context: Context, uri: Uri, maxSize: Int): Result<String> {
|
||||
return readFromUri(context, uri, maxSize) { it.readText() }
|
||||
}
|
||||
|
||||
inline fun writeToUri(context: Context, uri: Uri, block: (it: BufferedWriter) -> Unit): Result<Unit> {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
|
||||
}
|
||||
return runCatching {
|
||||
val contentResolver = context.contentResolver
|
||||
?: throw NullPointerException("System content resolver not available")
|
||||
// Must use "rwt" mode to ensure destination file length is truncated after writing.
|
||||
val outputStream = contentResolver.openOutputStream(uri, "rwt")
|
||||
?: throw NullPointerException("Cannot open output stream for given uri '$uri'")
|
||||
outputStream.bufferedWriter(Charsets.UTF_8).use { block(it) }
|
||||
}
|
||||
val rawText = inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
|
||||
return Result.success(rawText)
|
||||
}
|
||||
|
||||
fun writeTextToUri(context: Context, uri: Uri, text: String): Result<Unit> {
|
||||
val contentResolver = context.contentResolver
|
||||
?: return Result.failure(NullPointerException("System content resolver not available"))
|
||||
// Must use "rwt" mode to ensure destination file length is truncated after writing.
|
||||
val outputStream = contentResolver.openOutputStream(uri, "rwt")
|
||||
?: return Result.failure(NullPointerException("Cannot open output stream for given uri '$uri'"))
|
||||
outputStream.bufferedWriter(Charsets.UTF_8).use { it.write(text) }
|
||||
return Result.success(Unit)
|
||||
return writeToUri(context, uri) { it.write(text) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package dev.patrickgold.florisboard.ime.keyboard
|
||||
|
||||
import android.os.Build
|
||||
import android.view.inputmethod.EditorInfo
|
||||
|
||||
/**
|
||||
* Class which holds the same information as an [EditorInfo.imeOptions] int but more accessible and
|
||||
* readable.
|
||||
*/
|
||||
@JvmInline
|
||||
value class ImeOptions(val state: KeyboardState) {
|
||||
companion object {
|
||||
const val M_IME_OPTIONS: ULong = 0x0F_FFu
|
||||
const val O_IME_OPTIONS: Int = 32
|
||||
|
||||
const val M_ENTER_ACTION: ULong = 0x0Fu
|
||||
const val O_ENTER_ACTION: Int = 32
|
||||
|
||||
const val F_FORCE_ASCII: ULong = 0x00_00_00_10_00_00_00_00u
|
||||
const val F_NAVIGATE_NEXT: ULong = 0x00_00_00_20_00_00_00_00u
|
||||
const val F_NAVIGATE_PREVIOUS: ULong = 0x00_00_00_40_00_00_00_00u
|
||||
const val F_NO_ACCESSORY_ACTION: ULong = 0x00_00_00_80_00_00_00_00u
|
||||
const val F_NO_ENTER_ACTION: ULong = 0x00_00_01_00_00_00_00_00u
|
||||
const val F_NO_EXTRACT_UI: ULong = 0x00_00_02_00_00_00_00_00u
|
||||
const val F_NO_FULLSCREEN: ULong = 0x00_00_04_00_00_00_00_00u
|
||||
const val F_NO_PERSONALIZED_LEARNING: ULong = 0x00_00_08_00_00_00_00_00u
|
||||
}
|
||||
|
||||
var enterAction: EnterAction
|
||||
get() = EnterAction.fromInt(state.getRegion(M_ENTER_ACTION, O_ENTER_ACTION))
|
||||
private set(v) = state.setRegion(M_ENTER_ACTION, O_ENTER_ACTION, v.toInt())
|
||||
|
||||
var flagForceAscii: Boolean
|
||||
get() = state.getFlag(F_FORCE_ASCII)
|
||||
private set(v) = state.setFlag(F_FORCE_ASCII, v)
|
||||
var flagNavigateNext: Boolean
|
||||
get() = state.getFlag(F_NAVIGATE_NEXT)
|
||||
private set(v) = state.setFlag(F_NAVIGATE_NEXT, v)
|
||||
var flagNavigatePrevious: Boolean
|
||||
get() = state.getFlag(F_NAVIGATE_PREVIOUS)
|
||||
private set(v) = state.setFlag(F_NAVIGATE_PREVIOUS, v)
|
||||
var flagNoAccessoryAction: Boolean
|
||||
get() = state.getFlag(F_NO_ACCESSORY_ACTION)
|
||||
private set(v) = state.setFlag(F_NO_ACCESSORY_ACTION, v)
|
||||
var flagNoEnterAction: Boolean
|
||||
get() = state.getFlag(F_NO_ENTER_ACTION)
|
||||
private set(v) = state.setFlag(F_NO_ENTER_ACTION, v)
|
||||
var flagNoExtractUi: Boolean
|
||||
get() = state.getFlag(F_NO_EXTRACT_UI)
|
||||
private set(v) = state.setFlag(F_NO_EXTRACT_UI, v)
|
||||
var flagNoFullscreen: Boolean
|
||||
get() = state.getFlag(F_NO_FULLSCREEN)
|
||||
private set(v) = state.setFlag(F_NO_FULLSCREEN, v)
|
||||
var flagNoPersonalizedLearning: Boolean
|
||||
get() = state.getFlag(F_NO_PERSONALIZED_LEARNING)
|
||||
private set(v) = state.setFlag(F_NO_PERSONALIZED_LEARNING, v)
|
||||
|
||||
fun update(editorInfo: EditorInfo) {
|
||||
val imeOptionsRaw = editorInfo.imeOptions
|
||||
state.setRegion(M_IME_OPTIONS, O_IME_OPTIONS, 0) // reset imeOptions region
|
||||
enterAction = EnterAction.fromInt(imeOptionsRaw and EditorInfo.IME_MASK_ACTION)
|
||||
flagForceAscii = imeOptionsRaw and EditorInfo.IME_FLAG_FORCE_ASCII != 0
|
||||
flagNavigateNext = imeOptionsRaw and EditorInfo.IME_FLAG_NAVIGATE_NEXT != 0
|
||||
flagNavigatePrevious = imeOptionsRaw and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS != 0
|
||||
flagNoAccessoryAction = imeOptionsRaw and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION != 0
|
||||
flagNoEnterAction = imeOptionsRaw and EditorInfo.IME_FLAG_NO_ENTER_ACTION != 0
|
||||
flagNoExtractUi = imeOptionsRaw and EditorInfo.IME_FLAG_NO_EXTRACT_UI != 0
|
||||
flagNoFullscreen = imeOptionsRaw and EditorInfo.IME_FLAG_NO_FULLSCREEN != 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
flagNoPersonalizedLearning = imeOptionsRaw and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING != 0
|
||||
}
|
||||
}
|
||||
|
||||
enum class EnterAction(val value: Int) {
|
||||
UNSPECIFIED(EditorInfo.IME_ACTION_UNSPECIFIED),
|
||||
DONE(EditorInfo.IME_ACTION_DONE),
|
||||
GO(EditorInfo.IME_ACTION_GO),
|
||||
NEXT(EditorInfo.IME_ACTION_NEXT),
|
||||
NONE(EditorInfo.IME_ACTION_NONE),
|
||||
PREVIOUS(EditorInfo.IME_ACTION_PREVIOUS),
|
||||
SEARCH(EditorInfo.IME_ACTION_SEARCH),
|
||||
SEND(EditorInfo.IME_ACTION_SEND);
|
||||
|
||||
companion object {
|
||||
fun fromInt(int: Int) = values().firstOrNull { it.value == int } ?: NONE
|
||||
}
|
||||
|
||||
fun toInt() = value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package dev.patrickgold.florisboard.ime.keyboard
|
||||
|
||||
import android.text.InputType
|
||||
import android.view.inputmethod.EditorInfo
|
||||
|
||||
/**
|
||||
* Class which holds the same information as an [EditorInfo.inputType] int but more accessible and
|
||||
* readable.
|
||||
*/
|
||||
@JvmInline
|
||||
value class InputAttributes(val state: KeyboardState) {
|
||||
companion object {
|
||||
const val M_INPUT_ATTRIBUTES: ULong = 0x0F_FF_FFu
|
||||
const val O_INPUT_ATTRIBUTES: Int = 44
|
||||
|
||||
const val M_TYPE: ULong = 0x07u
|
||||
const val O_TYPE: Int = 44
|
||||
const val M_VARIATION: ULong = 0x1Fu
|
||||
const val O_VARIATION: Int = 47
|
||||
const val M_CAPS_MODE: ULong = 0x03u
|
||||
const val O_CAPS_MODE: Int = 52
|
||||
|
||||
const val F_NUMBER_DECIMAL: ULong = 0x00_40_00_00_00_00_00_00u
|
||||
const val F_NUMBER_SIGNED: ULong = 0x00_80_00_00_00_00_00_00u
|
||||
const val F_TEXT_AUTO_COMPLETE: ULong = 0x01_00_00_00_00_00_00_00u
|
||||
const val F_TEXT_AUTO_CORRECT: ULong = 0x02_00_00_00_00_00_00_00u
|
||||
const val F_TEXT_IME_MULTILINE: ULong = 0x04_00_00_00_00_00_00_00u
|
||||
const val F_TEXT_MULTILINE: ULong = 0x08_00_00_00_00_00_00_00u
|
||||
const val F_TEXT_NO_SUGGESTIONS: ULong = 0x10_00_00_00_00_00_00_00u
|
||||
}
|
||||
|
||||
var type: Type
|
||||
get() = Type.fromInt(state.getRegion(M_TYPE, O_TYPE))
|
||||
private set(v) = state.setRegion(M_TYPE, O_TYPE, v.toInt())
|
||||
|
||||
var variation: Variation
|
||||
get() = Variation.fromInt(state.getRegion(M_VARIATION, O_VARIATION))
|
||||
private set(v) = state.setRegion(M_VARIATION, O_VARIATION, v.toInt())
|
||||
|
||||
var capsMode: CapsMode
|
||||
get() = CapsMode.fromInt(state.getRegion(M_CAPS_MODE, O_CAPS_MODE))
|
||||
private set(v) = state.setRegion(M_CAPS_MODE, O_CAPS_MODE, v.toInt())
|
||||
|
||||
var flagNumberDecimal: Boolean
|
||||
get() = state.getFlag(F_NUMBER_DECIMAL)
|
||||
private set(v) = state.setFlag(F_NUMBER_DECIMAL, v)
|
||||
var flagNumberSigned: Boolean
|
||||
get() = state.getFlag(F_NUMBER_SIGNED)
|
||||
private set(v) = state.setFlag(F_NUMBER_SIGNED, v)
|
||||
var flagTextAutoComplete: Boolean
|
||||
get() = state.getFlag(F_TEXT_AUTO_COMPLETE)
|
||||
private set(v) = state.setFlag(F_TEXT_AUTO_COMPLETE, v)
|
||||
var flagTextAutoCorrect: Boolean
|
||||
get() = state.getFlag(F_TEXT_AUTO_CORRECT)
|
||||
private set(v) = state.setFlag(F_TEXT_AUTO_CORRECT, v)
|
||||
var flagTextImeMultiLine: Boolean
|
||||
get() = state.getFlag(F_TEXT_IME_MULTILINE)
|
||||
private set(v) = state.setFlag(F_TEXT_IME_MULTILINE, v)
|
||||
var flagTextMultiLine: Boolean
|
||||
get() = state.getFlag(F_TEXT_MULTILINE)
|
||||
private set(v) = state.setFlag(F_TEXT_MULTILINE, v)
|
||||
var flagTextNoSuggestions: Boolean
|
||||
get() = state.getFlag(F_TEXT_NO_SUGGESTIONS)
|
||||
private set(v) = state.setFlag(F_TEXT_NO_SUGGESTIONS, v)
|
||||
|
||||
fun update(editorInfo: EditorInfo) {
|
||||
val inputAttrsRaw = editorInfo.inputType
|
||||
state.setRegion(M_INPUT_ATTRIBUTES, O_INPUT_ATTRIBUTES, 0) // reset inputAttributes region
|
||||
when (inputAttrsRaw and InputType.TYPE_MASK_CLASS) {
|
||||
InputType.TYPE_NULL -> {
|
||||
type = Type.NULL
|
||||
variation = Variation.NORMAL
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
InputType.TYPE_CLASS_DATETIME -> {
|
||||
type = Type.DATETIME
|
||||
variation = when (inputAttrsRaw and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_DATETIME_VARIATION_DATE -> Variation.DATE
|
||||
InputType.TYPE_DATETIME_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_DATETIME_VARIATION_TIME -> Variation.TIME
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
InputType.TYPE_CLASS_NUMBER -> {
|
||||
type = Type.NUMBER
|
||||
variation = when (inputAttrsRaw and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_NUMBER_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> Variation.PASSWORD
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.NONE
|
||||
flagNumberDecimal = inputAttrsRaw and InputType.TYPE_NUMBER_FLAG_DECIMAL != 0
|
||||
flagNumberSigned = inputAttrsRaw and InputType.TYPE_NUMBER_FLAG_SIGNED != 0
|
||||
}
|
||||
InputType.TYPE_CLASS_PHONE -> {
|
||||
type = Type.PHONE
|
||||
variation = Variation.NORMAL
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
InputType.TYPE_CLASS_TEXT -> {
|
||||
type = Type.TEXT
|
||||
variation = when (inputAttrsRaw and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> Variation.EMAIL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> Variation.EMAIL_SUBJECT
|
||||
InputType.TYPE_TEXT_VARIATION_FILTER -> Variation.FILTER
|
||||
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> Variation.LONG_MESSAGE
|
||||
InputType.TYPE_TEXT_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_TEXT_VARIATION_PASSWORD -> Variation.PASSWORD
|
||||
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> Variation.PERSON_NAME
|
||||
InputType.TYPE_TEXT_VARIATION_PHONETIC -> Variation.PHONETIC
|
||||
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> Variation.POSTAL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> Variation.SHORT_MESSAGE
|
||||
InputType.TYPE_TEXT_VARIATION_URI -> Variation.URI
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> Variation.VISIBLE_PASSWORD
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> Variation.WEB_EDIT_TEXT
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> Variation.WEB_EMAIL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> Variation.WEB_PASSWORD
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.fromFlags(inputAttrsRaw)
|
||||
flagTextAutoComplete = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE != 0
|
||||
flagTextAutoCorrect = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT != 0
|
||||
flagTextImeMultiLine = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE != 0
|
||||
flagTextMultiLine = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_MULTI_LINE != 0
|
||||
flagTextNoSuggestions = inputAttrsRaw and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS != 0
|
||||
}
|
||||
else -> {
|
||||
type = Type.TEXT
|
||||
variation = Variation.NORMAL
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Type(val value: Int) {
|
||||
NULL(EditorInfo.TYPE_NULL),
|
||||
DATETIME(EditorInfo.TYPE_CLASS_DATETIME),
|
||||
NUMBER(EditorInfo.TYPE_CLASS_NUMBER),
|
||||
PHONE(EditorInfo.TYPE_CLASS_PHONE),
|
||||
TEXT(EditorInfo.TYPE_CLASS_TEXT);
|
||||
|
||||
companion object {
|
||||
fun fromInt(int: Int) = values().firstOrNull { it.value == int } ?: NULL
|
||||
}
|
||||
|
||||
fun toInt() = value
|
||||
}
|
||||
|
||||
enum class Variation(val value: Int) {
|
||||
NORMAL(0),
|
||||
DATE(1),
|
||||
EMAIL_ADDRESS(2),
|
||||
EMAIL_SUBJECT(3),
|
||||
FILTER(4),
|
||||
LONG_MESSAGE(5),
|
||||
PASSWORD(6),
|
||||
PERSON_NAME(7),
|
||||
PHONETIC(8),
|
||||
POSTAL_ADDRESS(9),
|
||||
SHORT_MESSAGE(10),
|
||||
TIME(11),
|
||||
URI(12),
|
||||
VISIBLE_PASSWORD(13),
|
||||
WEB_EDIT_TEXT(14),
|
||||
WEB_EMAIL_ADDRESS(15),
|
||||
WEB_PASSWORD(16);
|
||||
|
||||
companion object {
|
||||
fun fromInt(int: Int) = values().firstOrNull { it.value == int } ?: NORMAL
|
||||
}
|
||||
|
||||
fun toInt() = value
|
||||
}
|
||||
|
||||
enum class CapsMode(val value: Int) {
|
||||
NONE(0),
|
||||
ALL(1),
|
||||
SENTENCES(2),
|
||||
WORDS(3);
|
||||
|
||||
companion object {
|
||||
fun fromFlags(flags: Int): CapsMode {
|
||||
return when {
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS != 0 -> ALL
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES != 0 -> SENTENCES
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS != 0 -> WORDS
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
|
||||
fun fromInt(int: Int) = values().firstOrNull { it.value == int } ?: NONE
|
||||
}
|
||||
|
||||
fun toFlags(): Int {
|
||||
return when (this) {
|
||||
ALL -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
||||
SENTENCES -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
WORDS -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
fun toInt() = value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package dev.patrickgold.florisboard.ime.keyboard
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.core.view.children
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
|
||||
/**
|
||||
* This class is a helper managing the state of the text input logic which
|
||||
* affects the keyboard view in rendering and layouting the keys.
|
||||
*
|
||||
* The state class can hold flags or small unsigned integers, all added up
|
||||
* at max 64-bit though.
|
||||
*
|
||||
* The structure of this 8-byte state register is as follows: (Lower 4 bytes are pretty experimental rn)
|
||||
*
|
||||
* <Byte 3> | <Byte 2> | <Byte 1> | <Byte 0> | Description
|
||||
* ---------|----------|----------|----------|---------------------------------
|
||||
* | | | 1111 | Active [KeyboardMode]
|
||||
* | | | 1111 | Active [KeyVariation]
|
||||
* | | 1 | | Caps flag
|
||||
* | | 1 | | Caps lock flag
|
||||
* | | 1 | | Is selection active (length > 0)
|
||||
* | | 1 | | Is private mode
|
||||
* | 1 | | | Is Smartbar quick actions visible
|
||||
* | 1 | | | Is Smartbar showing inline suggestions
|
||||
* | 1 | | | Is composing enabled
|
||||
*
|
||||
* <Byte 7> | <Byte 6> | <Byte 5> | <Byte 4> | Description
|
||||
* ---------|----------|----------|----------|---------------------------------
|
||||
* | | | 1111 | [ImeOptions.enterAction]
|
||||
* | | | 1 | [ImeOptions.flagForceAscii]
|
||||
* | | | 1 | [ImeOptions.flagNavigateNext]
|
||||
* | | | 1 | [ImeOptions.flagNavigatePrevious]
|
||||
* | | | 1 | [ImeOptions.flagNoAccessoryAction]
|
||||
* | | 1 | | [ImeOptions.flagNoEnterAction]
|
||||
* | | 1 | | [ImeOptions.flagNoExtractUi]
|
||||
* | | 1 | | [ImeOptions.flagNoFullscreen]
|
||||
* | | 1 | | [ImeOptions.flagNoPersonalizedLearning]
|
||||
* | | 111 | | [InputAttributes.type]
|
||||
* | 1111 | 1 | | [InputAttributes.variation]
|
||||
* | 11 | | | [InputAttributes.capsMode]
|
||||
* | 1 | | | [InputAttributes.flagNumberDecimal]
|
||||
* | 1 | | | [InputAttributes.flagNumberSigned]
|
||||
* 1 | | | | [InputAttributes.flagTextAutoComplete]
|
||||
* 1 | | | | [InputAttributes.flagTextAutoCorrect]
|
||||
* 1 | | | | [InputAttributes.flagTextImeMultiLine]
|
||||
* 1 | | | | [InputAttributes.flagTextMultiLine]
|
||||
* 1 | | | | [InputAttributes.flagTextNoSuggestions]
|
||||
*
|
||||
* The resulting structure is only relevant during a runtime lifespan and
|
||||
* thus can easily be changed without worrying about destroying some saved state.
|
||||
*
|
||||
* @property value The internal register used to store the flags and region ints that
|
||||
* this keyboard state represents.
|
||||
* @property maskOfInterest The mask which is applied when comparing this state with another.
|
||||
* Is useful if only parts of a state instance is relevant to look at.
|
||||
*/
|
||||
class KeyboardState private constructor(var value: ULong, var maskOfInterest: ULong) {
|
||||
companion object {
|
||||
const val M_KEYBOARD_MODE: ULong = 0x0Fu
|
||||
const val O_KEYBOARD_MODE: Int = 0
|
||||
const val M_KEY_VARIATION: ULong = 0x0Fu
|
||||
const val O_KEY_VARIATION: Int = 4
|
||||
|
||||
const val F_CAPS: ULong = 0x00000100u
|
||||
const val F_CAPS_LOCK: ULong = 0x00000200u
|
||||
const val F_IS_SELECTION_MODE: ULong = 0x00000400u
|
||||
const val F_IS_PRIVATE_MODE: ULong = 0x00008000u
|
||||
const val F_IS_QUICK_ACTIONS_VISIBLE: ULong = 0x00010000u
|
||||
const val F_IS_SHOWING_INLINE_SUGGESTIONS: ULong = 0x00020000u
|
||||
const val F_IS_COMPOSING_ENABLED: ULong = 0x00040000u
|
||||
|
||||
const val STATE_ALL_ZERO: ULong = 0uL
|
||||
|
||||
const val INTEREST_ALL: ULong = ULong.MAX_VALUE
|
||||
const val INTEREST_NONE: ULong = 0uL
|
||||
const val INTEREST_TEXT: ULong = 0xFF_FF_FF_FF_00_00_FF_FFu
|
||||
const val INTEREST_MEDIA: ULong = 0x00_00_00_00_FF_00_00_00u
|
||||
|
||||
fun new(
|
||||
value: ULong = STATE_ALL_ZERO,
|
||||
maskOfInterest: ULong = INTEREST_ALL
|
||||
) = KeyboardState(value, maskOfInterest)
|
||||
}
|
||||
|
||||
val imeOptions: ImeOptions = ImeOptions(this)
|
||||
val inputAttributes: InputAttributes = InputAttributes(this)
|
||||
|
||||
/**
|
||||
* Resets this state register.
|
||||
*
|
||||
* @param newValue Optional, used to initialize the register value after the reset.
|
||||
* Defaults to [STATE_ALL_ZERO].
|
||||
*/
|
||||
fun reset(newValue: ULong = STATE_ALL_ZERO) {
|
||||
value = newValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this state register.
|
||||
*
|
||||
* @param newState A reference to a state which register value should be copied after
|
||||
* the reset.
|
||||
*/
|
||||
fun reset(newState: KeyboardState) {
|
||||
value = newState.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates this state based on the info passed from [editorInfo].
|
||||
*
|
||||
* @param editorInfo The [EditorInfo] used to initialize all flags and regions relevant
|
||||
* to the info this object provides.
|
||||
*/
|
||||
fun update(editorInfo: EditorInfo) {
|
||||
imeOptions.update(editorInfo)
|
||||
inputAttributes.update(editorInfo)
|
||||
}
|
||||
|
||||
internal fun getFlag(f: ULong): Boolean {
|
||||
return (value and f) != STATE_ALL_ZERO
|
||||
}
|
||||
|
||||
internal fun setFlag(f: ULong, v: Boolean) {
|
||||
value = if (v) { value or f } else { value and f.inv() }
|
||||
}
|
||||
|
||||
internal fun getRegion(m: ULong, o: Int): Int {
|
||||
return ((value shr o) and m).toInt()
|
||||
}
|
||||
|
||||
internal fun setRegion(m: ULong, o: Int, v: Int) {
|
||||
value = (value and (m shl o).inv()) or ((v.toULong() and m) shl o)
|
||||
}
|
||||
|
||||
fun isEqualTo(other: KeyboardState): Boolean {
|
||||
return (other.value and maskOfInterest) == (value and maskOfInterest)
|
||||
}
|
||||
|
||||
fun isDifferentTo(other: KeyboardState): Boolean {
|
||||
return !isEqualTo(other)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = value.hashCode()
|
||||
result = 31 * result + maskOfInterest.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as KeyboardState
|
||||
|
||||
if (value != other.value) return false
|
||||
if (maskOfInterest != other.maskOfInterest) return false
|
||||
if (imeOptions != other.imeOptions) return false
|
||||
if (inputAttributes != other.inputAttributes) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var keyVariation: KeyVariation
|
||||
get() = KeyVariation.fromInt(getRegion(M_KEY_VARIATION, O_KEY_VARIATION))
|
||||
set(v) { setRegion(M_KEY_VARIATION, O_KEY_VARIATION, v.toInt()) }
|
||||
|
||||
var keyboardMode: KeyboardMode
|
||||
get() = KeyboardMode.fromInt(getRegion(M_KEYBOARD_MODE, O_KEYBOARD_MODE))
|
||||
set(v) { setRegion(M_KEYBOARD_MODE, O_KEYBOARD_MODE, v.toInt()) }
|
||||
|
||||
var caps: Boolean
|
||||
get() = getFlag(F_CAPS)
|
||||
set(v) { setFlag(F_CAPS, v) }
|
||||
|
||||
var capsLock: Boolean
|
||||
get() = getFlag(F_CAPS_LOCK)
|
||||
set(v) { setFlag(F_CAPS_LOCK, v) }
|
||||
|
||||
var isSelectionMode: Boolean
|
||||
get() = getFlag(F_IS_SELECTION_MODE)
|
||||
set(v) { setFlag(F_IS_SELECTION_MODE, v) }
|
||||
|
||||
var isCursorMode: Boolean
|
||||
get() = !isSelectionMode
|
||||
set(v) { isSelectionMode = !v }
|
||||
|
||||
var isPrivateMode: Boolean
|
||||
get() = getFlag(F_IS_PRIVATE_MODE)
|
||||
set(v) { setFlag(F_IS_PRIVATE_MODE, v) }
|
||||
|
||||
val isRawInputEditor: Boolean
|
||||
get() = inputAttributes.type == InputAttributes.Type.NULL
|
||||
|
||||
val isRichInputEditor: Boolean
|
||||
get() = inputAttributes.type != InputAttributes.Type.NULL
|
||||
|
||||
var isQuickActionsVisible: Boolean
|
||||
get() = getFlag(F_IS_QUICK_ACTIONS_VISIBLE)
|
||||
set(v) { setFlag(F_IS_QUICK_ACTIONS_VISIBLE, v) }
|
||||
|
||||
var isShowingInlineSuggestions: Boolean
|
||||
get() = getFlag(F_IS_SHOWING_INLINE_SUGGESTIONS)
|
||||
set(v) { setFlag(F_IS_SHOWING_INLINE_SUGGESTIONS, v) }
|
||||
|
||||
var isComposingEnabled: Boolean
|
||||
get() = getFlag(F_IS_COMPOSING_ENABLED)
|
||||
set(v) { setFlag(F_IS_COMPOSING_ENABLED, v) }
|
||||
|
||||
interface OnUpdateStateListener {
|
||||
/**
|
||||
* Adds the ability for Views to intercept a update keyboard state dispatch.
|
||||
*
|
||||
* @param newState Reference to the new state.
|
||||
*
|
||||
* @return True if the update was intercepted (and thus the child views have to
|
||||
* be manually updated if needed, false if no interception happened.
|
||||
*/
|
||||
fun onInterceptUpdateKeyboardState(newState: KeyboardState): Boolean = false
|
||||
|
||||
/**
|
||||
* A new keyboard state is dispatched to all views in this view tree.
|
||||
*
|
||||
* @param newState Reference to the new state.
|
||||
*/
|
||||
fun onUpdateKeyboardState(newState: KeyboardState)
|
||||
}
|
||||
}
|
||||
|
||||
fun View.updateKeyboardState(newState: KeyboardState) {
|
||||
val intercepted: Boolean
|
||||
if (this is KeyboardState.OnUpdateStateListener) {
|
||||
intercepted = this.onInterceptUpdateKeyboardState(newState)
|
||||
this.onUpdateKeyboardState(newState)
|
||||
} else {
|
||||
intercepted = false
|
||||
}
|
||||
if (this is ViewGroup && !intercepted) {
|
||||
this.children.forEach { it.updateKeyboardState(newState) }
|
||||
}
|
||||
}
|
||||
@@ -20,27 +20,24 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.core.Preferences
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.sendBlocking
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
abstract class KeyboardView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
protected val florisboard: FlorisBoard?
|
||||
get() = FlorisBoard.getInstanceOrNull()
|
||||
protected val prefs: PrefHelper
|
||||
get() = PrefHelper.getDefaultInstance(context)
|
||||
protected val themeManager: ThemeManager?
|
||||
get() = ThemeManager.defaultOrNull()
|
||||
abstract class KeyboardView : ViewGroup, KeyboardState.OnUpdateStateListener, ThemeManager.OnThemeUpdatedListener {
|
||||
protected val florisboard get() = FlorisBoard.getInstanceOrNull()
|
||||
protected val prefs get() = Preferences.default()
|
||||
protected val themeManager get() = ThemeManager.defaultOrNull()
|
||||
|
||||
var isMeasured: Boolean = false
|
||||
private set
|
||||
protected var isTouchable: Boolean = true
|
||||
protected val touchEventChannel: Channel<MotionEvent> = Channel(16)
|
||||
protected val touchEventChannel: Channel<MotionEvent> = Channel(64)
|
||||
protected val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
@@ -48,6 +45,7 @@ abstract class KeyboardView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
init {
|
||||
layoutDirection = LAYOUT_DIRECTION_LTR
|
||||
mainScope.launch {
|
||||
for (event in touchEventChannel) {
|
||||
if (!isActive) break
|
||||
@@ -66,6 +64,10 @@ abstract class KeyboardView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
themeManager?.unregisterOnThemeUpdatedListener(this)
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
event ?: return false
|
||||
@@ -92,10 +94,5 @@ abstract class KeyboardView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
isMeasured = true
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
onLayoutInternal()
|
||||
}
|
||||
|
||||
protected abstract fun onLayoutInternal()
|
||||
abstract fun sync()
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.landscapeinput
|
||||
|
||||
import java.util.*
|
||||
|
||||
enum class LandscapeInputUiMode {
|
||||
DYNAMICALLY_SHOW,
|
||||
NEVER_SHOW,
|
||||
@@ -25,7 +23,7 @@ enum class LandscapeInputUiMode {
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String): LandscapeInputUiMode {
|
||||
return valueOf(string.toUpperCase(Locale.ENGLISH))
|
||||
return valueOf(string.uppercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,17 +22,18 @@ import android.view.View
|
||||
import android.widget.*
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.FlorisboardBinding
|
||||
import dev.patrickgold.florisboard.debug.LogTopic
|
||||
import dev.patrickgold.florisboard.debug.flogInfo
|
||||
import dev.patrickgold.florisboard.ime.core.EditorInstance
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
|
||||
import dev.patrickgold.florisboard.ime.core.InputView
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyboardView
|
||||
import dev.patrickgold.florisboard.ime.media.emoticon.EmoticonKeyData
|
||||
import dev.patrickgold.florisboard.ime.media.emoticon.EmoticonKeyboardView
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -57,8 +58,6 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
private var tabLayout: TabLayout? = null
|
||||
private val tabViews = EnumMap<Tab, LinearLayout>(Tab::class.java)
|
||||
|
||||
private var mediaViewGroup: LinearLayout? = null
|
||||
|
||||
companion object {
|
||||
private var instance: MediaInputManager? = null
|
||||
|
||||
@@ -75,27 +74,20 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
florisboard.addEventListener(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new input view has been registered. Used to initialize all media-relevant
|
||||
* views and layouts.
|
||||
* TODO: evaluate if the view initializing process can be optimized.
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onRegisterInputView(inputView: InputView) {
|
||||
Timber.i("onRegisterInputView(inputView)")
|
||||
override fun onInitializeInputUi(uiBinding: FlorisboardBinding) {
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
|
||||
launch(Dispatchers.Default) {
|
||||
mediaViewGroup = inputView.findViewById(R.id.media_input)
|
||||
mediaViewFlipper = inputView.findViewById(R.id.media_input_view_flipper)
|
||||
mediaViewFlipper = uiBinding.media.mediaInputViewFlipper
|
||||
|
||||
// Init bottom buttons
|
||||
inputView.findViewById<Button>(R.id.media_input_switch_to_text_input_button)
|
||||
.setOnTouchListener { view, event -> onBottomButtonEvent(view, event) }
|
||||
inputView.findViewById<ImageButton>(R.id.media_input_backspace_button)
|
||||
.setOnTouchListener { view, event -> onBottomButtonEvent(view, event) }
|
||||
// Init bottom buttons
|
||||
uiBinding.media.mediaInputSwitchToTextInputButton
|
||||
.setOnTouchListener { view, event -> onBottomButtonEvent(view, event) }
|
||||
uiBinding.media.mediaInputBackspaceButton
|
||||
.setOnTouchListener { view, event -> onBottomButtonEvent(view, event) }
|
||||
|
||||
tabLayout = inputView.findViewById(R.id.media_input_tabs)
|
||||
tabLayout?.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
tabLayout = uiBinding.media.mediaInputTabs.also {
|
||||
it.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
when (tab.position) {
|
||||
0 -> setActiveTab(Tab.EMOJI)
|
||||
@@ -107,15 +99,15 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||
})
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
for (tab in Tab.values()) {
|
||||
val tabView = createTabViewFor(tab)
|
||||
tabViews[tab] = tabView
|
||||
mediaViewFlipper?.addView(tabView)
|
||||
}
|
||||
tabLayout?.selectTab(tabLayout?.getTabAt(0))
|
||||
launch(Dispatchers.Main) {
|
||||
for (tab in Tab.values()) {
|
||||
val tabView = createTabViewFor(tab)
|
||||
tabViews[tab] = tabView
|
||||
mediaViewFlipper?.addView(tabView)
|
||||
}
|
||||
tabLayout?.selectTab(tabLayout?.getTabAt(0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +115,7 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Clean-up of resources and stopping all coroutines.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
Timber.i("onDestroy()")
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
|
||||
cancel()
|
||||
instance = null
|
||||
@@ -161,9 +153,9 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
*/
|
||||
private fun createTabViewFor(tab: Tab): LinearLayout {
|
||||
return when (tab) {
|
||||
Tab.EMOJI -> EmojiKeyboardView(florisboard)
|
||||
Tab.EMOTICON -> EmoticonKeyboardView(florisboard)
|
||||
else -> LinearLayout(florisboard).apply {
|
||||
Tab.EMOJI -> EmojiKeyboardView(florisboard.themeContext)
|
||||
Tab.EMOTICON -> EmoticonKeyboardView(florisboard.themeContext)
|
||||
else -> LinearLayout(florisboard.themeContext).apply {
|
||||
addView(TextView(context).apply {
|
||||
text = "not yet implemented"
|
||||
})
|
||||
|
||||
@@ -72,7 +72,7 @@ class MediaInputView : LinearLayout, FlorisBoard.EventListener,
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val height = florisboard?.inputView?.desiredMediaKeyboardViewHeight ?: 0.0f
|
||||
val height = florisboard?.uiBinding?.inputView?.desiredMediaKeyboardViewHeight ?: 0.0f
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height.roundToInt(), MeasureSpec.EXACTLY))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.media.emoji
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Enum for emoji category.
|
||||
* List taken from https://unicode.org/Public/emoji/13.0/emoji-test.txt
|
||||
@@ -39,7 +37,7 @@ enum class EmojiCategory {
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String): EmojiCategory {
|
||||
return valueOf(string.replace(" & ", "_").toUpperCase(Locale.ENGLISH))
|
||||
return valueOf(string.replace(" & ", "_").uppercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,27 +23,23 @@ import android.os.Handler
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.widget.ScrollView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
|
||||
import dev.patrickgold.florisboard.ime.core.Preferences
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* View class for managing the rendering and the events of a single emoji keyboard key.
|
||||
*
|
||||
* @property florisboard Reference to instance of core class [FlorisBoard].
|
||||
* @property emojiKeyboardView Reference to the parent [EmojiKeyboardView].
|
||||
* @property data The data the current key represents. Is used to determine rendering and possible
|
||||
* behaviour when events occur.
|
||||
* @property key The current key. Is used to determine rendering and possible behaviour when events occur.
|
||||
*/
|
||||
@SuppressLint("ViewConstructor")
|
||||
class EmojiKeyView(
|
||||
@@ -52,7 +48,7 @@ class EmojiKeyView(
|
||||
) : androidx.appcompat.widget.AppCompatTextView(emojiKeyboardView.context), CoroutineScope by MainScope(),
|
||||
FlorisBoard.EventListener, ThemeManager.OnThemeUpdatedListener {
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private val prefs get() = Preferences.default()
|
||||
|
||||
private var isCancelled: Boolean = false
|
||||
private var osHandler: Handler? = null
|
||||
@@ -107,8 +103,8 @@ class EmojiKeyView(
|
||||
(parent as RecyclerView)
|
||||
.requestDisallowInterceptTouchEvent(true)
|
||||
emojiKeyboardView.isScrollBlocked = true
|
||||
emojiKeyboardView.popupManager.show(key, KeyHintMode.DISABLED)
|
||||
emojiKeyboardView.popupManager.extend(key, KeyHintMode.DISABLED)
|
||||
emojiKeyboardView.popupManager.show(key, KeyHintConfiguration.HINTS_DISABLED)
|
||||
emojiKeyboardView.popupManager.extend(key, KeyHintConfiguration.HINTS_DISABLED)
|
||||
florisboard?.keyPressVibrate()
|
||||
florisboard?.keyPressSound()
|
||||
}, delayMillis.toLong())
|
||||
@@ -177,7 +173,7 @@ class EmojiKeyView(
|
||||
|
||||
canvas ?: return
|
||||
|
||||
if (key.computedPopups.isNotEmpty()) {
|
||||
if (key.computedPopups.getPopupKeys(KeyHintConfiguration.HINTS_DISABLED).isNotEmpty()) {
|
||||
triangleDrawable?.draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ fun parseRawEmojiSpecsFile(
|
||||
if (line.startsWith(GROUP_IDENTIFIER, true)) {
|
||||
// A new group begins
|
||||
val rawGroupName = line.trim().substring(GROUP_IDENTIFIER.length)
|
||||
if (rawGroupName.toUpperCase(Locale.ENGLISH) == "COMPONENT") {
|
||||
if (rawGroupName.uppercase() == "COMPONENT") {
|
||||
skipUntilNextGroup = true
|
||||
continue
|
||||
} else {
|
||||
@@ -130,7 +130,7 @@ fun parseRawEmojiSpecsFile(
|
||||
val dataC = data2[0].trim()
|
||||
val dataQ = data2[1].trim()
|
||||
val dataN = data[1].split(NAME_JUNK_SPLIT_REGEX)[1]
|
||||
if (dataQ.toLowerCase(Locale.ENGLISH) == FULLY_QUALIFIED) {
|
||||
if (dataQ.lowercase() == FULLY_QUALIFIED) {
|
||||
// Only fully-qualified emojis are accepted
|
||||
val dataCPs = dataC.split(" ")
|
||||
val key = EmojiKeyData(listStringToListInt(dataCPs), dataN)
|
||||
|
||||
@@ -16,12 +16,15 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.media.emoticon
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Data class for a single emoticon.
|
||||
*
|
||||
* @property icon The char sequence of the emoticon.
|
||||
* @property meaning List of possible meanings for this emoticon.
|
||||
*/
|
||||
@Serializable
|
||||
data class EmoticonKeyData(
|
||||
var icon: String = "",
|
||||
var meaning: List<String> = listOf()
|
||||
|
||||
@@ -36,7 +36,7 @@ class EmoticonKeyboardView : LinearLayout {
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
layout = mainScope.async(Dispatchers.IO) {
|
||||
EmoticonLayoutData.fromJsonFile(context, "ime/media/emoticon/emoticons.json")
|
||||
EmoticonLayoutData.fromJsonFile("ime/media/emoticon/emoticons.json")
|
||||
}
|
||||
orientation = VERTICAL
|
||||
}
|
||||
|
||||
@@ -16,9 +16,14 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.media.emoticon
|
||||
|
||||
import android.content.Context
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetManager
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetSource
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
typealias EmoticonLayoutDataArrangement = List<List<EmoticonKeyData>>
|
||||
|
||||
@Serializable
|
||||
data class EmoticonLayoutData(
|
||||
var type: String,
|
||||
var name: String,
|
||||
@@ -26,18 +31,10 @@ data class EmoticonLayoutData(
|
||||
var arrangement: EmoticonLayoutDataArrangement = listOf()
|
||||
) {
|
||||
companion object {
|
||||
fun fromJsonFile(context: Context, path: String): EmoticonLayoutData? {
|
||||
/*val rawJsonData: String = try {
|
||||
context.assets.open(path).bufferedReader().use { it.readText() }
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} ?: return null
|
||||
val moshi = Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
val layoutAdapter = moshi.adapter(EmoticonLayoutData::class.java)
|
||||
return layoutAdapter.fromJson(rawJsonData)*/
|
||||
return null
|
||||
fun fromJsonFile(path: String): EmoticonLayoutData? {
|
||||
return AssetManager.defaultOrNull()
|
||||
?.loadJsonAsset<EmoticonLayoutData>(AssetRef(AssetSource.Assets, path))
|
||||
?.getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
/**
|
||||
* Represents the root node to a n-gram tree.
|
||||
*/
|
||||
open class NgramTree(
|
||||
sameOrderChildren: MutableList<NgramNode> = mutableListOf(),
|
||||
higherOrderChildren: MutableList<NgramNode> = mutableListOf()
|
||||
) : NgramNode(0, '?', -1, sameOrderChildren, higherOrderChildren)
|
||||
|
||||
/**
|
||||
* A node of a n-gram tree, which holds the character it represents, the corresponding frequency,
|
||||
* a pre-computed string representing all parent characters and the current one as well as child
|
||||
* nodes, one for the same order n-gram nodes and one for the higher order n-gram nodes.
|
||||
*/
|
||||
open class NgramNode(
|
||||
val order: Int,
|
||||
val char: Char,
|
||||
val freq: Int,
|
||||
val sameOrderChildren: MutableList<NgramNode> = mutableListOf(),
|
||||
val higherOrderChildren: MutableList<NgramNode> = mutableListOf()
|
||||
) {
|
||||
companion object {
|
||||
const val FREQ_CHARACTER = -1
|
||||
const val FREQ_WORD_MIN = 0
|
||||
const val FREQ_WORD_MAX = 255
|
||||
const val FREQ_WORD_FILLER = -2
|
||||
const val FREQ_IS_POSSIBLY_OFFENSIVE = 0
|
||||
}
|
||||
|
||||
val isCharacter: Boolean
|
||||
get() = freq == FREQ_CHARACTER
|
||||
|
||||
val isWord: Boolean
|
||||
get() = freq in FREQ_WORD_MIN..FREQ_WORD_MAX
|
||||
|
||||
val isWordFiller: Boolean
|
||||
get() = freq == FREQ_WORD_FILLER
|
||||
|
||||
val isPossiblyOffensive: Boolean
|
||||
get() = freq == FREQ_IS_POSSIBLY_OFFENSIVE
|
||||
|
||||
fun findWord(word: String): NgramNode? {
|
||||
var currentNode = this
|
||||
for ((pos, char) in word.withIndex()) {
|
||||
val childNode = if (pos == 0) {
|
||||
currentNode.higherOrderChildren.find { it.char == char }
|
||||
} else {
|
||||
currentNode.sameOrderChildren.find { it.char == char }
|
||||
}
|
||||
if (childNode != null) {
|
||||
currentNode = childNode
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return if (currentNode.isWord || currentNode.isWordFiller) {
|
||||
currentNode
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function allows to search for a given [input] word with a given [maxEditDistance] and
|
||||
* adds all matches in the trie to the [list].
|
||||
*/
|
||||
fun listSimilarWords(
|
||||
input: String,
|
||||
list: StagedSuggestionList<String, Int>,
|
||||
word: StringBuilder,
|
||||
allowPossiblyOffensive: Boolean,
|
||||
maxEditDistance: Int,
|
||||
deletionCost: Int = 0,
|
||||
insertionCost: Int = 0,
|
||||
substitutionCost: Int = 0,
|
||||
pos: Int = -1
|
||||
) {
|
||||
if (pos > -1) {
|
||||
word.append(char)
|
||||
}
|
||||
val costSum = deletionCost + insertionCost + substitutionCost
|
||||
if (pos > -1 && (pos + 1 == input.length) && isWord && ((isPossiblyOffensive && allowPossiblyOffensive)
|
||||
|| !isPossiblyOffensive)) {
|
||||
// Using shift right instead of divide by 2^(costSum) as it is mathematically the
|
||||
// same but faster.
|
||||
if (list.canAdd(freq shr costSum)) {
|
||||
list.add(word.toString(), freq shr costSum)
|
||||
}
|
||||
}
|
||||
if (pos <= -1) {
|
||||
for (childNode in higherOrderChildren) {
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance, 0, 0, 0, 0
|
||||
)
|
||||
}
|
||||
} else if (maxEditDistance == costSum) {
|
||||
if (pos + 1 < input.length) {
|
||||
sameOrderChildren.find { it.char == input[pos + 1] }?.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost, substitutionCost, pos + 1
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Delete
|
||||
if (pos + 2 < input.length) {
|
||||
sameOrderChildren.find { it.char == input[pos + 2] }?.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost + 1, insertionCost, substitutionCost, pos + 2
|
||||
)
|
||||
}
|
||||
for (childNode in sameOrderChildren) {
|
||||
if (pos + 1 < input.length && childNode.char == input[pos + 1]) {
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost, substitutionCost, pos + 1
|
||||
)
|
||||
} else {
|
||||
// Insert
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost + 1, substitutionCost, pos
|
||||
)
|
||||
if (pos + 1 < input.length) {
|
||||
// Substitute
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost, substitutionCost + 1, pos + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pos > -1) {
|
||||
word.deleteAt(word.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun listAllSameOrderWords(list: StagedSuggestionList<String, Int>, word: StringBuilder, allowPossiblyOffensive: Boolean) {
|
||||
word.append(char)
|
||||
if (isWord && ((isPossiblyOffensive && allowPossiblyOffensive) || !isPossiblyOffensive)) {
|
||||
if (list.canAdd(freq)) {
|
||||
list.add(word.toString(), freq)
|
||||
}
|
||||
}
|
||||
for (childNode in sameOrderChildren) {
|
||||
childNode.listAllSameOrderWords(list, word, allowPossiblyOffensive)
|
||||
}
|
||||
word.deleteAt(word.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
open class FlorisLanguageModel(
|
||||
initTreeObj: NgramTree? = null
|
||||
) : LanguageModel<String, Int> {
|
||||
protected val ngramTree: NgramTree = initTreeObj ?: NgramTree()
|
||||
|
||||
override fun getNgram(vararg tokens: String): Ngram<String, Int> {
|
||||
val ngramOut = getNgramOrNull(*tokens)
|
||||
if (ngramOut != null) {
|
||||
return ngramOut
|
||||
} else {
|
||||
throw NullPointerException("No n-gram found matching the given tokens: $tokens")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNgram(ngram: Ngram<String, Int>): Ngram<String, Int> {
|
||||
val ngramOut = getNgramOrNull(ngram)
|
||||
if (ngramOut != null) {
|
||||
return ngramOut
|
||||
} else {
|
||||
throw NullPointerException("No n-gram found matching the given ngram: $ngram")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNgramOrNull(vararg tokens: String): Ngram<String, Int>? {
|
||||
var currentNode: NgramNode = ngramTree
|
||||
for (token in tokens) {
|
||||
val childNode = currentNode.findWord(token)
|
||||
if (childNode != null) {
|
||||
currentNode = childNode
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return Ngram(tokens.toList().map { Token(it) }, currentNode.freq)
|
||||
}
|
||||
|
||||
override fun getNgramOrNull(ngram: Ngram<String, Int>): Ngram<String, Int>? {
|
||||
return getNgramOrNull(*ngram.tokens.toStringList().toTypedArray())
|
||||
}
|
||||
|
||||
override fun hasNgram(ngram: Ngram<String, Int>, doMatchFreq: Boolean): Boolean {
|
||||
val result = getNgramOrNull(ngram)
|
||||
return if (result != null) {
|
||||
if (doMatchFreq) {
|
||||
ngram.freq == result.freq
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun matchAllNgrams(
|
||||
ngram: Ngram<String, Int>,
|
||||
maxEditDistance: Int,
|
||||
maxTokenCount: Int,
|
||||
allowPossiblyOffensive: Boolean
|
||||
): List<WeightedToken<String, Int>> {
|
||||
val ngramList = mutableListOf<WeightedToken<String, Int>>()
|
||||
var currentNode: NgramNode = ngramTree
|
||||
for ((t, token) in ngram.tokens.withIndex()) {
|
||||
val word = token.data
|
||||
if (t + 1 >= ngram.tokens.size) {
|
||||
if (word.isNotEmpty()) {
|
||||
// The last word is not complete, so find all possible words and sort
|
||||
val splitWord = mutableListOf<Char>()
|
||||
var splitNode: NgramNode? = currentNode
|
||||
for ((pos, char) in word.withIndex()) {
|
||||
val node = if (pos == 0) {
|
||||
splitNode?.higherOrderChildren?.find { it.char == char }
|
||||
} else {
|
||||
splitNode?.sameOrderChildren?.find { it.char == char }
|
||||
}
|
||||
splitWord.add(char)
|
||||
splitNode = node
|
||||
if (node == null) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (splitNode != null) {
|
||||
// Input thus far is valid
|
||||
val wordNodes = StagedSuggestionList<String, Int>(maxTokenCount)
|
||||
val strBuilder = StringBuilder().append(word.substring(0, word.length - 1))
|
||||
splitNode.listAllSameOrderWords(wordNodes, strBuilder, allowPossiblyOffensive)
|
||||
ngramList.addAll(wordNodes)
|
||||
}
|
||||
if (ngramList.size < maxTokenCount) {
|
||||
val wordNodes = StagedSuggestionList<String, Int>(maxTokenCount)
|
||||
val strBuilder = StringBuilder()
|
||||
currentNode.listSimilarWords(word, wordNodes, strBuilder, allowPossiblyOffensive, maxEditDistance)
|
||||
ngramList.addAll(wordNodes)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val node = currentNode.findWord(word)
|
||||
if (node == null) {
|
||||
return ngramList
|
||||
} else {
|
||||
currentNode = node
|
||||
}
|
||||
}
|
||||
}
|
||||
return ngramList
|
||||
}
|
||||
|
||||
fun toFlorisMutableLanguageModel(): FlorisMutableLanguageModel = FlorisMutableLanguageModel(ngramTree)
|
||||
}
|
||||
|
||||
open class FlorisMutableLanguageModel(
|
||||
initTreeObj: NgramTree? = null
|
||||
) : MutableLanguageModel<String, Int>, FlorisLanguageModel(initTreeObj) {
|
||||
override fun deleteNgram(ngram: Ngram<String, Int>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun insertNgram(ngram: Ngram<String, Int>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun updateNgram(ngram: Ngram<String, Int>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
fun toFlorisLanguageModel(): FlorisLanguageModel = FlorisLanguageModel(ngramTree)
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
/**
|
||||
* Abstract interface for a language model. Can house any n-grams with a minimum order of one.
|
||||
*/
|
||||
interface LanguageModel<T : Any, F : Comparable<F>> {
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [tokens]. Throws a NPE if no match could be found.
|
||||
*/
|
||||
@Throws(NullPointerException::class)
|
||||
fun getNgram(vararg tokens: T): Ngram<T, F>
|
||||
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [ngram], whereas the frequency is ignored while
|
||||
* searching. Throws a NPE if no match could be found.
|
||||
*/
|
||||
@Throws(NullPointerException::class)
|
||||
fun getNgram(ngram: Ngram<T, F>): Ngram<T, F>
|
||||
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [tokens]. Returns null if no match could be found.
|
||||
*/
|
||||
fun getNgramOrNull(vararg tokens: T): Ngram<T, F>?
|
||||
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [ngram], whereas the frequency is ignored while
|
||||
* searching. Returns null if no match could be found.
|
||||
*/
|
||||
fun getNgramOrNull(ngram: Ngram<T, F>): Ngram<T, F>?
|
||||
|
||||
/**
|
||||
* Checks if a given [ngram] exists within this model. If [doMatchFreq] is set to true, the
|
||||
* frequency is also matched.
|
||||
*/
|
||||
fun hasNgram(ngram: Ngram<T, F>, doMatchFreq: Boolean = false): Boolean
|
||||
|
||||
/**
|
||||
* Matches all n-grams which match the given [ngram], whereas the last item in the n-gram is
|
||||
* is used to search for predictions.
|
||||
*/
|
||||
fun matchAllNgrams(
|
||||
ngram: Ngram<T, F>,
|
||||
maxEditDistance: Int,
|
||||
maxTokenCount: Int,
|
||||
allowPossiblyOffensive: Boolean
|
||||
): List<WeightedToken<T, F>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutable version of [LanguageModel].
|
||||
*/
|
||||
interface MutableLanguageModel<T : Any, F : Comparable<F>> : LanguageModel<T, F> {
|
||||
fun deleteNgram(ngram: Ngram<T, F>)
|
||||
|
||||
fun insertNgram(ngram: Ngram<T, F>)
|
||||
|
||||
fun updateNgram(ngram: Ngram<T, F>)
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
/**
|
||||
* Abstract interface representing a n-gram of tokens. Each n-gram instance can be assigned a
|
||||
* unique frequency [freq].
|
||||
*/
|
||||
open class Ngram<T : Any, F : Comparable<F>>(_tokens: List<Token<T>>, _freq: F) {
|
||||
companion object {
|
||||
/** Constant order value for unigrams. */
|
||||
const val ORDER_UNIGRAM: Int = 1
|
||||
|
||||
/** Constant order value for bigrams. */
|
||||
const val ORDER_BIGRAM: Int = 2
|
||||
|
||||
/** Constant order value for trigrams. */
|
||||
const val ORDER_TRIGRAM: Int = 3
|
||||
}
|
||||
|
||||
init {
|
||||
if (_tokens.size < ORDER_UNIGRAM) {
|
||||
throw Exception("A n-gram must contain at least 1 token!")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of tokens for this n-gram. The length of this list is guaranteed to be matching
|
||||
* [order].
|
||||
*/
|
||||
val tokens: List<Token<T>> = _tokens
|
||||
|
||||
/**
|
||||
* The frequency value of this n-gram.
|
||||
*/
|
||||
val freq: F = _freq
|
||||
|
||||
/**
|
||||
* The order of this n-gram (1, 2, 3, ...).
|
||||
*/
|
||||
val order: Int
|
||||
get() = tokens.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract interface representing a token used in [Ngram].
|
||||
*/
|
||||
open class Token<T : Any>(_data: T) {
|
||||
/**
|
||||
* The data of this token.
|
||||
*/
|
||||
val data: T = _data
|
||||
|
||||
override fun toString(): String {
|
||||
return "Token(\"$data\")"
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return data.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Token<*>
|
||||
|
||||
if (data != other.data) return false
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as [Token] but allows to add a frequency value [freq].
|
||||
*/
|
||||
open class WeightedToken<T : Any, F : Comparable<F>>(_data: T, _freq: F) : Token<T>(_data) {
|
||||
/**
|
||||
* The frequency of this weighed token.
|
||||
*/
|
||||
val freq: F = _freq
|
||||
|
||||
override fun toString(): String {
|
||||
return "WeightedToken(\"$data\", $freq)"
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return data.hashCode() + 31 * freq.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as WeightedToken<*, *>
|
||||
|
||||
if (data != other.data || freq != other.freq) return false
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of tokens carrying [CharSequence] data to a list of [CharSequence].
|
||||
*/
|
||||
fun List<Token<CharSequence>>.toCharSequenceList(): List<CharSequence> {
|
||||
return this.map { it.data }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of tokens carrying [String] data to a list of [String].
|
||||
*/
|
||||
fun List<Token<String>>.toStringList(): List<String> {
|
||||
return this.map { it.data }
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
class StagedSuggestionList<T : Any, F : Comparable<F>>(
|
||||
private val maxSize: Int
|
||||
) : Collection<WeightedToken<T, F>> {
|
||||
private val internalArray: Array<WeightedToken<T, F>?> = Array(maxSize) { null }
|
||||
private var internalSize: Int = 0
|
||||
|
||||
override val size: Int
|
||||
get() = internalSize
|
||||
|
||||
fun add(token: T, freq: F): Boolean {
|
||||
if (internalSize < maxSize) {
|
||||
internalArray[internalSize++] = WeightedToken(token, freq)
|
||||
internalArray.sortByDescending { it?.freq }
|
||||
return true
|
||||
} else {
|
||||
if (internalArray.last()!!.freq < freq) {
|
||||
internalArray[internalArray.lastIndex] = WeightedToken(token, freq)
|
||||
internalArray.sortByDescending { it?.freq }
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun canAdd(freq: F): Boolean {
|
||||
return internalSize < maxSize || internalArray.last()!!.freq < freq
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
for (n in internalArray.indices) {
|
||||
internalArray[n] = null
|
||||
}
|
||||
internalSize = 0
|
||||
}
|
||||
|
||||
override fun contains(element: WeightedToken<T, F>): Boolean = internalArray.contains(element)
|
||||
|
||||
override fun containsAll(elements: Collection<WeightedToken<T, F>>): Boolean {
|
||||
elements.forEach { if (!contains(it)) return false }
|
||||
return true
|
||||
}
|
||||
|
||||
@Throws(IndexOutOfBoundsException::class)
|
||||
operator fun get(index: Int): WeightedToken<T, F> {
|
||||
val element = getOrNull(index)
|
||||
if (element == null) {
|
||||
throw IndexOutOfBoundsException("The specified index $index is not within the bounds of this list!")
|
||||
} else {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrNull(index: Int): WeightedToken<T, F>? {
|
||||
return internalArray.getOrNull(index)
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean = internalSize <= 0
|
||||
|
||||
override fun iterator(): Iterator<WeightedToken<T, F>> {
|
||||
return StagedIterator(this)
|
||||
}
|
||||
|
||||
class StagedIterator<T : Any, F : Comparable<F>> internal constructor (
|
||||
private val stagedSuggestionList: StagedSuggestionList<T, F>
|
||||
) : Iterator<WeightedToken<T, F>> {
|
||||
var index = 0
|
||||
|
||||
override fun next(): WeightedToken<T, F> = stagedSuggestionList[index++]
|
||||
|
||||
override fun hasNext(): Boolean = stagedSuggestionList.getOrNull(index) != null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
import dev.patrickgold.florisboard.common.NativeInstanceWrapper
|
||||
import dev.patrickgold.florisboard.common.NativePtr
|
||||
|
||||
@JvmInline
|
||||
value class SuggestionList private constructor(
|
||||
private val _nativePtr: NativePtr
|
||||
) : Collection<String>, NativeInstanceWrapper {
|
||||
companion object {
|
||||
fun new(maxSize: Int): SuggestionList {
|
||||
val nativePtr = nativeInitialize(maxSize)
|
||||
return SuggestionList(nativePtr)
|
||||
}
|
||||
|
||||
external fun nativeInitialize(maxSize: Int): NativePtr
|
||||
external fun nativeDispose(nativePtr: NativePtr)
|
||||
|
||||
external fun nativeAdd(nativePtr: NativePtr, word: Word, freq: Freq): Boolean
|
||||
external fun nativeClear(nativePtr: NativePtr)
|
||||
external fun nativeContains(nativePtr: NativePtr, element: Word): Boolean
|
||||
external fun nativeGetOrNull(nativePtr: NativePtr, index: Int): Word?
|
||||
external fun nativeGetIsPrimaryTokenAutoInsert(nativePtr: NativePtr): Boolean
|
||||
external fun nativeSetIsPrimaryTokenAutoInsert(nativePtr: NativePtr, v: Boolean)
|
||||
external fun nativeSize(nativePtr: NativePtr): Int
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = nativeSize(_nativePtr)
|
||||
|
||||
fun add(word: Word, freq: Freq): Boolean {
|
||||
return nativeAdd(_nativePtr, word, freq)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
nativeClear(_nativePtr)
|
||||
}
|
||||
|
||||
override fun contains(element: Word): Boolean {
|
||||
return nativeContains(_nativePtr, element)
|
||||
}
|
||||
|
||||
override fun containsAll(elements: Collection<Word>): Boolean {
|
||||
elements.forEach { if (!contains(it)) return false }
|
||||
return true
|
||||
}
|
||||
|
||||
@Throws(IndexOutOfBoundsException::class)
|
||||
operator fun get(index: Int): Word {
|
||||
val element = getOrNull(index)
|
||||
if (element == null) {
|
||||
throw IndexOutOfBoundsException("The specified index $index is not within the bounds of this list!")
|
||||
} else {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrNull(index: Int): Word? {
|
||||
return nativeGetOrNull(_nativePtr, index)
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean = size <= 0
|
||||
|
||||
val isPrimaryTokenAutoInsert: Boolean
|
||||
get() = nativeGetIsPrimaryTokenAutoInsert(_nativePtr)
|
||||
|
||||
override fun iterator(): Iterator<Word> {
|
||||
return SuggestionListIterator(this)
|
||||
}
|
||||
|
||||
override fun nativePtr(): NativePtr {
|
||||
return _nativePtr
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
nativeDispose(_nativePtr)
|
||||
}
|
||||
|
||||
class SuggestionListIterator internal constructor (
|
||||
private val suggestionList: SuggestionList
|
||||
) : Iterator<Word> {
|
||||
var index = 0
|
||||
|
||||
override fun next(): Word = suggestionList[index++]
|
||||
|
||||
override fun hasNext(): Boolean = suggestionList.getOrNull(index) != null
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,7 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.nlp
|
||||
|
||||
class TextProcessor {
|
||||
data class Word(
|
||||
val word: String,
|
||||
val isPossiblyOffensive: Boolean = false
|
||||
)
|
||||
}
|
||||
typealias Word = String
|
||||
typealias Freq = Int
|
||||
|
||||
const val NATIVE_NULLPTR = 0
|
||||
@@ -18,18 +18,21 @@ package dev.patrickgold.florisboard.ime.onehanded
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Resources
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.Preferences
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
|
||||
class OneHandedPanel : LinearLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
private var florisboard: FlorisBoard? = null
|
||||
private var themeManager: ThemeManager? = null
|
||||
private val prefs get() = Preferences.default()
|
||||
|
||||
private var closeBtn: ImageButton? = null
|
||||
private var moveBtn: ImageButton? = null
|
||||
@@ -55,14 +58,14 @@ class OneHandedPanel : LinearLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
closeBtn = findViewWithTag("one_handed_ctrl_close")
|
||||
closeBtn?.setOnClickListener {
|
||||
florisboard?.let {
|
||||
it.prefs.keyboard.oneHandedMode = OneHandedMode.OFF
|
||||
prefs.keyboard.oneHandedMode = OneHandedMode.OFF
|
||||
it.updateOneHandedPanelVisibility()
|
||||
}
|
||||
}
|
||||
moveBtn = findViewWithTag("one_handed_ctrl_move")
|
||||
moveBtn?.setOnClickListener {
|
||||
florisboard?.let {
|
||||
it.prefs.keyboard.oneHandedMode = panelSide
|
||||
prefs.keyboard.oneHandedMode = panelSide
|
||||
it.updateOneHandedPanelVisibility()
|
||||
}
|
||||
}
|
||||
@@ -94,9 +97,8 @@ class OneHandedPanel : LinearLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val florisboard = florisboard ?: return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
val width = (florisboard.inputView?.measuredWidth ?: 0) *
|
||||
((100 - florisboard.prefs.keyboard.oneHandedModeScaleFactor) / 100.0f)
|
||||
val width = (Resources.getSystem().displayMetrics.widthPixels) *
|
||||
((100 - prefs.keyboard.oneHandedModeScaleFactor) / 100.0f)
|
||||
super.onMeasure(MeasureSpec.makeMeasureSpec(width.toInt(), MeasureSpec.EXACTLY), heightMeasureSpec)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,17 +30,17 @@ import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.keyboard.Key
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
import dev.patrickgold.florisboard.common.ViewUtils
|
||||
import kotlin.math.min
|
||||
|
||||
class PopupExtendedView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
|
||||
private val activeBackgroundDrawable: PaintDrawable = PaintDrawable().apply {
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
setCornerRadius(ViewUtils.dp2px(6.0f))
|
||||
}
|
||||
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
setCornerRadius(ViewUtils.dp2px(6.0f))
|
||||
}
|
||||
private val labelPaint: Paint = Paint().apply {
|
||||
alpha = 255
|
||||
@@ -86,7 +86,7 @@ class PopupExtendedView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
layoutDirection = LAYOUT_DIRECTION_LTR
|
||||
visibility = GONE
|
||||
background = backgroundDrawable
|
||||
elevation = ViewLayoutUtils.convertDpToPixel(4.0f, context)
|
||||
elevation = ViewUtils.dp2px(4.0f)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
|
||||
@@ -45,6 +45,7 @@ class PopupLayerView : FrameLayout {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
setWillNotDraw(true)
|
||||
}
|
||||
|
||||
var clipboardPopupManager: ClipboardPopupManager? = null
|
||||
|
||||
@@ -82,16 +82,19 @@ class PopupManager<V : View>(
|
||||
* Helper function to create a element for the extended popup and preconfigure it.
|
||||
*
|
||||
* @param key Reference to the key currently controlling the popup.
|
||||
* @param keyHintConfiguration The key hint configuration to use.
|
||||
* @param adjustedIndex The index of the key in the key data popup array.
|
||||
* @return A preconfigured extended popup element.
|
||||
*/
|
||||
private fun createElement(
|
||||
key: Key,
|
||||
keyHintConfiguration: KeyHintConfiguration,
|
||||
adjustedIndex: Int
|
||||
): PopupExtendedView.Element {
|
||||
return when (key) {
|
||||
is TextKey -> {
|
||||
when (key.computedPopups[adjustedIndex].code) {
|
||||
val popupKey = key.computedPopups.getPopupKeys(keyHintConfiguration)[adjustedIndex]
|
||||
when (popupKey.code) {
|
||||
KeyCode.SETTINGS -> {
|
||||
getDrawable(keyboardView.context, R.drawable.ic_settings)?.let {
|
||||
PopupExtendedView.Element.Icon(it, adjustedIndex)
|
||||
@@ -114,7 +117,7 @@ class PopupManager<V : View>(
|
||||
}
|
||||
KeyCode.URI_COMPONENT_TLD -> {
|
||||
PopupExtendedView.Element.Tld(
|
||||
key.computedPopups[adjustedIndex].asString(isForDisplay = true), adjustedIndex
|
||||
popupKey.asString(isForDisplay = true), adjustedIndex
|
||||
)
|
||||
}
|
||||
KeyCode.TOGGLE_ONE_HANDED_MODE_LEFT,
|
||||
@@ -125,14 +128,15 @@ class PopupManager<V : View>(
|
||||
}
|
||||
else -> {
|
||||
PopupExtendedView.Element.Label(
|
||||
key.computedPopups[adjustedIndex].asString(isForDisplay = true), adjustedIndex
|
||||
popupKey.asString(isForDisplay = true), adjustedIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is EmojiKey -> {
|
||||
val popupKey = key.computedPopups.getPopupKeys(keyHintConfiguration)[adjustedIndex]
|
||||
PopupExtendedView.Element.Label(
|
||||
key.computedPopups[adjustedIndex].asString(isForDisplay = true), adjustedIndex
|
||||
popupKey.asString(isForDisplay = true), adjustedIndex
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
@@ -141,6 +145,28 @@ class PopupManager<V : View>(
|
||||
}
|
||||
}
|
||||
|
||||
fun isSuitableForPopups(key: Key): Boolean {
|
||||
return isSuitableForBasicPopup(key) || isSuitableForExtendedPopup(key)
|
||||
}
|
||||
|
||||
private fun isSuitableForBasicPopup(key: Key): Boolean {
|
||||
return if (key is TextKey) {
|
||||
val c = key.computedData.code
|
||||
c > KeyCode.SPACE && c != KeyCode.MULTIPLE_CODE_POINTS
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSuitableForExtendedPopup(key: Key): Boolean {
|
||||
return if (key is TextKey) {
|
||||
val c = key.computedData.code
|
||||
c > KeyCode.SPACE && c != KeyCode.MULTIPLE_CODE_POINTS || exceptionsForKeyCodes.contains(c)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates all attributes required by both the normal and the extended popup, regardless of
|
||||
* the passed [key]'s code.
|
||||
@@ -179,11 +205,10 @@ class PopupManager<V : View>(
|
||||
* key code is equal to or less than [KeyCode.SPACE].
|
||||
*
|
||||
* @param key Reference to the key currently controlling the popup.
|
||||
* @param keyHintConfiguration The key hint configuration to use.
|
||||
*/
|
||||
fun show(key: Key, keyHintMode: KeyHintMode) {
|
||||
if (key is TextKey && key.computedData.code <= KeyCode.SPACE && key.computedData.code != KeyCode.MULTIPLE_CODE_POINTS) {
|
||||
return
|
||||
}
|
||||
fun show(key: Key, keyHintConfiguration: KeyHintConfiguration) {
|
||||
if (!isSuitableForBasicPopup(key)) return
|
||||
|
||||
calc(key)
|
||||
|
||||
@@ -200,8 +225,8 @@ class PopupManager<V : View>(
|
||||
}
|
||||
labelTextSize = keyPopupTextSize
|
||||
shouldIndicateExtendedPopups = when (key) {
|
||||
is TextKey -> key.computedPopups.size(keyHintMode) > 0
|
||||
is EmojiKey -> key.computedPopups.isNotEmpty()
|
||||
is TextKey -> key.computedPopups.getPopupKeys(keyHintConfiguration).isNotEmpty()
|
||||
is EmojiKey -> key.computedPopups.getPopupKeys(keyHintConfiguration).isNotEmpty()
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -228,12 +253,10 @@ class PopupManager<V : View>(
|
||||
* K K ... K K
|
||||
*
|
||||
* @param key Reference to the key currently controlling the popup.
|
||||
* @param keyHintConfiguration The key hint configuration to use.
|
||||
*/
|
||||
fun extend(key: Key, keyHintMode: KeyHintMode) {
|
||||
if (key is TextKey && key.computedData.code <= KeyCode.SPACE && key.computedData.code != KeyCode.MULTIPLE_CODE_POINTS
|
||||
&& !exceptionsForKeyCodes.contains(key.computedData.code)) {
|
||||
return
|
||||
}
|
||||
fun extend(key: Key, keyHintConfiguration: KeyHintConfiguration) {
|
||||
if (!isSuitableForExtendedPopup(key)) return
|
||||
|
||||
if (!isShowingPopup) {
|
||||
calc(key)
|
||||
@@ -245,8 +268,8 @@ class PopupManager<V : View>(
|
||||
|
||||
// Determine key counts for each row
|
||||
val n = when (key) {
|
||||
is TextKey -> key.computedPopups.size(keyHintMode)
|
||||
is EmojiKey -> key.computedPopups.size(keyHintMode)
|
||||
is TextKey -> key.computedPopups.getPopupKeys(keyHintConfiguration).size
|
||||
is EmojiKey -> key.computedPopups.getPopupKeys(keyHintConfiguration).size
|
||||
else -> 0
|
||||
}
|
||||
when {
|
||||
@@ -303,43 +326,38 @@ class PopupManager<V : View>(
|
||||
val uiIndices = IntRange(0, (n - 1).coerceAtLeast(0))
|
||||
if (key is TextKey) {
|
||||
popupIndices = IntArray(n) { 0 }
|
||||
when (keyHintMode) {
|
||||
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when {
|
||||
key.computedPopups.main != null -> {
|
||||
popupIndices[initUiIndex] = PopupSet.MAIN_INDEX
|
||||
if (key.computedPopups.hint != null) when {
|
||||
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupSet.HINT_INDEX
|
||||
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupSet.HINT_INDEX
|
||||
}
|
||||
}
|
||||
key.computedPopups.hint != null -> when {
|
||||
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupSet.HINT_INDEX
|
||||
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupSet.HINT_INDEX
|
||||
else -> popupIndices[initUiIndex] = PopupSet.HINT_INDEX
|
||||
val popupKeys = key.computedPopups.getPopupKeys(keyHintConfiguration)
|
||||
when (popupKeys.prioritized.size) {
|
||||
// only one key: use initial position
|
||||
1 -> {
|
||||
popupIndices[initUiIndex] = PopupKeys.FIRST_PRIORITIZED
|
||||
}
|
||||
// two keys: use initial position and one to the right if available, otherwise one to the left
|
||||
2 -> {
|
||||
popupIndices[initUiIndex] = PopupKeys.FIRST_PRIORITIZED
|
||||
when {
|
||||
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupKeys.SECOND_PRIORITIZED
|
||||
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupKeys.SECOND_PRIORITIZED
|
||||
}
|
||||
}
|
||||
KeyHintMode.ENABLED_HINT_PRIORITY -> when {
|
||||
key.computedPopups.hint != null -> {
|
||||
popupIndices[initUiIndex] = PopupSet.HINT_INDEX
|
||||
if (key.computedPopups.main != null) when {
|
||||
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupSet.MAIN_INDEX
|
||||
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupSet.MAIN_INDEX
|
||||
// two keys: use initial position and one to either sides if available
|
||||
// otherwise two to the right or two to the left with decreasing priority
|
||||
3 -> {
|
||||
popupIndices[initUiIndex] = PopupKeys.FIRST_PRIORITIZED
|
||||
when {
|
||||
initUiIndex + 1 < n && initUiIndex - 1 >= 0 -> {
|
||||
popupIndices[initUiIndex + 1] = PopupKeys.SECOND_PRIORITIZED
|
||||
popupIndices[initUiIndex - 1] = PopupKeys.THIRD_PRIORITIZED
|
||||
}
|
||||
initUiIndex + 2 < n -> {
|
||||
popupIndices[initUiIndex + 1] = PopupKeys.SECOND_PRIORITIZED
|
||||
popupIndices[initUiIndex + 2] = PopupKeys.THIRD_PRIORITIZED
|
||||
}
|
||||
initUiIndex - 2 >= 0 -> {
|
||||
popupIndices[initUiIndex - 1] = PopupKeys.SECOND_PRIORITIZED
|
||||
popupIndices[initUiIndex - 2] = PopupKeys.THIRD_PRIORITIZED
|
||||
}
|
||||
}
|
||||
key.computedPopups.main != null -> popupIndices[initUiIndex] = PopupSet.MAIN_INDEX
|
||||
}
|
||||
KeyHintMode.ENABLED_SMART_PRIORITY -> when {
|
||||
key.computedPopups.main != null -> {
|
||||
popupIndices[initUiIndex] = PopupSet.MAIN_INDEX
|
||||
if (key.computedPopups.hint != null) when {
|
||||
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupSet.HINT_INDEX
|
||||
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupSet.HINT_INDEX
|
||||
}
|
||||
}
|
||||
key.computedPopups.hint != null -> popupIndices[initUiIndex] = PopupSet.HINT_INDEX
|
||||
}
|
||||
KeyHintMode.DISABLED -> when {
|
||||
key.computedPopups.main != null -> popupIndices[initUiIndex] = PopupSet.MAIN_INDEX
|
||||
}
|
||||
}
|
||||
var offset = 0
|
||||
@@ -360,7 +378,7 @@ class PopupManager<V : View>(
|
||||
for (uiIndex in uiIndices) {
|
||||
val rowIndex = if (uiIndex < row1count && row1count > 0) { 1 } else { 0 }
|
||||
popupViewExt.properties.elements[rowIndex].add(
|
||||
createElement(key, popupIndices[uiIndex])
|
||||
createElement(key, keyHintConfiguration, popupIndices[uiIndex])
|
||||
)
|
||||
}
|
||||
|
||||
@@ -417,7 +435,7 @@ class PopupManager<V : View>(
|
||||
return false
|
||||
}
|
||||
|
||||
popupViewExt.properties.activeElementIndex = when {
|
||||
val newActiveElementIndex = when {
|
||||
anchorLeft -> when {
|
||||
// check if out of boundary on x-axis
|
||||
x < keyPopupDiffX - (anchorOffset + 1) * keyPopupWidth ||
|
||||
@@ -462,23 +480,27 @@ class PopupManager<V : View>(
|
||||
}
|
||||
else -> -1
|
||||
}
|
||||
popupViewExt.invalidate()
|
||||
if (newActiveElementIndex != popupViewExt.properties.activeElementIndex) {
|
||||
popupViewExt.properties.activeElementIndex = newActiveElementIndex
|
||||
popupViewExt.invalidate()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the [KeyData] of the currently active key. May be either the key of the popup preview
|
||||
* Gets the [TextKeyData] of the currently active key. May be either the key of the popup preview
|
||||
* or one of the keys in extended popup, if shown. Returns null if [key] is not a subclass of [TextKey].
|
||||
*
|
||||
* @param key Reference to the key currently controlling the popup.
|
||||
* @return The [KeyData] object of the currently active key or null.
|
||||
* @param keyHintConfiguration The key hint configuration to be used.
|
||||
* @return The [TextKeyData] object of the currently active key or null.
|
||||
*/
|
||||
fun getActiveKeyData(key: Key): TextKeyData? {
|
||||
fun getActiveKeyData(key: Key, keyHintConfiguration: KeyHintConfiguration): TextKeyData? {
|
||||
return if (key is TextKey) {
|
||||
val element = popupViewExt.properties.getElementOrNull()
|
||||
if (element != null) {
|
||||
key.computedPopups.getOrNull(element.adjustedIndex) ?: key.computedData
|
||||
key.computedPopups.getPopupKeys(keyHintConfiguration).getOrNull(element.adjustedIndex) ?: key.computedData
|
||||
} else {
|
||||
key.computedData
|
||||
}
|
||||
@@ -498,7 +520,7 @@ class PopupManager<V : View>(
|
||||
return if (key is EmojiKey) {
|
||||
val element = popupViewExt.properties.getElementOrNull()
|
||||
if (element != null) {
|
||||
key.computedPopups.getOrNull(element.adjustedIndex) ?: key.computedData
|
||||
key.computedPopups.getPopupKeys(KeyHintConfiguration.HINTS_DISABLED).getOrNull(element.adjustedIndex) ?: key.computedData
|
||||
} else {
|
||||
key.computedData
|
||||
}
|
||||
|
||||
@@ -17,43 +17,215 @@
|
||||
package dev.patrickgold.florisboard.ime.popup
|
||||
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.BasicTextKeyData
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.TextComputingEvaluator
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* A popup set for a single key. This set describes, if the key has a [hint] character,
|
||||
* a [main] character and other [relevant] popups.
|
||||
* A popup set for a single key. This set describes, if the key has a [main] character and other [relevant] popups.
|
||||
*
|
||||
* Note, that a hint character should **never** be set in a json extended popup file, rather it
|
||||
* Note that a hint character cannot and should not be set in a json extended popup file, rather it
|
||||
* should only be dynamically set by the LayoutManager.
|
||||
*
|
||||
* The order in which these defined popups will be shown depends on the current [KeyHintMode],
|
||||
* al well as the calculations made by the KeyPopupManager.
|
||||
*
|
||||
* The popup set can be accessed like an array with the addition that negative indexes defined
|
||||
* within this companion object are allowed (as long as the corresponding [hint] or [main]
|
||||
* character is *not* null).
|
||||
* The order in which these defined popups will be shown depends on the current [KeyHintConfiguration].
|
||||
*/
|
||||
@Serializable
|
||||
open class PopupSet<T : KeyData>(
|
||||
open val hint: T? = null,
|
||||
open val main: T? = null,
|
||||
open val relevant: List<T> = listOf()
|
||||
) {
|
||||
private val popupKeys: PopupKeys<T> by lazy {
|
||||
PopupKeys(null, listOfNotNull(main), relevant)
|
||||
}
|
||||
|
||||
open fun getPopupKeys(keyHintConfiguration: KeyHintConfiguration): PopupKeys<T> {
|
||||
return popupKeys
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class MutablePopupSet<T : KeyData>(
|
||||
override var main: T? = null,
|
||||
override val relevant: ArrayList<T> = arrayListOf(),
|
||||
var symbolHint: T? = null,
|
||||
var numberHint: T? = null,
|
||||
private val symbolPopups: ArrayList<T> = arrayListOf(),
|
||||
private val numberPopups: ArrayList<T> = arrayListOf(),
|
||||
private val configCache: MutableMap<KeyHintConfiguration, PopupKeys<T>> = mutableMapOf()
|
||||
) : PopupSet<T>(main, relevant) {
|
||||
|
||||
fun clear() {
|
||||
symbolHint = null
|
||||
numberHint = null
|
||||
main = null
|
||||
relevant.clear()
|
||||
symbolPopups.clear()
|
||||
numberPopups.clear()
|
||||
configCache.clear()
|
||||
}
|
||||
|
||||
override fun getPopupKeys(keyHintConfiguration: KeyHintConfiguration): PopupKeys<T> {
|
||||
return configCache.getOrPut(keyHintConfiguration) {
|
||||
initPopupList(keyHintConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initPopupList(keyHintConfiguration: KeyHintConfiguration): PopupKeys<T> {
|
||||
val localMain = main
|
||||
val localRelevant = relevant
|
||||
val localSymbolHint = symbolHint
|
||||
val localNumberHint = numberHint
|
||||
if (localSymbolHint != null && keyHintConfiguration.symbolHintMode != KeyHintMode.DISABLED) {
|
||||
if (localNumberHint != null && keyHintConfiguration.numberHintMode != KeyHintMode.DISABLED) {
|
||||
val hintPopups = if (keyHintConfiguration.mergeHintPopups) { symbolPopups + numberPopups } else { listOf() }
|
||||
return when (keyHintConfiguration.symbolHintMode) {
|
||||
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when (keyHintConfiguration.numberHintMode) {
|
||||
// when both hints are present in accent priority, always have a non-hint key first if possible
|
||||
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when {
|
||||
localMain != null -> PopupKeys(localSymbolHint, listOf(localMain, localSymbolHint, localNumberHint), localRelevant + hintPopups)
|
||||
localRelevant.isNotEmpty() -> PopupKeys(localSymbolHint, listOf(localRelevant[0], localSymbolHint, localNumberHint), localRelevant.subList(1, localRelevant.size) + hintPopups)
|
||||
else -> PopupKeys(localSymbolHint, listOf(localSymbolHint, localNumberHint), hintPopups)
|
||||
}
|
||||
// hint priority of number hint wins and overrules accent priority of symbol hint
|
||||
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localNumberHint, localMain, localSymbolHint), localRelevant + hintPopups)
|
||||
// due to smart priority of number hint, main wins if it exists, otherwise number hint overrules accent priority of symbol hint
|
||||
else -> PopupKeys(localSymbolHint, listOfNotNull(localMain, localNumberHint, localSymbolHint), localRelevant + hintPopups)
|
||||
}
|
||||
KeyHintMode.ENABLED_HINT_PRIORITY -> when (keyHintConfiguration.symbolHintMode) {
|
||||
// when both hints are present in hint priority, symbol hint wins
|
||||
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localSymbolHint, localNumberHint, localMain), localRelevant + hintPopups)
|
||||
// hint priority of symbol hint wins, and overrules potential accent priority of number hint
|
||||
else -> PopupKeys(localSymbolHint, listOfNotNull(localSymbolHint, localMain, localNumberHint), localRelevant + hintPopups)
|
||||
}
|
||||
else -> when (keyHintConfiguration.numberHintMode) {
|
||||
// smart priority of symbol hint wins, and overrules accent priority of number hint
|
||||
KeyHintMode.ENABLED_ACCENT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localMain, localSymbolHint, localNumberHint), localRelevant + hintPopups)
|
||||
// hint priority of number hint wins
|
||||
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localNumberHint, localMain, localSymbolHint), localRelevant + hintPopups)
|
||||
// when both hints are in smart priority, always have main first if possible
|
||||
else -> PopupKeys(localSymbolHint, listOfNotNull(localMain, localSymbolHint, localNumberHint), localRelevant + hintPopups)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val hintPopups = if (keyHintConfiguration.mergeHintPopups) { symbolPopups } else { listOf() }
|
||||
return when (keyHintConfiguration.symbolHintMode) {
|
||||
// in accent priority, always show a non-hint key first if possible
|
||||
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when {
|
||||
localMain != null -> PopupKeys(localSymbolHint, listOf(localMain, localSymbolHint), localRelevant + hintPopups)
|
||||
localRelevant.isNotEmpty() -> PopupKeys(localSymbolHint, listOf(localRelevant[0], localSymbolHint), localRelevant.subList(1, localRelevant.size) + hintPopups)
|
||||
else -> PopupKeys(localSymbolHint, listOf(localSymbolHint), hintPopups)
|
||||
}
|
||||
// in hint priority, always show hint first
|
||||
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localSymbolHint, localMain), localRelevant + hintPopups)
|
||||
// in smart priority, show main first if possible
|
||||
else -> PopupKeys(localSymbolHint, listOfNotNull(localMain, localSymbolHint), localRelevant + hintPopups)
|
||||
}
|
||||
}
|
||||
} else if (localNumberHint != null && keyHintConfiguration.numberHintMode != KeyHintMode.DISABLED) {
|
||||
val hintPopups = if (keyHintConfiguration.mergeHintPopups) { numberPopups } else { listOf() }
|
||||
return when (keyHintConfiguration.numberHintMode) {
|
||||
// in accent priority, always show a non-hint key first if possible
|
||||
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when {
|
||||
localMain != null -> PopupKeys(localNumberHint, listOf(localMain, localNumberHint), localRelevant + hintPopups)
|
||||
localRelevant.isNotEmpty() -> PopupKeys(localNumberHint, listOf(localRelevant[0], localNumberHint), localRelevant.subList(1, localRelevant.size) + hintPopups)
|
||||
else -> PopupKeys(localNumberHint, listOf(localNumberHint), hintPopups)
|
||||
}
|
||||
// in hint priority, always show hint first
|
||||
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localNumberHint, listOfNotNull(localNumberHint, localMain), localRelevant + hintPopups)
|
||||
// in smart priority, show main first if possible
|
||||
else -> PopupKeys(localNumberHint, listOfNotNull(localMain, localNumberHint), localRelevant + hintPopups)
|
||||
}
|
||||
} else {
|
||||
// if no hints shall be shown, use main first if possible
|
||||
return PopupKeys(null, listOfNotNull(localMain), localRelevant)
|
||||
}
|
||||
}
|
||||
|
||||
fun merge(other: PopupSet<T>, evaluator: TextComputingEvaluator) {
|
||||
mergeInternal(other, evaluator, relevant, true)
|
||||
}
|
||||
|
||||
fun mergeSymbolHint(hintPopups: PopupSet<T>, evaluator: TextComputingEvaluator) {
|
||||
mergeInternal(hintPopups, evaluator, symbolPopups)
|
||||
}
|
||||
|
||||
fun mergeNumberHint(hintPopups: PopupSet<T>, evaluator: TextComputingEvaluator) {
|
||||
mergeInternal(hintPopups, evaluator, numberPopups)
|
||||
}
|
||||
|
||||
private fun mergeInternal(other: PopupSet<T>, evaluator: TextComputingEvaluator, targetList: MutableList<T>, useMain: Boolean = false) {
|
||||
other.relevant.forEach {
|
||||
val data = it.computeTextKeyData(evaluator) as? T
|
||||
if (data != null) {
|
||||
targetList.add(data)
|
||||
}
|
||||
}
|
||||
other.main?.let {
|
||||
val data = it.computeTextKeyData(evaluator) as? T
|
||||
if (data != null) {
|
||||
if (useMain && main == null) {
|
||||
main = data
|
||||
} else {
|
||||
targetList.add(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A fully configured collection of popup keys. It contains a list of keys to be prioritized
|
||||
* during rendering (ordered by relevance descending) by showing those keys close to the
|
||||
* popup spawning point.
|
||||
*
|
||||
* The keys contain a separate [hint] key to ease rendering the hint label, but the hint, if
|
||||
* present, also occurs in the [prioritized] list.
|
||||
*
|
||||
* The popup keys can be accessed like an array with the addition that negative indexes defined
|
||||
* within this companion object are allowed (as long as the corresponding [prioritized] list
|
||||
* contains the corresponding amount of keys.
|
||||
*/
|
||||
class PopupKeys<T>(
|
||||
val hint: T?,
|
||||
val prioritized: List<T>,
|
||||
val other: List<T>
|
||||
) : Collection<T> {
|
||||
companion object {
|
||||
const val HINT_INDEX: Int = -2
|
||||
const val MAIN_INDEX: Int = -1
|
||||
const val FIRST_PRIORITIZED = -1
|
||||
const val SECOND_PRIORITIZED = -2
|
||||
const val THIRD_PRIORITIZED = -3
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = if (hint != null) { 1 } else { 0 } + if (main != null) { 1 } else { 0 } + relevant.size
|
||||
get() = prioritized.size + other.size
|
||||
|
||||
fun size(keyHintMode: KeyHintMode): Int {
|
||||
return if (keyHintMode == KeyHintMode.DISABLED && hint != null) {
|
||||
size - 1
|
||||
} else {
|
||||
size
|
||||
override fun contains(element: T): Boolean {
|
||||
return prioritized.contains(element) || other.contains(element)
|
||||
}
|
||||
|
||||
override fun containsAll(elements: Collection<T>): Boolean {
|
||||
return (prioritized + other).containsAll(elements)
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return prioritized.isEmpty() && other.isEmpty()
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<T> {
|
||||
return (prioritized + other).listIterator()
|
||||
}
|
||||
|
||||
fun getOrNull(index: Int): T? {
|
||||
if (index >= other.size || index < -prioritized.size) {
|
||||
return null
|
||||
}
|
||||
return when (index) {
|
||||
FIRST_PRIORITIZED -> prioritized[0]
|
||||
SECOND_PRIORITIZED -> prioritized[1]
|
||||
THIRD_PRIORITIZED -> prioritized[2]
|
||||
else -> other.getOrNull(index)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,125 +233,10 @@ open class PopupSet<T : KeyData>(
|
||||
val item = getOrNull(index)
|
||||
if (item == null) {
|
||||
throw IndexOutOfBoundsException(
|
||||
"Specified index $index is not an valid entry in this PopupSet!"
|
||||
"Specified index $index is not an valid entry in this PopupKeys!"
|
||||
)
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrNull(index: Int): T? {
|
||||
if (index >= relevant.size || index < HINT_INDEX) {
|
||||
return null
|
||||
}
|
||||
return when (index) {
|
||||
HINT_INDEX -> hint
|
||||
MAIN_INDEX -> main
|
||||
else -> relevant.getOrNull(index)
|
||||
}
|
||||
}
|
||||
|
||||
override fun contains(element: T): Boolean {
|
||||
return hint == element || main == element || relevant.contains(element)
|
||||
}
|
||||
|
||||
override fun containsAll(elements: Collection<T>): Boolean {
|
||||
for (element in elements) {
|
||||
if (!contains(element)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<T> {
|
||||
return PopupSetIterator(this)
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return size == 0
|
||||
}
|
||||
|
||||
class PopupSetIterator<T : KeyData> internal constructor (
|
||||
private val popupSet: PopupSet<T>
|
||||
) : Iterator<T> {
|
||||
var index = HINT_INDEX
|
||||
|
||||
override fun next(): T = popupSet[index++]
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
if (index == HINT_INDEX) {
|
||||
if (popupSet.getOrNull(index) != null) {
|
||||
return true
|
||||
} else {
|
||||
index++
|
||||
}
|
||||
}
|
||||
if (index == MAIN_INDEX) {
|
||||
if (popupSet.getOrNull(index) != null) {
|
||||
return true
|
||||
} else {
|
||||
index++
|
||||
}
|
||||
}
|
||||
return popupSet.getOrNull(index) != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class MutablePopupSet<T : KeyData>(
|
||||
override var hint: T? = null,
|
||||
override var main: T? = null,
|
||||
override val relevant: ArrayList<T> = arrayListOf()
|
||||
) : PopupSet<T>(hint, main, relevant) {
|
||||
fun clear() {
|
||||
hint = null
|
||||
main = null
|
||||
relevant.clear()
|
||||
}
|
||||
|
||||
fun merge(other: PopupSet<T>) {
|
||||
relevant.addAll(other.relevant)
|
||||
other.hint?.let {
|
||||
if (hint == null) {
|
||||
hint = it
|
||||
} else {
|
||||
relevant.add(it)
|
||||
}
|
||||
}
|
||||
other.main?.let {
|
||||
if (main == null) {
|
||||
main = it
|
||||
} else {
|
||||
relevant.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun merge(other: PopupSet<T>, evaluator: TextComputingEvaluator) {
|
||||
other.relevant.forEach {
|
||||
val data = it.computeTextKeyData(evaluator) as? T
|
||||
if (data != null) {
|
||||
relevant.add(data)
|
||||
}
|
||||
}
|
||||
other.hint?.let {
|
||||
if (hint == null) {
|
||||
hint = it
|
||||
} else {
|
||||
relevant.add(it)
|
||||
}
|
||||
}
|
||||
other.main?.let {
|
||||
val data = it.computeTextKeyData(evaluator) as? T
|
||||
if (data != null) {
|
||||
if (main == null) {
|
||||
main = data
|
||||
} else {
|
||||
relevant.add(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.keyboard.Key
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
import dev.patrickgold.florisboard.common.ViewUtils
|
||||
|
||||
class PopupView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
|
||||
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
setCornerRadius(ViewUtils.dp2px(6.0f))
|
||||
}
|
||||
private val labelPaint: Paint = Paint().apply {
|
||||
alpha = 255
|
||||
@@ -91,7 +91,7 @@ class PopupView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
backgroundDrawable.apply {
|
||||
setTint(theme.getAttr(Theme.Attr.POPUP_BACKGROUND).toSolidColor().color)
|
||||
}
|
||||
elevation = ViewLayoutUtils.convertDpToPixel(4.0f, context)
|
||||
elevation = ViewUtils.dp2px(4.0f)
|
||||
threeDotsDrawable?.apply {
|
||||
setTint(theme.getAttr(Theme.Attr.POPUP_FOREGROUND).toSolidColor().color)
|
||||
}
|
||||
|
||||
@@ -16,24 +16,23 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.view.KeyEvent
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.FlorisboardBinding
|
||||
import dev.patrickgold.florisboard.debug.*
|
||||
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
|
||||
import dev.patrickgold.florisboard.ime.core.*
|
||||
import dev.patrickgold.florisboard.ime.dictionary.Dictionary
|
||||
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetManager
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetSource
|
||||
import dev.patrickgold.florisboard.ime.nlp.Token
|
||||
import dev.patrickgold.florisboard.ime.nlp.toStringList
|
||||
import dev.patrickgold.florisboard.ime.keyboard.ImeOptions
|
||||
import dev.patrickgold.florisboard.ime.keyboard.InputAttributes
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardState
|
||||
import dev.patrickgold.florisboard.ime.keyboard.updateKeyboardState
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.GlideTypingManager
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.key.*
|
||||
@@ -42,7 +41,6 @@ import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.SmartbarView
|
||||
import kotlinx.coroutines.*
|
||||
import org.json.JSONArray
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
/**
|
||||
* TextInputManager is responsible for managing everything which is related to text input. All of
|
||||
@@ -59,22 +57,19 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
FlorisBoard.EventListener, SmartbarView.EventListener {
|
||||
|
||||
var isGlidePostEffect: Boolean = false
|
||||
private val florisboard = FlorisBoard.getInstance()
|
||||
private val prefs: PrefHelper get() = florisboard.prefs
|
||||
private val florisboard get() = FlorisBoard.getInstance()
|
||||
private val prefs get() = Preferences.default()
|
||||
val symbolsWithSpaceAfter: List<String>
|
||||
private val activeEditorInstance: EditorInstance
|
||||
get() = florisboard.activeEditorInstance
|
||||
|
||||
lateinit var layoutManager: LayoutManager
|
||||
private set
|
||||
private var activeKeyboardMode: KeyboardMode? = null
|
||||
val keyboards = TextKeyboardCache()
|
||||
private var textInputKeyboardView: TextKeyboardView? = null
|
||||
lateinit var textKeyboardIconSet: TextKeyboardIconSet
|
||||
private set
|
||||
private var textViewGroup: LinearLayout? = null
|
||||
private val dictionaryManager: DictionaryManager = DictionaryManager.default()
|
||||
private var activeDictionary: Dictionary<String, Int>? = null
|
||||
private val dictionaryManager: DictionaryManager get() = DictionaryManager.default()
|
||||
val inputEventDispatcher: InputEventDispatcher = InputEventDispatcher.new(
|
||||
repeatableKeyCodes = intArrayOf(
|
||||
KeyCode.ARROW_DOWN,
|
||||
@@ -86,15 +81,11 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
)
|
||||
)
|
||||
|
||||
var keyVariation: KeyVariation = KeyVariation.NORMAL
|
||||
internal var smartbarView: SmartbarView? = null
|
||||
|
||||
// Caps/Shift related properties
|
||||
var caps: Boolean = false
|
||||
private set
|
||||
var capsLock: Boolean = false
|
||||
private set
|
||||
val activeState: KeyboardState get() = florisboard.activeState
|
||||
private var newCapsState: Boolean = false
|
||||
private var isNumberRowVisible: Boolean = false
|
||||
|
||||
// Composing text related properties
|
||||
var isManualSelectionMode: Boolean = false
|
||||
@@ -123,7 +114,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
|
||||
val evaluator = object : TextComputingEvaluator {
|
||||
override fun evaluateCaps(): Boolean {
|
||||
return caps || capsLock
|
||||
return activeState.caps || activeState.capsLock
|
||||
}
|
||||
|
||||
override fun evaluateCaps(data: TextKeyData): Boolean {
|
||||
@@ -134,8 +125,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
return when (data.code) {
|
||||
KeyCode.CLIPBOARD_COPY,
|
||||
KeyCode.CLIPBOARD_CUT -> {
|
||||
florisboard.activeEditorInstance.selection.isSelectionMode &&
|
||||
!florisboard.activeEditorInstance.isRawInputEditor
|
||||
activeState.isSelectionMode && activeState.isRichInputEditor
|
||||
}
|
||||
KeyCode.CLIPBOARD_PASTE -> {
|
||||
// such gore. checks
|
||||
@@ -146,7 +136,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
) == true
|
||||
}
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> {
|
||||
!florisboard.activeEditorInstance.isRawInputEditor
|
||||
activeState.isRichInputEditor
|
||||
}
|
||||
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT -> {
|
||||
prefs.clipboard.enableHistory
|
||||
@@ -194,7 +184,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
}
|
||||
|
||||
override fun getKeyVariation(): KeyVariation {
|
||||
return keyVariation
|
||||
return activeState.keyVariation
|
||||
}
|
||||
|
||||
override fun getKeyboard(): TextKeyboard {
|
||||
@@ -220,73 +210,33 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
layoutManager = LayoutManager()
|
||||
textKeyboardIconSet = TextKeyboardIconSet.new(florisboard)
|
||||
inputEventDispatcher.keyEventReceiver = this
|
||||
isNumberRowVisible = prefs.keyboard.numberRow
|
||||
var subtypes = florisboard.subtypeManager.subtypes
|
||||
if (subtypes.isEmpty()) {
|
||||
subtypes = listOf(Subtype.DEFAULT)
|
||||
}
|
||||
for (subtype in subtypes) {
|
||||
for (mode in KeyboardMode.values()) {
|
||||
keyboards.set(mode, subtype, keyboard = layoutManager.computeKeyboardAsync(mode, subtype, prefs))
|
||||
keyboards.set(mode, subtype, keyboard = layoutManager.computeKeyboardAsync(mode, subtype))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateInputView() {
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the newly registered input view.
|
||||
*/
|
||||
override fun onRegisterInputView(inputView: InputView) {
|
||||
override fun onInitializeInputUi(uiBinding: FlorisboardBinding) {
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
|
||||
textViewGroup = inputView.findViewById(R.id.text_input)
|
||||
textInputKeyboardView = inputView.findViewById(R.id.text_input_keyboard_view)
|
||||
textInputKeyboardView?.setIconSet(textKeyboardIconSet)
|
||||
textInputKeyboardView?.setComputingEvaluator(evaluator)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
val animator1 = textViewGroup?.let {
|
||||
ObjectAnimator.ofFloat(it, "alpha", 0.9f, 1.0f).apply {
|
||||
duration = 125
|
||||
repeatCount = 0
|
||||
start()
|
||||
}
|
||||
}
|
||||
val animator2 = textViewGroup?.let {
|
||||
ObjectAnimator.ofFloat(it, "alpha", 1.0f, 0.4f).apply {
|
||||
startDelay = 125
|
||||
duration = 500
|
||||
repeatCount = ValueAnimator.INFINITE
|
||||
repeatMode = ValueAnimator.REVERSE
|
||||
start()
|
||||
}
|
||||
}
|
||||
setActiveKeyboardMode(getActiveKeyboardMode())
|
||||
animator1?.cancel()
|
||||
animator2?.cancel()
|
||||
val animator3 = textViewGroup?.let {
|
||||
ObjectAnimator.ofFloat(it, "alpha", it.alpha, 1.0f).apply {
|
||||
duration = (((1.0f - it.alpha) / 0.6f) * 125f).roundToLong()
|
||||
repeatCount = 0
|
||||
start()
|
||||
}
|
||||
}
|
||||
delay(animator3?.duration ?: 1)
|
||||
animator3?.end()
|
||||
textInputKeyboardView = uiBinding.text.mainKeyboardView.also {
|
||||
it.setIconSet(textKeyboardIconSet)
|
||||
it.setComputingEvaluator(evaluator)
|
||||
it.sync()
|
||||
}
|
||||
}
|
||||
|
||||
fun registerSmartbarView(view: SmartbarView) {
|
||||
smartbarView = view
|
||||
smartbarView?.setEventListener(this)
|
||||
}
|
||||
|
||||
fun unregisterSmartbarView(view: SmartbarView) {
|
||||
if (smartbarView == view) {
|
||||
smartbarView = null
|
||||
smartbarView = uiBinding.text.smartbar.root.also {
|
||||
it.setEventListener(this)
|
||||
it.sync()
|
||||
}
|
||||
|
||||
setActiveKeyboardMode(getActiveKeyboardMode(), updateState = false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,32 +245,40 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
override fun onDestroy() {
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
|
||||
smartbarView?.setEventListener(null)
|
||||
smartbarView = null
|
||||
|
||||
textInputKeyboardView?.setComputingEvaluator(null)
|
||||
textInputKeyboardView = null
|
||||
keyboards.clear()
|
||||
|
||||
inputEventDispatcher.keyEventReceiver = null
|
||||
inputEventDispatcher.close()
|
||||
|
||||
dictionaryManager.unloadUserDictionariesIfNecessary()
|
||||
|
||||
cancel()
|
||||
layoutManager.onDestroy()
|
||||
instance = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the [activeKeyboardMode], [keyVariation] and [EditorInstance.isComposingEnabled]
|
||||
* Evaluates the [KeyboardState.keyboardMode], [KeyboardState.keyVariation] and [KeyboardState.isComposingEnabled]
|
||||
* property values when starting to interact with a input editor. Also resets the composing
|
||||
* texts and sets the initial caps mode accordingly.
|
||||
*/
|
||||
override fun onStartInputView(instance: EditorInstance, restarting: Boolean) {
|
||||
val keyboardMode = when (instance.inputAttributes.type) {
|
||||
val keyboardMode = when (activeState.inputAttributes.type) {
|
||||
InputAttributes.Type.NUMBER -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
activeState.keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.NUMERIC
|
||||
}
|
||||
InputAttributes.Type.PHONE -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
activeState.keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.PHONE
|
||||
}
|
||||
InputAttributes.Type.TEXT -> {
|
||||
keyVariation = when (instance.inputAttributes.variation) {
|
||||
activeState.keyVariation = when (activeState.inputAttributes.variation) {
|
||||
InputAttributes.Variation.EMAIL_ADDRESS,
|
||||
InputAttributes.Variation.WEB_EMAIL_ADDRESS -> {
|
||||
KeyVariation.EMAIL_ADDRESS
|
||||
@@ -340,92 +298,91 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
KeyboardMode.CHARACTERS
|
||||
}
|
||||
else -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
activeState.keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.CHARACTERS
|
||||
}
|
||||
}
|
||||
instance.apply {
|
||||
isComposingEnabled = when (keyboardMode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> false
|
||||
else -> keyVariation != KeyVariation.PASSWORD &&
|
||||
prefs.suggestion.enabled// &&
|
||||
//!instance.inputAttributes.flagTextAutoComplete &&
|
||||
//!instance.inputAttributes.flagTextNoSuggestions
|
||||
}
|
||||
isPrivateMode = prefs.advanced.forcePrivateMode ||
|
||||
imeOptions.flagNoPersonalizedLearning
|
||||
activeState.isComposingEnabled = when (keyboardMode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> false
|
||||
else -> activeState.keyVariation != KeyVariation.PASSWORD &&
|
||||
prefs.suggestion.enabled// &&
|
||||
//!instance.inputAttributes.flagTextAutoComplete &&
|
||||
//!instance.inputAttributes.flagTextNoSuggestions
|
||||
}
|
||||
val newIsNumberRowVisible = prefs.keyboard.numberRow
|
||||
if (isNumberRowVisible != newIsNumberRowVisible) {
|
||||
keyboards.clear(KeyboardMode.CHARACTERS)
|
||||
isNumberRowVisible = newIsNumberRowVisible
|
||||
}
|
||||
setActiveKeyboardMode(keyboardMode, updateState = false)
|
||||
instance.composingEnabledChanged()
|
||||
activeState.isPrivateMode = prefs.advanced.forcePrivateMode ||
|
||||
activeState.imeOptions.flagNoPersonalizedLearning
|
||||
if (!prefs.correction.rememberCapsLockState) {
|
||||
capsLock = false
|
||||
activeState.capsLock = false
|
||||
}
|
||||
isGlidePostEffect = false
|
||||
updateCapsState()
|
||||
setActiveKeyboardMode(keyboardMode)
|
||||
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle stuff when finishing to interact with a input editor.
|
||||
*/
|
||||
override fun onFinishInputView(finishingInput: Boolean) {
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
smartbarView?.updateSmartbarState()
|
||||
launch(Dispatchers.Default) {
|
||||
dictionaryManager.loadUserDictionariesIfNecessary()
|
||||
}
|
||||
textInputKeyboardView?.sync()
|
||||
smartbarView?.sync()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets [activeKeyboardMode].
|
||||
* Gets the active keyboard mode.
|
||||
*
|
||||
* @return If null [KeyboardMode.CHARACTERS], else [activeKeyboardMode].
|
||||
* @return The active keyboard mode.
|
||||
*/
|
||||
fun getActiveKeyboardMode(): KeyboardMode {
|
||||
return activeKeyboardMode ?: KeyboardMode.CHARACTERS
|
||||
return activeState.keyboardMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets [activeKeyboardMode] and updates the [SmartbarView.isQuickActionsVisible] state.
|
||||
* Sets the active keyboard mode and updates the [KeyboardState.isQuickActionsVisible] state.
|
||||
*/
|
||||
private fun setActiveKeyboardMode(mode: KeyboardMode) = launch {
|
||||
setActiveKeyboard(mode, florisboard.activeSubtype)
|
||||
activeKeyboardMode = mode
|
||||
private fun setActiveKeyboardMode(mode: KeyboardMode, updateState: Boolean = true) {
|
||||
activeState.keyboardMode = mode
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = false
|
||||
smartbarView?.isQuickActionsVisible = false
|
||||
smartbarView?.updateSmartbarState()
|
||||
activeState.isQuickActionsVisible = false
|
||||
setActiveKeyboard(mode, florisboard.activeSubtype, updateState)
|
||||
}
|
||||
|
||||
private fun setActiveKeyboard(mode: KeyboardMode, subtype: Subtype) = launch(Dispatchers.IO) {
|
||||
private fun setActiveKeyboard(mode: KeyboardMode, subtype: Subtype, updateState: Boolean = true) = launch(Dispatchers.IO) {
|
||||
val activeKeyboard = keyboards.getOrElseAsync(mode, subtype) {
|
||||
layoutManager.computeKeyboardAsync(
|
||||
keyboardMode = mode,
|
||||
subtype = subtype,
|
||||
prefs = prefs
|
||||
subtype = subtype
|
||||
).await()
|
||||
}.await()
|
||||
withContext(Dispatchers.Main) {
|
||||
textInputKeyboardView?.setComputedKeyboard(activeKeyboard)
|
||||
if (updateState) {
|
||||
florisboard.dispatchCurrentStateToInputUi()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSubtypeChanged(newSubtype: Subtype) {
|
||||
override fun onSubtypeChanged(newSubtype: Subtype, doRefreshLayouts: Boolean) {
|
||||
launch {
|
||||
if (activeEditorInstance.isComposingEnabled) {
|
||||
withContext(Dispatchers.IO) {
|
||||
dictionaryManager.loadDictionary(AssetRef(AssetSource.Assets, "ime/dict/en.flict")).let {
|
||||
activeDictionary = it.getOrDefault(null)
|
||||
}
|
||||
}
|
||||
if (activeState.isComposingEnabled) {
|
||||
dictionaryManager.prepareDictionaries(newSubtype)
|
||||
}
|
||||
if (PrefHelper.getDefaultInstance(florisboard).glide.enabled) {
|
||||
if (prefs.glide.enabled) {
|
||||
GlideTypingManager.getInstance().setWordData(newSubtype)
|
||||
}
|
||||
setActiveKeyboard(getActiveKeyboardMode(), newSubtype)
|
||||
if (doRefreshLayouts) {
|
||||
setActiveKeyboard(getActiveKeyboardMode(), newSubtype)
|
||||
}
|
||||
}
|
||||
isGlidePostEffect = false
|
||||
}
|
||||
@@ -438,29 +395,28 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
if (!inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
|
||||
updateCapsState()
|
||||
}
|
||||
smartbarView?.updateSmartbarState()
|
||||
flogInfo(LogTopic.IMS_EVENTS) { "current word: ${activeEditorInstance.cachedInput.currentWord.text}" }
|
||||
if (activeEditorInstance.isComposingEnabled && !inputEventDispatcher.isPressed(KeyCode.DELETE)) {
|
||||
if (activeState.isComposingEnabled && !inputEventDispatcher.isPressed(KeyCode.DELETE) && !isGlidePostEffect) {
|
||||
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 = 16,
|
||||
allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive
|
||||
).toStringList()
|
||||
if (BuildConfig.DEBUG) {
|
||||
val elapsed = (System.nanoTime() - startTime) / 1000.0
|
||||
flogInfo { "sugg fetch time: $elapsed us" }
|
||||
}
|
||||
launch(Dispatchers.Default) {
|
||||
val startTime = System.nanoTime()
|
||||
dictionaryManager.suggest(
|
||||
currentWord = activeEditorInstance.cachedInput.currentWord.text,
|
||||
preceidingWords = listOf(),
|
||||
subtype = florisboard.activeSubtype,
|
||||
allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive,
|
||||
maxSuggestionCount = 16
|
||||
) { suggestions ->
|
||||
withContext(Dispatchers.Main) {
|
||||
smartbarView?.setCandidateSuggestionWords(startTime, suggestions)
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
val elapsed = (System.nanoTime() - startTime) / 1000.0
|
||||
flogInfo { "sugg fetch time: $elapsed us" }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
|
||||
@@ -474,13 +430,12 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
|
||||
/**
|
||||
* Updates the current caps state according to the [EditorInstance.cursorCapsMode], while
|
||||
* respecting [capsLock] property and the correction.autoCapitalization preference.
|
||||
* respecting [KeyboardState.capsLock] property and the correction.autoCapitalization preference.
|
||||
*/
|
||||
private fun updateCapsState() {
|
||||
if (!capsLock) {
|
||||
caps = prefs.correction.autoCapitalization &&
|
||||
if (!activeState.capsLock) {
|
||||
activeState.caps = prefs.correction.autoCapitalization &&
|
||||
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
|
||||
textInputKeyboardView?.notifyStateChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,8 +499,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
|
||||
override fun onSmartbarQuickActionPressed(quickActionId: Int) {
|
||||
when (quickActionId) {
|
||||
R.id.quick_action_toggle -> {
|
||||
activeState.isQuickActionsVisible = !activeState.isQuickActionsVisible
|
||||
smartbarView?.updateKeyboardState(activeState)
|
||||
return
|
||||
}
|
||||
R.id.quick_action_switch_to_editing_context -> {
|
||||
if (activeKeyboardMode == KeyboardMode.EDITING) {
|
||||
if (activeState.keyboardMode == KeyboardMode.EDITING) {
|
||||
setActiveKeyboardMode(KeyboardMode.CHARACTERS)
|
||||
} else {
|
||||
setActiveKeyboardMode(KeyboardMode.EDITING)
|
||||
@@ -563,8 +523,8 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
return
|
||||
}
|
||||
}
|
||||
smartbarView?.isQuickActionsVisible = false
|
||||
smartbarView?.updateSmartbarState()
|
||||
activeState.isQuickActionsVisible = false
|
||||
smartbarView?.updateKeyboardState(activeState)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -598,17 +558,17 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
* Handles a [KeyCode.ENTER] event.
|
||||
*/
|
||||
private fun handleEnter() {
|
||||
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
|
||||
if (activeState.imeOptions.flagNoEnterAction) {
|
||||
activeEditorInstance.performEnter()
|
||||
} else {
|
||||
when (activeEditorInstance.imeOptions.action) {
|
||||
ImeOptions.Action.DONE,
|
||||
ImeOptions.Action.GO,
|
||||
ImeOptions.Action.NEXT,
|
||||
ImeOptions.Action.PREVIOUS,
|
||||
ImeOptions.Action.SEARCH,
|
||||
ImeOptions.Action.SEND -> {
|
||||
activeEditorInstance.performEnterAction(activeEditorInstance.imeOptions.action)
|
||||
when (activeState.imeOptions.enterAction) {
|
||||
ImeOptions.EnterAction.DONE,
|
||||
ImeOptions.EnterAction.GO,
|
||||
ImeOptions.EnterAction.NEXT,
|
||||
ImeOptions.EnterAction.PREVIOUS,
|
||||
ImeOptions.EnterAction.SEARCH,
|
||||
ImeOptions.EnterAction.SEND -> {
|
||||
activeEditorInstance.performEnterAction(activeState.imeOptions.enterAction)
|
||||
}
|
||||
else -> activeEditorInstance.performEnter()
|
||||
}
|
||||
@@ -634,34 +594,34 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
private fun handleShiftDown(ev: InputKeyEvent) {
|
||||
if (ev.isConsecutiveEventOf(inputEventDispatcher.lastKeyEventDown, prefs.keyboard.longPressDelay.toLong())) {
|
||||
newCapsState = true
|
||||
caps = true
|
||||
capsLock = true
|
||||
activeState.caps = true
|
||||
activeState.capsLock = true
|
||||
} else {
|
||||
newCapsState = !caps
|
||||
caps = true
|
||||
capsLock = false
|
||||
newCapsState = !activeState.caps
|
||||
activeState.caps = true
|
||||
activeState.capsLock = false
|
||||
}
|
||||
textInputKeyboardView?.notifyStateChanged()
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
florisboard.dispatchCurrentStateToInputUi()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] up event.
|
||||
*/
|
||||
private fun handleShiftUp() {
|
||||
caps = newCapsState
|
||||
textInputKeyboardView?.notifyStateChanged()
|
||||
activeState.caps = newCapsState
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
florisboard.dispatchCurrentStateToInputUi()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] cancel event.
|
||||
*/
|
||||
private fun handleShiftCancel() {
|
||||
caps = false
|
||||
capsLock = false
|
||||
textInputKeyboardView?.notifyStateChanged()
|
||||
activeState.caps = false
|
||||
activeState.capsLock = false
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
florisboard.dispatchCurrentStateToInputUi()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -671,10 +631,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
val lastKeyEvent = inputEventDispatcher.lastKeyEventDown ?: return
|
||||
if (lastKeyEvent.data.code == KeyCode.SHIFT && lastKeyEvent.action == InputKeyEvent.Action.DOWN) {
|
||||
newCapsState = true
|
||||
caps = true
|
||||
capsLock = true
|
||||
textInputKeyboardView?.notifyStateChanged()
|
||||
activeState.caps = true
|
||||
activeState.capsLock = true
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
florisboard.dispatchCurrentStateToInputUi()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -846,7 +806,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
KeyCode.VIEW_SYMBOLS2 -> setActiveKeyboardMode(KeyboardMode.SYMBOLS2)
|
||||
KeyCode.UNDO -> activeEditorInstance.performUndo()
|
||||
else -> {
|
||||
when (activeKeyboardMode) {
|
||||
when (activeState.keyboardMode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.NUMERIC_ADVANCED,
|
||||
KeyboardMode.PHONE,
|
||||
@@ -885,13 +845,12 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.code != KeyCode.SHIFT && !capsLock && !inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
|
||||
if (data.code != KeyCode.SHIFT && !activeState.capsLock && !inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
|
||||
updateCapsState()
|
||||
}
|
||||
if (ev.data.code > KeyCode.SPACE) {
|
||||
isGlidePostEffect = false
|
||||
}
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
override fun onInputKeyRepeat(ev: InputKeyEvent) {
|
||||
@@ -912,14 +871,14 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
|
||||
/**
|
||||
* Changes a word to the current case.
|
||||
* eg if [capsLock] is true, abc -> ABC
|
||||
* eg if [KeyboardState.capsLock] is true, abc -> ABC
|
||||
* if [caps] is true, abc -> Abc
|
||||
* otherwise , abc -> abc
|
||||
*/
|
||||
fun fixCase(word: String): String {
|
||||
return when {
|
||||
capsLock -> word.toUpperCase(florisboard.activeSubtype.locale)
|
||||
caps -> word.capitalize(florisboard.activeSubtype.locale)
|
||||
activeState.capsLock -> word.uppercase(florisboard.activeSubtype.locale)
|
||||
activeState.caps -> word.replaceFirstChar { if (it.isLowerCase()) it.titlecase(florisboard.activeSubtype.locale) else it.toString() }
|
||||
else -> word
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package dev.patrickgold.florisboard.ime.text.composing
|
||||
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
interface Composer {
|
||||
val name: String
|
||||
val label: String
|
||||
val toRead: Int
|
||||
|
||||
fun getActions(s: String, c: Char): Pair<Int, String>
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("appender")
|
||||
class Appender : Composer {
|
||||
companion object {
|
||||
const val name = "appender"
|
||||
}
|
||||
override val name: String = Appender.name
|
||||
override val label: String = "Appender"
|
||||
override val toRead: Int = 0
|
||||
|
||||
override fun getActions(s: String, c: Char): Pair<Int, String> {
|
||||
return Pair(0, "$c")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("with-rules")
|
||||
class WithRules(
|
||||
override val name: String,
|
||||
override val label: String,
|
||||
val rules: JsonObject
|
||||
) : Composer {
|
||||
override val toRead: Int = rules.keys.toList().sortedBy { it.length }.reversed()[0].length - 1
|
||||
|
||||
@Transient val ruleOrder: List<String> = rules.keys.toList().sortedBy { it.length }.reversed()
|
||||
@Transient val ruleMap: Map<String, String> = rules.entries.map { Pair(it.key, (it.value as JsonPrimitive).content) }.toMap()
|
||||
|
||||
override fun getActions(s: String, c: Char): Pair<Int, String> {
|
||||
val str = "${s}$c"
|
||||
for (key in ruleOrder) {
|
||||
if (str.endsWith(key)) {
|
||||
return Pair(key.length-1, ruleMap.getValue(key))
|
||||
}
|
||||
}
|
||||
return Pair(0, "$c")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package dev.patrickgold.florisboard.ime.text.composing
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@SerialName("hangul-unicode")
|
||||
class HangulUnicode : Composer {
|
||||
override val name: String = "hangul-unicode"
|
||||
override val label: String = "Hangul Unicode"
|
||||
override val toRead: Int = 1
|
||||
|
||||
// Initial consonants, ordered for syllable creation
|
||||
private val initials = "ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ"
|
||||
// Medial vowels, ordered for syllable creation
|
||||
private val medials = "ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ"
|
||||
// Final consonants (including none), ordered for syllable creation
|
||||
private val finals = "_ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ"
|
||||
|
||||
private val medialComp = mapOf(
|
||||
'ㅗ' to listOfNotNull("ㅏㅐㅣ", "ㅘㅙㅚ"),
|
||||
'ㅜ' to listOfNotNull("ㅓㅔㅣ", "ㅝㅞㅟ"),
|
||||
'ㅡ' to listOfNotNull("ㅣ", "ㅢ"),
|
||||
)
|
||||
|
||||
private val finalComp = mapOf(
|
||||
'ㄱ' to listOfNotNull("ㅅ", "ㄳ"),
|
||||
'ㄴ' to listOfNotNull("ㅈㅎ", "ㄵㄶ"),
|
||||
'ㄹ' to listOfNotNull("ㄱㅁㅂㅅㅌㅍㅎ", "ㄺㄻㄼㄽㄾㄿㅀ"),
|
||||
'ㅂ' to listOfNotNull("ㅅ", "ㅄ"),
|
||||
)
|
||||
|
||||
private fun reverseComp(map: Map<Char, List<String>>): Map<Char, List<Char>> {
|
||||
val ret = mutableMapOf<Char, List<Char>>()
|
||||
for ((first, v) in map) {
|
||||
val (seconds, comps) = v
|
||||
for (i in seconds.indices) {
|
||||
ret[comps[i]] = listOf(first, seconds[i])
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
private val finalCompRev = reverseComp(finalComp)
|
||||
private val medialCompRev = reverseComp(medialComp)
|
||||
|
||||
private fun syllable(ini: Int, med: Int, fin:Int): Char {
|
||||
return (ini*588 + med*28 + fin + 44032).toChar()
|
||||
}
|
||||
|
||||
private fun syllableBlocks(syllOrd: Int): List<Int> {
|
||||
val initial = (syllOrd-44032)/588
|
||||
val medial = (syllOrd-44032-initial*588)/28
|
||||
val fin = (syllOrd-44032)%28
|
||||
return listOf(initial, medial, fin)
|
||||
}
|
||||
|
||||
override fun getActions(s: String, c: Char): Pair<Int, String> {
|
||||
// s is "at least the last 1 character of what's currently here"
|
||||
if (s.isEmpty()) {
|
||||
return Pair(0, ""+c)
|
||||
}
|
||||
val lastChar = s.last()
|
||||
val lastOrd = lastChar.toInt()
|
||||
|
||||
if (lastChar in initials && c in medials) {
|
||||
return Pair(1, "${syllable(initials.indexOf(lastChar), medials.indexOf(c), 0)}")
|
||||
} else if (lastOrd in 44032..55203) { // syllable
|
||||
val (ini, med, fin) = syllableBlocks(lastOrd)
|
||||
|
||||
// underscore is a sentinel in the "finals" string
|
||||
if (c == '_')
|
||||
return Pair(0, ""+c)
|
||||
|
||||
// if there is no final and the new char is a final, merge
|
||||
if (fin == 0 && c in finals)
|
||||
return Pair(1, "${syllable(ini, med, finals.indexOf(c))}")
|
||||
|
||||
// if there is already a final but it is mergeable with the new char into a composed final, merge
|
||||
if ((finals[fin] in finalComp) && c in finalComp[finals[fin]]!![0]) {
|
||||
val tple = finalComp[finals[fin]]
|
||||
return Pair(1, "${syllable(ini, med, finals.indexOf(tple!![1][tple[0].indexOf(c)]))}")
|
||||
}
|
||||
|
||||
// if there is a simple final and the new char is a medial, split the old syllable
|
||||
if (fin != 0 && finals[fin] !in finalCompRev && c in medials)
|
||||
return Pair(1, "${syllable(ini, med, 0)}${syllable(initials.indexOf(finals[fin]), medials.indexOf(c), 0)}")
|
||||
|
||||
// if there is a composed final and the new char is a medial, split the old final
|
||||
if (finals[fin] in finalCompRev && c in medials)
|
||||
return Pair(1, "${syllable(ini, med, finals.indexOf(finalCompRev.getValue(finals[fin])[0]))}${syllable(initials.indexOf(finalCompRev.getValue(finals[fin])[1]), medials.indexOf(c), 0)}")
|
||||
|
||||
// if no final yet, and current medial can be composed with new char, merge
|
||||
if (medials[med] in medialComp && c in medialComp.getValue(medials[med])[0] && fin == 0) {
|
||||
val tple = medialComp[medials[med]]
|
||||
return Pair(1, "${syllable(ini, medials.indexOf(tple!![1][tple[0].indexOf(c)]), 0)}")
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(0, ""+c)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user