Compare commits
99 Commits
v0.3.8
...
v0.3.10-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2ec115505 | ||
|
|
670e6ca5e1 | ||
|
|
f2403d00e5 | ||
|
|
224d3e00e3 | ||
|
|
e89a374ce0 | ||
|
|
538e2dd9a2 | ||
|
|
1d3d85c211 | ||
|
|
d6121baca9 | ||
|
|
8c0337d6c9 | ||
|
|
563a4a919d | ||
|
|
2f0d607d02 | ||
|
|
65ae6c2b66 | ||
|
|
14513ec0f1 | ||
|
|
3c58144a3d | ||
|
|
d65b706f78 | ||
|
|
9d820677db | ||
|
|
69c52c00f6 | ||
|
|
c8cf256577 | ||
|
|
386a0999c4 | ||
|
|
d4ef2ea827 | ||
|
|
381ec68e6c | ||
|
|
a5706167b2 | ||
|
|
660871d6c8 | ||
|
|
6607ad1739 | ||
|
|
55c1bc05f2 | ||
|
|
7eb7f0ef80 | ||
|
|
78e5e417ce | ||
|
|
ffbf7f8ea7 | ||
|
|
27cc4897c3 | ||
|
|
e5111a8efe | ||
|
|
80fd5ca84a | ||
|
|
e8f2c6ce74 | ||
|
|
5676cbf18e | ||
|
|
2bdaea6189 | ||
|
|
da2287a739 | ||
|
|
3fafe0fac8 | ||
|
|
86042bb1e1 | ||
|
|
c99673ff1d | ||
|
|
8b89b27fb0 | ||
|
|
b56c976fa0 | ||
|
|
08889fdc60 | ||
|
|
e8d657e81c | ||
|
|
e581d6cbc4 | ||
|
|
ec13d008fb | ||
|
|
edfea1afcb | ||
|
|
25fc23d721 | ||
|
|
bfcea8b718 | ||
|
|
c701141be2 | ||
|
|
7f07686b6c | ||
|
|
e5b956857e | ||
|
|
67236ef58d | ||
|
|
2da17a0654 | ||
|
|
1f3221a886 | ||
|
|
47f80d00c4 | ||
|
|
e4ecc63b9d | ||
|
|
d648c480b5 | ||
|
|
9e26720674 | ||
|
|
a20c6bf148 | ||
|
|
d2df5cfcdf | ||
|
|
93b5503dfc | ||
|
|
4d4b54074a | ||
|
|
904fd9b85a | ||
|
|
aacb33bd5d | ||
|
|
a0aa446988 | ||
|
|
fe086ed6d8 | ||
|
|
64ddd0f421 | ||
|
|
40fe72e33c | ||
|
|
b229970ec3 | ||
|
|
ec32c211f1 | ||
|
|
e66b8a052a | ||
|
|
4a22c2698c | ||
|
|
ae95bbd7c4 | ||
|
|
0bdeeaa340 | ||
|
|
92a885a34c | ||
|
|
bc2f03a920 | ||
|
|
f60827b634 | ||
|
|
dcf81b27a0 | ||
|
|
0d8601cb15 | ||
|
|
ecf3c6bf27 | ||
|
|
e4f5fcf74b | ||
|
|
15f0316839 | ||
|
|
93654c4f88 | ||
|
|
62fc549ea9 | ||
|
|
d0dbd1cd4e | ||
|
|
af28f84b69 | ||
|
|
db7ee52029 | ||
|
|
7343617792 | ||
|
|
5898d7006b | ||
|
|
058be7a169 | ||
|
|
e6f2a25021 | ||
|
|
3a485a1574 | ||
|
|
0ee0f24119 | ||
|
|
004e999259 | ||
|
|
11775c4619 | ||
|
|
177bad95b3 | ||
|
|
610526d845 | ||
|
|
55e489bc07 | ||
|
|
589063be61 | ||
|
|
aa73ac706a |
@@ -11,3 +11,7 @@ trim_trailing_whitespace = true
|
||||
|
||||
[{*.har,*.json}]
|
||||
indent_size = 2
|
||||
|
||||
[*.kt]
|
||||
ij_kotlin_name_count_to_use_star_import = 99
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 99
|
||||
|
||||
32
README.md
@@ -1,14 +1,15 @@
|
||||
<img align="left" width="80" height="80"
|
||||
src="fastlane/metadata/android/en-US/images/icon.png" alt="App icon">
|
||||
|
||||
# FlorisBoard [](https://github.com/florisboard/florisboard/releases) [](https://crowdin.florisboard.patrickgold.dev) 
|
||||
# FlorisBoard [](https://crowdin.florisboard.patrickgold.dev) 
|
||||
|
||||
**FlorisBoard** is a free and open-source keyboard for Android 6.0+
|
||||
devices. It aims at being modern, user-friendly and customizable while
|
||||
fully respecting your privacy. Currently in alpha/early-beta state.
|
||||
fully respecting your privacy. Currently in early-beta state.
|
||||
|
||||
## Public Alpha Test Programme
|
||||
Wanna try it out on your device? Use one of the following options:
|
||||
### Stable [](https://github.com/florisboard/florisboard/releases/latest)
|
||||
|
||||
Releases on this track are in general stable and ready for everyday use, except for features marked as experimental. Use one of the following options to receive FlorisBoard's stable releases:
|
||||
|
||||
_A. Get it on F-Droid_:
|
||||
|
||||
@@ -36,6 +37,16 @@ for and download FlorisBoard without prior joining the alpha group.
|
||||
|
||||
_C. Use the APK provided in the release section of this repo_
|
||||
|
||||
### Beta [](https://github.com/florisboard/florisboard/releases)
|
||||
|
||||
Releases on this track are also in general stable and should be ready for everyday use, though crashes and bugs are more likely to occur. Use releases from this track if you want to get new features faster and give feedback for brand-new stuff. Options to get beta releases:
|
||||
|
||||
_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_
|
||||
|
||||
### Giving feedback
|
||||
If you want to give feedback to FlorisBoard, there are several ways to
|
||||
do so, as listed [here](CONTRIBUTING.md#giving-general-feedback).
|
||||
@@ -62,11 +73,9 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
|
||||
* [x] Landscape orientation support (needs tweaks)
|
||||
|
||||
### Layouts
|
||||
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish,
|
||||
Norwegian, Swedish/Finnish, Icelandic, Danish, Hungarian,
|
||||
Croatian, Polish, Romanian, Colemak, Dvorak); more coming in future versions
|
||||
* [x] Non-latin character layouts (Arabic, Persian, Greek, Russian
|
||||
(JCUKEN))
|
||||
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish, Norwegian, Swedish/Finnish, Icelandic, Danish,
|
||||
Hungarian, Croatian, Polish, Romanian, Colemak, Dvorak, Turkish-Q, Turkish-F, ...)
|
||||
* [x] Non-latin character layouts (Arabic, Persian, Greek, Russian (JCUKEN))
|
||||
* [x] Adapt to situation in app (password, url, text, etc. )
|
||||
* [x] Special character layout(s)
|
||||
* [x] Numeric layout
|
||||
@@ -88,6 +97,7 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
|
||||
### Other useful features
|
||||
* [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 integration in IME service list of Android (xml/method)
|
||||
@@ -138,6 +148,7 @@ close as possible.
|
||||
and `Follow time`)
|
||||
- Define a separate theme both for day and night theme
|
||||
- Adapt to app theme if possible
|
||||
- Theme import/export
|
||||
|
||||
### [v0.5.0](https://github.com/florisboard/florisboard/milestone/5)
|
||||
There's no exact roadmap yet but it is planned that the media part of
|
||||
@@ -150,7 +161,6 @@ passes...
|
||||
|
||||
Backlog (currently not assigned to any milestone):
|
||||
|
||||
- Theme import/export
|
||||
- Floating keyboard
|
||||
|
||||
[#91]: https://github.com/florisboard/florisboard/pull/91
|
||||
@@ -179,8 +189,6 @@ to get more information on this topic.
|
||||
[Jared Rummler](https://github.com/jaredrummler)
|
||||
* [Timber](https://github.com/JakeWharton/timber) by
|
||||
[JakeWharton](https://github.com/JakeWharton)
|
||||
* [kotlin-result](https://github.com/michaelbull/kotlin-result) by
|
||||
[Michael Bull](https://github.com/michaelbull)
|
||||
* [expandable-fab](https://github.com/nambicompany/expandable-fab) by
|
||||
[Nambi](https://github.com/nambicompany)
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
|
||||
plugins {
|
||||
id("com.android.application") version "4.1.2"
|
||||
kotlin("android") version "1.4.30"
|
||||
kotlin("kapt") version "1.4.30"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion(29)
|
||||
buildToolsVersion("29.0.2")
|
||||
compileSdkVersion(30)
|
||||
buildToolsVersion("30.0.3")
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
@@ -20,9 +22,9 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "dev.patrickgold.florisboard"
|
||||
minSdkVersion(23)
|
||||
targetSdkVersion(29)
|
||||
versionCode(27)
|
||||
versionName("0.3.8")
|
||||
targetSdkVersion(30)
|
||||
versionCode(32)
|
||||
versionName("0.3.10")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -34,11 +36,29 @@ android {
|
||||
buildTypes {
|
||||
named("debug").configure {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
|
||||
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_debug")
|
||||
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_debug_round")
|
||||
resValue("string", "floris_app_name", "FlorisBoard Debug")
|
||||
}
|
||||
|
||||
create("beta") // Needed because by default the "beta" BuildType does not exist
|
||||
named("beta").configure {
|
||||
applicationIdSuffix = ".beta"
|
||||
versionNameSuffix = "-beta04"
|
||||
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
|
||||
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_beta")
|
||||
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_beta_round")
|
||||
resValue("string", "floris_app_name", "FlorisBoard Beta")
|
||||
}
|
||||
|
||||
named("release").configure {
|
||||
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
|
||||
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_release")
|
||||
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_release_round")
|
||||
resValue("string", "floris_app_name", "@string/app_name")
|
||||
}
|
||||
}
|
||||
@@ -54,11 +74,15 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.activity", "activity-ktx", "1.2.1")
|
||||
implementation("androidx.appcompat", "appcompat", "1.2.0")
|
||||
implementation("androidx.core", "core-ktx", "1.3.2")
|
||||
implementation("androidx.fragment", "fragment-ktx", "1.3.0")
|
||||
implementation("androidx.preference", "preference-ktx", "1.1.1")
|
||||
implementation("androidx.constraintlayout", "constraintlayout", "2.0.4")
|
||||
implementation("androidx.lifecycle", "lifecycle-service", "2.2.0")
|
||||
implementation("com.google.android", "flexbox", "2.0.1") // requires jcenter as of version 2.0.1
|
||||
implementation("com.squareup.moshi", "moshi-kotlin", "1.11.0")
|
||||
implementation("com.squareup.moshi", "moshi-adapters", "1.11.0")
|
||||
@@ -66,8 +90,9 @@ dependencies {
|
||||
implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-android", "1.4.2")
|
||||
implementation("com.jaredrummler", "colorpicker", "1.1.0")
|
||||
implementation("com.jakewharton.timber", "timber", "4.7.1")
|
||||
implementation("com.michael-bull.kotlin-result", "kotlin-result", "1.1.10")
|
||||
implementation("com.nambimobile.widgets", "expandable-fab", "1.0.2")
|
||||
implementation("androidx.room", "room-runtime", "2.2.6")
|
||||
kapt("androidx.room", "room-compiler","2.2.6")
|
||||
|
||||
testImplementation("junit", "junit", "4.13.1")
|
||||
testImplementation("org.mockito", "mockito-inline", "3.7.7")
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
<application
|
||||
android:name=".ime.core.FlorisApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/floris_app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/SettingsTheme">
|
||||
|
||||
@@ -46,19 +46,19 @@
|
||||
<!-- Settings Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.SettingsMainActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__title"
|
||||
android:launchMode="singleTask"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Using an activity alias to disable/enable the app icon in the launcher -->
|
||||
<activity-alias
|
||||
android:name="dev.patrickgold.florisboard.SettingsLauncherAlias"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/floris_app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:targetActivity="dev.patrickgold.florisboard.setup.SetupActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
@@ -69,48 +69,56 @@
|
||||
<!-- Theme Selector Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.ThemeManagerActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__title"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Theme Editor Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.ThemeEditorActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__theme_editor__title"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- About Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.AboutActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/about__title"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Advanced Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.AdvancedActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__advanced__title"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Setup Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.setup.SetupActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/setup__title"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Crash Dialog Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.crashutility.CrashDialogActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/crash_dialog__title"
|
||||
android:theme="@style/CrashDialogTheme"/>
|
||||
|
||||
<provider
|
||||
android:name="dev.patrickgold.florisboard.ime.clip.provider.FlorisContentProvider"
|
||||
android:authorities="dev.patrickgold.florisboard.provider.clip"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
</provider>
|
||||
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -4,31 +4,33 @@
|
||||
"qwerty": "QWERTY",
|
||||
"qwertz": "QWERTZ",
|
||||
"azerty": "AZERTY",
|
||||
"arabic": "Arabic",
|
||||
"bepo": "BÉPO",
|
||||
"bulgarian_bds": "Bulgarian (BDS)",
|
||||
"bulgarian_phonetic": "Bulgarian (Phonetic)",
|
||||
"spanish": "Spanish (QWERTY)",
|
||||
"norwegian": "Norwegian (QWERTY)",
|
||||
"swedish_finnish": "Swedish/Finnish (QWERTY)",
|
||||
"canadian_french": "Canadian French (QWERTY)",
|
||||
"colemak": "Colemak",
|
||||
"danish": "Danish (QWERTY)",
|
||||
"dvorak": "Dvorak",
|
||||
"esperanto": "Esperanto",
|
||||
"esperanto_with_hx": "Esperanto with 'ĥ'",
|
||||
"greek": "Ελληνικά",
|
||||
"hebrew": "עברית",
|
||||
"hungarian": "Hungarian (QWERTZ)",
|
||||
"icelandic": "Icelandic (QWERTY)",
|
||||
"kurdish": "کوردی",
|
||||
"norwegian": "Norwegian (QWERTY)",
|
||||
"persian": "Persian",
|
||||
"jcuken_russian": "Russian (JCUKEN)",
|
||||
"serbian_latin": "Serbian (QWERTZ)",
|
||||
"serbian_cyrillic": "Serbian (ЉЊЕРТЗ)",
|
||||
"spanish": "Spanish (QWERTY)",
|
||||
"swedish_finnish": "Swedish/Finnish (QWERTY)",
|
||||
"swiss_german": "Swiss German (QWERTZ)",
|
||||
"swiss_french": "Swiss French (QWERTZ)",
|
||||
"swiss_italian": "Swiss Italian (QWERTZ)",
|
||||
"hungarian": "Hungarian (QWERTZ)",
|
||||
"persian": "Persian",
|
||||
"arabic": "Arabic",
|
||||
"esperanto": "Esperanto",
|
||||
"esperanto_with_hx": "Esperanto with 'ĥ'",
|
||||
"colemak": "Colemak",
|
||||
"dvorak": "Dvorak",
|
||||
"jcuken_russian": "Russian (JCUKEN)",
|
||||
"canadian_french": "Canadian French (QWERTY)",
|
||||
"greek": "Ελληνικά",
|
||||
"hebrew": "עברית",
|
||||
"serbian_latin": "Serbian (QWERTZ)",
|
||||
"serbian_cyrillic": "Serbian (ЉЊЕРТЗ)",
|
||||
"kurdish": "کوردی"
|
||||
"turkish_q": "Turkish-Q",
|
||||
"turkish_f": "Turkish-F"
|
||||
},
|
||||
"defaultSubtypes": [
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"relevant": [
|
||||
{ "code": 58, "label": ":" }
|
||||
]
|
||||
} }
|
||||
}, "shift": { "code": 58, "label": ":" } }
|
||||
],
|
||||
[
|
||||
{ "code": 97, "label": "a" },
|
||||
|
||||
@@ -12,25 +12,25 @@
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 34, "label": "\"" }
|
||||
]
|
||||
} },
|
||||
}, "shift": { "code": 34, "label": "\"" } },
|
||||
{ "code": 39, "label": "'", "groupId": 101, "variation": "password", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 34, "label": "\"" }
|
||||
]
|
||||
} },
|
||||
}, "shift": { "code": 34, "label": "\"" } },
|
||||
{ "code": 47, "label": "/", "groupId": 101, "variation": "uri" },
|
||||
{ "code": 44, "label": ",", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 60, "label": "<" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
} },
|
||||
}, "shift": { "code": 60, "label": "<" } },
|
||||
{ "code": 46, "label": ".", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 62, "label": ">" }
|
||||
]
|
||||
} },
|
||||
}, "shift": { "code": 62, "label": ">" } },
|
||||
{ "code": 112, "label": "p" },
|
||||
{ "code": 121, "label": "y" },
|
||||
{ "code": 102, "label": "f" },
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"~enter": {
|
||||
"main": { "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
|
||||
"relevant": [
|
||||
{ "code": -216, "label": "toggle_one_handed_mode_right", "type": "system_gui" }
|
||||
{ "code": -216, "label": "toggle_one_handed_mode_right", "type": "system_gui" },
|
||||
{ "code": -214, "label": "switch_to_clipboard_context", "type": "system_gui"}
|
||||
]
|
||||
},
|
||||
"~left": {
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
]
|
||||
},
|
||||
"z": {
|
||||
"relevant": [
|
||||
{ "code": 382, "label": "ž" }
|
||||
]
|
||||
"main": { "code": 382, "label": "ž" }
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
@@ -50,9 +48,9 @@
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".eu" },
|
||||
{ "code": -255, "label": ".rs" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,46 +1,100 @@
|
||||
{
|
||||
"type": "characters/extended_popups",
|
||||
"name": "tr",
|
||||
"authors": [ "kisekinopureya" ],
|
||||
"authors": [ "kisekinopureya", "patrickgold" ],
|
||||
"mapping": {
|
||||
"all": {
|
||||
"a": {
|
||||
"relevant": [
|
||||
{ "code": 226, "label": "â" }
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 225, "label": "á" }
|
||||
]
|
||||
},
|
||||
"c": {
|
||||
"main": { "code": 231, "label": "ç" },
|
||||
"relevant": [
|
||||
{ "code": 231, "label": "ç" }
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 263, "label": "ć" }
|
||||
]
|
||||
},
|
||||
"e": {
|
||||
"relevant": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 601, "label": "ə" },
|
||||
{ "code": 234, "label": "ê" }
|
||||
]
|
||||
},
|
||||
"g": {
|
||||
"relevant": [
|
||||
{ "code": 287, "label": "ğ" }
|
||||
]
|
||||
"main": { "code": 287, "label": "ğ" }
|
||||
},
|
||||
"i": {
|
||||
"main": { "code": 305, "label": "ı" },
|
||||
"relevant": [
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 305, "label": "ı" }
|
||||
{ "code": 239, "label": "ï" }
|
||||
]
|
||||
},
|
||||
"ı": {
|
||||
"main": { "code": 105, "label": "i" },
|
||||
"relevant": [
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 239, "label": "ï" }
|
||||
]
|
||||
},
|
||||
"n": {
|
||||
"relevant": [
|
||||
{ "code": 328, "label": "ň" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
]
|
||||
},
|
||||
"o": {
|
||||
"main": { "code": 246, "label": "ö" },
|
||||
"relevant": [
|
||||
{ "code": 246, "label": "ö" }
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 244, "label": "ô" }
|
||||
]
|
||||
},
|
||||
"s": {
|
||||
"main": { "code": 351, "label": "ş" },
|
||||
"relevant": [
|
||||
{ "code": 351, "label": "ş" }
|
||||
{ "code": 347, "label": "ś" },
|
||||
{ "code": 223, "label": "ß" },
|
||||
{ "code": 353, "label": "š" }
|
||||
]
|
||||
},
|
||||
"u": {
|
||||
"main": { "code": 252, "label": "ü" },
|
||||
"relevant": [
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 251, "label": "û" }
|
||||
]
|
||||
},
|
||||
"y": {
|
||||
"relevant": [
|
||||
{ "code": 253, "label": "ý" }
|
||||
]
|
||||
},
|
||||
"z": {
|
||||
"relevant": [
|
||||
{ "code": 382, "label": "ž" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
@@ -67,9 +121,9 @@
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".tr" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
46
app/src/main/assets/ime/text/characters/turkish_f.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "turkish_f",
|
||||
"authors": [ "patrickgold" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 102, "label": "f" },
|
||||
{ "code": 103, "label": "g" },
|
||||
{ "code": 287, "label": "ğ" },
|
||||
{ "code": 305, "label": "ı" },
|
||||
{ "code": 111, "label": "o" },
|
||||
{ "code": 100, "label": "d" },
|
||||
{ "code": 114, "label": "r" },
|
||||
{ "code": 110, "label": "n" },
|
||||
{ "code": 104, "label": "h" },
|
||||
{ "code": 112, "label": "p" },
|
||||
{ "code": 113, "label": "q" },
|
||||
{ "code": 119, "label": "w" }
|
||||
],
|
||||
[
|
||||
{ "code": 117, "label": "u" },
|
||||
{ "code": 105, "label": "i" },
|
||||
{ "code": 101, "label": "e" },
|
||||
{ "code": 97, "label": "a" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 116, "label": "t" },
|
||||
{ "code": 107, "label": "k" },
|
||||
{ "code": 109, "label": "m" },
|
||||
{ "code": 108, "label": "l" },
|
||||
{ "code": 121, "label": "y" },
|
||||
{ "code": 351, "label": "ş" }
|
||||
],
|
||||
[
|
||||
{ "code": 106, "label": "j" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 118, "label": "v" },
|
||||
{ "code": 99, "label": "c" },
|
||||
{ "code": 231, "label": "ç" },
|
||||
{ "code": 122, "label": "z" },
|
||||
{ "code": 115, "label": "s" },
|
||||
{ "code": 98, "label": "b" },
|
||||
{ "code": 120, "label": "x" }
|
||||
]
|
||||
]
|
||||
}
|
||||
46
app/src/main/assets/ime/text/characters/turkish_q.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "turkish_q",
|
||||
"authors": [ "patrickgold" ],
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 113, "label": "q" },
|
||||
{ "code": 119, "label": "w" },
|
||||
{ "code": 101, "label": "e" },
|
||||
{ "code": 114, "label": "r" },
|
||||
{ "code": 116, "label": "t" },
|
||||
{ "code": 121, "label": "y" },
|
||||
{ "code": 117, "label": "u" },
|
||||
{ "code": 305, "label": "ı" },
|
||||
{ "code": 111, "label": "o" },
|
||||
{ "code": 112, "label": "p" },
|
||||
{ "code": 287, "label": "ğ" },
|
||||
{ "code": 252, "label": "ü" }
|
||||
],
|
||||
[
|
||||
{ "code": 97, "label": "a" },
|
||||
{ "code": 115, "label": "s" },
|
||||
{ "code": 100, "label": "d" },
|
||||
{ "code": 102, "label": "f" },
|
||||
{ "code": 103, "label": "g" },
|
||||
{ "code": 104, "label": "h" },
|
||||
{ "code": 106, "label": "j" },
|
||||
{ "code": 107, "label": "k" },
|
||||
{ "code": 108, "label": "l" },
|
||||
{ "code": 351, "label": "ş" },
|
||||
{ "code": 105, "label": "i" }
|
||||
],
|
||||
[
|
||||
{ "code": 122, "label": "z" },
|
||||
{ "code": 120, "label": "x" },
|
||||
{ "code": 99, "label": "c" },
|
||||
{ "code": 118, "label": "v" },
|
||||
{ "code": 98, "label": "b" },
|
||||
{ "code": 110, "label": "n" },
|
||||
{ "code": 109, "label": "m" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 231, "label": "ç" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -10,7 +10,8 @@
|
||||
{ "code": -20, "label": "arrow_left", "type": "navigation" },
|
||||
{ "code": -21, "label": "arrow_right", "type": "navigation" },
|
||||
{ "code": -131, "label": "clipboard_cut", "type": "enter_editing" },
|
||||
{ "code": -132, "label": "clipboard_paste", "type": "enter_editing" }
|
||||
{ "code": -132, "label": "clipboard_paste", "type": "enter_editing" },
|
||||
{ "code": -214, "label": "switch_to_clipboard_context", "type": "system_gui"}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
64
app/src/main/assets/ime/theme/gboard_night.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
|
||||
"name": "gboard_night",
|
||||
"label": "Gboard Night",
|
||||
"authors": [ "Netscaping" ],
|
||||
"isNightTheme": true,
|
||||
"attributes": {
|
||||
"window": {
|
||||
"colorPrimary": "#5e97f6",
|
||||
"colorPrimaryDark": "#4285f4",
|
||||
"colorAccent": "#FF9800",
|
||||
"navigationBarColor": "@keyboard/background",
|
||||
"navigationBarLight": "false",
|
||||
"semiTransparentColor": "#20FFFFFF",
|
||||
"textColor": "#FFFFFF"
|
||||
},
|
||||
"keyboard": {
|
||||
"background": "#292e33"
|
||||
},
|
||||
"key": {
|
||||
"background": "#484c4f",
|
||||
"backgroundPressed": "#5e5e60",
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundPressed": "@window/textColor",
|
||||
"showBorder": "true"
|
||||
},
|
||||
"key:enter": {
|
||||
"background": "@window/colorPrimary",
|
||||
"backgroundPressed": "@window/colorPrimaryDark",
|
||||
"foreground": "#FFFFFF",
|
||||
"foregroundPressed": "#FFFFFF"
|
||||
},
|
||||
"key:shift:capslock": {
|
||||
"foreground": "@window/colorAccent",
|
||||
"foregroundPressed": "@window/colorAccent"
|
||||
},
|
||||
"media": {
|
||||
"foreground": "@window/textColor",
|
||||
"foregroundAlt": "#BDBDBD"
|
||||
},
|
||||
"oneHanded": {
|
||||
"background": "#373c41",
|
||||
"foreground": "#9b9da0"
|
||||
},
|
||||
"popup": {
|
||||
"background": "#373c41",
|
||||
"backgroundActive": "#5a5e60",
|
||||
"foreground": "@window/textColor"
|
||||
},
|
||||
"privateMode": {
|
||||
"background": "#A000FF",
|
||||
"foreground": "#FFFFFF"
|
||||
},
|
||||
"smartbar": {
|
||||
"background": "transparent",
|
||||
"foreground": "#d4d5d6",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"smartbarButton": {
|
||||
"background": "#FFFFFF",
|
||||
"foreground": "#686868"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/ic_app_icon_beta-playstore.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
app/src/main/ic_app_icon_debug-playstore.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
app/src/main/ic_app_icon_release-playstore.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,114 @@
|
||||
package dev.patrickgold.florisboard.ime.clip
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
|
||||
import dev.patrickgold.florisboard.ime.clip.provider.ItemType
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
|
||||
class ClipboardHistoryItemAdapter(
|
||||
private val dataSet: ArrayDeque<FlorisClipboardManager.TimedClipData>,
|
||||
private val pins: ArrayDeque<ClipboardItem>
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
class ClipboardHistoryTextViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val textView: TextView = view.findViewById(R.id.clipboard_history_item_text)
|
||||
}
|
||||
|
||||
class ClipboardHistoryImageViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val imgView: ImageView = view.findViewById(R.id.clipboard_history_item_img)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_SIZE: Int = 256
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position < pins.size) {
|
||||
// is a pin
|
||||
pins[position].type.value
|
||||
}else {
|
||||
// regular history item
|
||||
dataSet[position - pins.size].data.type.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
// Create a new view, which defines the UI of the list item
|
||||
val vh = when (viewType) {
|
||||
ItemType.IMAGE.value -> {
|
||||
val view = LayoutInflater.from(viewGroup.context)
|
||||
.inflate(R.layout.clipboard_history_item_image, viewGroup, false)
|
||||
|
||||
ClipboardHistoryImageViewHolder(view)
|
||||
}
|
||||
ItemType.TEXT.value -> {
|
||||
val view = LayoutInflater.from(viewGroup.context)
|
||||
.inflate(R.layout.clipboard_history_item_text, viewGroup, false)
|
||||
|
||||
ClipboardHistoryTextViewHolder(view)
|
||||
}
|
||||
else -> null
|
||||
}!!
|
||||
val clipboardInputManager = ClipboardInputManager.getInstance()
|
||||
(vh.itemView as ClipboardHistoryItemView).keyboardView = clipboardInputManager.getClipboardHistoryView()
|
||||
return vh
|
||||
}
|
||||
|
||||
// Replace the contents of a view (invoked by the layout manager)
|
||||
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (viewHolder) {
|
||||
is ClipboardHistoryTextViewHolder -> {
|
||||
var text = if (position < pins.size) {
|
||||
(viewHolder.itemView as ClipboardHistoryItemView).setPinned()
|
||||
pins[position].text
|
||||
}else {
|
||||
(viewHolder.itemView as ClipboardHistoryItemView).setUnpinned()
|
||||
dataSet[position - pins.size].data.text
|
||||
}
|
||||
if (text!!.length > MAX_SIZE) {
|
||||
text = text.subSequence(0 until MAX_SIZE).toString() + "..."
|
||||
}
|
||||
viewHolder.textView.text = text
|
||||
}
|
||||
|
||||
is ClipboardHistoryImageViewHolder -> {
|
||||
val uri = if (position < pins.size) {
|
||||
(viewHolder.itemView as ClipboardHistoryItemView).setPinned()
|
||||
pins[position].uri
|
||||
}else {
|
||||
(viewHolder.itemView as ClipboardHistoryItemView).setUnpinned()
|
||||
dataSet[position - pins.size].data.uri
|
||||
}
|
||||
|
||||
|
||||
viewHolder.imgView.clipToOutline = true
|
||||
viewHolder.imgView.visibility = GONE
|
||||
// For very large images, this can take a bit
|
||||
FlorisClipboardManager.getInstance().executor.execute {
|
||||
val resolver = FlorisBoard.getInstance().context.contentResolver
|
||||
val inputStream = resolver.openInputStream(uri!!)
|
||||
|
||||
val drawable = Drawable.createFromStream(inputStream, "clipboard URI")
|
||||
viewHolder.itemView.post {
|
||||
viewHolder.imgView.setImageDrawable(drawable)
|
||||
viewHolder.imgView.visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun getItemCount() = pins.size + dataSet.size
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package dev.patrickgold.florisboard.ime.clip
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
|
||||
class ClipboardHistoryItemView: ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
|
||||
lateinit var keyboardView: ClipboardHistoryView
|
||||
constructor(context: Context) : this(context, null as AttributeSet?)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
private var popupManager: ClipboardPopupManager? = null
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
popupManager = ClipboardPopupManager(keyboardView, FlorisBoard.getInstance().popupLayerView, this)
|
||||
|
||||
setOnClickListener{
|
||||
onClickItem()
|
||||
}
|
||||
|
||||
setOnLongClickListener{
|
||||
onLongClickItem()
|
||||
}
|
||||
|
||||
val themeManager = ThemeManager.default()
|
||||
themeManager.registerOnThemeUpdatedListener(this)
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
background.setTint(theme.getAttr(Theme.Attr.KEY_BACKGROUND).toSolidColor().color)
|
||||
val pin = findViewById<ImageView>(R.id.clipboard_pin).drawable
|
||||
pin?.setTint(theme.getAttr(Theme.Attr.KEY_FOREGROUND).toSolidColor().color)
|
||||
}
|
||||
|
||||
|
||||
private fun onLongClickItem() : Boolean {
|
||||
popupManager?.show(this)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onClickItem(){
|
||||
val position = ClipboardInputManager.getInstance().getPositionOfView(this)
|
||||
val instance = FlorisClipboardManager.getInstance()
|
||||
val canPaste = instance.canBePasted(instance.peekHistoryOrPin(position))
|
||||
if (canPaste) {
|
||||
instance.pasteItem(position)
|
||||
}else {
|
||||
Toast.makeText(context, context.getString(R.string.clip__cant_paste), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun setPinned() {
|
||||
val view = findViewById<TextView>(R.id.clipboard_history_item_text)
|
||||
view?.run {
|
||||
val params = layoutParams as LayoutParams
|
||||
params.marginEnd = resources.getDimensionPixelSize(R.dimen.clipboard_text_item_pin_margin)
|
||||
layoutParams = params
|
||||
}
|
||||
findViewById<ImageView>(R.id.clipboard_pin).visibility = VISIBLE
|
||||
invalidate()
|
||||
val themeManager = ThemeManager.default()
|
||||
onThemeUpdated(themeManager.activeTheme)
|
||||
}
|
||||
|
||||
fun setUnpinned(){
|
||||
val view = findViewById<TextView>(R.id.clipboard_history_item_text)
|
||||
// if text view, also update margin.
|
||||
view?.run {
|
||||
val params = layoutParams as LayoutParams
|
||||
params.marginEnd = 0
|
||||
layoutParams = params
|
||||
invalidate()
|
||||
}
|
||||
findViewById<ImageView>(R.id.clipboard_pin).visibility = INVISIBLE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package dev.patrickgold.florisboard.ime.clip
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
class ClipboardHistoryView : LinearLayout, FlorisBoard.EventListener,
|
||||
ThemeManager.OnThemeUpdatedListener {
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
|
||||
var backButton: ImageButton? = null
|
||||
private set
|
||||
|
||||
var clipText: TextView? = null
|
||||
private set
|
||||
|
||||
var clipboardBar: LinearLayout? = null
|
||||
private set
|
||||
|
||||
private var clipboardHistory: RecyclerView? = null
|
||||
|
||||
private var clearAll: ImageButton? = null
|
||||
|
||||
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?.addEventListener(this)
|
||||
themeManager.registerOnThemeUpdatedListener(this)
|
||||
backButton = findViewById(R.id.back_to_keyboard_button)
|
||||
clipText = findViewById(R.id.clipboard_text)
|
||||
clipboardBar = findViewById(R.id.clipboard_bar)
|
||||
clipboardHistory = findViewById(R.id.clipboard_history_items)
|
||||
clearAll = findViewById(R.id.clear_clipboard_history)
|
||||
|
||||
onApplyThemeAttributes()
|
||||
// lord alone knows why it doesn't work without this..
|
||||
onThemeUpdated(themeManager.activeTheme)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
florisboard?.removeEventListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
val fgColor = theme.getAttr(Theme.Attr.KEY_FOREGROUND).toSolidColor().color
|
||||
clipText?.setTextColor(fgColor)
|
||||
backButton?.drawable?.setTint(fgColor)
|
||||
clearAll?.setColorFilter(fgColor)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val height = florisboard?.inputView?.desiredMediaKeyboardViewHeight ?: 0.0f
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height.roundToInt(), MeasureSpec.EXACTLY))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package dev.patrickgold.florisboard.ime.clip
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
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.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.key.KeyData
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* Handles the clipboard view and allows for communication between UI and logic.
|
||||
*/
|
||||
class ClipboardInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
FlorisBoard.EventListener{
|
||||
|
||||
private val florisboard = FlorisBoard.getInstance()
|
||||
private var repeatedKeyPressHandler: Handler? = null
|
||||
private var recyclerView: RecyclerView? = null
|
||||
private var adapter: ClipboardHistoryItemAdapter? = null
|
||||
|
||||
companion object {
|
||||
private var instance: ClipboardInputManager? = null
|
||||
|
||||
@Synchronized
|
||||
fun getInstance(): ClipboardInputManager {
|
||||
if (instance == null) {
|
||||
instance = ClipboardInputManager()
|
||||
}
|
||||
return instance!!
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
florisboard.addEventListener(this)
|
||||
}
|
||||
|
||||
override fun onCreateInputView() {
|
||||
super.onCreateInputView()
|
||||
repeatedKeyPressHandler = Handler(florisboard.context.mainLooper)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new input view has been registered. Used to initialize all media-relevant
|
||||
* views and layouts.
|
||||
*/
|
||||
@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)
|
||||
|
||||
if (BuildConfig.DEBUG && adapter == null) {
|
||||
error("initClipboard() not called")
|
||||
}
|
||||
|
||||
recyclerView!!.adapter = adapter
|
||||
val manager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
|
||||
recyclerView!!.layoutManager = manager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean-up of resources and stopping all coroutines.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
|
||||
cancel()
|
||||
instance = null
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a reference to the [ClipboardHistoryView]
|
||||
*/
|
||||
fun getClipboardHistoryView() : ClipboardHistoryView{
|
||||
return FlorisBoard.getInstance().inputView?.mainViewFlipper?.getChildAt(2) as ClipboardHistoryView
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the adapter position of the view, i.e the position that the item is displayed at (including pins and
|
||||
* history items).
|
||||
*
|
||||
* @param view The ClipboardHistoryItemView whose position is to be determined.
|
||||
* @return The adapter position of the view
|
||||
*/
|
||||
fun getPositionOfView(view: View): Int {
|
||||
return recyclerView?.getChildLayoutPosition(view)!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify adapter that an item was inserted.
|
||||
*
|
||||
* @param position The position the item was inserted at
|
||||
*/
|
||||
fun notifyItemInserted(position: Int) = adapter?.notifyItemInserted(position)
|
||||
|
||||
/**
|
||||
* Notify adapter that an item was removed
|
||||
* @param position The position the item was removed from
|
||||
*/
|
||||
fun notifyItemRemoved(position: Int) = adapter?.notifyItemRemoved(position)
|
||||
|
||||
/**
|
||||
* Notify adapter that an item range was removed.
|
||||
* @param start The index the range starts at (inclusive)
|
||||
* @param numberOfItems The number of items removed
|
||||
*/
|
||||
fun notifyItemRangeRemoved(start: Int, numberOfItems: Int) = adapter?.notifyItemRangeRemoved(start, numberOfItems)
|
||||
|
||||
/**
|
||||
* Notify adapter that an item was moved
|
||||
* @param from The original position
|
||||
* @param to The final position
|
||||
*/
|
||||
fun notifyItemMoved(from: Int, to: Int) = adapter?.notifyItemMoved(from, to)
|
||||
|
||||
/**
|
||||
* Notify adapter that an item was changed.
|
||||
*
|
||||
* @param i The position of the item
|
||||
*/
|
||||
fun notifyItemChanged(i: Int) = adapter?.notifyItemChanged(i)
|
||||
|
||||
/**
|
||||
* Handles clicks on the back to keyboard button.
|
||||
*/
|
||||
private fun onButtonPressEvent(view: View, event: MotionEvent?): Boolean {
|
||||
|
||||
event ?: return false
|
||||
val data = when (view.id) {
|
||||
R.id.back_to_keyboard_button -> KeyData(code = KeyCode.SWITCH_TO_TEXT_CONTEXT)
|
||||
R.id.clear_clipboard_history -> KeyData(code = KeyCode.CLEAR_CLIPBOARD_HISTORY)
|
||||
else -> null
|
||||
}!!
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
florisboard.keyPressVibrate()
|
||||
florisboard.keyPressSound(data)
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.down(data))
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.up(data))
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
}
|
||||
|
||||
// MUST return false here so the background selector for showing a transparent bg works
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* [recyclerView] will be linked to [dataSet] and [pins] when initialized.
|
||||
*
|
||||
* @param dataSet the data set to link to
|
||||
* @param pins The pins to link to
|
||||
*/
|
||||
fun initClipboard(dataSet: ArrayDeque<FlorisClipboardManager.TimedClipData>, pins: ArrayDeque<ClipboardItem>) {
|
||||
this.adapter = ClipboardHistoryItemAdapter(dataSet = dataSet, pins= pins)
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays an animation of all items moving off the the clipboard from the top.
|
||||
*
|
||||
* @param start The index to start at (to ignore pins)
|
||||
* @param size The size of the clipboard
|
||||
* @return The time in millis till the last animation will complete.
|
||||
*/
|
||||
fun clearClipboardWithAnimation(start: Int, size: Int): Long {
|
||||
// list of views to animate
|
||||
val views = arrayListOf<View>()
|
||||
for(i in 0 until size){
|
||||
recyclerView?.findViewHolderForLayoutPosition(i + start)?.let {
|
||||
views.add(it.itemView)
|
||||
}
|
||||
}
|
||||
|
||||
// animate the views
|
||||
var delay = 1L
|
||||
for (view in views) {
|
||||
delay += (10 * delay.toDouble().pow(0.1)).toLong()
|
||||
val an = view.animate().translationX(1500f)
|
||||
an.startDelay = delay
|
||||
an.duration = 250
|
||||
}
|
||||
|
||||
// a little while later we reset the views so they can be reused.
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
for (view in views) {
|
||||
view.translationX = 0f
|
||||
}
|
||||
}, 450 + delay)
|
||||
|
||||
return 280 + delay
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package dev.patrickgold.florisboard.ime.clip
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Space
|
||||
import android.widget.TextView
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupLayerView
|
||||
import kotlin.math.max
|
||||
|
||||
class ClipboardPopupManager(private val keyboardView: ClipboardHistoryView,
|
||||
private val popupLayerView: PopupLayerView?,
|
||||
private val clipboardHistoryItem: ClipboardHistoryItemView) {
|
||||
|
||||
private val popupView: ClipboardPopupView = LayoutInflater.from(keyboardView.context).inflate(R.layout.clip_popup_layout, null) as ClipboardPopupView
|
||||
private var width = 0
|
||||
private var height = 0
|
||||
private var xOffset = 0
|
||||
private var yOffset = 0
|
||||
|
||||
|
||||
init {
|
||||
popupLayerView?.addView(popupView)
|
||||
}
|
||||
|
||||
|
||||
private fun pinButtonListener() {
|
||||
val pos = ClipboardInputManager.getInstance().getPositionOfView(clipboardHistoryItem)
|
||||
val pinned = FlorisClipboardManager.getInstance().isPinned(pos)
|
||||
if (pinned) {
|
||||
FlorisClipboardManager.getInstance().unpinClip(pos)
|
||||
hide()
|
||||
} else {
|
||||
FlorisClipboardManager.getInstance().pinClip(pos)
|
||||
hide()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a popup.
|
||||
*/
|
||||
fun show(view: ClipboardHistoryItemView) {
|
||||
val pinButton = popupView.findViewById<LinearLayout>(R.id.pin_clip_item)
|
||||
pinButton.setOnClickListener {
|
||||
pinButtonListener()
|
||||
}
|
||||
|
||||
val pos = ClipboardInputManager.getInstance().getPositionOfView(clipboardHistoryItem)
|
||||
val pinned = FlorisClipboardManager.getInstance().isPinned(pos)
|
||||
|
||||
if (pinned) {
|
||||
pinButton.findViewById<TextView>(R.id.pin_clip_item_text).text = view.context.getString(R.string.clip__unpin_item)
|
||||
}
|
||||
|
||||
val delete = popupView.findViewById<LinearLayout>(R.id.remove_from_history)
|
||||
delete.setOnClickListener {
|
||||
FlorisClipboardManager.getInstance().removeClip(pos)
|
||||
hide()
|
||||
}
|
||||
|
||||
val clipboardManager = FlorisClipboardManager.getInstance()
|
||||
val clipItem = clipboardManager.peekHistoryOrPin(pos)
|
||||
val pasteShouldBeEnabled = FlorisClipboardManager.getInstance().canBePasted(clipItem)
|
||||
// the clipboard item has any of the supported mime types of the editor OR is plain text.
|
||||
|
||||
val paste = popupView.findViewById<LinearLayout>(R.id.paste_clip_item)
|
||||
if (pasteShouldBeEnabled) {
|
||||
paste.setOnClickListener {
|
||||
FlorisClipboardManager.getInstance().pasteItem(pos)
|
||||
hide()
|
||||
}
|
||||
popupView.findViewById<Space>(R.id.paste_clip_item_space).visibility = VISIBLE
|
||||
paste.visibility = VISIBLE
|
||||
}else {
|
||||
popupView.findViewById<Space>(R.id.paste_clip_item_space).visibility = GONE
|
||||
paste.visibility = GONE
|
||||
}
|
||||
|
||||
FlorisBoard.getInstance().isClipboardContextMenuShown = true
|
||||
popupLayerView?.clipboardPopupManager = this
|
||||
popupLayerView?.intercept = popupView
|
||||
calc(view)
|
||||
|
||||
popupView.properties.let {
|
||||
it.width = this.width
|
||||
it.height = this.height
|
||||
it.xOffset = this.xOffset
|
||||
it.yOffset = this.yOffset
|
||||
}
|
||||
popupView.show(keyboardView)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate sizes of popup.
|
||||
*/
|
||||
private fun calc(view: ClipboardHistoryItemView) {
|
||||
val widthMeasureSpec: Int = View.MeasureSpec.makeMeasureSpec(view.width, View.MeasureSpec.AT_MOST)
|
||||
val heightMeasureSpec: Int = View.MeasureSpec.makeMeasureSpec(100000, View.MeasureSpec.AT_MOST)
|
||||
popupView.invalidate()
|
||||
popupView.measure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
width = view.width * 4 / 5
|
||||
height = popupView.measuredHeight
|
||||
xOffset = view.x.toInt() + (view.width - width) / 2
|
||||
// y offset is either where the top of the item is OR if the top is off screen, the top of the keyboard.
|
||||
yOffset = max(view.y.toInt() - keyboardView.height - height / 2 - 20, keyboardView.y.toInt() - keyboardView.height - height / 2 - 20)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides a popup.
|
||||
*/
|
||||
fun hide() {
|
||||
popupView.hide()
|
||||
popupLayerView?.intercept = null
|
||||
popupLayerView?.clipboardPopupManager = null
|
||||
FlorisBoard.getInstance().isClipboardContextMenuShown = false
|
||||
|
||||
popupView.apply {
|
||||
visibility = GONE
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package dev.patrickgold.florisboard.ime.clip
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.PaintDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
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
|
||||
|
||||
class ClipboardPopupView: LinearLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
|
||||
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
|
||||
}
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
|
||||
val properties: Properties = Properties(
|
||||
width = 0,
|
||||
height = 0,
|
||||
xOffset = 0,
|
||||
yOffset = 0
|
||||
)
|
||||
private val isShowing: Boolean
|
||||
get() = visibility == VISIBLE
|
||||
|
||||
init {
|
||||
visibility = GONE
|
||||
background = backgroundDrawable
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
themeManager.registerOnThemeUpdatedListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
backgroundDrawable.apply {
|
||||
setTint(theme.getAttr(Theme.Attr.POPUP_BACKGROUND).toSolidColor().color)
|
||||
}
|
||||
|
||||
this.findViewById<ImageView>(R.id.pin_clip_item_icon).drawable.apply {
|
||||
setTint(theme.getAttr(Theme.Attr.WINDOW_TEXT_COLOR).toSolidColor().color)
|
||||
}
|
||||
|
||||
|
||||
this.findViewById<ImageView>(R.id.remove_from_history_icon).drawable.apply {
|
||||
setTint(theme.getAttr(Theme.Attr.WINDOW_TEXT_COLOR).toSolidColor().color)
|
||||
}
|
||||
|
||||
this.findViewById<ImageView>(R.id.paste_clip_item_icon).drawable.apply {
|
||||
setTint(theme.getAttr(Theme.Attr.WINDOW_TEXT_COLOR).toSolidColor().color)
|
||||
}
|
||||
|
||||
if (isShowing) {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyProperties(anchor: View) {
|
||||
val anchorCoords = IntArray(2)
|
||||
anchor.getLocationInWindow(anchorCoords)
|
||||
val anchorX = anchorCoords[0]
|
||||
val anchorY = anchorCoords[1] + anchor.measuredHeight
|
||||
when (val lp = layoutParams) {
|
||||
is FrameLayout.LayoutParams -> lp.apply {
|
||||
width = properties.width
|
||||
height = properties.height
|
||||
setMargins(
|
||||
anchorX + properties.xOffset,
|
||||
anchorY + properties.yOffset,
|
||||
0,
|
||||
0
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
layoutParams = FrameLayout.LayoutParams(properties.width, properties.height).apply {
|
||||
setMargins(
|
||||
anchorX + properties.xOffset,
|
||||
anchorY + properties.yOffset,
|
||||
0,
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isShowing) {
|
||||
requestLayout()
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun show(anchor: View) {
|
||||
applyProperties(anchor)
|
||||
visibility = VISIBLE
|
||||
requestLayout()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
visibility = GONE
|
||||
requestLayout()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
data class Properties(
|
||||
var width: Int,
|
||||
var height: Int,
|
||||
var xOffset: Int,
|
||||
var yOffset: Int
|
||||
)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
package dev.patrickgold.florisboard.ime.clip
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
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.util.cancelAll
|
||||
import dev.patrickgold.florisboard.util.postAtScheduledRate
|
||||
import timber.log.Timber
|
||||
import java.io.Closeable
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutorService
|
||||
import kotlin.collections.ArrayDeque
|
||||
|
||||
/**
|
||||
* [FlorisClipboardManager] manages the clipboard and clipboard history.
|
||||
*
|
||||
* Also just going to document how all the classes here work.
|
||||
*
|
||||
* [FlorisClipboardManager] handles storage and retrieval of clipboard items. All manipulation of the
|
||||
* clipboard goes through here.
|
||||
*
|
||||
* [ClipboardInputManager] handles the input view and allows for communication between UI and logic
|
||||
*
|
||||
* [ClipboardHistoryView] is the view representing the clipboard context. Only does some theme stuff.
|
||||
*
|
||||
* [ClipboardHistoryItemView] is the view representing an item in the clipboard history (either image or text). Only
|
||||
* does UI stuff.
|
||||
*
|
||||
* [ClipboardHistoryItemAdapter] is the recyclerview adapter that backs the clipboard history.
|
||||
*
|
||||
* [ClipboardPopupManager] handles the popups for each [ClipboardHistoryItemView] (each item has its own popup manager)
|
||||
*
|
||||
* [ClipboardPopupView] is the view representing a popup displayed when long pressing on a clipboard history item.
|
||||
*/
|
||||
class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryClipChangedListener, Closeable {
|
||||
|
||||
private lateinit var pinsDao: PinnedClipboardItemDao
|
||||
lateinit var executor: ExecutorService
|
||||
|
||||
// Using ArrayDeque because it's "technically" the correct data structure (I think).
|
||||
// Newest stored first, oldest stored last.
|
||||
private var history: ArrayDeque<TimedClipData> = ArrayDeque()
|
||||
private var pins: ArrayDeque<ClipboardItem> = ArrayDeque()
|
||||
private var current: ClipboardItem? = null
|
||||
private var onPrimaryClipChangedListeners: ArrayList<OnPrimaryClipChangedListener> = arrayListOf()
|
||||
private lateinit var systemClipboardManager: ClipboardManager
|
||||
private lateinit var handler: Handler
|
||||
private lateinit var prefHelper: PrefHelper
|
||||
|
||||
data class TimedClipData(val data: ClipboardItem, val timeUTC: Long)
|
||||
|
||||
interface OnPrimaryClipChangedListener {
|
||||
fun onPrimaryClipChanged()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var instance: FlorisClipboardManager? = null
|
||||
|
||||
// 1 minute
|
||||
private const val INTERVAL = 60 * 1000L
|
||||
|
||||
@Synchronized
|
||||
fun getInstance(): FlorisClipboardManager {
|
||||
if (instance == null) {
|
||||
instance = FlorisClipboardManager()
|
||||
}
|
||||
return instance!!
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getInstanceOrNull(): FlorisClipboardManager? = instance
|
||||
|
||||
/**
|
||||
* Taken from ClipboardDescription.java from the AOSP
|
||||
*
|
||||
* Helper to compare two MIME types, where one may be a pattern.
|
||||
* @param concreteType A fully-specified MIME type.
|
||||
* @param desiredType A desired MIME type that may be a pattern such as * / *.
|
||||
* @return Returns true if the two MIME types match.
|
||||
*/
|
||||
fun compareMimeTypes(concreteType: String, desiredType: String): Boolean {
|
||||
val typeLength = desiredType.length
|
||||
if (typeLength == 3 && desiredType == "*/*") {
|
||||
return true
|
||||
}
|
||||
val slashpos = desiredType.indexOf('/')
|
||||
if (slashpos > 0) {
|
||||
if (typeLength == slashpos + 2 && desiredType[slashpos + 1] == '*') {
|
||||
if (desiredType.regionMatches(0, concreteType, 0, slashpos + 1)) {
|
||||
return true
|
||||
}
|
||||
} else if (desiredType == concreteType) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds a new item to the clipboard history (if enabled).
|
||||
*/
|
||||
fun updateHistory(newData: ClipboardItem) {
|
||||
val clipboardPrefs = prefHelper.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 timed = TimedClipData(newData, System.currentTimeMillis())
|
||||
history.addFirst(timed)
|
||||
ClipboardInputManager.getInstance().notifyItemInserted(pins.size)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Used so that [onPrimaryClipChanged] knows whether it was called by [changeCurrent] (and hence shouldn't update
|
||||
* history)
|
||||
*/
|
||||
private var shouldUpdateHistory = true
|
||||
|
||||
/**
|
||||
* Changes current clipboard item. WITHOUT updating the history.
|
||||
*/
|
||||
fun changeCurrent(newData: ClipboardItem, closePrevious: Boolean) {
|
||||
if (prefHelper.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)
|
||||
systemClipboardManager.setPrimaryClip(newData.toClipData())
|
||||
} else {
|
||||
shouldUpdateHistory = false
|
||||
systemClipboardManager.setPrimaryClip(newData.toClipData())
|
||||
}
|
||||
onPrimaryClipChangedListeners.forEach { it.onPrimaryClipChanged() }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Change the current text on clipboard, update history (if enabled).
|
||||
*
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps some plaintext in a ClipData and calls [addNewClip]
|
||||
*/
|
||||
fun addNewPlaintext(newText: String) {
|
||||
val newData = ClipboardItem(null, ItemType.TEXT, null, newText, ClipboardItem.TEXT_PLAIN)
|
||||
addNewClip(newData)
|
||||
}
|
||||
|
||||
val primaryClip: ClipboardItem?
|
||||
get() = if (prefHelper.clipboard.enableInternal) {
|
||||
current
|
||||
} else {
|
||||
systemClipboardManager.primaryClip?.let { ClipboardItem.fromClipData(it, false) }
|
||||
}
|
||||
|
||||
fun peekHistory(index: Int): ClipboardItem? {
|
||||
return history.getOrNull(index)?.data
|
||||
}
|
||||
|
||||
fun addPrimaryClipChangedListener(listener: OnPrimaryClipChangedListener) {
|
||||
onPrimaryClipChangedListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removePrimaryClipChangedListener(listener: OnPrimaryClipChangedListener) {
|
||||
onPrimaryClipChangedListeners.remove(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by system clipboard when the contents are changed
|
||||
*/
|
||||
override fun onPrimaryClipChanged() {
|
||||
// Run on async thread to avoid blocking.
|
||||
if (systemClipboardManager.primaryClip?.getItemAt(0)?.text == null &&
|
||||
systemClipboardManager.primaryClip?.getItemAt(0)?.uri == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val isEqual = when (primaryClip?.type) {
|
||||
ItemType.TEXT -> primaryClip?.text == systemClipboardManager.primaryClip?.getItemAt(0)?.text
|
||||
ItemType.IMAGE -> primaryClip?.uri == systemClipboardManager.primaryClip?.getItemAt(0)?.uri
|
||||
null -> false
|
||||
}
|
||||
systemClipboardManager.primaryClip?.let {
|
||||
if (prefHelper.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) {
|
||||
addNewClip(ClipboardItem.fromClipData(it, true))
|
||||
}
|
||||
} else if (prefHelper.clipboard.enableHistory) {
|
||||
// in the event history is enabled, and it should be updated it is updated
|
||||
if (shouldUpdateHistory) {
|
||||
updateHistory(ClipboardItem.fromClipData(it, false))
|
||||
} else {
|
||||
shouldUpdateHistory = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun hasPrimaryClip(): Boolean {
|
||||
return this.primaryClip != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up.
|
||||
*
|
||||
* Sets [instance] to null for GC. Unregisters the system clipboard listener, cancels clipboard clean ups.
|
||||
*/
|
||||
override fun close() {
|
||||
systemClipboardManager.removePrimaryClipChangedListener(this)
|
||||
handler.cancelAll()
|
||||
instance = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the floris clipboard manager. Exists to avoid dependency loop due to reference
|
||||
* to [FlorisBoard.context]
|
||||
*
|
||||
* Sets up the clipboard cleanup task, links the recycler view in clipInputManager to [history].
|
||||
*
|
||||
* @param context Required to register as an onPrimaryClipChangedListener of ClipboardManager
|
||||
*/
|
||||
fun initialize(context: Context) {
|
||||
|
||||
this.systemClipboardManager = (context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager)
|
||||
systemClipboardManager.addPrimaryClipChangedListener(this)
|
||||
|
||||
prefHelper = PrefHelper.getDefaultInstance(context)
|
||||
|
||||
val cleanUpClipboard = Runnable {
|
||||
|
||||
if (!prefHelper.clipboard.cleanUpOld) {
|
||||
return@Runnable
|
||||
}
|
||||
|
||||
val currentTime = System.currentTimeMillis()
|
||||
var numToPop = 0
|
||||
val expiryTime = prefHelper.clipboard.cleanUpAfter * 60 * 1000
|
||||
for (item in history.asReversed()) {
|
||||
if (item.timeUTC + expiryTime < currentTime) {
|
||||
numToPop += 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
for (i in 0 until numToPop) {
|
||||
history.removeLast().data.close()
|
||||
}
|
||||
ClipboardInputManager.getInstance().notifyItemRangeRemoved(pins.size + history.size, numToPop)
|
||||
}
|
||||
FlorisBoard.getInstance().clipInputManager.initClipboard(this.history, this.pins)
|
||||
handler = Handler(Looper.getMainLooper())
|
||||
prefHelper
|
||||
handler.postAtScheduledRate(0, INTERVAL, cleanUpClipboard)
|
||||
executor = FlorisBoard.getInstance().asyncExecutor
|
||||
executor.execute {
|
||||
pinsDao = PinnedItemsDatabase.getInstance().clipboardItemDao()
|
||||
pinsDao.getAll().toCollection(this.pins)
|
||||
FlorisContentProvider.getInstance().initIfNotAlready()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clears the history with an animation.
|
||||
*/
|
||||
fun clearHistoryWithAnimation() {
|
||||
val clipInputManager = FlorisBoard.getInstance().clipInputManager
|
||||
val delay = clipInputManager.clearClipboardWithAnimation(pins.size, history.size)
|
||||
|
||||
handler.postDelayed({
|
||||
val size = history.size
|
||||
for (item in history) {
|
||||
item.data.close()
|
||||
}
|
||||
history.clear()
|
||||
clipInputManager.notifyItemRangeRemoved(pins.size, size)
|
||||
}, delay)
|
||||
}
|
||||
|
||||
fun pinClip(adapterPos: Int) {
|
||||
val clipInputManager = FlorisBoard.getInstance().clipInputManager
|
||||
val pin = history.removeAt(adapterPos - pins.size)
|
||||
pins.addFirst(pin.data)
|
||||
clipInputManager.notifyItemMoved(adapterPos, 0)
|
||||
clipInputManager.notifyItemChanged(0)
|
||||
|
||||
executor.execute {
|
||||
val uid = pinsDao.insert(pin.data)
|
||||
pin.data.uid = uid
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item at a particular [adapterPos] (i.e the position the item is displayed at.)
|
||||
*/
|
||||
fun peekHistoryOrPin(adapterPos: Int): ClipboardItem {
|
||||
return when {
|
||||
adapterPos < pins.size -> pins[adapterPos]
|
||||
else -> history[adapterPos - pins.size].data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun isPinned(position: Int): Boolean {
|
||||
return when {
|
||||
position < pins.size -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun unpinClip(adapterPos: Int) {
|
||||
val clipInputManager = FlorisBoard.getInstance().clipInputManager
|
||||
val item = pins.removeAt(adapterPos)
|
||||
|
||||
val clipboardPrefs = prefHelper.clipboard
|
||||
if (clipboardPrefs.limitHistorySize) {
|
||||
var numRemoved = 0
|
||||
while (history.size >= clipboardPrefs.maxHistorySize) {
|
||||
numRemoved += 1
|
||||
history.removeLast().data.close()
|
||||
}
|
||||
ClipboardInputManager.getInstance().notifyItemRangeRemoved(history.size, numRemoved)
|
||||
}
|
||||
|
||||
val timed = TimedClipData(item, System.currentTimeMillis())
|
||||
history.addFirst(timed)
|
||||
|
||||
clipInputManager.notifyItemMoved(adapterPos, pins.size)
|
||||
clipInputManager.notifyItemChanged(pins.size)
|
||||
|
||||
executor.execute {
|
||||
pinsDao.delete(item)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeClip(pos: Int) {
|
||||
when {
|
||||
pos < pins.size -> {
|
||||
val item = pins.removeAt(pos)
|
||||
executor.execute {
|
||||
Timber.d("removing pin")
|
||||
pinsDao.delete(item)
|
||||
}
|
||||
item.close()
|
||||
}
|
||||
else -> {
|
||||
history.removeAt(pos - pins.size).data.close()
|
||||
}
|
||||
}
|
||||
val clipboardInputManager = ClipboardInputManager.getInstance()
|
||||
clipboardInputManager.notifyItemRemoved(pos)
|
||||
}
|
||||
|
||||
|
||||
fun pasteItem(pos: Int) {
|
||||
val item = peekHistoryOrPin(pos)
|
||||
FlorisBoard.getInstance().activeEditorInstance.commitClipboardItem(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the editor can accept the clip item, else false.
|
||||
*/
|
||||
fun canBePasted(clipItem: ClipboardItem?): Boolean {
|
||||
if (clipItem == null) return false
|
||||
|
||||
return clipItem.mimeTypes.contains("text/plain") || FlorisBoard.getInstance().activeEditorInstance.contentMimeTypes?.any { editorType ->
|
||||
clipItem.mimeTypes.any { clipType ->
|
||||
if (editorType != null) {
|
||||
compareMimeTypes(clipType, editorType)
|
||||
}else { false }
|
||||
}
|
||||
} == true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package dev.patrickgold.florisboard.ime.clip.provider
|
||||
|
||||
import android.net.Uri
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Backend class which is used by [FlorisContentProvider] to serve content.
|
||||
*/
|
||||
class FileStorage private constructor() {
|
||||
|
||||
|
||||
companion object {
|
||||
private const val BUF_SIZE = 1024 * 8
|
||||
private var instance: FileStorage? = null
|
||||
private var offset = 0
|
||||
|
||||
|
||||
fun getInstance() : FileStorage {
|
||||
if (this.instance == null){
|
||||
this.instance = FileStorage()
|
||||
}
|
||||
return instance!!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clones a content URI to internal storage.
|
||||
* @param uri The URI
|
||||
* @return the file's name which is a unique long
|
||||
*/
|
||||
@Synchronized
|
||||
fun cloneURI(uri: Uri) : Long {
|
||||
val context = FlorisBoard.getInstance().context
|
||||
// nanoTime + the number of items created so that it's unique.
|
||||
val name = (System.nanoTime() + offset)
|
||||
|
||||
// Just a normal copy from input stream to output stream.
|
||||
val source = context.contentResolver.openInputStream(uri)!!
|
||||
val sink = File(context.filesDir, name.toString()).outputStream()
|
||||
var nread = 0L
|
||||
val buf = ByteArray(BUF_SIZE)
|
||||
var n: Int
|
||||
while (source.read(buf).also { n = it } > 0) {
|
||||
sink.write(buf, 0, n)
|
||||
nread += n.toLong()
|
||||
}
|
||||
|
||||
source.close()
|
||||
sink.close()
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the file corresponding to an id.
|
||||
*/
|
||||
fun deleteById(id: Long) {
|
||||
Timber.d("Cleaning up $id")
|
||||
val file = File(FlorisBoard.getInstance().filesDir, id.toString())
|
||||
file.delete()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file address of an id.
|
||||
*/
|
||||
fun getAddress(id: Long): String {
|
||||
return FlorisBoard.getInstance().filesDir.toString() + "/$id"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package dev.patrickgold.florisboard.ime.clip.provider
|
||||
|
||||
import android.content.*
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.room.Room
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.ExecutorService
|
||||
|
||||
/**
|
||||
* Allows apps to access images on the clipboard.
|
||||
*
|
||||
* This is sometimes called by the UI thread, so all functions are non blocking.
|
||||
* Database accesses are performed async.
|
||||
*/
|
||||
class FlorisContentProvider : ContentProvider() {
|
||||
private lateinit var fileUriDao: FileUriDao
|
||||
private val mimeTypes: HashMap<Long, Array<String>> = hashMapOf()
|
||||
private lateinit var executor: ExecutorService
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
instance = this
|
||||
return true
|
||||
}
|
||||
|
||||
fun initIfNotAlready(){
|
||||
if (this::fileUriDao.isInitialized){
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
fileUriDao = Room.databaseBuilder(
|
||||
context!!,
|
||||
FileUriDatabase::class.java, "fileuridb"
|
||||
).build().fileUriDao()
|
||||
|
||||
executor = FlorisBoard.getInstance().asyncExecutor
|
||||
for (fileUri in fileUriDao.getAll()) {
|
||||
mimeTypes[fileUri.fileName] = fileUri.mimeTypes
|
||||
}
|
||||
}
|
||||
|
||||
override fun query(
|
||||
uri: Uri,
|
||||
projection: Array<out String>?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<out String>?,
|
||||
sortOrder: String?
|
||||
): Cursor? {
|
||||
// just return nothing, nothing should call this function at all.
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getType(uri: Uri): String {
|
||||
return when (matcher.match(uri)) {
|
||||
CLIP_ITEM -> mimeTypes.getOrElse(ContentUris.parseId(uri), { throw IllegalArgumentException("Don't have this item!") })[0]
|
||||
CLIPS_TABLE -> "vnd.android.cursor.dir/$AUTHORITY.clip"
|
||||
else -> throw IllegalArgumentException("Don't know what this is $uri")
|
||||
}
|
||||
}
|
||||
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
|
||||
val id = ContentUris.parseId(uri)
|
||||
val path = File(FileStorage.getInstance().getAddress(id))
|
||||
|
||||
// Nothing has permission to write anyway.
|
||||
return ParcelFileDescriptor.open(path, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
}
|
||||
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri {
|
||||
when (matcher.match(uri)){
|
||||
CLIPS_TABLE -> {
|
||||
val id = FileStorage.getInstance().cloneURI(Uri.parse(values?.getAsString("uri")))
|
||||
val mimes = values?.getAsString("mimetypes")?.split(",")?.toTypedArray()
|
||||
mimes?.let {
|
||||
mimeTypes[id] = mimes
|
||||
executor.execute {
|
||||
Timber.d("Inserted file uri $id")
|
||||
fileUriDao.insert(FileUri(id, mimes))
|
||||
}
|
||||
}
|
||||
|
||||
return ContentUris.withAppendedId(CLIPS_URI, id)
|
||||
}
|
||||
else -> throw IllegalArgumentException("Don't know what this is $uri")
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
|
||||
when (matcher.match(uri)){
|
||||
CLIP_ITEM -> {
|
||||
val id = ContentUris.parseId(uri)
|
||||
FileStorage.getInstance().deleteById(id)
|
||||
mimeTypes.remove(id)
|
||||
context?.revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
executor.execute {
|
||||
fileUriDao.delete(id)
|
||||
}
|
||||
return 1
|
||||
}
|
||||
else -> throw IllegalArgumentException("Don't know what this is $uri")
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
|
||||
throw IllegalArgumentException("This ContentProvider does not support update.")
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private var instance: FlorisContentProvider? = null
|
||||
const val AUTHORITY = "dev.patrickgold.florisboard.provider.clip"
|
||||
val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY")
|
||||
val CLIPS_URI: Uri = Uri.parse("content://$AUTHORITY/clips")
|
||||
|
||||
fun getInstance(): FlorisContentProvider {
|
||||
return instance!!
|
||||
}
|
||||
|
||||
private const val CLIPS_TABLE = 1
|
||||
private const val CLIP_ITEM = 0
|
||||
|
||||
val matcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
|
||||
addURI(AUTHORITY, "clips/#", CLIP_ITEM)
|
||||
addURI(AUTHORITY, "clips", CLIPS_TABLE)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package dev.patrickgold.florisboard.ime.clip.provider
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ContentValues
|
||||
import android.net.Uri
|
||||
import android.provider.BaseColumns
|
||||
import androidx.room.*
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import java.io.Closeable
|
||||
|
||||
|
||||
enum class ItemType(val value: Int) {
|
||||
TEXT(1),
|
||||
IMAGE(2);
|
||||
|
||||
companion object {
|
||||
fun fromInt(value : Int) : ItemType {
|
||||
return values().first { it.value == value }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents an item on the clipboard.
|
||||
* The URI stored belongs to FlorisContentProvider, not whatever app copied the image
|
||||
*
|
||||
* If type == ItemType.IMAGE there must be a uri set
|
||||
* if type == ItemType.TEXT there must be a text set
|
||||
*/
|
||||
@Entity(tableName = "pins")
|
||||
data class ClipboardItem(
|
||||
/** Only used for pins */
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name=BaseColumns._ID, index=true) var uid: Long?,
|
||||
val type: ItemType,
|
||||
val uri: Uri?,
|
||||
val text: String?,
|
||||
val mimeTypes: Array<String>) : Closeable{
|
||||
|
||||
/**
|
||||
* Creates a new ClipData which has the same contents as this.
|
||||
*/
|
||||
fun toClipData(): ClipData {
|
||||
return when (type) {
|
||||
ItemType.IMAGE -> {
|
||||
ClipData.newUri(FlorisBoard.getInstance().context.contentResolver, "Clipboard data", uri)
|
||||
}
|
||||
ItemType.TEXT -> {
|
||||
ClipData.newPlainText("Clipboard data", text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instructs the content provider to delete this URI. If not an image, is a noop
|
||||
*/
|
||||
override fun close() {
|
||||
if (type == ItemType.IMAGE) {
|
||||
FlorisBoard.getInstance().context.contentResolver.delete(this.uri!!, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ClipboardItem
|
||||
|
||||
if (uid != other.uid) return false
|
||||
if (type != other.type) return false
|
||||
if (uri != other.uri) return false
|
||||
if (text != other.text) return false
|
||||
if (!mimeTypes.contentEquals(other.mimeTypes)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = uid.hashCode()
|
||||
result = 31 * result + type.hashCode()
|
||||
result = 31 * result + (uri?.hashCode() ?: 0)
|
||||
result = 31 * result + (text?.hashCode() ?: 0)
|
||||
result = 31 * result + mimeTypes.contentHashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
fun stringRepresentation(): String {
|
||||
return when {
|
||||
uri != null -> "(Image) $uri"
|
||||
text != null -> text
|
||||
else -> "#ERROR"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* So that every item doesn't have to allocate its own array.
|
||||
*/
|
||||
val TEXT_PLAIN = arrayOf("text/plain")
|
||||
|
||||
/**
|
||||
* Returns a new ClipboardItem based on a ClipData
|
||||
*
|
||||
* @param data The ClipData to clone.
|
||||
* @param cloneUri Whether to store the image using [FlorisContentProvider].
|
||||
*/
|
||||
fun fromClipData(data: ClipData, cloneUri: Boolean) : ClipboardItem {
|
||||
|
||||
val type = when {
|
||||
data.getItemAt(0)?.uri != null -> ItemType.IMAGE
|
||||
data.getItemAt(0)?.text != null -> ItemType.TEXT
|
||||
else -> null
|
||||
}!!
|
||||
|
||||
val uri = if (type == ItemType.IMAGE) {
|
||||
if (data.getItemAt(0).uri.authority == FlorisContentProvider.CONTENT_URI.authority || !cloneUri){
|
||||
data.getItemAt(0).uri
|
||||
}else {
|
||||
val values = ContentValues().apply{
|
||||
put("uri", data.getItemAt(0).uri.toString())
|
||||
put("mimetypes", data.description.filterMimeTypes("*/*").joinToString(","))
|
||||
}
|
||||
FlorisBoard.getInstance().context.contentResolver.insert(FlorisContentProvider.CLIPS_URI, values)
|
||||
}
|
||||
} else { null }
|
||||
|
||||
val text = data.getItemAt(0).text?.toString()
|
||||
val mimeTypes = when (type) {
|
||||
ItemType.IMAGE -> {
|
||||
(0 until data.description.mimeTypeCount).map {
|
||||
data.description.getMimeType(it)
|
||||
}.toTypedArray()
|
||||
}
|
||||
ItemType.TEXT -> { TEXT_PLAIN }
|
||||
}
|
||||
|
||||
return ClipboardItem(null, type, uri, text, mimeTypes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Converters {
|
||||
@TypeConverter
|
||||
fun uriFromString(value: String?): Uri? {
|
||||
return Uri.parse(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun stringFromUri(value: Uri?): String {
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun itemTypeToInt(value: ItemType?): Int? {
|
||||
return value?.value
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun intToItemType(value: Int?): ItemType? {
|
||||
return value?.let { ItemType.fromInt(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Only works because the string array is a mimetype.
|
||||
* DOES NOT USE A GENERALIZED FORMAT.
|
||||
*/
|
||||
@TypeConverter
|
||||
fun mimeTypesToString(mimeTypes: Array<String>): String {
|
||||
return mimeTypes.joinToString(",")
|
||||
}
|
||||
@TypeConverter
|
||||
fun stringToMimeTypes(value: String): Array<String> {
|
||||
return value.split(",").toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Dao
|
||||
interface PinnedClipboardItemDao {
|
||||
@Query("SELECT * FROM pins")
|
||||
fun getAll(): List<ClipboardItem>
|
||||
|
||||
@Insert
|
||||
fun insert(item: ClipboardItem) : Long
|
||||
|
||||
@Delete
|
||||
fun delete(item: ClipboardItem)
|
||||
}
|
||||
|
||||
@Database(entities = [ClipboardItem::class], version = 1)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class PinnedItemsDatabase : RoomDatabase() {
|
||||
abstract fun clipboardItemDao() : PinnedClipboardItemDao
|
||||
|
||||
companion object {
|
||||
private var instance: PinnedItemsDatabase? = null
|
||||
|
||||
fun getInstance(): PinnedItemsDatabase {
|
||||
|
||||
if (instance == null) {
|
||||
instance = Room.databaseBuilder(
|
||||
FlorisBoard.getInstance().context,
|
||||
PinnedItemsDatabase::class.java,
|
||||
"pins").build()
|
||||
}
|
||||
|
||||
return instance!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(tableName = "file_uris")
|
||||
data class FileUri(
|
||||
@PrimaryKey @ColumnInfo(name=BaseColumns._ID, index=true) val fileName: Long,
|
||||
val mimeTypes: Array<String>
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as FileUri
|
||||
|
||||
if (fileName != other.fileName) return false
|
||||
if (!mimeTypes.contentEquals(other.mimeTypes)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = 31 + fileName.hashCode()
|
||||
result = 31 * result + mimeTypes.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface FileUriDao {
|
||||
@Query("SELECT * FROM file_uris WHERE ${BaseColumns._ID} == (:uid)")
|
||||
fun getById(uid: Long) : FileUri
|
||||
|
||||
@Query("DELETE FROM file_uris WHERE ${BaseColumns._ID} == (:id)")
|
||||
fun delete(id: Long)
|
||||
|
||||
@Insert
|
||||
fun insert(vararg fileUris: FileUri)
|
||||
|
||||
@Query("SELECT COUNT(*) FROM file_uris WHERE ${BaseColumns._ID} == (:id)")
|
||||
fun numberWithId(id: Long): Int
|
||||
|
||||
@Query("SELECT * FROM file_uris")
|
||||
fun getAll(): List<FileUri>
|
||||
}
|
||||
|
||||
@Database(entities = [FileUri::class], version = 1)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class FileUriDatabase : RoomDatabase() {
|
||||
abstract fun fileUriDao() : FileUriDao
|
||||
}
|
||||
@@ -16,15 +16,24 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import android.content.ClipDescription
|
||||
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.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 timber.log.Timber
|
||||
|
||||
/**
|
||||
* Class which holds information relevant to an editor instance like the [cachedInput], [selection],
|
||||
@@ -35,10 +44,12 @@ class EditorInstance private constructor(
|
||||
private val ims: InputMethodService?,
|
||||
val imeOptions: ImeOptions,
|
||||
val inputAttributes: InputAttributes,
|
||||
val packageName: String
|
||||
val packageName: String,
|
||||
private val editorInfo: EditorInfo
|
||||
) {
|
||||
val cachedInput: CachedInput = CachedInput(this)
|
||||
var contentMimeTypes: Array<out String?>? = null
|
||||
private val florisClipboardManager: FlorisClipboardManager = FlorisClipboardManager.getInstance()
|
||||
val cursorCapsMode: InputAttributes.CapsMode
|
||||
get() {
|
||||
val ic = inputConnection ?: return InputAttributes.CapsMode.NONE
|
||||
@@ -59,8 +70,6 @@ class EditorInstance private constructor(
|
||||
}
|
||||
}
|
||||
var shouldReevaluateComposingSuggestions: Boolean = false
|
||||
var isNewSelectionInBoundsOfOld: Boolean = false
|
||||
private set
|
||||
var isPrivateMode: Boolean = false
|
||||
val isRawInputEditor: Boolean
|
||||
get() = inputAttributes.type == InputAttributes.Type.NULL
|
||||
@@ -76,7 +85,8 @@ class EditorInstance private constructor(
|
||||
ims = null,
|
||||
imeOptions = ImeOptions.fromImeOptionsInt(EditorInfo.IME_NULL),
|
||||
inputAttributes = InputAttributes.fromInputTypeInt(InputType.TYPE_NULL),
|
||||
packageName = "undefined"
|
||||
packageName = "undefined",
|
||||
editorInfo = EditorInfo()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,7 +96,8 @@ class EditorInstance private constructor(
|
||||
ims = ims,
|
||||
imeOptions = ImeOptions.fromImeOptionsInt(editorInfo.imeOptions),
|
||||
inputAttributes = InputAttributes.fromInputTypeInt(editorInfo.inputType),
|
||||
packageName = editorInfo.packageName
|
||||
packageName = editorInfo.packageName,
|
||||
editorInfo = editorInfo
|
||||
).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
contentMimeTypes = editorInfo.contentMimeTypes
|
||||
@@ -109,24 +120,25 @@ class EditorInstance private constructor(
|
||||
newSelStart: Int, newSelEnd: Int,
|
||||
candidatesStart: Int, candidatesEnd: Int
|
||||
) {
|
||||
isNewSelectionInBoundsOfOld =
|
||||
newSelStart >= (oldSelStart - 1) &&
|
||||
newSelStart <= (oldSelStart + 1) &&
|
||||
newSelEnd >= (oldSelEnd - 1) &&
|
||||
newSelEnd <= (oldSelEnd + 1)
|
||||
selection.update(newSelStart, newSelEnd)
|
||||
cachedInput.update()
|
||||
// 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) {
|
||||
selection.update(newSelEnd, newSelStart)
|
||||
} else {
|
||||
selection.update(newSelStart, newSelEnd)
|
||||
}
|
||||
if (isPhantomSpaceActive && wasPhantomSpaceActiveLastUpdate) {
|
||||
isPhantomSpaceActive = false
|
||||
} else if (isPhantomSpaceActive && !wasPhantomSpaceActiveLastUpdate) {
|
||||
wasPhantomSpaceActiveLastUpdate = true
|
||||
}
|
||||
cachedInput.update()
|
||||
if (isComposingEnabled && candidatesStart >= 0 && candidatesEnd >= 0) {
|
||||
shouldReevaluateComposingSuggestions = true
|
||||
}
|
||||
if (selection.isCursorMode && isComposingEnabled && !isRawInputEditor && !isPhantomSpaceActive) {
|
||||
markComposingRegion(cachedInput.currentWord)
|
||||
} else {
|
||||
} else if (newSelStart >= 0) {
|
||||
markComposingRegion(null)
|
||||
}
|
||||
}
|
||||
@@ -169,22 +181,70 @@ class EditorInstance private constructor(
|
||||
*/
|
||||
fun commitText(text: String): Boolean {
|
||||
val ic = inputConnection ?: return false
|
||||
return if (isRawInputEditor) {
|
||||
return if (isRawInputEditor || selection.isSelectionMode || !isComposingEnabled) {
|
||||
ic.commitText(text, 1)
|
||||
} else {
|
||||
ic.beginBatchEdit()
|
||||
markComposingRegion(null)
|
||||
if (isPhantomSpaceActive && selection.start > 0 && getTextBeforeCursor(1) != " " && text != " ") {
|
||||
ic.commitText(" ", 1)
|
||||
val isWordComponent = CachedInput.isWordComponent(text)
|
||||
val isPhantomSpace = isPhantomSpaceActive && selection.start > 0 && getTextBeforeCursor(1) != " "
|
||||
when {
|
||||
isPhantomSpace && isWordComponent -> {
|
||||
ic.finishComposingText()
|
||||
ic.commitText(" ", 1)
|
||||
ic.setComposingText(text, 1)
|
||||
}
|
||||
!isPhantomSpace && isWordComponent -> {
|
||||
ic.finishComposingText()
|
||||
ic.commitText(text, 1)
|
||||
ic.setComposingRegion(cachedInput.currentWord.start, cachedInput.currentWord.end + text.length)
|
||||
}
|
||||
else -> {
|
||||
ic.finishComposingText()
|
||||
ic.commitText(text, 1)
|
||||
}
|
||||
}
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
ic.commitText(text, 1)
|
||||
ic.endBatchEdit()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Commits the given [ClipboardItem]. If the clip data is text (incl. HTML), it delegates to [commitText].
|
||||
* If the item has a content URI (and the EditText supports it), the item is committed as rich data.
|
||||
* This allows for committing (e.g) images.
|
||||
*
|
||||
* @param item The ClipboardItem to commit
|
||||
* @return True on success, false if something went wrong.
|
||||
*/
|
||||
fun commitClipboardItem(item: ClipboardItem): Boolean {
|
||||
val mimeTypes = item.mimeTypes
|
||||
return when (item.type){
|
||||
ItemType.IMAGE -> {
|
||||
val inputContentInfo = InputContentInfoCompat(
|
||||
item.uri!!,
|
||||
ClipDescription("clipboard image", mimeTypes),
|
||||
null
|
||||
)
|
||||
val ic = inputConnection ?: return false
|
||||
ic.finishComposingText()
|
||||
var flags = 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
flags = flags or InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION
|
||||
}else {
|
||||
FlorisBoard.getInstance().context.grantUriPermission(editorInfo.packageName, item.uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
InputConnectionCompat.commitContent(ic, editorInfo, inputContentInfo, flags, null)
|
||||
}
|
||||
ItemType.TEXT -> {
|
||||
commitText(item.text.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes a backward delete on this editor's text. If a text selection is active, all
|
||||
* characters inside this selection will be removed, else only the left-most character from
|
||||
@@ -193,22 +253,9 @@ class EditorInstance private constructor(
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun deleteBackwards(): Boolean {
|
||||
val ic = inputConnection ?: return false
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
return if (isRawInputEditor) {
|
||||
sendSystemKeyEvent(KeyEvent.KEYCODE_DEL)
|
||||
} else {
|
||||
ic.beginBatchEdit()
|
||||
markComposingRegion(null)
|
||||
sendSystemKeyEvent(KeyEvent.KEYCODE_DEL)
|
||||
cachedInput.update()
|
||||
if (isComposingEnabled) {
|
||||
markComposingRegion(cachedInput.currentWord)
|
||||
}
|
||||
ic.endBatchEdit()
|
||||
true
|
||||
}
|
||||
return sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,12 +277,14 @@ class EditorInstance private constructor(
|
||||
ic.beginBatchEdit()
|
||||
markComposingRegion(null)
|
||||
|
||||
getWordsInString(cachedInput.rawText.substring(0,
|
||||
(selection.start - cachedInput.offset).coerceAtLeast(0))).run {
|
||||
get(size - n.coerceAtLeast(0)).range
|
||||
}.run {
|
||||
ic.setSelection(first + cachedInput.offset, selection.start)
|
||||
}
|
||||
try {
|
||||
getWordsInString(cachedInput.rawText.substring(0,
|
||||
(selection.start - cachedInput.offset).coerceAtLeast(0))).run {
|
||||
get(size - n.coerceAtLeast(0)).range
|
||||
}.run {
|
||||
ic.setSelection(first + cachedInput.offset, selection.start)
|
||||
}
|
||||
} catch (e: Exception) {}
|
||||
|
||||
ic.commitText("", 1)
|
||||
|
||||
@@ -355,9 +404,11 @@ class EditorInstance private constructor(
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performClipboardCut(): Boolean {
|
||||
Timber.d("performClipboardCut")
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_X)
|
||||
florisClipboardManager.addNewPlaintext(selection.text)
|
||||
return sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -367,10 +418,11 @@ class EditorInstance private constructor(
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performClipboardCopy(): Boolean {
|
||||
Timber.d("performClipboardCopy")
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_C) &&
|
||||
selection.updateAndNotify(selection.end, selection.end)
|
||||
florisClipboardManager.addNewPlaintext(selection.text)
|
||||
return selection.updateAndNotify(selection.end, selection.end)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,7 +434,8 @@ class EditorInstance private constructor(
|
||||
fun performClipboardPaste(): Boolean {
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_V)
|
||||
Timber.d("Before commit clip data")
|
||||
return commitClipboardItem(florisClipboardManager.primaryClip!!)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -394,7 +447,14 @@ class EditorInstance private constructor(
|
||||
fun performClipboardSelectAll(): Boolean {
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_A)
|
||||
markComposingRegion(null)
|
||||
val ic = inputConnection ?: return false
|
||||
if (isRawInputEditor) {
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_A, meta(ctrl = true))
|
||||
} else {
|
||||
ic.performContextMenuAction(android.R.id.selectAll)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -406,7 +466,7 @@ class EditorInstance private constructor(
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
return if (isRawInputEditor) {
|
||||
sendSystemKeyEvent(KeyEvent.KEYCODE_ENTER)
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER)
|
||||
} else {
|
||||
commitText("\n")
|
||||
}
|
||||
@@ -434,21 +494,7 @@ class EditorInstance private constructor(
|
||||
fun performUndo(): Boolean {
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
val ic = inputConnection ?: return false
|
||||
return if (isRawInputEditor) {
|
||||
sendSystemKeyEventCtrl(KeyEvent.KEYCODE_Z)
|
||||
true
|
||||
} else {
|
||||
ic.beginBatchEdit()
|
||||
markComposingRegion(null)
|
||||
sendSystemKeyEventCtrl(KeyEvent.KEYCODE_Z)
|
||||
cachedInput.update()
|
||||
if (isComposingEnabled) {
|
||||
markComposingRegion(cachedInput.currentWord)
|
||||
}
|
||||
ic.endBatchEdit()
|
||||
true
|
||||
}
|
||||
return sendDownUpKeyEvent(KeyEvent.KEYCODE_Z, meta(ctrl = true))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -459,73 +505,40 @@ class EditorInstance private constructor(
|
||||
fun performRedo(): Boolean {
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
val ic = inputConnection ?: return false
|
||||
return if (isRawInputEditor) {
|
||||
sendSystemKeyEventCtrlShift(KeyEvent.KEYCODE_Z)
|
||||
true
|
||||
} else {
|
||||
ic.beginBatchEdit()
|
||||
markComposingRegion(null)
|
||||
sendSystemKeyEventCtrlShift(KeyEvent.KEYCODE_Z)
|
||||
cachedInput.update()
|
||||
if (isComposingEnabled) {
|
||||
markComposingRegion(cachedInput.currentWord)
|
||||
}
|
||||
ic.endBatchEdit()
|
||||
true
|
||||
return sendDownUpKeyEvent(KeyEvent.KEYCODE_Z, meta(ctrl = true, shift = true))
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a meta state integer flag which can be used for setting the `metaState` field when sending a KeyEvent
|
||||
* to the input connection. If this method is called without a meta modifier set to true, the default value `0` is
|
||||
* returned.
|
||||
*
|
||||
* @param ctrl Set to true to enable the CTRL meta modifier. Defaults to false.
|
||||
* @param alt Set to true to enable the ALT meta modifier. Defaults to false.
|
||||
* @param shift Set to true to enable the SHIFT meta modifier. Defaults to false.
|
||||
*
|
||||
* @return An integer containing all meta flags passed and formatted for use in a [KeyEvent].
|
||||
*/
|
||||
fun meta(
|
||||
ctrl: Boolean = false,
|
||||
alt: Boolean = false,
|
||||
shift: Boolean = false
|
||||
): Int {
|
||||
var metaState = 0
|
||||
if (ctrl) {
|
||||
metaState = metaState or KeyEvent.META_CTRL_ON or KeyEvent.META_CTRL_LEFT_ON
|
||||
}
|
||||
if (alt) {
|
||||
metaState = metaState or KeyEvent.META_ALT_ON or KeyEvent.META_ALT_LEFT_ON
|
||||
}
|
||||
if (shift) {
|
||||
metaState = metaState or KeyEvent.META_SHIFT_ON or KeyEvent.META_SHIFT_LEFT_ON
|
||||
}
|
||||
return metaState
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [keyEventCode] with [sendDownUpKeyEvent].
|
||||
*
|
||||
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun sendSystemKeyEvent(keyEventCode: Int): Boolean {
|
||||
return sendDownUpKeyEvent(keyEventCode, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [keyEventCode] with Ctrl pressed with [sendDownUpKeyEvent].
|
||||
*
|
||||
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
private fun sendSystemKeyEventCtrl(keyEventCode: Int): Boolean {
|
||||
return sendDownUpKeyEvent(keyEventCode, KeyEvent.META_CTRL_ON)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [keyEventCode] with Ctrl and Shift pressed with [sendDownUpKeyEvent].
|
||||
*
|
||||
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
private fun sendSystemKeyEventCtrlShift(keyEventCode: Int): Boolean {
|
||||
return sendDownUpKeyEvent(keyEventCode, KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [keyEventCode] with Alt pressed with [sendDownUpKeyEvent].
|
||||
*
|
||||
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun sendSystemKeyEventAlt(keyEventCode: Int): Boolean {
|
||||
return sendDownUpKeyEvent(keyEventCode, KeyEvent.META_ALT_LEFT_ON)
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as [InputMethodService.sendDownUpKeyEvents] but also allows to set meta state.
|
||||
*
|
||||
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
|
||||
* @param metaState Flags indicating which meta keys are currently pressed.
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
private fun sendDownUpKeyEvent(keyEventCode: Int, metaState: Int): Boolean {
|
||||
private fun sendDownKeyEvent(eventTime: Long, keyEventCode: Int, metaState: Int): Boolean {
|
||||
val ic = inputConnection ?: return false
|
||||
val eventTime = SystemClock.uptimeMillis()
|
||||
return ic.sendKeyEvent(
|
||||
KeyEvent(
|
||||
eventTime,
|
||||
@@ -536,9 +549,15 @@ class EditorInstance private constructor(
|
||||
metaState,
|
||||
KeyCharacterMap.VIRTUAL_KEYBOARD,
|
||||
0,
|
||||
KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE
|
||||
KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE,
|
||||
InputDevice.SOURCE_KEYBOARD
|
||||
)
|
||||
) && ic.sendKeyEvent(
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendUpKeyEvent(eventTime: Long, keyEventCode: Int, metaState: Int): Boolean {
|
||||
val ic = inputConnection ?: return false
|
||||
return ic.sendKeyEvent(
|
||||
KeyEvent(
|
||||
eventTime,
|
||||
SystemClock.uptimeMillis(),
|
||||
@@ -548,10 +567,52 @@ class EditorInstance private constructor(
|
||||
metaState,
|
||||
KeyCharacterMap.VIRTUAL_KEYBOARD,
|
||||
0,
|
||||
KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE
|
||||
KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE,
|
||||
InputDevice.SOURCE_KEYBOARD
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as [InputMethodService.sendDownUpKeyEvents] but also allows to set meta state.
|
||||
*
|
||||
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
|
||||
* @param metaState Flags indicating which meta keys are currently pressed.
|
||||
* @param count How often the key is pressed while the meta keys passed are down. Must be greater than or equal to
|
||||
* `1`, else this method will immediately return false.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun sendDownUpKeyEvent(keyEventCode: Int, metaState: Int = meta(), count: Int = 1): Boolean {
|
||||
if (count < 1) return false
|
||||
val ic = inputConnection ?: return false
|
||||
ic.beginBatchEdit()
|
||||
val eventTime = SystemClock.uptimeMillis()
|
||||
if (metaState and KeyEvent.META_CTRL_ON > 0) {
|
||||
sendDownKeyEvent(eventTime, KeyEvent.KEYCODE_CTRL_LEFT, 0)
|
||||
}
|
||||
if (metaState and KeyEvent.META_ALT_ON > 0) {
|
||||
sendDownKeyEvent(eventTime, KeyEvent.KEYCODE_ALT_LEFT, 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) {
|
||||
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_SHIFT_LEFT, 0)
|
||||
}
|
||||
if (metaState and KeyEvent.META_ALT_ON > 0) {
|
||||
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_ALT_LEFT, 0)
|
||||
}
|
||||
if (metaState and KeyEvent.META_CTRL_ON > 0) {
|
||||
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_CTRL_LEFT, 0)
|
||||
}
|
||||
ic.endBatchEdit()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -893,7 +954,7 @@ class CachedInput(private val editorInstance: EditorInstance) {
|
||||
* the target app's editor text is bigger than [CACHED_TEXT_N_CHARS_BEFORE_CURSOR] and
|
||||
* [CACHED_TEXT_N_CHARS_AFTER_CURSOR], but always caches the relevant text around the cursor.
|
||||
*/
|
||||
var rawText: String = ""
|
||||
var rawText: StringBuilder = StringBuilder()
|
||||
private set
|
||||
|
||||
companion object {
|
||||
@@ -902,6 +963,10 @@ class CachedInput(private val editorInstance: EditorInstance) {
|
||||
|
||||
private val WORD_EVAL_REGEX = """[^\p{L}\']""".toRegex()
|
||||
private val WORD_SPLIT_REGEX_EN = """((?<=$WORD_EVAL_REGEX)|(?=$WORD_EVAL_REGEX))""".toRegex()
|
||||
|
||||
fun isWordComponent(string: String): Boolean {
|
||||
return !WORD_EVAL_REGEX.matches(string)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -945,18 +1010,18 @@ class CachedInput(private val editorInstance: EditorInstance) {
|
||||
val ic = inputConnection
|
||||
if (ic == null) {
|
||||
offset = 0
|
||||
rawText = ""
|
||||
rawText.clear()
|
||||
expectedMaxLength = 0
|
||||
} else {
|
||||
val textBefore = getTextBeforeCursor(CACHED_TEXT_N_CHARS_BEFORE_CURSOR)
|
||||
val textSelected = ic.getSelectedText(0) ?: ""
|
||||
val textAfter = getTextAfterCursor(CACHED_TEXT_N_CHARS_AFTER_CURSOR)
|
||||
offset = (selection.start - textBefore.length).coerceAtLeast(0)
|
||||
rawText = StringBuilder().run {
|
||||
rawText.apply {
|
||||
clear()
|
||||
append(textBefore)
|
||||
append(textSelected)
|
||||
append(textAfter)
|
||||
toString()
|
||||
}
|
||||
expectedMaxLength = offset + rawText.length
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dev.patrickgold.florisboard.R
|
||||
|
||||
abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity() {
|
||||
private var _binding: V? = null
|
||||
protected val binding: V
|
||||
get() = _binding!!
|
||||
|
||||
private var _prefs: PrefHelper? = null
|
||||
protected val prefs: PrefHelper
|
||||
get() = _prefs!!
|
||||
|
||||
private var errorDialog: AlertDialog? = null
|
||||
private var errorSnackbar: Snackbar? = null
|
||||
private var errorThrowable: Throwable? = null
|
||||
private var messageSnackbar: Snackbar? = null
|
||||
|
||||
protected abstract fun onCreateBinding(): V
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
_prefs = PrefHelper.getDefaultInstance(applicationContext)
|
||||
onCreateBinding().let {
|
||||
_binding = it
|
||||
setContentView(it.root)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
_prefs = null
|
||||
errorDialog?.dismiss()
|
||||
errorDialog = null
|
||||
errorSnackbar?.dismiss()
|
||||
errorSnackbar = null
|
||||
errorThrowable = null
|
||||
messageSnackbar?.dismiss()
|
||||
messageSnackbar = null
|
||||
}
|
||||
|
||||
protected fun showMessage(@StringRes snackbarMessageResId: Int) {
|
||||
val snackbarMessage = resources.getString(snackbarMessageResId)
|
||||
showMessage(snackbarMessage)
|
||||
}
|
||||
|
||||
protected fun showMessage(snackbarMessage: String) {
|
||||
messageSnackbar?.dismiss()
|
||||
messageSnackbar = Snackbar.make(binding.root, snackbarMessage, Snackbar.LENGTH_LONG).apply {
|
||||
setAction(android.R.string.ok) {
|
||||
messageSnackbar?.dismiss()
|
||||
}
|
||||
show() }
|
||||
}
|
||||
|
||||
protected fun showError(throwable: Throwable) {
|
||||
val snackbarMessage = resources.getString(R.string.assets__error__snackbar_message)
|
||||
showError(snackbarMessage, throwable)
|
||||
}
|
||||
|
||||
protected fun showError(@StringRes snackbarMessageResId: Int, throwable: Throwable) {
|
||||
val snackbarMessage = resources.getString(snackbarMessageResId)
|
||||
showError(snackbarMessage, throwable)
|
||||
}
|
||||
|
||||
protected fun showError(snackbarMessage: String, throwable: Throwable) {
|
||||
errorDialog?.dismiss()
|
||||
errorDialog = null
|
||||
errorSnackbar?.dismiss()
|
||||
errorSnackbar = Snackbar.make(binding.root, snackbarMessage, Snackbar.LENGTH_LONG).apply {
|
||||
setAction(R.string.assets__error__details) {
|
||||
errorDialog?.dismiss()
|
||||
errorDialog = AlertDialog.Builder(this@FlorisActivity).run {
|
||||
setTitle(R.string.assets__error__details)
|
||||
setMessage(errorThrowable.toString())
|
||||
setPositiveButton(android.R.string.ok, null)
|
||||
setNeutralButton(R.string.crash_dialog__copy_to_clipboard) { _, _ ->
|
||||
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE)
|
||||
if (clipboardManager != null && clipboardManager is ClipboardManager) {
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(errorThrowable.toString(), errorThrowable.toString()))
|
||||
}
|
||||
}
|
||||
create()
|
||||
show()
|
||||
}
|
||||
}
|
||||
show()
|
||||
}
|
||||
errorThrowable = throwable
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,8 @@
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.inputmethodservice.ExtractEditText
|
||||
@@ -39,11 +37,15 @@ import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.lifecycle.*
|
||||
import com.squareup.moshi.Json
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.clip.ClipboardInputManager
|
||||
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
|
||||
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
|
||||
import dev.patrickgold.florisboard.ime.media.MediaInputManager
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupLayerView
|
||||
import dev.patrickgold.florisboard.ime.text.TextInputManager
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
@@ -57,6 +59,9 @@ import dev.patrickgold.florisboard.util.*
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Variable which holds the current [FlorisBoard] instance. To get this instance from another
|
||||
@@ -68,13 +73,16 @@ private var florisboardInstance: FlorisBoard? = null
|
||||
* Core class responsible to link together both the text and media input managers as well as
|
||||
* managing the one-handed UI.
|
||||
*/
|
||||
class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedListener,
|
||||
class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager.OnPrimaryClipChangedListener,
|
||||
ThemeManager.OnThemeUpdatedListener {
|
||||
|
||||
lateinit var prefs: PrefHelper
|
||||
private set
|
||||
|
||||
val context: Context
|
||||
get() = inputWindowView?.context ?: this
|
||||
private val serviceLifecycleDispatcher: ServiceLifecycleDispatcher = ServiceLifecycleDispatcher(this)
|
||||
|
||||
private var extractEditLayout: WeakReference<ViewGroup?> = WeakReference(null)
|
||||
var inputView: InputView? = null
|
||||
private set
|
||||
@@ -85,11 +93,22 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
|
||||
private var audioManager: AudioManager? = null
|
||||
var imeManager:InputMethodManager? = null
|
||||
var clipboardManager: ClipboardManager? = null
|
||||
var florisClipboardManager: FlorisClipboardManager? = null
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
private var vibrator: Vibrator? = null
|
||||
private val osHandler = Handler()
|
||||
|
||||
private var internalBatchNestingLevel: Int = 0
|
||||
private val internalSelectionCache = object {
|
||||
var selectionCatchCount: Int = 0
|
||||
var oldSelStart: Int = -1
|
||||
var oldSelEnd: Int = -1
|
||||
var newSelStart: Int = -1
|
||||
var newSelEnd: Int = -1
|
||||
var candidatesStart: Int = -1
|
||||
var candidatesEnd: Int = -1
|
||||
}
|
||||
|
||||
var activeEditorInstance: EditorInstance = EditorInstance.default()
|
||||
|
||||
lateinit var subtypeManager: SubtypeManager
|
||||
@@ -97,31 +116,42 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
private var currentThemeIsNight: Boolean = false
|
||||
private var currentThemeResId: Int = 0
|
||||
private var isNumberRowVisible: Boolean = false
|
||||
private var isWindowShown: Boolean = false
|
||||
|
||||
val textInputManager: TextInputManager
|
||||
val mediaInputManager: MediaInputManager
|
||||
val clipInputManager: ClipboardInputManager
|
||||
|
||||
var isClipboardContextMenuShown = false
|
||||
|
||||
init {
|
||||
florisboardInstance = this
|
||||
|
||||
textInputManager = TextInputManager.getInstance()
|
||||
mediaInputManager = MediaInputManager.getInstance()
|
||||
clipInputManager = ClipboardInputManager.getInstance()
|
||||
}
|
||||
|
||||
lateinit var asyncExecutor: ExecutorService
|
||||
|
||||
companion object {
|
||||
private const val IME_ID: String = "dev.patrickgold.florisboard/.ime.core.FlorisBoard"
|
||||
private const val IME_ID_BETA: String = "dev.patrickgold.florisboard.beta/dev.patrickgold.florisboard.ime.core.FlorisBoard"
|
||||
private const val IME_ID_DEBUG: String = "dev.patrickgold.florisboard.debug/dev.patrickgold.florisboard.ime.core.FlorisBoard"
|
||||
|
||||
fun checkIfImeIsEnabled(context: Context): Boolean {
|
||||
val activeImeIds = Settings.Secure.getString(
|
||||
context.contentResolver,
|
||||
Settings.Secure.ENABLED_INPUT_METHODS
|
||||
)
|
||||
) ?: "(none)"
|
||||
Timber.i("List of active IMEs: $activeImeIds")
|
||||
return when {
|
||||
BuildConfig.DEBUG -> {
|
||||
activeImeIds.split(":").contains(IME_ID_DEBUG)
|
||||
}
|
||||
context.packageName.endsWith(".beta") -> {
|
||||
activeImeIds.split(":").contains(IME_ID_BETA)
|
||||
}
|
||||
else -> {
|
||||
activeImeIds.split(":").contains(IME_ID)
|
||||
}
|
||||
@@ -132,12 +162,15 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
val selectedImeId = Settings.Secure.getString(
|
||||
context.contentResolver,
|
||||
Settings.Secure.DEFAULT_INPUT_METHOD
|
||||
)
|
||||
) ?: "(none)"
|
||||
Timber.i("Selected IME: $selectedImeId")
|
||||
return when {
|
||||
BuildConfig.DEBUG -> {
|
||||
selectedImeId == IME_ID_DEBUG
|
||||
}
|
||||
context.packageName.endsWith(".beta") -> {
|
||||
selectedImeId.split(":").contains(IME_ID_BETA)
|
||||
}
|
||||
else -> {
|
||||
selectedImeId == IME_ID
|
||||
}
|
||||
@@ -162,6 +195,10 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLifecycle(): Lifecycle {
|
||||
return serviceLifecycleDispatcher.lifecycle
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
/*if (BuildConfig.DEBUG) {
|
||||
StrictMode.setThreadPolicy(
|
||||
@@ -182,11 +219,10 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
)
|
||||
}*/
|
||||
Timber.i("onCreate()")
|
||||
serviceLifecycleDispatcher.onServicePreSuperOnCreate()
|
||||
|
||||
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
audioManager = getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
||||
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||
clipboardManager?.addPrimaryClipChangedListener(this)
|
||||
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
||||
prefs = PrefHelper.getDefaultInstance(this)
|
||||
prefs.initDefaultPreferences()
|
||||
@@ -202,6 +238,11 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
|
||||
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
|
||||
|
||||
asyncExecutor = Executors.newSingleThreadExecutor()
|
||||
florisClipboardManager = FlorisClipboardManager.getInstance()
|
||||
florisClipboardManager!!.initialize(context)
|
||||
florisClipboardManager?.addPrimaryClipChangedListener(this)
|
||||
|
||||
super.onCreate()
|
||||
eventListeners.toList().forEach { it?.onCreate() }
|
||||
}
|
||||
@@ -249,6 +290,22 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
return eel
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Timber.i("onDestroy()")
|
||||
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
florisClipboardManager!!.removePrimaryClipChangedListener(this)
|
||||
florisClipboardManager!!.close()
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
florisboardInstance = null
|
||||
|
||||
serviceLifecycleDispatcher.onServicePreSuperOnDestroy()
|
||||
|
||||
eventListeners.toList().forEach { it?.onDestroy() }
|
||||
eventListeners.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onEvaluateFullscreenMode(): Boolean {
|
||||
return resources?.configuration?.let { config ->
|
||||
if (config.orientation != Configuration.ORIENTATION_LANDSCAPE) {
|
||||
@@ -286,7 +343,6 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
}
|
||||
this.inputView = inputView
|
||||
initializeOneHandedEnvironment()
|
||||
updateSoftInputWindowLayoutParameters()
|
||||
updateOneHandedPanelVisibility()
|
||||
themeManager.notifyCallbackReceivers()
|
||||
@@ -295,24 +351,10 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
eventListeners.toList().forEach { it?.onRegisterInputView(inputView) }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Timber.i("onDestroy()")
|
||||
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
clipboardManager?.removePrimaryClipChangedListener(this)
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
florisboardInstance = null
|
||||
|
||||
eventListeners.toList().forEach { it?.onDestroy() }
|
||||
eventListeners.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
|
||||
Timber.i("onStartInput($attribute, $restarting)")
|
||||
|
||||
super.onStartInput(attribute, restarting)
|
||||
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE)
|
||||
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
|
||||
}
|
||||
|
||||
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
|
||||
@@ -346,7 +388,13 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
Timber.i("onWindowShown()")
|
||||
if (isWindowShown) {
|
||||
Timber.i("Ignoring onWindowShown()")
|
||||
return
|
||||
} else {
|
||||
Timber.i("onWindowShown()")
|
||||
}
|
||||
isWindowShown = true
|
||||
|
||||
prefs.sync()
|
||||
val newIsNumberRowVisible = prefs.keyboard.numberRow
|
||||
@@ -365,7 +413,13 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
|
||||
override fun onWindowHidden() {
|
||||
Timber.i("onWindowHidden()")
|
||||
if (!isWindowShown) {
|
||||
Timber.i("Ignoring onWindowHidden()")
|
||||
return
|
||||
} else {
|
||||
Timber.i("onWindowHidden()")
|
||||
}
|
||||
isWindowShown = false
|
||||
|
||||
super.onWindowHidden()
|
||||
eventListeners.toList().forEach { it?.onWindowHidden() }
|
||||
@@ -380,24 +434,74 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins a FlorisBoard internal batch edit. This enables the application to continue sending selection updates
|
||||
* (some apps need to to this else they absolutely refuse to give visual feedback on cursor movement etc.). The
|
||||
* selection update is then caught if [internalBatchNestingLevel] is greater than 0, thus not delegating the
|
||||
* update to the editor instance. This is needed because else the UI stutters when too many updates arrive in a
|
||||
* row.
|
||||
*/
|
||||
fun beginInternalBatchEdit() {
|
||||
internalBatchNestingLevel++
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends an internal batch edit, if [internalBatchNestingLevel] is <= 1 and calls [onUpdateSelection] with the
|
||||
* corresponding reported selection values. This call is not caught and the editor instance and other classes are
|
||||
* able to update the UI. Resets the internal selection cache and is ready for the next batch edit.
|
||||
*/
|
||||
fun endInternalBatchEdit() {
|
||||
internalBatchNestingLevel = (internalBatchNestingLevel - 1).coerceAtLeast(0)
|
||||
if (internalBatchNestingLevel == 0) {
|
||||
internalSelectionCache.apply {
|
||||
if (selectionCatchCount > 0) {
|
||||
onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd,
|
||||
candidatesStart, candidatesEnd
|
||||
)
|
||||
selectionCatchCount = 0
|
||||
oldSelStart = -1
|
||||
oldSelEnd = -1
|
||||
newSelStart = -1
|
||||
newSelEnd = -1
|
||||
candidatesStart = -1
|
||||
candidatesEnd = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUpdateSelection(
|
||||
oldSelStart: Int, oldSelEnd: Int,
|
||||
newSelStart: Int, newSelEnd: Int,
|
||||
candidatesStart: Int, candidatesEnd: Int
|
||||
) {
|
||||
Timber.i("onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
|
||||
|
||||
super.onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd,
|
||||
candidatesStart, candidatesEnd
|
||||
)
|
||||
activeEditorInstance.onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd,
|
||||
candidatesStart, candidatesEnd
|
||||
)
|
||||
eventListeners.toList().forEach { it?.onUpdateSelection() }
|
||||
|
||||
if (internalBatchNestingLevel == 0) {
|
||||
Timber.i("onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
|
||||
activeEditorInstance.onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd,
|
||||
candidatesStart, candidatesEnd
|
||||
)
|
||||
eventListeners.toList().forEach { it?.onUpdateSelection() }
|
||||
} else {
|
||||
Timber.i("onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd): caught due to internal batch level of $internalBatchNestingLevel!")
|
||||
if (internalSelectionCache.selectionCatchCount++ == 0) {
|
||||
internalSelectionCache.oldSelStart = oldSelStart
|
||||
internalSelectionCache.oldSelEnd = oldSelEnd
|
||||
}
|
||||
internalSelectionCache.newSelStart = newSelStart
|
||||
internalSelectionCache.newSelEnd = newSelEnd
|
||||
internalSelectionCache.candidatesStart = candidatesStart
|
||||
internalSelectionCache.candidatesEnd = candidatesEnd
|
||||
}
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
@@ -439,14 +543,6 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
|
||||
// Update InputView theme
|
||||
inputView?.setBackgroundColor(theme.getAttr(Theme.Attr.KEYBOARD_BACKGROUND).toSolidColor().color)
|
||||
inputView?.oneHandedCtrlPanelStart?.setBackgroundColor(theme.getAttr(Theme.Attr.ONE_HANDED_BACKGROUND).toSolidColor().color)
|
||||
inputView?.oneHandedCtrlPanelEnd?.setBackgroundColor(theme.getAttr(Theme.Attr.ONE_HANDED_BACKGROUND).toSolidColor().color)
|
||||
ColorStateList.valueOf(theme.getAttr(Theme.Attr.ONE_HANDED_FOREGROUND).toSolidColor().color).also {
|
||||
inputView?.oneHandedCtrlMoveStart?.imageTintList = it
|
||||
inputView?.oneHandedCtrlMoveEnd?.imageTintList = it
|
||||
inputView?.oneHandedCtrlCloseStart?.imageTintList = it
|
||||
inputView?.oneHandedCtrlCloseEnd?.imageTintList = it
|
||||
}
|
||||
inputView?.invalidate()
|
||||
|
||||
// Update ExtractTextView theme and attributes
|
||||
@@ -484,6 +580,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
val inputView = this.inputView ?: return
|
||||
val inputWindowView = this.inputWindowView ?: return
|
||||
// TODO: Check also if the keyboard is currently suppressed by a hardware keyboard
|
||||
|
||||
if (!isInputViewShown) {
|
||||
outInsets?.contentTopInsets = inputWindowView.height
|
||||
outInsets?.visibleTopInsets = inputWindowView.height
|
||||
@@ -492,6 +589,11 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
val visibleTopY = inputWindowView.height - inputView.measuredHeight
|
||||
outInsets?.contentTopInsets = visibleTopY
|
||||
outInsets?.visibleTopInsets = visibleTopY
|
||||
|
||||
if (isClipboardContextMenuShown) {
|
||||
outInsets?.touchableInsets = Insets.TOUCHABLE_INSETS_FRAME
|
||||
outInsets?.touchableRegion?.setEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -586,7 +688,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
startActivity(i)
|
||||
applicationContext.startActivity(i)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -641,71 +743,47 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
private fun onSubtypeChanged(newSubtype: Subtype) {
|
||||
textInputManager.onSubtypeChanged(newSubtype)
|
||||
mediaInputManager.onSubtypeChanged(newSubtype)
|
||||
clipInputManager.onSubtypeChanged(newSubtype)
|
||||
}
|
||||
|
||||
fun setActiveInput(type: Int) {
|
||||
when (type) {
|
||||
R.id.text_input -> {
|
||||
inputView?.mainViewFlipper?.displayedChild = 0
|
||||
textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.VIEW_CHARACTERS))
|
||||
}
|
||||
R.id.media_input -> {
|
||||
inputView?.mainViewFlipper?.displayedChild = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeOneHandedEnvironment() {
|
||||
{ v:View -> onOneHandedPanelButtonClick(v) }.also {
|
||||
inputView?.oneHandedCtrlMoveStart?.setOnClickListener(it)
|
||||
inputView?.oneHandedCtrlMoveEnd?.setOnClickListener(it)
|
||||
inputView?.oneHandedCtrlCloseStart?.setOnClickListener(it)
|
||||
inputView?.oneHandedCtrlCloseEnd?.setOnClickListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOneHandedPanelButtonClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.one_handed_ctrl_move_start -> {
|
||||
prefs.keyboard.oneHandedMode = "start"
|
||||
}
|
||||
R.id.one_handed_ctrl_move_end -> {
|
||||
prefs.keyboard.oneHandedMode = "end"
|
||||
}
|
||||
R.id.one_handed_ctrl_close_start,
|
||||
R.id.one_handed_ctrl_close_end -> {
|
||||
prefs.keyboard.oneHandedMode = "off"
|
||||
R.id.clip_input -> {
|
||||
inputView?.mainViewFlipper?.displayedChild = 2
|
||||
}
|
||||
}
|
||||
updateOneHandedPanelVisibility()
|
||||
}
|
||||
|
||||
fun toggleOneHandedMode(isRight: Boolean) {
|
||||
when (prefs.keyboard.oneHandedMode) {
|
||||
"off" -> {
|
||||
prefs.keyboard.oneHandedMode = if (isRight) { "end" } else { "start" }
|
||||
}
|
||||
else -> {
|
||||
prefs.keyboard.oneHandedMode = "off"
|
||||
}
|
||||
prefs.keyboard.oneHandedMode = when (prefs.keyboard.oneHandedMode) {
|
||||
OneHandedMode.OFF -> if (isRight) { OneHandedMode.END } else { OneHandedMode.START }
|
||||
else -> OneHandedMode.OFF
|
||||
}
|
||||
updateOneHandedPanelVisibility()
|
||||
}
|
||||
|
||||
private fun updateOneHandedPanelVisibility() {
|
||||
fun updateOneHandedPanelVisibility() {
|
||||
if (resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
|
||||
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
|
||||
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
|
||||
} else {
|
||||
when (prefs.keyboard.oneHandedMode) {
|
||||
"off" -> {
|
||||
OneHandedMode.OFF -> {
|
||||
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
|
||||
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
|
||||
}
|
||||
"start" -> {
|
||||
OneHandedMode.START -> {
|
||||
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
|
||||
inputView?.oneHandedCtrlPanelEnd?.visibility = View.VISIBLE
|
||||
}
|
||||
"end" -> {
|
||||
OneHandedMode.END -> {
|
||||
inputView?.oneHandedCtrlPanelStart?.visibility = View.VISIBLE
|
||||
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
|
||||
}
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import android.os.SystemClock
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* The main logic point of processing input events and delegating them to the registered event receivers. Currently,
|
||||
* only [InputKeyEvent]s are supported, but in the future this class is thought to be the single point where input
|
||||
* events can be dispatched.
|
||||
*/
|
||||
class InputEventDispatcher private constructor(
|
||||
parentScope: CoroutineScope,
|
||||
channelCapacity: Int,
|
||||
private val mainDispatcher: CoroutineDispatcher,
|
||||
private val defaultDispatcher: CoroutineDispatcher,
|
||||
private val repeatableKeyCodes: IntArray
|
||||
) : InputKeyEventSender {
|
||||
private val channel: Channel<InputKeyEvent> = Channel(channelCapacity)
|
||||
private val scope: CoroutineScope = CoroutineScope(parentScope.coroutineContext)
|
||||
private val pressedKeys: HashMap<Int, PressedKeyInfo> = hashMapOf()
|
||||
var lastKeyEventDown: InputKeyEvent? = null
|
||||
private set
|
||||
var lastKeyEventUp: InputKeyEvent? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* The input key event register. If null, the dispatcher will still process input, but won't dispatch them to an
|
||||
* event receiver.
|
||||
*/
|
||||
var keyEventReceiver: InputKeyEventReceiver? = null
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The default input event channel capacity to be used in [new].
|
||||
*/
|
||||
private const val DEFAULT_CHANNEL_CAPACITY: Int = 32
|
||||
|
||||
/**
|
||||
* Creates a new [InputEventDispatcher] instance from given arguments and returns it.
|
||||
*
|
||||
* @param parentScope The parent coroutine scope which this dispatcher will attach its own scope to.
|
||||
* @param channelCapacity The capacity of this input channel, defaults to [DEFAULT_CHANNEL_CAPACITY].
|
||||
* @param mainDispatcher The main dispatcher used to switch the context to call the receiver callbacks.
|
||||
* Defaults to [Dispatchers.Main].
|
||||
* @param defaultDispatcher The default dispatcher used to switch the context to call the receiver callbacks.
|
||||
* Defaults to [Dispatchers.Default].
|
||||
* @param repeatableKeyCodes An int array of all key codes which are repeatable while being pressed down.
|
||||
*
|
||||
* @return A new [InputEventDispatcher] instance initialized with given arguments.
|
||||
*/
|
||||
fun new(
|
||||
parentScope: CoroutineScope,
|
||||
channelCapacity: Int = DEFAULT_CHANNEL_CAPACITY,
|
||||
mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
|
||||
defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
repeatableKeyCodes: IntArray = intArrayOf()
|
||||
): InputEventDispatcher = InputEventDispatcher(
|
||||
parentScope, channelCapacity, mainDispatcher, defaultDispatcher, repeatableKeyCodes.clone()
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
scope.launch(defaultDispatcher) {
|
||||
for (ev in channel) {
|
||||
if (!isActive) break
|
||||
val startTime = System.nanoTime()
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.d(ev.toString())
|
||||
}
|
||||
when (ev.action) {
|
||||
InputKeyEvent.Action.DOWN -> {
|
||||
if (pressedKeys.containsKey(ev.data.code)) continue
|
||||
pressedKeys[ev.data.code] = PressedKeyInfo(
|
||||
eventTimeDown = ev.eventTime,
|
||||
repeatKeyPressJob = if (!repeatableKeyCodes.contains(ev.data.code)) { null } else {
|
||||
scope.launch(defaultDispatcher) {
|
||||
delay(600)
|
||||
while (isActive) {
|
||||
channel.send(InputKeyEvent.repeat(ev.data))
|
||||
delay(50)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
withContext(mainDispatcher) {
|
||||
keyEventReceiver?.onInputKeyDown(ev)
|
||||
}
|
||||
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
|
||||
lastKeyEventDown = ev
|
||||
}
|
||||
}
|
||||
InputKeyEvent.Action.DOWN_UP -> {
|
||||
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
|
||||
withContext(mainDispatcher) {
|
||||
keyEventReceiver?.onInputKeyDown(ev)
|
||||
keyEventReceiver?.onInputKeyUp(ev)
|
||||
}
|
||||
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
|
||||
lastKeyEventDown = ev
|
||||
lastKeyEventUp = ev
|
||||
}
|
||||
}
|
||||
InputKeyEvent.Action.UP -> {
|
||||
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
|
||||
withContext(mainDispatcher) {
|
||||
keyEventReceiver?.onInputKeyUp(ev)
|
||||
}
|
||||
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
|
||||
lastKeyEventUp = ev
|
||||
}
|
||||
}
|
||||
InputKeyEvent.Action.REPEAT -> {
|
||||
if (pressedKeys.containsKey(ev.data.code)) {
|
||||
withContext(mainDispatcher) {
|
||||
keyEventReceiver?.onInputKeyRepeat(ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
InputKeyEvent.Action.CANCEL -> {
|
||||
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
|
||||
withContext(mainDispatcher) {
|
||||
keyEventReceiver?.onInputKeyCancel(ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.d("Time elapsed: ${(System.nanoTime() - startTime) / 1_000_000}")
|
||||
}
|
||||
}
|
||||
val pressedKeysIterator = pressedKeys.iterator()
|
||||
while (pressedKeysIterator.hasNext()) {
|
||||
pressedKeysIterator.next().value.repeatKeyPressJob?.cancel()
|
||||
pressedKeysIterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun send(ev: InputKeyEvent) {
|
||||
scope.launch(mainDispatcher) {
|
||||
channel.send(ev)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's currently a key down with given [code].
|
||||
*
|
||||
* @param code The key code to check for.
|
||||
*
|
||||
* @return True if the given [code] is currently down, false otherwise.
|
||||
*/
|
||||
fun isPressed(code: Int): Boolean {
|
||||
return pressedKeys.containsKey(code)
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this dispatcher and cancels the local coroutine scope.
|
||||
*/
|
||||
fun close() {
|
||||
keyEventReceiver = null
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
data class PressedKeyInfo(
|
||||
val eventTimeDown: Long,
|
||||
val repeatKeyPressJob: Job?
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class representing a single input key event.
|
||||
*
|
||||
* @property eventTime The exact event time when this event occurred, measured in milliseconds since a static point in
|
||||
* the past. The exact point is irrelevant, but while this input dispatcher is active, the point must not change in
|
||||
* order for difference time calculation to succeed.
|
||||
* @property action The action of this event.
|
||||
* @property data The data of this event.
|
||||
* @property count The count how often this event occurred. Is only respected by other methods if the [action] of this
|
||||
* event is [Action.DOWN_UP] or [Action.REPEAT], else always 1 is assumed.
|
||||
*/
|
||||
data class InputKeyEvent(
|
||||
val eventTime: Long,
|
||||
val action: Action,
|
||||
val data: KeyData,
|
||||
val count: Int
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Creates a new input key event with given [keyData] and sets the action to [Action.DOWN].
|
||||
*
|
||||
* @param keyData The key data of the input key event event to create.
|
||||
*
|
||||
* @return The created input key event.
|
||||
*/
|
||||
fun down(keyData: KeyData): InputKeyEvent {
|
||||
return InputKeyEvent(
|
||||
eventTime = SystemClock.uptimeMillis(),
|
||||
action = Action.DOWN,
|
||||
data = keyData,
|
||||
count = 1
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new input key event with given [keyData] and sets the action to [Action.DOWN_UP].
|
||||
*
|
||||
* @param keyData The key data of the input key event event to create.
|
||||
* @param count How often this event occurred. Must be grater or equal to 1, defaults to 1.
|
||||
*
|
||||
* @return The created input key event.
|
||||
*/
|
||||
fun downUp(keyData: KeyData, count: Int = 1): InputKeyEvent {
|
||||
return InputKeyEvent(
|
||||
eventTime = SystemClock.uptimeMillis(),
|
||||
action = Action.DOWN_UP,
|
||||
data = keyData,
|
||||
count = count
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new input key event with given [keyData] and sets the action to [Action.UP].
|
||||
*
|
||||
* @param keyData The key data of the input key event event to create.
|
||||
*
|
||||
* @return The created input key event.
|
||||
*/
|
||||
fun up(keyData: KeyData): InputKeyEvent {
|
||||
return InputKeyEvent(
|
||||
eventTime = SystemClock.uptimeMillis(),
|
||||
action = Action.UP,
|
||||
data = keyData,
|
||||
count = 1
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new input key event with given [keyData] and sets the action to [Action.REPEAT].
|
||||
*
|
||||
* @param keyData The key data of the input key event event to create.
|
||||
* @param count How often this event occurred. Must be grater or equal to 1, defaults to 1.
|
||||
*
|
||||
* @return The created input key event.
|
||||
*/
|
||||
fun repeat(keyData: KeyData, count: Int = 1): InputKeyEvent {
|
||||
return InputKeyEvent(
|
||||
eventTime = SystemClock.uptimeMillis(),
|
||||
action = Action.REPEAT,
|
||||
data = keyData,
|
||||
count = count
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new input key event with given [keyData] and sets the action to [Action.CANCEL].
|
||||
*
|
||||
* @param keyData The key data of the input key event event to create.
|
||||
*
|
||||
* @return The created input key event.
|
||||
*/
|
||||
fun cancel(keyData: KeyData): InputKeyEvent {
|
||||
return InputKeyEvent(
|
||||
eventTime = SystemClock.uptimeMillis(),
|
||||
action = Action.CANCEL,
|
||||
data = keyData,
|
||||
count = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the [other] input key event is a consecutive event while respecting [maxEventTimeDiff].
|
||||
*
|
||||
* @param other The other input key event to compare with this one.
|
||||
* @param maxEventTimeDiff The maximum event time diff between this event and [other], in milliseconds.
|
||||
*
|
||||
* @return True if this event is a consecutive event of [other], false otherwise.
|
||||
*/
|
||||
fun isConsecutiveEventOf(other: InputKeyEvent?, maxEventTimeDiff: Long): Boolean {
|
||||
return other != null && data.code == other.data.code && eventTime - other.eventTime <= maxEventTimeDiff
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this input key event.
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return "FlorisKeyEvent { eventTime=${eventTime}ms, action=$action, data=$data, count=$count }"
|
||||
}
|
||||
|
||||
/**
|
||||
* The action of an input key event.
|
||||
*/
|
||||
enum class Action {
|
||||
DOWN,
|
||||
DOWN_UP,
|
||||
UP,
|
||||
REPEAT,
|
||||
CANCEL,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface which represents an input key event sender.
|
||||
*/
|
||||
interface InputKeyEventSender {
|
||||
/**
|
||||
* Sends given input key event [ev] to the underlying input channel, awaiting to be processed.
|
||||
*
|
||||
* @param ev The input key event to send.
|
||||
*/
|
||||
fun send(ev: InputKeyEvent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface which represents an input key event receiver.
|
||||
*/
|
||||
interface InputKeyEventReceiver {
|
||||
/**
|
||||
* Event method which gets called when a key went down.
|
||||
*
|
||||
* @param ev The associated input key event.
|
||||
*/
|
||||
fun onInputKeyDown(ev: InputKeyEvent)
|
||||
|
||||
/**
|
||||
* Event method which gets called when a key went up.
|
||||
*
|
||||
* @param ev The associated input key event.
|
||||
*/
|
||||
fun onInputKeyUp(ev: InputKeyEvent)
|
||||
|
||||
/**
|
||||
* Event method which gets called when a key is called repeatedly while being pressed down.
|
||||
*
|
||||
* @param ev The associated input key event.
|
||||
*/
|
||||
fun onInputKeyRepeat(ev: InputKeyEvent)
|
||||
|
||||
/**
|
||||
* Event method which gets called when a key press is cancelled.
|
||||
*
|
||||
* @param ev The associated input key event.
|
||||
*/
|
||||
fun onInputKeyCancel(ev: InputKeyEvent)
|
||||
}
|
||||
@@ -20,10 +20,11 @@ import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.widget.ImageButton
|
||||
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
|
||||
@@ -45,20 +46,16 @@ class InputView : LinearLayout {
|
||||
private set
|
||||
var desiredMediaKeyboardViewHeight: Float = resources.getDimension(R.dimen.mediaKeyboardView_baseHeight)
|
||||
private set
|
||||
var heightFactor: Float = 1.0f
|
||||
private set
|
||||
var shouldGiveAdditionalSpace: Boolean = false
|
||||
private set
|
||||
|
||||
var mainViewFlipper: ViewFlipper? = null
|
||||
private set
|
||||
var oneHandedCtrlPanelStart: LinearLayout? = null
|
||||
var oneHandedCtrlPanelStart: ViewGroup? = null
|
||||
private set
|
||||
var oneHandedCtrlPanelEnd: LinearLayout? = null
|
||||
private set
|
||||
var oneHandedCtrlMoveStart: ImageButton? = null
|
||||
private set
|
||||
var oneHandedCtrlMoveEnd: ImageButton? = null
|
||||
private set
|
||||
var oneHandedCtrlCloseStart: ImageButton? = null
|
||||
private set
|
||||
var oneHandedCtrlCloseEnd: ImageButton? = null
|
||||
var oneHandedCtrlPanelEnd: ViewGroup? = null
|
||||
private set
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
@@ -77,19 +74,15 @@ class InputView : LinearLayout {
|
||||
mainViewFlipper = findViewById(R.id.main_view_flipper)
|
||||
oneHandedCtrlPanelStart = findViewById(R.id.one_handed_ctrl_panel_start)
|
||||
oneHandedCtrlPanelEnd = findViewById(R.id.one_handed_ctrl_panel_end)
|
||||
oneHandedCtrlMoveStart = findViewById(R.id.one_handed_ctrl_move_start)
|
||||
oneHandedCtrlMoveEnd = findViewById(R.id.one_handed_ctrl_move_end)
|
||||
oneHandedCtrlCloseStart = findViewById(R.id.one_handed_ctrl_close_start)
|
||||
oneHandedCtrlCloseEnd = findViewById(R.id.one_handed_ctrl_close_end)
|
||||
|
||||
florisboard.registerInputView(this)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val heightFactor = when (resources.configuration.orientation) {
|
||||
heightFactor = when (resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> 1.0f
|
||||
else -> if (prefs.keyboard.oneHandedMode != "off") {
|
||||
0.9f
|
||||
else -> if (prefs.keyboard.oneHandedMode != OneHandedMode.OFF) {
|
||||
prefs.keyboard.oneHandedModeScaleFactor / 100.0f
|
||||
} else {
|
||||
1.0f
|
||||
}
|
||||
@@ -108,12 +101,12 @@ class InputView : LinearLayout {
|
||||
var baseSmartbarHeight = 0.16129f * baseHeight
|
||||
var baseTextInputHeight = baseHeight - baseSmartbarHeight
|
||||
val tim = florisboard.textInputManager
|
||||
val shouldGiveAdditionalSpace = prefs.keyboard.numberRow &&
|
||||
shouldGiveAdditionalSpace = prefs.keyboard.numberRow &&
|
||||
!(tim.getActiveKeyboardMode() == KeyboardMode.NUMERIC ||
|
||||
tim.getActiveKeyboardMode() == KeyboardMode.PHONE ||
|
||||
tim.getActiveKeyboardMode() == KeyboardMode.PHONE2)
|
||||
if (shouldGiveAdditionalSpace) {
|
||||
val additionalHeight = desiredTextKeyboardViewHeight * 0.18f
|
||||
val additionalHeight = baseTextInputHeight * 0.25f
|
||||
baseHeight += additionalHeight
|
||||
baseTextInputHeight += additionalHeight
|
||||
}
|
||||
@@ -171,5 +164,4 @@ class InputView : LinearLayout {
|
||||
resources.getDimension(R.dimen.inputView_baseHeight)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,11 +22,13 @@ import android.provider.Settings
|
||||
import androidx.preference.PreferenceManager
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.DistanceThreshold
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.VelocityThreshold
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
|
||||
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.CandidateView
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeMode
|
||||
import dev.patrickgold.florisboard.util.TimeUtil
|
||||
import dev.patrickgold.florisboard.util.VersionName
|
||||
@@ -52,6 +54,7 @@ class PrefHelper(
|
||||
val smartbar = Smartbar(this)
|
||||
val suggestion = Suggestion(this)
|
||||
val theme = Theme(this)
|
||||
val clipboard = Clipboard(this)
|
||||
|
||||
/**
|
||||
* Checks the cache if an entry for [key] exists, else calls [getPrefInternal] to retrieve the
|
||||
@@ -309,26 +312,28 @@ class PrefHelper(
|
||||
class Keyboard(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val BOTTOM_OFFSET_PORTRAIT = "keyboard__bottom_offset_portrait"
|
||||
const val BOTTOM_OFFSET_LANDSCAPE = "keyboard__bottom_offset_landscape"
|
||||
const val FONT_SIZE_MULTIPLIER_PORTRAIT = "keyboard__font_size_multiplier_portrait"
|
||||
const val FONT_SIZE_MULTIPLIER_LANDSCAPE = "keyboard__font_size_multiplier_landscape"
|
||||
const val HEIGHT_FACTOR = "keyboard__height_factor"
|
||||
const val HEIGHT_FACTOR_CUSTOM = "keyboard__height_factor_custom"
|
||||
const val HINTED_NUMBER_ROW_MODE = "keyboard__hinted_number_row_mode"
|
||||
const val HINTED_SYMBOLS_MODE = "keyboard__hinted_symbols_mode"
|
||||
const val KEY_SPACING_HORIZONTAL = "keyboard__key_spacing_horizontal"
|
||||
const val KEY_SPACING_VERTICAL = "keyboard__key_spacing_vertical"
|
||||
const val LANDSCAPE_INPUT_UI_MODE = "keyboard__landscape_input_ui_mode"
|
||||
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
|
||||
const val NUMBER_ROW = "keyboard__number_row"
|
||||
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
|
||||
const val POPUP_ENABLED = "keyboard__popup_enabled"
|
||||
const val SOUND_ENABLED = "keyboard__sound_enabled"
|
||||
const val SOUND_VOLUME = "keyboard__sound_volume"
|
||||
const val UTILITY_KEY_ACTION = "keyboard__utility_key_action"
|
||||
const val UTILITY_KEY_ENABLED = "keyboard__utility_key_enabled"
|
||||
const val VIBRATION_ENABLED = "keyboard__vibration_enabled"
|
||||
const val VIBRATION_STRENGTH = "keyboard__vibration_strength"
|
||||
const val BOTTOM_OFFSET_LANDSCAPE = "keyboard__bottom_offset_landscape"
|
||||
const val FONT_SIZE_MULTIPLIER_PORTRAIT = "keyboard__font_size_multiplier_portrait"
|
||||
const val FONT_SIZE_MULTIPLIER_LANDSCAPE = "keyboard__font_size_multiplier_landscape"
|
||||
const val HEIGHT_FACTOR = "keyboard__height_factor"
|
||||
const val HEIGHT_FACTOR_CUSTOM = "keyboard__height_factor_custom"
|
||||
const val HINTED_NUMBER_ROW_MODE = "keyboard__hinted_number_row_mode"
|
||||
const val HINTED_SYMBOLS_MODE = "keyboard__hinted_symbols_mode"
|
||||
const val KEY_SPACING_HORIZONTAL = "keyboard__key_spacing_horizontal"
|
||||
const val KEY_SPACING_VERTICAL = "keyboard__key_spacing_vertical"
|
||||
const val LANDSCAPE_INPUT_UI_MODE = "keyboard__landscape_input_ui_mode"
|
||||
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
|
||||
const val NUMBER_ROW = "keyboard__number_row"
|
||||
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
|
||||
const val ONE_HANDED_MODE_SCALE_FACTOR = "keyboard__one_handed_mode_scale_factor"
|
||||
const val POPUP_ENABLED = "keyboard__popup_enabled"
|
||||
const val SOUND_ENABLED = "keyboard__sound_enabled"
|
||||
const val SOUND_VOLUME = "keyboard__sound_volume"
|
||||
const val SPACE_BAR_SWITCHES_TO_CHARACTERS = "keyboard__space_bar_switches_to_characters"
|
||||
const val UTILITY_KEY_ACTION = "keyboard__utility_key_action"
|
||||
const val UTILITY_KEY_ENABLED = "keyboard__utility_key_enabled"
|
||||
const val VIBRATION_ENABLED = "keyboard__vibration_enabled"
|
||||
const val VIBRATION_STRENGTH = "keyboard__vibration_strength"
|
||||
}
|
||||
|
||||
var bottomOffsetPortrait: Int = 0
|
||||
@@ -371,8 +376,11 @@ class PrefHelper(
|
||||
get() = prefHelper.getPref(NUMBER_ROW, false)
|
||||
set(v) = prefHelper.setPref(NUMBER_ROW, v)
|
||||
var oneHandedMode: String
|
||||
get() = prefHelper.getPref(ONE_HANDED_MODE, "off")
|
||||
get() = prefHelper.getPref(ONE_HANDED_MODE, OneHandedMode.OFF)
|
||||
set(value) = prefHelper.setPref(ONE_HANDED_MODE, value)
|
||||
var oneHandedModeScaleFactor: Int
|
||||
get() = prefHelper.getPref(ONE_HANDED_MODE_SCALE_FACTOR, 87)
|
||||
set(v) = prefHelper.setPref(ONE_HANDED_MODE_SCALE_FACTOR, v)
|
||||
var popupEnabled: Boolean = false
|
||||
get() = prefHelper.getPref(POPUP_ENABLED, true)
|
||||
private set
|
||||
@@ -383,6 +391,9 @@ class PrefHelper(
|
||||
var soundVolume: Int = 0
|
||||
get() = prefHelper.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)
|
||||
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)
|
||||
@@ -434,20 +445,28 @@ class PrefHelper(
|
||||
class Suggestion(private val prefHelper: PrefHelper) {
|
||||
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 SUGGEST_CLIPBOARD_CONTENT = "suggestion__suggest_clipboard_content"
|
||||
const val USE_PREV_WORDS = "suggestion__use_prev_words"
|
||||
}
|
||||
|
||||
var blockPossiblyOffensive: Boolean
|
||||
get() = prefHelper.getPref(BLOCK_POSSIBLY_OFFENSIVE, true)
|
||||
set(v) = prefHelper.setPref(BLOCK_POSSIBLY_OFFENSIVE, v)
|
||||
var clipboardContentEnabled: Boolean
|
||||
get() = prefHelper.getPref(CLIPBOARD_CONTENT_ENABLED, false)
|
||||
set(v) = prefHelper.setPref(CLIPBOARD_CONTENT_ENABLED, v)
|
||||
var clipboardContentTimeout: Int
|
||||
get() = prefHelper.getPref(CLIPBOARD_CONTENT_TIMEOUT, 30)
|
||||
set(v) = prefHelper.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)
|
||||
var enabled: Boolean
|
||||
get() = prefHelper.getPref(ENABLED, true)
|
||||
set(v) = prefHelper.setPref(ENABLED, v)
|
||||
var suggestClipboardContent: Boolean
|
||||
get() = prefHelper.getPref(SUGGEST_CLIPBOARD_CONTENT, false)
|
||||
set(v) = prefHelper.setPref(SUGGEST_CLIPBOARD_CONTENT, v)
|
||||
var usePrevWords: Boolean
|
||||
get() = prefHelper.getPref(USE_PREV_WORDS, true)
|
||||
set(v) = prefHelper.setPref(USE_PREV_WORDS, v)
|
||||
@@ -489,4 +508,52 @@ class PrefHelper(
|
||||
get() = prefHelper.getPref(SUNSET_TIME, TimeUtil.encode(18, 0))
|
||||
set(v) = prefHelper.setPref(SUNSET_TIME, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for clipboard preferences
|
||||
*/
|
||||
class Clipboard(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val ENABLE_INTERNAL = "clipboard__enable_internal"
|
||||
const val SYNC_TO_SYSTEM = "clipboard__sync_to_system"
|
||||
const val SYNC_TO_FLORIS = "clipboard__sync_to_floris"
|
||||
const val ENABLE_HISTORY = "clipboard__enable_history"
|
||||
const val CLEAN_UP_OLD = "clipboard__clean_up_old"
|
||||
const val LIMIT_HISTORY_SIZE = "clipboard__limit_history_size"
|
||||
const val CLEAN_UP_AFTER = "clipboard__clean_up_after"
|
||||
const val MAX_HISTORY_SIZE = "clipboard__max_history_size"
|
||||
}
|
||||
|
||||
var enableInternal: Boolean
|
||||
get() = prefHelper.getPref(ENABLE_INTERNAL, false)
|
||||
set(v) = prefHelper.setPref(ENABLE_INTERNAL, v)
|
||||
|
||||
var syncToSystem: Boolean
|
||||
get() = prefHelper.getPref(SYNC_TO_SYSTEM, false)
|
||||
set(v) = prefHelper.setPref(SYNC_TO_SYSTEM, v)
|
||||
|
||||
var syncToFloris: Boolean
|
||||
get() = prefHelper.getPref(SYNC_TO_FLORIS, true)
|
||||
set(v) = prefHelper.setPref(SYNC_TO_FLORIS, v)
|
||||
|
||||
var enableHistory: Boolean
|
||||
get() = prefHelper.getPref(ENABLE_HISTORY, false)
|
||||
set(v) = prefHelper.setPref(ENABLE_HISTORY, v)
|
||||
|
||||
var cleanUpOld: Boolean
|
||||
get() = prefHelper.getPref(CLEAN_UP_OLD, false)
|
||||
set(v) = prefHelper.setPref(CLEAN_UP_OLD, v)
|
||||
|
||||
var limitHistorySize: Boolean
|
||||
get() = prefHelper.getPref(LIMIT_HISTORY_SIZE, true)
|
||||
set(v) = prefHelper.setPref(LIMIT_HISTORY_SIZE, v)
|
||||
|
||||
var cleanUpAfter: Int
|
||||
get() = prefHelper.getPref(CLEAN_UP_AFTER, 20)
|
||||
set(v) = prefHelper.setPref(CLEAN_UP_AFTER, v)
|
||||
|
||||
var maxHistorySize: Int
|
||||
get() = prefHelper.getPref(MAX_HISTORY_SIZE, 20)
|
||||
set(v) = prefHelper.setPref(MAX_HISTORY_SIZE, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package dev.patrickgold.florisboard.ime.dictionary
|
||||
|
||||
import android.content.Context
|
||||
import com.github.michaelbull.result.*
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -48,22 +47,22 @@ class DictionaryManager private constructor(private val applicationContext: Cont
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDictionary(ref: AssetRef): Result<Dictionary<String, Int>, Throwable> {
|
||||
fun loadDictionary(ref: AssetRef): Result<Dictionary<String, Int>> {
|
||||
dictionaryCache[ref.toString()]?.let {
|
||||
return Ok(it)
|
||||
return Result.success(it)
|
||||
}
|
||||
if (ref.path.endsWith(".flict")) {
|
||||
// Assume this is a Flictionary
|
||||
Flictionary.load(applicationContext, ref).onSuccess { flict ->
|
||||
dictionaryCache[ref.toString()] = flict
|
||||
return Ok(flict)
|
||||
return Result.success(flict)
|
||||
}.onFailure { err ->
|
||||
Timber.i(err)
|
||||
return Err(err)
|
||||
return Result.failure(err)
|
||||
}
|
||||
} else {
|
||||
return Err(Exception("Unable to determine supported type for given AssetRef!"))
|
||||
return Result.failure(Exception("Unable to determine supported type for given AssetRef!"))
|
||||
}
|
||||
return Err(Exception("If this message is ever thrown, something is completely broken..."))
|
||||
return Result.failure(Exception("If this message is ever thrown, something is completely broken..."))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
package dev.patrickgold.florisboard.ime.dictionary
|
||||
|
||||
import android.content.Context
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetSource
|
||||
import dev.patrickgold.florisboard.ime.nlp.*
|
||||
@@ -72,13 +69,13 @@ class Flictionary private constructor(
|
||||
* either the parsed dictionary or an exception giving information about the error which
|
||||
* occurred.
|
||||
*/
|
||||
fun load(context: Context, assetRef: AssetRef): Result<Flictionary, Exception> {
|
||||
fun load(context: Context, assetRef: AssetRef): Result<Flictionary> {
|
||||
val buffer = ByteArray(5000) { 0 }
|
||||
val inputStream: InputStream
|
||||
if (assetRef.source == AssetSource.Assets) {
|
||||
inputStream = context.assets.open(assetRef.path)
|
||||
} else {
|
||||
return Err(Exception("Only AssetSource.Assets is currently supported!"))
|
||||
return Result.failure(Exception("Only AssetSource.Assets is currently supported!"))
|
||||
}
|
||||
|
||||
var headerStr: String? = null
|
||||
@@ -99,7 +96,7 @@ class Flictionary private constructor(
|
||||
(cmd and MASK_BEGIN_PTREE_NODE) == CMDB_BEGIN_PTREE_NODE -> {
|
||||
if (pos == 0) {
|
||||
inputStream.close()
|
||||
return Err(
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_CMD_BEGIN_PTREE_NODE,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
@@ -125,7 +122,7 @@ class Flictionary private constructor(
|
||||
freq = buffer[1].toInt() and 0xFF
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Err(
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
@@ -134,7 +131,7 @@ class Flictionary private constructor(
|
||||
}
|
||||
freqSize = 1
|
||||
}
|
||||
else -> return Err(Exception("TODO: shortcut not supported"))
|
||||
else -> return Result.failure(Exception("TODO: shortcut not supported"))
|
||||
}
|
||||
if (inputStream.readNext(buffer, freqSize + 1, size) > 0) {
|
||||
val char = String(buffer, freqSize + 1, size, Charsets.UTF_8)[0]
|
||||
@@ -154,7 +151,7 @@ class Flictionary private constructor(
|
||||
pos += (freqSize + 1 + size)
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Err(
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
@@ -166,7 +163,7 @@ class Flictionary private constructor(
|
||||
(cmd and MASK_BEGIN_HEADER) == CMDB_BEGIN_HEADER -> {
|
||||
if (pos != 0) {
|
||||
inputStream.close()
|
||||
return Err(
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_CMD_BEGIN_HEADER,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
@@ -176,7 +173,7 @@ class Flictionary private constructor(
|
||||
version = cmd and ATTR_HEADER_VERSION
|
||||
if (version != VERSION_0) {
|
||||
inputStream.close()
|
||||
return Err(
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNSUPPORTED_FLICTIONARY_VERSION,
|
||||
address = pos,
|
||||
@@ -203,7 +200,7 @@ class Flictionary private constructor(
|
||||
pos += (10 + size)
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Err(
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
@@ -212,7 +209,7 @@ class Flictionary private constructor(
|
||||
}
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Err(
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
@@ -224,7 +221,7 @@ class Flictionary private constructor(
|
||||
(cmd and MASK_END) == CMDB_END -> {
|
||||
if (pos == 0) {
|
||||
inputStream.close()
|
||||
return Err(
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_CMD_END,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
@@ -240,7 +237,7 @@ class Flictionary private constructor(
|
||||
}
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Err(
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size - n
|
||||
@@ -249,7 +246,7 @@ class Flictionary private constructor(
|
||||
}
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Err(
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_CMD_END_ZERO_VALUE,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
@@ -260,7 +257,7 @@ class Flictionary private constructor(
|
||||
}
|
||||
else -> {
|
||||
inputStream.close()
|
||||
return Err(
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.INVALID_CMD_BYTE_PROVIDED,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
@@ -272,7 +269,7 @@ class Flictionary private constructor(
|
||||
inputStream.close()
|
||||
|
||||
if (ngramTreeStack.size != 0) {
|
||||
return Err(
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF,
|
||||
address = pos, cmdByte = 0x00.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
@@ -280,7 +277,7 @@ class Flictionary private constructor(
|
||||
)
|
||||
}
|
||||
|
||||
return Ok(
|
||||
return Result.success(
|
||||
Flictionary(
|
||||
name = "flict",
|
||||
label = "flict",
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
package dev.patrickgold.florisboard.ime.extension
|
||||
|
||||
import android.content.Context
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Result
|
||||
|
||||
/**
|
||||
* Interface for an Asset to use within FlorisBoard. An asset is everything from a dictionary to a
|
||||
@@ -61,6 +59,6 @@ interface Asset {
|
||||
/**
|
||||
* Loads an Asset of type [T] from the specified path.
|
||||
*/
|
||||
fun fromFile(context: Context, path: String): Result<T, Throwable> = Err(NotImplementedError())
|
||||
fun fromFile(context: Context, path: String): Result<T> = Result.failure(NotImplementedError())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
package dev.patrickgold.florisboard.ime.extension
|
||||
|
||||
import android.content.Context
|
||||
import com.github.michaelbull.result.*
|
||||
import android.net.Uri
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyTypeAdapter
|
||||
@@ -25,6 +25,7 @@ import dev.patrickgold.florisboard.ime.text.key.KeyVariationAdapter
|
||||
import dev.patrickgold.florisboard.ime.text.layout.LayoutTypeAdapter
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class AssetManager private constructor(private val applicationContext: Context) {
|
||||
private val moshi: Moshi = Moshi.Builder()
|
||||
@@ -59,22 +60,22 @@ class AssetManager private constructor(private val applicationContext: Context)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAsset(ref: AssetRef): Result<Nothing?, Throwable> {
|
||||
fun deleteAsset(ref: AssetRef): Result<Unit> {
|
||||
return when (ref.source) {
|
||||
AssetSource.Internal -> {
|
||||
val file = File(applicationContext.filesDir.absolutePath + "/" + ref.path)
|
||||
if (file.isFile) {
|
||||
val success = file.delete()
|
||||
if (success) {
|
||||
Ok(null)
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Err(Exception("Could not delete file."))
|
||||
Result.failure(Exception("Could not delete file."))
|
||||
}
|
||||
} else {
|
||||
Err(Exception("Provided reference is not a file."))
|
||||
Result.failure(Exception("Provided reference is not a file."))
|
||||
}
|
||||
}
|
||||
else -> Err(Exception("Can not delete an asset in source '${ref.source}'"))
|
||||
else -> Result.failure(Exception("Can not delete an asset in source '${ref.source}'"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +98,7 @@ class AssetManager private constructor(private val applicationContext: Context)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T: Asset> listAssets(ref: AssetRef, assetClass: Class<T>): Result<Map<AssetRef, T>, Throwable> {
|
||||
fun <T : Asset> listAssets(ref: AssetRef, assetClass: KClass<T>): Result<Map<AssetRef, T>> {
|
||||
val retMap = mutableMapOf<AssetRef, T>()
|
||||
return when (ref.source) {
|
||||
AssetSource.Assets -> {
|
||||
@@ -114,9 +115,9 @@ class AssetManager private constructor(private val applicationContext: Context)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(retMap.toMap())
|
||||
Result.success(retMap.toMap())
|
||||
} catch (e: Exception) {
|
||||
Err(e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
AssetSource.Internal -> {
|
||||
@@ -136,19 +137,19 @@ class AssetManager private constructor(private val applicationContext: Context)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(retMap.toMap())
|
||||
Result.success(retMap.toMap())
|
||||
}
|
||||
else -> Ok(retMap.toMap())
|
||||
else -> Result.success(retMap.toMap())
|
||||
}
|
||||
}
|
||||
|
||||
fun <T: Asset> loadAsset(ref: AssetRef, assetClass: Class<T>): Result<T, Throwable> {
|
||||
fun <T : Asset> loadAsset(ref: AssetRef, assetClass: KClass<T>): Result<T> {
|
||||
val rawJsonData = when (ref.source) {
|
||||
is AssetSource.Assets -> {
|
||||
try {
|
||||
applicationContext.assets.open(ref.path).bufferedReader().use { it.readText() }
|
||||
} catch (e: Exception) {
|
||||
return Err(e)
|
||||
return Result.failure(e)
|
||||
}
|
||||
}
|
||||
is AssetSource.Internal -> {
|
||||
@@ -163,28 +164,67 @@ class AssetManager private constructor(private val applicationContext: Context)
|
||||
else -> "{}"
|
||||
}
|
||||
return try {
|
||||
val adapter = moshi.adapter(assetClass)
|
||||
val adapter = moshi.adapter(assetClass.java)
|
||||
val asset = adapter.fromJson(rawJsonData)
|
||||
if (asset != null) {
|
||||
Ok(asset)
|
||||
Result.success(asset)
|
||||
} else {
|
||||
Err(NullPointerException("Asset failed to load!"))
|
||||
Result.failure(NullPointerException("Asset failed to load!"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Err(e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T: Asset> writeAsset(ref: AssetRef, assetClass: Class<T>, asset: T): Result<Boolean, Throwable> {
|
||||
fun <T : Asset> loadAsset(uri: Uri, assetClass: KClass<T>, maxSize: Int): Result<T> {
|
||||
val rawJsonData = ExternalContentUtils.readTextFromUri(applicationContext, uri, maxSize).getOrElse {
|
||||
return Result.failure(it)
|
||||
}
|
||||
return try {
|
||||
val adapter = moshi.adapter(assetClass.java)
|
||||
val asset = adapter.fromJson(rawJsonData)
|
||||
if (asset != null) {
|
||||
Result.success(asset)
|
||||
} else {
|
||||
Result.failure(NullPointerException("Asset failed to load!"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadAssetRaw(ref: AssetRef): Result<String> {
|
||||
return when (ref.source) {
|
||||
is AssetSource.Assets -> {
|
||||
try {
|
||||
Result.success(applicationContext.assets.open(ref.path).bufferedReader().use { it.readText() })
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
is AssetSource.Internal -> {
|
||||
val file = File(applicationContext.filesDir.absolutePath + "/" + ref.path)
|
||||
val contents = readFile(file)
|
||||
if (contents.isBlank()) {
|
||||
Result.failure(Exception("File is blank!"))
|
||||
} else {
|
||||
Result.success(contents)
|
||||
}
|
||||
}
|
||||
else -> Result.failure(Exception("Unsupported asset ref!"))
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Asset> writeAsset(ref: AssetRef, assetClass: KClass<T>, asset: T): Result<Unit> {
|
||||
return when (ref.source) {
|
||||
AssetSource.Internal -> {
|
||||
val adapter = moshi.adapter(assetClass)
|
||||
val adapter = moshi.adapter(assetClass.java)
|
||||
val rawJson = adapter.toJson(asset)
|
||||
val file = File(applicationContext.filesDir.absolutePath + "/" + ref.path)
|
||||
writeToFile(file, rawJson)
|
||||
Ok(true)
|
||||
Result.success(Unit)
|
||||
}
|
||||
else -> Err(Exception("Can not write an asset in source '${ref.source}'"))
|
||||
else -> Result.failure(Exception("Can not write an asset in source '${ref.source}'"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.extension
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.github.michaelbull.result.getOrElse
|
||||
|
||||
/**
|
||||
* Data class which is a reference to an asset file. It indicates in which storage medium the asset
|
||||
* is as well as the relative path to it.
|
||||
@@ -36,15 +31,15 @@ data class AssetRef(
|
||||
companion object {
|
||||
private const val DELIMITER: String = ":"
|
||||
|
||||
fun fromString(str: String): Result<AssetRef, String> {
|
||||
fun fromString(str: String): Result<AssetRef> {
|
||||
val items = str.split(DELIMITER)
|
||||
if (items.size != 2) {
|
||||
return Err("Unexpected length of given asset ref. Make sure that the asset ref string contains exactly 2 items separated by '$DELIMITER'!")
|
||||
return Result.failure(Exception("Unexpected length of given asset ref. Make sure that the asset ref string contains exactly 2 items separated by '$DELIMITER'!"))
|
||||
}
|
||||
val retSource = AssetSource.fromString(items[0]).getOrElse {
|
||||
return Err(it)
|
||||
return Result.failure(Exception(it))
|
||||
}
|
||||
return Ok(AssetRef(retSource, items[1]))
|
||||
return Result.success(AssetRef(retSource, items[1]))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.extension
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -52,16 +49,16 @@ sealed class AssetSource {
|
||||
companion object {
|
||||
private val externalRegex: Regex = """^external\\(([a-z]+\\.)*[a-z]+\\)\$""".toRegex()
|
||||
|
||||
fun fromString(str: String): Result<AssetSource, String> {
|
||||
fun fromString(str: String): Result<AssetSource> {
|
||||
return when (val string = str.toLowerCase(Locale.ENGLISH)) {
|
||||
"assets" -> Ok(Assets)
|
||||
"internal" -> Ok(Internal)
|
||||
"assets" -> Result.success(Assets)
|
||||
"internal" -> Result.success(Internal)
|
||||
else -> {
|
||||
if (string.matches(externalRegex)) {
|
||||
val packageName = string.substring(9, string.length - 1)
|
||||
Ok(External(packageName))
|
||||
Result.success(External(packageName))
|
||||
} else {
|
||||
Err("'$str' is not a valid AssetSource.")
|
||||
Result.failure(Exception("'$str' is not a valid AssetSource."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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.ime.extension
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
|
||||
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!"))
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
package dev.patrickgold.florisboard.ime.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Handler
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
@@ -25,16 +24,13 @@ import com.google.android.material.tabs.TabLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.EditorInstance
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
|
||||
import dev.patrickgold.florisboard.ime.core.InputView
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyboardView
|
||||
import dev.patrickgold.florisboard.ime.media.emoticon.EmoticonKeyData
|
||||
import dev.patrickgold.florisboard.ime.media.emoticon.EmoticonKeyboardView
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyType
|
||||
import dev.patrickgold.florisboard.util.cancelAll
|
||||
import dev.patrickgold.florisboard.util.postAtScheduledRate
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
@@ -58,11 +54,10 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
|
||||
private var activeTab: Tab? = null
|
||||
private var mediaViewFlipper: ViewFlipper? = null
|
||||
private var repeatedKeyPressHandler: Handler? = null
|
||||
private var tabLayout: TabLayout? = null
|
||||
private val tabViews = EnumMap<Tab, LinearLayout>(Tab::class.java)
|
||||
|
||||
var mediaViewGroup: LinearLayout? = null
|
||||
private var mediaViewGroup: LinearLayout? = null
|
||||
|
||||
companion object {
|
||||
private var instance: MediaInputManager? = null
|
||||
@@ -80,11 +75,6 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
florisboard.addEventListener(this)
|
||||
}
|
||||
|
||||
override fun onCreateInputView() {
|
||||
super.onCreateInputView()
|
||||
repeatedKeyPressHandler = Handler(florisboard.context.mainLooper)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new input view has been registered. Used to initialize all media-relevant
|
||||
* views and layouts.
|
||||
@@ -145,30 +135,21 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
private fun onBottomButtonEvent(view: View, event: MotionEvent?): Boolean {
|
||||
event ?: return false
|
||||
val data = when (view.id) {
|
||||
R.id.media_input_switch_to_text_input_button -> {
|
||||
KeyData(code = KeyCode.SWITCH_TO_TEXT_CONTEXT)
|
||||
}
|
||||
R.id.media_input_backspace_button -> {
|
||||
KeyData(code = KeyCode.DELETE, type = KeyType.ENTER_EDITING)
|
||||
}
|
||||
else -> null
|
||||
R.id.media_input_switch_to_text_input_button -> KeyData.SWITCH_TO_TEXT_CONTEXT
|
||||
R.id.media_input_backspace_button -> KeyData.DELETE
|
||||
else -> return false
|
||||
}
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
florisboard.keyPressVibrate()
|
||||
florisboard.keyPressSound(data)
|
||||
if (data?.code == KeyCode.DELETE && data.type == KeyType.ENTER_EDITING) {
|
||||
val delayMillis = florisboard.prefs.keyboard.longPressDelay.toLong()
|
||||
repeatedKeyPressHandler?.postAtScheduledRate(delayMillis, 25) {
|
||||
florisboard.textInputManager.sendKeyPress(data)
|
||||
}
|
||||
}
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.down(data))
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
repeatedKeyPressHandler?.cancelAll()
|
||||
if (event.actionMasked != MotionEvent.ACTION_CANCEL && data != null) {
|
||||
florisboard.textInputManager.sendKeyPress(data)
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.up(data))
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
}
|
||||
// MUST return false here so the background selector for showing a transparent bg works
|
||||
|
||||
@@ -33,6 +33,8 @@ import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
|
||||
/**
|
||||
* View class for managing the rendering and the events of a single emoji keyboard key.
|
||||
@@ -46,7 +48,7 @@ import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
class EmojiKeyView(
|
||||
private val emojiKeyboardView: EmojiKeyboardView,
|
||||
val data: EmojiKeyData
|
||||
) : androidx.appcompat.widget.AppCompatTextView(emojiKeyboardView.context),
|
||||
) : androidx.appcompat.widget.AppCompatTextView(emojiKeyboardView.context), CoroutineScope by MainScope(),
|
||||
FlorisBoard.EventListener, ThemeManager.OnThemeUpdatedListener {
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.onehanded
|
||||
|
||||
/**
|
||||
* Static object which contains all possible one-handed mode strings.
|
||||
*/
|
||||
object OneHandedMode {
|
||||
const val OFF: String = "off"
|
||||
const val START: String = "start"
|
||||
const val END: String = "end"
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.onehanded
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
|
||||
class OneHandedPanel : LinearLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
private var florisboard: FlorisBoard? = null
|
||||
private var themeManager: ThemeManager? = null
|
||||
|
||||
private var closeBtn: ImageButton? = null
|
||||
private var moveBtn: ImageButton? = null
|
||||
|
||||
private val panelSide: String
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
context.obtainStyledAttributes(attrs, R.styleable.OneHandedPanel).apply {
|
||||
panelSide = getString(R.styleable.OneHandedPanel_panelSide) ?: OneHandedMode.START
|
||||
recycle()
|
||||
}
|
||||
orientation = VERTICAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
florisboard = FlorisBoard.getInstanceOrNull()
|
||||
themeManager = ThemeManager.defaultOrNull()
|
||||
|
||||
closeBtn = findViewWithTag("one_handed_ctrl_close")
|
||||
closeBtn?.setOnClickListener {
|
||||
florisboard?.let {
|
||||
it.prefs.keyboard.oneHandedMode = OneHandedMode.OFF
|
||||
it.updateOneHandedPanelVisibility()
|
||||
}
|
||||
}
|
||||
moveBtn = findViewWithTag("one_handed_ctrl_move")
|
||||
moveBtn?.setOnClickListener {
|
||||
florisboard?.let {
|
||||
it.prefs.keyboard.oneHandedMode = panelSide
|
||||
it.updateOneHandedPanelVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
themeManager?.registerOnThemeUpdatedListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
florisboard = null
|
||||
themeManager?.unregisterOnThemeUpdatedListener(this)
|
||||
themeManager = null
|
||||
|
||||
closeBtn?.setOnClickListener(null)
|
||||
closeBtn = null
|
||||
moveBtn?.setOnClickListener(null)
|
||||
moveBtn = null
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
setBackgroundColor(theme.getAttr(Theme.Attr.ONE_HANDED_BACKGROUND).toSolidColor().color)
|
||||
ColorStateList.valueOf(theme.getAttr(Theme.Attr.ONE_HANDED_FOREGROUND).toSolidColor().color).also {
|
||||
closeBtn?.imageTintList = it
|
||||
moveBtn?.imageTintList = it
|
||||
}
|
||||
closeBtn?.invalidate()
|
||||
moveBtn?.invalidate()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val florisboard = florisboard ?: return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
val width = (florisboard.inputView?.measuredWidth ?: 0) *
|
||||
((100 - florisboard.prefs.keyboard.oneHandedModeScaleFactor) / 100.0f)
|
||||
super.onMeasure(MeasureSpec.makeMeasureSpec(width.toInt(), MeasureSpec.EXACTLY), heightMeasureSpec)
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,15 @@ package dev.patrickgold.florisboard.ime.popup
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import dev.patrickgold.florisboard.ime.clip.ClipboardPopupManager
|
||||
import dev.patrickgold.florisboard.ime.clip.ClipboardPopupView
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Basic helper view class which acts as a non-interactive layer view, which sits above the whole
|
||||
@@ -41,12 +46,29 @@ class PopupLayerView : FrameLayout {
|
||||
)
|
||||
}
|
||||
|
||||
var clipboardPopupManager: ClipboardPopupManager? = null
|
||||
var intercept: ClipboardPopupView? = null
|
||||
var shouldIntercept = true
|
||||
|
||||
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
|
||||
if (ev != null) {
|
||||
intercept?.run {
|
||||
val viewRect = Rect()
|
||||
getGlobalVisibleRect(viewRect)
|
||||
return when {
|
||||
!viewRect.contains(ev.x.toInt(), ev.y.toInt()) -> {
|
||||
clipboardPopupManager?.hide()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
override fun onTouchEvent(ev: MotionEvent?): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ class PopupManager<T_KBD: View, T_KV: View>(
|
||||
KeyCode.ENTER,
|
||||
KeyCode.LANGUAGE_SWITCH,
|
||||
KeyCode.SWITCH_TO_TEXT_CONTEXT,
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT,
|
||||
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT
|
||||
)
|
||||
private var keyPopupWidth: Int
|
||||
private var keyPopupHeight: Int
|
||||
@@ -108,6 +109,11 @@ class PopupManager<T_KBD: View, T_KV: View>(
|
||||
PopupExtendedView.Element.Icon(it, adjustedIndex)
|
||||
} ?: PopupExtendedView.Element.Undefined
|
||||
}
|
||||
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT -> {
|
||||
getDrawable(keyView.context, R.drawable.ic_assignment)?.let {
|
||||
PopupExtendedView.Element.Icon(it, adjustedIndex)
|
||||
} ?: PopupExtendedView.Element.Undefined
|
||||
}
|
||||
KeyCode.URI_COMPONENT_TLD -> {
|
||||
PopupExtendedView.Element.Tld(
|
||||
keyView.data.popup[adjustedIndex].label, adjustedIndex
|
||||
@@ -146,10 +152,10 @@ class PopupManager<T_KBD: View, T_KV: View>(
|
||||
when (keyboardView.resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||
if (keyboardView.isSmartbarKeyboardView) {
|
||||
keyPopupWidth = (keyView.measuredWidth * 0.6f).toInt()
|
||||
keyPopupWidth = (keyView.measuredWidth * 1.0f).toInt()
|
||||
keyPopupHeight = (keyboardView.desiredKeyHeight * 3.0f * 1.2f).toInt()
|
||||
} else {
|
||||
keyPopupWidth = (keyboardView.desiredKeyWidth * 0.6f).toInt()
|
||||
keyPopupWidth = (keyboardView.desiredKeyWidth * 1.0f).toInt()
|
||||
keyPopupHeight = (keyboardView.desiredKeyHeight * 3.0f).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,13 @@ package dev.patrickgold.florisboard.ime.text
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.os.Handler
|
||||
import android.view.KeyEvent
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import android.widget.ViewFlipper
|
||||
import com.github.michaelbull.result.getOr
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
|
||||
import dev.patrickgold.florisboard.ime.core.*
|
||||
import dev.patrickgold.florisboard.ime.dictionary.Dictionary
|
||||
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
|
||||
@@ -56,7 +55,7 @@ import kotlin.math.roundToLong
|
||||
* TextInputManager is also the hub in the communication between the system, the active editor
|
||||
* instance and the Smartbar.
|
||||
*/
|
||||
class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
class TextInputManager private constructor() : CoroutineScope by MainScope(), InputKeyEventReceiver,
|
||||
FlorisBoard.EventListener, SmartbarView.EventListener {
|
||||
|
||||
private val florisboard = FlorisBoard.getInstance()
|
||||
@@ -68,28 +67,37 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
private val keyboardViews = EnumMap<KeyboardMode, KeyboardView>(KeyboardMode::class.java)
|
||||
private var editingKeyboardView: EditingKeyboardView? = null
|
||||
private var loadingPlaceholderKeyboard: KeyboardView? = null
|
||||
private val osHandler = Handler()
|
||||
private var textViewFlipper: ViewFlipper? = null
|
||||
private var textViewGroup: LinearLayout? = null
|
||||
private val dictionaryManager: DictionaryManager = DictionaryManager.default()
|
||||
private var activeDictionary: Dictionary<String, Int>? = null
|
||||
val inputEventDispatcher: InputEventDispatcher = InputEventDispatcher.new(
|
||||
parentScope = this,
|
||||
repeatableKeyCodes = intArrayOf(
|
||||
KeyCode.ARROW_DOWN,
|
||||
KeyCode.ARROW_LEFT,
|
||||
KeyCode.ARROW_RIGHT,
|
||||
KeyCode.ARROW_UP,
|
||||
KeyCode.DELETE,
|
||||
KeyCode.FORWARD_DELETE
|
||||
)
|
||||
)
|
||||
|
||||
var keyVariation: KeyVariation = KeyVariation.NORMAL
|
||||
val layoutManager = LayoutManager(florisboard)
|
||||
private var smartbarView: SmartbarView? = null
|
||||
|
||||
// Caps/Space related properties
|
||||
// Caps/Shift related properties
|
||||
var caps: Boolean = false
|
||||
private set
|
||||
var capsLock: Boolean = false
|
||||
private set
|
||||
private var hasCapsRecentlyChanged: Boolean = false
|
||||
private var hasSpaceRecentlyPressed: Boolean = false
|
||||
private var newCapsState: Boolean = false
|
||||
|
||||
// Composing text related properties
|
||||
var isManualSelectionMode: Boolean = false
|
||||
private var isManualSelectionModeLeft: Boolean = false
|
||||
private var isManualSelectionModeRight: Boolean = false
|
||||
private var isManualSelectionModeStart: Boolean = false
|
||||
private var isManualSelectionModeEnd: Boolean = false
|
||||
|
||||
companion object {
|
||||
private var instance: TextInputManager? = null
|
||||
@@ -114,6 +122,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
override fun onCreate() {
|
||||
Timber.i("onCreate()")
|
||||
|
||||
inputEventDispatcher.keyEventReceiver = this
|
||||
var subtypes = florisboard.subtypeManager.subtypes
|
||||
if (subtypes.isEmpty()) {
|
||||
subtypes = listOf(Subtype.DEFAULT)
|
||||
@@ -199,8 +208,9 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
override fun onDestroy() {
|
||||
Timber.i("onDestroy()")
|
||||
|
||||
inputEventDispatcher.keyEventReceiver = null
|
||||
inputEventDispatcher.close()
|
||||
cancel()
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
layoutManager.onDestroy()
|
||||
instance = null
|
||||
}
|
||||
@@ -263,6 +273,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
updateCapsState()
|
||||
setActiveKeyboardMode(keyboardMode)
|
||||
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
@@ -300,8 +311,8 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
keyboardViews[mode]?.requestLayoutAllKeys()
|
||||
activeKeyboardMode = mode
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = false
|
||||
smartbarView?.isQuickActionsVisible = false
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
@@ -311,7 +322,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
if (activeEditorInstance.isComposingEnabled) {
|
||||
withContext(Dispatchers.IO) {
|
||||
dictionaryManager.loadDictionary(AssetRef(AssetSource.Assets,"ime/dict/en.flict")).let {
|
||||
activeDictionary = it.getOr(null)
|
||||
activeDictionary = it.getOrDefault(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,17 +337,14 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* and passing this info on to the [SmartbarView] to turn it into candidate suggestions.
|
||||
*/
|
||||
override fun onUpdateSelection() {
|
||||
if (!activeEditorInstance.isNewSelectionInBoundsOfOld) {
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
if (!inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
|
||||
updateCapsState()
|
||||
}
|
||||
updateCapsState()
|
||||
smartbarView?.updateSmartbarState()
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.i("current word: ${activeEditorInstance.cachedInput.currentWord.text}")
|
||||
}
|
||||
if (activeEditorInstance.isComposingEnabled) {
|
||||
if (activeEditorInstance.isComposingEnabled && !inputEventDispatcher.isPressed(KeyCode.DELETE)) {
|
||||
if (activeEditorInstance.shouldReevaluateComposingSuggestions) {
|
||||
activeEditorInstance.shouldReevaluateComposingSuggestions = false
|
||||
activeDictionary?.let {
|
||||
@@ -345,7 +353,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
val suggestions = it.getTokenPredictions(
|
||||
precedingTokens = listOf(),
|
||||
currentToken = Token(activeEditorInstance.cachedInput.currentWord.text),
|
||||
maxSuggestionCount = 3,
|
||||
maxSuggestionCount = 16,
|
||||
allowPossiblyOffensive = !florisboard.prefs.suggestion.blockPossiblyOffensive
|
||||
).toStringList()
|
||||
if (BuildConfig.DEBUG) {
|
||||
@@ -359,7 +367,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), listOf())
|
||||
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,9 +384,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
if (!capsLock) {
|
||||
caps = florisboard.prefs.correction.autoCapitalization &&
|
||||
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
|
||||
launch(Dispatchers.Main) {
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
}
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,22 +393,24 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* class.
|
||||
*/
|
||||
fun executeSwipeAction(swipeAction: SwipeAction) {
|
||||
when (swipeAction) {
|
||||
SwipeAction.DELETE_WORD -> handleDeleteWord()
|
||||
SwipeAction.INSERT_SPACE -> handleSpace()
|
||||
SwipeAction.MOVE_CURSOR_DOWN -> handleArrow(KeyCode.ARROW_DOWN)
|
||||
SwipeAction.MOVE_CURSOR_UP -> handleArrow(KeyCode.ARROW_UP)
|
||||
SwipeAction.MOVE_CURSOR_LEFT -> handleArrow(KeyCode.ARROW_LEFT)
|
||||
SwipeAction.MOVE_CURSOR_RIGHT -> handleArrow(KeyCode.ARROW_RIGHT)
|
||||
SwipeAction.MOVE_CURSOR_START_OF_LINE -> handleArrow(KeyCode.MOVE_START_OF_LINE)
|
||||
SwipeAction.MOVE_CURSOR_END_OF_LINE -> handleArrow(KeyCode.MOVE_END_OF_LINE)
|
||||
SwipeAction.MOVE_CURSOR_START_OF_PAGE -> handleArrow(KeyCode.MOVE_START_OF_PAGE)
|
||||
SwipeAction.MOVE_CURSOR_END_OF_PAGE -> handleArrow(KeyCode.MOVE_END_OF_PAGE)
|
||||
SwipeAction.SHIFT -> handleShift()
|
||||
SwipeAction.SHOW_INPUT_METHOD_PICKER -> sendKeyPress(
|
||||
KeyData(type = KeyType.FUNCTION, code = KeyCode.SHOW_INPUT_METHOD_PICKER)
|
||||
)
|
||||
else -> {}
|
||||
val keyData = when (swipeAction) {
|
||||
SwipeAction.DELETE_WORD -> KeyData.DELETE_WORD
|
||||
SwipeAction.INSERT_SPACE -> KeyData.SPACE
|
||||
SwipeAction.MOVE_CURSOR_DOWN -> KeyData.ARROW_DOWN
|
||||
SwipeAction.MOVE_CURSOR_UP -> KeyData.ARROW_UP
|
||||
SwipeAction.MOVE_CURSOR_LEFT -> KeyData.ARROW_LEFT
|
||||
SwipeAction.MOVE_CURSOR_RIGHT -> KeyData.ARROW_RIGHT
|
||||
SwipeAction.MOVE_CURSOR_START_OF_LINE -> KeyData.MOVE_START_OF_LINE
|
||||
SwipeAction.MOVE_CURSOR_END_OF_LINE -> KeyData.MOVE_END_OF_LINE
|
||||
SwipeAction.MOVE_CURSOR_START_OF_PAGE -> KeyData.MOVE_START_OF_PAGE
|
||||
SwipeAction.MOVE_CURSOR_END_OF_PAGE -> KeyData.MOVE_END_OF_PAGE
|
||||
SwipeAction.SHIFT -> KeyData.SHIFT
|
||||
SwipeAction.SWITCH_TO_CLIPBOARD_CONTEXT -> KeyData.SWITCH_TO_CLIPBOARD_CONTEXT
|
||||
SwipeAction.SHOW_INPUT_METHOD_PICKER -> KeyData.SHOW_INPUT_METHOD_PICKER
|
||||
else -> null
|
||||
}
|
||||
if (keyData != null) {
|
||||
inputEventDispatcher.send(InputKeyEvent.downUp(keyData))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,6 +422,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
activeEditorInstance.commitCompletion(word)
|
||||
}
|
||||
|
||||
override fun onSmartbarClipboardCandidatePressed(clipboardItem: ClipboardItem) {
|
||||
activeEditorInstance.commitClipboardItem(clipboardItem)
|
||||
}
|
||||
|
||||
override fun onSmartbarPrivateModeButtonClicked() {
|
||||
Toast.makeText(florisboard.context, R.string.private_mode_dialog__title, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
@@ -431,11 +443,11 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
R.id.quick_action_open_settings -> florisboard.launchSettings()
|
||||
R.id.quick_action_one_handed_toggle -> florisboard.toggleOneHandedMode(isRight = true)
|
||||
R.id.quick_action_undo -> {
|
||||
handleUndo()
|
||||
activeEditorInstance.performUndo()
|
||||
return
|
||||
}
|
||||
R.id.quick_action_redo -> {
|
||||
handleRedo()
|
||||
activeEditorInstance.performRedo()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -443,23 +455,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
private fun handleUndo(){
|
||||
activeEditorInstance.performUndo()
|
||||
}
|
||||
|
||||
private fun handleRedo(){
|
||||
activeEditorInstance.performRedo()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.DELETE] event.
|
||||
*/
|
||||
private fun handleDelete() {
|
||||
hasCapsRecentlyChanged = false
|
||||
hasSpaceRecentlyPressed = false
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = false
|
||||
activeEditorInstance.deleteBackwards()
|
||||
}
|
||||
|
||||
@@ -467,11 +469,9 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Handles a [KeyCode.DELETE_WORD] event.
|
||||
*/
|
||||
private fun handleDeleteWord() {
|
||||
hasCapsRecentlyChanged = false
|
||||
hasSpaceRecentlyPressed = false
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = false
|
||||
activeEditorInstance.deleteWordsBeforeCursor(1)
|
||||
}
|
||||
|
||||
@@ -479,8 +479,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Handles a [KeyCode.ENTER] event.
|
||||
*/
|
||||
private fun handleEnter() {
|
||||
hasCapsRecentlyChanged = false
|
||||
hasSpaceRecentlyPressed = false
|
||||
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
|
||||
activeEditorInstance.performEnter()
|
||||
} else {
|
||||
@@ -511,45 +509,70 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] event.
|
||||
* Handles a [KeyCode.SHIFT] down event.
|
||||
*/
|
||||
private fun handleShift() {
|
||||
if (hasCapsRecentlyChanged) {
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
private fun handleShiftDown(ev: InputKeyEvent) {
|
||||
if (ev.isConsecutiveEventOf(inputEventDispatcher.lastKeyEventDown, florisboard.prefs.keyboard.longPressDelay.toLong())) {
|
||||
newCapsState = true
|
||||
caps = true
|
||||
capsLock = true
|
||||
hasCapsRecentlyChanged = false
|
||||
} else {
|
||||
caps = !caps
|
||||
newCapsState = !caps
|
||||
caps = true
|
||||
capsLock = false
|
||||
hasCapsRecentlyChanged = true
|
||||
osHandler.postDelayed({
|
||||
hasCapsRecentlyChanged = false
|
||||
}, 300)
|
||||
}
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] up event.
|
||||
*/
|
||||
private fun handleShiftUp() {
|
||||
caps = newCapsState
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] cancel event.
|
||||
*/
|
||||
private fun handleShiftCancel() {
|
||||
caps = false
|
||||
capsLock = false
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] up event.
|
||||
*/
|
||||
private fun handleShiftLock() {
|
||||
val lastKeyEvent = inputEventDispatcher.lastKeyEventDown ?: return
|
||||
if (lastKeyEvent.data.code == KeyCode.SHIFT && lastKeyEvent.action == InputKeyEvent.Action.DOWN) {
|
||||
newCapsState = true
|
||||
caps = true
|
||||
capsLock = true
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SPACE] event. Also handles the auto-correction of two space taps if
|
||||
* enabled by the user.
|
||||
*/
|
||||
private fun handleSpace() {
|
||||
private fun handleSpace(ev: InputKeyEvent) {
|
||||
if (florisboard.prefs.keyboard.spaceBarSwitchesToCharacters && getActiveKeyboardMode() != KeyboardMode.CHARACTERS) {
|
||||
setActiveKeyboardMode(KeyboardMode.CHARACTERS)
|
||||
}
|
||||
if (florisboard.prefs.correction.doubleSpacePeriod) {
|
||||
if (hasSpaceRecentlyPressed) {
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
if (ev.isConsecutiveEventOf(inputEventDispatcher.lastKeyEventUp, florisboard.prefs.keyboard.longPressDelay.toLong())) {
|
||||
val text = activeEditorInstance.getTextBeforeCursor(2)
|
||||
if (text.length == 2 && !text.matches("""[.!?‽\s][\s]""".toRegex())) {
|
||||
activeEditorInstance.deleteBackwards()
|
||||
activeEditorInstance.commitText(".")
|
||||
}
|
||||
hasSpaceRecentlyPressed = false
|
||||
} else {
|
||||
hasSpaceRecentlyPressed = true
|
||||
osHandler.postDelayed({
|
||||
hasSpaceRecentlyPressed = false
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
activeEditorInstance.commitText(KeyCode.SPACE.toChar().toString())
|
||||
@@ -558,119 +581,66 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
/**
|
||||
* Handles [KeyCode] arrow and move events, behaves differently depending on text selection.
|
||||
*/
|
||||
private fun handleArrow(code: Int) = activeEditorInstance.run {
|
||||
val selectionStartMin = 0
|
||||
val selectionEndMax = cachedInput.expectedMaxLength
|
||||
if (selection.isSelectionMode && isManualSelectionMode) {
|
||||
// Text is selected and it is manual selection -> Expand selection depending on started
|
||||
// direction.
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
if (isManualSelectionModeLeft) {
|
||||
selection.updateAndNotify(
|
||||
(selection.start - 1).coerceAtLeast(selectionStartMin),
|
||||
selection.end
|
||||
)
|
||||
} else {
|
||||
selection.updateAndNotify(selection.start, selection.end - 1)
|
||||
}
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
if (isManualSelectionModeRight) {
|
||||
selection.updateAndNotify(
|
||||
selection.start,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
} else {
|
||||
selection.updateAndNotify(selection.start + 1, selection.end)
|
||||
}
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_START_OF_LINE -> {
|
||||
if (isManualSelectionModeLeft) {
|
||||
selection.updateAndNotify(selectionStartMin, selection.end)
|
||||
} else {
|
||||
selection.updateAndNotify(selectionStartMin, selection.start)
|
||||
}
|
||||
}
|
||||
KeyCode.MOVE_END_OF_LINE -> {
|
||||
if (isManualSelectionModeRight) {
|
||||
selection.updateAndNotify(selection.start, selectionEndMax)
|
||||
} else {
|
||||
selection.updateAndNotify(selection.end, selectionEndMax)
|
||||
}
|
||||
private fun handleArrow(code: Int, count: Int) = activeEditorInstance.apply {
|
||||
val isShiftPressed = isManualSelectionMode || inputEventDispatcher.isPressed(KeyCode.SHIFT)
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = true
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, meta(shift = isShiftPressed), count)
|
||||
}
|
||||
} else if (selection.isSelectionMode && !isManualSelectionMode) {
|
||||
// Text is selected but no manual selection mode -> arrows behave as if selection was
|
||||
// started in manual left mode
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
selection.updateAndNotify(selection.start, selection.end - 1)
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
selection.updateAndNotify(
|
||||
selection.start,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_START_OF_LINE -> {
|
||||
selection.updateAndNotify(selectionStartMin, selection.start)
|
||||
}
|
||||
KeyCode.MOVE_END_OF_LINE -> {
|
||||
selection.updateAndNotify(selection.start, selectionEndMax)
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = true
|
||||
isManualSelectionModeEnd = false
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, meta(shift = isShiftPressed), count)
|
||||
}
|
||||
} else if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
// No text is selected but manual selection mode is active, user wants to start a new
|
||||
// selection. Must set manual selection direction.
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
selection.updateAndNotify(
|
||||
(selection.start - 1).coerceAtLeast(selectionStartMin),
|
||||
selection.start
|
||||
)
|
||||
isManualSelectionModeLeft = true
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
selection.updateAndNotify(
|
||||
selection.end,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = true
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_START_OF_LINE -> {
|
||||
selection.updateAndNotify(selectionStartMin, selection.start)
|
||||
isManualSelectionModeLeft = true
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
KeyCode.MOVE_END_OF_LINE -> {
|
||||
selection.updateAndNotify(selection.end, selectionEndMax)
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = true
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = true
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, meta(shift = isShiftPressed), count)
|
||||
}
|
||||
} else {
|
||||
// No selection and no manual selection mode -> move cursor around
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
KeyCode.ARROW_LEFT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT)
|
||||
KeyCode.ARROW_RIGHT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT)
|
||||
KeyCode.ARROW_UP -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_UP)
|
||||
KeyCode.MOVE_START_OF_PAGE -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_UP)
|
||||
KeyCode.MOVE_END_OF_PAGE -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
KeyCode.MOVE_START_OF_LINE -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_LEFT)
|
||||
KeyCode.MOVE_END_OF_LINE -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_RIGHT)
|
||||
KeyCode.ARROW_UP -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = true
|
||||
isManualSelectionModeEnd = false
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_UP, meta(shift = isShiftPressed), count)
|
||||
}
|
||||
KeyCode.MOVE_START_OF_PAGE -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = true
|
||||
isManualSelectionModeEnd = false
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_UP, meta(alt = true, shift = isShiftPressed), count)
|
||||
}
|
||||
KeyCode.MOVE_END_OF_PAGE -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = true
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, meta(alt = true, shift = isShiftPressed), count)
|
||||
}
|
||||
KeyCode.MOVE_START_OF_LINE -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = true
|
||||
isManualSelectionModeEnd = false
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, meta(alt = true, shift = isShiftPressed), count)
|
||||
}
|
||||
KeyCode.MOVE_END_OF_LINE -> {
|
||||
if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = true
|
||||
}
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, meta(alt = true, shift = isShiftPressed), count)
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -678,7 +648,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
*/
|
||||
private fun handleClipboardSelect() = activeEditorInstance.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
if (isManualSelectionMode && isManualSelectionModeLeft) {
|
||||
if (isManualSelectionMode && isManualSelectionModeStart) {
|
||||
selection.updateAndNotify(selection.start, selection.start)
|
||||
} else {
|
||||
selection.updateAndNotify(selection.end, selection.end)
|
||||
@@ -692,14 +662,28 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Main logic point for sending a key press. Different actions may occur depending on the given
|
||||
* [KeyData]. This method handles all key press send events, which are text based. For media
|
||||
* input send events see MediaInputManager.
|
||||
*
|
||||
* @param keyData The [KeyData] object which should be sent.
|
||||
* Adjusts a given key data for caps state and returns the correct reference.
|
||||
*/
|
||||
fun sendKeyPress(keyData: KeyData) {
|
||||
when (keyData.code) {
|
||||
private fun getAdjustedKeyData(keyData: KeyData): KeyData {
|
||||
return if (caps && keyData is FlorisKeyData && keyData.shift != null) { keyData.shift!! } else { keyData }
|
||||
}
|
||||
|
||||
override fun onInputKeyDown(ev: InputKeyEvent) {
|
||||
val data = getAdjustedKeyData(ev.data)
|
||||
when (data.code) {
|
||||
KeyCode.INTERNAL_BATCH_EDIT -> {
|
||||
florisboard.beginInternalBatchEdit()
|
||||
return
|
||||
}
|
||||
KeyCode.SHIFT -> {
|
||||
handleShiftDown(ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInputKeyUp(ev: InputKeyEvent) {
|
||||
val data = getAdjustedKeyData(ev.data)
|
||||
when (data.code) {
|
||||
KeyCode.ARROW_DOWN,
|
||||
KeyCode.ARROW_LEFT,
|
||||
KeyCode.ARROW_RIGHT,
|
||||
@@ -707,29 +691,33 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
KeyCode.MOVE_START_OF_PAGE,
|
||||
KeyCode.MOVE_END_OF_PAGE,
|
||||
KeyCode.MOVE_START_OF_LINE,
|
||||
KeyCode.MOVE_END_OF_LINE -> handleArrow(keyData.code)
|
||||
KeyCode.MOVE_END_OF_LINE -> if (ev.action == InputKeyEvent.Action.DOWN_UP || ev.action == InputKeyEvent.Action.REPEAT) {
|
||||
handleArrow(data.code, ev.count)
|
||||
} else {
|
||||
handleArrow(data.code, 1)
|
||||
}
|
||||
KeyCode.CLIPBOARD_CUT -> activeEditorInstance.performClipboardCut()
|
||||
KeyCode.CLIPBOARD_COPY -> activeEditorInstance.performClipboardCopy()
|
||||
KeyCode.CLIPBOARD_PASTE -> {
|
||||
activeEditorInstance.performClipboardPaste()
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
KeyCode.CLIPBOARD_PASTE -> activeEditorInstance.performClipboardPaste()
|
||||
KeyCode.CLIPBOARD_SELECT -> handleClipboardSelect()
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> activeEditorInstance.performClipboardSelectAll()
|
||||
KeyCode.DELETE -> {
|
||||
handleDelete()
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
KeyCode.ENTER -> {
|
||||
handleEnter()
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
KeyCode.DELETE -> handleDelete()
|
||||
KeyCode.DELETE_WORD -> handleDeleteWord()
|
||||
KeyCode.ENTER -> handleEnter()
|
||||
KeyCode.INTERNAL_BATCH_EDIT -> {
|
||||
florisboard.endInternalBatchEdit()
|
||||
return
|
||||
}
|
||||
KeyCode.LANGUAGE_SWITCH -> handleLanguageSwitch()
|
||||
KeyCode.SETTINGS -> florisboard.launchSettings()
|
||||
KeyCode.SHIFT -> handleShift()
|
||||
KeyCode.SHIFT -> handleShiftUp()
|
||||
KeyCode.SHIFT_LOCK -> handleShiftLock()
|
||||
KeyCode.SHOW_INPUT_METHOD_PICKER -> florisboard.imeManager?.showInputMethodPicker()
|
||||
KeyCode.SPACE -> handleSpace(ev)
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> florisboard.setActiveInput(R.id.media_input)
|
||||
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT -> florisboard.setActiveInput(R.id.clip_input)
|
||||
KeyCode.SWITCH_TO_TEXT_CONTEXT -> florisboard.setActiveInput(R.id.text_input)
|
||||
KeyCode.CLEAR_CLIPBOARD_HISTORY -> florisboard.florisClipboardManager?.clearHistoryWithAnimation()
|
||||
KeyCode.TOGGLE_ONE_HANDED_MODE_LEFT -> florisboard.toggleOneHandedMode(isRight = false)
|
||||
KeyCode.TOGGLE_ONE_HANDED_MODE_RIGHT -> florisboard.toggleOneHandedMode(isRight = true)
|
||||
KeyCode.VIEW_CHARACTERS -> setActiveKeyboardMode(KeyboardMode.CHARACTERS)
|
||||
@@ -744,49 +732,57 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.NUMERIC_ADVANCED,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> when (keyData.type) {
|
||||
KeyboardMode.PHONE2 -> when (data.type) {
|
||||
KeyType.CHARACTER,
|
||||
KeyType.NUMERIC -> {
|
||||
val text = keyData.code.toChar().toString()
|
||||
val text = data.code.toChar().toString()
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
else -> when (keyData.code) {
|
||||
else -> when (data.code) {
|
||||
KeyCode.PHONE_PAUSE,
|
||||
KeyCode.PHONE_WAIT -> {
|
||||
val text = keyData.code.toChar().toString()
|
||||
val text = data.code.toChar().toString()
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> when (keyData.type) {
|
||||
KeyType.CHARACTER, KeyType.NUMERIC -> when (keyData.code) {
|
||||
KeyCode.SPACE -> handleSpace()
|
||||
else -> when (data.type) {
|
||||
KeyType.CHARACTER, KeyType.NUMERIC -> when (data.code) {
|
||||
KeyCode.URI_COMPONENT_TLD -> {
|
||||
val tld = keyData.label.toLowerCase(Locale.ENGLISH)
|
||||
val tld = data.label.toLowerCase(Locale.ENGLISH)
|
||||
activeEditorInstance.commitText(tld)
|
||||
}
|
||||
else -> {
|
||||
hasCapsRecentlyChanged = false
|
||||
hasSpaceRecentlyPressed = false
|
||||
var text = keyData.code.toChar().toString()
|
||||
var text = data.code.toChar().toString()
|
||||
val locale = if (florisboard.activeSubtype.locale.language == "el") { Locale.getDefault() } else { florisboard.activeSubtype.locale }
|
||||
text = when (caps && activeKeyboardMode == KeyboardMode.CHARACTERS) {
|
||||
true -> text.toUpperCase(florisboard.activeSubtype.locale)
|
||||
true -> text.toUpperCase(locale)
|
||||
false -> text
|
||||
}
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Timber.e("sendKeyPress(keyData): Received unknown key: $keyData")
|
||||
Timber.e("sendKeyPress(keyData): Received unknown key: $data")
|
||||
}
|
||||
}
|
||||
}
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
}
|
||||
if (keyData.code != KeyCode.SHIFT && !capsLock) {
|
||||
if (data.code != KeyCode.SHIFT && !capsLock && !inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
|
||||
updateCapsState()
|
||||
}
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
override fun onInputKeyRepeat(ev: InputKeyEvent) {
|
||||
onInputKeyUp(ev)
|
||||
}
|
||||
|
||||
override fun onInputKeyCancel(ev: InputKeyEvent) {
|
||||
val data = getAdjustedKeyData(ev.data)
|
||||
when (data.code) {
|
||||
KeyCode.SHIFT -> handleShiftCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import android.widget.Button
|
||||
import androidx.appcompat.widget.AppCompatImageButton
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
@@ -46,10 +47,25 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
private val data: KeyData
|
||||
private val data: KeyData = when (id) {
|
||||
R.id.arrow_down -> KeyData.ARROW_DOWN
|
||||
R.id.arrow_left -> KeyData.ARROW_LEFT
|
||||
R.id.arrow_right -> KeyData.ARROW_RIGHT
|
||||
R.id.arrow_up -> KeyData.ARROW_UP
|
||||
R.id.backspace -> KeyData.DELETE
|
||||
R.id.clipboard_copy -> KeyData.CLIPBOARD_COPY
|
||||
R.id.clipboard_cut -> KeyData.CLIPBOARD_CUT
|
||||
R.id.clipboard_paste -> KeyData.CLIPBOARD_PASTE
|
||||
R.id.move_start_of_line -> KeyData.MOVE_START_OF_LINE
|
||||
R.id.move_end_of_line -> KeyData.MOVE_END_OF_LINE
|
||||
R.id.select -> KeyData.CLIPBOARD_SELECT
|
||||
R.id.select_all -> KeyData.CLIPBOARD_SELECT_ALL
|
||||
else -> KeyData.UNSPECIFIED
|
||||
}
|
||||
private var isKeyPressed: Boolean = false
|
||||
private val repeatedKeyPressHandler: Handler = Handler(context.mainLooper)
|
||||
|
||||
private val defaultTextSize: Float = Button(context).textSize
|
||||
private var label: String? = null
|
||||
private var labelPaint: Paint = Paint().apply {
|
||||
alpha = 255
|
||||
@@ -57,7 +73,7 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
|
||||
isAntiAlias = true
|
||||
isFakeBoldText = false
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = Button(context).textSize
|
||||
textSize = defaultTextSize
|
||||
typeface = Typeface.DEFAULT
|
||||
}
|
||||
|
||||
@@ -71,22 +87,6 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, R.style.TextEditingButton)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
val code = when (id) {
|
||||
R.id.arrow_down -> KeyCode.ARROW_DOWN
|
||||
R.id.arrow_left -> KeyCode.ARROW_LEFT
|
||||
R.id.arrow_right -> KeyCode.ARROW_RIGHT
|
||||
R.id.arrow_up -> KeyCode.ARROW_UP
|
||||
R.id.backspace -> KeyCode.DELETE
|
||||
R.id.clipboard_copy -> KeyCode.CLIPBOARD_COPY
|
||||
R.id.clipboard_cut -> KeyCode.CLIPBOARD_CUT
|
||||
R.id.clipboard_paste -> KeyCode.CLIPBOARD_PASTE
|
||||
R.id.move_start_of_line -> KeyCode.MOVE_START_OF_LINE
|
||||
R.id.move_end_of_line -> KeyCode.MOVE_END_OF_LINE
|
||||
R.id.select -> KeyCode.CLIPBOARD_SELECT
|
||||
R.id.select_all -> KeyCode.CLIPBOARD_SELECT_ALL
|
||||
else -> 0
|
||||
}
|
||||
data = KeyData(code = code)
|
||||
context.obtainStyledAttributes(attrs, R.styleable.EditingKeyView).apply {
|
||||
label = getString(R.styleable.EditingKeyView_android_text)
|
||||
recycle()
|
||||
@@ -123,7 +123,7 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
|
||||
val delayMillis = prefs.keyboard.longPressDelay.toLong()
|
||||
repeatedKeyPressHandler.postAtScheduledRate(delayMillis, 25) {
|
||||
if (isKeyPressed) {
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.downUp(data))
|
||||
} else {
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
}
|
||||
@@ -135,7 +135,7 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
|
||||
isKeyPressed = false
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
if (event.actionMasked != MotionEvent.ACTION_CANCEL) {
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.downUp(data))
|
||||
}
|
||||
}
|
||||
else -> return false
|
||||
@@ -173,8 +173,10 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
|
||||
}
|
||||
val isPortrait =
|
||||
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
if (!isPortrait) {
|
||||
labelPaint.textSize *= 0.9f
|
||||
labelPaint.textSize = if (isPortrait) {
|
||||
defaultTextSize
|
||||
} else {
|
||||
defaultTextSize * 0.9f
|
||||
}
|
||||
val centerX = measuredWidth / 2.0f
|
||||
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2
|
||||
|
||||
@@ -35,8 +35,6 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
|
||||
private var arrowUpKey: EditingKeyView? = null
|
||||
private var arrowDownKey: EditingKeyView? = null
|
||||
private var selectKey: EditingKeyView? = null
|
||||
private var selectAllKey: EditingKeyView? = null
|
||||
private var cutKey: EditingKeyView? = null
|
||||
@@ -52,8 +50,6 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
|
||||
florisboard?.addEventListener(this)
|
||||
themeManager.registerOnThemeUpdatedListener(this)
|
||||
|
||||
arrowUpKey = findViewById(R.id.arrow_up)
|
||||
arrowDownKey = findViewById(R.id.arrow_down)
|
||||
selectKey = findViewById(R.id.select)
|
||||
selectAllKey = findViewById(R.id.select_all)
|
||||
cutKey = findViewById(R.id.clipboard_cut)
|
||||
@@ -74,8 +70,6 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
|
||||
override fun onUpdateSelection() {
|
||||
val isSelectionActive = florisboard?.activeEditorInstance?.selection?.isSelectionMode ?: false
|
||||
val isSelectionMode = florisboard?.textInputManager?.isManualSelectionMode ?: false
|
||||
arrowUpKey?.isEnabled = !(isSelectionActive || isSelectionMode)
|
||||
arrowDownKey?.isEnabled = !(isSelectionActive || isSelectionMode)
|
||||
selectKey?.isHighlighted = isSelectionActive || isSelectionMode
|
||||
selectAllKey?.visibility = when {
|
||||
isSelectionActive -> View.GONE
|
||||
@@ -86,7 +80,11 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
|
||||
else -> View.GONE
|
||||
}
|
||||
copyKey?.isEnabled = isSelectionActive
|
||||
pasteKey?.isEnabled = florisboard?.clipboardManager?.hasPrimaryClip() ?: false
|
||||
pasteKey?.isEnabled =
|
||||
florisboard?.florisClipboardManager?.hasPrimaryClip() == true &&
|
||||
florisboard.activeEditorInstance.contentMimeTypes?.any {
|
||||
florisboard.florisClipboardManager!!.primaryClip?.mimeTypes?.contains(it) ?: false
|
||||
} == true
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
|
||||
@@ -40,6 +40,7 @@ enum class SwipeAction {
|
||||
SHOW_INPUT_METHOD_PICKER,
|
||||
SWITCH_TO_PREV_SUBTYPE,
|
||||
SWITCH_TO_NEXT_SUBTYPE,
|
||||
SWITCH_TO_CLIPBOARD_CONTEXT,
|
||||
SWITCH_TO_PREV_KEYBOARD;
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -64,10 +64,7 @@ abstract class SwipeGesture {
|
||||
* @property listener The listener to report detected swipes to.
|
||||
*/
|
||||
class Detector(private val context: Context, private val listener: Listener) {
|
||||
private var firstMotionEvent: MotionEvent? = null
|
||||
private var lastMotionEvent: MotionEvent? = null
|
||||
private var absUnitCountX: Int = 0
|
||||
private var absUnitCountY: Int = 0
|
||||
private var pointerDataMap: MutableMap<Int, PointerData> = mutableMapOf()
|
||||
private var thresholdWidth: Double = numericValue(context, DistanceThreshold.NORMAL)
|
||||
private var unitWidth: Double = thresholdWidth / 4.0
|
||||
|
||||
@@ -92,67 +89,83 @@ abstract class SwipeGesture {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN,
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
resetState()
|
||||
firstMotionEvent = MotionEvent.obtainNoHistory(event)
|
||||
lastMotionEvent = firstMotionEvent
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
resetState()
|
||||
}
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
pointerDataMap[pointerId] = PointerData().apply {
|
||||
firstX = event.getX(pointerIndex)
|
||||
firstY = event.getY(pointerIndex)
|
||||
lastX = firstX
|
||||
lastY = firstY
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val firstEvent = firstMotionEvent ?: return false
|
||||
val absDiffX = event.x - firstEvent.x
|
||||
val absDiffY = event.y - firstEvent.y
|
||||
val lastEvent = lastMotionEvent ?: return false
|
||||
val relDiffX = event.x - lastEvent.x
|
||||
val relDiffY = event.y - lastEvent.y
|
||||
return if (alwaysTriggerOnMove || abs(relDiffX) > (thresholdWidth / 2.0) || abs(relDiffY) > (thresholdWidth / 2.0)) {
|
||||
lastMotionEvent = MotionEvent.obtainNoHistory(event)
|
||||
val direction = detectDirection(relDiffX.toDouble(), relDiffY.toDouble())
|
||||
val newAbsUnitCountX = (absDiffX / unitWidth).toInt()
|
||||
val newAbsUnitCountY = (absDiffY / unitWidth).toInt()
|
||||
val relUnitCountX = newAbsUnitCountX - absUnitCountX
|
||||
val relUnitCountY = newAbsUnitCountY - absUnitCountY
|
||||
absUnitCountX = newAbsUnitCountX
|
||||
absUnitCountY = newAbsUnitCountY
|
||||
listener.onSwipe(Event(
|
||||
direction = direction,
|
||||
type = Type.TOUCH_MOVE,
|
||||
absUnitCountX,
|
||||
absUnitCountY,
|
||||
relUnitCountX,
|
||||
relUnitCountY
|
||||
))
|
||||
} else {
|
||||
false
|
||||
for (pointerIndex in 0 until event.pointerCount) {
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
pointerDataMap[pointerId]?.apply {
|
||||
val absDiffX = event.getX(pointerIndex) - firstX
|
||||
val absDiffY = event.getY(pointerIndex) - firstY
|
||||
val relDiffX = event.getX(pointerIndex) - lastX
|
||||
val relDiffY = event.getY(pointerIndex) - lastY
|
||||
return if (alwaysTriggerOnMove || abs(relDiffX) > (thresholdWidth / 2.0) || abs(relDiffY) > (thresholdWidth / 2.0)) {
|
||||
lastX = event.getX(pointerIndex)
|
||||
lastY = event.getY(pointerIndex)
|
||||
val direction = detectDirection(relDiffX.toDouble(), relDiffY.toDouble())
|
||||
val newAbsUnitCountX = (absDiffX / unitWidth).toInt()
|
||||
val newAbsUnitCountY = (absDiffY / unitWidth).toInt()
|
||||
val relUnitCountX = newAbsUnitCountX - absUnitCountX
|
||||
val relUnitCountY = newAbsUnitCountY - absUnitCountY
|
||||
absUnitCountX = newAbsUnitCountX
|
||||
absUnitCountY = newAbsUnitCountY
|
||||
listener.onSwipe(Event(
|
||||
direction = direction,
|
||||
type = Type.TOUCH_MOVE,
|
||||
pointerId,
|
||||
absUnitCountX,
|
||||
absUnitCountY,
|
||||
relUnitCountX,
|
||||
relUnitCountY
|
||||
))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_POINTER_UP -> {
|
||||
val firstEvent = firstMotionEvent ?: return false
|
||||
val absDiffX = event.x - firstEvent.x
|
||||
val absDiffY = event.y - firstEvent.y
|
||||
/*val velocityThresholdNV = numericValue(velocityThreshold)
|
||||
val velocity =
|
||||
((convertPixelsToDp(
|
||||
sqrt(diffX.pow(2) + diffY.pow(2)),
|
||||
context
|
||||
) / event.downTime) * 10.0f.pow(8)).toInt()*/
|
||||
// return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
|
||||
val ret = if ((abs(absDiffX) > thresholdWidth || abs(absDiffY) > thresholdWidth)) {
|
||||
val direction = detectDirection(absDiffX.toDouble(), absDiffY.toDouble())
|
||||
absUnitCountX = (absDiffX / unitWidth).toInt()
|
||||
absUnitCountY = (absDiffY / unitWidth).toInt()
|
||||
listener.onSwipe(Event(
|
||||
direction = direction,
|
||||
type = Type.TOUCH_UP,
|
||||
absUnitCountX,
|
||||
absUnitCountY,
|
||||
absUnitCountX,
|
||||
absUnitCountY
|
||||
))
|
||||
} else {
|
||||
false
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
pointerDataMap.remove(pointerId)?.apply {
|
||||
val absDiffX = event.getX(pointerIndex) - firstX
|
||||
val absDiffY = event.getY(pointerIndex) - firstY
|
||||
/*val velocityThresholdNV = numericValue(velocityThreshold)
|
||||
val velocity =
|
||||
((convertPixelsToDp(
|
||||
sqrt(diffX.pow(2) + diffY.pow(2)),
|
||||
context
|
||||
) / event.downTime) * 10.0f.pow(8)).toInt()*/
|
||||
// return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
|
||||
return if ((abs(absDiffX) > thresholdWidth || abs(absDiffY) > thresholdWidth)) {
|
||||
val direction = detectDirection(absDiffX.toDouble(), absDiffY.toDouble())
|
||||
absUnitCountX = (absDiffX / unitWidth).toInt()
|
||||
absUnitCountY = (absDiffY / unitWidth).toInt()
|
||||
listener.onSwipe(Event(
|
||||
direction = direction,
|
||||
type = Type.TOUCH_UP,
|
||||
pointerId,
|
||||
absUnitCountX,
|
||||
absUnitCountY,
|
||||
absUnitCountX,
|
||||
absUnitCountY
|
||||
))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
resetState()
|
||||
return ret
|
||||
return false
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
resetState()
|
||||
@@ -160,7 +173,7 @@ abstract class SwipeGesture {
|
||||
else -> return false
|
||||
}
|
||||
return false
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -208,10 +221,16 @@ abstract class SwipeGesture {
|
||||
* Resets the state.
|
||||
*/
|
||||
private fun resetState() {
|
||||
firstMotionEvent = null
|
||||
lastMotionEvent = null
|
||||
absUnitCountX = 0
|
||||
absUnitCountY = 0
|
||||
pointerDataMap.clear()
|
||||
}
|
||||
|
||||
class PointerData {
|
||||
var firstX: Float = 0.0f
|
||||
var firstY: Float = 0.0f
|
||||
var lastX: Float = 0.0f
|
||||
var lastY: Float = 0.0f
|
||||
var absUnitCountX: Int = 0
|
||||
var absUnitCountY: Int = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +238,7 @@ abstract class SwipeGesture {
|
||||
* An interface which provides an abstract callback function, which will be called for any
|
||||
* detected swipe event.
|
||||
*/
|
||||
fun interface Listener {
|
||||
interface Listener {
|
||||
fun onSwipe(event: Event): Boolean
|
||||
}
|
||||
|
||||
@@ -231,6 +250,8 @@ abstract class SwipeGesture {
|
||||
val direction: Direction,
|
||||
/** The type of the swipe. */
|
||||
val type: Type,
|
||||
/** The pointer ID of this event, corresponds to the value reported by the original MotionEvent. */
|
||||
val pointerId: Int,
|
||||
/** The unit count on the x-axis, measured from the first event (ACTION_DOWN). */
|
||||
val absUnitCountX: Int,
|
||||
/** The unit count on the y-axis, measured from the first event (ACTION_DOWN). */
|
||||
|
||||
@@ -50,7 +50,7 @@ object KeyCode {
|
||||
const val CLEAR_INPUT = -13
|
||||
const val VOICE_INPUT = -4
|
||||
|
||||
const val DISABLED = 0
|
||||
const val UNSPECIFIED = 0
|
||||
|
||||
const val SPLIT_LAYOUT = -110
|
||||
const val MERGE_LAYOUT = -111
|
||||
@@ -65,9 +65,10 @@ object KeyCode {
|
||||
const val CLIPBOARD_PASTE_POPUP = -133
|
||||
const val CLIPBOARD_SELECT = -134
|
||||
const val CLIPBOARD_SELECT_ALL = -135
|
||||
const val CLEAR_CLIPBOARD_HISTORY = -136
|
||||
|
||||
const val UNDO = -136
|
||||
const val REDO = -137
|
||||
const val UNDO = -137
|
||||
const val REDO = -138
|
||||
|
||||
const val PHONE_PAUSE = 44
|
||||
const val PHONE_WAIT = 59
|
||||
@@ -89,6 +90,8 @@ object KeyCode {
|
||||
const val TOGGLE_ONE_HANDED_MODE_RIGHT =-216
|
||||
const val URI_COMPONENT_TLD = -255
|
||||
|
||||
const val INTERNAL_BATCH_EDIT = -901
|
||||
|
||||
const val KESHIDA = 1600
|
||||
const val HALF_SPACE = 8204
|
||||
}
|
||||
|
||||
@@ -34,7 +34,180 @@ open class KeyData(
|
||||
var type: KeyType = KeyType.CHARACTER,
|
||||
var code: Int = 0,
|
||||
var label: String = ""
|
||||
)
|
||||
) {
|
||||
companion object {
|
||||
/** Predefined key data for [KeyCode.ARROW_DOWN] */
|
||||
val ARROW_DOWN = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.ARROW_DOWN,
|
||||
label = "arrow_down"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.ARROW_LEFT] */
|
||||
val ARROW_LEFT = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.ARROW_LEFT,
|
||||
label = "arrow_left"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.ARROW_RIGHT] */
|
||||
val ARROW_RIGHT = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.ARROW_RIGHT,
|
||||
label = "arrow_right"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.ARROW_UP] */
|
||||
val ARROW_UP = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.ARROW_UP,
|
||||
label = "arrow_up"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_COPY] */
|
||||
val CLIPBOARD_COPY = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_COPY,
|
||||
label = "clipboard_copy"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_CUT] */
|
||||
val CLIPBOARD_CUT = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_CUT,
|
||||
label = "clipboard_cut"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_PASTE] */
|
||||
val CLIPBOARD_PASTE = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_PASTE,
|
||||
label = "clipboard_paste"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_SELECT] */
|
||||
val CLIPBOARD_SELECT = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_SELECT,
|
||||
label = "clipboard_select"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_SELECT_ALL] */
|
||||
val CLIPBOARD_SELECT_ALL = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_SELECT_ALL,
|
||||
label = "clipboard_select_all"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.DELETE] */
|
||||
val DELETE = KeyData(
|
||||
type = KeyType.ENTER_EDITING,
|
||||
code = KeyCode.DELETE,
|
||||
label = "delete"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.DELETE_WORD] */
|
||||
val DELETE_WORD = KeyData(
|
||||
type = KeyType.ENTER_EDITING,
|
||||
code = KeyCode.DELETE_WORD,
|
||||
label = "delete_word"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.INTERNAL_BATCH_EDIT] */
|
||||
val INTERNAL_BATCH_EDIT = KeyData(
|
||||
type = KeyType.FUNCTION,
|
||||
code = KeyCode.INTERNAL_BATCH_EDIT,
|
||||
label = "internal_batch_edit"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.MOVE_START_OF_LINE] */
|
||||
val MOVE_START_OF_LINE = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.MOVE_START_OF_LINE,
|
||||
label = "move_start_of_line"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.MOVE_END_OF_LINE] */
|
||||
val MOVE_END_OF_LINE = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.MOVE_END_OF_LINE,
|
||||
label = "move_end_of_line"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.MOVE_START_OF_PAGE] */
|
||||
val MOVE_START_OF_PAGE = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.MOVE_START_OF_PAGE,
|
||||
label = "move_start_of_page"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.MOVE_END_OF_PAGE] */
|
||||
val MOVE_END_OF_PAGE = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.MOVE_END_OF_PAGE,
|
||||
label = "move_end_of_page"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SHOW_INPUT_METHOD_PICKER] */
|
||||
val SHOW_INPUT_METHOD_PICKER = KeyData(
|
||||
type = KeyType.FUNCTION,
|
||||
code = KeyCode.SHOW_INPUT_METHOD_PICKER,
|
||||
label = "show_input_method_picker"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SWITCH_TO_TEXT_CONTEXT] */
|
||||
val SWITCH_TO_TEXT_CONTEXT = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.SWITCH_TO_TEXT_CONTEXT,
|
||||
label = "switch_to_text_context"
|
||||
)
|
||||
/** Predefined key data for [KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT] */
|
||||
val SWITCH_TO_CLIPBOARD_CONTEXT = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT,
|
||||
label = "switch_to_clipboard_context"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SHIFT] */
|
||||
val SHIFT = KeyData(
|
||||
type = KeyType.MODIFIER,
|
||||
code = KeyCode.SHIFT,
|
||||
label = "shift"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SHIFT_LOCK] */
|
||||
val SHIFT_LOCK = KeyData(
|
||||
type = KeyType.MODIFIER,
|
||||
code = KeyCode.SHIFT_LOCK,
|
||||
label = "shift_lock"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SPACE] */
|
||||
val SPACE = KeyData(
|
||||
type = KeyType.CHARACTER,
|
||||
code = KeyCode.SPACE,
|
||||
label = "space"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.UNSPECIFIED] */
|
||||
val UNSPECIFIED = KeyData(
|
||||
type = KeyType.UNSPECIFIED,
|
||||
code = KeyCode.UNSPECIFIED,
|
||||
label = "unspecified"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.VIEW_CHARACTERS] */
|
||||
val VIEW_CHARACTERS = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.VIEW_CHARACTERS,
|
||||
label = "view_characters"
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "KeyData { type=$type code=$code label=\"$label\" }"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class which describes a single key and its attributes, while also providing additional
|
||||
@@ -47,6 +220,10 @@ open class KeyData(
|
||||
* or if the key should always be visible. Defaults to [KeyVariation.ALL].
|
||||
* @property popup List of keys which will be accessible while long pressing the key. Defaults to
|
||||
* an empty set (no extended popup).
|
||||
* @property shift An alternative key to use when the keyboard caps state is true. Useful for layouts
|
||||
* such as Colemak and Dvorak. Defaults to null (don't override base uppercase key). This override
|
||||
* property should only be used to provide an uppercase variant of two else not related variants, but
|
||||
* should not be used for providing an uppercase letter (e.g. 'a' -> 'A').
|
||||
*/
|
||||
class FlorisKeyData(
|
||||
type: KeyType = KeyType.CHARACTER,
|
||||
@@ -54,7 +231,8 @@ class FlorisKeyData(
|
||||
label: String = "",
|
||||
var groupId: Int = GROUP_DEFAULT,
|
||||
var variation: KeyVariation = KeyVariation.ALL,
|
||||
var popup: PopupSet<KeyData> = PopupSet()
|
||||
var popup: PopupSet<KeyData> = PopupSet(),
|
||||
var shift: KeyData? = null
|
||||
) : KeyData(type, code, label) {
|
||||
companion object {
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,11 @@ package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.*
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Outline
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.PaintDrawable
|
||||
import android.os.Handler
|
||||
@@ -32,8 +36,10 @@ import com.google.android.flexbox.FlexboxLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.ImeOptions
|
||||
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupManager
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
@@ -41,7 +47,9 @@ import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeValue
|
||||
import dev.patrickgold.florisboard.util.*
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
import dev.patrickgold.florisboard.util.cancelAll
|
||||
import dev.patrickgold.florisboard.util.postDelayed
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
||||
@@ -69,7 +77,7 @@ class KeyView(
|
||||
private var hasTriggeredGestureMove: Boolean = false
|
||||
private var keyHintMode: KeyHintMode = KeyHintMode.DISABLED
|
||||
private val longKeyPressHandler: Handler = Handler(context.mainLooper)
|
||||
private val repeatedKeyPressHandler: Handler = Handler(context.mainLooper)
|
||||
val popupManager = PopupManager<KeyboardView, KeyView>(keyboardView, florisboard?.popupLayerView)
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private var shouldBlockNextKeyCode: Boolean = false
|
||||
|
||||
@@ -115,10 +123,10 @@ class KeyView(
|
||||
val keyMarginH: Int
|
||||
val keyMarginV: Int
|
||||
|
||||
if (keyboardView.isSmartbarKeyboardView){
|
||||
if (keyboardView.isSmartbarKeyboardView) {
|
||||
keyMarginH = resources.getDimension(R.dimen.key_marginH).toInt()
|
||||
keyMarginV = resources.getDimension(R.dimen.key_marginV).toInt()
|
||||
}else {
|
||||
} else {
|
||||
keyMarginV = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingVertical, context).toInt()
|
||||
keyMarginH = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
|
||||
}
|
||||
@@ -161,7 +169,7 @@ class KeyView(
|
||||
setPadding(0, 0, 0, 0)
|
||||
|
||||
background = backgroundDrawable
|
||||
elevation = if(themeValueCache.shouldShowBorder) 4.0f else 0.0f
|
||||
elevation = if (themeValueCache.shouldShowBorder) 4.0f else 0.0f
|
||||
|
||||
if (prefs.keyboard.hintedNumberRowMode != KeyHintMode.DISABLED && data.popup.hint?.type == KeyType.NUMERIC) {
|
||||
keyHintMode = prefs.keyboard.hintedNumberRowMode
|
||||
@@ -186,17 +194,23 @@ class KeyView(
|
||||
*/
|
||||
fun getComputedLetter(
|
||||
keyData: KeyData = data,
|
||||
caps: Boolean = florisboard?.textInputManager?.caps ?: false && florisboard?.textInputManager?.getActiveKeyboardMode() == KeyboardMode.CHARACTERS,
|
||||
caps: Boolean = florisboard?.textInputManager?.caps ?: false &&
|
||||
florisboard?.textInputManager?.getActiveKeyboardMode() == KeyboardMode.CHARACTERS,
|
||||
subtype: Subtype = florisboard?.activeSubtype ?: Subtype.DEFAULT
|
||||
): String {
|
||||
return when (data.code) {
|
||||
KeyCode.URI_COMPONENT_TLD -> keyData.label.toLowerCase(Locale.ENGLISH)
|
||||
else -> {
|
||||
val labelText = (keyData.code.toChar()).toString()
|
||||
if (caps) {
|
||||
labelText.toUpperCase(subtype.locale)
|
||||
} else {
|
||||
labelText
|
||||
return if (caps && keyData is FlorisKeyData && keyData.shift != null) {
|
||||
(keyData.shift!!.code.toChar()).toString()
|
||||
} else {
|
||||
when (data.code) {
|
||||
KeyCode.URI_COMPONENT_TLD -> keyData.label.toLowerCase(Locale.ENGLISH)
|
||||
else -> {
|
||||
val labelText = (keyData.code.toChar()).toString()
|
||||
val locale = if (subtype.locale.language == "el") { Locale.getDefault() } else { subtype.locale }
|
||||
if (caps) {
|
||||
labelText.toUpperCase(locale)
|
||||
} else {
|
||||
labelText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,78 +242,82 @@ class KeyView(
|
||||
*/
|
||||
fun onFlorisTouchEvent(event: MotionEvent?): Boolean {
|
||||
if (event == null || !isEnabled) return false
|
||||
val florisboard = florisboard ?: return false
|
||||
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
|
||||
val alwaysTriggerOnMove = (hasTriggeredGestureMove
|
||||
&& (data.code == KeyCode.DELETE && prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_CHARACTERS_PRECISELY
|
||||
|| data.code == KeyCode.SPACE))
|
||||
&& (data.code == KeyCode.DELETE && prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_CHARACTERS_PRECISELY
|
||||
|| data.code == KeyCode.SPACE))
|
||||
if (swipeGestureDetector.onTouchEvent(event, alwaysTriggerOnMove)) {
|
||||
isKeyPressed = false
|
||||
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
longKeyPressHandler.cancelAll()
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
keyboardView.popupManager.hide()
|
||||
popupManager.hide()
|
||||
return true
|
||||
}
|
||||
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (data.code == KeyCode.SHIFT) {
|
||||
isKeyPressed = true
|
||||
florisboard?.keyPressVibrate()
|
||||
florisboard?.keyPressSound(data)
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
} else {
|
||||
val delayMillis = prefs.keyboard.longPressDelay.toLong()
|
||||
hasTriggeredGestureMove = false
|
||||
shouldBlockNextKeyCode = false
|
||||
florisboard?.prefs?.keyboard?.let {
|
||||
if (it.popupEnabled){
|
||||
keyboardView.popupManager.show(this, keyHintMode)
|
||||
}
|
||||
}
|
||||
isKeyPressed = true
|
||||
florisboard?.keyPressVibrate()
|
||||
florisboard?.keyPressSound(data)
|
||||
when (data.code) {
|
||||
KeyCode.ARROW_DOWN,
|
||||
KeyCode.ARROW_LEFT,
|
||||
KeyCode.ARROW_RIGHT,
|
||||
KeyCode.ARROW_UP,
|
||||
KeyCode.DELETE -> {
|
||||
repeatedKeyPressHandler.postAtScheduledRate((delayMillis * 2.0f).toLong(), 25) {
|
||||
if (isKeyPressed) {
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
} else {
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.code == KeyCode.SPACE) {
|
||||
initSelectionStart = florisboard?.activeEditorInstance?.selection?.start ?: 0
|
||||
initSelectionEnd = florisboard?.activeEditorInstance?.selection?.end ?: 0
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.down(data))
|
||||
isKeyPressed = true
|
||||
val delayMillis = prefs.keyboard.longPressDelay.toLong()
|
||||
hasTriggeredGestureMove = false
|
||||
shouldBlockNextKeyCode = false
|
||||
if (florisboard.prefs.keyboard.popupEnabled) {
|
||||
popupManager.show(this, keyHintMode)
|
||||
}
|
||||
isKeyPressed = true
|
||||
florisboard.keyPressVibrate()
|
||||
florisboard.keyPressSound(data)
|
||||
|
||||
when (data.code) {
|
||||
KeyCode.SPACE -> {
|
||||
initSelectionStart = florisboard.activeEditorInstance.selection.start
|
||||
initSelectionEnd = florisboard.activeEditorInstance.selection.end
|
||||
longKeyPressHandler.postDelayed((delayMillis * 2.5f).toLong()) {
|
||||
when (prefs.gestures.spaceBarLongPress) {
|
||||
SwipeAction.NO_ACTION,
|
||||
SwipeAction.INSERT_SPACE -> {}
|
||||
SwipeAction.INSERT_SPACE -> {
|
||||
}
|
||||
else -> {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.spaceBarLongPress)
|
||||
this.florisboard.executeSwipeAction(prefs.gestures.spaceBarLongPress)
|
||||
shouldBlockNextKeyCode = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
KeyCode.SHIFT -> {
|
||||
longKeyPressHandler.postDelayed((delayMillis * 2.5).toLong()) {
|
||||
this.florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.SHIFT_LOCK))
|
||||
florisboard.keyPressVibrate()
|
||||
florisboard.keyPressSound(data)
|
||||
}
|
||||
}
|
||||
KeyCode.LANGUAGE_SWITCH -> {
|
||||
longKeyPressHandler.postDelayed((delayMillis * 2.0).toLong()) {
|
||||
shouldBlockNextKeyCode = true
|
||||
this.florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.SHOW_INPUT_METHOD_PICKER))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
longKeyPressHandler.postDelayed(delayMillis) {
|
||||
if (data.popup.isNotEmpty()) {
|
||||
keyboardView.popupManager.extend(this, keyHintMode)
|
||||
popupManager.extend(this@KeyView, keyHintMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (keyboardView.popupManager.isShowingExtendedPopup) {
|
||||
if (popupManager.isShowingExtendedPopup) {
|
||||
val isPointerWithinBounds =
|
||||
keyboardView.popupManager.propagateMotionEvent(this, event)
|
||||
popupManager.propagateMotionEvent(this, event)
|
||||
if (!isPointerWithinBounds && !shouldBlockNextKeyCode) {
|
||||
keyboardView.dismissActiveKeyViewReference()
|
||||
keyboardView.dismissActiveKeyViewReference(pointerId)
|
||||
}
|
||||
} else {
|
||||
val parent = parent as ViewGroup
|
||||
@@ -309,31 +327,49 @@ class KeyView(
|
||||
|| event.y > 1.35f * measuredHeight
|
||||
) {
|
||||
if (!shouldBlockNextKeyCode) {
|
||||
keyboardView.dismissActiveKeyViewReference()
|
||||
keyboardView.dismissActiveKeyViewReference(pointerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
MotionEvent.ACTION_UP -> {
|
||||
longKeyPressHandler.cancelAll()
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
if (data.code != KeyCode.SHIFT) {
|
||||
if (hasTriggeredGestureMove && data.code == KeyCode.DELETE) {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
deleteBackwards()
|
||||
if (hasTriggeredGestureMove && data.code == KeyCode.DELETE) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
florisboard.activeEditorInstance.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
deleteBackwards()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val retData = popupManager.getActiveKeyData(this)
|
||||
if (!shouldBlockNextKeyCode && retData != null) {
|
||||
if (retData == data) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.up(data))
|
||||
} else {
|
||||
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(retData))
|
||||
}
|
||||
} else {
|
||||
val retData = keyboardView.popupManager.getActiveKeyData(this)
|
||||
if (event.actionMasked != MotionEvent.ACTION_CANCEL && !shouldBlockNextKeyCode && retData != null) {
|
||||
florisboard?.textInputManager?.sendKeyPress(retData)
|
||||
} else {
|
||||
shouldBlockNextKeyCode = false
|
||||
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
keyboardView.popupManager.hide()
|
||||
}
|
||||
popupManager.hide()
|
||||
}
|
||||
shouldBlockNextKeyCode = false
|
||||
hasTriggeredGestureMove = false
|
||||
isKeyPressed = false
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
longKeyPressHandler.cancelAll()
|
||||
if (data.code != KeyCode.SHIFT) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
popupManager.hide()
|
||||
shouldBlockNextKeyCode = false
|
||||
hasTriggeredGestureMove = false
|
||||
isKeyPressed = false
|
||||
}
|
||||
@@ -354,7 +390,7 @@ class KeyView(
|
||||
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
|
||||
florisboard.activeEditorInstance.apply {
|
||||
selection.updateAndNotify(
|
||||
(selection.end + event.absUnitCountX).coerceIn(0, selection.end),
|
||||
(selection.end + event.absUnitCountX + 1).coerceIn(0, selection.end),
|
||||
selection.end
|
||||
)
|
||||
}
|
||||
@@ -398,12 +434,10 @@ class KeyView(
|
||||
}
|
||||
SwipeGesture.Direction.LEFT -> {
|
||||
if (prefs.gestures.spaceBarSwipeLeft == SwipeAction.MOVE_CURSOR_LEFT) {
|
||||
if (!florisboard.activeEditorInstance.isRawInputEditor) {
|
||||
val s = (initSelectionEnd + event.absUnitCountX).coerceIn(0, florisboard.activeEditorInstance.cachedInput.expectedMaxLength)
|
||||
florisboard.activeEditorInstance.selection.updateAndNotify(s, s)
|
||||
} else {
|
||||
for (n in 0 until abs(event.relUnitCountX)) {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
|
||||
abs(event.relUnitCountX).let {
|
||||
val count = if (!hasTriggeredGestureMove) { it - 1 } else { it }
|
||||
if (count > 0) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.ARROW_LEFT, count))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -415,12 +449,10 @@ class KeyView(
|
||||
}
|
||||
SwipeGesture.Direction.RIGHT -> {
|
||||
if (prefs.gestures.spaceBarSwipeRight == SwipeAction.MOVE_CURSOR_RIGHT) {
|
||||
if (!florisboard.activeEditorInstance.isRawInputEditor) {
|
||||
val s = (initSelectionEnd + event.absUnitCountX).coerceIn(0, florisboard.activeEditorInstance.cachedInput.expectedMaxLength)
|
||||
florisboard.activeEditorInstance.selection.updateAndNotify(s, s)
|
||||
} else {
|
||||
for (n in 0 until abs(event.relUnitCountX)) {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
|
||||
abs(event.relUnitCountX).let {
|
||||
val count = if (!hasTriggeredGestureMove) { it - 1 } else { it }
|
||||
if (count > 0) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.ARROW_RIGHT, count))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -445,14 +477,13 @@ class KeyView(
|
||||
* by Devunwired
|
||||
*/
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
|
||||
val keyMarginH: Int
|
||||
val keyMarginV: Int
|
||||
|
||||
if (keyboardView.isSmartbarKeyboardView){
|
||||
if (keyboardView.isSmartbarKeyboardView) {
|
||||
keyMarginH = resources.getDimension(R.dimen.key_marginH).toInt()
|
||||
keyMarginV = resources.getDimension(R.dimen.key_marginV).toInt()
|
||||
}else {
|
||||
} else {
|
||||
keyMarginV = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingVertical, context).toInt()
|
||||
keyMarginH = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
|
||||
}
|
||||
@@ -476,11 +507,11 @@ class KeyView(
|
||||
else -> when (data.code) {
|
||||
KeyCode.SHIFT,
|
||||
KeyCode.DELETE ->
|
||||
if ((keyboardView.computedLayout?.arrangement?.get(2)?.size ?: 0) > 10) {
|
||||
1.12f
|
||||
} else {
|
||||
1.56f
|
||||
}
|
||||
if ((keyboardView.computedLayout?.arrangement?.get(2)?.size ?: 0) > 10) {
|
||||
1.12f
|
||||
} else {
|
||||
1.56f
|
||||
}
|
||||
KeyCode.VIEW_CHARACTERS,
|
||||
KeyCode.VIEW_SYMBOLS,
|
||||
KeyCode.VIEW_SYMBOLS2,
|
||||
@@ -528,7 +559,7 @@ class KeyView(
|
||||
}
|
||||
|
||||
drawablePaddingH = (0.2f * width).toInt()
|
||||
drawablePaddingV = (0.2f * height).toInt()
|
||||
drawablePaddingV = (0.2f * height * (1.0f / (florisboard?.inputView?.heightFactor ?: 1.0f)).coerceAtMost(1.0f)).toInt()
|
||||
|
||||
// MUST CALL THIS
|
||||
setMeasuredDimension(width, height)
|
||||
@@ -553,12 +584,25 @@ class KeyView(
|
||||
KeyCode.CLIPBOARD_CUT -> (florisboard != null
|
||||
&& florisboard.activeEditorInstance.selection.isSelectionMode
|
||||
&& !florisboard.activeEditorInstance.isRawInputEditor)
|
||||
KeyCode.CLIPBOARD_PASTE -> florisboard?.clipboardManager?.hasPrimaryClip() == true
|
||||
KeyCode.CLIPBOARD_PASTE -> (
|
||||
// such gore. checks
|
||||
// 1. has a clipboard item
|
||||
// 2. the clipboard item has any of the supported mime types of the editor OR is plain text.
|
||||
florisboard?.florisClipboardManager?.canBePasted(florisboard.florisClipboardManager?.primaryClip)
|
||||
) == true
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> {
|
||||
florisboard?.activeEditorInstance?.isRawInputEditor == false
|
||||
}
|
||||
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT -> {
|
||||
visibility = when (prefs.clipboard.enableHistory ) {
|
||||
true -> VISIBLE
|
||||
false -> GONE
|
||||
}
|
||||
prefs.clipboard.enableHistory
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
if (data.code == KeyCode.CLIPBOARD_PASTE)
|
||||
if (!isEnabled) {
|
||||
isKeyPressed = false
|
||||
}
|
||||
@@ -642,10 +686,10 @@ class KeyView(
|
||||
val keyMarginH: Int
|
||||
val keyMarginV: Int
|
||||
|
||||
if (keyboardView.isSmartbarKeyboardView){
|
||||
if (keyboardView.isSmartbarKeyboardView) {
|
||||
keyMarginH = resources.getDimension(R.dimen.key_marginH).toInt()
|
||||
keyMarginV = resources.getDimension(R.dimen.key_marginV).toInt()
|
||||
}else {
|
||||
} else {
|
||||
keyMarginV = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingVertical, context).toInt()
|
||||
keyMarginH = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
|
||||
}
|
||||
@@ -691,6 +735,12 @@ class KeyView(
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT -> {
|
||||
when (prefs.clipboard.enableHistory) {
|
||||
true -> VISIBLE
|
||||
false -> GONE
|
||||
}
|
||||
}
|
||||
KeyCode.LANGUAGE_SWITCH -> {
|
||||
val tempUtilityKeyAction = when {
|
||||
prefs.keyboard.utilityKeyEnabled -> prefs.keyboard.utilityKeyAction
|
||||
@@ -711,7 +761,11 @@ class KeyView(
|
||||
}
|
||||
else -> if (data.variation != KeyVariation.ALL) {
|
||||
val keyVariation = florisboard?.textInputManager?.keyVariation ?: KeyVariation.NORMAL
|
||||
visibility = if (data.variation == keyVariation) { VISIBLE } else { GONE }
|
||||
visibility = if (data.variation == keyVariation) {
|
||||
VISIBLE
|
||||
} else {
|
||||
GONE
|
||||
}
|
||||
updateTouchHitBox()
|
||||
}
|
||||
}
|
||||
@@ -827,12 +881,16 @@ class KeyView(
|
||||
KeyboardMode.CHARACTERS -> {
|
||||
label = florisboard?.activeSubtype?.locale?.displayName
|
||||
}
|
||||
else -> {}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
|
||||
drawable = getDrawable(context, R.drawable.ic_sentiment_satisfied)
|
||||
}
|
||||
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT -> {
|
||||
drawable = getDrawable(context, R.drawable.ic_assignment)
|
||||
}
|
||||
KeyCode.SWITCH_TO_TEXT_CONTEXT,
|
||||
KeyCode.VIEW_CHARACTERS -> {
|
||||
label = resources.getString(R.string.key__view_characters)
|
||||
@@ -907,7 +965,7 @@ class KeyView(
|
||||
}
|
||||
else -> when {
|
||||
(data.type == KeyType.CHARACTER || data.type == KeyType.NUMERIC) &&
|
||||
data.code != KeyCode.SPACE -> {
|
||||
data.code != KeyCode.SPACE -> {
|
||||
val cachedTextSize = setTextSizeFor(
|
||||
labelPaint,
|
||||
measuredWidth - (2.6f * drawablePaddingH),
|
||||
@@ -926,7 +984,7 @@ class KeyView(
|
||||
else -> 1.0f
|
||||
}
|
||||
)
|
||||
keyboardView.popupManager.keyPopupTextSize = cachedTextSize
|
||||
popupManager.keyPopupTextSize = cachedTextSize
|
||||
}
|
||||
else -> {
|
||||
setTextSizeFor(
|
||||
@@ -949,7 +1007,11 @@ class KeyView(
|
||||
themeValueCache.keyForeground.toSolidColor().color
|
||||
}
|
||||
labelPaint.alpha = if (keyboardView.computedLayout?.mode == KeyboardMode.CHARACTERS &&
|
||||
data.code == KeyCode.SPACE) { 120 } else { 255 }
|
||||
data.code == KeyCode.SPACE) {
|
||||
120
|
||||
} else {
|
||||
255
|
||||
}
|
||||
val centerX = measuredWidth / 2.0f
|
||||
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2
|
||||
if (label.contains("\n")) {
|
||||
|
||||
@@ -22,16 +22,18 @@ import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.children
|
||||
import com.google.android.flexbox.FlexDirection
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupManager
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyView
|
||||
import dev.patrickgold.florisboard.ime.text.layout.ComputedLayoutData
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
@@ -47,12 +49,10 @@ import kotlin.math.roundToInt
|
||||
*
|
||||
* @property florisboard Reference to instance of core class [FlorisBoard].
|
||||
*/
|
||||
class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Listener,
|
||||
class KeyboardView : FlexboxLayout, FlorisBoard.EventListener, SwipeGesture.Listener,
|
||||
ThemeManager.OnThemeUpdatedListener {
|
||||
private var activeKeyView: KeyView? = null
|
||||
private var activePointerId: Int? = null
|
||||
private var activeX: Float = 0.0f
|
||||
private var activeY: Float = 0.0f
|
||||
private var activeKeyViews: MutableMap<Int, KeyView> = mutableMapOf()
|
||||
private var initialKeyCodes: MutableMap<Int, Int> = mutableMapOf()
|
||||
|
||||
var computedLayout: ComputedLayoutData? = null
|
||||
set(v) {
|
||||
@@ -62,11 +62,9 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
var desiredKeyWidth: Int = resources.getDimension(R.dimen.key_width).toInt()
|
||||
var desiredKeyHeight: Int = resources.getDimension(R.dimen.key_height).toInt()
|
||||
var florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private var initialKeyCode: Int = 0
|
||||
private val isPreviewMode: Boolean
|
||||
val isSmartbarKeyboardView: Boolean
|
||||
val isLoadingPlaceholderKeyboard: Boolean
|
||||
var popupManager = PopupManager<KeyboardView, KeyView>(this, florisboard?.popupLayerView)
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
|
||||
@@ -80,7 +78,8 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
isLoadingPlaceholderKeyboard = getBoolean(R.styleable.KeyboardView_isLoadingPlaceholderKeyboard, false)
|
||||
recycle()
|
||||
}
|
||||
orientation = VERTICAL
|
||||
flexDirection = FlexDirection.COLUMN
|
||||
justifyContent = JustifyContent.SPACE_BETWEEN
|
||||
layoutParams = layoutParams ?: FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||
@@ -132,7 +131,6 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
* Dismisses all shown key popups when keyboard is detached from window.
|
||||
*/
|
||||
override fun onDetachedFromWindow() {
|
||||
popupManager.dismissAllPopups()
|
||||
if (!isPreviewMode) {
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
}
|
||||
@@ -171,92 +169,93 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
event ?: return false
|
||||
if (isPreviewMode || isLoadingPlaceholderKeyboard) {
|
||||
return false
|
||||
}
|
||||
val eventFloris = MotionEvent.obtainNoHistory(event)
|
||||
if (event == null || isPreviewMode || isLoadingPlaceholderKeyboard) return false
|
||||
|
||||
if (!isSmartbarKeyboardView && swipeGestureDetector.onTouchEvent(event)) {
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_CANCEL)
|
||||
activeKeyView = null
|
||||
activePointerId = null
|
||||
for (pointerIndex in 0 until event.pointerCount) {
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_CANCEL)
|
||||
activeKeyViews.remove(pointerId)
|
||||
}
|
||||
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(KeyData.INTERNAL_BATCH_EDIT))
|
||||
}
|
||||
return true
|
||||
}
|
||||
val pointerIndex = event.actionIndex
|
||||
var pointerId = event.getPointerId(pointerIndex)
|
||||
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN,
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.down(KeyData.INTERNAL_BATCH_EDIT))
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
searchForActiveKeyView(event, pointerIndex, pointerId)
|
||||
initialKeyCodes[pointerId] = activeKeyViews[pointerId]?.data?.code ?: 0
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
|
||||
}
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
if (activePointerId == null) {
|
||||
activePointerId = pointerId
|
||||
activeX = event.getX(pointerIndex)
|
||||
activeY = event.getY(pointerIndex)
|
||||
searchForActiveKeyView()
|
||||
initialKeyCode = activeKeyView?.data?.code ?: 0
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
|
||||
} else if (activePointerId != pointerId) {
|
||||
// New pointer arrived. Send ACTION_UP to current active view and move on
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_UP)
|
||||
activePointerId = pointerId
|
||||
activeX = event.getX(pointerIndex)
|
||||
activeY = event.getY(pointerIndex)
|
||||
searchForActiveKeyView()
|
||||
initialKeyCode = activeKeyView?.data?.code ?: 0
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
|
||||
}
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
searchForActiveKeyView(event, pointerIndex, pointerId)
|
||||
initialKeyCodes[pointerId] = activeKeyViews[pointerId]?.data?.code ?: 0
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
for (index in 0 until event.pointerCount) {
|
||||
pointerId = event.getPointerId(index)
|
||||
if (activePointerId == pointerId) {
|
||||
activeX = event.getX(index)
|
||||
activeY = event.getY(index)
|
||||
if (activeKeyView == null) {
|
||||
searchForActiveKeyView()
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
|
||||
} else {
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_MOVE)
|
||||
}
|
||||
for (pointerIndex in 0 until event.pointerCount) {
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
if (!activeKeyViews.containsKey(pointerId)) {
|
||||
searchForActiveKeyView(event, pointerIndex, pointerId)
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
|
||||
} else {
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_MOVE)
|
||||
}
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL,
|
||||
MotionEvent.ACTION_POINTER_UP -> {
|
||||
if (activePointerId == pointerId) {
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_UP)
|
||||
activeKeyView = null
|
||||
activePointerId = null
|
||||
}
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, event.actionMasked)
|
||||
activeKeyViews.remove(pointerId)
|
||||
}
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, event.actionMasked)
|
||||
activeKeyViews.remove(pointerId)
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(KeyData.INTERNAL_BATCH_EDIT))
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
eventFloris.recycle()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a touch [event] to [activeKeyView] with action set to [actionParam]. Normalizes passed
|
||||
* actions (ACTION_POINTER_* will be converted to ACTION_*). Translates the absolute coords of
|
||||
* a passed [event] to relative ones so the [activeKeyView] can work with it.
|
||||
* Sends a touch [event] to the active key view which is associated with given [pointerId]. The action of the
|
||||
* event is set to [actionParam]. Normalizes passed actions (ACTION_POINTER_* will be converted to ACTION_*).
|
||||
* Translates the absolute coords of a passed [event] to relative ones so the active key view can work with it.
|
||||
*
|
||||
* @param event The event to pass to [activeKeyView].
|
||||
* @param event The event to pass to the active key view.
|
||||
* @param pointerIndex The index of the pointer, used for getting coordinates.
|
||||
* @param pointerId The unique ID of the pointer, used to reference the active key view.
|
||||
* @param actionParam The action to set the [event] to.
|
||||
*/
|
||||
private fun sendFlorisTouchEvent(event: MotionEvent, actionParam: Int) {
|
||||
val keyView = activeKeyView ?: return
|
||||
val keyViewParent = keyView.parent as ViewGroup
|
||||
keyView.onFlorisTouchEvent(event.apply {
|
||||
action = when (actionParam) {
|
||||
private fun sendFlorisTouchEvent(event: MotionEvent, pointerIndex: Int, pointerId: Int, actionParam: Int) {
|
||||
val keyView = activeKeyViews[pointerId] ?: return
|
||||
val keyViewParent = keyView.parent as? ViewGroup ?: return
|
||||
val eventToSend = MotionEvent.obtain(
|
||||
event.downTime,
|
||||
event.eventTime,
|
||||
when (actionParam) {
|
||||
MotionEvent.ACTION_POINTER_DOWN -> MotionEvent.ACTION_DOWN
|
||||
MotionEvent.ACTION_POINTER_UP -> MotionEvent.ACTION_UP
|
||||
else -> actionParam
|
||||
}
|
||||
setLocation(
|
||||
activeX - keyViewParent.x - keyView.x,
|
||||
activeY - keyViewParent.y - keyView.y
|
||||
)
|
||||
})
|
||||
},
|
||||
event.getX(pointerIndex) - keyViewParent.x - keyView.x,
|
||||
event.getY(pointerIndex) - keyViewParent.y - keyView.y,
|
||||
0
|
||||
)
|
||||
keyView.onFlorisTouchEvent(eventToSend)
|
||||
eventToSend.recycle()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,7 +264,7 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
*/
|
||||
override fun onSwipe(event: SwipeGesture.Event): Boolean {
|
||||
return when {
|
||||
initialKeyCode == KeyCode.DELETE -> {
|
||||
initialKeyCodes[event.pointerId] == KeyCode.DELETE -> {
|
||||
if (event.type == SwipeGesture.Type.TOUCH_UP && event.direction == SwipeGesture.Direction.LEFT &&
|
||||
prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_WORD) {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.deleteKeySwipeLeft)
|
||||
@@ -274,7 +273,16 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
false
|
||||
}
|
||||
}
|
||||
initialKeyCode > KeyCode.SPACE && !popupManager.isShowingExtendedPopup -> when {
|
||||
initialKeyCodes[event.pointerId] == KeyCode.SHIFT && activeKeyViews[event.pointerId]?.data?.code != KeyCode.SHIFT &&
|
||||
event.type == SwipeGesture.Type.TOUCH_UP -> {
|
||||
activeKeyViews[event.pointerId]?.let {
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(it.popupManager.getActiveKeyData(it) ?: it.data))
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.cancel(KeyData.SHIFT))
|
||||
}
|
||||
true
|
||||
}
|
||||
initialKeyCodes[event.pointerId] ?: 0 > KeyCode.SPACE &&
|
||||
activeKeyViews[event.pointerId]?.popupManager?.isShowingExtendedPopup == false -> when {
|
||||
!prefs.glide.enabled -> when (event.type) {
|
||||
SwipeGesture.Type.TOUCH_UP -> {
|
||||
val swipeAction = when (event.direction) {
|
||||
@@ -300,15 +308,17 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for an active key view at [activeX]/[activeY].
|
||||
* Searches for an active key view at the passed pointer location.
|
||||
*/
|
||||
private fun searchForActiveKeyView() {
|
||||
private fun searchForActiveKeyView(event: MotionEvent, pointerIndex: Int, pointerId: Int) {
|
||||
val activeX = event.getX(pointerIndex)
|
||||
val activeY = event.getY(pointerIndex)
|
||||
loop@ for (row in children) {
|
||||
if (row is FlexboxLayout) {
|
||||
for (keyView in row.children) {
|
||||
if (keyView is KeyView) {
|
||||
if (keyView.touchHitBox.contains(activeX.toInt(), activeY.toInt())) {
|
||||
activeKeyView = keyView
|
||||
activeKeyViews[pointerId] = keyView
|
||||
break@loop
|
||||
}
|
||||
}
|
||||
@@ -318,14 +328,12 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the current [activeKeyView] and sends a [MotionEvent.ACTION_CANCEL] to indicate
|
||||
* the loss of focus.
|
||||
* Invalidates the current active key view and sends a [MotionEvent.ACTION_CANCEL] to indicate the loss of focus.
|
||||
*/
|
||||
fun dismissActiveKeyViewReference() {
|
||||
activeKeyView?.onFlorisTouchEvent(MotionEvent.obtain(
|
||||
fun dismissActiveKeyViewReference(pointerId: Int) {
|
||||
activeKeyViews.remove(pointerId)?.onFlorisTouchEvent(MotionEvent.obtain(
|
||||
0, 0, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0
|
||||
))
|
||||
activeKeyView = null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,9 +362,17 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
} else {
|
||||
(florisboard?.inputView?.desiredTextKeyboardViewHeight ?: MeasureSpec.getSize(heightMeasureSpec).toFloat())
|
||||
} * if (isPreviewMode) { 0.90f } else { 1.00f }
|
||||
val layoutSize = computedLayout?.arrangement?.size?.toFloat() ?: 4.0f
|
||||
desiredKeyHeight = when {
|
||||
isSmartbarKeyboardView -> desiredHeight - 1.5f * keyMarginV
|
||||
else -> desiredHeight / (computedLayout?.arrangement?.size?.toFloat() ?: 4.0f) - 2.0f * keyMarginV
|
||||
isSmartbarKeyboardView -> {
|
||||
desiredHeight - 1.5f * keyMarginV
|
||||
}
|
||||
florisboard?.inputView?.shouldGiveAdditionalSpace == true -> {
|
||||
desiredHeight / (layoutSize + 0.5f).coerceAtMost(5.0f) - 2.0f * keyMarginV
|
||||
}
|
||||
else -> {
|
||||
desiredHeight / layoutSize - 2.0f * keyMarginV
|
||||
}
|
||||
}.roundToInt()
|
||||
|
||||
super.onMeasure(
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package dev.patrickgold.florisboard.ime.text.layout
|
||||
|
||||
import android.content.Context
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
@@ -78,10 +77,10 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
source = AssetSource.Assets,
|
||||
path = PopupManager.POPUP_EXTENSION_PATH_REL + "/" + (subtype?.locale?.language ?: "\$default") + ".json"
|
||||
)
|
||||
assetManager.loadAsset(langTagRef, PopupExtension::class.java).onSuccess {
|
||||
assetManager.loadAsset(langTagRef, PopupExtension::class).onSuccess {
|
||||
return it
|
||||
}
|
||||
assetManager.loadAsset(langRef, PopupExtension::class.java).onSuccess {
|
||||
assetManager.loadAsset(langRef, PopupExtension::class).onSuccess {
|
||||
return it
|
||||
}
|
||||
return PopupExtension.empty()
|
||||
@@ -210,7 +209,15 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
popupSet = extendedPopups.mapping[KeyVariation.ALL]?.get(label) ?:
|
||||
extendedPopupsDefault.mapping[KeyVariation.ALL]?.get(label)
|
||||
}
|
||||
popupSet?.let { key.popup.merge(it) }
|
||||
var keySpecificPopupSet: PopupSet<KeyData>? = null
|
||||
if (label != key.label) {
|
||||
keySpecificPopupSet = extendedPopups.mapping[KeyVariation.ALL]?.get(key.label) ?:
|
||||
extendedPopupsDefault.mapping[KeyVariation.ALL]?.get(key.label)
|
||||
}
|
||||
key.popup.apply {
|
||||
keySpecificPopupSet?.let { merge(it) }
|
||||
popupSet?.let { merge(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.smartbar
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.Typeface
|
||||
import android.text.TextPaint
|
||||
import android.text.TextUtils
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.VelocityTracker
|
||||
import android.view.View
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.OverScroller
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
|
||||
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeValue
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
/**
|
||||
* A candidate view allowing for easy of suggestions. Additionally it also features an integrated clipboard suggestion
|
||||
* support, which works together with the normal suggestions provided by the NLP algorithm.
|
||||
*/
|
||||
class CandidateView : View, ThemeManager.OnThemeUpdatedListener {
|
||||
private var themeManager: ThemeManager? = null
|
||||
private var florisClipboardManager: FlorisClipboardManager? = null
|
||||
private var eventListener: WeakReference<SmartbarView.EventListener?> = WeakReference(null)
|
||||
private var displayMode: DisplayMode = DisplayMode.DYNAMIC_SCROLLABLE
|
||||
|
||||
private val candidates: ArrayList<String> = ArrayList()
|
||||
private var clipboardItem: ClipboardItem? = null
|
||||
private var clipboardItemTime: Long = 0
|
||||
private var clipboardItemTimeout: Int = 60_000
|
||||
private var computedCandidates: ArrayList<ComputedCandidate> = ArrayList()
|
||||
private var computedCandidatesWidthPx: Int = 0
|
||||
private var selectedIndex: Int = -1
|
||||
|
||||
private var backgroundPaint: Paint = Paint().apply { color = Color.BLACK }
|
||||
private var candidateBackground: ThemeValue = ThemeValue.SolidColor.TRANSPARENT
|
||||
private var candidateForeground: ThemeValue = ThemeValue.SolidColor.TRANSPARENT
|
||||
private val candidateMarginH: Int = resources.getDimensionPixelOffset(R.dimen.smartbar_candidate_marginH)
|
||||
private var dividerBackground: ThemeValue = ThemeValue.SolidColor.TRANSPARENT
|
||||
private var dividerPaint: Paint = Paint().apply { color = Color.BLACK }
|
||||
private var dividerWidth: Int = resources.getDimensionPixelSize(R.dimen.smartbar_divider_width)
|
||||
private val pasteDrawable = ContextCompat.getDrawable(context, R.drawable.ic_content_paste)
|
||||
private var lastX: Float = 0.0f
|
||||
private val scroller: OverScroller = OverScroller(context, AccelerateDecelerateInterpolator())
|
||||
private val textPaint: TextPaint = TextPaint().apply {
|
||||
alpha = 255
|
||||
color = Color.BLACK
|
||||
isAntiAlias = true
|
||||
isFakeBoldText = false
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = resources.getDimension(R.dimen.smartbar_candidate_textSize)
|
||||
typeface = Typeface.DEFAULT
|
||||
}
|
||||
private var velocityTracker: VelocityTracker? = null
|
||||
|
||||
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)
|
||||
|
||||
init {
|
||||
isHorizontalScrollBarEnabled = false
|
||||
isVerticalScrollBarEnabled = false
|
||||
scrollTo(0, 0)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
themeManager = ThemeManager.defaultOrNull()
|
||||
themeManager?.registerOnThemeUpdatedListener(this)
|
||||
florisClipboardManager = FlorisClipboardManager.getInstanceOrNull()
|
||||
updateCandidates(candidates)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
themeManager?.unregisterOnThemeUpdatedListener(this)
|
||||
themeManager = null
|
||||
florisClipboardManager = null
|
||||
candidates.clear()
|
||||
velocityTracker?.recycle()
|
||||
velocityTracker = null
|
||||
}
|
||||
|
||||
fun updateCandidates(newCandidates: List<String>?) {
|
||||
candidates.clear()
|
||||
if (newCandidates != null) {
|
||||
candidates.addAll(newCandidates)
|
||||
}
|
||||
recomputeCandidates()
|
||||
}
|
||||
|
||||
fun updateClipboardItem(newClipboardCandidate: ClipboardItem) {
|
||||
clipboardItem = newClipboardCandidate
|
||||
clipboardItemTime = System.currentTimeMillis()
|
||||
recomputeCandidates()
|
||||
}
|
||||
|
||||
fun setEventListener(listener: SmartbarView.EventListener) {
|
||||
eventListener = WeakReference(listener)
|
||||
}
|
||||
|
||||
fun updateDisplaySettings(newDisplayMode: DisplayMode, newClipboardContentTimeout: Int) {
|
||||
if (newClipboardContentTimeout != clipboardItemTimeout) {
|
||||
clipboardItemTimeout = newClipboardContentTimeout
|
||||
}
|
||||
if (newDisplayMode != displayMode) {
|
||||
displayMode = newDisplayMode
|
||||
scroller.abortAnimation()
|
||||
scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun recomputeCandidates() {
|
||||
computedCandidates.clear()
|
||||
if (clipboardItem != null && System.currentTimeMillis() - clipboardItemTime > clipboardItemTimeout) {
|
||||
clipboardItem = null
|
||||
}
|
||||
val classicCandidateWidth = (measuredWidth - 2 * dividerWidth) / 3
|
||||
val maxDynamicCandidateWidth = (measuredWidth * 0.7).toInt()
|
||||
val clipItem = clipboardItem
|
||||
val clipItemAvailable = clipItem != null && florisClipboardManager?.canBePasted(clipItem) == true
|
||||
computedCandidatesWidthPx = 0
|
||||
if (candidates.isEmpty()) {
|
||||
if (clipItemAvailable) {
|
||||
computedCandidates.add(ComputedCandidate.Clip(clipItem!!, Rect(
|
||||
0,
|
||||
0,
|
||||
measuredWidth,
|
||||
measuredHeight
|
||||
)))
|
||||
} else if (displayMode == DisplayMode.CLASSIC) {
|
||||
for (n in 0 until 3) {
|
||||
val left = (classicCandidateWidth + dividerWidth) * n
|
||||
computedCandidates.add(ComputedCandidate.Empty(Rect(
|
||||
left,
|
||||
0,
|
||||
left + classicCandidateWidth,
|
||||
measuredHeight
|
||||
)))
|
||||
}
|
||||
}
|
||||
} else if (candidates.size == 1 && !clipItemAvailable) {
|
||||
computedCandidates.add(ComputedCandidate.Word(candidates[0], Rect(
|
||||
0,
|
||||
0,
|
||||
measuredWidth,
|
||||
measuredHeight
|
||||
)))
|
||||
} else {
|
||||
when (displayMode) {
|
||||
DisplayMode.CLASSIC -> {
|
||||
if (!clipItemAvailable) {
|
||||
for (n in 0 until candidates.size.coerceAtMost(3)) {
|
||||
val left = (classicCandidateWidth + dividerWidth) * n
|
||||
computedCandidates.add(ComputedCandidate.Word(candidates[n], Rect(
|
||||
left,
|
||||
0,
|
||||
left + classicCandidateWidth,
|
||||
measuredHeight
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
computedCandidates.add(ComputedCandidate.Clip(clipItem!!, Rect(
|
||||
0,
|
||||
0,
|
||||
classicCandidateWidth,
|
||||
measuredHeight
|
||||
)))
|
||||
for (n in 0 until candidates.size.coerceAtMost(2)) {
|
||||
val left = (classicCandidateWidth + dividerWidth) * (n + 1)
|
||||
computedCandidates.add(ComputedCandidate.Word(candidates[n], Rect(
|
||||
left,
|
||||
0,
|
||||
left + classicCandidateWidth,
|
||||
measuredHeight
|
||||
)))
|
||||
}
|
||||
}
|
||||
if (computedCandidates.size < 3) {
|
||||
for (n in computedCandidates.size until 3) {
|
||||
val left = (classicCandidateWidth + dividerWidth) * n
|
||||
computedCandidates.add(ComputedCandidate.Empty(Rect(
|
||||
left,
|
||||
0,
|
||||
left + classicCandidateWidth,
|
||||
measuredHeight
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplayMode.DYNAMIC -> {
|
||||
if (clipItemAvailable) {
|
||||
val candidateWidth = (textPaint.measureText(clipItem!!.stringRepresentation()).toInt() + candidateMarginH + measuredHeight * 4 / 6).coerceAtMost(maxDynamicCandidateWidth)
|
||||
computedCandidates.add(ComputedCandidate.Clip(clipItem, Rect(
|
||||
0,
|
||||
0,
|
||||
candidateWidth,
|
||||
measuredHeight
|
||||
)))
|
||||
computedCandidatesWidthPx += candidateWidth
|
||||
}
|
||||
for (n in candidates.indices) {
|
||||
var tmpWidthPx = computedCandidatesWidthPx
|
||||
if (tmpWidthPx > 0) {
|
||||
tmpWidthPx += dividerWidth
|
||||
}
|
||||
val candidateWidth = (textPaint.measureText(candidates[n]).toInt() + 2 * candidateMarginH).coerceAtMost(maxDynamicCandidateWidth)
|
||||
tmpWidthPx += candidateWidth
|
||||
if (tmpWidthPx > measuredWidth) {
|
||||
break
|
||||
} else {
|
||||
computedCandidates.add(ComputedCandidate.Word(candidates[n], Rect(
|
||||
computedCandidatesWidthPx,
|
||||
0,
|
||||
computedCandidatesWidthPx + candidateWidth,
|
||||
measuredHeight
|
||||
)))
|
||||
computedCandidatesWidthPx = tmpWidthPx
|
||||
}
|
||||
}
|
||||
val widthToIncreasePerItem = (measuredWidth - computedCandidatesWidthPx) / computedCandidates.size
|
||||
for (n in computedCandidates.indices) {
|
||||
computedCandidates[n].geometry.let {
|
||||
it.left += n * widthToIncreasePerItem
|
||||
it.right += (n + 1) * widthToIncreasePerItem
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplayMode.DYNAMIC_SCROLLABLE -> {
|
||||
if (clipItemAvailable) {
|
||||
val candidateWidth = (textPaint.measureText(clipItem!!.stringRepresentation()).toInt() + candidateMarginH + measuredHeight * 4 / 6).coerceAtMost(maxDynamicCandidateWidth)
|
||||
computedCandidates.add(ComputedCandidate.Clip(clipItem, Rect(
|
||||
0,
|
||||
0,
|
||||
candidateWidth,
|
||||
measuredHeight
|
||||
)))
|
||||
computedCandidatesWidthPx += candidateWidth
|
||||
}
|
||||
for (n in candidates.indices) {
|
||||
if (computedCandidatesWidthPx > 0) {
|
||||
computedCandidatesWidthPx += dividerWidth
|
||||
}
|
||||
val candidateWidth = (textPaint.measureText(candidates[n]).toInt() + 2 * candidateMarginH).coerceAtMost(maxDynamicCandidateWidth)
|
||||
computedCandidates.add(ComputedCandidate.Word(candidates[n], Rect(
|
||||
computedCandidatesWidthPx,
|
||||
0,
|
||||
computedCandidatesWidthPx + candidateWidth,
|
||||
measuredHeight
|
||||
)))
|
||||
computedCandidatesWidthPx += candidateWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectedIndex = -1
|
||||
scroller.abortAnimation()
|
||||
scrollTo(0, 0)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun computeScroll() {
|
||||
if (scroller.computeScrollOffset()) {
|
||||
scrollTo(scroller.currX, scroller.currY)
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun determineSelectedIndex(relX: Int, relY: Int): Int {
|
||||
val absX = relX + scrollX
|
||||
val absY = relY + scrollY
|
||||
var retIndex = -1
|
||||
for ((n, computedCandidate) in computedCandidates.withIndex()) {
|
||||
if (computedCandidate.geometry.contains(absX, absY)) {
|
||||
retIndex = n
|
||||
break
|
||||
}
|
||||
}
|
||||
return retIndex
|
||||
}
|
||||
|
||||
private fun onCandidateClick(index: Int) {
|
||||
computedCandidates.getOrNull(index)?.let { candidate ->
|
||||
when (candidate) {
|
||||
is ComputedCandidate.Word -> {
|
||||
eventListener.get()?.onSmartbarCandidatePressed(candidate.word)
|
||||
}
|
||||
is ComputedCandidate.Clip -> {
|
||||
eventListener.get()?.onSmartbarClipboardCandidatePressed(candidate.clipboardItem)
|
||||
}
|
||||
is ComputedCandidate.Empty -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
super.onTouchEvent(event)
|
||||
event ?: return false
|
||||
|
||||
return when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
isPressed = true
|
||||
selectedIndex = determineSelectedIndex(event.x.toInt(), event.y.toInt())
|
||||
if (displayMode == DisplayMode.DYNAMIC_SCROLLABLE) {
|
||||
velocityTracker?.recycle()
|
||||
velocityTracker = VelocityTracker.obtain()
|
||||
velocityTracker?.addMovement(event)
|
||||
lastX = event.x
|
||||
}
|
||||
invalidate()
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> when (displayMode) {
|
||||
DisplayMode.CLASSIC,
|
||||
DisplayMode.DYNAMIC -> {
|
||||
computedCandidates.getOrNull(selectedIndex)?.let { candidate ->
|
||||
if (!candidate.geometry.contains(scrollX + event.x.toInt(), scrollY + event.y.toInt())) {
|
||||
selectedIndex = -1
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
DisplayMode.DYNAMIC_SCROLLABLE -> {
|
||||
velocityTracker?.addMovement(event)
|
||||
selectedIndex = -1
|
||||
if (computedCandidatesWidthPx > measuredWidth) {
|
||||
scrollTo((scrollX + lastX - event.x).toInt().coerceIn(0, computedCandidatesWidthPx - measuredWidth), 0)
|
||||
lastX = event.x
|
||||
}
|
||||
invalidate()
|
||||
true
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
isPressed = false
|
||||
if (event.actionMasked == MotionEvent.ACTION_UP) {
|
||||
onCandidateClick(selectedIndex)
|
||||
}
|
||||
selectedIndex = -1
|
||||
if (displayMode == DisplayMode.DYNAMIC_SCROLLABLE) {
|
||||
velocityTracker?.let {
|
||||
it.addMovement(event)
|
||||
it.computeCurrentVelocity(1000)
|
||||
|
||||
if (computedCandidatesWidthPx > measuredWidth) {
|
||||
scroller.fling(
|
||||
scrollX, 0,
|
||||
-it.xVelocity.toInt(), 0,
|
||||
0, computedCandidatesWidthPx - measuredWidth,
|
||||
0, 0,
|
||||
0, 0
|
||||
)
|
||||
}
|
||||
it.recycle()
|
||||
velocityTracker = null
|
||||
}
|
||||
}
|
||||
invalidate()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
candidateBackground = theme.getAttr(Theme.Attr.WINDOW_SEMI_TRANSPARENT_COLOR)
|
||||
candidateForeground = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND)
|
||||
dividerBackground = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND_ALT)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
canvas ?: return
|
||||
backgroundPaint.apply { color = candidateBackground.toSolidColor().color }
|
||||
dividerPaint.apply { color = ColorUtils.setAlphaComponent(dividerBackground.toSolidColor().color, 64) }
|
||||
textPaint.apply { color = candidateForeground.toSolidColor().color }
|
||||
for ((n, computedCandidate) in computedCandidates.withIndex()) {
|
||||
with(computedCandidate) {
|
||||
if (n == selectedIndex) {
|
||||
canvas.drawRect(geometry, backgroundPaint)
|
||||
}
|
||||
when (this) {
|
||||
is ComputedCandidate.Word -> {
|
||||
val ellipsizedWord = TextUtils.ellipsize(
|
||||
word, textPaint, geometry.width().toFloat() - candidateMarginH, TextUtils.TruncateAt.MIDDLE
|
||||
).toString()
|
||||
canvas.drawText(
|
||||
ellipsizedWord,
|
||||
geometry.left + geometry.width() / 2.0f,
|
||||
geometry.top + geometry.height() / 2.0f - (textPaint.descent() + textPaint.ascent()) / 2.0f,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
is ComputedCandidate.Clip -> {
|
||||
pasteDrawable?.setTint(candidateForeground.toSolidColor().color)
|
||||
pasteDrawable?.setBounds(
|
||||
geometry.left + geometry.height() / 3,
|
||||
geometry.height() / 3,
|
||||
geometry.left + geometry.height() * 2 / 3,
|
||||
geometry.height() * 2 / 3
|
||||
)
|
||||
pasteDrawable?.draw(canvas)
|
||||
val pdWidth = geometry.height().toFloat()
|
||||
val ellipsizedWord = TextUtils.ellipsize(
|
||||
clipboardItem.stringRepresentation(), textPaint, geometry.width().toFloat() - pdWidth, TextUtils.TruncateAt.MIDDLE
|
||||
).toString()
|
||||
canvas.drawText(
|
||||
ellipsizedWord,
|
||||
geometry.left + geometry.width() / 2.0f + pdWidth / 2.0f - candidateMarginH,
|
||||
geometry.top + geometry.height() / 2.0f - (textPaint.descent() + textPaint.ascent()) / 2.0f,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
is ComputedCandidate.Empty -> {
|
||||
}
|
||||
}
|
||||
if (n + 1 < computedCandidates.size) {
|
||||
canvas.drawRect(
|
||||
geometry.right.toFloat(),
|
||||
(geometry.height() / 4).toFloat(),
|
||||
(geometry.right + dividerWidth).toFloat(),
|
||||
(geometry.height() * 3 / 4).toFloat(),
|
||||
dividerPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class describing a computed candidate item.
|
||||
*
|
||||
* @property geometry The geometry of the computed candidate, used to position and size the item correctly when
|
||||
* being drawn on a canvas.
|
||||
*/
|
||||
private sealed class ComputedCandidate(val geometry: Rect) {
|
||||
/**
|
||||
* Computed word candidate, used for suggestions provided by the NLP algorithm.
|
||||
*
|
||||
* @property word The word this computed candidate item represents. Used in the callback to provide which word
|
||||
* should be filled out.
|
||||
*/
|
||||
class Word(
|
||||
val word: String,
|
||||
geometry: Rect
|
||||
) : ComputedCandidate(geometry) {
|
||||
override fun toString(): String {
|
||||
return "Word { word=\"$word\", geometry=$geometry }"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed word candidate, used for filling space when [DisplayMode.CLASSIC] is active. Does not hold any data
|
||||
* and also does nothing when clicked on.
|
||||
*/
|
||||
class Empty(
|
||||
geometry: Rect
|
||||
) : ComputedCandidate(geometry) {
|
||||
override fun toString(): String {
|
||||
return "Empty { geometry=$geometry }"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed word candidate, used for clipboard paste suggestions.
|
||||
*
|
||||
* @property clipboardItem The clipboard item this computed candidate item represents. Used in the callback to
|
||||
* provide which item should be pasted.
|
||||
*/
|
||||
class Clip(
|
||||
val clipboardItem: ClipboardItem,
|
||||
geometry: Rect
|
||||
) : ComputedCandidate(geometry) {
|
||||
override fun toString(): String {
|
||||
return "Clip { clipboardItem=$clipboardItem, geometry=$geometry }"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum class defining the display mode for the candidate view.
|
||||
*/
|
||||
enum class DisplayMode {
|
||||
CLASSIC,
|
||||
DYNAMIC,
|
||||
DYNAMIC_SCROLLABLE;
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String): DisplayMode {
|
||||
return valueOf(string.toUpperCase(Locale.ENGLISH))
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return super.toString().toLowerCase(Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,12 @@ package dev.patrickgold.florisboard.ime.text.smartbar
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.children
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.SmartbarBinding
|
||||
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
@@ -32,8 +32,6 @@ import dev.patrickgold.florisboard.ime.text.key.KeyVariation
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
|
||||
import dev.patrickgold.florisboard.util.setDrawableTintColor2
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -51,7 +49,7 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private val themeManager = ThemeManager.default()
|
||||
private var eventListener: WeakReference<EventListener?>? = null
|
||||
private var eventListener: WeakReference<EventListener?> = WeakReference(null)
|
||||
private val mainScope = MainScope()
|
||||
private var lastSuggestionInitDate: Long = 0
|
||||
|
||||
@@ -66,15 +64,12 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
binding.quickActionToggle.rotation = if (v) 180.0f else 0.0f
|
||||
field = v
|
||||
}
|
||||
private var shouldSuggestClipboardContents: Boolean = false
|
||||
|
||||
private lateinit var binding: SmartbarBinding
|
||||
private var indexedActionStartArea: MutableList<Int> = mutableListOf()
|
||||
private var indexedMainArea: MutableList<Int> = mutableListOf()
|
||||
private var indexedActionEndArea: MutableList<Int> = mutableListOf()
|
||||
|
||||
private var candidateViewList: MutableList<Button> = mutableListOf()
|
||||
|
||||
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)
|
||||
@@ -102,11 +97,9 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
indexedActionEndArea.add(view.id)
|
||||
}
|
||||
|
||||
candidateViewList.add(binding.candidate0)
|
||||
candidateViewList.add(binding.candidate1)
|
||||
candidateViewList.add(binding.candidate2)
|
||||
binding.backButton.setOnClickListener { eventListener.get()?.onSmartbarBackButtonPressed() }
|
||||
|
||||
binding.backButton.setOnClickListener { eventListener?.get()?.onSmartbarBackButtonPressed() }
|
||||
binding.candidates.updateDisplaySettings(prefs.suggestion.displayMode, prefs.suggestion.clipboardContentTimeout * 1_000)
|
||||
|
||||
mainScope.launch(Dispatchers.Default) {
|
||||
florisboard?.let {
|
||||
@@ -122,28 +115,6 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
}
|
||||
}
|
||||
|
||||
binding.candidate0.setOnClickListener {
|
||||
if (it is Button) {
|
||||
eventListener?.get()?.onSmartbarCandidatePressed(it.text.toString())
|
||||
}
|
||||
}
|
||||
binding.candidate1.setOnClickListener {
|
||||
if (it is Button) {
|
||||
eventListener?.get()?.onSmartbarCandidatePressed(it.text.toString())
|
||||
}
|
||||
}
|
||||
binding.candidate2.setOnClickListener {
|
||||
if (it is Button) {
|
||||
eventListener?.get()?.onSmartbarCandidatePressed(it.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
binding.clipboardSuggestion.setOnClickListener {
|
||||
florisboard?.activeEditorInstance?.performClipboardPaste()
|
||||
shouldSuggestClipboardContents = false
|
||||
updateSmartbarState()
|
||||
}
|
||||
|
||||
mainScope.launch(Dispatchers.Default) {
|
||||
florisboard?.let {
|
||||
val layout = it.textInputManager.layoutManager.fetchComputedLayoutAsync(
|
||||
@@ -159,13 +130,13 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
}
|
||||
|
||||
binding.privateModeButton.setOnClickListener {
|
||||
eventListener?.get()?.onSmartbarPrivateModeButtonClicked()
|
||||
eventListener.get()?.onSmartbarPrivateModeButtonClicked()
|
||||
}
|
||||
|
||||
for (quickAction in binding.quickActions.children) {
|
||||
if (quickAction is SmartbarQuickActionButton) {
|
||||
quickAction.id.let { quickActionId ->
|
||||
quickAction.setOnClickListener { eventListener?.get()?.onSmartbarQuickActionPressed(quickActionId) }
|
||||
quickAction.setOnClickListener { eventListener.get()?.onSmartbarQuickActionPressed(quickActionId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +160,6 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
eventListener = null
|
||||
florisboard?.textInputManager?.unregisterSmartbarView(this)
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
@@ -248,6 +218,7 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
*/
|
||||
fun updateSmartbarState() {
|
||||
binding.clipboardCursorRow.updateVisibility()
|
||||
binding.candidates.updateDisplaySettings(prefs.suggestion.displayMode, prefs.suggestion.clipboardContentTimeout * 1_000)
|
||||
when (florisboard) {
|
||||
null -> configureFeatureVisibility(
|
||||
actionStartAreaVisible = false,
|
||||
@@ -275,9 +246,6 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> null
|
||||
else -> when {
|
||||
florisboard.activeEditorInstance.isComposingEnabled &&
|
||||
shouldSuggestClipboardContents
|
||||
-> R.id.clipboard_suggestion_row
|
||||
florisboard.activeEditorInstance.isComposingEnabled &&
|
||||
florisboard.activeEditorInstance.selection.isCursorMode
|
||||
-> R.id.candidates
|
||||
@@ -299,50 +267,21 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
}
|
||||
|
||||
fun onPrimaryClipChanged() {
|
||||
if (prefs.suggestion.enabled && prefs.suggestion.suggestClipboardContent &&
|
||||
florisboard?.activeEditorInstance?.isPrivateMode == false) {
|
||||
shouldSuggestClipboardContents = true
|
||||
val item = florisboard.clipboardManager?.primaryClip?.getItemAt(0)
|
||||
when {
|
||||
item?.text != null -> {
|
||||
binding.clipboardSuggestion.text = item.text
|
||||
}
|
||||
item?.uri != null -> {
|
||||
binding.clipboardSuggestion.text = "(Image) " + item.uri.toString()
|
||||
}
|
||||
else -> {
|
||||
binding.clipboardSuggestion.text = item?.text ?: "(Error while retrieving clipboard data)"
|
||||
}
|
||||
}
|
||||
if (prefs.suggestion.enabled && prefs.suggestion.clipboardContentEnabled && florisboard?.activeEditorInstance?.isPrivateMode == false ) {
|
||||
florisboard.florisClipboardManager?.primaryClip?.let { binding.candidates.updateClipboardItem(it) }
|
||||
updateSmartbarState()
|
||||
}
|
||||
}
|
||||
|
||||
fun resetClipboardSuggestion() {
|
||||
shouldSuggestClipboardContents = false
|
||||
updateSmartbarState()
|
||||
}
|
||||
|
||||
fun setCandidateSuggestionWords(suggestionInitDate: Long, suggestions: List<String>) {
|
||||
fun setCandidateSuggestionWords(suggestionInitDate: Long, suggestions: List<String>?) {
|
||||
if (suggestionInitDate > lastSuggestionInitDate) {
|
||||
lastSuggestionInitDate = suggestionInitDate
|
||||
binding.candidate1.text = suggestions.getOrNull(0) ?: ""
|
||||
binding.candidate0.text = suggestions.getOrNull(1) ?: ""
|
||||
binding.candidate2.text = suggestions.getOrNull(2) ?: ""
|
||||
binding.candidates.updateCandidates(suggestions)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCandidateSuggestionCapsState() {
|
||||
val tim = florisboard?.textInputManager ?: return
|
||||
if (tim.capsLock) {
|
||||
binding.candidate0.text = binding.candidate0.text.toString().toUpperCase(florisboard.activeSubtype.locale)
|
||||
binding.candidate1.text = binding.candidate1.text.toString().toUpperCase(florisboard.activeSubtype.locale)
|
||||
binding.candidate2.text = binding.candidate2.text.toString().toUpperCase(florisboard.activeSubtype.locale)
|
||||
} else {
|
||||
binding.candidate0.text = binding.candidate0.text.toString().toLowerCase(florisboard.activeSubtype.locale)
|
||||
binding.candidate1.text = binding.candidate1.text.toString().toLowerCase(florisboard.activeSubtype.locale)
|
||||
binding.candidate2.text = binding.candidate2.text.toString().toLowerCase(florisboard.activeSubtype.locale)
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
@@ -368,17 +307,12 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
|
||||
override fun onThemeUpdated(theme: Theme) {
|
||||
setBackgroundColor(theme.getAttr(Theme.Attr.SMARTBAR_BACKGROUND).toSolidColor().color)
|
||||
setBackgroundTintColor2(binding.clipboardSuggestion, theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_BACKGROUND).toSolidColor().color)
|
||||
setDrawableTintColor2(binding.clipboardSuggestion, theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_FOREGROUND).toSolidColor().color)
|
||||
binding.clipboardSuggestion.setTextColor(theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_FOREGROUND).toSolidColor().color)
|
||||
for (view in candidateViewList) {
|
||||
view.setTextColor(theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND).toSolidColor().color)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun setEventListener(listener: EventListener) {
|
||||
eventListener = WeakReference(listener)
|
||||
binding.candidates.setEventListener(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -388,6 +322,7 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
interface EventListener {
|
||||
fun onSmartbarBackButtonPressed() {}
|
||||
fun onSmartbarCandidatePressed(word: String) {}
|
||||
fun onSmartbarClipboardCandidatePressed(clipboardItem: ClipboardItem) {}
|
||||
//fun onSmartbarCandidateLongPressed() {}
|
||||
fun onSmartbarPrivateModeButtonClicked() {}
|
||||
fun onSmartbarQuickActionPressed(@IdRes quickActionId: Int) {}
|
||||
|
||||
@@ -22,7 +22,7 @@ import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import com.github.michaelbull.result.*
|
||||
import android.net.Uri
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetManager
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
@@ -58,10 +58,10 @@ class ThemeManager private constructor(
|
||||
private set
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The static relative path where a theme is located, regardless of the [AssetSource].
|
||||
*/
|
||||
/** The static relative path where a theme is located, regardless of the [AssetSource]. */
|
||||
const val THEME_PATH_REL: String = "ime/theme"
|
||||
/** Maximum size in bytes a theme file may have when loaded. */
|
||||
const val THEME_MAX_SIZE: Int = 512_000
|
||||
|
||||
private var defaultInstance: ThemeManager? = null
|
||||
|
||||
@@ -85,6 +85,8 @@ class ThemeManager private constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultOrNull(): ThemeManager? = defaultInstance
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -92,7 +94,7 @@ class ThemeManager private constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current theme ref and loads the corresponding theme, as well as notfies all
|
||||
* Updates the current theme ref and loads the corresponding theme, as well as notifies all
|
||||
* callback receivers about the new theme.
|
||||
*/
|
||||
fun update() {
|
||||
@@ -102,7 +104,7 @@ class ThemeManager private constructor(
|
||||
activeTheme = AdaptiveThemeOverlay(this, if (ref == null) {
|
||||
Theme.BASE_THEME
|
||||
} else {
|
||||
loadTheme(ref).getOr(Theme.BASE_THEME)
|
||||
loadTheme(ref).getOrDefault(Theme.BASE_THEME)
|
||||
})
|
||||
Timber.i(activeTheme.label)
|
||||
notifyCallbackReceivers()
|
||||
@@ -247,23 +249,30 @@ class ThemeManager private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteTheme(ref: AssetRef): Result<Nothing?, Throwable> {
|
||||
fun deleteTheme(ref: AssetRef): Result<Unit> {
|
||||
return assetManager.deleteAsset(ref)
|
||||
}
|
||||
|
||||
fun loadTheme(ref: AssetRef): Result<Theme, Throwable> {
|
||||
assetManager.loadAsset(ref, ThemeJson::class.java).onSuccess { themeJson ->
|
||||
val theme = themeJson.toTheme()
|
||||
return Ok(theme)
|
||||
}.onFailure {
|
||||
fun loadTheme(ref: AssetRef): Result<Theme> {
|
||||
val themeJson = assetManager.loadAsset(ref, ThemeJson::class).getOrElse {
|
||||
Timber.e(it.toString())
|
||||
return Err(it)
|
||||
return Result.failure(it)
|
||||
}
|
||||
return Err(Exception("Unreachable code"))
|
||||
val theme = themeJson.toTheme()
|
||||
return Result.success(theme)
|
||||
}
|
||||
|
||||
fun writeTheme(ref: AssetRef, theme: Theme): Result<Boolean, Throwable> {
|
||||
return assetManager.writeAsset(ref, ThemeJson::class.java, ThemeJson.fromTheme(theme))
|
||||
fun loadTheme(uri: Uri): Result<Theme> {
|
||||
val themeJson = assetManager.loadAsset(uri, ThemeJson::class, THEME_MAX_SIZE).getOrElse {
|
||||
Timber.e(it.toString())
|
||||
return Result.failure(it)
|
||||
}
|
||||
val theme = themeJson.toTheme()
|
||||
return Result.success(theme)
|
||||
}
|
||||
|
||||
fun writeTheme(ref: AssetRef, theme: Theme): Result<Unit> {
|
||||
return assetManager.writeAsset(ref, ThemeJson::class, ThemeJson.fromTheme(theme))
|
||||
}
|
||||
|
||||
private fun evaluateActiveThemeRef(): AssetRef? {
|
||||
@@ -300,7 +309,7 @@ class ThemeManager private constructor(
|
||||
prefs.theme.dayThemeRef
|
||||
}
|
||||
}
|
||||
}).onFailure { Timber.e(it) }.getOr(null)
|
||||
}).onFailure { Timber.e(it) }.getOrDefault(null)
|
||||
}
|
||||
|
||||
private fun indexThemeRefs() {
|
||||
@@ -308,7 +317,7 @@ class ThemeManager private constructor(
|
||||
indexedNightThemeRefs.clear()
|
||||
assetManager.listAssets(
|
||||
AssetRef(AssetSource.Assets, THEME_PATH_REL),
|
||||
ThemeMetaOnly::class.java
|
||||
ThemeMetaOnly::class
|
||||
).onSuccess {
|
||||
for ((ref, themeMetaOnly) in it) {
|
||||
if (themeMetaOnly.isNightTheme) {
|
||||
@@ -322,7 +331,7 @@ class ThemeManager private constructor(
|
||||
}
|
||||
assetManager.listAssets(
|
||||
AssetRef(AssetSource.Internal, THEME_PATH_REL),
|
||||
ThemeMetaOnly::class.java
|
||||
ThemeMetaOnly::class
|
||||
).onSuccess {
|
||||
for ((ref, themeMetaOnly) in it) {
|
||||
if (themeMetaOnly.isNightTheme) {
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
package dev.patrickgold.florisboard.ime.theme
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.ColorInt
|
||||
import dev.patrickgold.florisboard.R
|
||||
|
||||
/**
|
||||
@@ -32,16 +34,22 @@ sealed class ThemeValue {
|
||||
*/
|
||||
data class Reference(val group: String, val attr: String) : ThemeValue() {
|
||||
override fun toString(): String {
|
||||
return super.toString()
|
||||
return "@$group/$attr"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This holds a solid color as a color int.
|
||||
*/
|
||||
data class SolidColor(val color: Int) : ThemeValue() {
|
||||
data class SolidColor(@ColorInt val color: Int) : ThemeValue() {
|
||||
companion object {
|
||||
val TRANSPARENT = SolidColor(Color.TRANSPARENT)
|
||||
val BLACK = SolidColor(Color.BLACK)
|
||||
val WHITE = SolidColor(Color.WHITE)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return super.toString()
|
||||
return "#" + String.format("%08X", color)
|
||||
}
|
||||
|
||||
fun complimentaryTextColor(isAlt: Boolean = false): SolidColor {
|
||||
@@ -71,7 +79,7 @@ sealed class ThemeValue {
|
||||
*/
|
||||
data class LinearGradient(val dummy: Int) : ThemeValue() {
|
||||
override fun toString(): String {
|
||||
return super.toString()
|
||||
return "--undefined--"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +88,7 @@ sealed class ThemeValue {
|
||||
*/
|
||||
data class RadialGradient(val dummy: Int) : ThemeValue() {
|
||||
override fun toString(): String {
|
||||
return super.toString()
|
||||
return "--undefined--"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +97,7 @@ sealed class ThemeValue {
|
||||
*/
|
||||
data class OnOff(val state: Boolean) : ThemeValue() {
|
||||
override fun toString(): String {
|
||||
return super.toString()
|
||||
return state.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +107,7 @@ sealed class ThemeValue {
|
||||
*/
|
||||
data class Other(val rawValue: String) : ThemeValue() {
|
||||
override fun toString(): String {
|
||||
return super.toString()
|
||||
return rawValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,32 +180,6 @@ sealed class ThemeValue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this theme value to a string representation.
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return when (this) {
|
||||
is Reference -> {
|
||||
"@$group/$attr"
|
||||
}
|
||||
is SolidColor -> {
|
||||
"#" + String.format("%08X", color)
|
||||
}
|
||||
is LinearGradient -> {
|
||||
"--undefined--"
|
||||
}
|
||||
is RadialGradient -> {
|
||||
"--undefined--"
|
||||
}
|
||||
is OnOff -> {
|
||||
state.toString()
|
||||
}
|
||||
is Other -> {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this theme value to a string representation which can be shown to the user.
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package dev.patrickgold.florisboard.settings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
@@ -32,6 +33,7 @@ class AboutActivity : AppCompatActivity() {
|
||||
private lateinit var binding: AboutActivityBinding
|
||||
private var licensesAlertDialog: AlertDialog? = null
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = AboutActivityBinding.inflate(layoutInflater)
|
||||
@@ -41,7 +43,7 @@ class AboutActivity : AppCompatActivity() {
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
// Set app version string
|
||||
binding.appVersion.text = AppVersionUtils.getRawVersionName(this)
|
||||
binding.appVersion.text = "v" + AppVersionUtils.getRawVersionName(this)
|
||||
|
||||
// Set onClickListeners for buttons
|
||||
binding.privacyPolicyButton.setOnClickListener {
|
||||
|
||||
@@ -28,10 +28,10 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.forEach
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.ThemeEditorActivityBinding
|
||||
import dev.patrickgold.florisboard.databinding.ThemeEditorGroupViewBinding
|
||||
import dev.patrickgold.florisboard.databinding.ThemeEditorMetaDialogBinding
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
@@ -60,6 +60,12 @@ class ThemeEditorActivity : AppCompatActivity() {
|
||||
private var editedThemeRef: AssetRef? = null
|
||||
private var isSaved: Boolean = false
|
||||
|
||||
private var themeLabel: String = ""
|
||||
set(v) {
|
||||
field = v
|
||||
binding.themeNameLabel.text = v
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Constant code for a theme saved activity result. */
|
||||
const val RESULT_CODE_THEME_EDIT_SAVED: Int = 0xFBADC1
|
||||
@@ -85,6 +91,8 @@ class ThemeEditorActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
binding.themeNameEditBtn.setOnClickListener { showMetaEditDialog() }
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
@@ -129,22 +137,13 @@ class ThemeEditorActivity : AppCompatActivity() {
|
||||
R.id.add_group_btn -> addGroup()
|
||||
R.id.theme_cancel_btn -> onBackPressed()
|
||||
R.id.theme_save_btn -> {
|
||||
val themeName = binding.themeNameValue.text.toString().trim()
|
||||
if (Theme.validateField(Theme.ValidationField.THEME_LABEL, themeName)) {
|
||||
val ref = editedThemeRef
|
||||
if (ref != null) {
|
||||
themeManager.writeTheme(
|
||||
ref, editedTheme.copy(
|
||||
label = themeName
|
||||
)
|
||||
)
|
||||
isSaved = true
|
||||
finish()
|
||||
}
|
||||
} else {
|
||||
binding.themeNameLabel.error =
|
||||
resources.getString(R.string.settings__theme_editor__error_theme_label_empty)
|
||||
binding.themeNameLabel.isErrorEnabled = true
|
||||
val ref = editedThemeRef
|
||||
if (ref != null) {
|
||||
themeManager.writeTheme(
|
||||
ref, editedTheme.copy(label = themeLabel)
|
||||
)
|
||||
isSaved = true
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,7 +304,7 @@ class ThemeEditorActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
editedTheme = editedTheme.copy(
|
||||
label = binding.themeNameValue.text.toString(),
|
||||
label = themeLabel,
|
||||
attributes = tempMap.toMap()
|
||||
)
|
||||
binding.keyboardPreview.onThemeUpdated(editedTheme)
|
||||
@@ -315,7 +314,7 @@ class ThemeEditorActivity : AppCompatActivity() {
|
||||
* Builds the Ui for the current [editedTheme]. Also sorts the groups afterwards.
|
||||
*/
|
||||
private fun buildUi() {
|
||||
binding.themeNameValue.setText(editedTheme.label)
|
||||
themeLabel = editedTheme.label
|
||||
for ((groupName, groupAttrs) in editedTheme.attributes) {
|
||||
val groupView = addGroup(groupName).root
|
||||
for ((attrName, attrValue) in groupAttrs) {
|
||||
@@ -330,4 +329,29 @@ class ThemeEditorActivity : AppCompatActivity() {
|
||||
}
|
||||
sortGroups()
|
||||
}
|
||||
|
||||
private fun showMetaEditDialog() {
|
||||
val dialogView = ThemeEditorMetaDialogBinding.inflate(layoutInflater)
|
||||
dialogView.metaName.setText(editedTheme.label)
|
||||
val dialog: AlertDialog
|
||||
AlertDialog.Builder(this).apply {
|
||||
setTitle(R.string.settings__theme_editor__edit_theme_name_dialog_title)
|
||||
setCancelable(true)
|
||||
setView(dialogView.root)
|
||||
setPositiveButton(android.R.string.ok, null)
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
create()
|
||||
dialog = show()
|
||||
dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
|
||||
val tempThemeLabel = dialogView.metaName.text.toString().trim()
|
||||
if (Theme.validateField(Theme.ValidationField.THEME_LABEL, tempThemeLabel)) {
|
||||
themeLabel = tempThemeLabel
|
||||
dialog.dismiss()
|
||||
} else {
|
||||
dialogView.metaNameLabel.error = resources.getString(R.string.settings__theme_editor__error_theme_label_empty)
|
||||
dialogView.metaNameLabel.isErrorEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,26 +17,28 @@
|
||||
package dev.patrickgold.florisboard.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RadioButton
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.forEach
|
||||
import com.github.michaelbull.result.getOr
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.ThemeManagerActivityBinding
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisActivity
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetManager
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetSource
|
||||
import dev.patrickgold.florisboard.ime.extension.ExternalContentUtils
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
@@ -47,11 +49,9 @@ import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
class ThemeManagerActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ThemeManagerActivityBinding
|
||||
class ThemeManagerActivity : FlorisActivity<ThemeManagerActivityBinding>() {
|
||||
private lateinit var layoutManager: LayoutManager
|
||||
private val mainScope = MainScope()
|
||||
private lateinit var prefs: PrefHelper
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
|
||||
private var key: String = ""
|
||||
@@ -59,19 +59,70 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
private var selectedTheme: Theme = Theme.empty()
|
||||
private var selectedRef: AssetRef? = null
|
||||
|
||||
companion object {
|
||||
private const val EDITOR_REQ_CODE: Int = 0xFB01
|
||||
private val themeEditor = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult? ->
|
||||
if (result?.resultCode == ThemeEditorActivity.RESULT_CODE_THEME_EDIT_SAVED) {
|
||||
themeManager.update()
|
||||
buildUi()
|
||||
}
|
||||
}
|
||||
|
||||
private val importTheme = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||
// If uri is null it indicates that the selection activity was cancelled (mostly by pressing the back button),
|
||||
// so we don't display an error message here.
|
||||
if (uri == null) return@registerForActivityResult
|
||||
val toBeImportedTheme = themeManager.loadTheme(uri)
|
||||
if (toBeImportedTheme.isSuccess) {
|
||||
val newTheme = toBeImportedTheme.getOrNull()!!.copy(
|
||||
name = toBeImportedTheme.getOrNull()!!.name + "_imported",
|
||||
label = toBeImportedTheme.getOrNull()!!.label + " (Imported)"
|
||||
)
|
||||
val newAssetRef = AssetRef(
|
||||
AssetSource.Internal,
|
||||
ThemeManager.THEME_PATH_REL + "/" + newTheme.name + ".json"
|
||||
)
|
||||
themeManager.writeTheme(newAssetRef, newTheme).onSuccess {
|
||||
themeManager.update()
|
||||
selectedTheme = newTheme
|
||||
selectedRef = newAssetRef
|
||||
setThemeRefInPrefs(newAssetRef)
|
||||
buildUi()
|
||||
showMessage(R.string.settings__theme_manager__theme_import_success)
|
||||
}.onFailure {
|
||||
showError(it)
|
||||
}
|
||||
} else {
|
||||
showError(toBeImportedTheme.exceptionOrNull()!!)
|
||||
}
|
||||
}
|
||||
|
||||
private val exportTheme = registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri: Uri? ->
|
||||
// If uri is null it indicates that the selection activity was cancelled (mostly by pressing the back button,
|
||||
// so we don't display an error message here.
|
||||
if (uri == null) return@registerForActivityResult
|
||||
val selectedRef = selectedRef
|
||||
if (selectedRef != null) {
|
||||
AssetManager.default().loadAssetRaw(selectedRef).onSuccess {
|
||||
Timber.i(it)
|
||||
ExternalContentUtils.writeTextToUri(this, uri, it).onSuccess {
|
||||
showMessage(R.string.settings__theme_manager__theme_export_success)
|
||||
}.onFailure {
|
||||
showError(it)
|
||||
}
|
||||
}.onFailure {
|
||||
showError(it)
|
||||
}
|
||||
} else {
|
||||
showError(NullPointerException("selectedRef is null!"))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_KEY: String = "key"
|
||||
const val EXTRA_DEFAULT_VALUE: String = "default_value"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
prefs = PrefHelper.getDefaultInstance(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ThemeManagerActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
key = intent.getStringExtra(EXTRA_KEY) ?: ""
|
||||
defaultValue = intent.getStringExtra(EXTRA_DEFAULT_VALUE) ?: ""
|
||||
@@ -91,8 +142,10 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
|
||||
binding.fabOptionCreateEmpty.setOnClickListener { onActionClicked(it) }
|
||||
binding.fabOptionCreateFromSelected.setOnClickListener { onActionClicked(it) }
|
||||
binding.fabOptionImport.setOnClickListener { onActionClicked(it) }
|
||||
binding.themeDeleteBtn.setOnClickListener { onActionClicked(it) }
|
||||
binding.themeEditBtn.setOnClickListener { onActionClicked(it) }
|
||||
binding.themeExportBtn.setOnClickListener { onActionClicked(it) }
|
||||
|
||||
layoutManager = LayoutManager(this).apply {
|
||||
preloadComputedLayout(KeyboardMode.CHARACTERS, Subtype.DEFAULT, prefs)
|
||||
@@ -101,6 +154,10 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
buildUi()
|
||||
}
|
||||
|
||||
override fun onCreateBinding(): ThemeManagerActivityBinding {
|
||||
return ThemeManagerActivityBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
@@ -132,7 +189,7 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
PrefHelper.Theme.NIGHT_THEME_REF -> prefs.theme.nightThemeRef
|
||||
else -> ""
|
||||
}
|
||||
).getOr(null)
|
||||
).getOrDefault(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,15 +234,6 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == EDITOR_REQ_CODE) {
|
||||
themeManager.update()
|
||||
buildUi()
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
fun onActionClicked(view: View) {
|
||||
when (view.id) {
|
||||
R.id.fab_option_create_empty -> {
|
||||
@@ -202,9 +250,11 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
ThemeManager.THEME_PATH_REL + "/" + newTheme.name + ".json"
|
||||
)
|
||||
themeManager.writeTheme(newAssetRef, newTheme).onSuccess {
|
||||
startActivityForResult(Intent(this, ThemeEditorActivity::class.java).apply {
|
||||
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, newAssetRef.toString())
|
||||
}, EDITOR_REQ_CODE)
|
||||
themeEditor.launch(
|
||||
Intent(this, ThemeEditorActivity::class.java).apply {
|
||||
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, newAssetRef.toString())
|
||||
}
|
||||
)
|
||||
}.onFailure {
|
||||
Timber.e(it.toString())
|
||||
}
|
||||
@@ -227,16 +277,18 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
ThemeManager.THEME_PATH_REL + "/" + themeCopy.name + ".json"
|
||||
)
|
||||
themeManager.writeTheme(newAssetRef, themeCopy).onSuccess {
|
||||
startActivityForResult(Intent(this, ThemeEditorActivity::class.java).apply {
|
||||
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, newAssetRef.toString())
|
||||
}, EDITOR_REQ_CODE)
|
||||
themeEditor.launch(
|
||||
Intent(this, ThemeEditorActivity::class.java).apply {
|
||||
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, newAssetRef.toString())
|
||||
}
|
||||
)
|
||||
}.onFailure {
|
||||
Timber.e(it.toString())
|
||||
}
|
||||
}
|
||||
/*R.id.fab_option_import -> {
|
||||
Toast.makeText(this, "Import not yet implemented", Toast.LENGTH_SHORT).show()
|
||||
}*/
|
||||
R.id.fab_option_import -> {
|
||||
importTheme.launch("*/*")
|
||||
}
|
||||
R.id.theme_delete_btn -> {
|
||||
val deleteRef = selectedRef?.copy()
|
||||
if (deleteRef?.source == AssetSource.Internal) {
|
||||
@@ -273,9 +325,11 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
R.id.theme_edit_btn -> {
|
||||
val editRef = selectedRef
|
||||
if (editRef?.source == AssetSource.Internal) {
|
||||
startActivityForResult(Intent(this, ThemeEditorActivity::class.java).apply {
|
||||
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, editRef.toString())
|
||||
}, EDITOR_REQ_CODE)
|
||||
themeEditor.launch(
|
||||
Intent(this, ThemeEditorActivity::class.java).apply {
|
||||
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, editRef.toString())
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// This toast normally should never show, though if the edit button is enabled
|
||||
// even if it shouldn't, just show a toast so the user knows the app is
|
||||
@@ -287,6 +341,9 @@ class ThemeManagerActivity : AppCompatActivity() {
|
||||
).show()
|
||||
}
|
||||
}
|
||||
R.id.theme_export_btn -> {
|
||||
exportTheme.launch("${selectedTheme.name}.json")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.util.AttributeSet
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.Preference.OnPreferenceClickListener
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.github.michaelbull.result.onSuccess
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
|
||||
@@ -23,11 +23,13 @@ import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.settings.components.DialogSeekBarPreference
|
||||
|
||||
class KeyboardFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private var heightFactorCustom: DialogSeekBarPreference? = null
|
||||
private var oneHandedModeScaleFactor: DialogSeekBarPreference? = null
|
||||
private var utilityKeyAction: ListPreference? = null
|
||||
private var sharedPrefs: SharedPreferences? = null
|
||||
|
||||
@@ -36,8 +38,10 @@ class KeyboardFragment : PreferenceFragmentCompat(),
|
||||
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
heightFactorCustom = findPreference(PrefHelper.Keyboard.HEIGHT_FACTOR_CUSTOM)
|
||||
oneHandedModeScaleFactor = findPreference(PrefHelper.Keyboard.ONE_HANDED_MODE_SCALE_FACTOR)
|
||||
utilityKeyAction = findPreference(PrefHelper.Keyboard.UTILITY_KEY_ACTION)
|
||||
onSharedPreferenceChanged(null, PrefHelper.Keyboard.HEIGHT_FACTOR)
|
||||
onSharedPreferenceChanged(null, PrefHelper.Keyboard.ONE_HANDED_MODE)
|
||||
onSharedPreferenceChanged(null, PrefHelper.Keyboard.UTILITY_KEY_ENABLED)
|
||||
}
|
||||
|
||||
@@ -52,10 +56,16 @@ class KeyboardFragment : PreferenceFragmentCompat(),
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
if (key == PrefHelper.Keyboard.HEIGHT_FACTOR) {
|
||||
heightFactorCustom?.isVisible = sharedPrefs?.getString(key, "") == "custom"
|
||||
} else if (key == PrefHelper.Keyboard.UTILITY_KEY_ENABLED) {
|
||||
utilityKeyAction?.isVisible = sharedPrefs?.getBoolean(key, false) == true
|
||||
when (key) {
|
||||
PrefHelper.Keyboard.HEIGHT_FACTOR -> {
|
||||
heightFactorCustom?.isVisible = sharedPrefs?.getString(key, "") == "custom"
|
||||
}
|
||||
PrefHelper.Keyboard.ONE_HANDED_MODE -> {
|
||||
oneHandedModeScaleFactor?.isEnabled = sharedPrefs?.getString(key, "") != OneHandedMode.OFF
|
||||
}
|
||||
PrefHelper.Keyboard.UTILITY_KEY_ENABLED -> {
|
||||
utilityKeyAction?.isVisible = sharedPrefs?.getBoolean(key, false) == true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class EnableImeFragment : Fragment() {
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
): View {
|
||||
binding = SetupFragmentEnableImeBinding.inflate(inflater, container, false)
|
||||
binding.languageAndInputButton.setOnClickListener {
|
||||
val intent = Intent()
|
||||
|
||||
@@ -30,7 +30,7 @@ class FinishFragment : Fragment() {
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
): View {
|
||||
binding = SetupFragmentFinishBinding.inflate(inflater, container, false)
|
||||
|
||||
// Set theme to floris_day
|
||||
|
||||
@@ -68,12 +68,12 @@ data class VersionName(
|
||||
if (list.size == 3) {
|
||||
return VersionName(list[0], list[1], list[2])
|
||||
}
|
||||
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[.][0-9]+""".toRegex())) {
|
||||
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[-][0-9]+""".toRegex())) {
|
||||
val list = raw.split(".").map { it.toInt() }
|
||||
if (list.size == 4) {
|
||||
return VersionName(list[0], list[1], list[2], null, list[3])
|
||||
}
|
||||
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[.][a-zA-Z]+""".toRegex())) {
|
||||
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[-][a-zA-Z]+""".toRegex())) {
|
||||
val list = raw.split(".")
|
||||
if (list.size == 4) {
|
||||
return VersionName(
|
||||
@@ -81,7 +81,7 @@ data class VersionName(
|
||||
list[3], null
|
||||
)
|
||||
}
|
||||
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[.][a-zA-Z]+[0-9]+""".toRegex())) {
|
||||
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[-][a-zA-Z]+[0-9]+""".toRegex())) {
|
||||
val list = raw.split(".")
|
||||
if (list.size == 4) {
|
||||
val extraName = list[3].split("""[0-9]+""".toRegex())[0]
|
||||
|
||||
@@ -35,7 +35,7 @@ object LocaleUtils {
|
||||
}
|
||||
}
|
||||
|
||||
class JsonAdapter() {
|
||||
class JsonAdapter {
|
||||
@FromJson
|
||||
fun fromJson(raw: String): Locale {
|
||||
return stringToLocale(raw)
|
||||
|
||||
5
app/src/main/res/drawable/ic_assignment.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?android:attr/textColorPrimary" android:pathData="M7 15h7v2H7zm0-4h10v2H7zm0-4h10v2H7zm12-4h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-.14 0-.27.01-.4.04-.39.08-.74.28-1.01.55-.18.18-.33.4-.43.64-.1.23-.16.49-.16.77v14c0 .27.06.54.16.78s.25.45.43.64c.27.27.62.47 1.01.55.13.02.26.03.4.03h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7-.25c.41 0 .75.34.75.75s-.34.75-.75.75-.75-.34-.75-.75.34-.75.75-.75zM19 19H5V5h14v14z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_clear_all.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?android:attr/textColorPrimary" android:pathData="M5 13h14v-2H5v2zm-2 4h14v-2H3v2zM7 7v2h14V7H7z"/>
|
||||
</vector>
|
||||
6
app/src/main/res/drawable/ic_pin.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp" android:height="24dp"
|
||||
android:viewportWidth="24" android:viewportHeight="24"
|
||||
android:autoMirrored="true">
|
||||
<path android:fillColor="?android:attr/textColorPrimary" android:pathData="M14,4v5c0,1.12,0.37,2.16,1,3H9c0.65-0.86,1-1.9,1-3V4H14 M17,2H7C6.45,2,6,2.45,6,3c0,0.55,0.45,1,1,1c0,0,0,0,0,0l1,0v5 c0,1.66-1.34,3-3,3v2h5.97v7l1,1l1-1v-7H19v-2c0,0,0,0,0,0c-1.66,0-3-1.34-3-3V4l1,0c0,0,0,0,0,0c0.55,0,1-0.45,1-1 C18,2.45,17.55,2,17,2L17,2z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_share.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?android:attr/textColorPrimary" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
||||
23
app/src/main/res/drawable/shape_rect_rounded_3.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Bottom 2dp Shadow -->
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/cardview_shadow_start_color" />
|
||||
<corners android:radius="7dp" />
|
||||
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<!-- White Top color -->
|
||||
<item android:right="3px" android:bottom="5px">
|
||||
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="7dp" />
|
||||
|
||||
</shape>
|
||||
|
||||
</item>
|
||||
|
||||
</layer-list>
|
||||
@@ -20,11 +20,12 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/app_icon"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
android:background="@mipmap/floris_app_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:elevation="8dp"
|
||||
android:contentDescription="@string/about__app_icon_content_description"/>
|
||||
|
||||
<TextView
|
||||
|
||||
105
app/src/main/res/layout/clip_popup_layout.xml
Normal file
@@ -0,0 +1,105 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<dev.patrickgold.florisboard.ime.clip.ClipboardPopupView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="10dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/pin_clip_item"
|
||||
android:orientation="horizontal"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pin_clip_item_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:scaleX="0.8"
|
||||
android:scaleY="0.8"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_pin"
|
||||
android:contentDescription="@string/clip__pin_item" />
|
||||
|
||||
<Space
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="0dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/clip__pin_item"
|
||||
android:id="@+id/pin_clip_item_text"
|
||||
tools:layout_editor_absoluteY="3dp" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="10dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/remove_from_history"
|
||||
android:orientation="horizontal"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/remove_from_history_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:scaleX="0.8"
|
||||
android:scaleY="0.8"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_delete"
|
||||
android:contentDescription="@string/clip__delete_item" />
|
||||
|
||||
<Space
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="0dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/clip__delete_item"
|
||||
android:id="@+id/remove_from_history_text"
|
||||
tools:layout_editor_absoluteY="3dp" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Space
|
||||
android:id="@+id/paste_clip_item_space"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="10dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/paste_clip_item"
|
||||
android:orientation="horizontal"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/paste_clip_item_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:scaleX="0.8"
|
||||
android:scaleY="0.8"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_content_paste"
|
||||
android:contentDescription="@string/clip__paste_item" />
|
||||
|
||||
<Space
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="0dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/clip__paste_item"
|
||||
android:id="@+id/paste_clip_item_text"
|
||||
tools:layout_editor_absoluteY="3dp" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</dev.patrickgold.florisboard.ime.clip.ClipboardPopupView>
|
||||
38
app/src/main/res/layout/clipboard_history_item_image.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<dev.patrickgold.florisboard.ime.clip.ClipboardHistoryItemView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:padding="0dp"
|
||||
android:background="@drawable/shape_rect_rounded_3"
|
||||
android:foreground="?selectableItemBackground"
|
||||
>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/clipboard_history_item_img"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:padding="0dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:background="@drawable/shape_rect_rounded_3"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/clipboard_pin"
|
||||
android:layout_width="15dp"
|
||||
android:layout_height="15dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginTop="5dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:src="@drawable/ic_pin"
|
||||
android:contentDescription="@string/clip__pin_item"/>
|
||||
|
||||
</dev.patrickgold.florisboard.ime.clip.ClipboardHistoryItemView>
|
||||
36
app/src/main/res/layout/clipboard_history_item_text.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<dev.patrickgold.florisboard.ime.clip.ClipboardHistoryItemView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:padding="5dp"
|
||||
android:foreground="?selectableItemBackground"
|
||||
android:background="@drawable/shape_rect_rounded_3"
|
||||
>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/clipboard_history_item_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:padding="6dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="A clipboard item would go here." />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/clipboard_pin"
|
||||
android:layout_width="15dp"
|
||||
android:layout_height="15dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginTop="5dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:src="@drawable/ic_pin"
|
||||
android:contentDescription="@string/clip__pin_item"/>
|
||||
|
||||
</dev.patrickgold.florisboard.ime.clip.ClipboardHistoryItemView>
|
||||
63
app/src/main/res/layout/clipboard_layout.xml
Normal file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<dev.patrickgold.florisboard.ime.clip.ClipboardHistoryView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/clip_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/clipboard_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/back_to_keyboard_button"
|
||||
android:layout_width="@dimen/clipboard_button_width"
|
||||
android:layout_height="@dimen/clipboard_button_height"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="@drawable/button_transparent_bg_on_press"
|
||||
android:hapticFeedbackEnabled="false"
|
||||
android:soundEffectsEnabled="false"
|
||||
android:layout_weight="0"
|
||||
android:src="@drawable/ic_arrow_back"
|
||||
android:contentDescription="@string/clip__back_to_text_input" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/clipboard_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/clip__context_header"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/clear_clipboard_history"
|
||||
android:layout_width="@dimen/clipboard_button_width"
|
||||
android:layout_height="@dimen/clipboard_button_height"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="@drawable/button_transparent_bg_on_press"
|
||||
android:hapticFeedbackEnabled="false"
|
||||
android:soundEffectsEnabled="false"
|
||||
android:layout_weight="0"
|
||||
android:src="@drawable/ic_clear_all"
|
||||
android:contentDescription="@string/clip__clear_history" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/clipboard_history_items"
|
||||
android:paddingLeft="5dp"
|
||||
android:paddingRight="5dp"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
||||
</dev.patrickgold.florisboard.ime.clip.ClipboardHistoryView>
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<dev.patrickgold.florisboard.ime.core.InputWindowView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/florisboard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -18,29 +19,32 @@
|
||||
android:background="?inputView_bgColorFallback"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<LinearLayout
|
||||
<dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel
|
||||
android:id="@+id/one_handed_ctrl_panel_start"
|
||||
style="@style/OneHandedPanel"
|
||||
android:visibility="gone">
|
||||
android:layout_width="@dimen/one_handed_width"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="0"
|
||||
android:visibility="gone"
|
||||
app:panelSide="start">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_close_start"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:src="@drawable/ic_zoom_out_map"
|
||||
android:contentDescription="@string/one_handed__close_btn_content_description"/>
|
||||
|
||||
<View
|
||||
android:tag="one_handed_ctrl_close"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/one_handed_button_height"
|
||||
android:visibility="invisible"/>
|
||||
android:layout_marginBottom="@dimen/one_handed_button_height"
|
||||
android:src="@drawable/ic_zoom_out_map"
|
||||
android:background="@drawable/button_transparent_bg_on_press"
|
||||
android:contentDescription="@string/one_handed__close_btn_content_description"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_move_start"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:tag="one_handed_ctrl_move"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/one_handed_button_height"
|
||||
android:src="@drawable/ic_keyboard_arrow_left"
|
||||
android:background="@drawable/button_transparent_bg_on_press"
|
||||
android:contentDescription="@string/one_handed__move_start_btn_content_description"/>
|
||||
|
||||
</LinearLayout>
|
||||
</dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.core.FlorisViewFlipper
|
||||
android:id="@+id/main_view_flipper"
|
||||
@@ -53,31 +57,36 @@
|
||||
|
||||
<include layout="@layout/media_input_layout"/>
|
||||
|
||||
<include layout="@layout/clipboard_layout"/>
|
||||
|
||||
</dev.patrickgold.florisboard.ime.core.FlorisViewFlipper>
|
||||
|
||||
<LinearLayout
|
||||
<dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel
|
||||
android:id="@+id/one_handed_ctrl_panel_end"
|
||||
style="@style/OneHandedPanel"
|
||||
android:visibility="gone">
|
||||
android:layout_width="@dimen/one_handed_width"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="0"
|
||||
android:visibility="gone"
|
||||
app:panelSide="end">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_close_end"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:src="@drawable/ic_zoom_out_map"
|
||||
android:contentDescription="@string/one_handed__close_btn_content_description"/>
|
||||
|
||||
<View
|
||||
android:tag="one_handed_ctrl_close"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/one_handed_button_height"
|
||||
android:visibility="invisible"/>
|
||||
android:layout_marginBottom="@dimen/one_handed_button_height"
|
||||
android:src="@drawable/ic_zoom_out_map"
|
||||
android:background="@drawable/button_transparent_bg_on_press"
|
||||
android:contentDescription="@string/one_handed__close_btn_content_description"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_move_end"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:tag="one_handed_ctrl_move"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/one_handed_button_height"
|
||||
android:src="@drawable/ic_keyboard_arrow_right"
|
||||
android:contentDescription="@string/one_handed__move_end_btn_content_description"/>
|
||||
android:background="@drawable/button_transparent_bg_on_press"
|
||||
android:contentDescription="@string/one_handed__move_start_btn_content_description"/>
|
||||
|
||||
</LinearLayout>
|
||||
</dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel>
|
||||
|
||||
</dev.patrickgold.florisboard.ime.core.InputView>
|
||||
|
||||
|
||||
@@ -39,38 +39,9 @@
|
||||
app:layout_constraintEnd_toStartOf="@id/action_end_area"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
<dev.patrickgold.florisboard.ime.text.smartbar.CandidateView
|
||||
android:id="@+id/candidates"
|
||||
style="@style/SmartbarContainer">
|
||||
|
||||
<Button
|
||||
android:id="@+id/candidate0"
|
||||
style="@style/SmartbarCandidate"/>
|
||||
|
||||
<View style="@style/SmartbarDivider"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/candidate1"
|
||||
style="@style/SmartbarCandidate"/>
|
||||
|
||||
<View style="@style/SmartbarDivider"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/candidate2"
|
||||
style="@style/SmartbarCandidate"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/clipboard_suggestion_row"
|
||||
style="@style/SmartbarContainer">
|
||||
|
||||
<Button
|
||||
android:id="@+id/clipboard_suggestion"
|
||||
android:drawableStart="@drawable/ic_content_paste_with_padding"
|
||||
style="@style/SmartbarQuickAction.ClipboardSuggestion"/>
|
||||
|
||||
</LinearLayout>
|
||||
style="@style/SmartbarContainer"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/quick_actions"
|
||||
|
||||
@@ -42,28 +42,31 @@
|
||||
android:layout_margin="8dp"
|
||||
app:isPreviewKeyboard="true"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/theme_name_label"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:hint="@string/settings__theme_editor__name_label"
|
||||
app:boxBackgroundMode="outline"
|
||||
app:boxBackgroundColor="?android:windowBackground"
|
||||
app:boxStrokeColor="?colorAccent"
|
||||
app:boxStrokeErrorColor="?colorError"
|
||||
app:boxStrokeWidth="1dp">
|
||||
android:orientation="horizontal"
|
||||
android:layout_margin="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/theme_name_value"
|
||||
android:layout_width="match_parent"
|
||||
<TextView
|
||||
android:id="@+id/theme_name_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textFilter"
|
||||
android:imeOptions="flagForceAscii|flagNoExtractUi"/>
|
||||
android:layout_weight="1.0"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
tools:text="GroupName"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<ImageButton
|
||||
android:id="@+id/theme_name_edit_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_edit"
|
||||
android:tint="?android:textColorPrimary"
|
||||
android:contentDescription="@string/settings__theme_editor__edit_group_dialog_title"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
31
app/src/main/res/layout/theme_editor_meta_dialog.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingTop="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/meta_name_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/settings__theme_editor__name_label"
|
||||
app:boxBackgroundMode="outline"
|
||||
app:boxBackgroundColor="?android:windowBackground"
|
||||
app:boxStrokeColor="?colorAccent"
|
||||
app:boxStrokeErrorColor="?colorError"
|
||||
app:boxStrokeWidth="1dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/meta_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textFilter"
|
||||
android:imeOptions="flagForceAscii|flagNoExtractUi"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -112,15 +112,30 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end">
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/theme_export_btn"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAllCaps="false"
|
||||
android:text="@string/assets__action__export"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:drawableStart="@drawable/ic_share"
|
||||
android:drawablePadding="8dp"
|
||||
android:drawableTint="?colorAccent"/>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1.0"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/theme_delete_btn"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textAllCaps="false"
|
||||
android:text="@string/assets__action__delete"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
@@ -133,7 +148,6 @@
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textAllCaps="false"
|
||||
android:text="@string/assets__action__edit"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
@@ -160,7 +174,8 @@
|
||||
|
||||
<com.nambimobile.widgets.efab.ExpandableFabLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
app:layout_dodgeInsetEdges="bottom">
|
||||
|
||||
<com.nambimobile.widgets.efab.Overlay
|
||||
android:layout_width="match_parent"
|
||||
@@ -189,13 +204,13 @@
|
||||
app:fab_color="?colorPrimaryDark"
|
||||
app:fab_icon="@drawable/ic_file"
|
||||
app:label_text="@string/settings__theme_manager__create_from_selected"/>
|
||||
<!--<com.nambimobile.widgets.efab.FabOption
|
||||
<com.nambimobile.widgets.efab.FabOption
|
||||
android:id="@+id/fab_option_import"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:fab_color="?colorPrimaryDark"
|
||||
app:fab_icon="@drawable/ic_input"
|
||||
app:label_text="@string/assets__action__import"/>-->
|
||||
app:label_text="@string/assets__action__import"/>
|
||||
|
||||
</com.nambimobile.widgets.efab.ExpandableFabLayout>
|
||||
|
||||
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_app_icon_beta.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_app_icon_beta_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_app_icon_beta_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_app_icon_beta_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_app_icon_beta_foreground"/>
|
||||
</adaptive-icon>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_app_icon_debug.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_app_icon_debug_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_app_icon_debug_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_app_icon_debug_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_app_icon_debug_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_app_icon_release_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_app_icon_release_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_app_icon_release_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_app_icon_release_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_app_icon_beta.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_app_icon_beta_foreground.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_app_icon_beta_round.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_app_icon_debug.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_app_icon_debug_foreground.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_app_icon_debug_round.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_app_icon_release.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_app_icon_release_foreground.png
Normal file
|
After Width: | Height: | Size: 783 B |