Skip to content

refactor: extract ConjugationHandler from GeneralKeyboardIME (Part 12) - #426 - #687

Open
prince-0408 wants to merge 2 commits into
scribe-org:mainfrom
prince-0408:refactor/extract-conjugation-handler-426
Open

prince-0408 wants to merge 2 commits into
scribe-org:mainfrom
prince-0408:refactor/extract-conjugation-handler-426

Conversation

@prince-0408

Copy link
Copy Markdown
Collaborator

Description

This PR is Part 12 in modularizing GeneralKeyboardIME for #426.

It extracts verb conjugation state management, conjugation table layout calculation (2x1, 1x3, 3x2, 2x2), capitalization formatting, index boundary validation, and sub-view setup out of GeneralKeyboardIME.kt into a standalone helper class ConjugationHandler.

Detailed Changes Table

File / Component Changes Applied Detailed Impact
ConjugationHandler.kt Created standalone helper encapsulating conjugation state (subsequentAreaRequired, subsequentData, conjugateOutput, conjugateLabels), layout mode saving (saveConjugateModeType), capitalization formatting (applyCapitalizationToConjugations), index validation (getValidatedConjugateIndex), conjugation key press handling (handleConjugateKeys), secondary sub-view setup (setupConjugateSubView), and layout XML selection (getKeyboardLayoutForState). Extracts verb conjugation table formatting, multi-option selection sub-views, and layout grid mode calculations out of GeneralKeyboardIME.kt into a dedicated helper class.
GeneralKeyboardIME.kt Instantiated conjugationHandler and delegated conjugation state properties (subsequentAreaRequired, subsequentData) and helper methods (saveConjugateModeType, applyCapitalizationToConjugations, getValidatedConjugateIndex, returnIsSubsequentRequired, returnSubsequentData, handleConjugateKeys, setupConjugateSubView, getKeyboardLayoutForState). Removed duplicate private helper implementations and unused constants. Fulfills the core goal of #426 by decoupling verb conjugation layout and state management from GeneralKeyboardIME.kt while maintaining 100% backward compatibility for all callers.
ConjugationHandlerTest.kt Added unit tests covering capitalization formatting (applyCapitalizationToConjugations), index boundary clamping (getValidatedConjugateIndex), layout mode saving (saveConjugateModeType), and XML layout selection (getKeyboardLayoutForState). Ensures unit test coverage for verb conjugation helper logic using Robolectric and MockK.
CHANGELOG.md Added entry under ### ♻️ Code Refactoring detailing the extraction of ConjugationHandler from GeneralKeyboardIME. Satisfies ci_changelog_check workflow requirement for pull requests targeting main.

Key Benefits

  • Decoupled Conjugation & Layout Logic: Removes complex grid layout calculations (2x1, 1x3, 3x2, 2x2), verb tense capitalization rules, and multi-option selection sub-views from GeneralKeyboardIME.kt.
  • Improved Testability & Maintainability: Verb conjugation index validation, mode persistence, layout XML selection, and text capitalization can now be tested and maintained independently of IME service lifecycles.

Related Issue

Refactors part of #426

@angrezichatterbox angrezichatterbox added the refactor Refactor code to improve quality label Aug 30, 2026
@andrewtavis

Copy link
Copy Markdown
Member

Would you be able to fix the merge conflicts, @prince-0408? We'd then get to the review :)

@prince-0408
prince-0408 force-pushed the refactor/extract-conjugation-handler-426 branch from 1cd39ae to d2543e8 Compare September 5, 2026 18:39
@andrewtavis andrewtavis added the no-changelog No changelog entry is needed for this pull request label Sep 13, 2026
@prince-0408
prince-0408 force-pushed the refactor/extract-conjugation-handler-426 branch 2 times, most recently from f2a4f9e to 71c7ea3 Compare September 13, 2026 14:23
@andrewtavis

Copy link
Copy Markdown
Member

Thanks for the rebase, @prince-0408! We'll focus on bringing this one in next :)

var subsequentAreaRequired: Boolean = false
var subsequentData: MutableList<List<String>> = mutableListOf()

var conjugateOutput: MutableMap<String, MutableMap<String, Collection<String>>>?

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.

This only passes through to ime.conjugateOutput, so read it from ime directly and drop the property.

ime.conjugateOutput = value
}

var conjugateLabels: Set<String>

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.

ConjugateLabels is never used in the handler, so it can be deleted.

) {
val mode =
if (isSubsequent) {
"2x1"

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.

Sub-view now saves "2x1" here, but it used to save "none". Please keep the original behavior.

if (isSubsequent) {
"2x1"
} else {
when (getLanguageAlias(language).lowercase()) {

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.

SaveConjugateModeType("none") now resolves to English and saves 2x2, so the idle keyboard gets conjugation key heights

"2x1"
} else {
when (getLanguageAlias(language).lowercase()) {
"es", "it" -> "3x2"

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.

3x2 has no branch in KeyboardBase, so Spanish and Italian fall through to 3x3 height when they used to get 2x2.

conjugationHandler.subsequentAreaRequired = value
}

internal var subsequentData: MutableList<List<String>>

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.

Keep it private or remove the wrapper

* Delegated to [ConjugationHandler].
*/
private fun getKeyboardLayoutForState(
internal fun getKeyboardLayoutForState(

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.

This was private and is now internal ,does anything outside the class use it?

}

@Test
fun saveConjugateModeType_spanishReturns3x2() {

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.

Please add a test for saveConjugateModeType(none) that expects none


val prefs = context.getSharedPreferences("keyboard_preferences", Context.MODE_PRIVATE)
assertEquals("2x1", prefs.getString("conjugate_mode_type", null))
verify { keyboardView.setKeyLabel("hablo", "HI", KeyboardBase.CODE_1X3_RIGHT) }

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.

This asserts the LEFT/RIGHT bug, so please update it once line 164 is fixed.

}

@Test
fun returnSubsequentData_and_conjugateLabels() {

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.

This only checks that values are passed through,please test the behavior instead

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Roniscend, big question for the initial review is whether this has been resolved. Feel free to resolve the thread if so :)

@prince-0408
prince-0408 force-pushed the refactor/extract-conjugation-handler-426 branch 2 times, most recently from 6d344f3 to 2af3627 Compare September 14, 2026 10:35
@prince-0408
prince-0408 force-pushed the refactor/extract-conjugation-handler-426 branch from 2af3627 to 949c234 Compare September 14, 2026 10:41
@andrewtavis

Copy link
Copy Markdown
Member

Looks like there are a few minor edits from @Roniscend's review here, @prince-0408 😊 Let us know if you need any support finalizing them 😊

@prince-0408

Copy link
Copy Markdown
Collaborator Author

Looks like there are a few minor edits from @Roniscend's review here, @prince-0408 😊 Let us know if you need any support finalizing them 😊

Hi @andrewtavis

Everything is up to date and ready for review! 😊

@andrewtavis

Copy link
Copy Markdown
Member

CC @Roniscend that an initial review here would be appreciated :)

val flattenList = filteredData.flatten()
saveConjugateModeType(language = ime.language, isSubsequent = true)
val keyboardXmlId = getKeyboardLayoutForState(ime.currentState, isSubsequentArea = true, dataSize = flattenList.size)
ime.uiManager.initializeKeyboard(keyboardXmlId)

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.

Conjugate_mode_type is none when the sub-view inflates here, but on main the removed saveConjugateModeType inside getKeyboardLayoutForState left it 2x2, was the row-height change intended?

when (language) {
"English", "Russian", "Swedish",
"German", "French", "Italian", "Portuguese", "Spanish",
"en", "ru", "sv", "de", "fr", "it", "pt", "es",

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.

Ime.language is scribeLanguage.displayName , so these ISO-code branches are unreachable.

if (!isSubsequent) {
when (language) {
"English", "Russian", "Swedish",
"German", "French", "Italian", "Portuguese", "Spanish",

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.

This hardcoded list duplicates defaultConjugateModeType, which each IME already overrides , expose that on KeyboardIMEContext and read it instead.

*
* @return A valid, zero-based index for the conjugation type.
*/
fun getValidatedConjugateIndex(): Int {

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.

This is byte-identical to the private getValidatedConjugateIndex already in KeyboardUIManager, keep one and delete the other

private val ime: KeyboardIMEContext,
) {
var subsequentAreaRequired: Boolean = false
var subsequentData: MutableList<List<String>> = mutableListOf()

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.

SubsequentData is never assigned anywhere in the codebase, so returnSubsequentData() always returns empty wire it up or drop it


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) }

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.

UiManager is a real KeyboardUIManager , not a mockk, so MockK verifies the relaxed listener calls made inside initializeKeyboard instead and this passes even with the wrong XML id.


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) }

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.

Same here ,mock uiManager or assert on the inflated KeyboardBase so the layout id is actually checked


@Test
fun saveConjugateModeType_noneReturnsNone() {
handler.saveConjugateModeType("none", isSubsequent = false)

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.

None isn't a language the IME can pass , use a genuinely unsupported one like Hindi

every { ime.getInputConnection() } returns inputConnection
every { ime.suggestionHandler } returns suggestionHandler
every { ime.uiManager } returns uiManager
every { ime.binding } returns binding

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.

Every { ime.binding } returns binding is unused , the handler reads ime.uiManager.binding.

assertEquals(View.GONE, binding.ivInfo.visibility)
assertFalse(handler.subsequentAreaRequired)
}
}

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.

No test covers the dataSize == 0 path returning defaultConjugateLayoutXML, which is the only branch using the new interface member.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog No changelog entry is needed for this pull request refactor Refactor code to improve quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants