Compare commits

..

9 Commits

Author SHA1 Message Date
Patrick Goldinger
edc63aa680 Release v0.2.3 2020-11-11 23:08:55 +01:00
Patrick Goldinger
23def145b2 Finish reworking core (#35 #33) 2020-11-11 22:59:27 +01:00
Patrick Goldinger
3f7bd4f65d Fix delete key not working for emojis / Fix several other bugs 2020-11-10 23:44:07 +01:00
Patrick Goldinger
7b91d4f9d3 Add EditorInstance object to better manage state of input
- EditorInstance is an improved EditorInfo object which also holds the
  current state of the input like text, selection, ...
- Should help in cleaning up TextInputManager and resolve issues around
  non-updating caps states, etc.
2020-11-08 22:34:05 +01:00
Patrick Goldinger
175369f7d7 Improve onStartInputView behaviour 2020-11-05 19:41:09 +01:00
Patrick Goldinger
79c5acc007 Improve debugging inspection output
- Needed for inspection why FlorisBoard behaves strangely in some apps
2020-11-04 21:24:06 +01:00
Patrick Goldinger
94d470dd96 Fix font sizing bug in KeyView
- Calculation may require 2 iterations until the correct size is found
  because both width and height can be <=0 or >=0
2020-11-03 18:56:11 +01:00
Patrick Goldinger
ee9d61ad1e Add auto font sizing for text input keys (#32)
- Font of keys is now adjusted accordingly to the keyboard height
  preference.
- Affects hinted symbols / numbers too.
2020-11-01 22:22:14 +01:00
Patrick Goldinger
a3c7b538d0 Add option to remember caps lock state (#30)
- Located in Settings > Typing > Remember caps lock state
- Defaults to false (do not remember state)
2020-10-30 16:49:47 +01:00
14 changed files with 1273 additions and 436 deletions

View File

@@ -10,8 +10,8 @@ android {
applicationId "dev.patrickgold.florisboard"
minSdkVersion 23
targetSdkVersion 29
versionCode 14
versionName "0.2.2"
versionCode 15
versionName "0.2.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -0,0 +1,739 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.core
import android.inputmethodservice.InputMethodService
import android.os.Build
import android.text.InputType
import android.view.KeyEvent
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.ExtractedTextRequest
import androidx.annotation.RequiresApi
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import java.lang.StringBuilder
// Constants for detectLastUnicodeCharacterLengthBeforeCursor method
private const val LIGHT_SKIN_TONE = 0x1F3FB
private const val MEDIUM_LIGHT_SKIN_TONE = 0x1F3FC
private const val MEDIUM_SKIN_TONE = 0x1F3FD
private const val MEDIUM_DARK_SKIN_TONE = 0x1F3FE
private const val DARK_SKIN_TONE = 0x1F3FF
private const val RED_HAIR = 0x1F9B0
private const val CURLY_HAIR = 0x1F9B1
private const val WHITE_HAIR = 0x1F9B2
private const val BALD = 0x1F9B3
private const val ZERO_WIDTH_JOINER = 0x200D
private const val VARIATION_SELECTOR = 0xFE0F
// Array which holds all variations for easier checking (convenience only)
private val emojiVariationArray: Array<Int> = arrayOf(
LIGHT_SKIN_TONE,
MEDIUM_LIGHT_SKIN_TONE,
MEDIUM_SKIN_TONE,
MEDIUM_DARK_SKIN_TONE,
DARK_SKIN_TONE,
RED_HAIR,
CURLY_HAIR,
WHITE_HAIR,
BALD
)
/**
* Class which holds information relevant to an editor instance like the input [cachedText], [selection],
* [inputAttributes], [imeOptions], etc. This class is thought to be an improved [EditorInfo]
* object which also holds the state of the currently focused input editor.
*/
class EditorInstance private constructor(private val ims: InputMethodService?) {
val cursorCapsMode: InputAttributes.CapsMode
get() {
val ic = ims?.currentInputConnection ?: return InputAttributes.CapsMode.NONE
return InputAttributes.CapsMode.fromFlags(
ic.getCursorCapsMode(inputAttributes.capsMode.toFlags())
)
}
var currentWord: Region = Region(this)
private set
var imeOptions: ImeOptions = ImeOptions.fromImeOptionsInt(EditorInfo.IME_NULL)
private set
var inputAttributes: InputAttributes = InputAttributes.fromInputTypeInt(InputType.TYPE_NULL)
private set
var isComposingEnabled: Boolean = false
set(v) {
field = v
reevaluate()
if (v) {
markComposingRegion(currentWord)
} else {
markComposingRegion(null)
}
}
var isNewSelectionInBoundsOfOld: Boolean = false
private set
var packageName: String = "undefined"
private set
var selection: Selection = Selection(this)
private set
val cachedText: String
get() = cachedTextInternal.toString()
private var cachedTextInternal: StringBuilder = StringBuilder("")
companion object {
fun default(): EditorInstance {
return EditorInstance(null)
}
fun from(editorInfo: EditorInfo?, ims: InputMethodService?): EditorInstance {
return if (editorInfo == null) { default() } else {
EditorInstance(ims).apply {
imeOptions = ImeOptions.fromImeOptionsInt(editorInfo.imeOptions)
inputAttributes = InputAttributes.fromInputTypeInt(editorInfo.inputType)
packageName = editorInfo.packageName
/*selection = Selection(this).apply {
start = editorInfo.initialSelStart
end = editorInfo.initialSelEnd
}*/
}
}
}
}
init {
fetchExtractedTextFromInputConnection()
reevaluate()
}
/**
* Event handler which reacts to selection updates coming from the target app's editor.
*/
fun onUpdateSelection(
oldSelStart: Int, oldSelEnd: Int,
newSelStart: Int, newSelEnd: Int
) {
fetchExtractedTextFromInputConnection()
isNewSelectionInBoundsOfOld =
newSelStart >= (oldSelStart - 1) &&
newSelStart <= (oldSelStart + 1) &&
newSelEnd >= (oldSelEnd - 1) &&
newSelEnd <= (oldSelEnd + 1)
selection.apply {
start = newSelStart
end = newSelEnd
}
reevaluate()
if (selection.isCursorMode && isComposingEnabled) {
markComposingRegion(currentWord)
} else {
markComposingRegion(null)
}
}
/**
* Completes the given [text] in the current composing region. Does nothing if the current
* composing region is of zero length or null.
*
* @param text The text to complete in this editor's composing region.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun commitCompletion(text: String): Boolean {
return false
}
/**
* Commits the given [text] to this editor instance and adjusts both the cursor position and
* composing region, if any.
*
* This method overwrites any selected text and replaces it with given [text]. If there is no
* text selected (selection is in cursor mode), then this method will insert the [text] after
* the cursor, then set the cursor position to the first character after the inserted text.
*
* @param text The text to commit.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun commitText(text: String): Boolean {
val ic = ims?.currentInputConnection ?: return false
ic.beginBatchEdit()
markComposingRegion(null)
if (selection.isCursorMode) {
cachedTextInternal.insert(selection.start, text)
} else if (selection.isSelectionMode) {
cachedTextInternal.replace(selection.start, selection.end, text)
}
selection.apply {
start += text.length
end = start
}
reevaluate()
ic.commitText(text, 1)
if (isComposingEnabled) {
markComposingRegion(currentWord)
}
ic.setSelection(selection.start, selection.end)
ic.endBatchEdit()
return true
}
/**
* Executes a backward delete on this editor's text. If a text selection is active, all
* characters inside this selection will be removed, else only the left-most character from
* the cursor's position.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun deleteBackwards(): Boolean {
val ic = ims?.currentInputConnection ?: return false
ic.beginBatchEdit()
markComposingRegion(null)
if (selection.isCursorMode && selection.start > 0) {
val length = detectLastUnicodeCharacterLengthBeforeCursor()
cachedTextInternal.replace(selection.start - length, selection.start, "")
selection.apply {
start -= length
end = start
}
ic.deleteSurroundingText(length, 0)
} else if (selection.isSelectionMode) {
cachedTextInternal.replace(selection.start, selection.end, "")
selection.apply {
end = start
}
ic.commitText("", 1)
}
reevaluate()
if (isComposingEnabled) {
markComposingRegion(currentWord)
}
ic.setSelection(selection.start, selection.end)
ic.endBatchEdit()
return true
}
/**
* Deletes [n] words before the current cursor's position.
* NOTE: this implementation does currently only delete currentWord. This is due to change in
* future versions.
*
* @param n The number of words to delete before the cursor. Must be greater than 0 or this
* method will fail.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun deleteWordsBeforeCursor(n: Int): Boolean {
val ic = ims?.currentInputConnection ?: return false
if (n < 1) {
return false
}
ic.beginBatchEdit()
markComposingRegion(null)
if (currentWord.isValid) {
cachedTextInternal.replace(currentWord.start, currentWord.end, "")
selection.apply {
start = currentWord.start
end = start
}
ic.setSelection(currentWord.start, currentWord.end)
ic.commitText("", 1)
}
reevaluate()
ic.setSelection(selection.start, selection.end)
ic.endBatchEdit()
return true
}
/**
* Gets [n] characters after the cursor's current position. The resulting string may be any
* length ranging from 0 to n.
*
* @param n The number of characters to get after the cursor. Must be greater than 0 or this
* method will fail.
*
* @returns [n] or less characters after the cursor.
*/
fun getTextAfterCursor(n: Int): String {
if (!selection.isValid || n < 1) {
return ""
}
val from = selection.end
val to = (selection.end + n).coerceAtMost(cachedTextInternal.length)
return cachedTextInternal.substring(from, to)
}
/**
* Gets [n] characters before the cursor's current position. The resulting string may be any
* length ranging from 0 to n.
*
* @param n The number of characters to get before the cursor. Must be greater than 0 or this
* method will fail.
*
* @returns [n] or less characters after the cursor.
*/
fun getTextBeforeCursor(n: Int): String {
if (!selection.isValid || n < 1) {
return ""
}
val from = (selection.start - n).coerceAtLeast(0)
val to = selection.start
return cachedTextInternal.substring(from, to)
}
/**
* Performs a given [action] on the current input editor.
*
* @param action The action to be performed on this editor instance.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun performEnterAction(action: ImeOptions.Action): Boolean {
val ic = ims?.currentInputConnection ?: return false
return ic.performEditorAction(action.toInt())
}
/**
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN].
*
* @param ic The input connection on which this operation should be performed.
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun sendSystemKeyEvent(keyCode: Int): Boolean {
val ic = ims?.currentInputConnection ?: return false
return ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
}
/**
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN] with ALT pressed.
*
* @param ic The input connection on which this operation should be performed.
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun sendSystemKeyEventAlt(keyCode: Int): Boolean {
val ic = ims?.currentInputConnection ?: return false
return ic.sendKeyEvent(
KeyEvent(
0,
1,
KeyEvent.ACTION_DOWN, keyCode,
0,
KeyEvent.META_ALT_LEFT_ON
)
)
}
/**
* Sets the selection region of this instance and notifies the input connection.
*
* @param from The start index of the selection in characters (inclusive).
* @param to The end index of the selection in characters (exclusive).
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun setSelection(from: Int, to: Int): Boolean {
val ic = ims?.currentInputConnection ?: return false
selection.apply {
start = from
end = to
}
return ic.setSelection(from, to)
}
/**
* Detects the length of the character before the cursor, as many Unicode characters nowadays
* are longer than 1 Java char and thus the length has to be calculated in order to avoid
* deleting only half of an emoji...
* Is used primarily in [deleteBackwards].
*
* @returns The length of the last Unicode character, in Java characters or 0 if the current
* selection is invalid.
*/
private fun detectLastUnicodeCharacterLengthBeforeCursor(): Int {
if (!selection.isValid) {
return 0
}
var charIndex = 0
var charLength = 0
var charShouldGlue = false
val textToSearch = cachedTextInternal.substring(0, selection.start.coerceAtMost(cachedTextInternal.length))
var i = 0
while (i < textToSearch.length) {
val cp = textToSearch.codePointAt(i)
val cpLength = Character.charCount(cp)
when {
charShouldGlue || cp == VARIATION_SELECTOR || emojiVariationArray.contains(cp) -> {
charLength += cpLength
charShouldGlue = false
}
cp == ZERO_WIDTH_JOINER -> {
charLength += cpLength
charShouldGlue = true
}
else -> {
charIndex = i
charLength = 0
charShouldGlue = false
}
}
i += cpLength
}
return textToSearch.length - charIndex
}
/**
* Gets the current text from the app's editor view.
*
* @returns The target editor's content string.
*/
private fun fetchExtractedTextFromInputConnection() {
val ic = ims?.currentInputConnection ?: return
val et = ic.getExtractedText(
ExtractedTextRequest(), 0
) ?: return
val text = et.text ?: ""
cachedTextInternal.setLength(0)
cachedTextInternal.append(text)
selection.apply {
start = et.selectionStart.coerceAtMost(cachedTextInternal.length)
end = et.selectionEnd.coerceAtMost(cachedTextInternal.length)
}
}
/**
* Marks a given [region] as composing and notifies the input connection.
*
* @param region The region which should be marked as composing.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
private fun markComposingRegion(region: Region?): Boolean {
val ic = ims?.currentInputConnection ?: return false
return when (region) {
null -> ic.finishComposingText()
else -> if (region.isValid) {
ic.setComposingRegion(region.start, region.end)
} else {
ic.finishComposingText()
}
}
}
/**
* Evaluates the current word in this editor instance based on the current cursor position and
* given delimiter [regex].
*
* @param regex The delimiter regex which should be used to split up the content text and find
* words. May differ from locale to locale.
*
* @returns True on success, false if no current word could be found.
*/
private fun reevaluateCurrentWord(regex: Regex): Boolean {
var foundValidWord = false
if (selection.isValid && selection.isCursorMode) {
val words = cachedText.split("((?<=$regex)|(?=$regex))".toRegex())
var pos = 0
for (word in words) {
if (selection.start >= pos && selection.start <= pos + word.length &&
word.isNotEmpty() && !word.matches(regex)) {
currentWord.apply {
start = pos
end = pos + word.length
}
foundValidWord = true
break
} else {
pos += word.length
}
}
}
if (!foundValidWord) {
currentWord.apply {
start = -1
end = -1
}
}
return foundValidWord
}
/**
* Triggers all reevaluation processes.
*/
private fun reevaluate() {
val regex = "[^\\p{L}]".toRegex()
reevaluateCurrentWord(regex)
}
}
/**
* Class which holds the same information as an [EditorInfo.imeOptions] int but more accessible and
* readable.
*/
class ImeOptions private constructor(imeOptions: Int) {
val action: Action = Action.fromInt(imeOptions)
val flagForceAscii: Boolean = imeOptions and EditorInfo.IME_FLAG_FORCE_ASCII > 0
val flagNavigateNext: Boolean = imeOptions and EditorInfo.IME_FLAG_NAVIGATE_NEXT > 0
val flagNavigatePrevious: Boolean = imeOptions and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS > 0
val flagNoAccessoryAction: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION > 0
val flagNoEnterAction: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0
val flagNoExtractUi: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_EXTRACT_UI > 0
val flagNoFullscreen: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_FULLSCREEN > 0
@RequiresApi(Build.VERSION_CODES.O)
val flagNoPersonalizedLearning: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING > 0
companion object {
fun fromImeOptionsInt(imeOptions: Int): ImeOptions {
return ImeOptions(imeOptions)
}
}
enum class Action {
DONE,
GO,
NEXT,
NONE,
PREVIOUS,
SEARCH,
SEND,
UNSPECIFIED;
companion object {
fun fromInt(raw: Int): Action {
return when (raw and EditorInfo.IME_MASK_ACTION) {
EditorInfo.IME_ACTION_DONE -> DONE
EditorInfo.IME_ACTION_GO -> GO
EditorInfo.IME_ACTION_NEXT -> NEXT
EditorInfo.IME_ACTION_NONE -> NONE
EditorInfo.IME_ACTION_PREVIOUS -> PREVIOUS
EditorInfo.IME_ACTION_SEARCH -> SEARCH
EditorInfo.IME_ACTION_SEND -> SEND
EditorInfo.IME_ACTION_UNSPECIFIED -> UNSPECIFIED
else -> NONE
}
}
}
fun toInt(): Int {
return when (this) {
DONE -> EditorInfo.IME_ACTION_DONE
GO -> EditorInfo.IME_ACTION_GO
NEXT -> EditorInfo.IME_ACTION_NEXT
NONE -> EditorInfo.IME_ACTION_NONE
PREVIOUS -> EditorInfo.IME_ACTION_PREVIOUS
SEARCH -> EditorInfo.IME_ACTION_SEARCH
SEND -> EditorInfo.IME_ACTION_SEND
UNSPECIFIED-> EditorInfo.IME_ACTION_UNSPECIFIED
}
}
}
}
/**
* Class which holds the same information as an [EditorInfo.inputType] int but more accessible and
* readable.
*/
class InputAttributes private constructor(inputType: Int) {
val type: Type
val variation: Variation
val capsMode: CapsMode
var flagNumberDecimal: Boolean = false
private set
var flagNumberSigned: Boolean = false
private set
var flagTextAutoComplete: Boolean = false
private set
var flagTextAutoCorrect: Boolean = false
private set
var flagTextImeMultiLine: Boolean = false
private set
var flagTextMultiLine: Boolean = false
private set
var flagTextNoSuggestions: Boolean = false
private set
init {
when (inputType and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_CLASS_DATETIME -> {
type = Type.DATETIME
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_DATETIME_VARIATION_DATE -> Variation.DATE
InputType.TYPE_DATETIME_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_DATETIME_VARIATION_TIME -> Variation.TIME
else -> Variation.NORMAL
}
capsMode = CapsMode.NONE
}
InputType.TYPE_CLASS_NUMBER -> {
type = Type.NUMBER
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_NUMBER_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> Variation.PASSWORD
else -> Variation.NORMAL
}
capsMode = CapsMode.NONE
flagNumberDecimal = inputType and InputType.TYPE_NUMBER_FLAG_DECIMAL > 0
flagNumberSigned = inputType and InputType.TYPE_NUMBER_FLAG_SIGNED > 0
}
InputType.TYPE_CLASS_PHONE -> {
type = Type.PHONE
variation = Variation.NORMAL
capsMode = CapsMode.NONE
}
InputType.TYPE_CLASS_TEXT -> {
type = Type.TEXT
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> Variation.EMAIL_ADDRESS
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> Variation.EMAIL_SUBJECT
InputType.TYPE_TEXT_VARIATION_FILTER -> Variation.FILTER
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> Variation.LONG_MESSAGE
InputType.TYPE_TEXT_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_TEXT_VARIATION_PASSWORD -> Variation.PASSWORD
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> Variation.PERSON_NAME
InputType.TYPE_TEXT_VARIATION_PHONETIC -> Variation.PHONETIC
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> Variation.POSTAL_ADDRESS
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> Variation.SHORT_MESSAGE
InputType.TYPE_TEXT_VARIATION_URI -> Variation.URI
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> Variation.VISIBLE_PASSWORD
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> Variation.WEB_EDIT_TEXT
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> Variation.WEB_EMAIL_ADDRESS
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> Variation.WEB_PASSWORD
else -> Variation.NORMAL
}
capsMode = CapsMode.fromFlags(inputType)
flagTextAutoComplete = inputType and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE > 0
flagTextAutoCorrect = inputType and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT > 0
flagTextImeMultiLine = inputType and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE > 0
flagTextMultiLine = inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE > 0
flagTextNoSuggestions = inputType and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS > 0
}
else -> {
type = Type.TEXT
variation = Variation.NORMAL
capsMode = CapsMode.NONE
}
}
}
companion object {
fun fromInputTypeInt(inputType: Int): InputAttributes {
return InputAttributes(inputType)
}
}
enum class Type {
DATETIME,
NUMBER,
PHONE,
TEXT;
}
enum class Variation {
DATE,
EMAIL_ADDRESS,
EMAIL_SUBJECT,
FILTER,
LONG_MESSAGE,
NORMAL,
PASSWORD,
PERSON_NAME,
PHONETIC,
POSTAL_ADDRESS,
SHORT_MESSAGE,
TIME,
URI,
VISIBLE_PASSWORD,
WEB_EDIT_TEXT,
WEB_EMAIL_ADDRESS,
WEB_PASSWORD;
}
enum class CapsMode {
ALL,
NONE,
SENTENCES,
WORDS;
companion object {
fun fromFlags(flags: Int): CapsMode {
return when {
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0 -> ALL
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0 -> SENTENCES
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0 -> WORDS
else -> NONE
}
}
}
fun toFlags(): Int {
return when (this) {
ALL -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
SENTENCES -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
WORDS -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
else -> 0
}
}
}
}
/**
* Class which marks a region of the [text] in [editorInstance].
*/
open class Region(private val editorInstance: EditorInstance) {
var start: Int = -1
var end: Int = -1
val isValid: Boolean
get() = start >= 0 && end >= 0 && length >= 0
val length: Int
get() = end - start
val text: String
get() {
val eiText = editorInstance.cachedText
return if (!isValid || start >= eiText.length) {
""
} else {
val end = if (end >= eiText.length) { eiText.length } else { end }
editorInstance.cachedText.substring(start, end)
}
}
override operator fun equals(other: Any?): Boolean {
return if (other is Region) {
start == other.start && end == other.end
} else {
super.equals(other)
}
}
override fun hashCode(): Int {
var result = start
result = 31 * result + end
return result
}
}
/**
* Class which holds selection attributes and returns the correct text for set selection based on
* the text in [editorInstance].
*/
class Selection(private val editorInstance: EditorInstance) : Region(editorInstance) {
val isCursorMode: Boolean
get() = length == 0 && isValid
val isSelectionMode: Boolean
get() = length != 0 && isValid
}

View File

@@ -30,7 +30,6 @@ import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.CursorAnchorInfo
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.widget.ImageButton
@@ -71,6 +70,8 @@ class FlorisBoard : InputMethodService() {
private var vibrator: Vibrator? = null
private val osHandler = Handler()
var activeEditorInstance: EditorInstance = EditorInstance.default()
lateinit var subtypeManager: SubtypeManager
lateinit var activeSubtype: Subtype
private var currentThemeIsNight: Boolean = false
@@ -88,6 +89,7 @@ class FlorisBoard : InputMethodService() {
companion object {
private const val IME_ID: String = "dev.patrickgold.florisboard/.ime.core.FlorisBoard"
private val TAG: String? = FlorisBoard::class.simpleName
fun checkIfImeIsEnabled(context: Context): Boolean {
val activeImeIds = Settings.Secure.getString(
@@ -144,7 +146,7 @@ class FlorisBoard : InputMethodService() {
.build()
)
}
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
if (BuildConfig.DEBUG) Log.i(TAG, "onCreate()")
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
@@ -168,7 +170,7 @@ class FlorisBoard : InputMethodService() {
@SuppressLint("InflateParams")
override fun onCreateInputView(): View? {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreateInputView()")
if (BuildConfig.DEBUG) Log.i(TAG, "onCreateInputView()")
baseContext.setTheme(currentThemeResId)
@@ -180,7 +182,7 @@ class FlorisBoard : InputMethodService() {
}
fun registerInputView(inputView: InputView) {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "registerInputView(inputView)")
if (BuildConfig.DEBUG) Log.i(TAG, "registerInputView($inputView)")
this.inputView = inputView
initializeOneHandedEnvironment()
@@ -192,7 +194,7 @@ class FlorisBoard : InputMethodService() {
}
override fun onDestroy() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
if (BuildConfig.DEBUG) Log.i(TAG, "onDestroy()")
osHandler.removeCallbacksAndMessages(null)
florisboardInstance = null
@@ -202,22 +204,44 @@ class FlorisBoard : InputMethodService() {
super.onDestroy()
}
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
if (BuildConfig.DEBUG) Log.i(TAG, "onStartInput($attribute, $restarting)")
super.onStartInput(attribute, restarting)
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE)
}
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
if (BuildConfig.DEBUG) Log.i(TAG, "onStartInputView($info, $restarting)")
Log.i(TAG, "onStartInputView: " + info?.debugSummarize())
super.onStartInputView(info, restarting)
eventListeners.toList().forEach { it.onStartInputView(info, restarting) }
activeEditorInstance = EditorInstance.from(info, this)
eventListeners.toList().forEach {
it.onStartInputView(activeEditorInstance, restarting)
}
}
override fun onFinishInputView(finishingInput: Boolean) {
currentInputConnection?.requestCursorUpdates(0)
if (BuildConfig.DEBUG) Log.i(TAG, "onFinishInputView($finishingInput)")
if (finishingInput) {
activeEditorInstance = EditorInstance.default()
}
super.onFinishInputView(finishingInput)
eventListeners.toList().forEach { it.onFinishInputView(finishingInput) }
}
override fun onFinishInput() {
if (BuildConfig.DEBUG) Log.i(TAG, "onFinishInput()")
super.onFinishInput()
currentInputConnection?.requestCursorUpdates(0)
}
override fun onWindowShown() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onWindowShown()")
if (BuildConfig.DEBUG) Log.i(TAG, "onWindowShown()")
prefs.sync()
updateTheme()
@@ -231,13 +255,14 @@ class FlorisBoard : InputMethodService() {
}
override fun onWindowHidden() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onWindowHidden()")
if (BuildConfig.DEBUG) Log.i(TAG, "onWindowHidden()")
super.onWindowHidden()
eventListeners.toList().forEach { it.onWindowHidden() }
}
override fun onConfigurationChanged(newConfig: Configuration) {
if (BuildConfig.DEBUG) Log.i(TAG, "onConfigurationChanged($newConfig)")
if (isInputViewShown) {
updateOneHandedPanelVisibility()
}
@@ -245,37 +270,23 @@ class FlorisBoard : InputMethodService() {
super.onConfigurationChanged(newConfig)
}
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
super.onUpdateCursorAnchorInfo(cursorAnchorInfo)
eventListeners.toList().forEach { it.onUpdateCursorAnchorInfo(cursorAnchorInfo) }
}
override fun onUpdateSelection(
oldSelStart: Int,
oldSelEnd: Int,
newSelStart: Int,
newSelEnd: Int,
candidatesStart: Int,
candidatesEnd: Int
oldSelStart: Int, oldSelEnd: Int,
newSelStart: Int, newSelEnd: Int,
candidatesStart: Int, candidatesEnd: Int
) {
if (BuildConfig.DEBUG) Log.i(TAG, "onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
super.onUpdateSelection(
oldSelStart,
oldSelEnd,
newSelStart,
newSelEnd,
candidatesStart,
candidatesEnd
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
eventListeners.toList().forEach {
it.onUpdateSelection(
oldSelStart,
oldSelEnd,
newSelStart,
newSelEnd,
candidatesStart,
candidatesEnd
)
}
activeEditorInstance.onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd
)
eventListeners.toList().forEach { it.onUpdateSelection() }
}
/**
@@ -550,21 +561,13 @@ class FlorisBoard : InputMethodService() {
fun onRegisterInputView(inputView: InputView) {}
fun onDestroy() {}
fun onStartInputView(info: EditorInfo?, restarting: Boolean) {}
fun onStartInputView(instance: EditorInstance, restarting: Boolean) {}
fun onFinishInputView(finishingInput: Boolean) {}
fun onWindowShown() {}
fun onWindowHidden() {}
fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {}
fun onUpdateSelection(
oldSelStart: Int,
oldSelEnd: Int,
newSelStart: Int,
newSelEnd: Int,
candidatesStart: Int,
candidatesEnd: Int
) {}
fun onUpdateSelection() {}
fun onApplyThemeAttributes() {}
fun onSubtypeChanged(newSubtype: Subtype) {}

View File

@@ -180,17 +180,20 @@ class PrefHelper(
*/
class Correction(private val prefHelper: PrefHelper) {
companion object {
const val AUTO_CAPITALIZATION = "correction__auto_capitalization"
const val DOUBLE_SPACE_PERIOD = "correction__double_space_period"
const val AUTO_CAPITALIZATION = "correction__auto_capitalization"
const val DOUBLE_SPACE_PERIOD = "correction__double_space_period"
const val REMEMBER_CAPS_LOCK_STATE = "correction__remember_caps_lock_state"
}
var autoCapitalization: Boolean = false
get() = prefHelper.getPref(AUTO_CAPITALIZATION, true)
private set
var doubleSpacePeriod: Boolean = false
get() = prefHelper.getPref(DOUBLE_SPACE_PERIOD, true)
private set
var autoCapitalization: Boolean
get() = prefHelper.getPref(AUTO_CAPITALIZATION, true)
set(v) = prefHelper.setPref(AUTO_CAPITALIZATION, v)
var doubleSpacePeriod: Boolean
get() = prefHelper.getPref(DOUBLE_SPACE_PERIOD, true)
set(v) = prefHelper.setPref(DOUBLE_SPACE_PERIOD, v)
var rememberCapsLockState: Boolean
get() = prefHelper.getPref(REMEMBER_CAPS_LOCK_STATE, false)
set(v) = prefHelper.setPref(REMEMBER_CAPS_LOCK_STATE, v)
}
/**

View File

@@ -24,6 +24,7 @@ import android.widget.*
import com.google.android.material.tabs.TabLayout
import dev.patrickgold.florisboard.BuildConfig
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.InputView
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
@@ -50,6 +51,8 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
FlorisBoard.EventListener {
private val florisboard = FlorisBoard.getInstance()
private val activeEditorInstance: EditorInstance
get() = florisboard.activeEditorInstance
private var activeTab: Tab? = null
private var mediaViewFlipper: ViewFlipper? = null
@@ -199,18 +202,14 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
* Sends a given [emojiKeyData] to the current input editor.
*/
fun sendEmojiKeyPress(emojiKeyData: EmojiKeyData) {
val ic = florisboard.currentInputConnection
ic?.finishComposingText()
ic?.commitText(emojiKeyData.getCodePointsAsString(), 1)
activeEditorInstance.commitText(emojiKeyData.getCodePointsAsString())
}
/**
* Sends a given [emoticonKeyData] to the current input editor.
*/
fun sendEmoticonKeyPress(emoticonKeyData: EmoticonKeyData) {
val ic = florisboard.currentInputConnection
ic?.finishComposingText()
ic?.commitText(emoticonKeyData.icon, 1)
activeEditorInstance.commitText(emoticonKeyData.icon)
}
/**

View File

@@ -19,7 +19,6 @@ package dev.patrickgold.florisboard.ime.text
import android.content.ClipData
import android.content.Context
import android.os.Handler
import android.text.InputType
import android.util.Log
import android.view.KeyEvent
import android.view.inputmethod.*
@@ -27,9 +26,7 @@ import android.widget.LinearLayout
import android.widget.ViewFlipper
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputView
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.core.*
import dev.patrickgold.florisboard.ime.text.editing.EditingKeyboardView
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.KeyCode
@@ -59,6 +56,8 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
FlorisBoard.EventListener {
private val florisboard = FlorisBoard.getInstance()
private val activeEditorInstance: EditorInstance
get() = florisboard.activeEditorInstance
private var activeKeyboardMode: KeyboardMode? = null
private val keyboardViews = EnumMap<KeyboardMode, KeyboardView>(KeyboardMode::class.java)
@@ -69,35 +68,23 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
var keyVariation: KeyVariation = KeyVariation.NORMAL
private val layoutManager = LayoutManager(florisboard)
lateinit var smartbarManager: SmartbarManager
private lateinit var smartbarManager: SmartbarManager
// Caps/Space related properties
var caps: Boolean = false
private set
var capsLock: Boolean = false
private set
private var cursorCapsMode: CapsMode = CapsMode.NONE
private var editorCapsMode: CapsMode = CapsMode.NONE
private var hasCapsRecentlyChanged: Boolean = false
private var hasSpaceRecentlyPressed: Boolean = false
// Composing text related properties
private var composingText: String? = null
private var composingTextStart: Int? = null
private var cursorPos: Int = 0
private var isComposingEnabled: Boolean = false
var isManualSelectionMode: Boolean = false
private var isManualSelectionModeLeft: Boolean = false
private var isManualSelectionModeRight: Boolean = false
val isTextSelected: Boolean
get() = selectionEnd - selectionStart != 0
private var lastCursorAnchorInfo: CursorAnchorInfo? = null
private var selectionStart: Int = 0
private val selectionStartMin: Int = 0
private var selectionEnd: Int = 0
private var selectionEndMax: Int = 0
companion object {
private val TAG: String? = TextInputManager::class.simpleName
private var instance: TextInputManager? = null
@Synchronized
@@ -118,7 +105,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* background).
*/
override fun onCreate() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
if (BuildConfig.DEBUG) Log.i(TAG, "onCreate()")
var subtypes = florisboard.subtypeManager.subtypes
if (subtypes.isEmpty()) {
@@ -145,7 +132,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* Sets up the newly registered input view.
*/
override fun onRegisterInputView(inputView: InputView) {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onRegisterInputView(inputView)")
if (BuildConfig.DEBUG) Log.i(TAG, "onRegisterInputView(inputView)")
launch(Dispatchers.Default) {
textViewGroup = inputView.findViewById(R.id.text_input)
@@ -169,7 +156,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* Cancels all coroutines and cleans up.
*/
override fun onDestroy() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
if (BuildConfig.DEBUG) Log.i(TAG, "onDestroy()")
cancel()
osHandler.removeCallbacksAndMessages(null)
@@ -179,58 +166,60 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
/**
* Evaluates the [activeKeyboardMode], [keyVariation] and [isComposingEnabled] property values
* when starting to interact with a input editor. Also resets the composing texts and sets the
* initial caps mode accordingly.
* Evaluates the [activeKeyboardMode], [keyVariation] and [EditorInstance.isComposingEnabled]
* property values when starting to interact with a input editor. Also resets the composing
* texts and sets the initial caps mode accordingly.
*/
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
val keyboardMode = when (info) {
null -> KeyboardMode.CHARACTERS
else -> when (info.inputType and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_CLASS_NUMBER -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.NUMERIC
}
InputType.TYPE_CLASS_PHONE -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.PHONE
}
InputType.TYPE_CLASS_TEXT -> {
keyVariation = when (info.inputType and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> {
KeyVariation.EMAIL_ADDRESS
}
InputType.TYPE_TEXT_VARIATION_PASSWORD,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> {
KeyVariation.PASSWORD
}
InputType.TYPE_TEXT_VARIATION_URI -> {
KeyVariation.URI
}
else -> {
KeyVariation.NORMAL
}
override fun onStartInputView(instance: EditorInstance, restarting: Boolean) {
val keyboardMode = when (instance.inputAttributes.type) {
InputAttributes.Type.NUMBER -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.NUMERIC
}
InputAttributes.Type.PHONE -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.PHONE
}
InputAttributes.Type.TEXT -> {
keyVariation = when (instance.inputAttributes.variation) {
InputAttributes.Variation.EMAIL_ADDRESS,
InputAttributes.Variation.WEB_EMAIL_ADDRESS -> {
KeyVariation.EMAIL_ADDRESS
}
InputAttributes.Variation.PASSWORD,
InputAttributes.Variation.VISIBLE_PASSWORD,
InputAttributes.Variation.WEB_PASSWORD -> {
KeyVariation.PASSWORD
}
InputAttributes.Variation.URI -> {
KeyVariation.URI
}
else -> {
KeyVariation.NORMAL
}
KeyboardMode.CHARACTERS
}
else -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.CHARACTERS
}
KeyboardMode.CHARACTERS
}
else -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.CHARACTERS
}
}
isComposingEnabled = when (keyboardMode) {
instance.isComposingEnabled = when (keyboardMode) {
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> false
else -> keyVariation != KeyVariation.PASSWORD && florisboard.prefs.suggestion.enabled
else -> keyVariation != KeyVariation.PASSWORD &&
florisboard.prefs.suggestion.enabled &&
//!instance.inputAttributes.flagTextAutoComplete &&
!instance.inputAttributes.flagTextNoSuggestions
}
if (!florisboard.prefs.correction.rememberCapsLockState) {
capsLock = false
}
updateCapsState()
resetComposingText()
setActiveKeyboardMode(keyboardMode)
smartbarManager.onStartInputView(keyboardMode, isComposingEnabled)
smartbarManager.onStartInputView(keyboardMode, instance.isComposingEnabled)
}
/**
@@ -284,147 +273,27 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* Main logic point for processing cursor updates as well as parsing the current composing word
* and passing this info on to the [SmartbarManager] to turn it into candidate suggestions.
*/
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
cursorAnchorInfo ?: return
lastCursorAnchorInfo = cursorAnchorInfo
val ic = florisboard.currentInputConnection
val isNewSelectionInBoundsOfOld =
cursorAnchorInfo.selectionStart >= (selectionStart - 1) &&
cursorAnchorInfo.selectionStart <= (selectionStart + 1) &&
cursorAnchorInfo.selectionEnd >= (selectionEnd - 1) &&
cursorAnchorInfo.selectionEnd <= (selectionEnd + 1)
selectionStart = cursorAnchorInfo.selectionStart
selectionEnd = cursorAnchorInfo.selectionEnd
val inputText =
(ic?.getExtractedText(ExtractedTextRequest(), 0)?.text ?: "").toString()
selectionEndMax = inputText.length
// TODO: separate composing text from delete swipe word detection
//if (isComposingEnabled) {
if (!isTextSelected) {
val newCursorPos = cursorAnchorInfo.selectionStart
val prevComposingText = (cursorAnchorInfo.composingText ?: "").toString()
setComposingTextBasedOnInput(inputText, newCursorPos)
if ((newCursorPos == cursorPos) && (composingText == prevComposingText)) {
// Ignore this, as nothing has changed
} else {
cursorPos = newCursorPos
if (composingText != null && composingTextStart != null) {
ic?.setComposingRegion(
composingTextStart!!,
composingTextStart!! + composingText!!.length
)
} else {
resetComposingText()
}
}
} else {
resetComposingText()
}
smartbarManager.generateCandidatesFromComposing(composingText)
//}
if (!isNewSelectionInBoundsOfOld) {
override fun onUpdateSelection() {
if (activeEditorInstance.selection.isCursorMode) {
smartbarManager.generateCandidatesFromComposing(activeEditorInstance.currentWord.text)
}
if (!activeEditorInstance.isNewSelectionInBoundsOfOld) {
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
}
updateCapsState()
smartbarManager.onUpdateCursorAnchorInfo(cursorAnchorInfo)
smartbarManager.onUpdateSelection()
}
/**
* Resets the [composingText] and [composingTextStart] properties. Does NOT sync with
* [SmartbarManager]!
*
* @param notifyInputConnection If the current input connection should be notified.
*/
private fun resetComposingText(notifyInputConnection: Boolean = true) {
if (notifyInputConnection) {
val ic = florisboard.currentInputConnection
ic?.finishComposingText()
}
composingText = null
composingTextStart = null
}
/**
* Tries to parse the [composingText] from a given [inputCursorPos] within [inputText].
* Sets both [composingText] and [composingTextStart] to null if it fails, else to its
* parsed values.
*
* @param inputText The input text to search in.
* @param inputCursorPos The position where to search in [inputText].
*/
private fun setComposingTextBasedOnInput(inputText: String, inputCursorPos: Int) {
val words = inputText.split("[^\\p{L}]".toRegex())
var pos = 0
resetComposingText(false)
for (word in words) {
if (inputCursorPos >= pos && inputCursorPos <= pos + word.length && word.isNotEmpty()) {
composingText = word
composingTextStart = pos
break
} else {
pos += word.length + 1
}
}
}
/**
* Should primarily pe used by [SmartbarManager.candidateViewOnClickListener] to commit
* a candidate if a user has pressed on it.
*/
fun commitCandidate(candidateText: String) {
val ic = florisboard.currentInputConnection
ic?.setComposingText(candidateText, 1)
ic?.finishComposingText()
}
/**
* Parses the [CapsMode] out of the given [flags].
*
* @param flags The input flags.
* @return A [CapsMode] value.
*/
private fun parseCapsModeFromFlags(flags: Int): CapsMode {
return when {
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0 -> {
CapsMode.ALL
}
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0 -> {
CapsMode.SENTENCES
}
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0 -> {
CapsMode.WORDS
}
else -> {
CapsMode.NONE
}
}
}
/**
* Fetches the current cursor caps mode from the current input connection.
*
* @return The [CapsMode] according to the returned flags by the current input connection.
*/
private fun fetchCurrentCursorCapsMode(): CapsMode {
val ic = florisboard.currentInputConnection
val info = florisboard.currentInputEditorInfo
val capsFlags = ic?.getCursorCapsMode(info.inputType) ?: 0
return parseCapsModeFromFlags(capsFlags)
}
/**
* Updates the current caps state according to the [cursorCapsMode], while respecting
* [capsLock] property and the correction.autoCapitalization preference.
* Updates the current caps state according to the [EditorInstance.cursorCapsMode], while
* respecting [capsLock] property and the correction.autoCapitalization preference.
*/
private fun updateCapsState() {
cursorCapsMode = fetchCurrentCursorCapsMode()
editorCapsMode = parseCapsModeFromFlags(florisboard.currentInputEditorInfo.inputType)
if (!capsLock) {
caps = florisboard.prefs.correction.autoCapitalization && cursorCapsMode != CapsMode.NONE
caps = florisboard.prefs.correction.autoCapitalization &&
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
}
}
@@ -445,90 +314,43 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
}
/**
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN].
*
* @param ic The input connection on which this operation should be performed.
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
*/
private fun sendSystemKeyEvent(ic: InputConnection?, keyCode: Int) {
ic?.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
}
/**
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN] with ALT pressed.
*
* @param ic The input connection on which this operation should be performed.
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
*/
private fun sendSystemKeyEventAlt(ic: InputConnection?, keyCode: Int) {
ic?.sendKeyEvent(
KeyEvent(
0,
1,
KeyEvent.ACTION_DOWN, keyCode,
0,
KeyEvent.META_ALT_LEFT_ON
)
)
}
/**
* Handles a [KeyCode.DELETE] event.
*/
private fun handleDelete() {
val ic = florisboard.currentInputConnection
ic?.beginBatchEdit()
resetComposingText()
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DEL)
ic?.endBatchEdit()
activeEditorInstance.deleteBackwards()
}
/**
* Handles a [KeyCode.DELETE_WORD] event.
*/
private fun handleDeleteWord() {
val ic = florisboard.currentInputConnection
ic?.beginBatchEdit()
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
ic?.setComposingText("", 1)
ic?.finishComposingText()
if (ic?.getTextBeforeCursor(1, 0)?.length ?: 0 > 0) {
ic?.deleteSurroundingText(1, 0)
}
composingText = null
composingTextStart = null
ic?.endBatchEdit()
activeEditorInstance.deleteWordsBeforeCursor(1)
}
/**
* Handles a [KeyCode.ENTER] event.
*/
private fun handleEnter() {
val ic = florisboard.currentInputConnection
resetComposingText()
val action = florisboard.currentInputEditorInfo?.imeOptions ?: 0
val actionMasked = action and EditorInfo.IME_MASK_ACTION
if (action and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
sendSystemKeyEvent(ic, KeyEvent.KEYCODE_ENTER)
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
activeEditorInstance.commitText("\n")
} else {
when (actionMasked) {
EditorInfo.IME_ACTION_DONE,
EditorInfo.IME_ACTION_GO,
EditorInfo.IME_ACTION_NEXT,
EditorInfo.IME_ACTION_PREVIOUS,
EditorInfo.IME_ACTION_SEARCH,
EditorInfo.IME_ACTION_SEND -> {
ic?.performEditorAction(actionMasked)
when (activeEditorInstance.imeOptions.action) {
ImeOptions.Action.DONE,
ImeOptions.Action.GO,
ImeOptions.Action.NEXT,
ImeOptions.Action.PREVIOUS,
ImeOptions.Action.SEARCH,
ImeOptions.Action.SEND -> {
activeEditorInstance.performEnterAction(activeEditorInstance.imeOptions.action)
}
else -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_ENTER)
else -> activeEditorInstance.commitText("\n")
}
}
}
@@ -558,14 +380,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* enabled by the user.
*/
private fun handleSpace() {
val ic = florisboard.currentInputConnection
if (florisboard.prefs.correction.doubleSpacePeriod) {
if (hasSpaceRecentlyPressed) {
osHandler.removeCallbacksAndMessages(null)
val text = ic?.getTextBeforeCursor(2, 0) ?: ""
val text = activeEditorInstance.getTextBeforeCursor(2)
if (text.length == 2 && !text.matches("""[.!?‽\s][\s]""".toRegex())) {
ic?.deleteSurroundingText(1, 0)
ic?.commitText(".", 1)
activeEditorInstance.deleteBackwards()
activeEditorInstance.commitText(".")
}
hasSpaceRecentlyPressed = false
} else {
@@ -575,107 +396,107 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}, 300)
}
}
ic?.commitText(KeyCode.SPACE.toChar().toString(), 1)
activeEditorInstance.commitText(KeyCode.SPACE.toChar().toString())
}
/**
* Handles [KeyCode] arrow and move events, behaves differently depending on text selection.
*/
private fun handleArrow(code: Int) {
val ic = florisboard.currentInputConnection
resetComposingText()
if (isTextSelected && isManualSelectionMode) {
private fun handleArrow(code: Int) = activeEditorInstance.apply {
val selectionStartMin = 0
val selectionEndMax = cachedText.length
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) {
ic?.setSelection(
(selectionStart - 1).coerceAtLeast(selectionStartMin),
selectionEnd
setSelection(
(selection.start - 1).coerceAtLeast(selectionStartMin),
selection.end
)
} else {
ic?.setSelection(selectionStart, selectionEnd - 1)
setSelection(selection.start, selection.end - 1)
}
}
KeyCode.ARROW_RIGHT -> {
if (isManualSelectionModeRight) {
ic?.setSelection(
selectionStart,
(selectionEnd + 1).coerceAtMost(selectionEndMax)
setSelection(
selection.start,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
} else {
ic?.setSelection(selectionStart + 1, selectionEnd)
setSelection(selection.start + 1, selection.end)
}
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
if (isManualSelectionModeLeft) {
ic?.setSelection(selectionStartMin, selectionEnd)
setSelection(selectionStartMin, selection.end)
} else {
ic?.setSelection(selectionStartMin, selectionStart)
setSelection(selectionStartMin, selection.start)
}
}
KeyCode.MOVE_END -> {
if (isManualSelectionModeRight) {
ic?.setSelection(selectionStart, selectionEndMax)
setSelection(selection.start, selectionEndMax)
} else {
ic?.setSelection(selectionEnd, selectionEndMax)
setSelection(selection.end, selectionEndMax)
}
}
}
} else if (isTextSelected && !isManualSelectionMode) {
} 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 -> {
ic?.setSelection(selectionStart, selectionEnd - 1)
setSelection(selection.start, selection.end - 1)
}
KeyCode.ARROW_RIGHT -> {
ic?.setSelection(
selectionStart,
(selectionEnd + 1).coerceAtMost(selectionEndMax)
setSelection(
selection.start,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
ic?.setSelection(selectionStartMin, selectionStart)
setSelection(selectionStartMin, selection.start)
}
KeyCode.MOVE_END -> {
ic?.setSelection(selectionStart, selectionEndMax)
setSelection(selection.start, selectionEndMax)
}
}
} else if (!isTextSelected && isManualSelectionMode) {
} 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 -> {
ic?.setSelection(
(selectionStart - 1).coerceAtLeast(selectionStartMin),
selectionStart
setSelection(
(selection.start - 1).coerceAtLeast(selectionStartMin),
selection.start
)
isManualSelectionModeLeft = true
isManualSelectionModeRight = false
}
KeyCode.ARROW_RIGHT -> {
ic?.setSelection(
selectionEnd,
(selectionEnd + 1).coerceAtMost(selectionEndMax)
setSelection(
selection.end,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
isManualSelectionModeLeft = false
isManualSelectionModeRight = true
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
ic?.setSelection(selectionStartMin, selectionStart)
setSelection(selectionStartMin, selection.start)
isManualSelectionModeLeft = true
isManualSelectionModeRight = false
}
KeyCode.MOVE_END -> {
ic?.setSelection(selectionEnd, selectionEndMax)
setSelection(selection.end, selectionEndMax)
isManualSelectionModeLeft = false
isManualSelectionModeRight = true
}
@@ -683,12 +504,12 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
} else {
// No selection and no manual selection mode -> move cursor around
when (code) {
KeyCode.ARROW_DOWN -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_DOWN)
KeyCode.ARROW_LEFT -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_LEFT)
KeyCode.ARROW_RIGHT -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_RIGHT)
KeyCode.ARROW_UP -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_HOME -> sendSystemKeyEventAlt(ic, KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_END -> sendSystemKeyEventAlt(ic, KeyEvent.KEYCODE_DPAD_DOWN)
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_HOME -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_END -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_DOWN)
}
}
}
@@ -698,14 +519,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* TODO: handle other data than text too, e.g. Uri, Intent, ...
*/
private fun handleClipboardCut() {
val ic = florisboard.currentInputConnection
val selectedText = ic?.getSelectedText(0)
if (selectedText != null) {
florisboard.clipboardManager
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
}
resetComposingText()
ic?.commitText("", 1)
val selectedText = activeEditorInstance.selection.text
florisboard.clipboardManager
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
activeEditorInstance.commitText("")
}
/**
@@ -713,14 +530,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* TODO: handle other data than text too, e.g. Uri, Intent, ...
*/
private fun handleClipboardCopy() {
val ic = florisboard.currentInputConnection
val selectedText = ic?.getSelectedText(0)
if (selectedText != null) {
florisboard.clipboardManager
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
}
resetComposingText()
ic?.setSelection(selectionEnd, selectionEnd)
val selectedText = activeEditorInstance.selection.text
florisboard.clipboardManager
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
activeEditorInstance.apply { setSelection(selection.end, selection.end) }
}
/**
@@ -728,42 +541,36 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* TODO: handle other data than text too, e.g. Uri, Intent, ...
*/
private fun handleClipboardPaste() {
val ic = florisboard.currentInputConnection
val item = florisboard.clipboardManager?.primaryClip?.getItemAt(0)
val pasteText = item?.text
if (pasteText != null) {
resetComposingText()
ic?.commitText(pasteText, 1)
activeEditorInstance.commitText(pasteText.toString())
}
}
/**
* Handles a [KeyCode.CLIPBOARD_SELECT] event.
*/
private fun handleClipboardSelect() {
val ic = florisboard.currentInputConnection
resetComposingText()
if (isTextSelected) {
private fun handleClipboardSelect() = activeEditorInstance.apply {
if (selection.isSelectionMode) {
if (isManualSelectionMode && isManualSelectionModeLeft) {
ic?.setSelection(selectionStart, selectionStart)
setSelection(selection.start, selection.start)
} else {
ic?.setSelection(selectionEnd, selectionEnd)
setSelection(selection.end, selection.end)
}
isManualSelectionMode = false
} else {
isManualSelectionMode = !isManualSelectionMode
// Must recall to update UI properly
florisboard.onUpdateCursorAnchorInfo(lastCursorAnchorInfo)
// Must call to update UI properly
editingKeyboardView?.onUpdateSelection()
}
}
}
/**
* Handles a [KeyCode.CLIPBOARD_SELECT_ALL] event.
*/
private fun handleClipboardSelectAll() {
val ic = florisboard.currentInputConnection
resetComposingText()
ic?.setSelection(selectionStartMin, selectionEndMax)
activeEditorInstance.setSelection(0, activeEditorInstance.cachedText.length)
}
/**
@@ -774,8 +581,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* @param keyData The [KeyData] object which should be sent.
*/
fun sendKeyPress(keyData: KeyData) {
val ic = florisboard.currentInputConnection
when (keyData.code) {
KeyCode.ARROW_DOWN,
KeyCode.ARROW_LEFT,
@@ -809,8 +614,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
KeyCode.VIEW_SYMBOLS -> setActiveKeyboardMode(KeyboardMode.SYMBOLS)
KeyCode.VIEW_SYMBOLS2 -> setActiveKeyboardMode(KeyboardMode.SYMBOLS2)
else -> {
ic?.beginBatchEdit()
resetComposingText()
when (activeKeyboardMode) {
KeyboardMode.NUMERIC,
KeyboardMode.NUMERIC_ADVANCED,
@@ -819,13 +622,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
KeyType.CHARACTER,
KeyType.NUMERIC -> {
val text = keyData.code.toChar().toString()
ic?.commitText(text, 1)
activeEditorInstance.commitText(text)
}
else -> when (keyData.code) {
KeyCode.PHONE_PAUSE,
KeyCode.PHONE_WAIT -> {
val text = keyData.code.toChar().toString()
ic?.commitText(text, 1)
activeEditorInstance.commitText(text)
}
}
}
@@ -837,7 +640,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
true -> keyData.label.toUpperCase(Locale.getDefault())
false -> keyData.label.toLowerCase(Locale.getDefault())
}
ic?.commitText(tld, 1)
activeEditorInstance.commitText(tld)
}
else -> {
var text = keyData.code.toChar().toString()
@@ -845,26 +648,15 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
true -> text.toUpperCase(Locale.getDefault())
false -> text.toLowerCase(Locale.getDefault())
}
ic?.commitText(text, 1)
activeEditorInstance.commitText(text)
}
}
else -> {
Log.e(
this::class.simpleName,
"sendKeyPress(keyData): Received unknown key: $keyData"
)
Log.e(TAG,"sendKeyPress(keyData): Received unknown key: $keyData")
}
}
}
ic?.endBatchEdit()
}
}
}
enum class CapsMode {
ALL,
NONE,
SENTENCES,
WORDS;
}
}

View File

@@ -20,7 +20,6 @@ import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.View
import android.view.inputmethod.CursorAnchorInfo
import androidx.constraintlayout.widget.ConstraintLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
@@ -60,8 +59,8 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener {
pasteKey = findViewById(R.id.clipboard_paste)
}
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
val isSelectionActive = florisboard?.textInputManager?.isTextSelected ?: false
override fun onUpdateSelection() {
val isSelectionActive = florisboard?.activeEditorInstance?.selection?.isSelectionMode ?: false
val isSelectionMode = florisboard?.textInputManager?.isManualSelectionMode ?: false
arrowUpKey?.isEnabled = !(isSelectionActive || isSelectionMode)
arrowDownKey?.isEnabled = !(isSelectionActive || isSelectionMode)

View File

@@ -17,7 +17,6 @@
package dev.patrickgold.florisboard.ime.text.key
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.graphics.*
import android.graphics.drawable.Drawable
import android.os.Handler
@@ -39,6 +38,7 @@ import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
import java.util.*
import kotlin.math.abs
/**
* View class for managing the rendering and the events of a single keyboard key.
@@ -64,6 +64,8 @@ class KeyView(
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private var shouldBlockNextKeyCode: Boolean = false
private var desiredWidth: Int = 0
private var desiredHeight: Int = 0
private var drawable: Drawable? = null
private var drawableColor: Int = 0
private var drawablePadding: Int = 0
@@ -87,6 +89,7 @@ class KeyView(
textSize = resources.getDimension(R.dimen.key_textHintSize)
typeface = Typeface.DEFAULT
}
private val tempRect: Rect = Rect()
var florisboard: FlorisBoard? = null
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
@@ -326,7 +329,7 @@ class KeyView(
* by Devunwired
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredWidth = when (keyboardView.computedLayout?.mode) {
desiredWidth = when (keyboardView.computedLayout?.mode) {
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> (keyboardView.desiredKeyWidth * 2.68f).toInt()
@@ -345,7 +348,7 @@ class KeyView(
else -> keyboardView.desiredKeyWidth
}
}
val desiredHeight = keyboardView.desiredKeyHeight
desiredHeight = keyboardView.desiredKeyHeight
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
@@ -497,6 +500,45 @@ class KeyView(
}
}
/**
* Automatically sets the text size of [boxPaint] for given [text] so it fits within the given
* bounds.
*
* Implementation based on this SO answer by Michael Scheper, but has been modified to
* incorporate the height as well: https://stackoverflow.com/a/21895626/6801193
*
* @param boxPaint The [Paint] object which the text size should be applied to.
* @param boxWidth The max width for the surrounding box of [text].
* @param boxHeight The max height for the surrounding box of [text].
* @param text The text for which the size should be calculated.
*/
private fun setTextSizeFor(boxPaint: Paint, boxWidth: Float, boxHeight: Float, text: String) {
var textSize = 64.0f
// Must loop twice as there can be bot with and height which are too big, which requires
// 2 iterations to adjust
for (n in 0..1) {
boxPaint.textSize = textSize
boxPaint.getTextBounds(text, 0, text.length, tempRect)
val diffWidth = tempRect.width() - boxWidth
val diffHeight = tempRect.height() - boxHeight
val factor = if (diffWidth < 0 && diffHeight < 0) {
// Text box is smaller as given box, text size must be increased
if (abs(diffWidth) < abs(diffHeight)) {
boxWidth / tempRect.width()
} else {
boxHeight / tempRect.height()
}
} else if (diffWidth > diffHeight) {
// Text box is larger on minimum one side than given box, text size must be decreased
boxWidth / tempRect.width()
} else {
boxHeight / tempRect.height()
}
textSize *= factor
}
boxPaint.textSize = textSize
}
/**
* Draw the key label / drawable.
*/
@@ -613,9 +655,6 @@ class KeyView(
}
}
val isPortrait =
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
// Draw drawable
val drawable = drawable
if (drawable != null) {
@@ -641,14 +680,36 @@ class KeyView(
// Draw label
val label = label
if (label != null) {
if (data.code == KeyCode.VIEW_NUMERIC || data.code == KeyCode.VIEW_NUMERIC_ADVANCED
|| data.code == KeyCode.SPACE) {
labelPaint.textSize = resources.getDimension(R.dimen.key_numeric_textSize)
} else {
labelPaint.textSize = resources.getDimension(R.dimen.key_textSize)
}
if (prefs.keyboard.oneHandedMode != "off" && isPortrait) {
labelPaint.textSize *= 0.9f
when (data.code) {
KeyCode.VIEW_NUMERIC, KeyCode.VIEW_NUMERIC_ADVANCED -> {
labelPaint.textSize = resources.getDimension(R.dimen.key_numeric_textSize)
}
else -> when {
data.type == KeyType.CHARACTER && data.code != KeyCode.SPACE -> {
setTextSizeFor(
labelPaint,
desiredWidth - (2.6f * drawablePadding),
desiredHeight - (3.6f * drawablePadding),
// Note: taking a "X" here because it is one of the biggest letters and
// the keys must have the same base character for calculation, else
// they will all look different and weird...
"X"
)
}
else -> {
setTextSizeFor(
labelPaint,
measuredWidth - (2.6f * drawablePadding),
measuredHeight - (3.6f * drawablePadding),
when (data.code) {
KeyCode.VIEW_CHARACTERS, KeyCode.VIEW_SYMBOLS, KeyCode.VIEW_SYMBOLS2 -> {
resources.getString(R.string.key__view_symbols)
}
else -> label
}
)
}
}
}
labelPaint.color = prefs.theme.keyFgColor
labelPaint.alpha = if (keyboardView.computedLayout?.mode == KeyboardMode.CHARACTERS &&
@@ -668,10 +729,15 @@ class KeyView(
// Draw hinted label
val hintedLabel = hintedLabel
if (hintedLabel != null) {
hintedLabelPaint.textSize = resources.getDimension(R.dimen.key_textHintSize)
if (prefs.keyboard.oneHandedMode != "off" && isPortrait) {
hintedLabelPaint.textSize *= 0.9f
}
setTextSizeFor(
hintedLabelPaint,
desiredWidth * 1.0f / 6.0f,
desiredHeight * 1.0f / 6.0f,
// Note: taking a "X" here because it is one of the biggest letters and
// the keys must have the same base character for calculation, else
// they will all look different and weird...
"X"
)
hintedLabelPaint.color = prefs.theme.keyFgColor
hintedLabelPaint.alpha = 120
val centerX = measuredWidth * 5.0f / 6.0f

View File

@@ -33,7 +33,7 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
val view = v as Button
val text = view.text.toString()
if (text.isNotEmpty()) {
textInputManager.commitCandidate(text)
florisboard.activeEditorInstance.commitCompletion(text)
}
}
private val candidateViewOnLongClickListener = View.OnLongClickListener { v ->
@@ -161,8 +161,8 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
//spellCheckerSession?.close()
}
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
val isSelectionActive = florisboard.textInputManager.isTextSelected
override fun onUpdateSelection() {
val isSelectionActive = florisboard.activeEditorInstance.selection.isSelectionMode
smartbarView?.findViewById<View>(R.id.cc_cut)?.isEnabled = isSelectionActive
smartbarView?.findViewById<View>(R.id.cc_copy)?.isEnabled = isSelectionActive
smartbarView?.findViewById<View>(R.id.cc_paste)?.isEnabled =
@@ -178,10 +178,10 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
//
}
fun generateCandidatesFromComposing(composingText: String?) {
fun generateCandidatesFromComposing(composingText: String) {
val smartbarView = smartbarView ?: return
if (composingText == null) {
if (composingText == "") {
smartbarView.candidateViewList[0].text = "candidate"
smartbarView.candidateViewList[1].text = "suggestions"
smartbarView.candidateViewList[2].text = "nyi"

View File

@@ -0,0 +1,217 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.util
import android.text.InputType
import android.text.TextUtils
import android.view.inputmethod.EditorInfo
import kotlin.reflect.KClass
fun EditorInfo.debugSummarize(): String {
var summary = this::class.qualifiedName + "\r\n"
summary += "imeOptions: " + this.imeOptions.debugSummarize(EditorInfo::class) + "\r\n"
summary += "initialCapsMode: " + this.initialCapsMode.debugSummarize(TextUtils::class) + "\r\n"
summary += "initialSelStart: " + this.initialSelStart + "\r\n"
summary += "initialSelEnd: " + this.initialSelEnd + "\r\n"
summary += "inputType: " + this.inputType.debugSummarize(InputType::class) + "\r\n"
summary += "packageName: " + this.packageName
return summary
}
fun <T: Any> Int.debugSummarize(type: KClass<T>): String {
var summary = ""
when (type) {
EditorInfo::class -> {
when (this) {
EditorInfo.IME_NULL -> {
summary += "IME_NULL"
}
else -> {
val tAction = when (this and EditorInfo.IME_MASK_ACTION) {
EditorInfo.IME_ACTION_DONE -> "IME_ACTION_DONE"
EditorInfo.IME_ACTION_GO -> "IME_ACTION_GO"
EditorInfo.IME_ACTION_NEXT -> "IME_ACTION_NEXT"
EditorInfo.IME_ACTION_NONE -> "IME_ACTION_NONE"
EditorInfo.IME_ACTION_PREVIOUS -> "IME_ACTION_PREVIOUS"
EditorInfo.IME_ACTION_SEARCH -> "IME_ACTION_SEARCH"
EditorInfo.IME_ACTION_SEND -> "IME_ACTION_SEND"
EditorInfo.IME_ACTION_UNSPECIFIED -> "IME_ACTION_UNSPECIFIED"
else -> String.format("0x%08x", this and EditorInfo.IME_MASK_ACTION)
}
var tFlags = ""
if (this and EditorInfo.IME_FLAG_FORCE_ASCII > 0) {
tFlags += "IME_FLAG_FORCE_ASCII|"
}
if (this and EditorInfo.IME_FLAG_NAVIGATE_NEXT > 0) {
tFlags += "IME_FLAG_NAVIGATE_NEXT|"
}
if (this and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS > 0) {
tFlags += "IME_FLAG_NAVIGATE_PREVIOUS|"
}
if (this and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION > 0) {
tFlags += "IME_FLAG_NO_ACCESSORY_ACTION|"
}
if (this and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
tFlags += "IME_FLAG_NO_ENTER_ACTION|"
}
if (this and EditorInfo.IME_FLAG_NO_EXTRACT_UI > 0) {
tFlags += "IME_FLAG_NO_EXTRACT_UI|"
}
if (this and EditorInfo.IME_FLAG_NO_FULLSCREEN > 0) {
tFlags += "IME_FLAG_NO_FULLSCREEN|"
}
if (this and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING > 0) {
tFlags += "IME_FLAG_NO_PERSONALIZED_LEARNING|"
}
if (tFlags.isEmpty()) {
tFlags = "(none)"
}
if (tFlags.endsWith("|")) {
tFlags = tFlags.substring(0, tFlags.length - 1)
}
summary += "action=$tAction flags=$tFlags"
}
}
}
InputType::class -> {
when (this) {
InputType.TYPE_NULL -> {
summary += "TYPE_NULL"
}
else -> {
val tClass: String
val tVariation: String
var tFlags = ""
when (this and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_CLASS_DATETIME -> {
tClass = "TYPE_CLASS_DATETIME"
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_DATETIME_VARIATION_DATE -> "TYPE_DATETIME_VARIATION_DATE"
InputType.TYPE_DATETIME_VARIATION_NORMAL -> "TYPE_DATETIME_VARIATION_NORMAL"
InputType.TYPE_DATETIME_VARIATION_TIME -> "TYPE_DATETIME_VARIATION_TIME"
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
}
}
InputType.TYPE_CLASS_NUMBER -> {
tClass = "TYPE_CLASS_NUMBER"
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_NUMBER_VARIATION_NORMAL -> "TYPE_NUMBER_VARIATION_NORMAL"
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> "TYPE_NUMBER_VARIATION_PASSWORD"
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
}
if (this and InputType.TYPE_NUMBER_FLAG_DECIMAL > 0) {
tFlags += "TYPE_NUMBER_FLAG_DECIMAL|"
}
if (this and InputType.TYPE_NUMBER_FLAG_SIGNED > 0) {
tFlags += "TYPE_NUMBER_FLAG_SIGNED|"
}
}
InputType.TYPE_CLASS_PHONE -> {
tClass = "TYPE_CLASS_PHONE"
tVariation = String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
}
InputType.TYPE_CLASS_TEXT -> {
tClass = "TYPE_CLASS_TEXT"
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> "TYPE_TEXT_VARIATION_EMAIL_ADDRESS"
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> "TYPE_TEXT_VARIATION_EMAIL_SUBJECT"
InputType.TYPE_TEXT_VARIATION_FILTER -> "TYPE_TEXT_VARIATION_FILTER"
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> "TYPE_TEXT_VARIATION_LONG_MESSAGE"
InputType.TYPE_TEXT_VARIATION_NORMAL -> "TYPE_TEXT_VARIATION_NORMAL"
InputType.TYPE_TEXT_VARIATION_PASSWORD -> "TYPE_TEXT_VARIATION_PASSWORD"
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> "TYPE_TEXT_VARIATION_PERSON_NAME"
InputType.TYPE_TEXT_VARIATION_PHONETIC -> "TYPE_TEXT_VARIATION_PHONETIC"
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> "TYPE_TEXT_VARIATION_POSTAL_ADDRESS"
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> "TYPE_TEXT_VARIATION_SHORT_MESSAGE"
InputType.TYPE_TEXT_VARIATION_URI -> "TYPE_TEXT_VARIATION_URI"
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> "TYPE_TEXT_VARIATION_VISIBLE_PASSWORD"
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> "TYPE_TEXT_VARIATION_WEB_EDIT_TEXT"
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> "TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS"
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> "TYPE_TEXT_VARIATION_WEB_PASSWORD"
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
}
if (this and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE > 0) {
tFlags += "TYPE_TEXT_FLAG_AUTO_COMPLETE|"
}
if (this and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT > 0) {
tFlags += "TYPE_TEXT_FLAG_AUTO_CORRECT|"
}
if (this and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0) {
tFlags += "TYPE_TEXT_FLAG_CAP_CHARACTERS|"
}
if (this and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0) {
tFlags += "TYPE_TEXT_FLAG_CAP_SENTENCES|"
}
if (this and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0) {
tFlags += "TYPE_TEXT_FLAG_CAP_WORDS|"
}
if (this and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE > 0) {
tFlags += "TYPE_TEXT_FLAG_IME_MULTI_LINE|"
}
if (this and InputType.TYPE_TEXT_FLAG_MULTI_LINE > 0) {
tFlags += "TYPE_TEXT_FLAG_MULTI_LINE|"
}
if (this and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS > 0) {
tFlags += "TYPE_TEXT_FLAG_NO_SUGGESTIONS|"
}
}
else -> {
tClass = String.format("0x%08x", this and InputType.TYPE_MASK_CLASS)
tVariation = String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
}
}
if (tFlags.isEmpty()) {
tFlags = "(none)"
}
if (tFlags.endsWith("|")) {
tFlags = tFlags.substring(0, tFlags.length - 1)
}
summary += "class=$tClass variation=$tVariation flags=$tFlags"
}
}
}
TextUtils::class -> {
var tFlags = ""
if (this and TextUtils.CAP_MODE_CHARACTERS > 0) {
tFlags += "CAP_MODE_CHARACTERS|"
}
if (this and TextUtils.CAP_MODE_SENTENCES > 0) {
tFlags += "CAP_MODE_SENTENCES|"
}
if (this and TextUtils.CAP_MODE_WORDS > 0) {
tFlags += "CAP_MODE_WORDS|"
}
if (this and TextUtils.SAFE_STRING_FLAG_FIRST_LINE > 0) {
tFlags += "SAFE_STRING_FLAG_FIRST_LINE|"
}
if (this and TextUtils.SAFE_STRING_FLAG_SINGLE_LINE > 0) {
tFlags += "SAFE_STRING_FLAG_SINGLE_LINE|"
}
if (this and TextUtils.SAFE_STRING_FLAG_TRIM > 0) {
tFlags += "SAFE_STRING_FLAG_TRIM|"
}
if (tFlags.isEmpty()) {
tFlags = "(none)"
}
if (tFlags.endsWith("|")) {
tFlags = tFlags.substring(0, tFlags.length - 1)
}
summary += "flags=$tFlags"
}
}
return summary
}

View File

@@ -141,6 +141,8 @@
<string name="pref__correction__title">Corrections</string>
<string name="pref__correction__auto_capitalization__label">Auto-capitalization</string>
<string name="pref__correction__auto_capitalization__summary">Capitalize words based on the current input context</string>
<string name="pref__correction__remember_caps_lock_state__label">Remember caps lock state</string>
<string name="pref__correction__remember_caps_lock_state__summary">Caps lock will stay on when moving to another text field</string>
<string name="pref__correction__double_space_period__label">Double-space period</string>
<string name="pref__correction__double_space_period__summary">Tapping twice on spacebar inserts a period followed by a space</string>

View File

@@ -44,6 +44,13 @@
app:title="@string/pref__correction__auto_capitalization__label"
app:summary="@string/pref__correction__auto_capitalization__summary"/>
<SwitchPreferenceCompat
android:defaultValue="false"
app:key="correction__remember_caps_lock_state"
app:iconSpaceReserved="false"
app:title="@string/pref__correction__remember_caps_lock_state__label"
app:summary="@string/pref__correction__remember_caps_lock_state__summary"/>
<SwitchPreferenceCompat
android:defaultValue="true"
app:key="correction__double_space_period"

View File

@@ -0,0 +1,5 @@
- Rework core to better implement interface between FlorisBoard and other apps
- Shift state should now update after a key press (#35)
- Send key should now send the desired action or a newline character (#33)
- Adjusting keyboard height also affects font size of keys (#32)
- Add option to remember / forget caps lock state throughout different input fields (#30)

View File

@@ -0,0 +1,5 @@
- Riorganizzazione del core per implementare al meglio l'interfaccia tra FlorisBoard e le altre applicazioni
- Lo stato del turno dovrebbe ora aggiornarsi dopo la pressione di un tasto (#35)
- Il tasto Invio dovrebbe ora inviare l'azione desiderata o un carattere di nuova linea (#33)
- La regolazione dell'altezza della tastiera influisce anche sulla dimensione dei caratteri dei tasti (#32)
- Aggiungere l'opzione per ricordare / dimenticare lo stato di blocco dei maiuscoli in diversi campi di input (#30)