move cursor in whole codepoints on space / delete swipe

and when space-swiping across emoji, send fake arrow keypress instead (discouraged, but avoids e.g. splitting pirate flag emoji and thus justified)
fixes #859
This commit is contained in:
Helium314 2024-06-12 22:46:42 +02:00
parent 5b1f40f0f6
commit d91350524a
3 changed files with 47 additions and 13 deletions

View file

@ -6,6 +6,9 @@ import helium314.keyboard.latin.LatinIME
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
import helium314.keyboard.latin.common.StringUtils
import helium314.keyboard.latin.common.loopOverCodePoints
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
@ -80,7 +83,23 @@ 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 = inputLogic.mConnection.expectedSelectionEnd
val start = inputLogic.mConnection.expectedSelectionStart + steps 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)
actualSteps >= steps
}
} else {
val text = inputLogic.mConnection.getTextBeforeCursor(-steps * 4, 0)
if (text == null) actualSteps = steps
else loopOverCodePointsBackwards(text) {
actualSteps -= Character.charCount(it)
actualSteps <= steps
}
}
val start = inputLogic.mConnection.expectedSelectionStart + actualSteps
if (start > end) return if (start > end) return
inputLogic.mConnection.setSelection(start, end) inputLogic.mConnection.setSelection(start, end)
} }
@ -120,29 +139,44 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
private fun onMoveCursorHorizontally(rawSteps: Int): Boolean { private fun onMoveCursorHorizontally(rawSteps: Int): Boolean {
if (rawSteps == 0) return false if (rawSteps == 0) return false
var steps = rawSteps
// for RTL languages we want to invert pointer movement // for RTL languages we want to invert pointer movement
if (RichInputMethodManager.getInstance().currentSubtype.isRtlSubtype) steps = -steps val steps = if (RichInputMethodManager.getInstance().currentSubtype.isRtlSubtype) -rawSteps else rawSteps
val moveSteps: Int val moveSteps: Int
if (steps < 0) { if (steps < 0) {
val availableCharacters = inputLogic.mConnection.getTextBeforeCursor(64, 0)?.length ?: return false var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint
moveSteps = if (availableCharacters < -steps) -availableCharacters else steps 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))
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
while (steps != 0) { repeat(-steps) {
onCodeInput(KeyCode.ARROW_LEFT, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false) onCodeInput(KeyCode.ARROW_LEFT, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
++steps
} }
return true return true
} }
} else { } else {
val availableCharacters = inputLogic.mConnection.getTextAfterCursor(64, 0)?.length ?: return false var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint
moveSteps = availableCharacters.coerceAtMost(steps) 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)
if (moveSteps == 0) { if (moveSteps == 0) {
while (steps != 0) { 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)
--steps
} }
return true return true
} }

View file

@ -625,7 +625,7 @@ public final class StringUtils {
return false; return false;
} }
public static boolean mightBeEmoji(final String s) { public static boolean mightBeEmoji(final CharSequence s) {
int offset = 0; int offset = 0;
final int length = s.length(); final int length = s.length();
while (offset < length) { while (offset < length) {

View file

@ -109,7 +109,7 @@ fun String.decapitalize(locale: Locale): String {
fun isEmoji(c: Int): Boolean = mightBeEmoji(c) && isEmoji(newSingleCodePointString(c)) fun isEmoji(c: Int): Boolean = mightBeEmoji(c) && isEmoji(newSingleCodePointString(c))
fun isEmoji(s: String): Boolean = mightBeEmoji(s) && s.matches(emoRegex) fun isEmoji(s: CharSequence): Boolean = mightBeEmoji(s) && s.matches(emoRegex)
fun String.splitOnWhitespace() = split(whitespaceSplitRegex) fun String.splitOnWhitespace() = split(whitespaceSplitRegex)