From 3d9c6fe23ee8010487103cf71e8f0ae1099feb5d Mon Sep 17 00:00:00 2001 From: Sebastian K Date: Sun, 18 Oct 2026 23:03:00 +0200 Subject: [PATCH] feat: add a speed option for spacebar cursor control Spacebar cursor control is now driven by finger movement instead of the long press: the press origin comes from the ACTION_DOWN x, the platform touch slop decides that the finger is swiping (cancelling the pending language dialog), and the configured step decides how much travel one character needs. Previously the first movement event only seeded the origin and any later event more than a fixed 8dp away moved the cursor. Adds one setting under Typing and input, "Cursor movement speed": Slow (24dp), Normal (16dp, default) and Fast (8dp), where Fast reproduces the previous behaviour. Cursor movement now buzzes with KEYBOARD_TAP like the rest of the keyboard, instead of TEXT_HANDLE_MOVE, which produces no vibration on some devices. Refs #377 --- .../keyboard/activities/SettingsActivity.kt | 29 ++++ .../org/fossify/keyboard/helpers/Config.kt | 4 + .../org/fossify/keyboard/helpers/Constants.kt | 6 + .../helpers/KeyboardFeedbackManager.kt | 2 +- .../fossify/keyboard/views/MyKeyboardView.kt | 131 ++++++++++++------ app/src/main/res/layout/activity_settings.xml | 22 +++ app/src/main/res/values/dimens.xml | 4 + app/src/main/res/values/strings.xml | 4 + 8 files changed, 156 insertions(+), 46 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/keyboard/activities/SettingsActivity.kt b/app/src/main/kotlin/org/fossify/keyboard/activities/SettingsActivity.kt index 3d0576450..1df6d0935 100644 --- a/app/src/main/kotlin/org/fossify/keyboard/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/org/fossify/keyboard/activities/SettingsActivity.kt @@ -20,6 +20,9 @@ import org.fossify.keyboard.extensions.getKeyboardLanguageText import org.fossify.keyboard.extensions.getKeyboardLanguagesRadioItems import org.fossify.keyboard.extensions.getVoiceInputMethods import org.fossify.keyboard.extensions.getVoiceInputRadioItems +import org.fossify.keyboard.helpers.CURSOR_SPEED_FAST +import org.fossify.keyboard.helpers.CURSOR_SPEED_NORMAL +import org.fossify.keyboard.helpers.CURSOR_SPEED_SLOW import org.fossify.keyboard.helpers.KEYBOARD_HEIGHT_100_PERCENT import org.fossify.keyboard.helpers.KEYBOARD_HEIGHT_120_PERCENT import org.fossify.keyboard.helpers.KEYBOARD_HEIGHT_140_PERCENT @@ -65,6 +68,7 @@ class SettingsActivity : SimpleActivity() { setupShowLanguageSwitchKey() setupShowClipboardContent() setupSentencesCapitalization() + setupSpaceBarCursorSpeed() setupShowNumbersRow() setupVoiceInputMethod() @@ -272,6 +276,31 @@ class SettingsActivity : SimpleActivity() { } } + private fun setupSpaceBarCursorSpeed() { + binding.apply { + settingsSpaceBarCursorSpeed.text = getCursorSpeedText(config.spaceBarCursorSpeed) + settingsSpaceBarCursorSpeedHolder.setOnClickListener { + val items = arrayListOf( + RadioItem(CURSOR_SPEED_SLOW, getString(R.string.cursor_speed_slow)), + RadioItem(CURSOR_SPEED_NORMAL, getString(R.string.cursor_speed_normal)), + RadioItem(CURSOR_SPEED_FAST, getString(R.string.cursor_speed_fast)) + ) + RadioGroupDialog(this@SettingsActivity, items, config.spaceBarCursorSpeed) { + config.spaceBarCursorSpeed = it as Int + settingsSpaceBarCursorSpeed.text = getCursorSpeedText(config.spaceBarCursorSpeed) + } + } + } + } + + private fun getCursorSpeedText(speed: Int): String = getString( + when (speed) { + CURSOR_SPEED_SLOW -> R.string.cursor_speed_slow + CURSOR_SPEED_NORMAL -> R.string.cursor_speed_normal + else -> R.string.cursor_speed_fast + } + ) + private fun setupShowEmojiKey() { binding.apply { settingsShowEmojiKeyHolder.setOnClickListener { diff --git a/app/src/main/kotlin/org/fossify/keyboard/helpers/Config.kt b/app/src/main/kotlin/org/fossify/keyboard/helpers/Config.kt index 46003b5ce..5d0f992aa 100644 --- a/app/src/main/kotlin/org/fossify/keyboard/helpers/Config.kt +++ b/app/src/main/kotlin/org/fossify/keyboard/helpers/Config.kt @@ -67,6 +67,10 @@ class Config(context: Context) : BaseConfig(context) { get() = prefs.getString(VOICE_INPUT_METHOD, "")!! set(voiceInputMethod) = prefs.edit().putString(VOICE_INPUT_METHOD, voiceInputMethod).apply() + var spaceBarCursorSpeed: Int + get() = prefs.getInt(SPACE_BAR_CURSOR_SPEED, CURSOR_SPEED_NORMAL) + set(spaceBarCursorSpeed) = prefs.edit().putInt(SPACE_BAR_CURSOR_SPEED, spaceBarCursorSpeed).apply() + var selectedLanguages: MutableSet get() { val defaultLanguage = getDefaultLanguage().toString() diff --git a/app/src/main/kotlin/org/fossify/keyboard/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/keyboard/helpers/Constants.kt index c31276dbd..a84fe63cd 100644 --- a/app/src/main/kotlin/org/fossify/keyboard/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/keyboard/helpers/Constants.kt @@ -31,6 +31,12 @@ const val SHOW_NUMBERS_ROW = "show_numbers_row" const val SELECTED_LANGUAGES = "selected_languages" const val VOICE_INPUT_METHOD = "voice_input_method" const val RECENTLY_USED_EMOJIS = "recently_used_emojis" +const val SPACE_BAR_CURSOR_SPEED = "space_bar_cursor_speed" + +// cursor movement speed, higher = cursor moves with less finger travel +const val CURSOR_SPEED_SLOW = 0 +const val CURSOR_SPEED_NORMAL = 1 +const val CURSOR_SPEED_FAST = 2 // differentiate current and pinned clips at the keyboards' Clipboard section const val ITEM_SECTION_LABEL = 0 diff --git a/app/src/main/kotlin/org/fossify/keyboard/helpers/KeyboardFeedbackManager.kt b/app/src/main/kotlin/org/fossify/keyboard/helpers/KeyboardFeedbackManager.kt index aea444e80..07c7b3bdb 100644 --- a/app/src/main/kotlin/org/fossify/keyboard/helpers/KeyboardFeedbackManager.kt +++ b/app/src/main/kotlin/org/fossify/keyboard/helpers/KeyboardFeedbackManager.kt @@ -36,7 +36,7 @@ class KeyboardFeedbackManager(private val context: Context) { if (isOreoMr1Plus()) { @Suppress("DEPRECATION") view.performHapticFeedback( - HapticFeedbackConstants.TEXT_HANDLE_MOVE, + HapticFeedbackConstants.KEYBOARD_TAP, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING ) } diff --git a/app/src/main/kotlin/org/fossify/keyboard/views/MyKeyboardView.kt b/app/src/main/kotlin/org/fossify/keyboard/views/MyKeyboardView.kt index 3126313fb..291d6d0f5 100644 --- a/app/src/main/kotlin/org/fossify/keyboard/views/MyKeyboardView.kt +++ b/app/src/main/kotlin/org/fossify/keyboard/views/MyKeyboardView.kt @@ -86,6 +86,8 @@ import org.fossify.keyboard.extensions.isDeviceLocked import org.fossify.keyboard.extensions.onScroll import org.fossify.keyboard.extensions.safeStorageContext import org.fossify.keyboard.helpers.AccessHelper +import org.fossify.keyboard.helpers.CURSOR_SPEED_NORMAL +import org.fossify.keyboard.helpers.CURSOR_SPEED_SLOW import org.fossify.keyboard.helpers.EMOJI_SPEC_FILE_PATH import org.fossify.keyboard.helpers.EmojiData import org.fossify.keyboard.helpers.KeyboardFeedbackManager @@ -116,6 +118,7 @@ import org.fossify.keyboard.models.ClipsSectionLabel import org.fossify.keyboard.models.ListItem import java.util.Arrays import java.util.Locale +import kotlin.math.abs @SuppressLint("UseCompatLoadingForDrawables", "ClickableViewAccessibility") class MyKeyboardView @JvmOverloads constructor( @@ -194,13 +197,16 @@ class MyKeyboardView @JvmOverloads constructor( private var mRepeatKeyIndex = NOT_A_KEY private var mPopupLayout = 0 private var mAbortKey = false - private var mCursorControlActive = false - private var mLastSpaceMoveX = 0 + private var mSpaceCursorArmed = false + private var mSpaceCursorActive = false + private var mSpaceCursorDownX = 0 + private var mSpaceCursorAnchorX = 0 + private var mSpaceCursorStepPx = 0 + private val mSpaceCursorSlopPx = ViewConfiguration.get(context).scaledTouchSlop private var mPopupMaxMoveDistance = 0f private var mTopSmallNumberSize = 0f private var mTopSmallNumberMarginWidth = 0f private var mTopSmallNumberMarginHeight = 0f - private val mSpaceMoveThreshold: Int private var ignoreTouches = false private var mKeyBackground: Drawable? = null @@ -270,7 +276,6 @@ class MyKeyboardView @JvmOverloads constructor( mLabelTextSize = resources.getDimension(R.dimen.label_text_size).toInt() mSpaceBarTextSize = resources.getDimension(R.dimen.space_bar_text_size).toInt() mPreviewHeight = resources.getDimension(R.dimen.key_height).toInt() - mSpaceMoveThreshold = resources.getDimension(R.dimen.medium_margin).toInt() with(safeStorageContext) { mTextColor = getProperTextColor() @@ -1439,11 +1444,11 @@ class MyKeyboardView @JvmOverloads constructor( val msg = mHandler!!.obtainMessage(MSG_REPEAT) mHandler!!.sendMessageDelayed(msg, REPEAT_START_DELAY.toLong()) - // if the user long presses space bar, move the cursor after swiping left/right + // holding the space bar and swiping left/right moves the cursor instead of repeating the key if (mKeys[mCurrentKey].code == KEYCODE_SPACE) { - mLastSpaceMoveX = -1 - mCursorControlActive = false + armSpaceCursor(touchX) } else { + mSpaceCursorArmed = false repeatKey() } @@ -1488,29 +1493,10 @@ class MyKeyboardView @JvmOverloads constructor( } } - // activate cursor control immediately on sufficient movement - val currentKey = mKeys.getOrNull(mCurrentKey) - if (currentKey?.code == KEYCODE_SPACE && mLastSpaceMoveX != 0) { - if (mLastSpaceMoveX == -1) { - mLastSpaceMoveX = mLastX - } - - val diff = mLastX - mLastSpaceMoveX - if (diff < -mSpaceMoveThreshold) { - for (i in diff / mSpaceMoveThreshold until 0) { - mOnKeyboardActionListener?.moveCursorLeft() - } - mLastSpaceMoveX = mLastX - if (!mCursorControlActive) mHandler?.removeMessages(MSG_LONGPRESS) - mCursorControlActive = true - } else if (diff > mSpaceMoveThreshold) { - for (i in 0 until diff / mSpaceMoveThreshold) { - mOnKeyboardActionListener?.moveCursorRight() - } - mLastSpaceMoveX = mLastX - if (!mCursorControlActive) mHandler?.removeMessages(MSG_LONGPRESS) - mCursorControlActive = true - } + // move the cursor when the space bar is held and swiped, without waiting for a long press + val isSpaceCursorSwipe = mSpaceCursorArmed && mKeys.getOrNull(mCurrentKey)?.code == KEYCODE_SPACE + if (isSpaceCursorSwipe) { + moveCursorBySpaceSwipe(touchX) } else if (!continueLongPress) { // Cancel old long-press mHandler!!.removeMessages(MSG_LONGPRESS) @@ -1530,7 +1516,7 @@ class MyKeyboardView @JvmOverloads constructor( MotionEvent.ACTION_UP -> { setCurrentKeyPressed(false) - mLastSpaceMoveX = 0 + mSpaceCursorArmed = false removeMessages() if (keyIndex == mCurrentKey) { mCurrentKeyTime += eventTime - mLastMoveTime @@ -1554,18 +1540,18 @@ class MyKeyboardView @JvmOverloads constructor( // If we're not on a repeating key (which sends on a DOWN event) if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) { detectAndSendKey(mCurrentKey, touchX, touchY, eventTime) - } else if (currentKeyCode == KEYCODE_SPACE && !mCursorControlActive) { + } else if (currentKeyCode == KEYCODE_SPACE && !mSpaceCursorActive) { detectAndSendKey(mCurrentKey, touchX, touchY, eventTime) } mRepeatKeyIndex = NOT_A_KEY mOnKeyboardActionListener!!.onActionUp() - mCursorControlActive = false + mSpaceCursorActive = false } MotionEvent.ACTION_CANCEL -> { - mCursorControlActive = false - mLastSpaceMoveX = 0 + mSpaceCursorActive = false + mSpaceCursorArmed = false removeMessages() dismissPopupKeyboard() mAbortKey = true @@ -1607,17 +1593,72 @@ class MyKeyboardView @JvmOverloads constructor( setupStoredClips() } - private fun onSpaceBarLongPressed(): Boolean { - return if (!mCursorControlActive) { - setCurrentKeyPressed(false) - mRepeatKeyIndex = NOT_A_KEY - mHandler?.removeMessages(MSG_REPEAT) - vibrateIfNeeded() - SwitchLanguageDialog(this) { - mOnKeyboardActionListener?.reloadKeyboard() + /** + * Arms cursor control for a space bar press. Nothing moves yet: the finger must first travel past the touch + * slop for the press to count as a swipe (which cancels the pending long press), and the cursor then moves one + * character per [mSpaceCursorStepPx] of further travel. + */ + private fun armSpaceCursor(downX: Int) { + mSpaceCursorArmed = true + mSpaceCursorActive = false + mSpaceCursorDownX = downX + mSpaceCursorAnchorX = downX + mSpaceCursorStepPx = getSpaceCursorStepPx() + } + + /** + * Moves the cursor left/right by one character for each [mSpaceCursorStepPx] the finger travelled along the + * space bar. The long press is cancelled as soon as the finger moves, so the language dialog never opens + * mid-swipe. + */ + private fun moveCursorBySpaceSwipe(x: Int) { + if (!mSpaceCursorActive) { + if (abs(x - mSpaceCursorDownX) <= mSpaceCursorSlopPx) return + mSpaceCursorActive = true + mHandler?.removeMessages(MSG_LONGPRESS) + } + + val diff = x - mSpaceCursorAnchorX + when { + diff <= -mSpaceCursorStepPx -> repeat(-diff / mSpaceCursorStepPx) { + mOnKeyboardActionListener?.moveCursorLeft() } - true - } else false + diff >= mSpaceCursorStepPx -> repeat(diff / mSpaceCursorStepPx) { + mOnKeyboardActionListener?.moveCursorRight() + } + else -> return + } + mSpaceCursorAnchorX = x + } + + /** + * Distance the finger must travel along the space bar to move the cursor by one character, based on the speed + * setting. + */ + private fun getSpaceCursorStepPx(): Int { + val step = when (context.config.spaceBarCursorSpeed) { + CURSOR_SPEED_SLOW -> R.dimen.space_cursor_step_slow + CURSOR_SPEED_NORMAL -> R.dimen.space_cursor_step_normal + else -> R.dimen.space_cursor_step_fast // CURSOR_SPEED_FAST, the original Fossify speed + } + return resources.getDimension(step).toInt() + } + + private fun onSpaceBarLongPressed(): Boolean { + if (mSpaceCursorActive) { + return false + } + + // the language dialog takes over, so stop tracking the space bar for cursor control + mSpaceCursorArmed = false + setCurrentKeyPressed(false) + mRepeatKeyIndex = NOT_A_KEY + mHandler?.removeMessages(MSG_REPEAT) + vibrateIfNeeded() + SwitchLanguageDialog(this) { + mOnKeyboardActionListener?.reloadKeyboard() + } + return true } private fun onEmojiOrLanguageLongPressed(): Boolean { diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index f1706073d..e6f623824 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -334,6 +334,28 @@ + + + + + + + + 42dp 200dp + 24dp + 16dp + 8dp + 22sp 26sp 16sp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cceb4dcf7..ee5cdcb3e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,6 +43,10 @@ Show key borders Show numbers on a separate row Start sentences with a capital letter + Cursor movement speed + Slow + Normal + Fast Show emoji key Show language switch key