Newer
Older
package org.futo.inputmethod.v2keyboard
import android.content.Context
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer
import org.futo.inputmethod.keyboard.internal.KeyboardParams
object RowKeyListSerializer : SpacedListSerializer<Key>(KeyPathSerializer)
const val NumberRowHeight: Double = 0.8
enum class RowNumberRowMode(val displayByDefault: Boolean, val displayWhenExplicitlyActive: Boolean, val displayWhenExplicitlyInactive: Boolean) {
Default(true, true, true),
Filler(false, true, false),
Hideable(false, false, true),
}
Aleksandras Kostarevas
committed
typealias KeyList = @Serializable(with = RowKeyListSerializer::class) List<Key>
/**
* A keyboard row. Only one of [numbers], [letters], or [bottom] must be defined. The row type is
* determined by which one of those is defined.
*/
@Serializable
data class Row(
Aleksandras Kostarevas
committed
/**
* If defined, this is a number row. Number rows by default have grow keys, no background,
* and smaller height. They may also be hidden depending on the user settings and the value
* of [Keyboard.numberRowMode].
*
* See [DefaultNumberRow]
*/
val numbers: KeyList? = null,
/**
* If defined, this is a letters row. Letter rows by default are splittable.
*/
val letters: KeyList? = null,
Aleksandras Kostarevas
committed
/**
* If defined, this is a bottom row. Bottom row should typically contain:
* $symbols $action $space $contextual $enter
*
* See [DefaultBottomRow]
*/
val bottom: KeyList? = null,
/**
* (optional) The height multiplier for this row
*/
val rowHeight: Double = if(numbers == null) { 1.0 } else { NumberRowHeight },
Aleksandras Kostarevas
committed
/**
* (optional) Whether or not this row is splittable. Enabled for letter rows by default.
*/
val splittable: Boolean = letters != null,
Aleksandras Kostarevas
committed
/**
* (optional) How this row should behave with respect to the number row. Valid values:
* * `Default` - always display this row
* * `Filler` - only display when the number row is explicitly active
* * `Hideable` - only display when the number row is explicitly inactive
*/
val numRowMode: RowNumberRowMode = RowNumberRowMode.Default,
Aleksandras Kostarevas
committed
/**
* (optional) Default key attributes for keys in this row. Values set here supersede values
* set in `Keyboard.attributes`.
*/
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
val attributes: KeyAttributes = KeyAttributes(
style = when {
numbers != null -> KeyVisualStyle.NoBackground
bottom != null -> KeyVisualStyle.Functional
else -> null
},
width = when {
numbers != null -> KeyWidth.Grow
else -> null
},
)
) {
val keys: List<Key>
get() = bottom ?: numbers ?: letters!!
val isNumberRow: Boolean
get() = numbers != null
val isLetterRow: Boolean
get() = letters != null
val isBottomRow: Boolean
get() = bottom != null
init {
// Ensure only one is defined
assert(
(numbers != null && letters == null && bottom == null)
|| (numbers == null && letters != null && bottom == null)
|| (numbers == null && letters == null && bottom != null)
) {
"Only one of numbers, letters, or actions can be defined per row. In yaml, make sure you did not miss a `-` to start a new row."
}
}
}
val DefaultNumberRow = Row(
numbers = "1234567890".mapIndexed { i, c ->
CaseSelector(
normal = BaseKey(c.toString()),
shiftedManually = BaseKey("!@#$%^&*()"[i].toString())
)
}
)
val DefaultBottomRow = Row(
bottom = listOf(
TemplateSymbolsKey,
BaseKey(","),
TemplateActionKey,
TemplateSpaceKey,
TemplateContextualKey,
BaseKey("."),
TemplateEnterKey
)
)
enum class NumberRowMode {
UserConfigurable,
AlwaysEnabled,
AlwaysDisabled
}
fun NumberRowMode.isActive(userSetting: Boolean) =
when(this) {
NumberRowMode.UserConfigurable -> userSetting
NumberRowMode.AlwaysEnabled -> true
NumberRowMode.AlwaysDisabled -> false
}
enum class BottomRowHeightMode {
Fixed,
Flexible
}
enum class BottomRowWidthMode(val separateFunctional: Boolean) {
SeparateFunctional(true),
Identical(false)
}
enum class RowHeightMode(val clampHeight: Boolean) {
ClampHeight(true),
FillHeight(false)
}
object SpacedLanguageListSerializer : SpacedListSerializer<String>(String.serializer(), { it.split(" ") })
typealias SpacedStringList = @Serializable(with = SpacedLanguageListSerializer::class) List<String>
Aleksandras Kostarevas
committed
/**
* Override the symbols and other layouts for a specific layout.
*/
@Serializable
data class LayoutSetOverrides(
val symbols: String = "symbols",
val symbolsShifted: String = "symbols_shift",
val number: String = "number",
val numberShifted: String = "number_shift",
val phone: String = "phone",
val phoneShifted: String = "phone_shift"
)
/**
* A keyboard layout definition, the entry point for the layout yaml files.
*/
@Serializable
data class Keyboard(
Aleksandras Kostarevas
committed
/**
* The human-readable name of the layout. If the layout is for a specific language, this should
* be written in the relevant language.
*/
Aleksandras Kostarevas
committed
/**
* The rows defined for the layout. Defining the number row, bottom row, or the functional
* keys (shift/backspace) is optional here. If they are missing, defaults will automatically be
* added to `effectiveRows`.
*/
private val rows: List<Row>,
Aleksandras Kostarevas
committed
/**
* List of languages this layout is intended for. It will be displayed as an option for the
* specified languages.
*/
val languages: SpacedStringList = listOf(),
Aleksandras Kostarevas
committed
/**
* (optional) A human-readable description of the layout. Authorship/origin information may
* be added here. This is intended to be displayed to the user when they are selecting layouts.
*/
val description: String = "",
/**
* (optional) Override the symbols layout or other layouts for this layout set.
*/
val layoutSetOverrides: LayoutSetOverrides = LayoutSetOverrides(),
/**
* (optional) Whether the number row should be user-configurable, always displayed, or never.
*/
val numberRowMode: NumberRowMode = NumberRowMode.UserConfigurable,
Aleksandras Kostarevas
committed
/**
* (optional) Whether the bottom row should always maintain a consistent height, or whether
* it should grow and shrink.
*/
val bottomRowHeightMode: BottomRowHeightMode = BottomRowHeightMode.Fixed,
Aleksandras Kostarevas
committed
/**
* (optional) Whether the bottom row should follow key widths of other rows, or should maintain
* separate widths for consistency.
*/
val bottomRowWidthMode: BottomRowWidthMode = BottomRowWidthMode.SeparateFunctional,
Aleksandras Kostarevas
committed
/**
* (optional) Default attributes to use for all rows
*/
val attributes: KeyAttributes = KeyAttributes(),
Aleksandras Kostarevas
committed
/**
* (optional) Definitions of custom key widths. Values are between 0.0 and 1.0, with 1.0
* representing 100% of the keyboard width.
*/
val overrideWidths: Map<KeyWidth, Float> = mapOf(),
Aleksandras Kostarevas
committed
/**
* (optional) Whether or not rows should fill the vertical space, or have vertical gaps added.
*/
val rowHeightMode: RowHeightMode = RowHeightMode.ClampHeight,
Aleksandras Kostarevas
committed
/**
* (optional) Whether or not the ZWNJ key should be shown in place of the contextual key.
*/
val useZWNJKey: Boolean = false,
Aleksandras Kostarevas
committed
/**
* (optional) Minimum width for functional keys.
*/
val minimumFunctionalKeyWidth: Float = 0.125f,
Aleksandras Kostarevas
committed
/**
* (optional) Minimum width for functional keys in the bottom row.
*/
val minimumBottomRowFunctionalKeyWidth: Float = 0.15f,
Aleksandras Kostarevas
committed
/**
* (optional) Alternative pages for this layout, use in conjunction with $alt0, $alt1, $alt2
*/
val altPages: List<List<Row>> = listOf()
Aleksandras Kostarevas
committed
// TODO: Custom long-press key settings configuration
//val element: KeyboardElement = KeyboardElement.Alphabet,
//val rowWidthMode: RowWidthMode = RowWidthMode.PadSides,
//val script: Script = Script.Latin,
//val longPressKeysMode: LongPressKeysMode = LongPressKeysMode.UserConfigurable,
) {
var id: String = ""
private fun ensureRowsValid(rows: List<Row>) {
assert(rows.first().isNumberRow) { "The first row in a keyboard must be the number row" }
assert(rows.last().isBottomRow) { "The last row in a keyboard must be the bottom row" }
assert(rows.count { it.isNumberRow } == 1) { "Keyboard can only contain one number row" }
assert(rows.count { it.isBottomRow } == 1) { "Keyboard can only contain one bottom row" }
assert(rows.count { it.isLetterRow } in 1..8) { "Keyboard must contain between 1 and 8 letter rows" }
}
Aleksandras Kostarevas
committed
val effectiveRows = rows.toMutableList().apply {
if(find { it.isNumberRow } == null) {
add(0, DefaultNumberRow)
}
if(find { it.isBottomRow } == null) {
// If action row is not explicitly defined, shift and delete are implicitly added to last row
Aleksandras Kostarevas
committed
// (unless a row has explicitly defined shift or delete key)
if(!any {
it.isLetterRow && it.letters != null && (
it.letters.contains(TemplateShiftKey)
|| it.letters.contains(TemplateDeleteKey))
}) {
val ultimateRow = removeAt(size - 1)
assert(ultimateRow.isLetterRow)
val updatedRow = ultimateRow.copy(
letters = ultimateRow.letters!!.toMutableList().apply {
add(0, TemplateShiftKey)
add(TemplateDeleteKey)
}
Aleksandras Kostarevas
committed
)
add(updatedRow)
}
// Add default bottom row
add(DefaultBottomRow)
}
ensureRowsValid(this)
}.toList()
fun build(context: Context, params: KeyboardParams, layoutParams: LayoutParams): org.futo.inputmethod.keyboard.Keyboard {
val engine = LayoutEngine(context, this, params, layoutParams)
return engine.build()
}
}