diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt index 89fdcecb..019bfa80 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt @@ -4,6 +4,7 @@ import android.view.KeyEvent import android.view.inputmethod.InputMethodSubtype import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode import helium314.keyboard.latin.LatinIME +import helium314.keyboard.latin.RichInputConnection import helium314.keyboard.latin.RichInputMethodManager import helium314.keyboard.latin.common.Constants import helium314.keyboard.latin.common.InputPointers @@ -13,9 +14,12 @@ import helium314.keyboard.latin.common.loopOverCodePointsBackwards import helium314.keyboard.latin.inputlogic.InputLogic import helium314.keyboard.latin.settings.Settings import kotlin.math.abs +import kotlin.math.min class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inputLogic: InputLogic) : KeyboardActionListener { + private val connection = inputLogic.mConnection + private val keyboardSwitcher = KeyboardSwitcher.getInstance() private val settings = Settings.getInstance() private var metaState = 0 // is this enough, or are there threading issues with the different PointerTrackers? @@ -70,8 +74,9 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp keyboardSwitcher.onFinishSlidingInput(latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState) override fun onCustomRequest(requestCode: Int): Boolean { - if (requestCode == Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER) + if (requestCode == Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER) { return latinIME.showInputPickerDialog() + } return false } @@ -101,30 +106,34 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp override fun onMoveDeletePointer(steps: Int) { inputLogic.finishInput() - val end = inputLogic.mConnection.expectedSelectionEnd - var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint + val end = connection.expectedSelectionEnd + val actualSteps = actualSteps(steps) + val start = connection.expectedSelectionStart + actualSteps + if (start > end) return + connection.setSelection(start, end) + } + + private fun actualSteps(steps: Int): Int { + var actualSteps = 0 + // corrected steps to avoid splitting chars belonging to the same codepoint if (steps > 0) { - val text = inputLogic.mConnection.getSelectedText(0) - if (text == null) actualSteps = steps - else loopOverCodePoints(text) { - actualSteps += Character.charCount(it) + val text = connection.getSelectedText(0) ?: return steps + loopOverCodePoints(text) { cp, charCount -> + actualSteps += charCount actualSteps >= steps } } else { - val text = inputLogic.mConnection.getTextBeforeCursor(-steps * 4, 0) - if (text == null) actualSteps = steps - else loopOverCodePointsBackwards(text) { - actualSteps -= Character.charCount(it) + val text = connection.getTextBeforeCursor(-steps * 4, 0) ?: return steps + loopOverCodePointsBackwards(text) { cp, charCount -> + actualSteps -= charCount actualSteps <= steps } } - val start = inputLogic.mConnection.expectedSelectionStart + actualSteps - if (start > end) return - inputLogic.mConnection.setSelection(start, end) + return actualSteps } override fun onUpWithDeletePointerActive() { - if (!inputLogic.mConnection.hasSelection()) return + if (!connection.hasSelection()) return inputLogic.finishInput() onCodeInput(KeyCode.DELETE, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false) } @@ -143,16 +152,15 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp val current = RichInputMethodManager.getInstance().currentSubtype.rawSubtype var wantedIndex = subtypes.indexOf(current) + if (steps > 0) 1 else -1 wantedIndex %= subtypes.size - if (wantedIndex < 0) - wantedIndex += subtypes.size + if (wantedIndex < 0) wantedIndex += subtypes.size val newSubtype = subtypes[wantedIndex] // do not switch if we would switch to the initial subtype after cycling all other subtypes - if (initialSubtype == null) - initialSubtype = current + if (initialSubtype == null) initialSubtype = current if (initialSubtype == newSubtype) { - if ((subtypeSwitchCount > 0 && steps > 0) || ((subtypeSwitchCount < 0 && steps < 0))) + if (subtypeSwitchCount > 0 && steps > 0 || subtypeSwitchCount < 0 && steps < 0) { return true + } } if (steps > 0) subtypeSwitchCount++ else subtypeSwitchCount-- @@ -173,17 +181,8 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp val steps = if (RichInputMethodManager.getInstance().currentSubtype.isRtlSubtype) -rawSteps else rawSteps val moveSteps: Int if (steps < 0) { - var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint - val text = inputLogic.mConnection.getTextBeforeCursor(-steps * 4, 0) ?: return false - loopOverCodePointsBackwards(text) { - if (StringUtils.mightBeEmoji(it)) { - actualSteps = 0 - return@loopOverCodePointsBackwards true - } - actualSteps -= Character.charCount(it) - actualSteps <= steps - } - moveSteps = -text.length.coerceAtMost(abs(actualSteps)) + val text = connection.getTextBeforeCursor(-steps * 4, 0) ?: return false + moveSteps = negativeMoveSteps(text, steps) if (moveSteps == 0) { // some apps don't return any text via input connection, and the cursor can't be moved // we fall back to virtually pressing the left/right key one or more times instead @@ -193,18 +192,11 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp return true } } else { - var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint - val text = inputLogic.mConnection.getTextAfterCursor(steps * 4, 0) ?: return false - loopOverCodePoints(text) { - if (StringUtils.mightBeEmoji(it)) { - actualSteps = 0 - return@loopOverCodePoints true - } - actualSteps += Character.charCount(it) - actualSteps >= steps - } - moveSteps = text.length.coerceAtMost(actualSteps) + val text = connection.getTextAfterCursor(steps * 4, 0) ?: return false + moveSteps = positiveMoveSteps(text, steps) if (moveSteps == 0) { + // some apps don't return any text via input connection, and the cursor can't be moved + // we fall back to virtually pressing the left/right key one or more times instead repeat(steps) { onCodeInput(KeyCode.ARROW_RIGHT, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false) } @@ -214,15 +206,36 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp if (inputLogic.moveCursorByAndReturnIfInsideComposingWord(moveSteps)) { // no need to finish input and restart suggestions if we're still in the word // this is a noticeable performance improvement - val newPosition = inputLogic.mConnection.expectedSelectionStart + moveSteps - inputLogic.mConnection.setSelection(newPosition, newPosition) + val newPosition = connection.expectedSelectionStart + moveSteps + connection.setSelection(newPosition, newPosition) return true } inputLogic.finishInput() - val newPosition = inputLogic.mConnection.expectedSelectionStart + moveSteps - inputLogic.mConnection.setSelection(newPosition, newPosition) + val newPosition = connection.expectedSelectionStart + moveSteps + connection.setSelection(newPosition, newPosition) inputLogic.restartSuggestionsOnWordTouchedByCursor(settings.current, keyboardSwitcher.currentKeyboardScript) return true } + private fun positiveMoveSteps(text: CharSequence, steps: Int): Int { + var actualSteps = 0 + // corrected steps to avoid splitting chars belonging to the same codepoint + loopOverCodePoints(text) { cp, charCount -> + if (StringUtils.mightBeEmoji(cp)) return 0 + actualSteps += charCount + actualSteps >= steps + } + return min(actualSteps, text.length) + } + + private fun negativeMoveSteps(text: CharSequence, steps: Int): Int { + var actualSteps = 0 + // corrected steps to avoid splitting chars belonging to the same codepoint + loopOverCodePointsBackwards(text) { cp, charCount -> + if (StringUtils.mightBeEmoji(cp)) return 0 + actualSteps -= charCount + actualSteps <= steps + } + return -min(-actualSteps, text.length) + } } diff --git a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt index 5b8af87c..d7bce1f8 100644 --- a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt @@ -9,64 +9,61 @@ import helium314.keyboard.latin.settings.SpacingAndPunctuations import java.math.BigInteger import java.util.Locale -fun loopOverCodePoints(s: CharSequence, run: (Int) -> Boolean) { - val text = if (s is String) s else s.toString() +inline fun loopOverCodePoints(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) { + val s = text.toString() var offset = 0 - while (offset < text.length) { - val codepoint = text.codePointAt(offset) - if (run(codepoint)) return - offset += Character.charCount(codepoint) + while (offset < s.length) { + val cp = s.codePointAt(offset) + val charCount = Character.charCount(cp) + if (loop(cp, charCount)) return + offset += charCount } } -fun loopOverCodePointsBackwards(s: CharSequence, run: (Int) -> Boolean) { - val text = if (s is String) s else s.toString() - var offset = text.length +inline fun loopOverCodePointsBackwards(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) { + val s = text.toString() + var offset = s.length while (offset > 0) { - val codepoint = text.codePointBefore(offset) - if (run(codepoint)) return - offset -= Character.charCount(codepoint) + val cp = s.codePointBefore(offset) + val charCount = Character.charCount(cp) + if (loop(cp, charCount)) return + offset -= charCount } } -fun nonWordCodePointAndNoSpaceBeforeCursor(s: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean { +fun nonWordCodePointAndNoSpaceBeforeCursor(text: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean { var space = false var nonWordCodePoint = false - loopOverCodePointsBackwards(s) { - if (!space && Character.isWhitespace(it)) - space = true - // treat double quote like a word codepoint for the purpose of this function (not great, maybe clarify name, or extend list of chars?) - if (!nonWordCodePoint && !spacingAndPunctuations.isWordCodePoint(it) && it != '"'.code) + loopOverCodePointsBackwards(text) { cp, _ -> + if (!space && Character.isWhitespace(cp)) space = true + // treat double quote like a word codepoint for this function (not great, maybe clarify name or extend list of chars?) + if (!nonWordCodePoint && !spacingAndPunctuations.isWordCodePoint(cp) && cp != '"'.code) { nonWordCodePoint = true + } space && nonWordCodePoint // stop if both are found } - return nonWordCodePoint && !space // return true if an non-word codepoint and no space was found + return nonWordCodePoint // return true if a non-word codepoint and no space was found } -fun hasLetterBeforeLastSpaceBeforeCursor(s: CharSequence): Boolean { - var letter = false - loopOverCodePointsBackwards(s) { - if (Character.isWhitespace(it)) true - else if (Character.isLetter(it)) { - letter = true - true - } - else false +fun hasLetterBeforeLastSpaceBeforeCursor(text: CharSequence): Boolean { + loopOverCodePointsBackwards(text) { cp, _ -> + if (Character.isWhitespace(cp)) return false + else if (Character.isLetter(cp)) return true + false // continue } - return letter + return false } -/** get the complete emoji at end of [s], considering that emojis can be joined with ZWJ resulting in different emojis */ -fun getFullEmojiAtEnd(s: CharSequence): String { - val text = if (s is String) s else s.toString() - var offset = text.length +/** get the complete emoji at end of [text], considering that emojis can be joined with ZWJ resulting in different emojis */ +fun getFullEmojiAtEnd(text: CharSequence): String { + val s = text.toString() + var offset = s.length while (offset > 0) { - val codepoint = text.codePointBefore(offset) + val codepoint = s.codePointBefore(offset) // stop if codepoint can't be emoji - if (!mightBeEmoji(codepoint)) - return text.substring(offset) + if (!mightBeEmoji(codepoint)) return text.substring(offset) offset -= Character.charCount(codepoint) - if (offset > 0 && text[offset - 1].code == KeyCode.ZWJ) { + if (offset > 0 && s[offset - 1].code == KeyCode.ZWJ) { // todo: this appends ZWJ in weird cases like text, ZWJ, emoji // and detects single ZWJ as emoji (at least irrelevant for current use of getFullEmojiAtEnd) offset -= 1 @@ -76,19 +73,17 @@ fun getFullEmojiAtEnd(s: CharSequence): String { if (codepoint in 0x1F3FB..0x1F3FF) { // Skin tones are not added with ZWJ, but just appended. This is not nice as they can be emojis on their own, // but that's how it is done. Assume that an emoji before the skin tone will get merged (usually correct in practice) - val codepointBefore = text.codePointBefore(offset) + val codepointBefore = s.codePointBefore(offset) if (isEmoji(codepointBefore)) { offset -= Character.charCount(codepointBefore) continue } } // check the whole text after offset - val textToCheck = text.substring(offset) - if (isEmoji(textToCheck)) { - return textToCheck - } + val textToCheck = s.substring(offset) + if (isEmoji(textToCheck)) return textToCheck } - return text.substring(offset) + return s.substring(offset) } /** split the string on the first of consecutive space only, further consecutive spaces are added to the next split */ @@ -110,8 +105,7 @@ fun String.splitOnFirstSpacesOnly(): List { sb.append(c) } } - if (sb.isNotBlank()) - out.add(sb.toString()) + if (sb.isNotBlank()) out.add(sb.toString()) return out } @@ -120,8 +114,7 @@ fun CharSequence.isValidNumber(): Boolean { } fun String.decapitalize(locale: Locale): String { - if (isEmpty() || !this[0].isUpperCase()) - return this + if (isEmpty() || !this[0].isUpperCase()) return this return replaceFirstChar { it.lowercase(locale) } } @@ -136,7 +129,7 @@ fun containsValueWhenSplit(string: String?, value: String, split: String): Boole fun isEmoji(c: Int): Boolean = mightBeEmoji(c) && isEmoji(newSingleCodePointString(c)) -fun isEmoji(s: CharSequence): Boolean = mightBeEmoji(s) && s.matches(emoRegex) +fun isEmoji(text: CharSequence): Boolean = mightBeEmoji(text) && text.matches(emoRegex) fun String.splitOnWhitespace() = split(whitespaceSplitRegex)