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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,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)).
- `KeyboardLayoutHandler` was extracted from `GeneralKeyboardIME` to encapsulate layout XML resolution, symbol keyboard mapping, and width calculations ([#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)).
5 changes: 5 additions & 0 deletions app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ interface KeyboardIMEContext {
val keyboardSymbols: Int
val keyboardSymbolShift: Int

val isFloatingMode: Boolean
val enterKeyType: Int

var lastShiftPressTS: Long

val currentCommandBarHint: String
Expand Down Expand Up @@ -79,6 +82,8 @@ interface KeyboardIMEContext {

fun getKeyboardLayoutXML(): Int

fun isNumericKeyboardActive(): Boolean

fun handleDelete(isLongPress: Boolean = false)

fun isDeleteRepeating(): Boolean
Expand Down
135 changes: 135 additions & 0 deletions app/src/keyboards/java/be/scri/helpers/KeyboardLayoutHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package be.scri.helpers

import android.text.InputType.TYPE_CLASS_DATETIME
import android.text.InputType.TYPE_CLASS_NUMBER
import android.text.InputType.TYPE_CLASS_PHONE
import android.text.InputType.TYPE_MASK_CLASS
import be.scri.R
import be.scri.models.ScribeState

private const val DATA_SIZE_2 = 2
private const val DATA_SIZE_3 = 3

/**
* Encapsulates keyboard XML layout resolution, symbol layout mapping,
* keyboard width calculations, state-based layout XML selection, and view re-creation.
*/
class KeyboardLayoutHandler(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couples to the concrete GeneralKeyboardIME instead of KeyboardIMEContext like every sibling,needs handler,getKeyboardLayoutXML(), defaultConjugateLayoutXML, enterKeyType, isFloatingMode, and isNumericKeyboardActive as a property added to the interface first.

private val ime: KeyboardIMEContext,
) {
/**
* Resolves the XML resource ID for the active keyboard layout.
*
* @return The XML layout resource ID.
*/
fun getCurrentKeyboardLayoutXML(): Int =
when (ime.keyboardMode) {
ime.keyboardSymbols -> getPrimarySymbolKeyboardLayoutXML()
ime.keyboardSymbolShift -> R.xml.keys_symbols_shift
else -> ime.getKeyboardLayoutXML()
}

/**
* Resolves the primary symbol or numeric layout XML resource ID.
*
* @return The XML layout resource ID.
*/
fun getPrimarySymbolKeyboardLayoutXML(): Int =
if (ime.isNumericKeyboardActive()) {
R.xml.keys_numeric
} else {
R.xml.keys_symbols
}

/**
* Determines which keyboard layout XML to use based on the current [ScribeState].
*
* @param state The current state of the Scribe keyboard.
* @param isSubsequentArea true if this is for a secondary conjugation view.
* @param dataSize The number of items to display, used to select an appropriate layout.
* @return The resource ID of the keyboard layout XML.
*/
fun getKeyboardLayoutForState(
state: ScribeState,
isSubsequentArea: Boolean = false,
dataSize: Int = 0,
): Int =
when (state) {
ScribeState.SELECT_VERB_CONJUNCTION -> {
ime.saveConjugateModeType(ime.language)
if (!isSubsequentArea && dataSize == 0) {
ime.defaultConjugateLayoutXML
} else {
when (dataSize) {
DATA_SIZE_2 -> R.xml.conjugate_view_2x1
DATA_SIZE_3 -> R.xml.conjugate_view_1x3
else -> R.xml.conjugate_view_2x2
}
}
}

else -> {
ime.getKeyboardLayoutXML()
}
}

/**
* Calculates the width of the keyboard container.
*
* @return The keyboard width in pixels.
*/
fun getKeyboardWidth(): Int =
if (ime.isFloatingMode) {
val density = ime.getImeResources().displayMetrics.density
val screenWidth = ime.getImeResources().displayMetrics.widthPixels
val floatWidth = (320f * density).toInt()
Math.min(floatWidth, (screenWidth * 0.85f).toInt())
} else {
ime.getImeResources().displayMetrics.widthPixels
}

/**
* Re-instantiates the [KeyboardBase] and applies the updated shift state and layout.
*/
fun recreateKeyboard() {
if (!ime.isUiManagerInitialized) return

val xmlId = getCurrentKeyboardLayoutXML()
val currentShiftState = ime.keyboard?.mShiftState ?: SHIFT_OFF
ime.keyboard = KeyboardBase(ime.imeContext, xmlId, ime.enterKeyType, getKeyboardWidth())
ime.keyboard?.setShifted(currentShiftState)
ime.keyboardView?.setKeyboard(ime.keyboard!!)

if (xmlId == R.xml.keys_symbols) {
ime.uiManager.setupCurrencySymbol(ime.language)
}
ime.keyboardView?.invalidateAllKeys()
}

companion object {
/**
* Returns true if the given [inputType] requires a numeric keyboard layout.
*/
fun shouldUseNumericKeyboard(inputType: Int): Boolean =
when (inputType and TYPE_MASK_CLASS) {
TYPE_CLASS_NUMBER, TYPE_CLASS_DATETIME, TYPE_CLASS_PHONE -> true
else -> false
}

/**
* Returns the appropriate keyboard layout XML resource ID for the given [inputType].
* Falls back to [letterKeyboardLayoutXML] when a numeric layout is not required.
*/
fun getKeyboardLayoutXMLForInputType(
inputType: Int,
letterKeyboardLayoutXML: Int,
): Int =
if (shouldUseNumericKeyboard(inputType)) {
R.xml.keys_numeric
} else {
letterKeyboardLayoutXML
}
}
}
84 changes: 25 additions & 59 deletions app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ import android.content.res.Resources
import android.graphics.Rect
import android.inputmethodservice.InputMethodService
import android.text.InputType
import android.text.InputType.TYPE_CLASS_DATETIME
import android.text.InputType.TYPE_CLASS_NUMBER
import android.text.InputType.TYPE_CLASS_PHONE
import android.text.InputType.TYPE_MASK_CLASS
import android.view.View
import android.view.inputmethod.EditorInfo
Expand All @@ -37,6 +34,7 @@ import be.scri.helpers.KeyboardBase
import be.scri.helpers.KeyboardDataHandler
import be.scri.helpers.KeyboardIMEContext
import be.scri.helpers.KeyboardLanguageMappingConstants
import be.scri.helpers.KeyboardLayoutHandler
import be.scri.helpers.KeyboardStateManager
import be.scri.helpers.LanguageMappingConstants.getLanguageAlias
import be.scri.helpers.NativeSuggestionEngine
Expand Down Expand Up @@ -100,7 +98,7 @@ abstract class GeneralKeyboardIME(
abstract override var lastShiftPressTS: Long
abstract override var keyboardMode: Int
abstract var inputTypeClass: Int
abstract var enterKeyType: Int
abstract override var enterKeyType: Int
abstract var switchToLetters: Boolean

// Language-specific layout and behavior configurations (decoupled from base class).
Expand Down Expand Up @@ -159,6 +157,7 @@ abstract class GeneralKeyboardIME(
override lateinit var autocompletionHandler: AutocompletionHandler
internal lateinit var keyHandler: KeyHandler
internal val floatingKeyboardHandler by lazy { FloatingKeyboardHandler(this) }
internal val layoutHandler by lazy { KeyboardLayoutHandler(this) }
internal val commandHandler by lazy { CommandHandler(this) }
internal val shiftHandler by lazy { ShiftHandler(this) }
internal val suggestionUIHandler by lazy { SuggestionUIHandler(this) }
Expand Down Expand Up @@ -227,7 +226,7 @@ abstract class GeneralKeyboardIME(
override var wordSuggestions: List<String>? = null
override var checkIfPluralWord: Boolean = false
private var currentEnterKeyType: Int? = null
private var isNumericKeyboardActive: Boolean = false
internal var isNumericKeyboardActive: Boolean = false

internal val stateManager = KeyboardStateManager()
override val themeManager = KeyboardThemeManager()
Expand Down Expand Up @@ -273,22 +272,6 @@ abstract class GeneralKeyboardIME(
internal const val MAX_TEXT_LENGTH = 1000
const val COMMIT_TEXT_CURSOR_POSITION = 1
internal const val CUSTOM_CURSOR = "│" // special tall cursor character

internal fun shouldUseNumericKeyboard(inputType: Int): Boolean =
when (inputType and TYPE_MASK_CLASS) {
TYPE_CLASS_NUMBER, TYPE_CLASS_DATETIME, TYPE_CLASS_PHONE -> true
else -> false
}

internal fun getKeyboardLayoutXMLForInputType(
inputType: Int,
letterKeyboardLayoutXML: Int,
): Int =
if (shouldUseNumericKeyboard(inputType)) {
R.xml.keys_numeric
} else {
letterKeyboardLayoutXML
}
}

// MARK: Lifecycle Methods
Expand Down Expand Up @@ -445,9 +428,9 @@ abstract class GeneralKeyboardIME(
// This setter triggers the logic in the property override if not shadowed.
hasTextBeforeCursor = currentInputConnection?.getTextBeforeCursor(1, 0)?.isNotEmpty() == true

isNumericKeyboardActive = shouldUseNumericKeyboard(editorInfo.inputType)
isNumericKeyboardActive = KeyboardLayoutHandler.shouldUseNumericKeyboard(editorInfo.inputType)
keyboardMode = if (isNumericKeyboardActive) keyboardSymbols else keyboardLetters
val keyboardXml = getKeyboardLayoutXMLForInputType(editorInfo.inputType, getKeyboardLayoutXML())
val keyboardXml = KeyboardLayoutHandler.getKeyboardLayoutXMLForInputType(editorInfo.inputType, getKeyboardLayoutXML())

loadLanguageData()

Expand Down Expand Up @@ -765,20 +748,17 @@ abstract class GeneralKeyboardIME(

override fun isNumericKeyboardActive(): Boolean = isNumericKeyboardActive

override fun getCurrentKeyboardLayoutXML(): Int =
when (keyboardMode) {
keyboardSymbols -> getPrimarySymbolKeyboardLayoutXML()
keyboardSymbolShift -> R.xml.keys_symbols_shift
else -> getKeyboardLayoutXML()
}

internal fun getPrimarySymbolKeyboardLayoutXML(): Int =
/**
* Resolves the XML resource ID for the active keyboard layout.
* Delegated to [KeyboardLayoutHandler].
*/
override fun getCurrentKeyboardLayoutXML(): Int = layoutHandler.getCurrentKeyboardLayoutXML()

if (isNumericKeyboardActive) {
R.xml.keys_numeric
} else {
R.xml.keys_symbols
}
/**
* Resolves the primary symbol or numeric layout XML resource ID.
* Delegated to [KeyboardLayoutHandler].
*/
internal fun getPrimarySymbolKeyboardLayoutXML(): Int = layoutHandler.getPrimarySymbolKeyboardLayoutXML()

override fun onKeyboardActionListener(): KeyboardView.OnKeyboardActionListener = this

Expand Down Expand Up @@ -1215,6 +1195,12 @@ abstract class GeneralKeyboardIME(
* Returns the subsequent dataset for conjugation sub-views.
* Delegated to [ConjugationHandler].
*/
internal fun getKeyboardLayoutForState(
state: ScribeState,
isSubsequentArea: Boolean = false,
dataSize: Int = 0,
): Int = layoutHandler.getKeyboardLayoutForState(state, isSubsequentArea, dataSize)

override fun returnSubsequentData(): List<List<String>> = conjugationHandler.subsequentData

/**
Expand All @@ -1234,31 +1220,11 @@ abstract class GeneralKeyboardIME(

// MARK: Floating Keyboard Integration

override fun getKeyboardWidth(): Int =
if (isFloatingMode) {
val density = resources.displayMetrics.density
val screenWidth = resources.displayMetrics.widthPixels
val floatWidth = (320f * density).toInt()
Math.min(floatWidth, (screenWidth * 0.85f).toInt())
} else {
resources.displayMetrics.widthPixels
}
override fun getKeyboardWidth(): Int = layoutHandler.getKeyboardWidth()

override fun recreateKeyboard() {
if (!this::uiManager.isInitialized) return
val xmlId = getCurrentKeyboardLayoutXML()
val currentShiftState = keyboard?.mShiftState ?: SHIFT_OFF
keyboard = KeyboardBase(this, xmlId, enterKeyType, getKeyboardWidth())
keyboard?.setShifted(currentShiftState)
keyboardView?.setKeyboard(keyboard!!)

if (xmlId == R.xml.keys_symbols) {
uiManager.setupCurrencySymbol(language)
}
keyboardView?.invalidateAllKeys()
}
override fun recreateKeyboard() = layoutHandler.recreateKeyboard()

val isFloatingMode: Boolean
override val isFloatingMode: Boolean
get() = floatingKeyboardHandler.isFloatingMode

fun initFloatingMode() {
Expand Down
Loading
Loading