Newer
Older
package org.futo.inputmethod.v2keyboard
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer
Aleksandras Kostarevas
committed
import org.futo.inputmethod.keyboard.KeyConsts
import org.futo.inputmethod.keyboard.KeyboardId
import org.futo.inputmethod.keyboard.internal.KeySpecParser
import org.futo.inputmethod.keyboard.internal.KeyboardParams
import org.futo.inputmethod.keyboard.internal.MoreKeySpec
import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.common.StringUtils
import org.futo.inputmethod.latin.settings.LongPressKeySettings
Aleksandras Kostarevas
committed
/**
* Width tokens for keys. Rather than explicitly specifying a width in percentage as is common in
* other layout systems, we instead use width tokens, which eliminates the need to explicitly
* calculate and specify width percentages for most cases.
*/
@Serializable
enum class KeyWidth {
Aleksandras Kostarevas
committed
/**
* Regular key width. Used for normal letters (QWERTY etc)
*
* ##### Width calculation
* Simply put, the width of this is calculated by dividing the total keyboard width by the
* maximum number of keys in a row. It is consistent across the entire keyboard except in some cases.
Aleksandras Kostarevas
committed
*
* For example, if a keyboard has 3 rows, with 10, 9, and 7 keys respectively,
* the regular key width will be 100% / 10 = 10% for the entire keyboard.
* The rows with 9 and 7 keys will receive padding by default to keep them centered due
* to the extra space.
*
* There are 3 cases where regular width will be inconsistent:
* 1. In the bottom row
* 2. In the split layout, to maintain middle alignment
* 3. In a row with functional keys (shift or delete), in order to maintain a minimum functional key width
Aleksandras Kostarevas
committed
*/
Aleksandras Kostarevas
committed
/**
* Functional key width. Used for functional keys (Shift, Backspace, Enter, Symbols, etc)
*
* ##### Width calculation
* The width of this is at least the value specified in [Keyboard.minimumFunctionalKeyWidth]
* or, for the bottom row, [Keyboard.minimumBottomRowFunctionalKeyWidth].
*
* The width may be larger than the minimum if the available space is there.
* For example on the QWERTY layout, the ZXCV row has only 7 keys at a width of 10%, using up
* only 70% of space. The remaining 30% of space is divided among the shift and backspace,
* meaning the functional width is 15%.
*/
Aleksandras Kostarevas
committed
/**
* Grow width. Takes up all remaining space divided evenly among all grow keys in the row.
* Mainly used for spacebar.
*
* Grow keys are not supported in split layouts, and their presence can complicate width
* calculation for functional keys and others. Avoid use when possible.
*/
Aleksandras Kostarevas
committed
/**
* The Custom1 width as defined in [Keyboard.overrideWidths] (values are between 0.0 and 1.0)
*/
Aleksandras Kostarevas
committed
/**
* The Custom2 width as defined in [Keyboard.overrideWidths] (values are between 0.0 and 1.0)
*/
Aleksandras Kostarevas
committed
/**
* The Custom3 width as defined in [Keyboard.overrideWidths] (values are between 0.0 and 1.0)
*/
Aleksandras Kostarevas
committed
/**
* The Custom4 width as defined in [Keyboard.overrideWidths] (values are between 0.0 and 1.0)
*/
Aleksandras Kostarevas
committed
/**
* Specifies which morekeys can be automatically added to the key.
*/
@Serializable
enum class MoreKeyMode(
val autoFromKeyspec: Boolean,
val autoNumFromCoord: Boolean,
val autoSymFromCoord: Boolean,
val autoFromLanguageKey: Boolean,
Aleksandras Kostarevas
committed
/**
* Automatically insert morekeys from keyspec shortcuts, as well as numbers, symbols and actions
* (if not disabled by user). These count towards KeyCoordinate.
*/
All(true, true, true, true),
Aleksandras Kostarevas
committed
Aleksandras Kostarevas
committed
/**
* Only automatically insert morekeys from keyspec shortcut or language-related accents
Aleksandras Kostarevas
committed
*/
OnlyFromLetter(true, false, false, true),
Aleksandras Kostarevas
committed
Aleksandras Kostarevas
committed
/**
* Do not automatically insert any morekeys.
*/
OnlyExplicit(false, false, false, false),
}
private fun Int.and(other: Boolean): Int {
return if(other) { this } else { 0 }
}
Aleksandras Kostarevas
committed
/**
* Flags for the key label
*/
@Serializable
data class LabelFlags(
val alignHintLabelToBottom: Boolean = false,
val alignIconToBottom: Boolean = false,
val alignLabelOffCenter: Boolean = false,
val hasHintLabel: Boolean = false,
val followKeyLabelRatio: Boolean = false,
val followKeyLetterRatio: Boolean = false,
val followKeyLargeLetterRatio: Boolean = false,
val autoXScale: Boolean = false,
) {
fun getValue(): Int =
Aleksandras Kostarevas
committed
KeyConsts.LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER.and(alignLabelOffCenter) or
KeyConsts.LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM.and(alignHintLabelToBottom) or
KeyConsts.LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM.and(alignIconToBottom) or
KeyConsts.LABEL_FLAGS_HAS_HINT_LABEL.and(hasHintLabel) or
KeyConsts.LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO.and(followKeyLabelRatio) or
KeyConsts.LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO.and(followKeyLetterRatio) or
KeyConsts.LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO.and(followKeyLargeLetterRatio) or
KeyConsts.LABEL_FLAGS_AUTO_X_SCALE.and(autoXScale)
Aleksandras Kostarevas
committed
/**
* Attributes for keys.
*
* Values are inherited in the following order:
* `Key.attributes > Row.attributes > Keyboard.attributes > DefaultKeyAttributes`
*/
@Serializable
data class KeyAttributes(
Aleksandras Kostarevas
committed
/**
* Key width token
*/
Aleksandras Kostarevas
committed
/**
* Visual style (background) for the key
*/
val style: KeyVisualStyle? = null,
Aleksandras Kostarevas
committed
/**
* Whether or not to anchor the key to the edges.
*
* When a row is not wide enough to fill 100%, padding is added to the edges of the row.
* If there are anchored keys in the row, the padding will be added after the anchored
* keys, keeping the anchored keys at the edge.
*/
val anchored: Boolean? = null,
Aleksandras Kostarevas
committed
/**
* Whether or not to show the popup indicator when the key is pressed.
* This is usually desirable for letters on normal layouts (QWERTY letters), but undesirable
* for functional keys (Shift, Backspace), or certain layouts (phone layout)
*/
val showPopup: Boolean? = null,
Aleksandras Kostarevas
committed
/**
* Which moreKeys to add automatically
*/
val moreKeyMode: MoreKeyMode? = null,
Aleksandras Kostarevas
committed
/**
* Whether or not to use keyspec shortcuts.
* For example, `$` gets automatically converted to `!text/keyspec_currency`.
*
* The full list of keyspec shortcuts is defined in `KeySpecShortcuts`.
*/
val useKeySpecShortcut: Boolean? = null,
Aleksandras Kostarevas
committed
/**
* Whether or not longpress is enabled for the key
*/
val longPressEnabled: Boolean? = null,
Aleksandras Kostarevas
committed
/**
* Label flags for how the key's label (and its hint) should be presented
*/
val labelFlags: LabelFlags? = null,
Aleksandras Kostarevas
committed
/**
* Whether or not the key is repeatable, intended for backspace
*/
val repeatableEnabled: Boolean? = null,
Aleksandras Kostarevas
committed
/**
* Whether or not the key is automatically shiftable. If true, it automatically becomes
* uppercased when the layout is shifted. If this is not desired, this can be set to false.
* Shift behavior can be customized by using a [CaseSelector].
*/
val shiftable: Boolean? = null,
) {
fun getEffectiveAttributes(row: Row, keyboard: Keyboard): KeyAttributes {
val attrs = if(row.isBottomRow) {
listOf(this, row.attributes, DefaultKeyAttributes)
} else {
listOf(this, row.attributes, keyboard.attributes, DefaultKeyAttributes)
}
val effectiveWidth = resolve(attrs) { it.width }
val defaultMoreKeyMode = if((row.isLetterRow || row.isBottomRow) && effectiveWidth == KeyWidth.Regular) {
MoreKeyMode.OnlyFromLetter
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
}
return KeyAttributes(
width = resolve(attrs) { it.width },
style = resolve(attrs) { it.style },
anchored = resolve(attrs) { it.anchored },
showPopup = resolve(attrs) { it.showPopup },
moreKeyMode = resolve(attrs) { it.moreKeyMode } ?: defaultMoreKeyMode,
useKeySpecShortcut = resolve(attrs) { it.useKeySpecShortcut },
longPressEnabled = resolve(attrs) { it.longPressEnabled },
labelFlags = resolve(attrs) { it.labelFlags },
repeatableEnabled = resolve(attrs) { it.repeatableEnabled },
shiftable = resolve(attrs) { it.shiftable },
)
}
}
private fun<T, O> resolve(attributes: List<O>, getter: (O) -> T?): T? =
attributes.firstNotNullOfOrNull(getter)
val DefaultKeyAttributes = KeyAttributes(
width = KeyWidth.Regular,
style = KeyVisualStyle.Normal,
anchored = false,
showPopup = true,
moreKeyMode = null, // Default value is calculated in getEffectiveAttributes based on other attribute values
useKeySpecShortcut = true,
longPressEnabled = false,
labelFlags = LabelFlags(autoXScale = true),
repeatableEnabled = false,
shiftable = true,
)
object MoreKeysListSerializer: SpacedListSerializer<String>(String.serializer(), {
MoreKeySpec.splitKeySpecs(it)?.toList() ?: listOf()
})
Aleksandras Kostarevas
committed
/**
* The base key
*/
@Serializable
@SerialName("base")
data class BaseKey(
Aleksandras Kostarevas
committed
/**
* AOSP key spec. It can contain a custom label, code, icon, output text.
*
* Each key specification is one of the following:
* - Label optionally followed by keyOutputText (keyLabel|keyOutputText).
* - Label optionally followed by code point (keyLabel|!code/code_name).
* - Icon followed by keyOutputText (!icon/icon_name|keyOutputText).
* - Icon followed by code point (!icon/icon_name|!code/code_name).
*
* Label and keyOutputText are one of the following:
* - Literal string.
* - Label reference represented by (!text/label_name), see {@link KeyboardTextsSet}.
* - String resource reference represented by (!text/resource_name), see {@link KeyboardTextsSet}.
*
* Icon is represented by (!icon/icon_name), see {@link KeyboardIconsSet}.
*
* Code is one of the following:
* - Code point presented by hexadecimal string prefixed with "0x"
* - Code reference represented by (!code/code_name), see {@link KeyboardCodesSet}.
*
* Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character.
* Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
* as well.
*/
Aleksandras Kostarevas
committed
/**
* Attributes for this key. Values defined here supersede any other values. Values which are
* not defined are inherited from the row, keyboard, or default attributes.
*/
val attributes: KeyAttributes = KeyAttributes(),
Aleksandras Kostarevas
committed
/**
* More keys for this key. In YAML, it can be defined as a list or a comma-separated string.
*
* The values here are key specs.
*/
val moreKeys: @Serializable(with = MoreKeysListSerializer::class) List<String> = listOf(),
Aleksandras Kostarevas
committed
/**
* If set, overrides a default hint from the value of moreKeys.
*/
val hint: String? = null,
) : AbstractKey {
override fun countsToKeyCoordinate(params: KeyboardParams, row: Row, keyboard: Keyboard): Boolean {
val attributes = attributes.getEffectiveAttributes(row, keyboard)
val moreKeyMode = attributes.moreKeyMode!!
return moreKeyMode.autoNumFromCoord && moreKeyMode.autoSymFromCoord
}
override fun computeData(params: KeyboardParams, row: Row, keyboard: Keyboard, coordinate: KeyCoordinate): ComputedKeyData {
val attributes = attributes.getEffectiveAttributes(row, keyboard)
val shifted = (attributes.shiftable == true) && when(params.mId.mElementId) {
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> true
KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED -> true
KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED -> true
KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED -> true
KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED -> true
else -> false
}
val relevantSpecShortcut = if(attributes.useKeySpecShortcut != false || attributes.moreKeyMode?.autoFromKeyspec != false) {
resolveSpecWithOptionalShortcut(spec, params.mTextsSet, coordinate)
} else {
null
}
val expandedSpec: String? = params.mTextsSet.resolveTextReference(
if(attributes.useKeySpecShortcut != false) { relevantSpecShortcut?.get(0) } else { null }
?: spec
Aleksandras Kostarevas
committed
val label = expandedSpec?.let { KeySpecParser.getLabel(it) } ?: ""
val icon = expandedSpec?.let { KeySpecParser.getIconId(it) } ?: ""
val code = KeySpecParser.getCode(expandedSpec)
val outputText = KeySpecParser.getOutputText(expandedSpec)
val moreKeyMode = attributes.moreKeyMode!!
var moreKeysBuilder = MoreKeysBuilder(code = code, mode = moreKeyMode, coordinate = coordinate, row = row, keyboard = keyboard, params = params)
// 1. Add layout-defined moreKeys
moreKeysBuilder =
moreKeysBuilder.insertMoreKeys(LongPressKeySettings.joinMoreKeys(moreKeys))
// 2. Add moreKeys from keyspec
if (moreKeyMode.autoFromKeyspec) {
moreKeysBuilder =
moreKeysBuilder.insertMoreKeys(getDefaultMoreKeysForKey(code, relevantSpecShortcut))
}
// 3. Add settings-defined moreKeys (numbers, symbols, actions, language, etc) in their order
params.mId.mLongPressKeySettings.currentOrder.forEach {
moreKeysBuilder = moreKeysBuilder.insertMoreKeys(it)
// 4. Add special (period and comma)
if (moreKeyMode.autoSymFromCoord) {
moreKeysBuilder =
moreKeysBuilder.insertMoreKeys(getSpecialFromRow(coordinate, row))
val moreKeys = moreKeysBuilder.build(shifted)
return ComputedKeyData(
label = if(shifted) {
StringUtils.toTitleCaseOfKeyLabel(label, params.mId.locale) ?: label
} else {
label
},
code = if(shifted) {
StringUtils.toTitleCaseOfKeyCode(code, params.mId.locale)
} else {
code
},
outputText = outputText,
width = attributes.width!!,
icon = icon,
style = attributes.style!!,
anchored = attributes.anchored!!,
showPopup = attributes.showPopup!!,
moreKeys = moreKeys.specs,
longPressEnabled = (attributes.longPressEnabled ?: false) || moreKeys.specs.isNotEmpty(),
repeatable = attributes.repeatableEnabled ?: false,
moreKeyFlags = moreKeys.flags,
countsToKeyCoordinate = moreKeyMode.autoNumFromCoord && moreKeyMode.autoSymFromCoord,
hint = hint ?: "",
labelFlags = attributes.labelFlags?.getValue() ?: 0
)
}
}
Aleksandras Kostarevas
committed
/**
* Case selector key. Allows specifying a different type of key depending on when the layout is
* shifted or not.
*/
@Serializable
@SerialName("case")
data class CaseSelector(
Aleksandras Kostarevas
committed
/**
* Key to use normally
*/
Aleksandras Kostarevas
committed
/**
* Key to use when shifted
*/
Aleksandras Kostarevas
committed
/**
* Key to use when shifted, excluding automatic shift
*/
val shiftedManually: Key = shifted,
Aleksandras Kostarevas
committed
/**
* Key to use when shift locked (caps lock), defaults to [shiftedManually]
Aleksandras Kostarevas
committed
*/
val shiftLocked: Key = shiftedManually,
Aleksandras Kostarevas
committed
/**
* Key to use when in symbols layout, defaults to [normal]. Mainly used internally for
* [TemplateShiftKey]
*/
Aleksandras Kostarevas
committed
/**
* Key to use when in symbols layout, defaults to [normal]. Mainly used internally for
* [TemplateShiftKey]
*/
val symbolsShifted: Key = normal
) : AbstractKey {
private fun selectKeyFromElement(elementId: Int): Key =
when(elementId) {
KeyboardId.ELEMENT_ALPHABET -> normal
Aleksandras Kostarevas
committed
KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED -> shifted
KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED -> shiftedManually
Aleksandras Kostarevas
committed
// KeyboardState.kt currently doesn't distinguish between these
KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED,
KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED -> shiftLocked
KeyboardId.ELEMENT_SYMBOLS -> symbols
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> symbolsShifted
else -> normal
}
override fun countsToKeyCoordinate(params: KeyboardParams, row: Row, keyboard: Keyboard): Boolean =
selectKeyFromElement(params.mId.mElementId).countsToKeyCoordinate(params, row, keyboard)
override fun computeData(
params: KeyboardParams,
row: Row,
keyboard: Keyboard,
coordinate: KeyCoordinate
): ComputedKeyData? =
selectKeyFromElement(params.mId.mElementId).computeData(params, row, keyboard, coordinate)
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
}
typealias Key = @Serializable(with = KeyPathSerializer::class) AbstractKey
object KeyPathSerializer : PathDependentModifier<AbstractKey>(
KeyContextualSerializer, { path, v ->
if(v is BaseKey) {
v
} else {
v
}
}
)
object KeyContextualSerializer : ClassOrScalarsSerializer<AbstractKey>(
AbstractKey.serializer(),
{ contents ->
assert(contents.isNotEmpty()) { "Key requires at least 1 element" }
if(contents[0].startsWith("$") && contents[0] != "$" && contents.size == 1) {
TemplateKeys[contents[0].trimStart('$')]
?: throw IllegalStateException("Unknown template key $contents")
} else {
if(contents.size == 1) {
BaseKey(contents[0])
} else {
val moreKeys = contents.subList(1, contents.size)
BaseKey(contents[0], moreKeys = moreKeys)
}
}
}
)
Aleksandras Kostarevas
committed
/**
* Affects the background for the key. Depending on the user theme settings, backgrounds may be
* different.
*/
@Serializable
enum class KeyVisualStyle {
Aleksandras Kostarevas
committed
/**
* Uses a normal key background, intended for all letters.
*/
Aleksandras Kostarevas
committed
/**
* Uses no key background, intended for number row numbers.
*/
Aleksandras Kostarevas
committed
/**
* Uses a slightly darker colored background, intended for functional keys (backspace, etc)
*/
Aleksandras Kostarevas
committed
/**
* Intended for Shift when it's not shiftlocked
*/
Aleksandras Kostarevas
committed
/**
* Intended for Shift to indicate it's shiftlocked. Uses a more bright background
*/
Aleksandras Kostarevas
committed
/**
* Uses a bright fully rounded background, normally used for the enter key
*/
Aleksandras Kostarevas
committed
/**
* Depending on the key borders setting, this is either
* the same as [Normal] (key borders enabled) or a
* fully rounded rectangle (key borders disabled)
*/
Aleksandras Kostarevas
committed
Spacebar,
Aleksandras Kostarevas
committed
/**
* Visual style for moreKeys
*/
MoreKey,
Aleksandras Kostarevas
committed
/**
* An empty gap in place of a key
*/
@Serializable
@SerialName("gap")
class GapKey(val attributes: KeyAttributes = KeyAttributes()) : AbstractKey {
override fun countsToKeyCoordinate(params: KeyboardParams, row: Row, keyboard: Keyboard): Boolean = false
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
override fun computeData(
params: KeyboardParams,
row: Row,
keyboard: Keyboard,
coordinate: KeyCoordinate
): ComputedKeyData {
val attributes = attributes.getEffectiveAttributes(row, keyboard)
val moreKeyMode = attributes.moreKeyMode!!
return ComputedKeyData(
label = "",
code = Constants.CODE_UNSPECIFIED,
outputText = null,
width = attributes.width!!,
icon = "",
style = KeyVisualStyle.NoBackground,
anchored = attributes.anchored!!,
showPopup = false,
moreKeys = listOf(),
longPressEnabled = false,
repeatable = false,
moreKeyFlags = 0,
countsToKeyCoordinate = moreKeyMode.autoNumFromCoord && moreKeyMode.autoSymFromCoord,
hint = "",
labelFlags = 0
)
}
}