improve support for special keys and floris layouts (not fully there yet...)

This commit is contained in:
Helium314 2024-03-12 21:37:53 +01:00
parent c29eb0d03e
commit b47bc52e7d
7 changed files with 241 additions and 67 deletions

View file

@ -1027,6 +1027,11 @@ public class Key implements Comparable<Key> {
return popupKeysColumnAndFlags; return popupKeysColumnAndFlags;
} }
// only for testing
public String getOutputText() {
return mOptionalAttributes == null ? null : mOptionalAttributes.mOutputText;
}
public KeyParams( public KeyParams(
@NonNull final String keySpec, @NonNull final String keySpec,
@NonNull final KeyboardParams params, @NonNull final KeyboardParams params,

View file

@ -11,7 +11,6 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.floris.AutoTextKeyDa
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.CaseSelector import helium314.keyboard.keyboard.internal.keyboard_parser.floris.CaseSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.CharWidthSelector import helium314.keyboard.keyboard.internal.keyboard_parser.floris.CharWidthSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KanaSelector import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KanaSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.convertFloris
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyData import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyData
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.LayoutDirectionSelector import helium314.keyboard.keyboard.internal.keyboard_parser.floris.LayoutDirectionSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.MultiTextKeyData import helium314.keyboard.keyboard.internal.keyboard_parser.floris.MultiTextKeyData
@ -28,14 +27,14 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.floris.VariationSele
* codes of multi_text_key not used, only the label * codes of multi_text_key not used, only the label
* (currently) popups is always read to [number, main, relevant] layoutPopupKeys, no choice of which to use or which hint is provided * (currently) popups is always read to [number, main, relevant] layoutPopupKeys, no choice of which to use or which hint is provided
*/ */
class JsonKeyboardParser(private val params: KeyboardParams, private val context: Context) : KeyboardParser(params, context) { class JsonKeyboardParser(private val params: KeyboardParams, context: Context) : KeyboardParser(params, context) {
override fun parseCoreLayout(layoutContent: String): MutableList<List<KeyData>> { override fun parseCoreLayout(layoutContent: String): MutableList<List<KeyData>> {
val florisKeyData: List<List<AbstractKeyData>> = florisJsonConfig.decodeFromString(layoutContent) val florisKeyData: List<List<AbstractKeyData>> = florisJsonConfig.decodeFromString(layoutContent)
// initially 200 ms parse (debug build on S4 mini) // initially 200 ms parse (debug build on S4 mini)
// after a few parses it's optimized and 20-30 ms // after a few parses it's optimized and 20-30 ms
// whole load is 50-70 ms vs 30-55 with simple parser -> it's ok // whole load is 50-70 ms vs 30-55 with simple parser -> it's ok
return florisKeyData.mapTo(mutableListOf()) { it.mapNotNull { it.compute(params)?.convertFloris() } } return florisKeyData.mapTo(mutableListOf()) { it.mapNotNull { it.compute(params) } }
} }
} }

View file

@ -244,18 +244,15 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
val paramsRow = ArrayList<KeyParams>() val paramsRow = ArrayList<KeyParams>()
row.forEach { key -> row.forEach { key ->
var keyParams: KeyParams? = null var keyParams: KeyParams? = null
// try parsing a functional key, converting names from florisBoard to what is used here (should be unified) // try parsing a functional key
// note that this is ignoring code on those keys, if any // todo: note that this is ignoring code on those keys, if any
val functionalKeyName = when (key.label) { val functionalKeyName = when (key.label) {
"view_characters" -> "alpha"
"view_symbols" -> "symbol"
"enter" -> "action"
// todo (later): maybe add special popupKeys for phone and number layouts? // todo (later): maybe add special popupKeys for phone and number layouts?
"." -> if (params.mId.mElementId == KeyboardId.ELEMENT_NUMPAD) "period" else "." "." -> if (params.mId.mElementId == KeyboardId.ELEMENT_NUMPAD) "period" else "."
"," -> if (params.mId.mElementId == KeyboardId.ELEMENT_NUMPAD) "comma" else "," "," -> if (params.mId.mElementId == KeyboardId.ELEMENT_NUMPAD) "comma" else ","
else -> key.label else -> key.label
} }
if (functionalKeyName.length > 1 && key.type != KeyType.NUMERIC) { if (functionalKeyName.length > 1 && key.type != KeyType.NUMERIC) { // todo: why exception for numeric?
try { try {
keyParams = getFunctionalKeyParams(functionalKeyName) keyParams = getFunctionalKeyParams(functionalKeyName)
} catch (_: Throwable) {} // just use normal label } catch (_: Throwable) {} // just use normal label

View file

@ -271,7 +271,7 @@ private const val READER_MODE_LABELS = 3
private const val READER_MODE_NUMBER_ROW = 4 private const val READER_MODE_NUMBER_ROW = 4
// probably could be improved and extended, currently this is what's done in key_styles_currency.xml // probably could be improved and extended, currently this is what's done in key_styles_currency.xml
private fun getCurrencyKey(locale: Locale): Pair<String, Array<String>> { private fun getCurrencyKey(locale: Locale): Pair<String, List<String>> {
if (locale.country.matches(euroCountries)) if (locale.country.matches(euroCountries))
return euro return euro
if (locale.toString().matches(euroLocales)) if (locale.toString().matches(euroLocales))
@ -298,7 +298,7 @@ private fun getCurrencyKey(locale: Locale): Pair<String, Array<String>> {
} }
private fun genericCurrencyKey(currency: String) = currency to genericCurrencyPopupKeys private fun genericCurrencyKey(currency: String) = currency to genericCurrencyPopupKeys
private val genericCurrencyPopupKeys = arrayOf("£", "", "$", "¢", "¥", "") private val genericCurrencyPopupKeys = listOf("£", "", "$", "¢", "¥", "")
private fun getCurrency(locale: Locale): String { private fun getCurrency(locale: Locale): String {
if (locale.country == "BD") return "" if (locale.country == "BD") return ""
@ -320,13 +320,13 @@ private fun getCurrency(locale: Locale): String {
} }
// needs at least 4 popupKeys for working shift-symbol keyboard // needs at least 4 popupKeys for working shift-symbol keyboard
private val euro = "" to arrayOf("£", "¥", "$", "¢", "") private val euro = "" to listOf("£", "¥", "$", "¢", "")
private val dram = "֏" to arrayOf("", "", "$", "£", "¥") private val dram = "֏" to listOf("", "", "$", "£", "¥")
private val rupee = "" to arrayOf("£", "", "$", "¢", "¥", "") private val rupee = "" to listOf("£", "", "$", "¢", "¥", "")
private val pound = "£" to arrayOf("", "¥", "$", "¢", "") private val pound = "£" to listOf("", "¥", "$", "¢", "")
private val ruble = "" to arrayOf("", "$", "£", "¥", "") private val ruble = "" to listOf("", "$", "£", "¥", "")
private val lira = "" to arrayOf("", "$", "£", "¥", "") private val lira = "" to listOf("", "$", "£", "¥", "")
private val dollar = "$" to arrayOf("£", "¢", "", "¥", "") private val dollar = "$" to listOf("£", "¢", "", "¥", "")
private val euroCountries = "AD|AT|BE|BG|HR|CY|CZ|DA|EE|FI|FR|DE|GR|HU|IE|IT|XK|LV|LT|LU|MT|MO|ME|NL|PL|PT|RO|SM|SK|SI|ES|VA".toRegex() private val euroCountries = "AD|AT|BE|BG|HR|CY|CZ|DA|EE|FI|FR|DE|GR|HU|IE|IT|XK|LV|LT|LU|MT|MO|ME|NL|PL|PT|RO|SM|SK|SI|ES|VA".toRegex()
private val euroLocales = "bg|ca|cs|da|de|el|en|es|et|eu|fi|fr|ga|gl|hr|hu|it|lb|lt|lv|mt|nl|pl|pt|ro|sk|sl|sq|sr|sv".toRegex() private val euroLocales = "bg|ca|cs|da|de|el|en|es|et|eu|fi|fr|ga|gl|hr|hu|it|lb|lt|lv|mt|nl|pl|pt|ro|sk|sl|sq|sr|sv".toRegex()

View file

@ -16,6 +16,7 @@ object KeyCode {
const val INTERNAL_FLORIS_MAX = -1 const val INTERNAL_FLORIS_MAX = -1
val INTERNAL_FLORIS = INTERNAL_FLORIS_MIN..INTERNAL_FLORIS_MAX val INTERNAL_FLORIS = INTERNAL_FLORIS_MIN..INTERNAL_FLORIS_MAX
val INTERNAL_HELI = -19999..-10000 // for keys exclusive to this app val INTERNAL_HELI = -19999..-10000 // for keys exclusive to this app
val CURRENCY = CURRENCY_SLOT_6..CURRENCY_SLOT_1
} }
const val UNSPECIFIED = 0 const val UNSPECIFIED = 0
@ -131,11 +132,9 @@ object KeyCode {
const val NOT_SPECIFIED = -10008 // todo: not sure if there is need to have the "old" unspecified keyCode different, just test it and maybe merge const val NOT_SPECIFIED = -10008 // todo: not sure if there is need to have the "old" unspecified keyCode different, just test it and maybe merge
/** to make sure a FlorisBoard code works when reading a JSON layout */ /** to make sure a FlorisBoard code works when reading a JSON layout */
private fun Int.checkOrConvertCode(): Int = if (this > 0) this else when (this) { fun Int.checkAndConvertCode(): Int = if (this > 0) this else when (this) {
// todo: should work, but not yet
// CURRENCY_SLOT_1, CURRENCY_SLOT_2, CURRENCY_SLOT_3, CURRENCY_SLOT_4, CURRENCY_SLOT_5, CURRENCY_SLOT_6,
// working // working
CURRENCY_SLOT_1, CURRENCY_SLOT_2, CURRENCY_SLOT_3, CURRENCY_SLOT_4, CURRENCY_SLOT_5, CURRENCY_SLOT_6,
VOICE_INPUT, LANGUAGE_SWITCH, SETTINGS, DELETE, ALPHA, SYMBOL, EMOJI, CLIPBOARD, VOICE_INPUT, LANGUAGE_SWITCH, SETTINGS, DELETE, ALPHA, SYMBOL, EMOJI, CLIPBOARD,
UNDO, REDO, ARROW_DOWN, ARROW_UP, ARROW_RIGHT, ARROW_LEFT, CLIPBOARD_COPY, CLIPBOARD_SELECT_ALL, UNDO, REDO, ARROW_DOWN, ARROW_UP, ARROW_RIGHT, ARROW_LEFT, CLIPBOARD_COPY, CLIPBOARD_SELECT_ALL,
CLIPBOARD_SELECT_WORD, TOGGLE_INCOGNITO_MODE, TOGGLE_AUTOCORRECT, MOVE_START_OF_LINE, MOVE_END_OF_LINE, CLIPBOARD_SELECT_WORD, TOGGLE_INCOGNITO_MODE, TOGGLE_AUTOCORRECT, MOVE_START_OF_LINE, MOVE_END_OF_LINE,
@ -148,17 +147,19 @@ object KeyCode {
// conversion // conversion
IME_UI_MODE_TEXT -> ALPHA IME_UI_MODE_TEXT -> ALPHA
VIEW_PHONE -> ALPHA // phone keyboard is treated like alphabet, just with different layout
VIEW_PHONE2 -> SYMBOL
else -> throw IllegalStateException("key code $this not yet supported") else -> throw IllegalStateException("key code $this not yet supported")
} }
/** to make sure a FlorisBoard label works when reading a JSON layout */ /** to make sure a FlorisBoard label works when reading a JSON layout */
private fun String.convertFlorisLabel(): String = when (this) { fun String.convertFlorisLabel(): String = when (this) {
"view_characters" -> "alpha" "view_characters" -> "alpha"
"view_symbols" -> "symbol" "view_symbols" -> "symbol"
"view_numeric_advanced" -> "numpad" "view_numeric_advanced" -> "numpad"
"view_phone" -> "alpha" "view_phone" -> "alpha" // phone keyboard is treated like alphabet, just with different layout
"view_phone2" -> "symbols" "view_phone2" -> "symbols" // phone symbols
"ime_ui_mode_media" -> "emoji" "ime_ui_mode_media" -> "emoji"
"ime_ui_mode_clipboard" -> "clipboard" "ime_ui_mode_clipboard" -> "clipboard"
"ime_ui_mode_text" -> "alpha" "ime_ui_mode_text" -> "alpha"
@ -168,12 +169,7 @@ object KeyCode {
"currency_slot_4" -> "$$$3" "currency_slot_4" -> "$$$3"
"currency_slot_5" -> "$$$4" "currency_slot_5" -> "$$$4"
"currency_slot_6" -> "$$$5" "currency_slot_6" -> "$$$5"
"enter" -> "action"
else -> this else -> this
} }
fun KeyData.convertFloris() = when (this) {
is TextKeyData -> { TextKeyData(type, code.checkOrConvertCode(), label.convertFlorisLabel(), groupId, popup, labelFlags) }
is AutoTextKeyData -> { AutoTextKeyData(type, code.checkOrConvertCode(), label.convertFlorisLabel(), groupId, popup, labelFlags) }
is MultiTextKeyData -> { MultiTextKeyData(type, codePoints, label.convertFlorisLabel(), groupId, popup, labelFlags) }
}
} }

View file

@ -10,6 +10,8 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import helium314.keyboard.keyboard.Key import helium314.keyboard.keyboard.Key
import helium314.keyboard.keyboard.internal.KeyboardParams import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.checkAndConvertCode
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.convertFlorisLabel
import helium314.keyboard.keyboard.internal.keyboard_parser.rtlLabel import helium314.keyboard.keyboard.internal.keyboard_parser.rtlLabel
import helium314.keyboard.latin.common.Constants import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.StringUtils import helium314.keyboard.latin.common.StringUtils
@ -71,7 +73,40 @@ sealed interface KeyData : AbstractKeyData {
} }
// make it non-nullable for simplicity, and to reflect current implementations // make it non-nullable for simplicity, and to reflect current implementations
override fun compute(params: KeyboardParams): KeyData override fun compute(params: KeyboardParams): KeyData {
val newLabel = label.convertFlorisLabel()
val newCode = code.checkAndConvertCode()
// resolve currency keys
if (newLabel.startsWith("$$$") || newCode in KeyCode.Spec.CURRENCY) {
val currencyKey = params.mLocaleKeyboardInfos.currencyKey
val currencyCodeAsString = if (newCode in KeyCode.Spec.CURRENCY) {
when (newCode) {
KeyCode.CURRENCY_SLOT_1 -> "|" + currencyKey.first
KeyCode.CURRENCY_SLOT_2 -> "|" + currencyKey.second[0]
KeyCode.CURRENCY_SLOT_3 -> "|" + currencyKey.second[1]
KeyCode.CURRENCY_SLOT_4 -> "|" + currencyKey.second[2]
KeyCode.CURRENCY_SLOT_5 -> "|" + currencyKey.second[3]
KeyCode.CURRENCY_SLOT_6 -> "|" + currencyKey.second[4]
else -> ""
}
} else ""
if (newLabel == "$$$") {
val finalLabel = currencyKey.first + currencyCodeAsString
// the flag is to match old parser, but why is it there for main currency key and not for others?
return TextKeyData(type, KeyCode.UNSPECIFIED, finalLabel, groupId, SimplePopups(currencyKey.second), labelFlags or Key.LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO)
}
val n = newLabel.substringAfter("$$$").toIntOrNull()
if (n != null && n <= 5 && n > 0) {
val finalLabel = currencyKey.second[n - 1] + currencyCodeAsString
return TextKeyData(type, KeyCode.UNSPECIFIED, finalLabel, groupId, popup, labelFlags)
}
}
if (newCode != code || newLabel != label)
return TextKeyData(type, newCode, newLabel, groupId, popup, labelFlags).compute(params)
return this
}
fun isSpaceKey(): Boolean { fun isSpaceKey(): Boolean {
return type == KeyType.CHARACTER && (code == Constants.CODE_SPACE || code == KeyCode.CJK_SPACE return type == KeyType.CHARACTER && (code == Constants.CODE_SPACE || code == KeyCode.CJK_SPACE
@ -89,6 +124,18 @@ sealed interface KeyData : AbstractKeyData {
return if (code == KeyCode.UNSPECIFIED || code == KeyCode.MULTIPLE_CODE_POINTS) { return if (code == KeyCode.UNSPECIFIED || code == KeyCode.MULTIPLE_CODE_POINTS) {
// code will be determined from label if possible (i.e. label is single code point) // code will be determined from label if possible (i.e. label is single code point)
// but also longer labels should work without issues, also for MultiTextKeyData // but also longer labels should work without issues, also for MultiTextKeyData
if (this is MultiTextKeyData) {
val outputText = String(codePoints, 0, codePoints.size)
Key.KeyParams(
"$label|$outputText",
code,
params,
width,
labelFlags or additionalLabelFlags,
Key.BACKGROUND_TYPE_NORMAL, // todo (when supported): determine type
popup,
)
} else {
Key.KeyParams( Key.KeyParams(
label.rtlLabel(params), // todo (when supported): convert special labels to keySpec label.rtlLabel(params), // todo (when supported): convert special labels to keySpec
params, params,
@ -97,6 +144,7 @@ sealed interface KeyData : AbstractKeyData {
Key.BACKGROUND_TYPE_NORMAL, // todo (when supported): determine type Key.BACKGROUND_TYPE_NORMAL, // todo (when supported): determine type
popup, popup,
) )
}
} else { } else {
Key.KeyParams( Key.KeyParams(
label.ifEmpty { StringUtils.newSingleCodePointString(code) }, label.ifEmpty { StringUtils.newSingleCodePointString(code) },
@ -132,23 +180,6 @@ class TextKeyData(
override val popup: PopupSet<AbstractKeyData> = PopupSet(), override val popup: PopupSet<AbstractKeyData> = PopupSet(),
override val labelFlags: Int = 0 override val labelFlags: Int = 0
) : KeyData { ) : KeyData {
override fun compute(params: KeyboardParams): KeyData {
// if (evaluator.isSlot(this)) { // todo: currency key stuff probably should be taken from florisboard too
// return evaluator.slotData(this)?.let { data ->
// TextKeyData(type, data.code, data.label, groupId, popup).compute(params)
// }
// }
if (label.startsWith("$$$")) { // currency key
if (label == "$$$")
return params.mLocaleKeyboardInfos.currencyKey
.let { it.first.toTextKey(it.second.toList(), labelFlags = Key.LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO) } // the flag is to match old parser, but why for main currency key, but not for others?
val n = label.substringAfter("$$$").toIntOrNull()
if (n != null && n <= 5 && n > 0)
return params.mLocaleKeyboardInfos.currencyKey.second[n - 1].toTextKey()
}
return this
}
override fun asString(isForDisplay: Boolean): String { override fun asString(isForDisplay: Boolean): String {
return buildString { return buildString {
if (isForDisplay || code == KeyCode.URI_COMPONENT_TLD || code < Constants.CODE_SPACE) { if (isForDisplay || code == KeyCode.URI_COMPONENT_TLD || code < Constants.CODE_SPACE) {
@ -168,6 +199,8 @@ class TextKeyData(
} }
// AutoTextKeyData is just for converting case with shift, which HeliBoard always does anyway
// (maybe change later if there is a use case)
@Serializable @Serializable
@SerialName("auto_text_key") @SerialName("auto_text_key")
class AutoTextKeyData( class AutoTextKeyData(
@ -178,16 +211,6 @@ class AutoTextKeyData(
override val popup: PopupSet<AbstractKeyData> = PopupSet(), override val popup: PopupSet<AbstractKeyData> = PopupSet(),
override val labelFlags: Int = 0 override val labelFlags: Int = 0
) : KeyData { ) : KeyData {
// state and recompute not needed, as upcasing is done when creating KeyParams
override fun compute(params: KeyboardParams): KeyData {
// if (evaluator.isSlot(this)) { // todo: see above
// return evaluator.slotData(this)?.let { data ->
// TextKeyData(type, data.code, data.label, groupId, popup).compute(evaluator)
// }
// }
return this
}
override fun asString(isForDisplay: Boolean): String { override fun asString(isForDisplay: Boolean): String {
return buildString { return buildString {
@ -220,6 +243,8 @@ class MultiTextKeyData(
@Transient override val code: Int = KeyCode.MULTIPLE_CODE_POINTS @Transient override val code: Int = KeyCode.MULTIPLE_CODE_POINTS
override fun compute(params: KeyboardParams): KeyData { override fun compute(params: KeyboardParams): KeyData {
// todo: does this work? maybe convert label to | style?
// but if i allow negative codes, ctrl+z could be on a single key (but floris doesn't support this anyway)
return this return this
} }

View file

@ -12,12 +12,17 @@ import helium314.keyboard.keyboard.internal.KeyboardBuilder
import helium314.keyboard.keyboard.internal.KeyboardParams import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.keyboard.internal.TouchPositionCorrection import helium314.keyboard.keyboard.internal.TouchPositionCorrection
import helium314.keyboard.keyboard.internal.UniqueKeysCache import helium314.keyboard.keyboard.internal.UniqueKeysCache
import helium314.keyboard.keyboard.internal.keyboard_parser.JsonKeyboardParser
import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_NORMAL import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_NORMAL
import helium314.keyboard.keyboard.internal.keyboard_parser.SimpleKeyboardParser import helium314.keyboard.keyboard.internal.keyboard_parser.SimpleKeyboardParser
import helium314.keyboard.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams import helium314.keyboard.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyType
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.MultiTextKeyData
import helium314.keyboard.latin.LatinIME import helium314.keyboard.latin.LatinIME
import helium314.keyboard.latin.RichInputMethodSubtype import helium314.keyboard.latin.RichInputMethodSubtype
import helium314.keyboard.latin.utils.AdditionalSubtypeUtils.createEmojiCapableAdditionalSubtype import helium314.keyboard.latin.utils.AdditionalSubtypeUtils.createEmojiCapableAdditionalSubtype
import helium314.keyboard.latin.utils.POPUP_KEYS_LAYOUT
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -114,6 +119,153 @@ f""", // no newline at the end
} }
} }
@Test fun jsonParser() {
val params = KeyboardParams()
params.mId = KeyboardLayoutSet.getFakeKeyboardId(KeyboardId.ELEMENT_ALPHABET)
params.mPopupKeyTypes.add(POPUP_KEYS_LAYOUT)
addLocaleKeyTextsToParams(latinIME, params, POPUP_KEYS_NORMAL)
data class Expected(val label: String, val text: String?, val code: Int, val popups: List<String>?)
val expected = listOf(
Expected("a", null, 'a'.code, null),
Expected("a", null, 'a'.code, null),
Expected("$", null, '$'.code, listOf("£", "", "¢", "¥", "")),
Expected("$", null, '¥'.code, listOf("£", "", "¢", "¥", "")),
Expected("i", null, 105, null),
Expected("্র", "্র", KeyCode.MULTIPLE_CODE_POINTS, null),
Expected("x", "্র", KeyCode.MULTIPLE_CODE_POINTS, null),
Expected(";", null, ';'.code, listOf(":")),
Expected(".", null, '.'.code, listOf(">")),
Expected("'", null, '\''.code, listOf("!", "\"")),
Expected("9", null, '9'.code, null), // todo (later): also should have different background or whatever is related to type
Expected("", null, -7, null), // todo: expect an icon
Expected("?123", null, -207, null),
Expected("", null, ' '.code, null),
Expected("(", null, '('.code, listOf("<", "[", "{")),
Expected("$", null, '$'.code, listOf("£", "", "", "¢", "¥")),
Expected("p", null, 'p'.code, null),
)
val layoutString = """
[
[
{ "$": "auto_text_key" "label": "a" },
{ "$": "text_key" "label": "a" },
{ "label": "$$$" },
{ "label": "$$$", code: -805 },
{ "$": "case_selector",
"lower": { "code": 105, "label": "i" },
"upper": { "code": 304, "label": "İ" }
},
{ "$": "multi_text_key", "codePoints": [2509, 2480], "label": "্র" },
{ "$": "multi_text_key", "codePoints": [2509, 2480], "label": "x" },
{ "$": "case_selector",
"lower": { "code": 59, "label": ";", "popup": {
"relevant": [
{ "code": 58, "label": ":" }
]
} },
"upper": { "code": 58, "label": ":", "popup": {
"relevant": [
{ "code": 59, "label": ";" }
]
} }
},
{ "$": "shift_state_selector",
"shiftedManual": { "code": 62, "label": ">", "popup": {
"relevant": [
{ "code": 46, "label": "." }
]
} },
"default": { "code": 46, "label": ".", "popup": {
"relevant": [
{ "code": 62, "label": ">" }
]
} }
},
{ "$": "shift_state_selector",
"shiftedManual": { "code": 34, "label": "\"", "popup": {
"relevant": [
{ "code": 33, "label": "!" },
{ "code": 39, "label": "'"}
]
} },
"default": { "$": "variation_selector",
"email": { "code": 64, "label": "@" },
"uri": { "code": 47, "label": "/" },
"default": { "code": 39, "label": "'", "popup": {
"relevant": [
{ "code": 33, "label": "!" },
{ "code": 34, "label": "\"" }
]
} }
}
},
{ "code": 57, "label": "9", "type": "numeric" },
{ "code": -7, "label": "delete", "type": "enter_editing" },
{ "code": -207, "label": "view_phone2", "type": "system_gui" },
{ "code": 32, "label": "space" },
{ "$": "layout_direction_selector",
"ltr": { "code": 40, "label": "(", "popup": {
"main": { "code": 60, "label": "<" },
"relevant": [
{ "code": 91, "label": "[" },
{ "code": 123, "label": "{" }
]
} },
"rtl": { "code": 41, "label": "(", "popup": {
"main": { "code": 62, "label": "<" },
"relevant": [
{ "code": 93, "label": "[" },
{ "code": 125, "label": "{" }
]
} }
},
{ "code": -801, "label": "currency_slot_1", "popup": {
"main": { "code": -802, "label": "currency_slot_2" },
"relevant": [
{ "code": -806, "label": "currency_slot_6" },
{ "code": -803, "label": "currency_slot_3" },
{ "code": -804, "label": "currency_slot_4" },
{ "code": -805, "label": "currency_slot_5" }
]
} },
{ "label": "p" }
],
[
{ "label": "q" },
{ "label": "s" },
{ "label": "d" },
{ "label": "f" },
{ "label": "g" },
{ "label": "h" },
{ "label": "j" },
{ "label": "k" },
{ "label": "l" },
{ "label": "m", "popup": { "main": { "label": "/" } } }
],
[
{ "label": "w" },
{ "label": "x" },
{ "label": "c" },
{ "label": "v" },
{ "label": "b" },
{ "label": "n" }
]
]
""".trimIndent()
val keys = JsonKeyboardParser(params, latinIME).parseCoreLayout(layoutString)
keys.first().forEachIndexed { index, keyData ->
println("key ${keyData.label}: code ${keyData.code}, popups: ${keyData.popup.getPopupKeyLabels(params)}")
if (keyData.type == KeyType.ENTER_EDITING || keyData.type == KeyType.SYSTEM_GUI) return@forEachIndexed // todo: currently not accepted, but should be (see below)
val keyParams = keyData.toKeyParams(params)
println("key ${keyParams.mLabel}: code ${keyParams.mCode}, popups: ${keyParams.mPopupKeys?.toList()}")
if (keyParams.outputText == "space") return@forEachIndexed // todo: only works for numeric layouts... idea: parse space anywhere, and otherwise only if special type
assertEquals(expected[index].label, keyParams.mLabel)
assertEquals(expected[index].code, keyParams.mCode)
assertEquals(expected[index].popups?.sorted(), keyParams.mPopupKeys?.mapNotNull { it.mLabel }?.sorted()) // todo (later): what's wrong with order?
assertEquals(expected[index].text, keyParams.outputText)
}
}
@Test fun canLoadKeyboard() { @Test fun canLoadKeyboard() {
val editorInfo = EditorInfo() val editorInfo = EditorInfo()
val subtype = createEmojiCapableAdditionalSubtype(Locale.ENGLISH, "qwerty", true) val subtype = createEmojiCapableAdditionalSubtype(Locale.ENGLISH, "qwerty", true)