This commit is contained in:
Devy Ballard 2025-03-26 23:24:39 +00:00 committed by GitHub
commit 1e5bf2b238
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 100 additions and 94 deletions

View file

@ -4,6 +4,7 @@ import android.view.KeyEvent
import android.view.inputmethod.InputMethodSubtype import android.view.inputmethod.InputMethodSubtype
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.LatinIME import helium314.keyboard.latin.LatinIME
import helium314.keyboard.latin.RichInputConnection
import helium314.keyboard.latin.RichInputMethodManager import helium314.keyboard.latin.RichInputMethodManager
import helium314.keyboard.latin.common.Constants import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.InputPointers 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.inputlogic.InputLogic
import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.settings.Settings
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min
class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inputLogic: InputLogic) : KeyboardActionListener { class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inputLogic: InputLogic) : KeyboardActionListener {
private val connection = inputLogic.mConnection
private val keyboardSwitcher = KeyboardSwitcher.getInstance() private val keyboardSwitcher = KeyboardSwitcher.getInstance()
private val settings = Settings.getInstance() private val settings = Settings.getInstance()
private var metaState = 0 // is this enough, or are there threading issues with the different PointerTrackers? 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) keyboardSwitcher.onFinishSlidingInput(latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState)
override fun onCustomRequest(requestCode: Int): Boolean { 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 latinIME.showInputPickerDialog()
}
return false return false
} }
@ -101,30 +106,34 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
override fun onMoveDeletePointer(steps: Int) { override fun onMoveDeletePointer(steps: Int) {
inputLogic.finishInput() inputLogic.finishInput()
val end = inputLogic.mConnection.expectedSelectionEnd val end = connection.expectedSelectionEnd
var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint 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) { if (steps > 0) {
val text = inputLogic.mConnection.getSelectedText(0) val text = connection.getSelectedText(0) ?: return steps
if (text == null) actualSteps = steps loopOverCodePoints(text) { cp, charCount ->
else loopOverCodePoints(text) { actualSteps += charCount
actualSteps += Character.charCount(it)
actualSteps >= steps actualSteps >= steps
} }
} else { } else {
val text = inputLogic.mConnection.getTextBeforeCursor(-steps * 4, 0) val text = connection.getTextBeforeCursor(-steps * 4, 0) ?: return steps
if (text == null) actualSteps = steps loopOverCodePointsBackwards(text) { cp, charCount ->
else loopOverCodePointsBackwards(text) { actualSteps -= charCount
actualSteps -= Character.charCount(it)
actualSteps <= steps actualSteps <= steps
} }
} }
val start = inputLogic.mConnection.expectedSelectionStart + actualSteps return actualSteps
if (start > end) return
inputLogic.mConnection.setSelection(start, end)
} }
override fun onUpWithDeletePointerActive() { override fun onUpWithDeletePointerActive() {
if (!inputLogic.mConnection.hasSelection()) return if (!connection.hasSelection()) return
inputLogic.finishInput() inputLogic.finishInput()
onCodeInput(KeyCode.DELETE, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false) 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 val current = RichInputMethodManager.getInstance().currentSubtype.rawSubtype
var wantedIndex = subtypes.indexOf(current) + if (steps > 0) 1 else -1 var wantedIndex = subtypes.indexOf(current) + if (steps > 0) 1 else -1
wantedIndex %= subtypes.size wantedIndex %= subtypes.size
if (wantedIndex < 0) if (wantedIndex < 0) wantedIndex += subtypes.size
wantedIndex += subtypes.size
val newSubtype = subtypes[wantedIndex] val newSubtype = subtypes[wantedIndex]
// do not switch if we would switch to the initial subtype after cycling all other subtypes // do not switch if we would switch to the initial subtype after cycling all other subtypes
if (initialSubtype == null) if (initialSubtype == null) initialSubtype = current
initialSubtype = current
if (initialSubtype == newSubtype) { if (initialSubtype == newSubtype) {
if ((subtypeSwitchCount > 0 && steps > 0) || ((subtypeSwitchCount < 0 && steps < 0))) if (subtypeSwitchCount > 0 && steps > 0 || subtypeSwitchCount < 0 && steps < 0) {
return true return true
}
} }
if (steps > 0) subtypeSwitchCount++ else subtypeSwitchCount-- 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 steps = if (RichInputMethodManager.getInstance().currentSubtype.isRtlSubtype) -rawSteps else rawSteps
val moveSteps: Int val moveSteps: Int
if (steps < 0) { if (steps < 0) {
var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint val text = connection.getTextBeforeCursor(-steps * 4, 0) ?: return false
val text = inputLogic.mConnection.getTextBeforeCursor(-steps * 4, 0) ?: return false moveSteps = negativeMoveSteps(text, steps)
loopOverCodePointsBackwards(text) {
if (StringUtils.mightBeEmoji(it)) {
actualSteps = 0
return@loopOverCodePointsBackwards true
}
actualSteps -= Character.charCount(it)
actualSteps <= steps
}
moveSteps = -text.length.coerceAtMost(abs(actualSteps))
if (moveSteps == 0) { if (moveSteps == 0) {
// some apps don't return any text via input connection, and the cursor can't be moved // 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 // 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 return true
} }
} else { } else {
var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint val text = connection.getTextAfterCursor(steps * 4, 0) ?: return false
val text = inputLogic.mConnection.getTextAfterCursor(steps * 4, 0) ?: return false moveSteps = positiveMoveSteps(text, steps)
loopOverCodePoints(text) {
if (StringUtils.mightBeEmoji(it)) {
actualSteps = 0
return@loopOverCodePoints true
}
actualSteps += Character.charCount(it)
actualSteps >= steps
}
moveSteps = text.length.coerceAtMost(actualSteps)
if (moveSteps == 0) { 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) { repeat(steps) {
onCodeInput(KeyCode.ARROW_RIGHT, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false) 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)) { if (inputLogic.moveCursorByAndReturnIfInsideComposingWord(moveSteps)) {
// no need to finish input and restart suggestions if we're still in the word // no need to finish input and restart suggestions if we're still in the word
// this is a noticeable performance improvement // this is a noticeable performance improvement
val newPosition = inputLogic.mConnection.expectedSelectionStart + moveSteps val newPosition = connection.expectedSelectionStart + moveSteps
inputLogic.mConnection.setSelection(newPosition, newPosition) connection.setSelection(newPosition, newPosition)
return true return true
} }
inputLogic.finishInput() inputLogic.finishInput()
val newPosition = inputLogic.mConnection.expectedSelectionStart + moveSteps val newPosition = connection.expectedSelectionStart + moveSteps
inputLogic.mConnection.setSelection(newPosition, newPosition) connection.setSelection(newPosition, newPosition)
inputLogic.restartSuggestionsOnWordTouchedByCursor(settings.current, keyboardSwitcher.currentKeyboardScript) inputLogic.restartSuggestionsOnWordTouchedByCursor(settings.current, keyboardSwitcher.currentKeyboardScript)
return true 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)
}
} }

View file

@ -9,64 +9,61 @@ import helium314.keyboard.latin.settings.SpacingAndPunctuations
import java.math.BigInteger import java.math.BigInteger
import java.util.Locale import java.util.Locale
fun loopOverCodePoints(s: CharSequence, run: (Int) -> Boolean) { inline fun loopOverCodePoints(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) {
val text = if (s is String) s else s.toString() val s = text.toString()
var offset = 0 var offset = 0
while (offset < text.length) { while (offset < s.length) {
val codepoint = text.codePointAt(offset) val cp = s.codePointAt(offset)
if (run(codepoint)) return val charCount = Character.charCount(cp)
offset += Character.charCount(codepoint) if (loop(cp, charCount)) return
offset += charCount
} }
} }
fun loopOverCodePointsBackwards(s: CharSequence, run: (Int) -> Boolean) { inline fun loopOverCodePointsBackwards(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) {
val text = if (s is String) s else s.toString() val s = text.toString()
var offset = text.length var offset = s.length
while (offset > 0) { while (offset > 0) {
val codepoint = text.codePointBefore(offset) val cp = s.codePointBefore(offset)
if (run(codepoint)) return val charCount = Character.charCount(cp)
offset -= Character.charCount(codepoint) 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 space = false
var nonWordCodePoint = false var nonWordCodePoint = false
loopOverCodePointsBackwards(s) { loopOverCodePointsBackwards(text) { cp, _ ->
if (!space && Character.isWhitespace(it)) if (!space && Character.isWhitespace(cp)) space = true
space = true // treat double quote like a word codepoint for this function (not great, maybe clarify name or extend list of chars?)
// 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(cp) && cp != '"'.code) {
if (!nonWordCodePoint && !spacingAndPunctuations.isWordCodePoint(it) && it != '"'.code)
nonWordCodePoint = true nonWordCodePoint = true
}
space && nonWordCodePoint // stop if both are found 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 { fun hasLetterBeforeLastSpaceBeforeCursor(text: CharSequence): Boolean {
var letter = false loopOverCodePointsBackwards(text) { cp, _ ->
loopOverCodePointsBackwards(s) { if (Character.isWhitespace(cp)) return false
if (Character.isWhitespace(it)) true else if (Character.isLetter(cp)) return true
else if (Character.isLetter(it)) { false // continue
letter = true
true
}
else false
} }
return letter return false
} }
/** get the complete emoji at end of [s], considering that emojis can be joined with ZWJ resulting in different emojis */ /** get the complete emoji at end of [text], considering that emojis can be joined with ZWJ resulting in different emojis */
fun getFullEmojiAtEnd(s: CharSequence): String { fun getFullEmojiAtEnd(text: CharSequence): String {
val text = if (s is String) s else s.toString() val s = text.toString()
var offset = text.length var offset = s.length
while (offset > 0) { while (offset > 0) {
val codepoint = text.codePointBefore(offset) val codepoint = s.codePointBefore(offset)
// stop if codepoint can't be emoji // stop if codepoint can't be emoji
if (!mightBeEmoji(codepoint)) if (!mightBeEmoji(codepoint)) return text.substring(offset)
return text.substring(offset)
offset -= Character.charCount(codepoint) 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 // 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) // and detects single ZWJ as emoji (at least irrelevant for current use of getFullEmojiAtEnd)
offset -= 1 offset -= 1
@ -76,19 +73,17 @@ fun getFullEmojiAtEnd(s: CharSequence): String {
if (codepoint in 0x1F3FB..0x1F3FF) { 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, // 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) // 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)) { if (isEmoji(codepointBefore)) {
offset -= Character.charCount(codepointBefore) offset -= Character.charCount(codepointBefore)
continue continue
} }
} }
// check the whole text after offset // check the whole text after offset
val textToCheck = text.substring(offset) val textToCheck = s.substring(offset)
if (isEmoji(textToCheck)) { if (isEmoji(textToCheck)) return 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 */ /** 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<String> {
sb.append(c) sb.append(c)
} }
} }
if (sb.isNotBlank()) if (sb.isNotBlank()) out.add(sb.toString())
out.add(sb.toString())
return out return out
} }
@ -120,8 +114,7 @@ fun CharSequence.isValidNumber(): Boolean {
} }
fun String.decapitalize(locale: Locale): String { fun String.decapitalize(locale: Locale): String {
if (isEmpty() || !this[0].isUpperCase()) if (isEmpty() || !this[0].isUpperCase()) return this
return this
return replaceFirstChar { it.lowercase(locale) } 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(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) fun String.splitOnWhitespace() = split(whitespaceSplitRegex)