mirror of
https://gitlab.futo.org/keyboard/latinime.git
synced 2024-09-28 14:54:30 +01:00
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:
parent
ea07319ecb
commit
968b39af24
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user