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