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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## [Unreleased]

### Added
- Optional swipe-up and swipe-down shortcut menus, aligned with the top and bottom character rows and editable through Secondary layouts. The upper gesture follows the number row when shown. (#156)

### Fixed
- Cancel queued glide updates when a swipe shortcut menu takes over the touch gesture. (#156)

### Reliability & testing
- Exercise swipe menus with glide detection both enabled and disabled, including completed glide paths, pending timers and recovery after cancellation; clarify popup input completion and coordinate spaces. (#156)

### Upstream
- Based on LeanBitLab/LeanType commit `2a9d5fecb67c12b834ba2a1e80ffcdc8ab7d6720`; upstream release history remains in `docs/releasenote/`.

Expand Down
11 changes: 11 additions & 0 deletions app/src/main/assets/layouts/swipe_down/swipe_down.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[
[
{ "label": "left" },
{ "label": "right" },
{ "label": "word_left" },
{ "label": "word_right" },
{ "label": "clipboard" },
{ "label": "numpad" },
{ "label": "emoji" }
]
]
11 changes: 11 additions & 0 deletions app/src/main/assets/layouts/swipe_up/swipe_up.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[
[
{ "label": "undo" },
{ "label": "redo" },
{ "label": "cut" },
{ "label": "copy" },
{ "label": "paste" },
{ "label": "select_all" },
{ "label": "clipboard" }
]
]
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,13 @@ class PopupKeysKeyboardAccessibilityDelegate(
mKeyboardView.onUpEvent(x, y, pointerId, eventTime)
// TODO: Should fix this reference. This is a hack to clear the state of
// {@link PointerTracker}.
PointerTracker.dismissAllPopupKeysPanels()
PointerTracker.finishAllPopupKeysInput()
return
}
// Close the popup keys keyboard.
// TODO: Should fix this reference. This is a hack to clear the state of
// {@link PointerTracker}.
PointerTracker.dismissAllPopupKeysPanels()
PointerTracker.finishAllPopupKeysInput()
}

companion object {
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/helium314/keyboard/keyboard/Keyboard.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ open class Keyboard {
val mAltCodeKeysWhileTyping: List<Key>
val mIconsSet: KeyboardIconsSet

internal val swipeShortcutRows by lazy { SwipeShortcutMenu.findRows(this) }

private val mKeyCache = SparseArray<Key?>()

private val mProximityInfo: ProximityInfo
Expand Down
49 changes: 47 additions & 2 deletions app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import helium314.keyboard.latin.settings.DebugSettings
import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.*
import helium314.keyboard.settings.FeedbackManager
import java.util.Locale
import java.util.WeakHashMap

Expand Down Expand Up @@ -292,6 +293,48 @@ class MainKeyboardView @JvmOverloads constructor(
return popupKeysKeyboardView
}

override fun showSwipeShortcutMenu(
tracker: PointerTracker, direction: SwipeShortcutMenu.Direction
): PopupKeysPanel? {
val sourceKeyboard = keyboard ?: return null
val sourceRow = sourceKeyboard.swipeShortcutRows[direction] ?: return null
val menu = try {
SwipeShortcutMenu.Builder(context, sourceKeyboard, sourceRow, direction).build()
} catch (e: IllegalArgumentException) {
reportInvalidSwipeLayout(direction, e)
return null
} catch (e: IllegalStateException) {
reportInvalidSwipeLayout(direction, e)
return null
}
val container = mPopupKeysKeyboardContainer
container.findViewById<View>(R.id.description_view)?.visibility = View.GONE
val popup = container.findViewById<PopupKeysKeyboardView>(R.id.popup_keys_keyboard_view)
popup.setKeyboard(menu)
container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
container.layout(0, 0, container.measuredWidth, container.measuredHeight)
val downInView = CoordinateUtils.newInstance()
tracker.getDownCoordinates(downInView)
val menuTopInKeyboard = if (direction == SwipeShortcutMenu.Direction.UP)
sourceRow.top - menu.mOccupiedHeight - sourceKeyboard.mVerticalGap
else sourceRow.bottom + sourceKeyboard.mVerticalGap
popup.showSwipeShortcutPanel(
parentView = this,
controller = this,
keysLeftInParent = paddingLeft + sourceRow.left,
keysTopInParent = paddingTop + menuTopInKeyboard,
direction = direction,
startYInParent = CoordinateUtils.y(downInView),
listener = mKeyboardActionListener
)
return popup
}

private fun reportInvalidSwipeLayout(direction: SwipeShortcutMenu.Direction, error: RuntimeException) {
Log.e("SwipeShortcutMenu", "Invalid ${direction.layoutType} layout", error)
FeedbackManager.message(context, context.getString(R.string.layout_error, error.message))
}

fun isInDraggingFinger(): Boolean = isShowingPopupKeysPanel() || PointerTracker.isAnyInDraggingFinger()

override fun onShowPopupKeysPanel(panel: PopupKeysPanel) {
Expand All @@ -304,7 +347,7 @@ class MainKeyboardView @JvmOverloads constructor(
}

fun isShowingPopupKeysPanel(): Boolean = mPopupKeysPanel?.isShowingInParent == true
override fun onCancelPopupKeysPanel() { PointerTracker.dismissAllPopupKeysPanels() }
override fun onCancelPopupKeysPanel() { PointerTracker.finishAllPopupKeysInput() }
override fun onDismissPopupKeysPanel() { if (isShowingPopupKeysPanel()) { mPopupKeysPanel?.removeFromParent(); mPopupKeysPanel = null } }

fun startDoubleTapShiftKeyTimer() { mTimerHandler.startDoubleTapShiftKeyTimer() }
Expand All @@ -315,6 +358,7 @@ class MainKeyboardView @JvmOverloads constructor(
override fun onTouchEvent(event: MotionEvent): Boolean {
if (keyboard == null) return false
if (mNonDistinctMultitouchHelper != null) {
if (event.actionMasked == MotionEvent.ACTION_POINTER_DOWN) PointerTracker.cancelSwipeShortcutMenus()
if (event.pointerCount > 1 && mTimerHandler.isInKeyRepeat()) mTimerHandler.cancelKeyRepeatTimers()
mNonDistinctMultitouchHelper.processMotionEvent(event, mKeyDetector)
return true
Expand All @@ -323,6 +367,7 @@ class MainKeyboardView @JvmOverloads constructor(
}

fun processMotionEvent(event: MotionEvent): Boolean {
if (event.actionMasked == MotionEvent.ACTION_POINTER_DOWN) PointerTracker.cancelSwipeShortcutMenus()
val index = event.actionIndex
val id = event.getPointerId(index)
val tracker = PointerTracker.getPointerTracker(id)
Expand All @@ -337,7 +382,7 @@ class MainKeyboardView @JvmOverloads constructor(
PointerTracker.setReleasedKeyGraphicsToAllKeys()
mGestureFloatingTextDrawingPreview.dismissGestureFloatingPreviewText()
mSlidingKeyInputDrawingPreview.dismissSlidingKeyInputPreview()
PointerTracker.dismissAllPopupKeysPanels()
PointerTracker.finishAllPopupKeysInput()
dismissAllKeyPreviews()
PointerTracker.cancelAllPointerTrackers()
}
Expand Down
87 changes: 77 additions & 10 deletions app/src/main/java/helium314/keyboard/keyboard/PointerTracker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ class PointerTracker private constructor(
private var mIsAllowedDraggingFinger = false
// true if a keyswipe gesture is enabled and warranted.
private var mKeySwipeAllowed = false
// The candidate is cleared when another gesture wins; an active menu consumes the pointer.
private var mSwipeShortcutCandidateKey: Key? = null
private var mSwipeShortcutActive = false

private var mInTouchpadMode = false
private var mTouchpadLastX = 0
Expand Down Expand Up @@ -230,6 +233,7 @@ class PointerTracker private constructor(
if (keyDetector === mKeyDetector && keyboard === mKeyboard) {
return
}
cancelSwipeShortcut()
val oldKeyboard = mKeyboard
if (oldKeyboard != null) {
// changing keyboards may change height
Expand Down Expand Up @@ -361,7 +365,7 @@ class PointerTracker private constructor(
Log.d(TAG, String.format(Locale.US, "[%d] onStartBatchInput", mPointerId))
}
sListener.onStartBatchInput()
dismissAllPopupKeysPanels()
finishAllPopupKeysInput()
getTimerProxy().cancelLongPressTimersOf(this)
}

Expand Down Expand Up @@ -471,6 +475,7 @@ class PointerTracker private constructor(
}

val key = getKeyOn(x, y)
cancelSwipeShortcutMenus()
mBogusMoveEventDetector.onActualDownEvent(x, y)
if (key != null && key.isModifier()) {
if (sInGesture) {
Expand Down Expand Up @@ -502,14 +507,25 @@ class PointerTracker private constructor(

fun isShowingPopupKeysPanel(): Boolean = mPopupKeysPanel != null

private fun dismissPopupKeysPanel() {
private fun finishPopupKeysInput() {
if (mSwipeShortcutActive) {
mSwipeShortcutActive = false
// Dismissing or committing a menu must never fall through to typing its source key.
mIsTrackingForActionDisabled = true
}
if (isShowingPopupKeysPanel()) {
mPopupKeysPanel?.dismissPopupKeysPanel()
mPopupKeysPanel = null
}
}

private fun cancelSwipeShortcut() {
mSwipeShortcutCandidateKey = null
if (mSwipeShortcutActive) finishPopupKeysInput()
}

private fun onDownEventInternal(x: Int, y: Int, eventTime: Long) {
mSwipeShortcutCandidateKey = null
var key = onDownKey(x, y, eventTime)
val isEmojiClipBottomRow = mKeyboard?.mId?.isEmojiClipBottomRow == true
mIsAllowedDraggingFinger = (sParams?.mKeySelectionByDraggingFinger == true) ||
Expand Down Expand Up @@ -538,6 +554,11 @@ class PointerTracker private constructor(
mStartX = x
mStartY = y
mStartTime = System.currentTimeMillis()
val settings = Settings.getValues()
if (getActivePointerTrackerCount() == 1 && !mKeySwipeAllowed &&
(settings.mSwipeUpMenuEnabled || settings.mSwipeDownMenuEnabled)) {
mSwipeShortcutCandidateKey = key
}
}
}

Expand Down Expand Up @@ -608,15 +629,18 @@ class PointerTracker private constructor(
return
}

if (sGestureEnabler.shouldHandleGesture() && me != null) {
// Add historical points to gesture path.
if (!isShowingPopupKeysPanel() && me != null &&
(mSwipeShortcutCandidateKey != null || sGestureEnabler.shouldHandleGesture())) {
// Let the earliest decisive sample choose between shortcuts and glide.
val pointerIndex = me.findPointerIndex(mPointerId)
val historicalSize = me.historySize
for (h in 0 until historicalSize) {
val historicalX = me.getHistoricalX(pointerIndex, h).toInt()
val historicalY = me.getHistoricalY(pointerIndex, h).toInt()
val historicalTime = me.getHistoricalEventTime(h)
onGestureMoveEvent(historicalX, historicalY, historicalTime, false, null)
if (tryStartSwipeShortcut(historicalX, historicalY, historicalTime)) break
if (sGestureEnabler.shouldHandleGesture())
onGestureMoveEvent(historicalX, historicalY, historicalTime, false, null)
}
}

Expand All @@ -631,9 +655,41 @@ class PointerTracker private constructor(
}
return
}
if (tryStartSwipeShortcut(x, y, eventTime)) return
onMoveEventInternal(x, y, eventTime)
}

private fun tryStartSwipeShortcut(x: Int, y: Int, eventTime: Long): Boolean {
val source = mSwipeShortcutCandidateKey ?: return false
if (sInGesture || sInKeySwipe || isShowingPopupKeysPanel() ||
getActivePointerTrackerCount() != 1 || mIsTrackingForActionDisabled) {
mSwipeShortcutCandidateKey = null
return false
}
val dx = x - CoordinateUtils.x(mDownCoordinates)
val dy = y - CoordinateUtils.y(mDownCoordinates)
val threshold = maxOf(2 * sPointerStep, source.height / 4)
if (abs(dx) < threshold && abs(dy) < threshold) return false
mSwipeShortcutCandidateKey = null
if (abs(dy) <= abs(dx)) return false
val direction = if (dy < 0) SwipeShortcutMenu.Direction.UP else SwipeShortcutMenu.Direction.DOWN
val settings = Settings.getValues()
if (direction == SwipeShortcutMenu.Direction.UP && !settings.mSwipeUpMenuEnabled ||
direction == SwipeShortcutMenu.Direction.DOWN && !settings.mSwipeDownMenuEnabled) return false
val row = mKeyboard?.swipeShortcutRows?.get(direction) ?: return false
if (source !in row.keys) return false
val panel = sDrawingProxy?.showSwipeShortcutMenu(this, direction) ?: return false
getTimerProxy().cancelKeyTimersOf(this)
getTimerProxy().cancelUpdateBatchInputTimer(this)
mIsDetectingGesture = false
setReleasedKeyGraphics(mCurrentKey, true)
mCurrentKey = null
mSwipeShortcutActive = true
mPopupKeysPanel = panel
panel.onDownEvent(panel.translateX(x), panel.translateY(y), mPointerId, eventTime)
return true
}

private fun processDraggingFingerInToNewKey(newKey: Key, x: Int, y: Int, eventTime: Long) {
var key: Key? = newKey
if (callListenerOnPressAndCheckKeyboardLayoutChange(newKey, 0)) {
Expand Down Expand Up @@ -858,6 +914,7 @@ class PointerTracker private constructor(
printTouchEvent("onUpEvent :", x, y, eventTime)
}

tryStartSwipeShortcut(x, y, eventTime)
getTimerProxy().cancelUpdateBatchInputTimer(this)
if (!sInGesture) {
if (mCurrentKey?.isModifier() == true) {
Expand All @@ -879,6 +936,7 @@ class PointerTracker private constructor(
}

private fun onUpEventInternal(x: Int, y: Int, eventTime: Long) {
mSwipeShortcutCandidateKey = null
getTimerProxy().cancelKeyTimersOf(this)
val isInDraggingFinger = mIsInDraggingFinger
val isInSlidingKeyInput = mIsInSlidingKeyInput
Expand All @@ -903,7 +961,7 @@ class PointerTracker private constructor(
panel.onUpEvent(translatedX, translatedY, mPointerId, eventTime)
}
}
dismissPopupKeysPanel()
finishPopupKeysInput()
if (isInSlidingKeyInput) {
callListenerOnFinishSlidingInput()
}
Expand Down Expand Up @@ -955,7 +1013,8 @@ class PointerTracker private constructor(
}

override fun cancelTrackingForAction() {
if (isShowingPopupKeysPanel()) {
mSwipeShortcutCandidateKey = null
if (isShowingPopupKeysPanel() && !mSwipeShortcutActive) {
return
}
mIsTrackingForActionDisabled = true
Expand All @@ -964,6 +1023,7 @@ class PointerTracker private constructor(
val isInOperation: Boolean get() = !mIsTrackingForActionDisabled

fun onLongPressed() {
mSwipeShortcutCandidateKey = null
getTimerProxy().cancelLongPressTimersOf(this)
if (isShowingPopupKeysPanel()) {
return
Expand Down Expand Up @@ -1028,7 +1088,7 @@ class PointerTracker private constructor(
getTimerProxy().cancelKeyTimersOf(this)
setReleasedKeyGraphics(mCurrentKey, true)
resetKeySelectionByDraggingFinger()
dismissPopupKeysPanel()
finishPopupKeysInput()
}

private fun isMajorEnoughMoveToBeOnNewKey(
Expand Down Expand Up @@ -1118,6 +1178,7 @@ class PointerTracker private constructor(
}

fun onKeyRepeat(code: Int, repeatCount: Int) {
mSwipeShortcutCandidateKey = null
val key = key
if (key == null || key.code != code) {
mCurrentRepeatingKeyCode = Constants.NOT_A_CODE
Expand Down Expand Up @@ -1270,6 +1331,12 @@ class PointerTracker private constructor(

fun isAnyInDraggingFinger(): Boolean = sPointerTrackerQueue.isAnyInDraggingFinger()

fun cancelSwipeShortcutMenus() {
for (tracker in sTrackers) {
tracker.cancelSwipeShortcut()
}
}

fun cancelAllPointerTrackers() {
sPointerTrackerQueue.cancelAllPointerTrackers()
}
Expand All @@ -1296,11 +1363,11 @@ class PointerTracker private constructor(
}
}

fun dismissAllPopupKeysPanels() {
fun finishAllPopupKeysInput() {
val trackersSize = sTrackers.size
for (i in 0 until trackersSize) {
val tracker = sTrackers[i]
tracker.dismissPopupKeysPanel()
tracker.finishPopupKeysInput()
}
}

Expand Down
Loading
Loading