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,4 +94,6 @@ 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)).
- Introduced `KeyboardIMEContext` interface contract to decouple handler dependencies from the concrete `GeneralKeyboardIME` class ([#426](https://github.com/scribe-org/Scribe-Android/issues/426)).

129 changes: 129 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,129 @@
// 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
import be.scri.services.GeneralKeyboardIME

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.

Circular import: KeyboardLayoutHandler (helpers) and GeneralKeyboardIME (services) import each other, the handler isn't truly decoupled, it just holds a full ime reference.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch. In this PR (Part 15), KeyboardLayoutHandler followed the same structure as previous helper extractions. In the dedicated follow-up #693 (Part 16), we introduce the KeyboardIMEContext interface contract across all handlers, removing the GeneralKeyboardIME reference from KeyboardLayoutHandler and breaking the circular import across the codebase.


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(
private val ime: GeneralKeyboardIME,
) {
/**
* 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.resources.displayMetrics.density
val screenWidth = ime.resources.displayMetrics.widthPixels
val floatWidth = (320f * density).toInt()
Math.min(floatWidth, (screenWidth * 0.85f).toInt())
} else {
ime.resources.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, 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 {
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
}
}
}
104 changes: 19 additions & 85 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.KeyEvent
import android.view.View
Expand Down Expand Up @@ -40,6 +37,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 @@ -167,6 +165,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 var dataContract: DataContract?
get() = dataHandler.dataContract
Expand Down Expand Up @@ -232,7 +231,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()
internal val themeManager = KeyboardThemeManager()
Expand Down Expand Up @@ -280,22 +279,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 @@ -451,9 +434,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(attribute.inputType)
isNumericKeyboardActive = KeyboardLayoutHandler.shouldUseNumericKeyboard(attribute.inputType)
keyboardMode = if (isNumericKeyboardActive) keyboardSymbols else keyboardLetters
val keyboardXml = getKeyboardLayoutXMLForInputType(attribute.inputType, getKeyboardLayoutXML())
val keyboardXml = KeyboardLayoutHandler.getKeyboardLayoutXMLForInputType(attribute.inputType, getKeyboardLayoutXML())

loadLanguageData()

Expand Down Expand Up @@ -784,19 +767,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()
}
/**
* Resolves the XML resource ID for the active keyboard layout.
* Delegated to [KeyboardLayoutHandler].
*/
override fun getCurrentKeyboardLayoutXML(): Int = layoutHandler.getCurrentKeyboardLayoutXML()

private fun getPrimarySymbolKeyboardLayoutXML(): Int =
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 @@ -1969,29 +1950,11 @@ abstract class GeneralKeyboardIME(
*
* @return The resource ID of the keyboard layout XML.
*/
private fun getKeyboardLayoutForState(
internal fun getKeyboardLayoutForState(
state: ScribeState,
isSubsequentArea: Boolean = false,
dataSize: Int = 0,
): Int =
when (state) {
ScribeState.SELECT_VERB_CONJUNCTION -> {
saveConjugateModeType(language)
if (!isSubsequentArea && dataSize == 0) {
defaultConjugateLayoutXML
} else {
when (dataSize) {
DATA_SIZE_2 -> R.xml.conjugate_view_2x1
DATA_CONSTANT_3 -> R.xml.conjugate_view_1x3

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.

Naming nit , DATA_CONSTANT_3 should be DATA_SIZE_3 to match DATA_SIZE_2 wherever data_constant is mentioned

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated DATA_CONSTANT_3 to DATA_SIZE_3 for consistency.

else -> R.xml.conjugate_view_2x2
}
}
}

else -> {
getKeyboardLayoutXML()
}
}
): Int = layoutHandler.getKeyboardLayoutForState(state, isSubsequentArea, dataSize)

/**
* Updates the visibility of the suggestion buttons based on device type (phone/tablet)
Expand All @@ -2010,29 +1973,9 @@ 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 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!!)
override fun getKeyboardWidth(): Int = layoutHandler.getKeyboardWidth()

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

val isFloatingMode: Boolean
get() = floatingKeyboardHandler.isFloatingMode
Expand Down Expand Up @@ -2077,12 +2020,3 @@ abstract class GeneralKeyboardIME(
clipboardHandler.closeClipboardPanel()
}
}

private fun Float.coerceInSafe(
bound1: Float,
bound2: Float,
): Float {
val minVal = if (bound1 < bound2) bound1 else bound2
val maxVal = if (bound1 > bound2) bound1 else bound2
return this.coerceIn(minVal, maxVal)
}
Loading
Loading