Improve text selection logic

* Shift + swiping space now lets you select text
* Text editor uses more consistent selection logic instead of sending dpad and relying on apps to implement shift+dpad selection, which many of them don't. Dpad is still used for up/down
This commit is contained in:
Aleksandras Kostarevas 2024-01-10 18:42:20 +02:00
parent ea07319ecb
commit 968b39af24
6 changed files with 231 additions and 27 deletions

View File

@ -1369,21 +1369,17 @@ public class LatinIMELegacy implements KeyboardActionListener,
@Override
public void onMovePointer(int steps) {
if (mInputLogic.mConnection.hasCursorPosition()) {
if (TextUtils.getLayoutDirectionFromLocale(mLocale) == View.LAYOUT_DIRECTION_RTL)
steps = -steps;
int shiftMode = mKeyboardSwitcher.getKeyboardShiftMode();
boolean select = (shiftMode == WordComposer.CAPS_MODE_MANUAL_SHIFTED) || (shiftMode == WordComposer.CAPS_MODE_MANUAL_SHIFT_LOCKED);
steps = mInputLogic.mConnection.getUnicodeSteps(steps, true);
final int end = mInputLogic.mConnection.getExpectedSelectionEnd() + steps;
final int start = mInputLogic.mConnection.hasSelection() ? mInputLogic.mConnection.getExpectedSelectionStart() : end;
if(select) {
mInputLogic.disableRecapitalization();
}
mInputLogic.finishInput();
mInputLogic.mConnection.setSelection(start, end);
if(steps < 0) {
mInputLogic.cursorLeft(steps, false, select);
} else {
for (; steps < 0; steps++)
mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, 0);
for (; steps > 0; steps--)
mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, 0);
mInputLogic.cursorRight(steps, false, select);
}
}

View File

@ -1081,4 +1081,80 @@ public final class RichInputConnection implements PrivateCommandPerformer {
}
return steps;
}
private int getCharacterClass(char c) {
if(Character.isLetter(c) || c == '_') return 1;
else if(Character.isDigit(c)) return 2;
else if(Character.isWhitespace(c)) return 3;
else return 4;
}
/**
* Gets number of steps needed to step by a whole word
* @param direction direction to step, only sign is checked
* @param rightSidePointer whether or not right side is fixed
* @return number of characters to step
*/
public int getWordBoundarySteps(int direction, boolean rightSidePointer) {
int steps = 0;
if (direction < 0) {
CharSequence charsBeforeCursor = !rightSidePointer && hasSelection() ?
getSelectedText(0) :
getTextBeforeCursor(64, 0);
if (charsBeforeCursor != null) {
int i = charsBeforeCursor.length() - 1;
// Skip trailing whitespace
while (i >= 0 && Character.isWhitespace(charsBeforeCursor.charAt(i))) {
i--;
steps--;
}
// Find the last word boundary
int charClass = getCharacterClass(charsBeforeCursor.charAt(i));
while (i >= 0 && getCharacterClass(charsBeforeCursor.charAt(i)) == charClass) {
i--;
steps--;
}
if(!rightSidePointer) {
// Skip initial whitespace
while (i >= 0 && Character.isWhitespace(charsBeforeCursor.charAt(i))) {
i--;
steps--;
}
}
}
} else if (direction > 0) {
CharSequence charsAfterCursor = rightSidePointer && hasSelection() ?
getSelectedText(0) :
getTextAfterCursor(64, 0);
if (charsAfterCursor != null) {
int i = 0;
// Skip initial whitespace
while (i < charsAfterCursor.length() && Character.isWhitespace(charsAfterCursor.charAt(i))) {
i++;
steps++;
}
// Find the first word boundary
int charClass = getCharacterClass(charsAfterCursor.charAt(i));
while (i < charsAfterCursor.length() && getCharacterClass(charsAfterCursor.charAt(i)) == charClass) {
i++;
steps++;
}
if(rightSidePointer) {
// Skip trailing whitespace
while (i < charsAfterCursor.length() && Character.isWhitespace(charsAfterCursor.charAt(i))) {
i++;
steps++;
}
}
}
}
return steps;
}
}

View File

@ -2394,4 +2394,98 @@ public final class InputLogic {
public int getComposingLength() {
return mWordComposer.size();
}
/**
* Which direction the selection should be expanded/contracted in via cursorLeft/Right methods
* If true, the right side of the selection (the end) is fixed and the left side (start) gets
* moved around, and vice versa.
*/
private boolean isRightSidePointer = true;
/**
* Shifts the cursor/selection based on isRightSidePointer and the parameters
* @param steps How many characters to step over, or the direction if stepOverWords
* @param stepOverWords Whether to ignore the magnitude of steps and step over full words
* @param select Whether or not to start/continue selection
*/
private void cursorStep(int steps, boolean stepOverWords, boolean select) {
if(stepOverWords) {
steps = mConnection.getWordBoundarySteps(steps, isRightSidePointer);
} else {
steps = mConnection.getUnicodeSteps(steps, isRightSidePointer);
}
int cursor = isRightSidePointer ? mConnection.getExpectedSelectionStart() : mConnection.getExpectedSelectionEnd();
int start = mConnection.getExpectedSelectionStart();
int end = mConnection.getExpectedSelectionEnd();
if(isRightSidePointer) {
start += steps;
cursor += steps;
} else {
end += steps;
cursor += steps;
}
if (!select) {
start = cursor;
end = cursor;
}
mConnection.setSelection(start, end);
}
/**
* Disables recapitalization
*/
public void disableRecapitalization() {
mRecapitalizeStatus.disable();
}
/**
* Shifts the cursor left by a number of characters
* @param steps How many characters to step over, or the direction if stepOverWords
* @param stepOverWords Whether to ignore the magnitude of steps and step over full words
* @param select Whether or not to start/continue selection
*/
public void cursorLeft(int steps, boolean stepOverWords, boolean select) {
steps = Math.abs(steps);
if(!mConnection.hasCursorPosition()) {
int meta = 0;
if(stepOverWords) meta = meta | KeyEvent.META_CTRL_ON;
if(select) meta = meta | KeyEvent.META_SHIFT_ON;
for(int i=0; i<steps; i++)
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, meta);
}
finishInput();
if(!mConnection.hasSelection()) isRightSidePointer = true;
cursorStep(-steps, stepOverWords, select);
}
/**
* Shifts the cursor right by a number of characters
* @param steps How many characters to step over, or the direction if stepOverWords
* @param stepOverWords Whether to ignore the magnitude of steps and step over full words
* @param select Whether or not to start/continue selection
*/
public void cursorRight(int steps, boolean stepOverWords, boolean select) {
steps = Math.abs(steps);
if(!mConnection.hasCursorPosition()) {
int meta = 0;
if(stepOverWords) meta = meta | KeyEvent.META_CTRL_ON;
if(select) meta = meta | KeyEvent.META_SHIFT_ON;
for(int i=0; i<steps; i++)
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, meta);
}
finishInput();
if(!mConnection.hasSelection()) isRightSidePointer = false;
cursorStep(steps, stepOverWords, select);
}
}

View File

@ -32,6 +32,9 @@ interface KeyboardManagerForAction {
fun sendCodePointEvent(codePoint: Int)
fun sendKeyEvent(keyCode: Int, metaState: Int)
fun cursorLeft(steps: Int, stepOverWords: Boolean, select: Boolean)
fun cursorRight(steps: Int, stepOverWords: Boolean, select: Boolean)
}
interface ActionWindow {

View File

@ -117,6 +117,14 @@ class UixActionKeyboardManager(val uixManager: UixManager, val latinIME: LatinIM
override fun sendKeyEvent(keyCode: Int, metaState: Int) {
latinIME.inputLogic.sendDownUpKeyEvent(keyCode, metaState)
}
override fun cursorLeft(steps: Int, stepOverWords: Boolean, select: Boolean) {
latinIME.inputLogic.cursorLeft(steps, stepOverWords, select)
}
override fun cursorRight(steps: Int, stepOverWords: Boolean, select: Boolean) {
latinIME.inputLogic.cursorRight(steps, stepOverWords, select)
}
}
class UixManager(private val latinIME: LatinIME) {

View File

@ -130,13 +130,16 @@ fun ActionKey(
}
@Composable
fun ArrowKeys(modifier: Modifier, sendEvent: (Int) -> Unit) {
fun ArrowKeys(
modifier: Modifier,
moveCursor: (direction: Direction) -> Unit
) {
Row(modifier = modifier) {
ActionKey(
modifier = Modifier
.weight(1.0f)
.fillMaxHeight(),
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_LEFT) }
onTrigger = { moveCursor(Direction.Left) }
) {
IconWithColor(
iconId = R.drawable.arrow_left,
@ -151,7 +154,7 @@ fun ArrowKeys(modifier: Modifier, sendEvent: (Int) -> Unit) {
modifier = Modifier
.weight(1.0f)
.fillMaxWidth(),
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_UP) }
onTrigger = { moveCursor(Direction.Up) }
) {
IconWithColor(
iconId = R.drawable.arrow_up,
@ -164,7 +167,7 @@ fun ArrowKeys(modifier: Modifier, sendEvent: (Int) -> Unit) {
modifier = Modifier
.weight(1.0f)
.fillMaxWidth(),
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_DOWN) }
onTrigger = { moveCursor(Direction.Down) }
) {
IconWithColor(
iconId = R.drawable.arrow_down,
@ -177,7 +180,7 @@ fun ArrowKeys(modifier: Modifier, sendEvent: (Int) -> Unit) {
modifier = Modifier
.weight(1.0f)
.fillMaxHeight(),
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_RIGHT) }
onTrigger = { moveCursor(Direction.Right) }
) {
IconWithColor(
iconId = R.drawable.arrow_right,
@ -299,16 +302,24 @@ fun SideKeys(modifier: Modifier, onEvent: (Int, Int) -> Unit, onCodePoint: (Int)
}
}
enum class Direction {
Left,
Right,
Up,
Down
}
@Composable
fun TextEditScreen(onCodePoint: (Int) -> Unit, onEvent: (Int, Int) -> Unit, keyboardShown: Boolean) {
fun TextEditScreen(
onCodePoint: (Int) -> Unit,
onEvent: (Int, Int) -> Unit,
moveCursor: (direction: Direction, ctrl: Boolean, shift: Boolean) -> Unit,
keyboardShown: Boolean
) {
val shiftState = remember { mutableStateOf(false) }
val ctrlState = remember { mutableStateOf(false) }
val metaState = 0 or
(if(shiftState.value) { KeyEvent.META_SHIFT_ON } else { 0 }) or
(if(ctrlState.value) { KeyEvent.META_CTRL_ON } else { 0 })
val sendEvent = { keycode: Int -> onEvent(keycode, metaState) }
val sendMoveCursor = { direction: Direction -> moveCursor(direction, ctrlState.value, shiftState.value) }
Row(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier
@ -318,7 +329,7 @@ fun TextEditScreen(onCodePoint: (Int) -> Unit, onEvent: (Int, Int) -> Unit, keyb
modifier = Modifier
.weight(3.0f)
.fillMaxWidth(),
sendEvent = sendEvent
moveCursor = sendMoveCursor
)
CtrlShiftMetaKeys(
modifier = Modifier
@ -354,7 +365,23 @@ val TextEditAction = Action(
@Composable
override fun WindowContents(keyboardShown: Boolean) {
TextEditScreen(onCodePoint = { a -> manager.sendCodePointEvent(a)}, onEvent = { a, b -> manager.sendKeyEvent(a, b) }, keyboardShown = keyboardShown)
TextEditScreen(
onCodePoint = { a -> manager.sendCodePointEvent(a)},
onEvent = { a, b -> manager.sendKeyEvent(a, b) },
moveCursor = { direction, ctrl, shift ->
val keyEventMetaState = 0 or
(if(shift) { KeyEvent.META_SHIFT_ON } else { 0 }) or
(if(ctrl) { KeyEvent.META_CTRL_ON } else { 0 })
when(direction) {
Direction.Left -> manager.cursorLeft(1, stepOverWords = ctrl, select = shift)
Direction.Right -> manager.cursorRight(1, stepOverWords = ctrl, select = shift)
Direction.Up -> manager.sendKeyEvent(KeyEvent.KEYCODE_DPAD_UP, keyEventMetaState)
Direction.Down -> manager.sendKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, keyEventMetaState)
}
},
keyboardShown = keyboardShown
)
}
override fun close() {
@ -367,13 +394,13 @@ val TextEditAction = Action(
@Preview(showBackground = true)
fun TextEditScreenPreview() {
Surface(modifier = Modifier.height(256.dp)) {
TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> }, keyboardShown = false)
TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> }, moveCursor = { _, _, _ -> }, keyboardShown = false)
}
}
@Composable
@Preview(showBackground = true)
fun TextEditScreenPreviewWithKb() {
Surface(modifier = Modifier.height(256.dp)) {
TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> }, keyboardShown = true)
TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> }, moveCursor = { _, _, _ -> }, keyboardShown = true)
}
}