From 949c23492df94b285b5f3f89a95160ceb6901210 Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:15:51 +0530 Subject: [PATCH 1/4] refactor: extract ConjugationHandler from GeneralKeyboardIME (Part 12) - #426 --- .../java/be/scri/helpers/CommandHandler.kt | 2 +- .../be/scri/helpers/ConjugationHandler.kt | 190 +++++++++++++++ .../be/scri/helpers/KeyboardIMEContext.kt | 4 + .../be/scri/services/GeneralKeyboardIME.kt | 163 ++----------- .../be/scri/helpers/ConjugationHandlerTest.kt | 219 ++++++++++++++++++ 5 files changed, 427 insertions(+), 151 deletions(-) create mode 100644 app/src/keyboards/java/be/scri/helpers/ConjugationHandler.kt create mode 100644 app/src/testKeyboards/kotlin/be/scri/helpers/ConjugationHandlerTest.kt diff --git a/app/src/keyboards/java/be/scri/helpers/CommandHandler.kt b/app/src/keyboards/java/be/scri/helpers/CommandHandler.kt index 9c09f0a50..15cb478b0 100644 --- a/app/src/keyboards/java/be/scri/helpers/CommandHandler.kt +++ b/app/src/keyboards/java/be/scri/helpers/CommandHandler.kt @@ -115,7 +115,7 @@ class CommandHandler( if (tempOutput?.isEmpty() == true || tempOutput?.values?.all { it.isEmpty() } == true) { null } else if ((isAllCaps || isCapitalized) && tempOutput != null) { - ime.applyCapitalizationToConjugations(tempOutput, isAllCaps) + ConjugationHandler.applyCapitalizationToConjugations(tempOutput, isAllCaps) } else { tempOutput } diff --git a/app/src/keyboards/java/be/scri/helpers/ConjugationHandler.kt b/app/src/keyboards/java/be/scri/helpers/ConjugationHandler.kt new file mode 100644 index 000000000..b6fbb00cf --- /dev/null +++ b/app/src/keyboards/java/be/scri/helpers/ConjugationHandler.kt @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package be.scri.helpers + +import android.content.Context.MODE_PRIVATE +import android.view.View +import androidx.core.content.edit +import be.scri.R +import be.scri.models.ScribeState + +private const val DATA_SIZE_2 = 2 +private const val DATA_SIZE_3 = 3 + +/** + * Encapsulates verb conjugation state management, conjugation table layout logic, + * and capitalization formatting for GeneralKeyboardIME. + */ +class ConjugationHandler( + private val ime: KeyboardIMEContext, +) { + var subsequentAreaRequired: Boolean = false + var subsequentData: MutableList> = mutableListOf() + + /** + * Saves the type of conjugation layout being used (e.g., "2x2", "none") to shared preferences. + * + * @param language The current keyboard language. + * @param isSubsequent true if saving for a sub-view, false for standard layout. + */ + fun saveConjugateModeType( + language: String, + isSubsequent: Boolean = false, + ) { + val mode = + if (!isSubsequent) { + when (language) { + "English", "Russian", "Swedish", + "German", "French", "Italian", "Portuguese", "Spanish", + "en", "ru", "sv", "de", "fr", "it", "pt", "es", + -> "2x2" + else -> "none" + } + } else { + "none" + } + val sharedPref = ime.imeContext.getSharedPreferences("keyboard_preferences", MODE_PRIVATE) + sharedPref.edit { putString("conjugate_mode_type", mode) } + } + + /** + * Retrieves and validates the stored index for the current conjugation view. + * Ensures the index is within the bounds of available conjugation types. + * + * @return A valid, zero-based index for the conjugation type. + */ + fun getValidatedConjugateIndex(): Int { + val prefs = ime.imeContext.getSharedPreferences("keyboard_preferences", MODE_PRIVATE) + var index = prefs.getInt("conjugate_index", 0) + val maxIndex = + ime.conjugateOutput + ?.keys + ?.count() + ?.minus(1) ?: -1 + index = if (maxIndex >= 0) index.coerceIn(0, maxIndex) else 0 + prefs.edit { putInt("conjugate_index", index) } + return index + } + + /** + * Handles a key press on one of the special conjugation keys. + * It either commits the text directly or prepares for a subsequent selection view. + * + * @param code The key code of the pressed key. + * @param isSubsequentRequired true if a sub-view is needed for more options. + * + * @return The label of the key that was pressed. + */ + fun handleConjugateKeys( + code: Int, + isSubsequentRequired: Boolean, + ): String? { + val keyLabel = ime.keyboardView?.getKeyLabel(code) + if (!isSubsequentRequired) { + if (!keyLabel.isNullOrEmpty()) { + ime.getInputConnection()?.commitText("$keyLabel ", 1) + ime.suggestionHandler.processLinguisticSuggestions(keyLabel) + } + } + return keyLabel + } + + /** + * Sets up a secondary "sub-view" for conjugation when a single key has multiple options. + * + * @param data The full dataset of subsequent options. + * @param word The specific word selected from the primary view, used to filter the data. + */ + fun setupConjugateSubView( + data: List>, + word: String?, + ) { + val uniqueData = data.distinct() + val filteredData = uniqueData.filter { sublist -> sublist.contains(word) } + val flattenList = filteredData.flatten() + saveConjugateModeType(language = ime.language, isSubsequent = true) + val keyboardXmlId = getKeyboardLayoutForState(ime.currentState, isSubsequentArea = true, dataSize = flattenList.size) + ime.uiManager.initializeKeyboard(keyboardXmlId) + when (flattenList.size) { + DATA_SIZE_2 -> { + ime.keyboardView?.setKeyLabel(flattenList[0], "HI", KeyboardBase.CODE_2X1_TOP) + ime.keyboardView?.setKeyLabel(flattenList[1], "HI", KeyboardBase.CODE_2X1_BOTTOM) + subsequentAreaRequired = false + } + + DATA_SIZE_3 -> { + ime.keyboardView?.setKeyLabel(flattenList[0], "HI", KeyboardBase.CODE_1X3_LEFT) + ime.keyboardView?.setKeyLabel(flattenList[1], "HI", KeyboardBase.CODE_1X3_CENTER) + ime.keyboardView?.setKeyLabel(flattenList[DATA_SIZE_2], "HI", KeyboardBase.CODE_1X3_RIGHT) + subsequentAreaRequired = false + } + } + ime.uiManager.binding.ivInfo.visibility = View.GONE + } + + /** + * 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. + */ + private fun getKeyboardLayoutForState( + state: ScribeState, + isSubsequentArea: Boolean = false, + dataSize: Int = 0, + ): Int = + when (state) { + ScribeState.SELECT_VERB_CONJUNCTION -> { + 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() + } + } + + companion object { + /** + * Applies capitalization to all conjugated forms in the output map. + * Supports both standard capitalization (first letter) and all capital letters formatting. + * + * @param conjugations The original map of conjugations from the database. + * @param isAllCaps If true, applies all capital letters; if false, capitalizes only first letter. + * + * @return A new map with properly formatted conjugations. + */ + fun applyCapitalizationToConjugations( + conjugations: MutableMap>>, + isAllCaps: Boolean = false, + ): MutableMap>> { + val formattedOutput: MutableMap>> = mutableMapOf() + conjugations.forEach { (tenseKey, conjugationMap) -> + val formattedConjugations: MutableMap> = mutableMapOf() + conjugationMap.forEach { (categoryKey, forms) -> + val formattedForms = + forms.map { form -> + when { + form.isEmpty() -> form + isAllCaps -> form.uppercase() + else -> form.replaceFirstChar { it.uppercase() } + } + } + formattedConjugations[categoryKey] = formattedForms + } + formattedOutput[tenseKey] = formattedConjugations + } + return formattedOutput + } + } +} diff --git a/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt b/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt index 184fb238b..378759465 100644 --- a/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt +++ b/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt @@ -73,6 +73,10 @@ interface KeyboardIMEContext { val caseAnnotation: HashMap> val suggestionWords: HashMap> val emojiKeywords: HashMap>? + var conjugateOutput: MutableMap>>? + val defaultConjugateLayoutXML: Int + + fun getKeyboardLayoutXML(): Int fun handleDelete(isLongPress: Boolean = false) diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index d079c06f0..038c15551 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -22,13 +22,13 @@ import android.view.inputmethod.EditorInfo.IME_MASK_ACTION import android.view.inputmethod.ExtractedTextRequest import android.view.inputmethod.InputConnection import android.widget.TextView -import androidx.core.content.edit import be.scri.R import be.scri.activities.MainActivity import be.scri.databinding.InputMethodViewBinding import be.scri.helpers.AutocompletionHandler import be.scri.helpers.BackspaceHandler import be.scri.helpers.CommandHandler +import be.scri.helpers.ConjugationHandler import be.scri.helpers.DatabaseManagers import be.scri.helpers.EmojiUtils.insertEmoji import be.scri.helpers.FloatingKeyboardHandler @@ -61,9 +61,6 @@ import be.scri.models.ScribeState import be.scri.views.KeyboardView import java.util.Locale -private const val DATA_SIZE_2 = 2 -private const val DATA_CONSTANT_3 = 3 - @Suppress("TooManyFunctions", "LargeClass") abstract class GeneralKeyboardIME( override val scribeLanguage: ScribeLanguage, @@ -107,7 +104,7 @@ abstract class GeneralKeyboardIME( // Language-specific layout and behavior configurations (decoupled from base class). open val defaultConjugateModeType: String = "3x2" - open val defaultConjugateLayoutXML: Int = R.xml.conjugate_view_3x2 + override val defaultConjugateLayoutXML: Int = R.xml.conjugate_view_3x2 open val isPluralCapitalized: Boolean = false /** @@ -146,8 +143,7 @@ abstract class GeneralKeyboardIME( // MARK: State Variables override var isSingularAndPlural: Boolean = false - private var subsequentAreaRequired: Boolean = false - private var subsequentData: MutableList> = mutableListOf() + internal val conjugationHandler by lazy { ConjugationHandler(this) } private val shiftPermToggleSpeed: Int = DEFAULT_SHIFT_PERM_TOGGLE_SPEED @@ -181,7 +177,7 @@ abstract class GeneralKeyboardIME( dataHandler.emojiKeywords = value } - internal var conjugateOutput: MutableMap>>? + override var conjugateOutput: MutableMap>>? get() = dataHandler.conjugateOutput set(value) { dataHandler.conjugateOutput = value @@ -655,20 +651,7 @@ abstract class GeneralKeyboardIME( override fun saveConjugateModeType( language: String, isSubsequentArea: Boolean, - ) { - val sharedPref = applicationContext.getSharedPreferences("keyboard_preferences", MODE_PRIVATE) - val mode = - if (!isSubsequentArea) { - when (language) { - "English", "Russian", "Swedish" -> "2x2" - "German", "French", "Italian", "Portuguese", "Spanish" -> "2x2" - else -> "none" - } - } else { - "none" - } - sharedPref.edit { putString("conjugate_mode_type", mode) } - } + ) = conjugationHandler.saveConjugateModeType(language, isSubsequentArea) // MARK: UI Update Delegation @@ -805,7 +788,7 @@ abstract class GeneralKeyboardIME( override fun commitText(text: String) { if (currentState == ScribeState.SELECT_VERB_CONJUNCTION) { val label = text.trim() - val conjugateIndex = getValidatedConjugateIndex() + val conjugateIndex = conjugationHandler.getValidatedConjugateIndex() val title = conjugateOutput?.keys?.elementAtOrNull(conjugateIndex) val languageOutput = title?.let { conjugateOutput!![it] } @@ -1061,47 +1044,6 @@ abstract class GeneralKeyboardIME( commandBarInput: String, ): String = dataHandler.getTranslation(language, commandBarInput) - /** - * @return A new map with properly formatted conjugations. - */ - internal fun applyCapitalizationToConjugations( - conjugations: MutableMap>>, - isAllCaps: Boolean = false, - ): MutableMap>> { - val formattedOutput: MutableMap>> = mutableMapOf() - conjugations.forEach { (tenseKey, conjugationMap) -> - val formattedConjugations: MutableMap> = mutableMapOf() - conjugationMap.forEach { (categoryKey, forms) -> - val formattedForms = - forms.map { form -> - when { - form.isEmpty() -> form - isAllCaps -> form.uppercase() - else -> form.replaceFirstChar { it.uppercase() } - } - } - formattedConjugations[categoryKey] = formattedForms - } - formattedOutput[tenseKey] = formattedConjugations - } - return formattedOutput - } - - /** - * Retrieves and validates the stored index for the current conjugation view. - * Ensures the index is within the bounds of available conjugation types. - * - * @return A valid, zero-based index for the conjugation type. - */ - private fun getValidatedConjugateIndex(): Int { - val prefs = getSharedPreferences("keyboard_preferences", MODE_PRIVATE) - var index = prefs.getInt("conjugate_index", 0) - val maxIndex = conjugateOutput?.keys?.count()?.minus(1) ?: -1 - index = if (maxIndex >= 0) index.coerceIn(0, maxIndex) else 0 - prefs.edit { putInt("conjugate_index", index) } - return index - } - /** * 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. @@ -1297,108 +1239,29 @@ abstract class GeneralKeyboardIME( /** * Returns whether the current conjugation state requires a subsequent selection view. - * This is used, for example, when a conjugation form has multiple options (e.g., "am/is/are" in English). - * - * @return true if a subsequent selection screen is needed, false otherwise. + * Delegated to [ConjugationHandler]. */ - override fun returnIsSubsequentRequired(): Boolean = subsequentAreaRequired + override fun returnIsSubsequentRequired(): Boolean = conjugationHandler.subsequentAreaRequired - override fun returnSubsequentData(): List> = subsequentData + override fun returnSubsequentData(): List> = conjugationHandler.subsequentData /** * Handles a key press on one of the special conjugation keys. - * It either commits the text directly or prepares for a subsequent selection view. - * - * @param code The key code of the pressed key. - * @param isSubsequentRequired true if a sub-view is needed for more options. - * - * @return The label of the key that was pressed. + * Delegated to [ConjugationHandler]. */ override fun handleConjugateKeys( code: Int, isSubsequentRequired: Boolean, - ): String? { - val keyLabel = keyboardView?.getKeyLabel(code) - if (!isSubsequentRequired) { - if (!keyLabel.isNullOrEmpty()) { - currentInputConnection?.commitText("$keyLabel ", 1) - suggestionHandler.processLinguisticSuggestions(keyLabel) - } - } - return keyLabel - } + ): String? = conjugationHandler.handleConjugateKeys(code, isSubsequentRequired) /** * Sets up a secondary "sub-view" for conjugation when a single key has multiple options. - * - * @param data The full dataset of subsequent options. - * @param word The specific word selected from the primary view, used to filter the data. + * Delegated to [ConjugationHandler]. */ override fun setupConjugateSubView( data: List>, word: String?, - ) { - val uniqueData = data.distinct() - val filteredData = uniqueData.filter { sublist -> sublist.contains(word) } - val flattenList = filteredData.flatten() - saveConjugateModeType(language = language, true) - val prefs = applicationContext.getSharedPreferences("keyboard_preferences", MODE_PRIVATE) - prefs.edit(commit = true) { putString("conjugate_mode_type", "2x1") } - val keyboardXmlId = getKeyboardLayoutForState(currentState, true, flattenList.size) - // Re-initialize keyboard via UI manager helper which calls 'initializeKeyboard(xml)'. - uiManager.initializeKeyboard(keyboardXmlId) - prefs.edit(commit = true) { putString("conjugate_mode_type", "2x1") } - when (flattenList.size) { - DATA_SIZE_2 -> { - keyboardView?.setKeyLabel(flattenList[0], "HI", KeyboardBase.CODE_2X1_TOP) - keyboardView?.setKeyLabel(flattenList[1], "HI", KeyboardBase.CODE_2X1_BOTTOM) - subsequentAreaRequired = false - } - - DATA_CONSTANT_3 -> { - keyboardView?.setKeyLabel(flattenList[0], "HI", KeyboardBase.CODE_1X3_RIGHT) - keyboardView?.setKeyLabel(flattenList[1], "HI", KeyboardBase.CODE_1X3_CENTER) - keyboardView?.setKeyLabel(flattenList[DATA_SIZE_2], "HI", KeyboardBase.CODE_1X3_RIGHT) - subsequentAreaRequired = false - } - } - prefs.edit(commit = true) { putString("conjugate_mode_type", "2x1") } - // Binding access via uiManager. - uiManager.binding.ivInfo.visibility = View.GONE - } - - /** - * 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. - */ - private 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 - else -> R.xml.conjugate_view_2x2 - } - } - } - - else -> { - getKeyboardLayoutXML() - } - } + ) = conjugationHandler.setupConjugateSubView(data, word) /** * Updates the visibility of the suggestion buttons based on device type (phone/tablet) diff --git a/app/src/testKeyboards/kotlin/be/scri/helpers/ConjugationHandlerTest.kt b/app/src/testKeyboards/kotlin/be/scri/helpers/ConjugationHandlerTest.kt new file mode 100644 index 000000000..ed723bd1e --- /dev/null +++ b/app/src/testKeyboards/kotlin/be/scri/helpers/ConjugationHandlerTest.kt @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package be.scri.helpers + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.inputmethod.InputConnection +import androidx.test.core.app.ApplicationProvider +import be.scri.R +import be.scri.databinding.InputMethodViewBinding +import be.scri.helpers.ui.KeyboardUIManager +import be.scri.models.ScribeState +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.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) +class ConjugationHandlerTest { + private lateinit var context: Context + private lateinit var binding: InputMethodViewBinding + private lateinit var uiManager: KeyboardUIManager + private val ime = mockk(relaxed = true) + private val keyboardView = mockk(relaxed = true) + private val inputConnection = mockk(relaxed = true) + private val suggestionHandler = mockk(relaxed = true) + + private lateinit var handler: ConjugationHandler + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + binding = InputMethodViewBinding.inflate(LayoutInflater.from(context)) + val listener = mockk(relaxed = true) + uiManager = KeyboardUIManager(binding, context, listener) + + every { ime.imeContext } returns context + every { ime.keyboardView } returns keyboardView + every { ime.getInputConnection() } returns inputConnection + every { ime.suggestionHandler } returns suggestionHandler + every { ime.uiManager } returns uiManager + every { ime.binding } returns binding + + handler = ConjugationHandler(ime) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun applyCapitalizationToConjugations_firstLetterCapitalization() { + val input: MutableMap>> = + mutableMapOf( + "Present" to mutableMapOf("I" to listOf("go", "run", "")), + ) + + val result = ConjugationHandler.applyCapitalizationToConjugations(input, isAllCaps = false) + + assertEquals(listOf("Go", "Run", ""), result["Present"]?.get("I")) + } + + @Test + fun applyCapitalizationToConjugations_allCapsFormat() { + val input: MutableMap>> = + mutableMapOf( + "Present" to mutableMapOf("I" to listOf("go", "run", "")), + ) + + val result = ConjugationHandler.applyCapitalizationToConjugations(input, isAllCaps = true) + + assertEquals(listOf("GO", "RUN", ""), result["Present"]?.get("I")) + } + + @Test + fun getValidatedConjugateIndex_clampsToValidRange() { + val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) + prefs.edit().putInt("conjugate_index", 5).commit() + + every { ime.conjugateOutput } returns + mutableMapOf( + "Present" to mutableMapOf(), + "Past" to mutableMapOf(), + ) + + val index = handler.getValidatedConjugateIndex() + + // Max index is 1 (size 2 - 1) + assertEquals(1, index) + assertEquals(1, prefs.getInt("conjugate_index", -1)) + } + + @Test + fun getValidatedConjugateIndex_nullOrEmptyOutputDefaultsToZero() { + val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) + prefs.edit().putInt("conjugate_index", 3).commit() + + every { ime.conjugateOutput } returns null + + val index = handler.getValidatedConjugateIndex() + + assertEquals(0, index) + assertEquals(0, prefs.getInt("conjugate_index", -1)) + } + + @Test + fun saveConjugateModeType_spanishReturns2x2() { + handler.saveConjugateModeType("Spanish", isSubsequent = false) + + val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) + assertEquals("2x2", prefs.getString("conjugate_mode_type", null)) + } + + @Test + fun saveConjugateModeType_englishReturns2x2() { + handler.saveConjugateModeType("English", isSubsequent = false) + + val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) + assertEquals("2x2", prefs.getString("conjugate_mode_type", null)) + } + + @Test + fun saveConjugateModeType_noneReturnsNone() { + handler.saveConjugateModeType("none", isSubsequent = false) + + val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) + assertEquals("none", prefs.getString("conjugate_mode_type", null)) + } + + @Test + fun saveConjugateModeType_subsequentReturnsNone() { + handler.saveConjugateModeType("English", isSubsequent = true) + + val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) + assertEquals("none", prefs.getString("conjugate_mode_type", null)) + } + + @Test + fun handleConjugateKeys_whenNotSubsequent_commitsTextAndProcessesSuggestions() { + every { keyboardView.getKeyLabel(10) } returns "hablo" + + val label = handler.handleConjugateKeys(code = 10, isSubsequentRequired = false) + + assertEquals("hablo", label) + verify { inputConnection.commitText("hablo ", 1) } + verify { suggestionHandler.processLinguisticSuggestions("hablo") } + } + + @Test + fun handleConjugateKeys_whenSubsequentRequired_doesNotCommitText() { + every { keyboardView.getKeyLabel(10) } returns "hablo" + + val label = handler.handleConjugateKeys(code = 10, isSubsequentRequired = true) + + assertEquals("hablo", label) + verify(exactly = 0) { inputConnection.commitText(any(), any()) } + verify(exactly = 0) { suggestionHandler.processLinguisticSuggestions(any()) } + } + + @Test + fun handleConjugateKeys_whenKeyLabelNull_returnsNullAndDoesNotCommit() { + every { keyboardView.getKeyLabel(10) } returns null + + val label = handler.handleConjugateKeys(code = 10, isSubsequentRequired = false) + + assertNull(label) + verify(exactly = 0) { inputConnection.commitText(any(), any()) } + } + + @Test + fun setupConjugateSubView_withTwoItems_configures2x1Layout() { + every { ime.language } returns "Spanish" + every { ime.currentState } returns ScribeState.SELECT_VERB_CONJUNCTION + handler.subsequentAreaRequired = true + + val data = listOf(listOf("hablo", "hablas")) + handler.setupConjugateSubView(data, word = "hablo") + + val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) + assertEquals("none", prefs.getString("conjugate_mode_type", null)) + verify { uiManager.initializeKeyboard(R.xml.conjugate_view_2x1) } + verify { keyboardView.setKeyLabel("hablo", "HI", KeyboardBase.CODE_2X1_TOP) } + verify { keyboardView.setKeyLabel("hablas", "HI", KeyboardBase.CODE_2X1_BOTTOM) } + assertEquals(View.GONE, binding.ivInfo.visibility) + assertFalse(handler.subsequentAreaRequired) + } + + @Test + fun setupConjugateSubView_withThreeItems_configures1x3Layout() { + every { ime.language } returns "Spanish" + every { ime.currentState } returns ScribeState.SELECT_VERB_CONJUNCTION + handler.subsequentAreaRequired = true + + val data = listOf(listOf("hablo", "hablas", "habla")) + handler.setupConjugateSubView(data, word = "hablo") + + val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) + assertEquals("none", prefs.getString("conjugate_mode_type", null)) + verify { uiManager.initializeKeyboard(R.xml.conjugate_view_1x3) } + verify { keyboardView.setKeyLabel("hablo", "HI", KeyboardBase.CODE_1X3_LEFT) } + verify { keyboardView.setKeyLabel("hablas", "HI", KeyboardBase.CODE_1X3_CENTER) } + verify { keyboardView.setKeyLabel("habla", "HI", KeyboardBase.CODE_1X3_RIGHT) } + assertEquals(View.GONE, binding.ivInfo.visibility) + assertFalse(handler.subsequentAreaRequired) + } +} From e415ea242a6a7607c3514edc19b9911cf2843a86 Mon Sep 17 00:00:00 2001 From: prince-0408 <66916296+prince-0408@users.noreply.github.com> Date: Mon, 21 Sep 2026 12:58:13 +0530 Subject: [PATCH 2/4] refactor: address Roniscend review comments on ConjugationHandler (Part 12) - #426 --- .../be/scri/helpers/ConjugationHandler.kt | 79 +++++++-------- .../java/be/scri/helpers/KeyHandler.kt | 2 +- .../be/scri/helpers/KeyboardIMEContext.kt | 3 +- .../be/scri/helpers/ui/KeyboardUIManager.kt | 2 +- .../be/scri/services/GeneralKeyboardIME.kt | 5 +- .../main/java/be/scri/helpers/KeyboardBase.kt | 2 +- .../be/scri/helpers/ConjugationHandlerTest.kt | 99 ++++++++++--------- 7 files changed, 94 insertions(+), 98 deletions(-) diff --git a/app/src/keyboards/java/be/scri/helpers/ConjugationHandler.kt b/app/src/keyboards/java/be/scri/helpers/ConjugationHandler.kt index b6fbb00cf..d1f8c1015 100644 --- a/app/src/keyboards/java/be/scri/helpers/ConjugationHandler.kt +++ b/app/src/keyboards/java/be/scri/helpers/ConjugationHandler.kt @@ -19,53 +19,38 @@ class ConjugationHandler( private val ime: KeyboardIMEContext, ) { var subsequentAreaRequired: Boolean = false - var subsequentData: MutableList> = mutableListOf() + private set /** * Saves the type of conjugation layout being used (e.g., "2x2", "none") to shared preferences. * - * @param language The current keyboard language. - * @param isSubsequent true if saving for a sub-view, false for standard layout. + * For the **primary conjugation view** (isSubsequent = false): + * - If [language] is `"none"` (idle/reset sentinel), saves `"none"` directly. + * - Otherwise uses [KeyboardIMEContext.defaultConjugateModeType], so each IME subclass + * controls its own mode without a hardcoded language list here. + * + * For a **sub-view** (isSubsequent = true), saves [subViewMode] (e.g. "2x1" or "1x3") + * so that [be.scri.helpers.KeyboardBase] picks the correct row height. + * + * @param language The current keyboard language, or "none" to reset to the idle mode. + * @param isSubsequent true if saving for a sub-view, false for the standard conjugation view. + * @param subViewMode The layout mode string for the sub-view. Only used when [isSubsequent] is true. */ fun saveConjugateModeType( language: String, isSubsequent: Boolean = false, + subViewMode: String = "none", ) { val mode = - if (!isSubsequent) { - when (language) { - "English", "Russian", "Swedish", - "German", "French", "Italian", "Portuguese", "Spanish", - "en", "ru", "sv", "de", "fr", "it", "pt", "es", - -> "2x2" - else -> "none" - } - } else { - "none" + when { + isSubsequent -> subViewMode + language == "none" -> "none" + else -> ime.defaultConjugateModeType } val sharedPref = ime.imeContext.getSharedPreferences("keyboard_preferences", MODE_PRIVATE) sharedPref.edit { putString("conjugate_mode_type", mode) } } - /** - * Retrieves and validates the stored index for the current conjugation view. - * Ensures the index is within the bounds of available conjugation types. - * - * @return A valid, zero-based index for the conjugation type. - */ - fun getValidatedConjugateIndex(): Int { - val prefs = ime.imeContext.getSharedPreferences("keyboard_preferences", MODE_PRIVATE) - var index = prefs.getInt("conjugate_index", 0) - val maxIndex = - ime.conjugateOutput - ?.keys - ?.count() - ?.minus(1) ?: -1 - index = if (maxIndex >= 0) index.coerceIn(0, maxIndex) else 0 - prefs.edit { putInt("conjugate_index", index) } - return index - } - /** * Handles a key press on one of the special conjugation keys. * It either commits the text directly or prepares for a subsequent selection view. @@ -102,8 +87,14 @@ class ConjugationHandler( val uniqueData = data.distinct() val filteredData = uniqueData.filter { sublist -> sublist.contains(word) } val flattenList = filteredData.flatten() - saveConjugateModeType(language = ime.language, isSubsequent = true) - val keyboardXmlId = getKeyboardLayoutForState(ime.currentState, isSubsequentArea = true, dataSize = flattenList.size) + val keyboardXmlId = getKeyboardLayoutForState(ime.currentState, dataSize = flattenList.size) + val subViewMode = + when (flattenList.size) { + DATA_SIZE_2 -> "2x1" + DATA_SIZE_3 -> "1x3" + else -> return + } + saveConjugateModeType(language = ime.language, isSubsequent = true, subViewMode = subViewMode) ime.uiManager.initializeKeyboard(keyboardXmlId) when (flattenList.size) { DATA_SIZE_2 -> { @@ -123,29 +114,25 @@ class ConjugationHandler( } /** - * Determines which keyboard layout XML to use based on the current [ScribeState]. + * Determines which keyboard layout XML to use for a conjugation sub-view based on the number + * of items in the sub-view. Only called from [setupConjugateSubView] for sizes 2 and 3; + * other sizes trigger an early return before this is used. * * @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. */ private fun getKeyboardLayoutForState( state: ScribeState, - isSubsequentArea: Boolean = false, - dataSize: Int = 0, + dataSize: Int, ): Int = when (state) { ScribeState.SELECT_VERB_CONJUNCTION -> { - 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 - } + when (dataSize) { + DATA_SIZE_2 -> R.xml.conjugate_view_2x1 + DATA_SIZE_3 -> R.xml.conjugate_view_1x3 + else -> ime.defaultConjugateLayoutXML } } diff --git a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt index ec4035219..05b40e5dd 100644 --- a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt +++ b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt @@ -339,7 +339,7 @@ class KeyHandler( ime.saveConjugateModeType(language, isSubsequentArea = false) } else { val word = ime.handleConjugateKeys(code, true) - ime.setupConjugateSubView(ime.returnSubsequentData(), word) + ime.setupConjugateSubView(emptyList(), word) } } diff --git a/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt b/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt index 378759465..183ac9544 100644 --- a/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt +++ b/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt @@ -75,6 +75,7 @@ interface KeyboardIMEContext { val emojiKeywords: HashMap>? var conjugateOutput: MutableMap>>? val defaultConjugateLayoutXML: Int + val defaultConjugateModeType: String fun getKeyboardLayoutXML(): Int @@ -196,8 +197,6 @@ interface KeyboardIMEContext { word: String?, ) - fun returnSubsequentData(): List> - fun getKeyboardWidth(): Int fun recreateKeyboard() diff --git a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt index 7a78353da..ff0656b56 100644 --- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt +++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt @@ -673,7 +673,7 @@ class KeyboardUIManager( * Retrieves and validates the stored index for the current conjugation view. * Ensures the index is within the bounds of available conjugation types. */ - private fun getValidatedConjugateIndex(conjugateOutput: Map?): Int { + internal fun getValidatedConjugateIndex(conjugateOutput: Map?): Int { val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) var index = prefs.getInt("conjugate_index", 0) val maxIndex = conjugateOutput?.keys?.count()?.minus(1) ?: -1 diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index 7c9b63e76..a949e23e4 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -103,7 +103,7 @@ abstract class GeneralKeyboardIME( abstract var switchToLetters: Boolean // Language-specific layout and behavior configurations (decoupled from base class). - open val defaultConjugateModeType: String = "3x2" + open override val defaultConjugateModeType: String = "3x2" override val defaultConjugateLayoutXML: Int = R.xml.conjugate_view_3x2 open val isPluralCapitalized: Boolean = false @@ -789,7 +789,7 @@ abstract class GeneralKeyboardIME( override fun commitText(text: String) { if (currentState == ScribeState.SELECT_VERB_CONJUNCTION) { val label = text.trim() - val conjugateIndex = conjugationHandler.getValidatedConjugateIndex() + val conjugateIndex = uiManager.getValidatedConjugateIndex(conjugateOutput) val title = conjugateOutput?.keys?.elementAtOrNull(conjugateIndex) val languageOutput = title?.let { conjugateOutput!![it] } @@ -1244,7 +1244,6 @@ abstract class GeneralKeyboardIME( */ override fun returnIsSubsequentRequired(): Boolean = conjugationHandler.subsequentAreaRequired - override fun returnSubsequentData(): List> = conjugationHandler.subsequentData /** * Handles a key press on one of the special conjugation keys. diff --git a/app/src/main/java/be/scri/helpers/KeyboardBase.kt b/app/src/main/java/be/scri/helpers/KeyboardBase.kt index 9704e93f3..354368f71 100644 --- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt +++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt @@ -217,7 +217,7 @@ class KeyboardBase { when (conjugateMode) { "2x2" -> res.getDimension(R.dimen.conjugate_view_key_height_2x2).toInt() "3x3" -> res.getDimension(R.dimen.conjugate_view_key_height_3x3).toInt() - "2x1" -> res.getDimension(R.dimen.conjugate_view_key_height_2x1).toInt() + "2x1", "1x3" -> res.getDimension(R.dimen.conjugate_view_key_height_2x1).toInt() else -> res.getDimension(R.dimen.conjugate_view_key_height_3x3).toInt() } } else { diff --git a/app/src/testKeyboards/kotlin/be/scri/helpers/ConjugationHandlerTest.kt b/app/src/testKeyboards/kotlin/be/scri/helpers/ConjugationHandlerTest.kt index ed723bd1e..03a9a7530 100644 --- a/app/src/testKeyboards/kotlin/be/scri/helpers/ConjugationHandlerTest.kt +++ b/app/src/testKeyboards/kotlin/be/scri/helpers/ConjugationHandlerTest.kt @@ -31,8 +31,8 @@ import org.robolectric.annotation.Config class ConjugationHandlerTest { private lateinit var context: Context private lateinit var binding: InputMethodViewBinding - private lateinit var uiManager: KeyboardUIManager private val ime = mockk(relaxed = true) + private val uiManager = mockk(relaxed = true) private val keyboardView = mockk(relaxed = true) private val inputConnection = mockk(relaxed = true) private val suggestionHandler = mockk(relaxed = true) @@ -43,15 +43,13 @@ class ConjugationHandlerTest { fun setUp() { context = ApplicationProvider.getApplicationContext() binding = InputMethodViewBinding.inflate(LayoutInflater.from(context)) - val listener = mockk(relaxed = true) - uiManager = KeyboardUIManager(binding, context, listener) every { ime.imeContext } returns context every { ime.keyboardView } returns keyboardView every { ime.getInputConnection() } returns inputConnection every { ime.suggestionHandler } returns suggestionHandler every { ime.uiManager } returns uiManager - every { ime.binding } returns binding + every { uiManager.binding } returns binding handler = ConjugationHandler(ime) } @@ -85,64 +83,52 @@ class ConjugationHandlerTest { assertEquals(listOf("GO", "RUN", ""), result["Present"]?.get("I")) } - @Test - fun getValidatedConjugateIndex_clampsToValidRange() { - val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) - prefs.edit().putInt("conjugate_index", 5).commit() - - every { ime.conjugateOutput } returns - mutableMapOf( - "Present" to mutableMapOf(), - "Past" to mutableMapOf(), - ) - - val index = handler.getValidatedConjugateIndex() - - // Max index is 1 (size 2 - 1) - assertEquals(1, index) - assertEquals(1, prefs.getInt("conjugate_index", -1)) - } @Test - fun getValidatedConjugateIndex_nullOrEmptyOutputDefaultsToZero() { - val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) - prefs.edit().putInt("conjugate_index", 3).commit() - - every { ime.conjugateOutput } returns null + fun saveConjugateModeType_usesDefaultConjugateModeTypeFromIme() { + every { ime.defaultConjugateModeType } returns "2x2" - val index = handler.getValidatedConjugateIndex() + handler.saveConjugateModeType("Spanish", isSubsequent = false) - assertEquals(0, index) - assertEquals(0, prefs.getInt("conjugate_index", -1)) + val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) + assertEquals("2x2", prefs.getString("conjugate_mode_type", null)) } @Test - fun saveConjugateModeType_spanishReturns2x2() { - handler.saveConjugateModeType("Spanish", isSubsequent = false) + fun saveConjugateModeType_3x2ImeReturns3x2() { + every { ime.defaultConjugateModeType } returns "3x2" + + handler.saveConjugateModeType("French", isSubsequent = false) val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) - assertEquals("2x2", prefs.getString("conjugate_mode_type", null)) + assertEquals("3x2", prefs.getString("conjugate_mode_type", null)) } @Test - fun saveConjugateModeType_englishReturns2x2() { - handler.saveConjugateModeType("English", isSubsequent = false) + fun saveConjugateModeType_unsupportedLanguageReturnsNoneFromIme() { + // Hindi is not a supported conjugation language — the IME default covers this. + every { ime.defaultConjugateModeType } returns "none" + + handler.saveConjugateModeType("Hindi", isSubsequent = false) val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) - assertEquals("2x2", prefs.getString("conjugate_mode_type", null)) + assertEquals("none", prefs.getString("conjugate_mode_type", null)) } @Test - fun saveConjugateModeType_noneReturnsNone() { - handler.saveConjugateModeType("none", isSubsequent = false) + fun saveConjugateModeType_subsequentSavesSubViewMode() { + handler.saveConjugateModeType("English", isSubsequent = true, subViewMode = "2x1") val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) - assertEquals("none", prefs.getString("conjugate_mode_type", null)) + assertEquals("2x1", prefs.getString("conjugate_mode_type", null)) } @Test - fun saveConjugateModeType_subsequentReturnsNone() { - handler.saveConjugateModeType("English", isSubsequent = true) + fun saveConjugateModeType_noneSentinelSavesNoneRegardlessOfImeDefault() { + // IME default is "2x2" but passing "none" as language resets to idle mode. + every { ime.defaultConjugateModeType } returns "2x2" + + handler.saveConjugateModeType("none", isSubsequent = false) val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) assertEquals("none", prefs.getString("conjugate_mode_type", null)) @@ -180,17 +166,42 @@ class ConjugationHandlerTest { verify(exactly = 0) { inputConnection.commitText(any(), any()) } } + @Test + fun setupConjugateSubView_withDataSizeZero_earlyReturnsWithoutInflatingKeyboard() { + every { ime.currentState } returns ScribeState.SELECT_VERB_CONJUNCTION + every { ime.defaultConjugateLayoutXML } returns R.xml.conjugate_view_3x2 + + // Empty data — flattenList.size == 0 hits else -> return before initializeKeyboard + handler.setupConjugateSubView(emptyList(), word = null) + + verify(exactly = 0) { uiManager.initializeKeyboard(any()) } + } + + @Test + fun setupConjugateSubView_withUnsupportedDataSize_usesDefaultConjugateLayoutXML() { + every { ime.language } returns "Spanish" + every { ime.currentState } returns ScribeState.SELECT_VERB_CONJUNCTION + every { ime.defaultConjugateModeType } returns "2x2" + every { ime.defaultConjugateLayoutXML } returns R.xml.conjugate_view_3x2 + + // 4 items → not 2 or 3, so getKeyboardLayoutForState returns defaultConjugateLayoutXML + val data = listOf(listOf("hablo", "hablas", "habla", "hablamos")) + handler.setupConjugateSubView(data, word = "hablo") + + verify { uiManager.initializeKeyboard(R.xml.conjugate_view_3x2) } + } + @Test fun setupConjugateSubView_withTwoItems_configures2x1Layout() { every { ime.language } returns "Spanish" every { ime.currentState } returns ScribeState.SELECT_VERB_CONJUNCTION - handler.subsequentAreaRequired = true + every { ime.defaultConjugateModeType } returns "2x2" val data = listOf(listOf("hablo", "hablas")) handler.setupConjugateSubView(data, word = "hablo") val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) - assertEquals("none", prefs.getString("conjugate_mode_type", null)) + assertEquals("2x1", prefs.getString("conjugate_mode_type", null)) verify { uiManager.initializeKeyboard(R.xml.conjugate_view_2x1) } verify { keyboardView.setKeyLabel("hablo", "HI", KeyboardBase.CODE_2X1_TOP) } verify { keyboardView.setKeyLabel("hablas", "HI", KeyboardBase.CODE_2X1_BOTTOM) } @@ -202,13 +213,13 @@ class ConjugationHandlerTest { fun setupConjugateSubView_withThreeItems_configures1x3Layout() { every { ime.language } returns "Spanish" every { ime.currentState } returns ScribeState.SELECT_VERB_CONJUNCTION - handler.subsequentAreaRequired = true + every { ime.defaultConjugateModeType } returns "2x2" val data = listOf(listOf("hablo", "hablas", "habla")) handler.setupConjugateSubView(data, word = "hablo") val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE) - assertEquals("none", prefs.getString("conjugate_mode_type", null)) + assertEquals("1x3", prefs.getString("conjugate_mode_type", null)) verify { uiManager.initializeKeyboard(R.xml.conjugate_view_1x3) } verify { keyboardView.setKeyLabel("hablo", "HI", KeyboardBase.CODE_1X3_LEFT) } verify { keyboardView.setKeyLabel("hablas", "HI", KeyboardBase.CODE_1X3_CENTER) } From 3b0c025c5befbd954a282866b2996292c9f58f3a Mon Sep 17 00:00:00 2001 From: prince-0408 <66916296+prince-0408@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:26:52 +0530 Subject: [PATCH 3/4] fix(ci): fix ktlint consecutive blank lines and sub-view test assertion --- .../java/be/scri/services/GeneralKeyboardIME.kt | 1 - .../kotlin/be/scri/helpers/ConjugationHandlerTest.kt | 12 +++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index a949e23e4..ec8df1cfd 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -1244,7 +1244,6 @@ abstract class GeneralKeyboardIME( */ override fun returnIsSubsequentRequired(): Boolean = conjugationHandler.subsequentAreaRequired - /** * Handles a key press on one of the special conjugation keys. * Delegated to [ConjugationHandler]. diff --git a/app/src/testKeyboards/kotlin/be/scri/helpers/ConjugationHandlerTest.kt b/app/src/testKeyboards/kotlin/be/scri/helpers/ConjugationHandlerTest.kt index 03a9a7530..2e1ae5b27 100644 --- a/app/src/testKeyboards/kotlin/be/scri/helpers/ConjugationHandlerTest.kt +++ b/app/src/testKeyboards/kotlin/be/scri/helpers/ConjugationHandlerTest.kt @@ -83,7 +83,6 @@ class ConjugationHandlerTest { assertEquals(listOf("GO", "RUN", ""), result["Present"]?.get("I")) } - @Test fun saveConjugateModeType_usesDefaultConjugateModeTypeFromIme() { every { ime.defaultConjugateModeType } returns "2x2" @@ -178,17 +177,12 @@ class ConjugationHandlerTest { } @Test - fun setupConjugateSubView_withUnsupportedDataSize_usesDefaultConjugateLayoutXML() { - every { ime.language } returns "Spanish" - every { ime.currentState } returns ScribeState.SELECT_VERB_CONJUNCTION - every { ime.defaultConjugateModeType } returns "2x2" - every { ime.defaultConjugateLayoutXML } returns R.xml.conjugate_view_3x2 - - // 4 items → not 2 or 3, so getKeyboardLayoutForState returns defaultConjugateLayoutXML + fun setupConjugateSubView_withUnsupportedDataSize_earlyReturnsWithoutInflatingKeyboard() { + // 4 items → not 2 or 3, so setupConjugateSubView early-returns without inflating keyboard val data = listOf(listOf("hablo", "hablas", "habla", "hablamos")) handler.setupConjugateSubView(data, word = "hablo") - verify { uiManager.initializeKeyboard(R.xml.conjugate_view_3x2) } + verify(exactly = 0) { uiManager.initializeKeyboard(any()) } } @Test From 443a1f262e0baed83ee6163ff5c45883ff8b7006 Mon Sep 17 00:00:00 2001 From: prince-0408 <66916296+prince-0408@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:39:52 +0530 Subject: [PATCH 4/4] refactor: address Roniscend review comments on ConjugationHandler (Part 13) - #426 --- .../keyboards/java/be/scri/helpers/ConjugationHandler.kt | 1 + app/src/keyboards/java/be/scri/helpers/KeyHandler.kt | 2 +- .../keyboards/java/be/scri/helpers/KeyboardIMEContext.kt | 2 ++ .../keyboards/java/be/scri/services/GeneralKeyboardIME.kt | 8 +++++++- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/keyboards/java/be/scri/helpers/ConjugationHandler.kt b/app/src/keyboards/java/be/scri/helpers/ConjugationHandler.kt index d1f8c1015..19b2ed73e 100644 --- a/app/src/keyboards/java/be/scri/helpers/ConjugationHandler.kt +++ b/app/src/keyboards/java/be/scri/helpers/ConjugationHandler.kt @@ -20,6 +20,7 @@ class ConjugationHandler( ) { var subsequentAreaRequired: Boolean = false private set + var subsequentData: MutableList> = mutableListOf() /** * Saves the type of conjugation layout being used (e.g., "2x2", "none") to shared preferences. diff --git a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt index 05b40e5dd..ec4035219 100644 --- a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt +++ b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt @@ -339,7 +339,7 @@ class KeyHandler( ime.saveConjugateModeType(language, isSubsequentArea = false) } else { val word = ime.handleConjugateKeys(code, true) - ime.setupConjugateSubView(emptyList(), word) + ime.setupConjugateSubView(ime.returnSubsequentData(), word) } } diff --git a/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt b/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt index 183ac9544..31c97cc4e 100644 --- a/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt +++ b/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt @@ -197,6 +197,8 @@ interface KeyboardIMEContext { word: String?, ) + fun returnSubsequentData(): List> + fun getKeyboardWidth(): Int fun recreateKeyboard() diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index ec8df1cfd..7aa40cecb 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -103,7 +103,7 @@ abstract class GeneralKeyboardIME( abstract var switchToLetters: Boolean // Language-specific layout and behavior configurations (decoupled from base class). - open override val defaultConjugateModeType: String = "3x2" + open override val defaultConjugateModeType: String = "2x2" override val defaultConjugateLayoutXML: Int = R.xml.conjugate_view_3x2 open val isPluralCapitalized: Boolean = false @@ -1262,6 +1262,12 @@ abstract class GeneralKeyboardIME( word: String?, ) = conjugationHandler.setupConjugateSubView(data, word) + /** + * Returns the subsequent dataset for conjugation sub-views. + * Delegated to [ConjugationHandler]. + */ + override fun returnSubsequentData(): List> = conjugationHandler.subsequentData + /** * Updates the visibility of the suggestion buttons based on device type (phone/tablet) * and whether auto-suggestions are currently active.