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
|
@Override
|
||||||
public void onMovePointer(int steps) {
|
public void onMovePointer(int steps) {
|
||||||
if (mInputLogic.mConnection.hasCursorPosition()) {
|
int shiftMode = mKeyboardSwitcher.getKeyboardShiftMode();
|
||||||
if (TextUtils.getLayoutDirectionFromLocale(mLocale) == View.LAYOUT_DIRECTION_RTL)
|
boolean select = (shiftMode == WordComposer.CAPS_MODE_MANUAL_SHIFTED) || (shiftMode == WordComposer.CAPS_MODE_MANUAL_SHIFT_LOCKED);
|
||||||
steps = -steps;
|
|
||||||
|
|
||||||
steps = mInputLogic.mConnection.getUnicodeSteps(steps, true);
|
if(select) {
|
||||||
final int end = mInputLogic.mConnection.getExpectedSelectionEnd() + steps;
|
mInputLogic.disableRecapitalization();
|
||||||
final int start = mInputLogic.mConnection.hasSelection() ? mInputLogic.mConnection.getExpectedSelectionStart() : end;
|
}
|
||||||
|
|
||||||
mInputLogic.finishInput();
|
if(steps < 0) {
|
||||||
mInputLogic.mConnection.setSelection(start, end);
|
mInputLogic.cursorLeft(steps, false, select);
|
||||||
} else {
|
} else {
|
||||||
for (; steps < 0; steps++)
|
mInputLogic.cursorRight(steps, false, select);
|
||||||
mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, 0);
|
|
||||||
for (; steps > 0; steps--)
|
|
||||||
mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1081,4 +1081,80 @@ public final class RichInputConnection implements PrivateCommandPerformer {
|
|||||||
}
|
}
|
||||||
return steps;
|
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() {
|
public int getComposingLength() {
|
||||||
return mWordComposer.size();
|
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 sendCodePointEvent(codePoint: Int)
|
||||||
fun sendKeyEvent(keyCode: Int, metaState: 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 {
|
interface ActionWindow {
|
||||||
|
@ -117,6 +117,14 @@ class UixActionKeyboardManager(val uixManager: UixManager, val latinIME: LatinIM
|
|||||||
override fun sendKeyEvent(keyCode: Int, metaState: Int) {
|
override fun sendKeyEvent(keyCode: Int, metaState: Int) {
|
||||||
latinIME.inputLogic.sendDownUpKeyEvent(keyCode, metaState)
|
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) {
|
class UixManager(private val latinIME: LatinIME) {
|
||||||
|
@ -130,13 +130,16 @@ fun ActionKey(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ArrowKeys(modifier: Modifier, sendEvent: (Int) -> Unit) {
|
fun ArrowKeys(
|
||||||
|
modifier: Modifier,
|
||||||
|
moveCursor: (direction: Direction) -> Unit
|
||||||
|
) {
|
||||||
Row(modifier = modifier) {
|
Row(modifier = modifier) {
|
||||||
ActionKey(
|
ActionKey(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1.0f)
|
.weight(1.0f)
|
||||||
.fillMaxHeight(),
|
.fillMaxHeight(),
|
||||||
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_LEFT) }
|
onTrigger = { moveCursor(Direction.Left) }
|
||||||
) {
|
) {
|
||||||
IconWithColor(
|
IconWithColor(
|
||||||
iconId = R.drawable.arrow_left,
|
iconId = R.drawable.arrow_left,
|
||||||
@ -151,7 +154,7 @@ fun ArrowKeys(modifier: Modifier, sendEvent: (Int) -> Unit) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1.0f)
|
.weight(1.0f)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_UP) }
|
onTrigger = { moveCursor(Direction.Up) }
|
||||||
) {
|
) {
|
||||||
IconWithColor(
|
IconWithColor(
|
||||||
iconId = R.drawable.arrow_up,
|
iconId = R.drawable.arrow_up,
|
||||||
@ -164,7 +167,7 @@ fun ArrowKeys(modifier: Modifier, sendEvent: (Int) -> Unit) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1.0f)
|
.weight(1.0f)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_DOWN) }
|
onTrigger = { moveCursor(Direction.Down) }
|
||||||
) {
|
) {
|
||||||
IconWithColor(
|
IconWithColor(
|
||||||
iconId = R.drawable.arrow_down,
|
iconId = R.drawable.arrow_down,
|
||||||
@ -177,7 +180,7 @@ fun ArrowKeys(modifier: Modifier, sendEvent: (Int) -> Unit) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1.0f)
|
.weight(1.0f)
|
||||||
.fillMaxHeight(),
|
.fillMaxHeight(),
|
||||||
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_RIGHT) }
|
onTrigger = { moveCursor(Direction.Right) }
|
||||||
) {
|
) {
|
||||||
IconWithColor(
|
IconWithColor(
|
||||||
iconId = R.drawable.arrow_right,
|
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
|
@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 shiftState = remember { mutableStateOf(false) }
|
||||||
val ctrlState = remember { mutableStateOf(false) }
|
val ctrlState = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val metaState = 0 or
|
val sendMoveCursor = { direction: Direction -> moveCursor(direction, ctrlState.value, shiftState.value) }
|
||||||
(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) }
|
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxSize()) {
|
Row(modifier = Modifier.fillMaxSize()) {
|
||||||
Column(modifier = Modifier
|
Column(modifier = Modifier
|
||||||
@ -318,7 +329,7 @@ fun TextEditScreen(onCodePoint: (Int) -> Unit, onEvent: (Int, Int) -> Unit, keyb
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(3.0f)
|
.weight(3.0f)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
sendEvent = sendEvent
|
moveCursor = sendMoveCursor
|
||||||
)
|
)
|
||||||
CtrlShiftMetaKeys(
|
CtrlShiftMetaKeys(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -354,7 +365,23 @@ val TextEditAction = Action(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun WindowContents(keyboardShown: Boolean) {
|
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() {
|
override fun close() {
|
||||||
@ -367,13 +394,13 @@ val TextEditAction = Action(
|
|||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
fun TextEditScreenPreview() {
|
fun TextEditScreenPreview() {
|
||||||
Surface(modifier = Modifier.height(256.dp)) {
|
Surface(modifier = Modifier.height(256.dp)) {
|
||||||
TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> }, keyboardShown = false)
|
TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> }, moveCursor = { _, _, _ -> }, keyboardShown = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Composable
|
@Composable
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
fun TextEditScreenPreviewWithKb() {
|
fun TextEditScreenPreviewWithKb() {
|
||||||
Surface(modifier = Modifier.height(256.dp)) {
|
Surface(modifier = Modifier.height(256.dp)) {
|
||||||
TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> }, keyboardShown = true)
|
TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> }, moveCursor = { _, _, _ -> }, keyboardShown = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user