Skip to content
CleverKeys Wiki
implemented v2.0.0 development User guide

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

ComponentFilePurpose
KeyboardReceiverKeyboardReceiver.kt (handle_event_key)Handles SWITCH_FORWARD, SWITCH_BACKWARD, SWITCH_GREEKMATH events
LayoutManagerLayoutManager.ktCurrent text/special layout; incrTextLayout, setTextLayout, current_layout_unmodified
LayoutBridgewiring/LayoutBridge.ktService-facing wrapper that also applies the layout to the view
ConfigConfig.ktlayouts, layout_languages, get_current_layout, set_current_layout
LayoutLanguageBindingLayoutLanguageBinding.ktResolves the active languages for the selected layout
ActiveLanguageSyncActiveLanguageSync.ktReloads 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.

SwitchEffect on languages
Unbound → unboundNone; nothing reloads, no message
Unbound → bound to XPrimary becomes X, secondary unloaded; bar shows “Language: X” (or “(no dictionary installed)”)
Bound to X → bound to YPrimary becomes Y; bar shows “Language: Y”
Bound → unboundBack 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.

SwitchEffect 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 XRe-inflated in X; suggestion strip rebuilt with it while shown
Bound to X → follow globalRe-inflated in the global theme
Bound to a theme missing on the deviceGlobal 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

SettingKeyDefaultDescription
LayoutslayoutsBuilt-in default listOrdered enabled layouts; each entry may carry a language and a theme binding
Portrait selectioncurrent_layout_portrait0Selected index when not wide
Landscape selectioncurrent_layout_landscape0Selected index when wide (≥ 600 dp)

Test Coverage

SuiteFile
Pure JVMsrc/test/kotlin/tribixbite/cleverkeys/LayoutLanguageBindingTest.kt (includes a three-language cycle)
Pure JVMsrc/test/kotlin/tribixbite/cleverkeys/ActiveLanguageSyncTest.kt (unbound switches reload nothing)
Mocksrc/test/kotlin/tribixbite/cleverkeys/LayoutLanguageBindingConfigTest.kt (set_current_layout onto a bound layout)
Pure JVMsrc/test/kotlin/tribixbite/cleverkeys/LayoutThemeBindingTest.kt (bound / unbound / missing theme resolution, XML attribute)
Mocksrc/test/kotlin/tribixbite/cleverkeys/LayoutThemeBindingConfigTest.kt (re-inflate only when the effective theme changes)