Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).

92 changes: 92 additions & 0 deletions app/src/keyboards/java/be/scri/helpers/ShiftHandler.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
83 changes: 11 additions & 72 deletions app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -138,8 +139,6 @@ abstract class GeneralKeyboardIME(
private var subsequentAreaRequired: Boolean = false
private var subsequentData: MutableList<List<String>> = mutableListOf()

private val shiftPermToggleSpeed: Int = DEFAULT_SHIFT_PERM_TOGGLE_SPEED

internal val dataHandler = KeyboardDataHandler()

internal val dbManagers: DatabaseManagers
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -813,7 +801,8 @@ abstract class GeneralKeyboardIME(
else -> getKeyboardLayoutXML()
}

private fun getPrimarySymbolKeyboardLayoutXML(): Int =
internal fun getPrimarySymbolKeyboardLayoutXML(): Int =

if (isNumericKeyboardActive) {
R.xml.keys_numeric
} else {
Expand Down Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions app/src/testKeyboards/kotlin/be/scri/helpers/ShiftHandlerTest.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading