Switching Layouts Technical Specification
Overview
The enabled text layouts are the ordered list Config.layouts (Layout Manager order).
switch_forward and switch_backward cycle through that list; special layouts (numeric,
emoji, Greek/math) are shown on top of the current text layout without changing the
selection. The selected index is stored separately for narrow and wide screens. Since
GH #186/#61 a layout may carry a language binding, so a layout switch can also be a
language switch; see Multi-Language Input.
Key Components
| Component | File | Purpose |
|---|---|---|
| KeyboardReceiver | KeyboardReceiver.kt (handle_event_key) | Handles SWITCH_FORWARD, SWITCH_BACKWARD, SWITCH_GREEKMATH events |
| LayoutManager | LayoutManager.kt | Current text/special layout; incrTextLayout, setTextLayout, current_layout_unmodified |
| LayoutBridge | wiring/LayoutBridge.kt | Service-facing wrapper that also applies the layout to the view |
| Config | Config.kt | layouts, layout_languages, get_current_layout, set_current_layout |
| LayoutLanguageBinding | LayoutLanguageBinding.kt | Resolves the active languages for the selected layout |
| ActiveLanguageSync | ActiveLanguageSync.kt | Reloads dictionaries/contractions and re-warms swipe when the active language changes |
Cycling
// KeyboardReceiver.kt
KeyValue.Event.SWITCH_FORWARD -> {
if (layoutManager.getLayoutCount() > 1) {
keyboardView.setKeyboard(layoutManager.incrTextLayout(1))
}
}
// LayoutManager.kt
fun incrTextLayout(delta: Int): KeyboardData {
val s = config.layouts.size
val newIndex = (config.get_current_layout() + delta + s) % s
return setTextLayout(newIndex)
}
setTextLayout calls Config.set_current_layout, clears any special layout and returns the
current layout with modifiers applied. With one enabled layout the switch events do nothing.
An index past the end of the list resolves to layout 0 (current_layout_unmodified); a
null entry is the System layout and resolves to the locale text layout.
Narrow and Wide Selection
Config.wide_screen is true when the screen is at least 600 dp wide
(WIDE_DEVICE_THRESHOLD). get_current_layout() returns current_layout_wide or
current_layout_narrow accordingly, and set_current_layout writes the one in use. Both
are persisted (current_layout_landscape, current_layout_portrait), so rotating can
change the selected layout.
Language Change on Switch
set_current_layout resolves the active languages immediately and re-publishes the config
snapshot when they changed, so gesture paths never read the old language:
// Config.kt
// GH #186/#61: a layout switch is a language switch when either layout is bound. Resolve
// now and re-publish the snapshot (gesture hot paths read primary_language from it), so
// nothing reads the old language between this call and the preference-listener refresh
// that follows the write below and drives ActiveLanguageSync.
if (recomputeActiveLanguages()) edit { }
The preference write that follows triggers a Config refresh; CleverKeysService.onConfigChanged
hands the active languages to ActiveLanguageSync, which reloads only what changed.
| Switch | Effect on languages |
|---|---|
| Unbound → unbound | None; nothing reloads, no message |
| Unbound → bound to X | Primary becomes X, secondary unloaded; bar shows “Language: X” (or “(no dictionary installed)”) |
| Bound to X → bound to Y | Primary becomes Y; bar shows “Language: Y” |
| Bound → unbound | Back to the user’s Multi-Language settings; no message |
A rotation that changes the narrow/wide selection follows the same path.
Theme Change on Switch
Each entry may also carry a theme binding (absent = the XML theme default, "none" = follow
the global theme, an id = bound). Unlike the language, set_current_layout does NOT resolve the
theme: the selection write triggers ConfigurationManager.refresh, which compares the theme
style id and name before and after Config.refresh (where recomputeActiveTheme resolves the
effective theme) and calls onThemeChanged only when they differ.
| Switch | Effect on the keyboard view |
|---|---|
| Same effective theme (both follow global, both bound to X, or bound to the global theme) | None; no re-inflation |
| Follow global → bound to X | Re-inflated in X; suggestion strip rebuilt with it while shown |
| Bound to X → follow global | Re-inflated in the global theme |
| Bound to a theme missing on the device | Global theme; Layout Manager marks the binding missing |
The global theme preference is never written. Full rules: docs/specs/layout-system.md
“Per-layout theme binding”.
Configuration
| Setting | Key | Default | Description |
|---|---|---|---|
| Layouts | layouts | Built-in default list | Ordered enabled layouts; each entry may carry a language and a theme binding |
| Portrait selection | current_layout_portrait | 0 | Selected index when not wide |
| Landscape selection | current_layout_landscape | 0 | Selected index when wide (≥ 600 dp) |
Test Coverage
| Suite | File |
|---|---|
| Pure JVM | src/test/kotlin/tribixbite/cleverkeys/LayoutLanguageBindingTest.kt (includes a three-language cycle) |
| Pure JVM | src/test/kotlin/tribixbite/cleverkeys/ActiveLanguageSyncTest.kt (unbound switches reload nothing) |
| Mock | src/test/kotlin/tribixbite/cleverkeys/LayoutLanguageBindingConfigTest.kt (set_current_layout onto a bound layout) |
| Pure JVM | src/test/kotlin/tribixbite/cleverkeys/LayoutThemeBindingTest.kt (bound / unbound / missing theme resolution, XML attribute) |
| Mock | src/test/kotlin/tribixbite/cleverkeys/LayoutThemeBindingConfigTest.kt (re-inflate only when the effective theme changes) |
Related Specifications
- Multi-Language Input - Active languages and per-layout binding
- Adding Layouts - Layout management
- Layout System - Full architecture