diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b6f195b..2aaa0324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,3 +104,4 @@ 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..28f22141 --- /dev/null +++ b/app/src/keyboards/java/be/scri/helpers/ShiftHandler.kt @@ -0,0 +1,99 @@ +// 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, + private val timeProvider: () -> Long = { System.currentTimeMillis() }, +) { + var lastShiftPressTS: Long + get() = ime.lastShiftPressTS + set(value) { + ime.lastShiftPressTS = value + } + + 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 + val currentTime = timeProvider() + when { + shiftState == SHIFT_ON_PERMANENT -> keyboardView?.setShifted(SHIFT_OFF) + currentTime - ime.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) + } + ime.lastShiftPressTS = currentTime + } 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 7aa40cec..ca86994d 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -49,6 +49,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 @@ -145,8 +146,6 @@ abstract class GeneralKeyboardIME( override var isSingularAndPlural: Boolean = false internal val conjugationHandler by lazy { ConjugationHandler(this) } - private val shiftPermToggleSpeed: Int = DEFAULT_SHIFT_PERM_TOGGLE_SPEED - internal val dataHandler = KeyboardDataHandler() internal val dbManagers: DatabaseManagers @@ -161,6 +160,7 @@ abstract class GeneralKeyboardIME( internal lateinit var keyHandler: KeyHandler internal val floatingKeyboardHandler by lazy { FloatingKeyboardHandler(this) } internal val commandHandler by lazy { CommandHandler(this) } + internal val shiftHandler by lazy { ShiftHandler(this) } internal val suggestionUIHandler by lazy { SuggestionUIHandler(this) } internal var dataContract: DataContract? @@ -266,7 +266,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 SUGGESTION_SIZE = 15f const val DARK_THEME = "#aeb3be" @@ -773,7 +772,8 @@ abstract class GeneralKeyboardIME( else -> getKeyboardLayoutXML() } - private fun getPrimarySymbolKeyboardLayoutXML(): Int = + internal fun getPrimarySymbolKeyboardLayoutXML(): Int = + if (isNumericKeyboardActive) { R.xml.keys_numeric } else { @@ -1046,74 +1046,23 @@ abstract class GeneralKeyboardIME( ): String = dataHandler.getTranslation(language, commandBarInput) /** - * 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]. */ override 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]. */ override 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..e6f36302 --- /dev/null +++ b/app/src/testKeyboards/kotlin/be/scri/helpers/ShiftHandlerTest.kt @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package be.scri.helpers + +import be.scri.services.GeneralKeyboardIME +import be.scri.views.KeyboardView +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class ShiftHandlerTest { + private val ime = mockk(relaxed = true) + private val keyboardView = mockk(relaxed = true) + private val keyboard = mockk(relaxed = true) + + @Before + fun setUp() { + every { keyboardView.mKeyboard } returns keyboard + every { ime.keyboardLetters } returns 0 + every { ime.keyboardSymbols } returns 1 + every { ime.keyboardSymbolShift } returns 2 + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun lastShiftPressTS_readsAndWritesToIme() { + every { ime.lastShiftPressTS } returns 12345L + val handler = ShiftHandler(ime) + + assertEquals(12345L, handler.lastShiftPressTS) + + handler.lastShiftPressTS = 67890L + verify { ime.lastShiftPressTS = 67890L } + } + + @Test + fun handleKeyboardLetters_singleTapFromOff_setsShiftOnOneChar() { + val currentTime = 1000L + every { keyboard.mShiftState } returns SHIFT_OFF + every { ime.lastShiftPressTS } returns 0L + + val handler = ShiftHandler(ime, timeProvider = { currentTime }) + handler.handleKeyboardLetters(keyboardMode = 0, keyboardView = keyboardView) + + verify { keyboardView.setShifted(SHIFT_ON_ONE_CHAR) } + verify { ime.lastShiftPressTS = 1000L } + } + + @Test + fun handleKeyboardLetters_doubleTapWithin500ms_setsShiftOnPermanent() { + val currentTime = 1000L + every { keyboard.mShiftState } returns SHIFT_ON_ONE_CHAR + every { ime.lastShiftPressTS } returns 800L + + val handler = ShiftHandler(ime, timeProvider = { currentTime }) + handler.handleKeyboardLetters(keyboardMode = 0, keyboardView = keyboardView) + + verify { keyboardView.setShifted(SHIFT_ON_PERMANENT) } + verify { ime.lastShiftPressTS = 1000L } + } + + @Test + fun handleKeyboardLetters_tapAfterReset_setsShiftOff() { + val currentTime = 2000L + every { keyboard.mShiftState } returns SHIFT_ON_ONE_CHAR + every { ime.lastShiftPressTS } returns 0L + + val handler = ShiftHandler(ime, timeProvider = { currentTime }) + handler.handleKeyboardLetters(keyboardMode = 0, keyboardView = keyboardView) + + verify { keyboardView.setShifted(SHIFT_OFF) } + verify { ime.lastShiftPressTS = 2000L } + } + + @Test + fun handleKeyboardLetters_permanentShiftState_togglesShiftOff() { + val currentTime = 1500L + every { keyboard.mShiftState } returns SHIFT_ON_PERMANENT + every { ime.lastShiftPressTS } returns 1000L + + val handler = ShiftHandler(ime, timeProvider = { currentTime }) + handler.handleKeyboardLetters(keyboardMode = 0, keyboardView = keyboardView) + + verify { keyboardView.setShifted(SHIFT_OFF) } + verify { ime.lastShiftPressTS = 1500L } + } + + @Test + fun handleKeyboardLetters_tapAfterSpeedLimit_setsShiftOff() { + val currentTime = 2000L + every { keyboard.mShiftState } returns SHIFT_ON_ONE_CHAR + every { ime.lastShiftPressTS } returns 1200L + + val handler = ShiftHandler(ime, timeProvider = { currentTime }) + handler.handleKeyboardLetters(keyboardMode = 0, keyboardView = keyboardView) + + verify { keyboardView.setShifted(SHIFT_OFF) } + verify { ime.lastShiftPressTS = 2000L } + } +}