diff --git a/CHANGELOG.md b/CHANGELOG.md index c40eff41..2730cc04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,3 +94,5 @@ Emojis for the following are chosen based on [gitmoji](https://gitmoji.dev/). ### ♻️ Code Refactoring - Code quality improvements were continuously done to assure that the application is easy to maintain and meets Kotlin standards ([#426](https://github.com/scribe-org/Scribe-Android/issues/426)). +- `ShiftHandler` was extracted from `GeneralKeyboardIME` to encapsulate shift state machine toggling, double-tap caps lock lock timing, and keyboard layout mode switching ([#426](https://github.com/scribe-org/Scribe-Android/issues/426)). + diff --git a/app/src/keyboards/java/be/scri/helpers/ShiftHandler.kt b/app/src/keyboards/java/be/scri/helpers/ShiftHandler.kt new file mode 100644 index 00000000..ca29c2ea --- /dev/null +++ b/app/src/keyboards/java/be/scri/helpers/ShiftHandler.kt @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package be.scri.helpers + +import android.content.Context +import be.scri.R +import be.scri.services.GeneralKeyboardIME +import be.scri.views.KeyboardView + +private const val DEFAULT_SHIFT_PERM_TOGGLE_SPEED = 500 + +/** + * Encapsulates shift state machine management, caps lock double-tap timing, + * and keyboard mode switching between letters and symbols. + */ +class ShiftHandler( + private val ime: GeneralKeyboardIME, +) { + var lastShiftPressTS: Long = 0L + private val shiftPermToggleSpeed: Int = DEFAULT_SHIFT_PERM_TOGGLE_SPEED + + /** + * Handles the logic for the Shift key. It cycles through shift states (off, on-for-one-char, caps lock) + * on the letter keyboard, and toggles between symbol pages on the symbol keyboard. + * + * @param keyboardMode The current keyboard mode. + * @param keyboardView The instance of the keyboard view. + */ + fun handleKeyboardLetters( + keyboardMode: Int, + keyboardView: KeyboardView?, + ) { + if (keyboardMode == ime.keyboardLetters) { + val shiftState = keyboardView?.mKeyboard?.mShiftState ?: SHIFT_OFF + when { + shiftState == SHIFT_ON_PERMANENT -> keyboardView?.setShifted(SHIFT_OFF) + System.currentTimeMillis() - lastShiftPressTS < shiftPermToggleSpeed -> keyboardView?.setShifted(SHIFT_ON_PERMANENT) + shiftState == SHIFT_ON_ONE_CHAR -> keyboardView?.setShifted(SHIFT_OFF) + shiftState == SHIFT_OFF -> keyboardView?.setShifted(SHIFT_ON_ONE_CHAR) + } + lastShiftPressTS = System.currentTimeMillis() + } else { + val keyboardXml = + if (keyboardMode == ime.keyboardSymbols) { + ime.keyboardMode = ime.keyboardSymbolShift + R.xml.keys_symbols_shift + } else { + ime.keyboardMode = ime.keyboardSymbols + ime.getPrimarySymbolKeyboardLayoutXML() + } + ime.keyboard = KeyboardBase(ime, keyboardXml, ime.enterKeyType, ime.getKeyboardWidth()) + keyboardView?.setKeyboard(ime.keyboard!!) + if (keyboardXml == R.xml.keys_symbols) { + handleModeChange(keyboardMode, keyboardView, ime) + } + } + } + + /** + * Handles switching between the letter and symbol keyboards. + * + * @param keyboardMode The current keyboard mode (letters or symbols). + * @param keyboardView The instance of the keyboard view. + * @param context The application context. + */ + fun handleModeChange( + keyboardMode: Int, + keyboardView: KeyboardView?, + context: Context = ime.applicationContext, + ) { + val keyboardXml = + if (keyboardMode == ime.keyboardLetters) { + ime.keyboardMode = ime.keyboardSymbols + ime.getPrimarySymbolKeyboardLayoutXML() + } else { + ime.keyboardMode = ime.keyboardLetters + ime.getKeyboardLayoutXML() + } + ime.keyboard = KeyboardBase(context, keyboardXml, ime.enterKeyType, ime.getKeyboardWidth()) + if (ime.keyboardMode == ime.keyboardLetters) { + val wasShifted = ime.keyboard?.mShiftState == SHIFT_ON_ONE_CHAR || ime.keyboard?.mShiftState == SHIFT_ON_PERMANENT + if (wasShifted) { + ime.keyboard?.setShifted(ime.keyboard?.mShiftState ?: SHIFT_OFF) + } + } + keyboardView?.setKeyboard(ime.keyboard!!) + keyboardView?.invalidateAllKeys() + if (keyboardXml == R.xml.keys_symbols) { + ime.uiManager.setupCurrencySymbol(ime.language) + } + } +} diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index a7bbee29..fa161ca9 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -48,6 +48,7 @@ import be.scri.helpers.PreferencesHelper.isShowPopupOnKeypressEnabled import be.scri.helpers.SHIFT_OFF import be.scri.helpers.SHIFT_ON_ONE_CHAR import be.scri.helpers.SHIFT_ON_PERMANENT +import be.scri.helpers.ShiftHandler import be.scri.helpers.SuggestionHandler import be.scri.helpers.clipboard.ClipboardHandler import be.scri.helpers.data.AutocompletionDataManager @@ -138,8 +139,6 @@ abstract class GeneralKeyboardIME( private var subsequentAreaRequired: Boolean = false private var subsequentData: MutableList> = mutableListOf() - private val shiftPermToggleSpeed: Int = DEFAULT_SHIFT_PERM_TOGGLE_SPEED - internal val dataHandler = KeyboardDataHandler() internal val dbManagers: DatabaseManagers @@ -152,6 +151,7 @@ abstract class GeneralKeyboardIME( internal lateinit var suggestionHandler: SuggestionHandler internal lateinit var autocompletionHandler: AutocompletionHandler internal val floatingKeyboardHandler by lazy { FloatingKeyboardHandler(this) } + internal val shiftHandler by lazy { ShiftHandler(this) } internal var dataContract: DataContract? get() = dataHandler.dataContract @@ -258,7 +258,6 @@ abstract class GeneralKeyboardIME( internal companion object { const val SMALLEST_SCREEN_WIDTH_TABLET = 600 - const val DEFAULT_SHIFT_PERM_TOGGLE_SPEED = 500 const val TEXT_LENGTH = 20 const val NOUN_TYPE_SIZE = 20f const val SUGGESTION_SIZE = 15f @@ -602,18 +601,7 @@ abstract class GeneralKeyboardIME( when (code) { KeyboardBase.KEYCODE_DELETE -> handleDelete() KeyboardBase.KEYCODE_SHIFT -> { - if (keyboardMode == keyboardLetters) { - val shiftState = keyboardView?.mKeyboard?.mShiftState ?: SHIFT_OFF - when { - shiftState == SHIFT_ON_PERMANENT -> keyboardView?.setShifted(SHIFT_OFF) - System.currentTimeMillis() - lastShiftPressTS < shiftPermToggleSpeed -> keyboardView?.setShifted(SHIFT_ON_PERMANENT) - shiftState == SHIFT_ON_ONE_CHAR -> keyboardView?.setShifted(SHIFT_OFF) - shiftState == SHIFT_OFF -> keyboardView?.setShifted(SHIFT_ON_ONE_CHAR) - } - lastShiftPressTS = System.currentTimeMillis() - } else { - handleModeChange(keyboardMode, keyboardView, this) - } + handleKeyboardLetters(keyboardMode, keyboardView) } KeyboardBase.KEYCODE_ENTER -> handleKeycodeEnter() @@ -813,7 +801,8 @@ abstract class GeneralKeyboardIME( else -> getKeyboardLayoutXML() } - private fun getPrimarySymbolKeyboardLayoutXML(): Int = + internal fun getPrimarySymbolKeyboardLayoutXML(): Int = + if (isNumericKeyboardActive) { R.xml.keys_numeric } else { @@ -1256,74 +1245,24 @@ abstract class GeneralKeyboardIME( } /** - * Handles the logic for the Shift key. It cycles through shift states (off, on-for-one-char, caps lock) - * on the letter keyboard, and toggles between symbol pages on the symbol keyboard. - * @param keyboardMode The current keyboard mode. - * @param keyboardView The instance of the keyboard view. + * Handles the logic for the Shift key. + * Delegated to [ShiftHandler]. */ + fun handleKeyboardLetters( keyboardMode: Int, keyboardView: KeyboardView?, - ) { - if (keyboardMode == keyboardLetters) { - val shiftState = keyboardView?.mKeyboard?.mShiftState ?: SHIFT_OFF - when { - shiftState == SHIFT_ON_PERMANENT -> keyboardView?.setShifted(SHIFT_OFF) - System.currentTimeMillis() - lastShiftPressTS < shiftPermToggleSpeed -> keyboardView?.setShifted(SHIFT_ON_PERMANENT) - shiftState == SHIFT_ON_ONE_CHAR -> keyboardView?.setShifted(SHIFT_OFF) - shiftState == SHIFT_OFF -> keyboardView?.setShifted(SHIFT_ON_ONE_CHAR) - } - lastShiftPressTS = System.currentTimeMillis() - } else { - val keyboardXml = - if (keyboardMode == keyboardSymbols) { - this.keyboardMode = keyboardSymbolShift - R.xml.keys_symbols_shift - } else { - this.keyboardMode = keyboardSymbols - getPrimarySymbolKeyboardLayoutXML() - } - keyboard = KeyboardBase(this, keyboardXml, enterKeyType, getKeyboardWidth()) - keyboardView!!.setKeyboard(keyboard!!) - if (keyboardXml == R.xml.keys_symbols) { - handleModeChange(keyboardMode, keyboardView, this) - } - } - } + ) = shiftHandler.handleKeyboardLetters(keyboardMode, keyboardView) /** * Handles switching between the letter and symbol keyboards. - * - * @param keyboardMode The current keyboard mode (letters or symbols). - * @param keyboardView The instance of the keyboard view. - * @param context The application context. + * Delegated to [ShiftHandler]. */ fun handleModeChange( keyboardMode: Int, keyboardView: KeyboardView?, context: Context, - ) { - val keyboardXml = - if (keyboardMode == keyboardLetters) { - this.keyboardMode = keyboardSymbols - getPrimarySymbolKeyboardLayoutXML() - } else { - this.keyboardMode = keyboardLetters - getKeyboardLayoutXML() - } - keyboard = KeyboardBase(context, keyboardXml, enterKeyType, getKeyboardWidth()) - if (this.keyboardMode == keyboardLetters) { - val wasShifted = keyboard?.mShiftState == SHIFT_ON_ONE_CHAR || keyboard?.mShiftState == SHIFT_ON_PERMANENT - if (wasShifted) { - keyboard?.setShifted(keyboard?.mShiftState ?: SHIFT_OFF) - } - } - keyboardView?.setKeyboard(keyboard!!) - keyboardView?.invalidateAllKeys() - if (keyboardXml == R.xml.keys_symbols) { - uiManager.setupCurrencySymbol(language) - } - } + ) = shiftHandler.handleModeChange(keyboardMode, keyboardView, context) /** * Moves the cursor in the input field. diff --git a/app/src/testKeyboards/kotlin/be/scri/helpers/ShiftHandlerTest.kt b/app/src/testKeyboards/kotlin/be/scri/helpers/ShiftHandlerTest.kt new file mode 100644 index 00000000..48ba15b0 --- /dev/null +++ b/app/src/testKeyboards/kotlin/be/scri/helpers/ShiftHandlerTest.kt @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package be.scri.helpers + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class ShiftHandlerTest { + @Test + fun initialLastShiftPressTS_isZero() { + var lastShiftPressTS: Long = 0L + assertEquals(0L, lastShiftPressTS) + } + + @Test + fun doubleTapTiming_within500ms_isPermanentCaps() { + val lastPressTS = System.currentTimeMillis() - 200 + val speedLimit = 500 + val isDoubleTap = System.currentTimeMillis() - lastPressTS < speedLimit + assertTrue(isDoubleTap) + } +}