Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -527,11 +527,68 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
deleteSwipeStepCount = 0
}

// Vertical movement is delegated to the editor, so the selection always ends exactly one
// visual (wrapped) line above/below, like the arrow keys do. If the editor can't do that,
// we fall back to the approximated line boundaries below.
val movedByEditor = stepsY != 0 && moveDeleteSelectionByEditorLines(stepsY)
val remainingStepsY = if (movedByEditor) 0 else stepsY
if (movedByEditor && stepsX == 0) return

if (settings.current.mDeleteSwipeWordByWord) {
onMoveDeletePointerWords2D(stepsX, stepsY)
onMoveDeletePointerWords2D(stepsX, remainingStepsY)
} else {
onMoveDeletePointerChars2D(stepsX, stepsY)
onMoveDeletePointerChars2D(stepsX, remainingStepsY)
}
}

/**
* Moves the left end of the delete-swipe selection by whole editor lines (negative = up, positive = down),
* using Shift+DPAD_UP/DOWN on a reversed selection (anchor = right end, extent = left end).
* Afterwards the horizontal state is re-based on the new left end.
* Returns false (and leaves the selection as it was) if the editor does not behave as expected.
*/
private fun moveDeleteSelectionByEditorLines(stepsY: Int): Boolean {
val initialEnd = deleteSwipeInitialEnd
val currentStart = connection.expectedSelectionStart
if (initialEnd < 0 || currentStart < 0 || currentStart > initialEnd) return false

// make the moving end the extent: anchor at the right end, extent at the left end
if (!connection.setSelection(initialEnd, currentStart)) return false

val keyCode = if (stepsY < 0) KeyEvent.KEYCODE_DPAD_UP else KeyEvent.KEYCODE_DPAD_DOWN
repeat(abs(stepsY)) {
val eventTime = android.os.SystemClock.uptimeMillis()
connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0))
connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, keyCode, 0, KeyEvent.META_SHIFT_ON))
connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, keyCode, 0, KeyEvent.META_SHIFT_ON))
connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0))
}

val selection = connection.getEditorSelection()
if (selection == null || selection.first != initialEnd) {
// editor doesn't support extracted text or reversed selections: restore and use the fallback
connection.setSelection(currentStart, initialEnd)
return false
}

// moving down must not go past the original cursor position
val newStart = selection.second.coerceIn(0, initialEnd)
connection.setSelection(newStart, initialEnd) // normal order again, also updates expected selection

val textBefore = connection.getTextBeforeCursor(2000, 0)?.toString() ?: ""
deleteSwipeWordBoundaries = getWordBoundariesBackwards(textBefore, newStart)
deleteSwipeLineBoundaries = listOf(newStart)
deleteSwipeWordIndex = 0
deleteSwipeLineIndex = 0
deleteSwipeCharOffset = 0

if (newStart != currentStart) {
deleteSwipeStepCount++
if (deleteSwipeStepCount % 2 != 0) {
performHapticFeedback(HapticEvent.GESTURE_MOVE)
}
}
return true
}

private fun onMoveDeletePointerChars2D(stepsX: Int, stepsY: Int) {
Expand Down
31 changes: 30 additions & 1 deletion app/src/main/java/helium314/keyboard/keyboard/PointerTracker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ class PointerTracker private constructor(

// true if keyboard layout has been changed.
private var mKeyboardLayoutHasBeenChanged = false

// True as long as every keyboard change since the flag above was last reset only dropped the one-shot
// (automatic or manual) shift of the same alphabet layout. Such a change must not make us
// re-detect the key on release, see detectAndSendKey.
private var mOnlyShiftStateHasChanged = true
private var keyboardChangeOccupiedHeightDifference = 0

// true if this pointer is no longer triggering any action because it has been canceled.
Expand Down Expand Up @@ -160,6 +165,7 @@ class PointerTracker private constructor(
sListener.onPressKey(key.code, repeatCount, getActivePointerTrackerCount() == 1, HapticEvent.KEY_PRESS)
val keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged
mKeyboardLayoutHasBeenChanged = false
mOnlyShiftStateHasChanged = true
getTimerProxy().startTypingStateTimer(key)
return keyboardLayoutHasBeenChanged
}
Expand Down Expand Up @@ -243,6 +249,9 @@ class PointerTracker private constructor(
// since y is measured from top of view, this change needs to be considered in some places
keyboardChangeOccupiedHeightDifference = keyboard.mOccupiedHeight - oldKeyboard.mOccupiedHeight
}
val onlyShiftStateChanged = oldKeyboard != null && isOnlyShiftStateChange(oldKeyboard, keyboard)
mOnlyShiftStateHasChanged = if (mKeyboardLayoutHasBeenChanged) mOnlyShiftStateHasChanged && onlyShiftStateChanged
else onlyShiftStateChanged
mKeyDetector = keyDetector
mKeyboard = keyboard
// Mark that keyboard layout has been changed.
Expand All @@ -253,6 +262,22 @@ class PointerTracker private constructor(
mBogusMoveEventDetector.setKeyboardGeometry(keyWidth, keyHeight)
}

/**
* True if [newKb] is the same alphabet keyboard as [oldKb], but the one-shot shift (automatic or manual)
* has been dropped. This happens when a second finger touches a key while the first one is still down.
* The opposite direction (e.g. chording with the shift key) is not matched, as re-detecting the key
* is intended there.
*/
private fun isOnlyShiftStateChange(oldKb: Keyboard, newKb: Keyboard): Boolean {
val oldId = oldKb.mId
val newId = newKb.mId
return newId.mElementId == KeyboardId.ELEMENT_ALPHABET
&& (oldId.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED
|| oldId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED)
&& oldId.mSubtype == newId.mSubtype
&& oldId.mHeight == newId.mHeight
}

override fun isInDraggingFinger(): Boolean = mIsInDraggingFinger

val key: Key? get() = mCurrentKey
Expand Down Expand Up @@ -529,6 +554,7 @@ class PointerTracker private constructor(
sInKeySwipe = true
}
mKeyboardLayoutHasBeenChanged = false
mOnlyShiftStateHasChanged = true
mIsTrackingForActionDisabled = false
resetKeySelectionByDraggingFinger()
if (key != null) {
Expand Down Expand Up @@ -1248,7 +1274,10 @@ class PointerTracker private constructor(
return
}

val targetKey = if (mKeyboardLayoutHasBeenChanged && !key.isModifier()) {
// Don't re-detect the key if the layout only changed its shift state after this pointer went down.
// Otherwise typing fast (second key touched before the first one is released) resets the automatic
// shift, and the first key would then be re-detected on the unshifted layout and typed as lower case.
val targetKey = if (mKeyboardLayoutHasBeenChanged && !mOnlyShiftStateHasChanged && !key.isModifier()) {
mKeyDetector.detectHitKey(x, y) ?: key
} else {
key
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/helium314/keyboard/latin/RichInputConnection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,17 @@ class RichInputConnection(private val mParent: InputMethodService) : PrivateComm
return true
}

/**
* Synchronously asks the editor for its current selection as (anchor, extent) in absolute offsets.
* For a reversed selection, extent < anchor. Returns null if the editor does not provide extracted text.
*/
fun getEditorSelection(): Pair<Int, Int>? {
if (!isConnected()) return null
val et = mIC?.getExtractedText(ExtractedTextRequest(), 0) ?: return null
if (et.selectionStart < 0 || et.selectionEnd < 0) return null
return Pair(et.selectionStart + et.startOffset, et.selectionEnd + et.startOffset)
}

private fun reloadCursorPosition() {
if (!isConnected()) return

Expand Down
Loading