Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e581d6cbc4 | ||
|
|
ec13d008fb | ||
|
|
edfea1afcb | ||
|
|
25fc23d721 | ||
|
|
c701141be2 | ||
|
|
e5b956857e | ||
|
|
67236ef58d | ||
|
|
2da17a0654 | ||
|
|
1f3221a886 | ||
|
|
47f80d00c4 | ||
|
|
d648c480b5 | ||
|
|
9e26720674 | ||
|
|
a20c6bf148 | ||
|
|
d2df5cfcdf | ||
|
|
93b5503dfc | ||
|
|
4d4b54074a | ||
|
|
904fd9b85a | ||
|
|
e4f5fcf74b | ||
|
|
15f0316839 | ||
|
|
93654c4f88 | ||
|
|
62fc549ea9 | ||
|
|
d0dbd1cd4e | ||
|
|
af28f84b69 | ||
|
|
db7ee52029 | ||
|
|
7343617792 | ||
|
|
5898d7006b | ||
|
|
058be7a169 | ||
|
|
e6f2a25021 | ||
|
|
3a485a1574 | ||
|
|
0ee0f24119 | ||
|
|
004e999259 | ||
|
|
11775c4619 | ||
|
|
177bad95b3 | ||
|
|
610526d845 | ||
|
|
55e489bc07 | ||
|
|
589063be61 | ||
|
|
aa73ac706a |
@@ -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
|
||||
|
||||
12
README.md
@@ -62,11 +62,9 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
|
||||
* [x] Landscape orientation support (needs tweaks)
|
||||
|
||||
### Layouts
|
||||
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish,
|
||||
Norwegian, Swedish/Finnish, Icelandic, Danish, Hungarian,
|
||||
Croatian, Polish, Romanian, 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, ...)
|
||||
* [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
|
||||
@@ -138,6 +136,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 +149,6 @@ passes...
|
||||
|
||||
Backlog (currently not assigned to any milestone):
|
||||
|
||||
- Theme import/export
|
||||
- Floating keyboard
|
||||
|
||||
[#91]: https://github.com/florisboard/florisboard/pull/91
|
||||
@@ -179,8 +177,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)
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion(29)
|
||||
buildToolsVersion("29.0.2")
|
||||
compileSdkVersion(30)
|
||||
buildToolsVersion("30.0.3")
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
@@ -20,9 +20,9 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "dev.patrickgold.florisboard"
|
||||
minSdkVersion(23)
|
||||
targetSdkVersion(29)
|
||||
versionCode(27)
|
||||
versionName("0.3.8")
|
||||
targetSdkVersion(30)
|
||||
versionCode(28)
|
||||
versionName("0.3.9")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -34,11 +34,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 = "-beta"
|
||||
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
|
||||
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_beta")
|
||||
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_beta_round")
|
||||
resValue("string", "floris_app_name", "FlorisBoard Beta")
|
||||
}
|
||||
|
||||
named("release").configure {
|
||||
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
|
||||
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_release")
|
||||
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_release_round")
|
||||
resValue("string", "floris_app_name", "@string/app_name")
|
||||
}
|
||||
}
|
||||
@@ -55,10 +73,13 @@ 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,7 +87,6 @@ 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")
|
||||
|
||||
testImplementation("junit", "junit", "4.13.1")
|
||||
|
||||
@@ -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,45 +69,45 @@
|
||||
<!-- Theme Selector Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.ThemeManagerActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__title"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Theme Editor Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.ThemeEditorActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__theme_editor__title"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- About Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.AboutActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/about__title"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Advanced Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.AdvancedActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__advanced__title"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Setup Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.setup.SetupActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/setup__title"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Crash Dialog Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.crashutility.CrashDialogActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/crash_dialog__title"
|
||||
android:theme="@style/CrashDialogTheme"/>
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
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 |
@@ -20,6 +20,7 @@ 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
|
||||
@@ -59,8 +60,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
|
||||
@@ -109,24 +108,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,17 +169,30 @@ 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
|
||||
}
|
||||
@@ -193,22 +206,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)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,7 +357,13 @@ class EditorInstance private constructor(
|
||||
fun performClipboardCut(): Boolean {
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_X)
|
||||
val ic = inputConnection ?: return false
|
||||
if (isRawInputEditor) {
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_X, meta(ctrl = true))
|
||||
} else {
|
||||
ic.performContextMenuAction(android.R.id.cut)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -369,8 +375,15 @@ class EditorInstance private constructor(
|
||||
fun performClipboardCopy(): Boolean {
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_C) &&
|
||||
selection.updateAndNotify(selection.end, selection.end)
|
||||
val ic = inputConnection ?: return false
|
||||
if (isRawInputEditor) {
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_C, meta(ctrl = true)) &&
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT)
|
||||
} else {
|
||||
ic.performContextMenuAction(android.R.id.copy)
|
||||
selection.updateAndNotify(selection.end, selection.end)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,7 +395,13 @@ class EditorInstance private constructor(
|
||||
fun performClipboardPaste(): Boolean {
|
||||
isPhantomSpaceActive = false
|
||||
wasPhantomSpaceActiveLastUpdate = false
|
||||
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_V)
|
||||
val ic = inputConnection ?: return false
|
||||
if (isRawInputEditor) {
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_V, meta(ctrl = true))
|
||||
} else {
|
||||
ic.performContextMenuAction(android.R.id.paste)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -394,7 +413,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 +432,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 +460,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 +471,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 +515,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 +533,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 +920,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 +929,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 +976,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
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ 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 +38,13 @@ import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.lifecycle.*
|
||||
import com.squareup.moshi.Json
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
|
||||
import dev.patrickgold.florisboard.ime.media.MediaInputManager
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupLayerView
|
||||
import dev.patrickgold.florisboard.ime.text.TextInputManager
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
@@ -68,13 +69,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, ClipboardManager.OnPrimaryClipChangedListener,
|
||||
ThemeManager.OnThemeUpdatedListener {
|
||||
|
||||
lateinit var prefs: PrefHelper
|
||||
private set
|
||||
|
||||
val context: Context
|
||||
get() = inputWindowView?.context ?: this
|
||||
private val serviceLifecycleDispatcher: ServiceLifecycleDispatcher = ServiceLifecycleDispatcher(this)
|
||||
|
||||
private var extractEditLayout: WeakReference<ViewGroup?> = WeakReference(null)
|
||||
var inputView: InputView? = null
|
||||
private set
|
||||
@@ -90,6 +94,17 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
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,6 +112,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
private var currentThemeIsNight: Boolean = false
|
||||
private var currentThemeResId: Int = 0
|
||||
private var isNumberRowVisible: Boolean = false
|
||||
private var isWindowShown: Boolean = false
|
||||
|
||||
val textInputManager: TextInputManager
|
||||
val mediaInputManager: MediaInputManager
|
||||
@@ -110,18 +126,22 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
|
||||
companion object {
|
||||
private const val IME_ID: String = "dev.patrickgold.florisboard/.ime.core.FlorisBoard"
|
||||
private const val IME_ID_BETA: String = "dev.patrickgold.florisboard.beta/dev.patrickgold.florisboard.ime.core.FlorisBoard"
|
||||
private const val IME_ID_DEBUG: String = "dev.patrickgold.florisboard.debug/dev.patrickgold.florisboard.ime.core.FlorisBoard"
|
||||
|
||||
fun checkIfImeIsEnabled(context: Context): Boolean {
|
||||
val activeImeIds = Settings.Secure.getString(
|
||||
context.contentResolver,
|
||||
Settings.Secure.ENABLED_INPUT_METHODS
|
||||
)
|
||||
) ?: "(none)"
|
||||
Timber.i("List of active IMEs: $activeImeIds")
|
||||
return when {
|
||||
BuildConfig.DEBUG -> {
|
||||
activeImeIds.split(":").contains(IME_ID_DEBUG)
|
||||
}
|
||||
context.packageName.endsWith(".beta") -> {
|
||||
activeImeIds.split(":").contains(IME_ID_BETA)
|
||||
}
|
||||
else -> {
|
||||
activeImeIds.split(":").contains(IME_ID)
|
||||
}
|
||||
@@ -132,12 +152,15 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
val selectedImeId = Settings.Secure.getString(
|
||||
context.contentResolver,
|
||||
Settings.Secure.DEFAULT_INPUT_METHOD
|
||||
)
|
||||
) ?: "(none)"
|
||||
Timber.i("Selected IME: $selectedImeId")
|
||||
return when {
|
||||
BuildConfig.DEBUG -> {
|
||||
selectedImeId == IME_ID_DEBUG
|
||||
}
|
||||
context.packageName.endsWith(".beta") -> {
|
||||
selectedImeId.split(":").contains(IME_ID_BETA)
|
||||
}
|
||||
else -> {
|
||||
selectedImeId == IME_ID
|
||||
}
|
||||
@@ -162,6 +185,10 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLifecycle(): Lifecycle {
|
||||
return serviceLifecycleDispatcher.lifecycle
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
/*if (BuildConfig.DEBUG) {
|
||||
StrictMode.setThreadPolicy(
|
||||
@@ -183,6 +210,8 @@ 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
|
||||
@@ -249,6 +278,21 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
return eel
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Timber.i("onDestroy()")
|
||||
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
clipboardManager?.removePrimaryClipChangedListener(this)
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
florisboardInstance = null
|
||||
|
||||
serviceLifecycleDispatcher.onServicePreSuperOnDestroy()
|
||||
|
||||
eventListeners.toList().forEach { it?.onDestroy() }
|
||||
eventListeners.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onEvaluateFullscreenMode(): Boolean {
|
||||
return resources?.configuration?.let { config ->
|
||||
if (config.orientation != Configuration.ORIENTATION_LANDSCAPE) {
|
||||
@@ -286,7 +330,6 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
}
|
||||
this.inputView = inputView
|
||||
initializeOneHandedEnvironment()
|
||||
updateSoftInputWindowLayoutParameters()
|
||||
updateOneHandedPanelVisibility()
|
||||
themeManager.notifyCallbackReceivers()
|
||||
@@ -295,24 +338,11 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
eventListeners.toList().forEach { it?.onRegisterInputView(inputView) }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Timber.i("onDestroy()")
|
||||
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
clipboardManager?.removePrimaryClipChangedListener(this)
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
florisboardInstance = null
|
||||
|
||||
eventListeners.toList().forEach { it?.onDestroy() }
|
||||
eventListeners.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
|
||||
Timber.i("onStartInput($attribute, $restarting)")
|
||||
|
||||
super.onStartInput(attribute, restarting)
|
||||
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE)
|
||||
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
|
||||
}
|
||||
|
||||
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
|
||||
@@ -346,7 +376,13 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
Timber.i("onWindowShown()")
|
||||
if (isWindowShown) {
|
||||
Timber.i("Ignoring onWindowShown()")
|
||||
return
|
||||
} else {
|
||||
Timber.i("onWindowShown()")
|
||||
}
|
||||
isWindowShown = true
|
||||
|
||||
prefs.sync()
|
||||
val newIsNumberRowVisible = prefs.keyboard.numberRow
|
||||
@@ -365,7 +401,13 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
|
||||
override fun onWindowHidden() {
|
||||
Timber.i("onWindowHidden()")
|
||||
if (!isWindowShown) {
|
||||
Timber.i("Ignoring onWindowHidden()")
|
||||
return
|
||||
} else {
|
||||
Timber.i("onWindowHidden()")
|
||||
}
|
||||
isWindowShown = false
|
||||
|
||||
super.onWindowHidden()
|
||||
eventListeners.toList().forEach { it?.onWindowHidden() }
|
||||
@@ -380,24 +422,74 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins a FlorisBoard internal batch edit. This enables the application to continue sending selection updates
|
||||
* (some apps need to to this else they absolutely refuse to give visual feedback on cursor movement etc.). The
|
||||
* selection update is then caught if [internalBatchNestingLevel] is greater than 0, thus not delegating the
|
||||
* update to the editor instance. This is needed because else the UI stutters when too many updates arrive in a
|
||||
* row.
|
||||
*/
|
||||
fun beginInternalBatchEdit() {
|
||||
internalBatchNestingLevel++
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends an internal batch edit, if [internalBatchNestingLevel] is <= 1 and calls [onUpdateSelection] with the
|
||||
* corresponding reported selection values. This call is not caught and the editor instance and other classes are
|
||||
* able to update the UI. Resets the internal selection cache and is ready for the next batch edit.
|
||||
*/
|
||||
fun endInternalBatchEdit() {
|
||||
internalBatchNestingLevel = (internalBatchNestingLevel - 1).coerceAtLeast(0)
|
||||
if (internalBatchNestingLevel == 0) {
|
||||
internalSelectionCache.apply {
|
||||
if (selectionCatchCount > 0) {
|
||||
onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd,
|
||||
candidatesStart, candidatesEnd
|
||||
)
|
||||
selectionCatchCount = 0
|
||||
oldSelStart = -1
|
||||
oldSelEnd = -1
|
||||
newSelStart = -1
|
||||
newSelEnd = -1
|
||||
candidatesStart = -1
|
||||
candidatesEnd = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUpdateSelection(
|
||||
oldSelStart: Int, oldSelEnd: Int,
|
||||
newSelStart: Int, newSelEnd: Int,
|
||||
candidatesStart: Int, candidatesEnd: Int
|
||||
) {
|
||||
Timber.i("onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
|
||||
|
||||
super.onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd,
|
||||
candidatesStart, candidatesEnd
|
||||
)
|
||||
activeEditorInstance.onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd,
|
||||
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 +531,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
|
||||
@@ -586,7 +670,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)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -654,58 +738,29 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeOneHandedEnvironment() {
|
||||
{ v:View -> onOneHandedPanelButtonClick(v) }.also {
|
||||
inputView?.oneHandedCtrlMoveStart?.setOnClickListener(it)
|
||||
inputView?.oneHandedCtrlMoveEnd?.setOnClickListener(it)
|
||||
inputView?.oneHandedCtrlCloseStart?.setOnClickListener(it)
|
||||
inputView?.oneHandedCtrlCloseEnd?.setOnClickListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOneHandedPanelButtonClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.one_handed_ctrl_move_start -> {
|
||||
prefs.keyboard.oneHandedMode = "start"
|
||||
}
|
||||
R.id.one_handed_ctrl_move_end -> {
|
||||
prefs.keyboard.oneHandedMode = "end"
|
||||
}
|
||||
R.id.one_handed_ctrl_close_start,
|
||||
R.id.one_handed_ctrl_close_end -> {
|
||||
prefs.keyboard.oneHandedMode = "off"
|
||||
}
|
||||
}
|
||||
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,12 @@ import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
@@ -48,17 +50,9 @@ class InputView : LinearLayout {
|
||||
|
||||
var mainViewFlipper: ViewFlipper? = null
|
||||
private set
|
||||
var oneHandedCtrlPanelStart: LinearLayout? = null
|
||||
var oneHandedCtrlPanelStart: ViewGroup? = null
|
||||
private set
|
||||
var oneHandedCtrlPanelEnd: LinearLayout? = null
|
||||
private set
|
||||
var oneHandedCtrlMoveStart: ImageButton? = null
|
||||
private set
|
||||
var oneHandedCtrlMoveEnd: ImageButton? = null
|
||||
private set
|
||||
var oneHandedCtrlCloseStart: ImageButton? = null
|
||||
private set
|
||||
var oneHandedCtrlCloseEnd: ImageButton? = null
|
||||
var oneHandedCtrlPanelEnd: ViewGroup? = null
|
||||
private set
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
@@ -77,10 +71,6 @@ class InputView : LinearLayout {
|
||||
mainViewFlipper = findViewById(R.id.main_view_flipper)
|
||||
oneHandedCtrlPanelStart = findViewById(R.id.one_handed_ctrl_panel_start)
|
||||
oneHandedCtrlPanelEnd = findViewById(R.id.one_handed_ctrl_panel_end)
|
||||
oneHandedCtrlMoveStart = findViewById(R.id.one_handed_ctrl_move_start)
|
||||
oneHandedCtrlMoveEnd = findViewById(R.id.one_handed_ctrl_move_end)
|
||||
oneHandedCtrlCloseStart = findViewById(R.id.one_handed_ctrl_close_start)
|
||||
oneHandedCtrlCloseEnd = findViewById(R.id.one_handed_ctrl_close_end)
|
||||
|
||||
florisboard.registerInputView(this)
|
||||
}
|
||||
@@ -88,8 +78,8 @@ class InputView : LinearLayout {
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val heightFactor = when (resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> 1.0f
|
||||
else -> if (prefs.keyboard.oneHandedMode != "off") {
|
||||
0.9f
|
||||
else -> if (prefs.keyboard.oneHandedMode != OneHandedMode.OFF) {
|
||||
prefs.keyboard.oneHandedModeScaleFactor / 100.0f
|
||||
} else {
|
||||
1.0f
|
||||
}
|
||||
@@ -171,5 +161,4 @@ class InputView : LinearLayout {
|
||||
resources.getDimension(R.dimen.inputView_baseHeight)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ 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
|
||||
@@ -322,6 +323,7 @@ class PrefHelper(
|
||||
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"
|
||||
@@ -371,8 +373,11 @@ class PrefHelper(
|
||||
get() = prefHelper.getPref(NUMBER_ROW, false)
|
||||
set(v) = prefHelper.setPref(NUMBER_ROW, v)
|
||||
var oneHandedMode: String
|
||||
get() = prefHelper.getPref(ONE_HANDED_MODE, "off")
|
||||
get() = prefHelper.getPref(ONE_HANDED_MODE, OneHandedMode.OFF)
|
||||
set(value) = prefHelper.setPref(ONE_HANDED_MODE, value)
|
||||
var oneHandedModeScaleFactor: Int
|
||||
get() = prefHelper.getPref(ONE_HANDED_MODE_SCALE_FACTOR, 87)
|
||||
set(v) = prefHelper.setPref(ONE_HANDED_MODE_SCALE_FACTOR, v)
|
||||
var popupEnabled: Boolean = false
|
||||
get() = prefHelper.getPref(POPUP_ENABLED, true)
|
||||
private set
|
||||
|
||||
@@ -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,68 @@ class AssetManager private constructor(private val applicationContext: Context)
|
||||
else -> "{}"
|
||||
}
|
||||
return try {
|
||||
val adapter = moshi.adapter(assetClass)
|
||||
val adapter = moshi.adapter(assetClass.java)
|
||||
val asset = adapter.fromJson(rawJsonData)
|
||||
if (asset != null) {
|
||||
Ok(asset)
|
||||
Result.success(asset)
|
||||
} else {
|
||||
Err(NullPointerException("Asset failed to load!"))
|
||||
Result.failure(NullPointerException("Asset failed to load!"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Err(e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T: Asset> writeAsset(ref: AssetRef, assetClass: Class<T>, asset: T): Result<Boolean, Throwable> {
|
||||
fun <T : Asset> loadAsset(uri: Uri, assetClass: KClass<T>): Result<T> {
|
||||
val rawJsonData = ExternalContentUtils.readTextFromUri(applicationContext, uri).onFailure {
|
||||
return Result.failure(it)
|
||||
}
|
||||
return try {
|
||||
val adapter = moshi.adapter(assetClass.java)
|
||||
val asset = adapter.fromJson(rawJsonData.getOrNull()!!)
|
||||
if (asset != null) {
|
||||
Result.success(asset)
|
||||
} else {
|
||||
Result.failure(NullPointerException("Asset failed to load!"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun loadAssetRaw(ref: AssetRef): Result<String> {
|
||||
return when (ref.source) {
|
||||
is AssetSource.Assets -> {
|
||||
try {
|
||||
Result.success(applicationContext.assets.open(ref.path).bufferedReader().use { it.readText() })
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
is AssetSource.Internal -> {
|
||||
val file = File(applicationContext.filesDir.absolutePath + "/" + ref.path)
|
||||
val contents = readFile(file)
|
||||
if (contents.isBlank()) {
|
||||
Result.failure(Exception("File is blank!"))
|
||||
} else {
|
||||
Result.success(contents)
|
||||
}
|
||||
}
|
||||
else -> Result.failure(Exception("Unsupported asset ref!"))
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Asset> writeAsset(ref: AssetRef, assetClass: KClass<T>, asset: T): Result<Unit> {
|
||||
return when (ref.source) {
|
||||
AssetSource.Internal -> {
|
||||
val adapter = moshi.adapter(assetClass)
|
||||
val adapter = moshi.adapter(assetClass.java)
|
||||
val rawJson = adapter.toJson(asset)
|
||||
val file = File(applicationContext.filesDir.absolutePath + "/" + ref.path)
|
||||
writeToFile(file, rawJson)
|
||||
Ok(true)
|
||||
Result.success(Unit)
|
||||
}
|
||||
else -> Err(Exception("Can not write an asset in source '${ref.source}'"))
|
||||
else -> Result.failure(Exception("Can not write an asset in source '${ref.source}'"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.extension
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.github.michaelbull.result.getOrElse
|
||||
|
||||
/**
|
||||
* Data class which is a reference to an asset file. It indicates in which storage medium the asset
|
||||
* is as well as the relative path to it.
|
||||
@@ -36,15 +31,15 @@ data class AssetRef(
|
||||
companion object {
|
||||
private const val DELIMITER: String = ":"
|
||||
|
||||
fun fromString(str: String): Result<AssetRef, String> {
|
||||
fun fromString(str: String): Result<AssetRef> {
|
||||
val items = str.split(DELIMITER)
|
||||
if (items.size != 2) {
|
||||
return Err("Unexpected length of given asset ref. Make sure that the asset ref string contains exactly 2 items separated by '$DELIMITER'!")
|
||||
return Result.failure(Exception("Unexpected length of given asset ref. Make sure that the asset ref string contains exactly 2 items separated by '$DELIMITER'!"))
|
||||
}
|
||||
val retSource = AssetSource.fromString(items[0]).getOrElse {
|
||||
return Err(it)
|
||||
return Result.failure(Exception(it))
|
||||
}
|
||||
return Ok(AssetRef(retSource, items[1]))
|
||||
return Result.success(AssetRef(retSource, items[1]))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.extension
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -52,16 +49,16 @@ sealed class AssetSource {
|
||||
companion object {
|
||||
private val externalRegex: Regex = """^external\\(([a-z]+\\.)*[a-z]+\\)\$""".toRegex()
|
||||
|
||||
fun fromString(str: String): Result<AssetSource, String> {
|
||||
fun fromString(str: String): Result<AssetSource> {
|
||||
return when (val string = str.toLowerCase(Locale.ENGLISH)) {
|
||||
"assets" -> Ok(Assets)
|
||||
"internal" -> Ok(Internal)
|
||||
"assets" -> Result.success(Assets)
|
||||
"internal" -> Result.success(Internal)
|
||||
else -> {
|
||||
if (string.matches(externalRegex)) {
|
||||
val packageName = string.substring(9, string.length - 1)
|
||||
Ok(External(packageName))
|
||||
Result.success(External(packageName))
|
||||
} else {
|
||||
Err("'$str' is not a valid AssetSource.")
|
||||
Result.failure(Exception("'$str' is not a valid AssetSource."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.extension
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
|
||||
class ExternalContentUtils private constructor() {
|
||||
companion object {
|
||||
fun readTextFromUri(context: Context, uri: Uri): Result<String> {
|
||||
val contentResolver = context.contentResolver
|
||||
?: return Result.failure(NullPointerException("System content resolver not available"))
|
||||
val inputStream = contentResolver.openInputStream(uri)
|
||||
?: return Result.failure(NullPointerException("Cannot open input stream for given uri '$uri'"))
|
||||
val rawText = inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
|
||||
return Result.success(rawText)
|
||||
}
|
||||
|
||||
fun writeTextToUri(context: Context, uri: Uri, text: String): Result<Unit> {
|
||||
val contentResolver = context.contentResolver
|
||||
?: return Result.failure(NullPointerException("System content resolver not available"))
|
||||
// Must use "rwt" mode to ensure destination file length is truncated after writing.
|
||||
val outputStream = contentResolver.openOutputStream(uri, "rwt")
|
||||
?: return Result.failure(NullPointerException("Cannot open output stream for given uri '$uri'"))
|
||||
outputStream.bufferedWriter(Charsets.UTF_8).use { it.flush(); it.write(text) }
|
||||
return Result.success(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,12 +18,10 @@ 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.core.*
|
||||
@@ -56,7 +54,7 @@ import kotlin.math.roundToLong
|
||||
* TextInputManager is also the hub in the communication between the system, the active editor
|
||||
* instance and the Smartbar.
|
||||
*/
|
||||
class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
class TextInputManager private constructor() : CoroutineScope by MainScope(), InputKeyEventReceiver,
|
||||
FlorisBoard.EventListener, SmartbarView.EventListener {
|
||||
|
||||
private val florisboard = FlorisBoard.getInstance()
|
||||
@@ -68,28 +66,37 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
private val keyboardViews = EnumMap<KeyboardMode, KeyboardView>(KeyboardMode::class.java)
|
||||
private var editingKeyboardView: EditingKeyboardView? = null
|
||||
private var loadingPlaceholderKeyboard: KeyboardView? = null
|
||||
private val osHandler = Handler()
|
||||
private var textViewFlipper: ViewFlipper? = null
|
||||
private var textViewGroup: LinearLayout? = null
|
||||
private val dictionaryManager: DictionaryManager = DictionaryManager.default()
|
||||
private var activeDictionary: Dictionary<String, Int>? = null
|
||||
val inputEventDispatcher: InputEventDispatcher = InputEventDispatcher.new(
|
||||
parentScope = this,
|
||||
repeatableKeyCodes = intArrayOf(
|
||||
KeyCode.ARROW_DOWN,
|
||||
KeyCode.ARROW_LEFT,
|
||||
KeyCode.ARROW_RIGHT,
|
||||
KeyCode.ARROW_UP,
|
||||
KeyCode.DELETE,
|
||||
KeyCode.FORWARD_DELETE
|
||||
)
|
||||
)
|
||||
|
||||
var keyVariation: KeyVariation = KeyVariation.NORMAL
|
||||
val layoutManager = LayoutManager(florisboard)
|
||||
private var smartbarView: SmartbarView? = null
|
||||
|
||||
// Caps/Space related properties
|
||||
// Caps/Shift related properties
|
||||
var caps: Boolean = false
|
||||
private set
|
||||
var capsLock: Boolean = false
|
||||
private set
|
||||
private var hasCapsRecentlyChanged: Boolean = false
|
||||
private var hasSpaceRecentlyPressed: Boolean = false
|
||||
private var newCapsState: Boolean = false
|
||||
|
||||
// Composing text related properties
|
||||
var isManualSelectionMode: Boolean = false
|
||||
private var isManualSelectionModeLeft: Boolean = false
|
||||
private var isManualSelectionModeRight: Boolean = false
|
||||
private var isManualSelectionModeStart: Boolean = false
|
||||
private var isManualSelectionModeEnd: Boolean = false
|
||||
|
||||
companion object {
|
||||
private var instance: TextInputManager? = null
|
||||
@@ -114,6 +121,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
override fun onCreate() {
|
||||
Timber.i("onCreate()")
|
||||
|
||||
inputEventDispatcher.keyEventReceiver = this
|
||||
var subtypes = florisboard.subtypeManager.subtypes
|
||||
if (subtypes.isEmpty()) {
|
||||
subtypes = listOf(Subtype.DEFAULT)
|
||||
@@ -199,8 +207,9 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
override fun onDestroy() {
|
||||
Timber.i("onDestroy()")
|
||||
|
||||
inputEventDispatcher.keyEventReceiver = null
|
||||
inputEventDispatcher.close()
|
||||
cancel()
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
layoutManager.onDestroy()
|
||||
instance = null
|
||||
}
|
||||
@@ -300,8 +309,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 +320,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 +335,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 {
|
||||
@@ -376,9 +382,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
if (!capsLock) {
|
||||
caps = florisboard.prefs.correction.autoCapitalization &&
|
||||
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
|
||||
launch(Dispatchers.Main) {
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
}
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,22 +391,23 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* class.
|
||||
*/
|
||||
fun executeSwipeAction(swipeAction: SwipeAction) {
|
||||
when (swipeAction) {
|
||||
SwipeAction.DELETE_WORD -> handleDeleteWord()
|
||||
SwipeAction.INSERT_SPACE -> handleSpace()
|
||||
SwipeAction.MOVE_CURSOR_DOWN -> handleArrow(KeyCode.ARROW_DOWN)
|
||||
SwipeAction.MOVE_CURSOR_UP -> handleArrow(KeyCode.ARROW_UP)
|
||||
SwipeAction.MOVE_CURSOR_LEFT -> handleArrow(KeyCode.ARROW_LEFT)
|
||||
SwipeAction.MOVE_CURSOR_RIGHT -> handleArrow(KeyCode.ARROW_RIGHT)
|
||||
SwipeAction.MOVE_CURSOR_START_OF_LINE -> handleArrow(KeyCode.MOVE_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.SHOW_INPUT_METHOD_PICKER -> KeyData.SHOW_INPUT_METHOD_PICKER
|
||||
else -> null
|
||||
}
|
||||
if (keyData != null) {
|
||||
inputEventDispatcher.send(InputKeyEvent.downUp(keyData))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,11 +436,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 +448,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
private fun handleUndo(){
|
||||
activeEditorInstance.performUndo()
|
||||
}
|
||||
|
||||
private fun handleRedo(){
|
||||
activeEditorInstance.performRedo()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.DELETE] event.
|
||||
*/
|
||||
private fun handleDelete() {
|
||||
hasCapsRecentlyChanged = false
|
||||
hasSpaceRecentlyPressed = false
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = false
|
||||
activeEditorInstance.deleteBackwards()
|
||||
}
|
||||
|
||||
@@ -467,11 +462,9 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Handles a [KeyCode.DELETE_WORD] event.
|
||||
*/
|
||||
private fun handleDeleteWord() {
|
||||
hasCapsRecentlyChanged = false
|
||||
hasSpaceRecentlyPressed = false
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
isManualSelectionModeStart = false
|
||||
isManualSelectionModeEnd = false
|
||||
activeEditorInstance.deleteWordsBeforeCursor(1)
|
||||
}
|
||||
|
||||
@@ -479,8 +472,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Handles a [KeyCode.ENTER] event.
|
||||
*/
|
||||
private fun handleEnter() {
|
||||
hasCapsRecentlyChanged = false
|
||||
hasSpaceRecentlyPressed = false
|
||||
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
|
||||
activeEditorInstance.performEnter()
|
||||
} else {
|
||||
@@ -511,45 +502,67 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] event.
|
||||
* Handles a [KeyCode.SHIFT] down event.
|
||||
*/
|
||||
private fun handleShift() {
|
||||
if (hasCapsRecentlyChanged) {
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
private fun handleShiftDown(ev: InputKeyEvent) {
|
||||
if (ev.isConsecutiveEventOf(inputEventDispatcher.lastKeyEventDown, florisboard.prefs.keyboard.longPressDelay.toLong())) {
|
||||
newCapsState = true
|
||||
caps = true
|
||||
capsLock = true
|
||||
hasCapsRecentlyChanged = false
|
||||
} else {
|
||||
caps = !caps
|
||||
newCapsState = !caps
|
||||
caps = true
|
||||
capsLock = false
|
||||
hasCapsRecentlyChanged = true
|
||||
osHandler.postDelayed({
|
||||
hasCapsRecentlyChanged = false
|
||||
}, 300)
|
||||
}
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] up event.
|
||||
*/
|
||||
private fun handleShiftUp() {
|
||||
caps = newCapsState
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] cancel event.
|
||||
*/
|
||||
private fun handleShiftCancel() {
|
||||
caps = false
|
||||
capsLock = false
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SHIFT] up event.
|
||||
*/
|
||||
private fun handleShiftLock() {
|
||||
val lastKeyEvent = inputEventDispatcher.lastKeyEventDown ?: return
|
||||
if (lastKeyEvent.data.code == KeyCode.SHIFT && lastKeyEvent.action == InputKeyEvent.Action.DOWN) {
|
||||
newCapsState = true
|
||||
caps = true
|
||||
capsLock = true
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
smartbarView?.updateCandidateSuggestionCapsState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.SPACE] event. Also handles the auto-correction of two space taps if
|
||||
* enabled by the user.
|
||||
*/
|
||||
private fun handleSpace() {
|
||||
private fun handleSpace(ev: InputKeyEvent) {
|
||||
if (florisboard.prefs.correction.doubleSpacePeriod) {
|
||||
if (hasSpaceRecentlyPressed) {
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
if (ev.isConsecutiveEventOf(inputEventDispatcher.lastKeyEventUp, florisboard.prefs.keyboard.longPressDelay.toLong())) {
|
||||
val text = activeEditorInstance.getTextBeforeCursor(2)
|
||||
if (text.length == 2 && !text.matches("""[.!?‽\s][\s]""".toRegex())) {
|
||||
activeEditorInstance.deleteBackwards()
|
||||
activeEditorInstance.commitText(".")
|
||||
}
|
||||
hasSpaceRecentlyPressed = false
|
||||
} else {
|
||||
hasSpaceRecentlyPressed = true
|
||||
osHandler.postDelayed({
|
||||
hasSpaceRecentlyPressed = false
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
activeEditorInstance.commitText(KeyCode.SPACE.toChar().toString())
|
||||
@@ -558,119 +571,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 +638,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 +652,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,7 +681,11 @@ 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 -> {
|
||||
@@ -718,15 +696,28 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> activeEditorInstance.performClipboardSelectAll()
|
||||
KeyCode.DELETE -> {
|
||||
handleDelete()
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
if (ev.action == InputKeyEvent.Action.DOWN_UP || ev.action == InputKeyEvent.Action.UP) {
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
}
|
||||
KeyCode.DELETE_WORD -> {
|
||||
handleDeleteWord()
|
||||
if (ev.action == InputKeyEvent.Action.DOWN_UP || ev.action == InputKeyEvent.Action.UP) {
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
}
|
||||
KeyCode.ENTER -> {
|
||||
handleEnter()
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
KeyCode.INTERNAL_BATCH_EDIT -> {
|
||||
florisboard.endInternalBatchEdit()
|
||||
return
|
||||
}
|
||||
KeyCode.LANGUAGE_SWITCH -> handleLanguageSwitch()
|
||||
KeyCode.SETTINGS -> florisboard.launchSettings()
|
||||
KeyCode.SHIFT -> handleShift()
|
||||
KeyCode.SHIFT -> handleShiftUp()
|
||||
KeyCode.SHIFT_LOCK -> handleShiftLock()
|
||||
KeyCode.SHOW_INPUT_METHOD_PICKER -> florisboard.imeManager?.showInputMethodPicker()
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> florisboard.setActiveInput(R.id.media_input)
|
||||
KeyCode.SWITCH_TO_TEXT_CONTEXT -> florisboard.setActiveInput(R.id.text_input)
|
||||
@@ -744,49 +735,59 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.NUMERIC_ADVANCED,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> when (keyData.type) {
|
||||
KeyboardMode.PHONE2 -> when (data.type) {
|
||||
KeyType.CHARACTER,
|
||||
KeyType.NUMERIC -> {
|
||||
val text = keyData.code.toChar().toString()
|
||||
val text = data.code.toChar().toString()
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
else -> when (keyData.code) {
|
||||
else -> when (data.code) {
|
||||
KeyCode.PHONE_PAUSE,
|
||||
KeyCode.PHONE_WAIT -> {
|
||||
val text = keyData.code.toChar().toString()
|
||||
val text = data.code.toChar().toString()
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> when (keyData.type) {
|
||||
KeyType.CHARACTER, KeyType.NUMERIC -> when (keyData.code) {
|
||||
KeyCode.SPACE -> handleSpace()
|
||||
else -> when (data.type) {
|
||||
KeyType.CHARACTER, KeyType.NUMERIC -> when (data.code) {
|
||||
KeyCode.SPACE -> handleSpace(ev)
|
||||
KeyCode.URI_COMPONENT_TLD -> {
|
||||
val tld = keyData.label.toLowerCase(Locale.ENGLISH)
|
||||
val tld = data.label.toLowerCase(Locale.ENGLISH)
|
||||
activeEditorInstance.commitText(tld)
|
||||
}
|
||||
else -> {
|
||||
hasCapsRecentlyChanged = false
|
||||
hasSpaceRecentlyPressed = false
|
||||
var text = keyData.code.toChar().toString()
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -89,6 +89,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,167 @@ open class KeyData(
|
||||
var type: KeyType = KeyType.CHARACTER,
|
||||
var code: Int = 0,
|
||||
var label: String = ""
|
||||
)
|
||||
) {
|
||||
companion object {
|
||||
/** Predefined key data for [KeyCode.ARROW_DOWN] */
|
||||
val ARROW_DOWN = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.ARROW_DOWN,
|
||||
label = "arrow_down"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.ARROW_LEFT] */
|
||||
val ARROW_LEFT = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.ARROW_LEFT,
|
||||
label = "arrow_left"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.ARROW_RIGHT] */
|
||||
val ARROW_RIGHT = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.ARROW_RIGHT,
|
||||
label = "arrow_right"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.ARROW_UP] */
|
||||
val ARROW_UP = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.ARROW_UP,
|
||||
label = "arrow_up"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_COPY] */
|
||||
val CLIPBOARD_COPY = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_COPY,
|
||||
label = "clipboard_copy"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_CUT] */
|
||||
val CLIPBOARD_CUT = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_CUT,
|
||||
label = "clipboard_cut"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_PASTE] */
|
||||
val CLIPBOARD_PASTE = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_PASTE,
|
||||
label = "clipboard_paste"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_SELECT] */
|
||||
val CLIPBOARD_SELECT = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_SELECT,
|
||||
label = "clipboard_select"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.CLIPBOARD_SELECT_ALL] */
|
||||
val CLIPBOARD_SELECT_ALL = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.CLIPBOARD_SELECT_ALL,
|
||||
label = "clipboard_select_all"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.DELETE] */
|
||||
val DELETE = KeyData(
|
||||
type = KeyType.ENTER_EDITING,
|
||||
code = KeyCode.DELETE,
|
||||
label = "delete"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.DELETE_WORD] */
|
||||
val DELETE_WORD = KeyData(
|
||||
type = KeyType.ENTER_EDITING,
|
||||
code = KeyCode.DELETE_WORD,
|
||||
label = "delete_word"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.INTERNAL_BATCH_EDIT] */
|
||||
val INTERNAL_BATCH_EDIT = KeyData(
|
||||
type = KeyType.FUNCTION,
|
||||
code = KeyCode.INTERNAL_BATCH_EDIT,
|
||||
label = "internal_batch_edit"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.MOVE_START_OF_LINE] */
|
||||
val MOVE_START_OF_LINE = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.MOVE_START_OF_LINE,
|
||||
label = "move_start_of_line"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.MOVE_END_OF_LINE] */
|
||||
val MOVE_END_OF_LINE = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.MOVE_END_OF_LINE,
|
||||
label = "move_end_of_line"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.MOVE_START_OF_PAGE] */
|
||||
val MOVE_START_OF_PAGE = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.MOVE_START_OF_PAGE,
|
||||
label = "move_start_of_page"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.MOVE_END_OF_PAGE] */
|
||||
val MOVE_END_OF_PAGE = KeyData(
|
||||
type = KeyType.NAVIGATION,
|
||||
code = KeyCode.MOVE_END_OF_PAGE,
|
||||
label = "move_end_of_page"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SHOW_INPUT_METHOD_PICKER] */
|
||||
val SHOW_INPUT_METHOD_PICKER = KeyData(
|
||||
type = KeyType.FUNCTION,
|
||||
code = KeyCode.SHOW_INPUT_METHOD_PICKER,
|
||||
label = "show_input_method_picker"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SWITCH_TO_TEXT_CONTEXT] */
|
||||
val SWITCH_TO_TEXT_CONTEXT = KeyData(
|
||||
type = KeyType.SYSTEM_GUI,
|
||||
code = KeyCode.SWITCH_TO_TEXT_CONTEXT,
|
||||
label = "switch_to_text_context"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SHIFT] */
|
||||
val SHIFT = KeyData(
|
||||
type = KeyType.MODIFIER,
|
||||
code = KeyCode.SHIFT,
|
||||
label = "shift"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SHIFT_LOCK] */
|
||||
val SHIFT_LOCK = KeyData(
|
||||
type = KeyType.MODIFIER,
|
||||
code = KeyCode.SHIFT_LOCK,
|
||||
label = "shift_lock"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.SPACE] */
|
||||
val SPACE = KeyData(
|
||||
type = KeyType.CHARACTER,
|
||||
code = KeyCode.SPACE,
|
||||
label = "space"
|
||||
)
|
||||
|
||||
/** Predefined key data for [KeyCode.UNSPECIFIED] */
|
||||
val UNSPECIFIED = KeyData(
|
||||
type = KeyType.UNSPECIFIED,
|
||||
code = KeyCode.UNSPECIFIED,
|
||||
label = "unspecified"
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "KeyData { type=$type code=$code label=\"$label\" }"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class which describes a single key and its attributes, while also providing additional
|
||||
@@ -47,6 +207,10 @@ open class KeyData(
|
||||
* or if the key should always be visible. Defaults to [KeyVariation.ALL].
|
||||
* @property popup List of keys which will be accessible while long pressing the key. Defaults to
|
||||
* an empty set (no extended popup).
|
||||
* @property shift An alternative key to use when the keyboard caps state is true. Useful for layouts
|
||||
* such as Colemak and Dvorak. Defaults to null (don't override base uppercase key). This override
|
||||
* property should only be used to provide an uppercase variant of two else not related variants, but
|
||||
* should not be used for providing an uppercase letter (e.g. 'a' -> 'A').
|
||||
*/
|
||||
class FlorisKeyData(
|
||||
type: KeyType = KeyType.CHARACTER,
|
||||
@@ -54,7 +218,8 @@ class FlorisKeyData(
|
||||
label: String = "",
|
||||
var groupId: Int = GROUP_DEFAULT,
|
||||
var variation: KeyVariation = KeyVariation.ALL,
|
||||
var popup: PopupSet<KeyData> = PopupSet()
|
||||
var popup: PopupSet<KeyData> = PopupSet(),
|
||||
var shift: KeyData? = null
|
||||
) : KeyData(type, code, label) {
|
||||
companion object {
|
||||
/**
|
||||
|
||||
@@ -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,22 @@ class KeyView(
|
||||
*/
|
||||
fun getComputedLetter(
|
||||
keyData: KeyData = data,
|
||||
caps: Boolean = florisboard?.textInputManager?.caps ?: false && florisboard?.textInputManager?.getActiveKeyboardMode() == KeyboardMode.CHARACTERS,
|
||||
caps: Boolean = florisboard?.textInputManager?.caps ?: false,
|
||||
subtype: Subtype = florisboard?.activeSubtype ?: Subtype.DEFAULT
|
||||
): String {
|
||||
return when (data.code) {
|
||||
KeyCode.URI_COMPONENT_TLD -> keyData.label.toLowerCase(Locale.ENGLISH)
|
||||
else -> {
|
||||
val labelText = (keyData.code.toChar()).toString()
|
||||
if (caps) {
|
||||
labelText.toUpperCase(subtype.locale)
|
||||
} else {
|
||||
labelText
|
||||
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 +241,80 @@ class KeyView(
|
||||
*/
|
||||
fun onFlorisTouchEvent(event: MotionEvent?): Boolean {
|
||||
if (event == null || !isEnabled) return false
|
||||
val florisboard = florisboard ?: return false
|
||||
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
|
||||
val alwaysTriggerOnMove = (hasTriggeredGestureMove
|
||||
&& (data.code == KeyCode.DELETE && prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_CHARACTERS_PRECISELY
|
||||
|| data.code == KeyCode.SPACE))
|
||||
&& (data.code == KeyCode.DELETE && prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_CHARACTERS_PRECISELY
|
||||
|| data.code == KeyCode.SPACE))
|
||||
if (swipeGestureDetector.onTouchEvent(event, alwaysTriggerOnMove)) {
|
||||
isKeyPressed = false
|
||||
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
longKeyPressHandler.cancelAll()
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
keyboardView.popupManager.hide()
|
||||
popupManager.hide()
|
||||
return true
|
||||
}
|
||||
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (data.code == KeyCode.SHIFT) {
|
||||
isKeyPressed = true
|
||||
florisboard?.keyPressVibrate()
|
||||
florisboard?.keyPressSound(data)
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
} else {
|
||||
val delayMillis = prefs.keyboard.longPressDelay.toLong()
|
||||
hasTriggeredGestureMove = false
|
||||
shouldBlockNextKeyCode = false
|
||||
florisboard?.prefs?.keyboard?.let {
|
||||
if (it.popupEnabled){
|
||||
keyboardView.popupManager.show(this, keyHintMode)
|
||||
}
|
||||
}
|
||||
isKeyPressed = true
|
||||
florisboard?.keyPressVibrate()
|
||||
florisboard?.keyPressSound(data)
|
||||
when (data.code) {
|
||||
KeyCode.ARROW_DOWN,
|
||||
KeyCode.ARROW_LEFT,
|
||||
KeyCode.ARROW_RIGHT,
|
||||
KeyCode.ARROW_UP,
|
||||
KeyCode.DELETE -> {
|
||||
repeatedKeyPressHandler.postAtScheduledRate((delayMillis * 2.0f).toLong(), 25) {
|
||||
if (isKeyPressed) {
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
} else {
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.code == KeyCode.SPACE) {
|
||||
initSelectionStart = florisboard?.activeEditorInstance?.selection?.start ?: 0
|
||||
initSelectionEnd = florisboard?.activeEditorInstance?.selection?.end ?: 0
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.down(data))
|
||||
isKeyPressed = true
|
||||
val delayMillis = prefs.keyboard.longPressDelay.toLong()
|
||||
hasTriggeredGestureMove = false
|
||||
shouldBlockNextKeyCode = false
|
||||
if (florisboard.prefs.keyboard.popupEnabled) {
|
||||
popupManager.show(this, keyHintMode)
|
||||
}
|
||||
isKeyPressed = true
|
||||
florisboard.keyPressVibrate()
|
||||
florisboard.keyPressSound(data)
|
||||
|
||||
when (data.code) {
|
||||
KeyCode.SPACE -> {
|
||||
initSelectionStart = florisboard.activeEditorInstance.selection.start
|
||||
initSelectionEnd = florisboard.activeEditorInstance.selection.end
|
||||
longKeyPressHandler.postDelayed((delayMillis * 2.5f).toLong()) {
|
||||
when (prefs.gestures.spaceBarLongPress) {
|
||||
SwipeAction.NO_ACTION,
|
||||
SwipeAction.INSERT_SPACE -> {}
|
||||
SwipeAction.INSERT_SPACE -> {
|
||||
}
|
||||
else -> {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.spaceBarLongPress)
|
||||
this.florisboard.executeSwipeAction(prefs.gestures.spaceBarLongPress)
|
||||
shouldBlockNextKeyCode = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
KeyCode.SHIFT -> {
|
||||
longKeyPressHandler.postDelayed((delayMillis * 2.5).toLong()) {
|
||||
this.florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.SHIFT_LOCK))
|
||||
}
|
||||
}
|
||||
KeyCode.LANGUAGE_SWITCH -> {
|
||||
longKeyPressHandler.postDelayed((delayMillis * 2.0).toLong()) {
|
||||
shouldBlockNextKeyCode = true
|
||||
this.florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.SHOW_INPUT_METHOD_PICKER))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
longKeyPressHandler.postDelayed(delayMillis) {
|
||||
if (data.popup.isNotEmpty()) {
|
||||
keyboardView.popupManager.extend(this, keyHintMode)
|
||||
popupManager.extend(this@KeyView, keyHintMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (keyboardView.popupManager.isShowingExtendedPopup) {
|
||||
if (popupManager.isShowingExtendedPopup) {
|
||||
val isPointerWithinBounds =
|
||||
keyboardView.popupManager.propagateMotionEvent(this, event)
|
||||
popupManager.propagateMotionEvent(this, event)
|
||||
if (!isPointerWithinBounds && !shouldBlockNextKeyCode) {
|
||||
keyboardView.dismissActiveKeyViewReference()
|
||||
keyboardView.dismissActiveKeyViewReference(pointerId)
|
||||
}
|
||||
} else {
|
||||
val parent = parent as ViewGroup
|
||||
@@ -309,31 +324,49 @@ class KeyView(
|
||||
|| event.y > 1.35f * measuredHeight
|
||||
) {
|
||||
if (!shouldBlockNextKeyCode) {
|
||||
keyboardView.dismissActiveKeyViewReference()
|
||||
keyboardView.dismissActiveKeyViewReference(pointerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
MotionEvent.ACTION_UP -> {
|
||||
longKeyPressHandler.cancelAll()
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
if (data.code != KeyCode.SHIFT) {
|
||||
if (hasTriggeredGestureMove && data.code == KeyCode.DELETE) {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
deleteBackwards()
|
||||
if (hasTriggeredGestureMove && data.code == KeyCode.DELETE) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
florisboard.activeEditorInstance.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
deleteBackwards()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val retData = popupManager.getActiveKeyData(this)
|
||||
if (!shouldBlockNextKeyCode && retData != null) {
|
||||
if (retData == data) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.up(data))
|
||||
} else {
|
||||
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(retData))
|
||||
}
|
||||
} else {
|
||||
val retData = keyboardView.popupManager.getActiveKeyData(this)
|
||||
if (event.actionMasked != MotionEvent.ACTION_CANCEL && !shouldBlockNextKeyCode && retData != null) {
|
||||
florisboard?.textInputManager?.sendKeyPress(retData)
|
||||
} else {
|
||||
shouldBlockNextKeyCode = false
|
||||
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
keyboardView.popupManager.hide()
|
||||
}
|
||||
popupManager.hide()
|
||||
}
|
||||
shouldBlockNextKeyCode = false
|
||||
hasTriggeredGestureMove = false
|
||||
isKeyPressed = false
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
longKeyPressHandler.cancelAll()
|
||||
if (data.code != KeyCode.SHIFT) {
|
||||
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
|
||||
}
|
||||
popupManager.hide()
|
||||
shouldBlockNextKeyCode = false
|
||||
hasTriggeredGestureMove = false
|
||||
isKeyPressed = false
|
||||
}
|
||||
@@ -354,7 +387,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 +431,10 @@ class KeyView(
|
||||
}
|
||||
SwipeGesture.Direction.LEFT -> {
|
||||
if (prefs.gestures.spaceBarSwipeLeft == SwipeAction.MOVE_CURSOR_LEFT) {
|
||||
if (!florisboard.activeEditorInstance.isRawInputEditor) {
|
||||
val s = (initSelectionEnd + event.absUnitCountX).coerceIn(0, florisboard.activeEditorInstance.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 +446,10 @@ class KeyView(
|
||||
}
|
||||
SwipeGesture.Direction.RIGHT -> {
|
||||
if (prefs.gestures.spaceBarSwipeRight == SwipeAction.MOVE_CURSOR_RIGHT) {
|
||||
if (!florisboard.activeEditorInstance.isRawInputEditor) {
|
||||
val s = (initSelectionEnd + event.absUnitCountX).coerceIn(0, florisboard.activeEditorInstance.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 {
|
||||
@@ -449,10 +478,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()
|
||||
}
|
||||
@@ -476,11 +505,11 @@ class KeyView(
|
||||
else -> when (data.code) {
|
||||
KeyCode.SHIFT,
|
||||
KeyCode.DELETE ->
|
||||
if ((keyboardView.computedLayout?.arrangement?.get(2)?.size ?: 0) > 10) {
|
||||
1.12f
|
||||
} else {
|
||||
1.56f
|
||||
}
|
||||
if ((keyboardView.computedLayout?.arrangement?.get(2)?.size ?: 0) > 10) {
|
||||
1.12f
|
||||
} else {
|
||||
1.56f
|
||||
}
|
||||
KeyCode.VIEW_CHARACTERS,
|
||||
KeyCode.VIEW_SYMBOLS,
|
||||
KeyCode.VIEW_SYMBOLS2,
|
||||
@@ -551,8 +580,8 @@ class KeyView(
|
||||
isEnabled = when (data.code) {
|
||||
KeyCode.CLIPBOARD_COPY,
|
||||
KeyCode.CLIPBOARD_CUT -> (florisboard != null
|
||||
&& florisboard.activeEditorInstance.selection.isSelectionMode
|
||||
&& !florisboard.activeEditorInstance.isRawInputEditor)
|
||||
&& florisboard.activeEditorInstance.selection.isSelectionMode
|
||||
&& !florisboard.activeEditorInstance.isRawInputEditor)
|
||||
KeyCode.CLIPBOARD_PASTE -> florisboard?.clipboardManager?.hasPrimaryClip() == true
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> {
|
||||
florisboard?.activeEditorInstance?.isRawInputEditor == false
|
||||
@@ -642,10 +671,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()
|
||||
}
|
||||
@@ -711,7 +740,11 @@ class KeyView(
|
||||
}
|
||||
else -> if (data.variation != KeyVariation.ALL) {
|
||||
val keyVariation = florisboard?.textInputManager?.keyVariation ?: KeyVariation.NORMAL
|
||||
visibility = if (data.variation == keyVariation) { VISIBLE } else { GONE }
|
||||
visibility = if (data.variation == keyVariation) {
|
||||
VISIBLE
|
||||
} else {
|
||||
GONE
|
||||
}
|
||||
updateTouchHitBox()
|
||||
}
|
||||
}
|
||||
@@ -827,7 +860,8 @@ class KeyView(
|
||||
KeyboardMode.CHARACTERS -> {
|
||||
label = florisboard?.activeSubtype?.locale?.displayName
|
||||
}
|
||||
else -> {}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
|
||||
@@ -907,7 +941,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 +960,7 @@ class KeyView(
|
||||
else -> 1.0f
|
||||
}
|
||||
)
|
||||
keyboardView.popupManager.keyPopupTextSize = cachedTextSize
|
||||
popupManager.keyPopupTextSize = cachedTextSize
|
||||
}
|
||||
else -> {
|
||||
setTextSizeFor(
|
||||
@@ -949,7 +983,11 @@ class KeyView(
|
||||
themeValueCache.keyForeground.toSolidColor().color
|
||||
}
|
||||
labelPaint.alpha = if (keyboardView.computedLayout?.mode == KeyboardMode.CHARACTERS &&
|
||||
data.code == KeyCode.SPACE) { 120 } else { 255 }
|
||||
data.code == KeyCode.SPACE) {
|
||||
120
|
||||
} else {
|
||||
255
|
||||
}
|
||||
val centerX = measuredWidth / 2.0f
|
||||
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2
|
||||
if (label.contains("\n")) {
|
||||
|
||||
@@ -27,11 +27,12 @@ import androidx.core.view.children
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.popup.PopupManager
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.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
|
||||
@@ -49,10 +50,8 @@ import kotlin.math.roundToInt
|
||||
*/
|
||||
class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Listener,
|
||||
ThemeManager.OnThemeUpdatedListener {
|
||||
private var activeKeyView: KeyView? = null
|
||||
private var activePointerId: Int? = null
|
||||
private var activeX: Float = 0.0f
|
||||
private var activeY: Float = 0.0f
|
||||
private var activeKeyViews: MutableMap<Int, KeyView> = mutableMapOf()
|
||||
private var initialKeyCodes: MutableMap<Int, Int> = mutableMapOf()
|
||||
|
||||
var computedLayout: ComputedLayoutData? = null
|
||||
set(v) {
|
||||
@@ -62,11 +61,9 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
var desiredKeyWidth: Int = resources.getDimension(R.dimen.key_width).toInt()
|
||||
var desiredKeyHeight: Int = resources.getDimension(R.dimen.key_height).toInt()
|
||||
var florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private var initialKeyCode: Int = 0
|
||||
private val isPreviewMode: Boolean
|
||||
val isSmartbarKeyboardView: Boolean
|
||||
val isLoadingPlaceholderKeyboard: Boolean
|
||||
var popupManager = PopupManager<KeyboardView, KeyView>(this, florisboard?.popupLayerView)
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private val themeManager: ThemeManager = ThemeManager.default()
|
||||
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
|
||||
@@ -132,7 +129,6 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
* Dismisses all shown key popups when keyboard is detached from window.
|
||||
*/
|
||||
override fun onDetachedFromWindow() {
|
||||
popupManager.dismissAllPopups()
|
||||
if (!isPreviewMode) {
|
||||
themeManager.unregisterOnThemeUpdatedListener(this)
|
||||
}
|
||||
@@ -171,92 +167,93 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
event ?: return false
|
||||
if (isPreviewMode || isLoadingPlaceholderKeyboard) {
|
||||
return false
|
||||
}
|
||||
val eventFloris = MotionEvent.obtainNoHistory(event)
|
||||
if (event == null || isPreviewMode || isLoadingPlaceholderKeyboard) return false
|
||||
|
||||
if (!isSmartbarKeyboardView && swipeGestureDetector.onTouchEvent(event)) {
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_CANCEL)
|
||||
activeKeyView = null
|
||||
activePointerId = null
|
||||
for (pointerIndex in 0 until event.pointerCount) {
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_CANCEL)
|
||||
activeKeyViews.remove(pointerId)
|
||||
}
|
||||
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(KeyData.INTERNAL_BATCH_EDIT))
|
||||
}
|
||||
return true
|
||||
}
|
||||
val pointerIndex = event.actionIndex
|
||||
var pointerId = event.getPointerId(pointerIndex)
|
||||
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN,
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.down(KeyData.INTERNAL_BATCH_EDIT))
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
searchForActiveKeyView(event, pointerIndex, pointerId)
|
||||
initialKeyCodes[pointerId] = activeKeyViews[pointerId]?.data?.code ?: 0
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
|
||||
}
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
if (activePointerId == null) {
|
||||
activePointerId = pointerId
|
||||
activeX = event.getX(pointerIndex)
|
||||
activeY = event.getY(pointerIndex)
|
||||
searchForActiveKeyView()
|
||||
initialKeyCode = activeKeyView?.data?.code ?: 0
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
|
||||
} else if (activePointerId != pointerId) {
|
||||
// New pointer arrived. Send ACTION_UP to current active view and move on
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_UP)
|
||||
activePointerId = pointerId
|
||||
activeX = event.getX(pointerIndex)
|
||||
activeY = event.getY(pointerIndex)
|
||||
searchForActiveKeyView()
|
||||
initialKeyCode = activeKeyView?.data?.code ?: 0
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
|
||||
}
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
searchForActiveKeyView(event, pointerIndex, pointerId)
|
||||
initialKeyCodes[pointerId] = activeKeyViews[pointerId]?.data?.code ?: 0
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
for (index in 0 until event.pointerCount) {
|
||||
pointerId = event.getPointerId(index)
|
||||
if (activePointerId == pointerId) {
|
||||
activeX = event.getX(index)
|
||||
activeY = event.getY(index)
|
||||
if (activeKeyView == null) {
|
||||
searchForActiveKeyView()
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
|
||||
} else {
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_MOVE)
|
||||
}
|
||||
for (pointerIndex in 0 until event.pointerCount) {
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
if (!activeKeyViews.containsKey(pointerId)) {
|
||||
searchForActiveKeyView(event, pointerIndex, pointerId)
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
|
||||
} else {
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_MOVE)
|
||||
}
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL,
|
||||
MotionEvent.ACTION_POINTER_UP -> {
|
||||
if (activePointerId == pointerId) {
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_UP)
|
||||
activeKeyView = null
|
||||
activePointerId = null
|
||||
}
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, event.actionMasked)
|
||||
activeKeyViews.remove(pointerId)
|
||||
}
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
sendFlorisTouchEvent(event, pointerIndex, pointerId, event.actionMasked)
|
||||
activeKeyViews.remove(pointerId)
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(KeyData.INTERNAL_BATCH_EDIT))
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
eventFloris.recycle()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a touch [event] to [activeKeyView] with action set to [actionParam]. Normalizes passed
|
||||
* actions (ACTION_POINTER_* will be converted to ACTION_*). Translates the absolute coords of
|
||||
* a passed [event] to relative ones so the [activeKeyView] can work with it.
|
||||
* Sends a touch [event] to the active key view which is associated with given [pointerId]. The action of the
|
||||
* event is set to [actionParam]. Normalizes passed actions (ACTION_POINTER_* will be converted to ACTION_*).
|
||||
* Translates the absolute coords of a passed [event] to relative ones so the active key view can work with it.
|
||||
*
|
||||
* @param event The event to pass to [activeKeyView].
|
||||
* @param event The event to pass to the active key view.
|
||||
* @param pointerIndex The index of the pointer, used for getting coordinates.
|
||||
* @param pointerId The unique ID of the pointer, used to reference the active key view.
|
||||
* @param actionParam The action to set the [event] to.
|
||||
*/
|
||||
private fun sendFlorisTouchEvent(event: MotionEvent, actionParam: Int) {
|
||||
val keyView = activeKeyView ?: return
|
||||
val keyViewParent = keyView.parent as ViewGroup
|
||||
keyView.onFlorisTouchEvent(event.apply {
|
||||
action = when (actionParam) {
|
||||
private fun sendFlorisTouchEvent(event: MotionEvent, pointerIndex: Int, pointerId: Int, actionParam: Int) {
|
||||
val keyView = activeKeyViews[pointerId] ?: return
|
||||
val keyViewParent = keyView.parent as? ViewGroup ?: return
|
||||
val eventToSend = MotionEvent.obtain(
|
||||
event.downTime,
|
||||
event.eventTime,
|
||||
when (actionParam) {
|
||||
MotionEvent.ACTION_POINTER_DOWN -> MotionEvent.ACTION_DOWN
|
||||
MotionEvent.ACTION_POINTER_UP -> MotionEvent.ACTION_UP
|
||||
else -> actionParam
|
||||
}
|
||||
setLocation(
|
||||
activeX - keyViewParent.x - keyView.x,
|
||||
activeY - keyViewParent.y - keyView.y
|
||||
)
|
||||
})
|
||||
},
|
||||
event.getX(pointerIndex) - keyViewParent.x - keyView.x,
|
||||
event.getY(pointerIndex) - keyViewParent.y - keyView.y,
|
||||
0
|
||||
)
|
||||
keyView.onFlorisTouchEvent(eventToSend)
|
||||
eventToSend.recycle()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,7 +262,7 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
*/
|
||||
override fun onSwipe(event: SwipeGesture.Event): Boolean {
|
||||
return when {
|
||||
initialKeyCode == KeyCode.DELETE -> {
|
||||
initialKeyCodes[event.pointerId] == KeyCode.DELETE -> {
|
||||
if (event.type == SwipeGesture.Type.TOUCH_UP && event.direction == SwipeGesture.Direction.LEFT &&
|
||||
prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_WORD) {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.deleteKeySwipeLeft)
|
||||
@@ -274,7 +271,16 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
false
|
||||
}
|
||||
}
|
||||
initialKeyCode > KeyCode.SPACE && !popupManager.isShowingExtendedPopup -> when {
|
||||
initialKeyCodes[event.pointerId] == KeyCode.SHIFT && activeKeyViews[event.pointerId]?.data?.code != KeyCode.SHIFT &&
|
||||
event.type == SwipeGesture.Type.TOUCH_UP -> {
|
||||
activeKeyViews[event.pointerId]?.let {
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(it.popupManager.getActiveKeyData(it) ?: it.data))
|
||||
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.cancel(KeyData.SHIFT))
|
||||
}
|
||||
true
|
||||
}
|
||||
initialKeyCodes[event.pointerId] ?: 0 > KeyCode.SPACE &&
|
||||
activeKeyViews[event.pointerId]?.popupManager?.isShowingExtendedPopup == false -> when {
|
||||
!prefs.glide.enabled -> when (event.type) {
|
||||
SwipeGesture.Type.TOUCH_UP -> {
|
||||
val swipeAction = when (event.direction) {
|
||||
@@ -300,15 +306,17 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for an active key view at [activeX]/[activeY].
|
||||
* Searches for an active key view at the passed pointer location.
|
||||
*/
|
||||
private fun searchForActiveKeyView() {
|
||||
private fun searchForActiveKeyView(event: MotionEvent, pointerIndex: Int, pointerId: Int) {
|
||||
val activeX = event.getX(pointerIndex)
|
||||
val activeY = event.getY(pointerIndex)
|
||||
loop@ for (row in children) {
|
||||
if (row is FlexboxLayout) {
|
||||
for (keyView in row.children) {
|
||||
if (keyView is KeyView) {
|
||||
if (keyView.touchHitBox.contains(activeX.toInt(), activeY.toInt())) {
|
||||
activeKeyView = keyView
|
||||
activeKeyViews[pointerId] = keyView
|
||||
break@loop
|
||||
}
|
||||
}
|
||||
@@ -318,14 +326,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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -276,7 +276,8 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
|
||||
KeyboardMode.PHONE2 -> null
|
||||
else -> when {
|
||||
florisboard.activeEditorInstance.isComposingEnabled &&
|
||||
shouldSuggestClipboardContents
|
||||
shouldSuggestClipboardContents &&
|
||||
florisboard.activeEditorInstance.selection.isCursorMode
|
||||
-> R.id.clipboard_suggestion_row
|
||||
florisboard.activeEditorInstance.isComposingEnabled &&
|
||||
florisboard.activeEditorInstance.selection.isCursorMode
|
||||
|
||||
@@ -22,7 +22,7 @@ import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import com.github.michaelbull.result.*
|
||||
import android.net.Uri
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetManager
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
@@ -85,6 +85,8 @@ class ThemeManager private constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultOrNull(): ThemeManager? = defaultInstance
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -102,7 +104,7 @@ class ThemeManager private constructor(
|
||||
activeTheme = AdaptiveThemeOverlay(this, if (ref == null) {
|
||||
Theme.BASE_THEME
|
||||
} else {
|
||||
loadTheme(ref).getOr(Theme.BASE_THEME)
|
||||
loadTheme(ref).getOrDefault(Theme.BASE_THEME)
|
||||
})
|
||||
Timber.i(activeTheme.label)
|
||||
notifyCallbackReceivers()
|
||||
@@ -247,23 +249,34 @@ class ThemeManager private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteTheme(ref: AssetRef): Result<Nothing?, Throwable> {
|
||||
fun deleteTheme(ref: AssetRef): Result<Unit> {
|
||||
return assetManager.deleteAsset(ref)
|
||||
}
|
||||
|
||||
fun loadTheme(ref: AssetRef): Result<Theme, Throwable> {
|
||||
assetManager.loadAsset(ref, ThemeJson::class.java).onSuccess { themeJson ->
|
||||
fun loadTheme(ref: AssetRef): Result<Theme> {
|
||||
assetManager.loadAsset(ref, ThemeJson::class).onSuccess { themeJson ->
|
||||
val theme = themeJson.toTheme()
|
||||
return Ok(theme)
|
||||
return Result.success(theme)
|
||||
}.onFailure {
|
||||
Timber.e(it.toString())
|
||||
return Err(it)
|
||||
return Result.failure(it)
|
||||
}
|
||||
return Err(Exception("Unreachable code"))
|
||||
return Result.failure(Exception("Unreachable code"))
|
||||
}
|
||||
|
||||
fun writeTheme(ref: AssetRef, theme: Theme): Result<Boolean, Throwable> {
|
||||
return assetManager.writeAsset(ref, ThemeJson::class.java, ThemeJson.fromTheme(theme))
|
||||
fun loadTheme(uri: Uri): Result<Theme> {
|
||||
assetManager.loadAsset(uri, ThemeJson::class).onSuccess { themeJson ->
|
||||
val theme = themeJson.toTheme()
|
||||
return Result.success(theme)
|
||||
}.onFailure {
|
||||
Timber.e(it.toString())
|
||||
return Result.failure(it)
|
||||
}
|
||||
return Result.failure(Exception("Unreachable code"))
|
||||
}
|
||||
|
||||
fun writeTheme(ref: AssetRef, theme: Theme): Result<Unit> {
|
||||
return assetManager.writeAsset(ref, ThemeJson::class, ThemeJson.fromTheme(theme))
|
||||
}
|
||||
|
||||
private fun evaluateActiveThemeRef(): AssetRef? {
|
||||
@@ -300,7 +313,7 @@ class ThemeManager private constructor(
|
||||
prefs.theme.dayThemeRef
|
||||
}
|
||||
}
|
||||
}).onFailure { Timber.e(it) }.getOr(null)
|
||||
}).onFailure { Timber.e(it) }.getOrDefault(null)
|
||||
}
|
||||
|
||||
private fun indexThemeRefs() {
|
||||
@@ -308,7 +321,7 @@ class ThemeManager private constructor(
|
||||
indexedNightThemeRefs.clear()
|
||||
assetManager.listAssets(
|
||||
AssetRef(AssetSource.Assets, THEME_PATH_REL),
|
||||
ThemeMetaOnly::class.java
|
||||
ThemeMetaOnly::class
|
||||
).onSuccess {
|
||||
for ((ref, themeMetaOnly) in it) {
|
||||
if (themeMetaOnly.isNightTheme) {
|
||||
@@ -322,7 +335,7 @@ class ThemeManager private constructor(
|
||||
}
|
||||
assetManager.listAssets(
|
||||
AssetRef(AssetSource.Internal, THEME_PATH_REL),
|
||||
ThemeMetaOnly::class.java
|
||||
ThemeMetaOnly::class
|
||||
).onSuccess {
|
||||
for ((ref, themeMetaOnly) in it) {
|
||||
if (themeMetaOnly.isNightTheme) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package dev.patrickgold.florisboard.settings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
@@ -32,6 +33,7 @@ class AboutActivity : AppCompatActivity() {
|
||||
private lateinit var binding: AboutActivityBinding
|
||||
private var licensesAlertDialog: AlertDialog? = null
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = AboutActivityBinding.inflate(layoutInflater)
|
||||
@@ -41,7 +43,7 @@ class AboutActivity : AppCompatActivity() {
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
// Set app version string
|
||||
binding.appVersion.text = AppVersionUtils.getRawVersionName(this)
|
||||
binding.appVersion.text = "v" + AppVersionUtils.getRawVersionName(this)
|
||||
|
||||
// Set onClickListeners for buttons
|
||||
binding.privacyPolicyButton.setOnClickListener {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
5
app/src/main/res/drawable/ic_share.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?android:attr/textColorPrimary" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
||||
@@ -20,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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<dev.patrickgold.florisboard.ime.core.InputWindowView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/florisboard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -18,29 +19,32 @@
|
||||
android:background="?inputView_bgColorFallback"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<LinearLayout
|
||||
<dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel
|
||||
android:id="@+id/one_handed_ctrl_panel_start"
|
||||
style="@style/OneHandedPanel"
|
||||
android:visibility="gone">
|
||||
android:layout_width="@dimen/one_handed_width"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="0"
|
||||
android:visibility="gone"
|
||||
app:panelSide="start">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_close_start"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:src="@drawable/ic_zoom_out_map"
|
||||
android:contentDescription="@string/one_handed__close_btn_content_description"/>
|
||||
|
||||
<View
|
||||
android:tag="one_handed_ctrl_close"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/one_handed_button_height"
|
||||
android:visibility="invisible"/>
|
||||
android:layout_marginBottom="@dimen/one_handed_button_height"
|
||||
android:src="@drawable/ic_zoom_out_map"
|
||||
android:background="@drawable/button_transparent_bg_on_press"
|
||||
android:contentDescription="@string/one_handed__close_btn_content_description"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_move_start"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:tag="one_handed_ctrl_move"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/one_handed_button_height"
|
||||
android:src="@drawable/ic_keyboard_arrow_left"
|
||||
android:background="@drawable/button_transparent_bg_on_press"
|
||||
android:contentDescription="@string/one_handed__move_start_btn_content_description"/>
|
||||
|
||||
</LinearLayout>
|
||||
</dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.core.FlorisViewFlipper
|
||||
android:id="@+id/main_view_flipper"
|
||||
@@ -55,29 +59,32 @@
|
||||
|
||||
</dev.patrickgold.florisboard.ime.core.FlorisViewFlipper>
|
||||
|
||||
<LinearLayout
|
||||
<dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel
|
||||
android:id="@+id/one_handed_ctrl_panel_end"
|
||||
style="@style/OneHandedPanel"
|
||||
android:visibility="gone">
|
||||
android:layout_width="@dimen/one_handed_width"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="0"
|
||||
android:visibility="gone"
|
||||
app:panelSide="end">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_close_end"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:src="@drawable/ic_zoom_out_map"
|
||||
android:contentDescription="@string/one_handed__close_btn_content_description"/>
|
||||
|
||||
<View
|
||||
android:tag="one_handed_ctrl_close"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/one_handed_button_height"
|
||||
android:visibility="invisible"/>
|
||||
android:layout_marginBottom="@dimen/one_handed_button_height"
|
||||
android:src="@drawable/ic_zoom_out_map"
|
||||
android:background="@drawable/button_transparent_bg_on_press"
|
||||
android:contentDescription="@string/one_handed__close_btn_content_description"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_move_end"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:tag="one_handed_ctrl_move"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/one_handed_button_height"
|
||||
android:src="@drawable/ic_keyboard_arrow_right"
|
||||
android:contentDescription="@string/one_handed__move_end_btn_content_description"/>
|
||||
android:background="@drawable/button_transparent_bg_on_press"
|
||||
android:contentDescription="@string/one_handed__move_start_btn_content_description"/>
|
||||
|
||||
</LinearLayout>
|
||||
</dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel>
|
||||
|
||||
</dev.patrickgold.florisboard.ime.core.InputView>
|
||||
|
||||
|
||||
@@ -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 |
BIN
app/src/main/res/mipmap-hdpi/ic_app_icon_release_round.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 707 B |
|
Before Width: | Height: | Size: 3.7 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_app_icon_beta.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_app_icon_beta_foreground.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_app_icon_beta_round.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_app_icon_debug.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_app_icon_debug_foreground.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_app_icon_debug_round.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_app_icon_release.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_app_icon_release_foreground.png
Normal file
|
After Width: | Height: | Size: 894 B |
BIN
app/src/main/res/mipmap-mdpi/ic_app_icon_release_round.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_app_icon_beta.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_app_icon_beta_foreground.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_app_icon_beta_round.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_app_icon_debug.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_app_icon_debug_foreground.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_app_icon_debug_round.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_app_icon_release.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_app_icon_release_foreground.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_app_icon_release_round.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_app_icon_beta.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_app_icon_beta_foreground.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_app_icon_beta_round.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_app_icon_debug.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_app_icon_debug_foreground.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_app_icon_debug_round.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_app_icon_release.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |