Compare commits

...

37 Commits

Author SHA1 Message Date
Patrick Goldinger
e581d6cbc4 Release v0.3.9 2021-03-16 20:15:45 +01:00
Patrick Goldinger
ec13d008fb Fix Greek uppercase bug (#452) 2021-03-16 20:03:51 +01:00
Patrick Goldinger
edfea1afcb Merge pull request #461 from florisboard/metadata-refresh
App Store presence metadata update
2021-03-16 16:45:10 +01:00
Patrick Goldinger
25fc23d721 Update store presence metadata to represent all implemented features 2021-03-16 16:34:54 +01:00
Patrick Goldinger
c701141be2 Remove Italian store metadata 2021-03-16 15:15:04 +01:00
Patrick Goldinger
e5b956857e Merge pull request #459 from florisboard/beta-track-prep
Beta track preperation / App icon revamp
2021-03-16 11:53:12 +01:00
Patrick Goldinger
67236ef58d Add beta build variant 2021-03-16 03:17:34 +01:00
Patrick Goldinger
2da17a0654 Add new app icons for all build variants 2021-03-16 03:16:56 +01:00
Patrick Goldinger
1f3221a886 Merge pull request #457 from florisboard/one-handed-improvements
Add one-handed width option / Improve one-handed code
2021-03-15 20:13:02 +01:00
Patrick Goldinger
47f80d00c4 Add one-handed width option / Improve one-handed code 2021-03-15 17:49:18 +01:00
Patrick Goldinger
d648c480b5 Merge pull request #455 from florisboard/theme-import-export
Add theme import/export / Fix theme editor jumping to top
2021-03-15 09:33:18 +01:00
Patrick Goldinger
9e26720674 Fix export UI not requesting to create document 2021-03-15 01:51:11 +01:00
Patrick Goldinger
a20c6bf148 Fix theme editor jumping to top (#379) 2021-03-15 00:57:35 +01:00
Patrick Goldinger
d2df5cfcdf Switch to Kotlin Result 2021-03-15 00:08:10 +01:00
Patrick Goldinger
93b5503dfc Fix file write bug and improve UI 2021-03-14 23:39:57 +01:00
Patrick Goldinger
4d4b54074a Improve import/export feature stability 2021-03-14 19:44:13 +01:00
Patrick Goldinger
904fd9b85a Add simple theme import/export functionality 2021-03-14 02:15:47 +01:00
Patrick Goldinger
e4f5fcf74b Merge pull request #451 from florisboard/alternate-shift-code
Add option for an alternate key code when caps state is active
2021-03-10 23:15:19 +01:00
Patrick Goldinger
15f0316839 Add shift variants for Colemak and Dvorak (#145) 2021-03-10 19:38:11 +01:00
Patrick Goldinger
93654c4f88 Add alternate key code option for FlorisKeyData (#145) 2021-03-10 19:37:50 +01:00
Patrick Goldinger
62fc549ea9 Fix crash on setup when no other IME is installed (#423) 2021-03-10 18:37:27 +01:00
Patrick Goldinger
d0dbd1cd4e Merge pull request #444 from florisboard/input-logic-rework
Input logic rework
2021-03-10 16:08:10 +01:00
Patrick Goldinger
af28f84b69 Fix delete precise char selection init value always 2 units (#448) 2021-03-10 12:09:18 +01:00
Patrick Goldinger
db7ee52029 Fix label text size decreasing bug in selection keyboard 2021-03-10 11:59:51 +01:00
Patrick Goldinger
7343617792 Fix space bar arrow movement initial count always 2 (#448) 2021-03-10 11:31:23 +01:00
Patrick Goldinger
5898d7006b Add internal batch edit level to prevent stuttering UI 2021-03-09 20:17:30 +01:00
Patrick Goldinger
058be7a169 Fix editor instance commit text logic 2021-03-09 02:03:17 +01:00
Patrick Goldinger
e6f2a25021 Improve input event logic / Fix extended popup bug 2021-03-08 19:51:37 +01:00
Patrick Goldinger
3a485a1574 Fix bugs and improve code 2021-03-08 01:09:43 +01:00
Patrick Goldinger
0ee0f24119 Add shift slide behavior / Improve performance of input logic 2021-03-07 19:35:08 +01:00
Patrick Goldinger
004e999259 Document InputEventDispatcher 2021-03-07 16:26:45 +01:00
Patrick Goldinger
11775c4619 Separate input event dispatcher logic into another file 2021-03-07 15:27:20 +01:00
Patrick Goldinger
177bad95b3 Clean up static KeyData object definitions 2021-03-06 19:33:53 +01:00
Patrick Goldinger
610526d845 Add multi-pointer support for gestures 2021-03-06 14:19:30 +01:00
Patrick Goldinger
55e489bc07 Complete overhaul of core input logic 2021-03-05 20:13:35 +01:00
Patrick Goldinger
589063be61 Rework cursor/selection implementation 2021-03-03 23:43:27 +01:00
Patrick Goldinger
aa73ac706a Update target SDK to API 30 (Android 11) 2021-03-01 20:13:59 +01:00
143 changed files with 2217 additions and 1058 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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")

View File

@@ -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"/>

View File

@@ -18,7 +18,7 @@
"relevant": [
{ "code": 58, "label": ":" }
]
} }
}, "shift": { "code": 58, "label": ":" } }
],
[
{ "code": 97, "label": "a" },

View File

@@ -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" },

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
)
}
}

View File

@@ -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

View File

@@ -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..."))
}
}

View File

@@ -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",

View File

@@ -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())
}
}

View File

@@ -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}'"))
}
}

View File

@@ -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]))
}
}

View File

@@ -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."))
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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"
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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). */

View File

@@ -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
}

View File

@@ -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 {
/**

View File

@@ -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")) {

View File

@@ -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
}
/**

View File

@@ -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()

View File

@@ -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

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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
}
}
}
}
}

View File

@@ -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")
}
}
}

View File

@@ -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

View File

@@ -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
}
}
}
}

View File

@@ -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]

View 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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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_release_background"/>
<foreground android:drawable="@mipmap/ic_app_icon_release_foreground"/>
</adaptive-icon>

View 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_release_background"/>
<foreground android:drawable="@mipmap/ic_app_icon_release_foreground"/>
</adaptive-icon>

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Some files were not shown because too many files have changed in this diff Show More