add keyboard parser for more complicated layouts

parsing mostly taken from FlorisBoard, thus json files are compatible
some small modifications
added json files for missing latin layouts
This commit is contained in:
Helium314 2023-11-24 12:19:29 +01:00
parent dc0490a0e1
commit ca3790b519
18 changed files with 1776 additions and 635 deletions

View file

@ -0,0 +1,38 @@
[
[
{ "label": "a", "popup": { "main": { "label": "%" } } },
{ "label": "z", "popup": { "main": { "label": "\\" } } },
{ "label": "e", "popup": { "main": { "label": "|" } } },
{ "label": "r", "popup": { "main": { "label": "=" } } },
{ "label": "t", "popup": { "main": { "label": "[" } } },
{ "label": "y", "popup": { "main": { "label": "]" } } },
{ "label": "u", "popup": { "main": { "label": "<" } } },
{ "label": "i", "popup": { "main": { "label": ">" } } },
{ "label": "o", "popup": { "main": { "label": "{" } } },
{ "label": "p", "popup": { "main": { "label": "}" } } }
],
[
{ "label": "q", "popup": { "main": { "label": "@" } } },
{ "label": "s", "popup": { "main": { "label": "#" } } },
{ "label": "d", "popup": { "main": { "label": "$$$" } } },
{ "label": "f", "popup": { "main": { "label": "_" } } },
{ "label": "g", "popup": { "main": { "label": "&" } } },
{ "label": "h", "popup": { "main": { "label": "-" } } },
{ "label": "j", "popup": { "main": { "label": "+" } } },
{ "label": "k", "popup": { "main": { "label": "(" } } },
{ "label": "l", "popup": { "main": { "label": ")" } } },
{ "label": "m", "popup": { "main": { "label": "/" } } }
],
[
{ "label": "w", "popup": { "main": { "label": "*" } } },
{ "label": "x", "popup": { "main": { "label": "\"" } } },
{ "label": "c", "popup": { "main": { "label": "'" } } },
{ "label": "v", "popup": { "main": { "label": ":" } } },
{ "label": "b", "popup": { "main": { "label": ";" } } },
{ "label": "n", "popup": { "main": { "label": "!" } } },
{ "$": "shift_state_selector",
"shiftedManual": { "label": "?" },
"default": { "label": "'" }, "popup": { "main": { "label": "?" } }
}
]
]

View file

@ -0,0 +1,40 @@
[
[
{ "label": "q", "popup": { "main": { "label": "%" } } },
{ "label": "w", "popup": { "main": { "label": "\\" } } },
{ "label": "f", "popup": { "main": { "label": "|" } } },
{ "label": "p", "popup": { "main": { "label": "=" } } },
{ "label": "g", "popup": { "main": { "label": "[" } } },
{ "label": "j", "popup": { "main": { "label": "]" } } },
{ "label": "l", "popup": { "main": { "label": "<" } } },
{ "label": "u", "popup": { "main": { "label": ">" } } },
{ "label": "y", "popup": { "main": { "label": "{" } } },
{ "$": "shift_state_selector",
"shiftedManual": { "label": ":" },
"default": { "label": ";", "popup": { "relevant": [
{ "label": ":" }, { "label": "}" }
] } }
}
],
[
{ "label": "a", "popup": { "main": { "label": "@" } } },
{ "label": "r", "popup": { "main": { "label": "#" } } },
{ "label": "s", "popup": { "main": { "label": "$$$" } } },
{ "label": "t", "popup": { "main": { "label": "_" } } },
{ "label": "d", "popup": { "main": { "label": "&" } } },
{ "label": "h", "popup": { "main": { "label": "-" } } },
{ "label": "n", "popup": { "main": { "label": "+" } } },
{ "label": "e", "popup": { "main": { "label": "(" } } },
{ "label": "i", "popup": { "main": { "label": ")" } } },
{ "label": "o", "popup": { "main": { "label": "…" } } }
],
[
{ "label": "z", "popup": { "main": { "label": "*" } } },
{ "label": "x", "popup": { "main": { "label": "\"" } } },
{ "label": "c", "popup": { "main": { "label": "'" } } },
{ "label": "v", "popup": { "main": { "label": ":" } } },
{ "label": "b", "popup": { "main": { "label": ";" } } },
{ "label": "k", "popup": { "main": { "label": "!" } } },
{ "label": "m", "popup": { "main": { "label": "?" } } }
]
]

View file

@ -0,0 +1,40 @@
[
[
{ "label": "q", "popup": { "main": { "label": "%" } } },
{ "label": "w", "popup": { "main": { "label": "\\" } } },
{ "label": "f", "popup": { "main": { "label": "|" } } },
{ "label": "p", "popup": { "main": { "label": "=" } } },
{ "label": "b", "popup": { "main": { "label": "[" } } },
{ "label": "j", "popup": { "main": { "label": "]" } } },
{ "label": "l", "popup": { "main": { "label": "<" } } },
{ "label": "u", "popup": { "main": { "label": ">" } } },
{ "label": "y", "popup": { "main": { "label": "{" } } },
{ "$": "shift_state_selector",
"shiftedManual": { "label": ":" },
"default": { "label": ";", "popup": { "relevant": [
{ "label": ":" }, { "label": "}" }
] } }
}
],
[
{ "label": "a", "popup": { "main": { "label": "@" } } },
{ "label": "r", "popup": { "main": { "label": "#" } } },
{ "label": "s", "popup": { "main": { "label": "$$$" } } },
{ "label": "t", "popup": { "main": { "label": "_" } } },
{ "label": "g", "popup": { "main": { "label": "&" } } },
{ "label": "m", "popup": { "main": { "label": "-" } } },
{ "label": "n", "popup": { "main": { "label": "+" } } },
{ "label": "e", "popup": { "main": { "label": "(" } } },
{ "label": "i", "popup": { "main": { "label": ")" } } },
{ "label": "o", "popup": { "main": { "label": "…" } } }
],
[
{ "label": "z", "popup": { "main": { "label": "*" } } },
{ "label": "x", "popup": { "main": { "label": "\"" } } },
{ "label": "c", "popup": { "main": { "label": "'" } } },
{ "label": "d", "popup": { "main": { "label": ":" } } },
{ "label": "v", "popup": { "main": { "label": ";" } } },
{ "label": "k", "popup": { "main": { "label": "!" } } },
{ "label": "h", "popup": { "main": { "label": "?" } } }
]
]

View file

@ -0,0 +1,55 @@
[
[
{ "$": "shift_state_selector",
"shifted": { "label": "\"" },
"default": { "$": "variation_selector",
"uri": { "label": "/" },
"email": { "label": "@" },
"default": { "label": "'", "popup": { "relevant": [
{ "label": "!" },
{ "label": "\"" }
] } }
}
},
{ "$": "shift_state_selector",
"shifted": { "label": "<" },
"default": { "label": "," }
},
{ "$": "shift_state_selector",
"shifted": { "label": ">" },
"default": { "label": "." }
},
{ "label": "p" },
{ "label": "y" },
{ "label": "f" },
{ "label": "g" },
{ "label": "c" },
{ "label": "r" },
{ "label": "l" }
],
[
{ "label": "a" },
{ "label": "o" },
{ "label": "e" },
{ "label": "u" },
{ "label": "i" },
{ "label": "d" },
{ "label": "h" },
{ "label": "t" },
{ "label": "n" },
{ "label": "s" }
],
[
{ "label": "j" },
{ "label": "k" },
{ "label": "x" },
{ "label": "b" },
{ "label": "m" },
{ "label": "w" },
{ "label": "v" }
],
[
{ "label": "q" },
{ "label": "z" }
]
]

View file

@ -956,7 +956,7 @@ public class Key implements Comparable<Key> {
// params that remains constant
public final int mCode;
@Nullable final String mLabel;
@Nullable public final String mLabel;
@Nullable final String mHintLabel;
final int mLabelFlags;
final int mIconId;
@ -1156,6 +1156,17 @@ public class Key implements Comparable<Key> {
mEnabled = true;
}
public KeyParams(
@NonNull final String keySpec,
@NonNull final KeyboardParams params,
final float relativeWidth,
final int labelFlags,
final int backgroundType,
@Nullable final String[] layoutMoreKeys
) {
this(keySpec, KeySpecParser.getCode(keySpec), params, relativeWidth, labelFlags, backgroundType, layoutMoreKeys);
}
/**
* constructor that does not require attrs, style or absolute key dimension / position
* setDimensionsFromRelativeSize needs to be called before creating the key
@ -1163,6 +1174,7 @@ public class Key implements Comparable<Key> {
public KeyParams(
// todo (much later): replace keySpec? these encoded icons and codes are not really great
@NonNull final String keySpec, // key text or some special string for KeySpecParser, e.g. "!icon/shift_key|!code/key_shift" (avoid using !text, should be removed)
final int code,
@NonNull final KeyboardParams params,
final float relativeWidth,
final int labelFlags,
@ -1200,7 +1212,6 @@ public class Key implements Comparable<Key> {
mMoreKeys = null;
}
final int code = KeySpecParser.getCode(keySpec);
if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) {
mLabel = params.mId.mCustomActionLabel;
} else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) {
@ -1223,7 +1234,7 @@ public class Key implements Comparable<Key> {
if (hintLabelAlwaysFromFirstLongPressKey) {
hintLabel = mMoreKeys == null ? null : mMoreKeys[0].mLabel;
} else {
hintLabel = layoutMoreKeys == null ? null : layoutMoreKeys[0];
hintLabel = layoutMoreKeys == null ? null : layoutMoreKeys[0]; // note that some entries may have been changed to other string or null
if (hintLabel != null && hintLabel.length() > 1 && hintLabel.startsWith("!")) // this is not great, but other than removing com key label this is definitely ok
hintLabel = null;
if (hintLabel != null && hintLabel.length() == 2 && hintLabel.startsWith("\\"))

View file

@ -173,6 +173,11 @@ public final class KeyboardId {
return (mEditorInfo.inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
}
public boolean isAlphabetShifted() {
return mElementId == ELEMENT_ALPHABET_SHIFT_LOCKED || mElementId == ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED
|| mElementId == ELEMENT_ALPHABET_AUTOMATIC_SHIFTED || mElementId == ELEMENT_ALPHABET_MANUAL_SHIFTED;
}
public int imeAction() {
return InputTypeUtils.getImeOptionsActionIdFromEditorInfo(mEditorInfo);
}

View file

@ -8,18 +8,19 @@ package org.dslul.openboard.inputmethod.keyboard.internal
import android.content.Context
import android.content.res.Resources
import android.util.Log
import android.widget.Toast
import org.dslul.openboard.inputmethod.annotations.UsedForTesting
import org.dslul.openboard.inputmethod.keyboard.Key
import org.dslul.openboard.inputmethod.keyboard.Key.KeyParams
import org.dslul.openboard.inputmethod.keyboard.Keyboard
import org.dslul.openboard.inputmethod.keyboard.KeyboardId
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.MORE_KEYS_ALL
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.MORE_KEYS_MORE
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.SimpleKeyboardParser
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.KeyboardParser
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.XmlKeyboardParser
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams
import org.dslul.openboard.inputmethod.latin.BuildConfig
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.common.Constants
import org.dslul.openboard.inputmethod.latin.define.DebugFlags
import org.dslul.openboard.inputmethod.latin.settings.Settings
import org.dslul.openboard.inputmethod.latin.utils.sumOf
import org.xmlpull.v1.XmlPullParserException
@ -47,24 +48,23 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
mParams.mAllowRedundantMoreKeys = enabled
}
fun loadSimpleKeyboard(id: KeyboardId): KeyboardBuilder<KP> {
fun loadFromAssets(id: KeyboardId): KeyboardBuilder<KP>? {
mParams.mId = id
addLocaleKeyTextsToParams(mContext, mParams)
when (Settings.getInstance().current.mShowMoreKeys) {
MORE_KEYS_ALL -> mParams.mLocaleKeyTexts.addFile(mContext.assets.open("language_key_texts/all_more_keys.txt"))
MORE_KEYS_MORE -> mParams.mLocaleKeyTexts.addFile(mContext.assets.open("language_key_texts/more_more_keys.txt"))
addLocaleKeyTextsToParams(mContext, mParams, Settings.getInstance().current.mShowMoreKeys)
try {
val parser = KeyboardParser.createParserForLayout(mParams, mContext) ?: return null
Log.d(TAG, "parsing $id using ${parser::class.simpleName}")
keysInRows = parser.parseLayoutFromAssets(id.mSubtype.keyboardLayoutSetName)
} catch (e: Throwable) {
if (DebugFlags.DEBUG_ENABLED || BuildConfig.DEBUG)
Toast.makeText(mContext, "error loading keyboard: ${e.message}", Toast.LENGTH_LONG).show()
Log.e(TAG, "loading $id from assets failed", e)
return null
}
keysInRows = SimpleKeyboardParser(mParams, mContext).parseFromAssets(id.mSubtype.keyboardLayoutSetName)
determineAbsoluteValues()
return this
// todo: further plan
// add a parser for more complex layouts, and slowly extend it with whatever is needed
// try to make the format compatible with florisboard, or just take it if it has all we need
// if so, then make sure additional stuff unwanted in this app (if there is some) does not cause errors
// probably need to deal with different functional key definition style, but only if allowing numpad and similar layouts
// initially it's just alternative key for shifted layout
// so dvorak and azerty and colemak and others can be migrated
// migrate symbol layouts to this style
// simplified if possible, but json should be fine too
// migrate keypad layouts to this style
@ -144,15 +144,14 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
}
fun loadFromXml(xmlId: Int, id: KeyboardId): KeyboardBuilder<KP> {
mParams.mId = id
if (Settings.getInstance().current.mUseNewKeyboardParsing
&& id.isAlphabetKeyboard
&& this::class == KeyboardBuilder::class // otherwise this will apply to moreKeys and moreSuggestions
&& SimpleKeyboardParser.hasLayoutFile(mParams.mId.mSubtype.keyboardLayoutSetName)
&& this::class == KeyboardBuilder::class // otherwise this will apply to moreKeys and moreSuggestions, and then some parameters are off
) {
loadSimpleKeyboard(id)
return this
if (loadFromAssets(id) != null)
return this
}
mParams.mId = id
// loading a keyboard should set default params like mParams.readAttributes(mContext, attrs);
// attrs may be null, then default values are used (looks good for "normal" keyboards)
try {

View file

@ -24,6 +24,8 @@ import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.SortedSet;
import java.util.TreeSet;
@ -81,6 +83,8 @@ public class KeyboardParams {
public final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
@NonNull
public final KeyStylesSet mKeyStyles = new KeyStylesSet(mTextsSet);
@NonNull // todo: not good, this only works because params are currently always created for the active subtype
public final List<Locale> mSecondaryLocales = Settings.getInstance().getCurrent().mSecondaryLocales;
@NonNull
private final UniqueKeysCache mUniqueKeysCache;

View file

@ -0,0 +1,75 @@
// SPDX-License-Identifier: GPL-3.0-only
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser
import android.content.Context
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.AbstractKeyData
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.AutoTextKeyData
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.CaseSelector
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.CharWidthSelector
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.KanaSelector
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.KeyData
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.LayoutDirectionSelector
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.MultiTextKeyData
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.ShiftStateSelector
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.TextKeyData
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.VariationSelector
/**
* Parser for json layout files as used in FlorisBoard, see floris directory for classes taken from FlorisBoard.
* Some differences to the FlorisBoard keys:
* (currently) only normal keys supported
* if label or code are missing one is created from the other
* auto_text_key ignored (i.e. interpreted like the default TextKey)
* codes of multi_text_key not used, only the label
* (currently) popups is always read to [number, main, relevant] layoutMoreKeys, no choice of which to use or which hint is provided
*/
class JsonKeyboardParser(private val params: KeyboardParams, private val context: Context) : KeyboardParser(params, context) {
override fun getLayoutFromAssets(layoutName: String) =
context.assets.open("layouts/$layoutName.json").reader().readText()
override fun parseCoreLayout(layoutContent: String): MutableList<List<KeyData>> {
val florisKeyData: List<List<AbstractKeyData>> = florisJsonConfig.decodeFromString(layoutContent)
// initially 200 ms parse (debug build on S4 mini)
// 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
return florisKeyData.mapTo(mutableListOf()) { it.mapNotNull { it.compute(params) } }
}
}
/*
* Copyright (C) 2021 Patrick Goldinger
* modified
* SPDX-License-Identifier: Apache-2.0
*/
private val florisJsonConfig = Json {
classDiscriminator = "$"
encodeDefaults = true
ignoreUnknownKeys = true
isLenient = true
serializersModule = SerializersModule {
polymorphic(AbstractKeyData::class) {
subclass(TextKeyData::class, TextKeyData.serializer())
subclass(AutoTextKeyData::class, AutoTextKeyData.serializer())
subclass(MultiTextKeyData::class, MultiTextKeyData.serializer())
subclass(CaseSelector::class, CaseSelector.serializer())
subclass(ShiftStateSelector::class, ShiftStateSelector.serializer())
subclass(VariationSelector::class, VariationSelector.serializer())
subclass(LayoutDirectionSelector::class, LayoutDirectionSelector.serializer())
subclass(CharWidthSelector::class, CharWidthSelector.serializer())
subclass(KanaSelector::class, KanaSelector.serializer())
defaultDeserializer { TextKeyData.serializer() }
}
polymorphic(KeyData::class) {
subclass(TextKeyData::class, TextKeyData.serializer())
subclass(AutoTextKeyData::class, AutoTextKeyData.serializer())
subclass(MultiTextKeyData::class, MultiTextKeyData.serializer())
defaultDeserializer { TextKeyData.serializer() }
}
}
}

View file

@ -0,0 +1,576 @@
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser
import android.content.Context
import android.content.res.Resources
import android.view.inputmethod.EditorInfo
import org.dslul.openboard.inputmethod.keyboard.Key
import org.dslul.openboard.inputmethod.keyboard.Key.KeyParams
import org.dslul.openboard.inputmethod.keyboard.KeyboardId
import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardIconsSet
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.KeyData
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.toTextKey
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.common.splitOnWhitespace
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils
import org.dslul.openboard.inputmethod.latin.utils.RunInLocale
import org.dslul.openboard.inputmethod.latin.utils.sumOf
/**
* Abstract parser class that handles creation of keyboard from [KeyData] arranged in rows,
* provided by the extending class.
*
* Functional keys are pre-defined and can't be changed, with exception of comma, period and similar
* keys in symbol layouts.
* By default, all normal keys have the same width and flags, which may cause issues with the
* requirements of certain non-latin languages. todo: add labelFlags to Json parser, or determine automatically?
*
* Currently the number, phone and numpad layouts are not compatible with this parser.
*/
abstract class KeyboardParser(private val params: KeyboardParams, private val context: Context) {
protected abstract fun getLayoutFromAssets(layoutName: String): String
protected abstract fun parseCoreLayout(layoutContent: String): MutableList<List<KeyData>>
fun parseLayoutFromAssets(layoutName: String): ArrayList<ArrayList<KeyParams>> =
parseLayoutString(getLayoutFromAssets(layoutName))
fun parseLayoutString(layoutContent: String): ArrayList<ArrayList<KeyParams>> {
params.readAttributes(context, null)
val keysInRows = ArrayList<ArrayList<KeyParams>>()
val baseKeys: MutableList<List<KeyData>> = parseCoreLayout(layoutContent)
if (!params.mId.mNumberRowEnabled) {
// todo (non-latin): not all layouts have numbers on first row, so maybe have some layout flag to switch it off (or an option)
((1..9) + 0).forEachIndexed { i, n -> baseKeys.first().getOrNull(i)?.popup?.number = n }
}
val functionalKeysReversed = parseFunctionalKeys().reversed()
// keyboard parsed bottom-up because the number of rows is not fixed, but the functional keys
// are always added to the rows near the bottom
keysInRows.add(getBottomRowAndAdjustBaseKeys(baseKeys))
baseKeys.reversed().forEachIndexed { i, it ->
val row: List<KeyData> = if (i == 0) {
// add bottom row extra keys
it + context.getString(R.string.key_def_extra_bottom_right)
.split(",").mapNotNull { if (it.isBlank()) null else it.trim().toTextKey() }
} else {
it
}
// parse functional keys for this row (if any)
val functionalKeysDefs = if (i < functionalKeysReversed.size) functionalKeysReversed[i]
else emptyList<String>() to emptyList()
val functionalKeysLeft = functionalKeysDefs.first.map { getFunctionalKeyParams(it) }
val functionalKeysRight = functionalKeysDefs.second.map { getFunctionalKeyParams(it) }
val paramsRow = ArrayList<KeyParams>(functionalKeysLeft)
// determine key width, maybe scale factor for keys, and spacers to add
val usedKeyWidth = params.mDefaultRelativeKeyWidth * row.size
val functionalKeyWidth = (functionalKeysLeft.sumOf { it.mRelativeWidth }) + (functionalKeysRight.sumOf { it.mRelativeWidth })
val availableWidth = 1f - functionalKeyWidth
var keyWidth: Float
val spacerWidth: Float
if (availableWidth - usedKeyWidth > 0.0001f) { // don't add spacers if only a tiny bit is empty
// width available, add spacer
keyWidth = params.mDefaultRelativeKeyWidth
spacerWidth = (availableWidth - usedKeyWidth) / 2
} else {
// need more width, re-scale
spacerWidth = 0f
keyWidth = availableWidth / row.size
}
if (spacerWidth != 0f) {
paramsRow.add(KeyParams.newSpacer(params, spacerWidth))
}
if (keyWidth < params.mDefaultRelativeKeyWidth * 0.82 && spacerWidth == 0f) {
// keys are very narrow, also rescale the functional keys to make keys a little wider
// 0.82 is just some guess for "too narrow"
// todo (idea): works reasonably well, but actually functional keys could give some more of their width
val allKeyScale = 1f / (functionalKeyWidth + row.size * params.mDefaultRelativeKeyWidth)
keyWidth = params.mDefaultRelativeKeyWidth * allKeyScale
functionalKeysLeft.forEach { it.mRelativeWidth *= allKeyScale }
functionalKeysRight.forEach { it.mRelativeWidth *= allKeyScale }
}
for (key in row) {
// todo: maybe autoScale / autoXScale if label has more than 2 characters (exception for emojis?)
// but that could also be determined in toKeyParams
val keyParams = key.compute(params)?.toKeyParams(params, keyWidth) ?: continue
paramsRow.add(keyParams)
}
if (spacerWidth != 0f) {
paramsRow.add(KeyParams.newSpacer(params, spacerWidth))
}
functionalKeysRight.forEach { paramsRow.add(it) }
keysInRows.add(0, paramsRow) // we're doing it backwards, so add on top
}
resizeLastNormalRowIfNecessaryForAlignment(keysInRows)
// rescale height if we have more than 4 rows
val heightRescale = if (keysInRows.size > 4) 4f / keysInRows.size else 1f
if (params.mId.mNumberRowEnabled)
keysInRows.add(0, getNumberRow())
if (heightRescale != 1f) {
// rescale all keys, so number row doesn't look weird (this is done like in current parsing)
// todo: in symbols view, number row is not rescaled
// so the symbols keyboard is higher than the normal one
// not a new issue, but should be solved in this migration
// how? possibly scale all keyboards to height of main alphabet? (consider suggestion strip)
keysInRows.forEach { it.forEach { it.mRelativeHeight *= heightRescale } }
}
return keysInRows
}
// resize keys in last row if they are wider than keys in the row above
// this is done so the keys align with the keys above, like in original layouts
// done e.g. for nordic and swiss layouts
private fun resizeLastNormalRowIfNecessaryForAlignment(keysInRows: ArrayList<ArrayList<KeyParams>>) {
if (keysInRows.size < 3)
return
val lastNormalRow = keysInRows[keysInRows.lastIndex - 1]
val rowAboveLastNormalRow = keysInRows[keysInRows.lastIndex - 2]
if (lastNormalRow.any { it.isSpacer } || rowAboveLastNormalRow.any { it.isSpacer })
return // annoying to deal with, and probably no resize needed anyway
val lastNormalRowKeyWidth = lastNormalRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }.mRelativeWidth
val rowAboveLastNormalRowKeyWidth = rowAboveLastNormalRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }.mRelativeWidth
if (lastNormalRowKeyWidth <= rowAboveLastNormalRowKeyWidth + 0.0001f)
return // no need
if (lastNormalRow.any { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL && it.mRelativeWidth != lastNormalRowKeyWidth })
return // normal keys have different width, don't deal with this
val numberOfNormalKeys = lastNormalRow.count { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }
val widthBefore = numberOfNormalKeys * lastNormalRowKeyWidth
val widthAfter = numberOfNormalKeys * rowAboveLastNormalRowKeyWidth
val spacerWidth = (widthBefore - widthAfter) / 2
// resize keys and add spacers
lastNormalRow.forEach { if (it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL) it.mRelativeWidth = rowAboveLastNormalRowKeyWidth }
lastNormalRow.add(lastNormalRow.indexOfFirst { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }, KeyParams.newSpacer(params, spacerWidth))
lastNormalRow.add(lastNormalRow.indexOfLast { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL } + 1, KeyParams.newSpacer(params, spacerWidth))
}
private fun parseFunctionalKeys(): List<Pair<List<String>, List<String>>> =
context.getString(R.string.key_def_functional).split("\n").mapNotNull { line ->
if (line.isBlank()) return@mapNotNull null
val p = line.split(";")
p.first().let { if (it.isBlank()) emptyList() else it.split(",") } to
p.last().let { if (it.isBlank()) emptyList() else it.split(",") }
}
private fun getBottomRowAndAdjustBaseKeys(baseKeys: MutableList<List<KeyData>>): ArrayList<KeyParams> {
val adjustableKeyCount = when (params.mId.mElementId) {
KeyboardId.ELEMENT_SYMBOLS -> 3
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> 4
else -> 2 // must be alphabet, parser doesn't work for other elementIds
}
val adjustedKeys = if (baseKeys.last().size == adjustableKeyCount) baseKeys.last()
else null
if (adjustedKeys != null)
baseKeys.removeLast()
val bottomRow = ArrayList<KeyParams>()
context.getString(R.string.key_def_bottom_row).split(",").forEach {
val key = it.trim().splitOnWhitespace().first()
val adjustKey = when (key) {
"comma" -> adjustedKeys?.first()
"period" -> adjustedKeys?.last()
else -> null
}
val keyParams = getFunctionalKeyParams(it, adjustKey?.label, adjustKey?.popup?.toMoreKeys(params))
if (key == "space") { // add the extra keys around space
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) {
bottomRow.add(getFunctionalKeyParams(FunctionalKey.NUMPAD))
bottomRow.add(keyParams)
bottomRow.add(KeyParams(
adjustedKeys?.get(1)?.label ?: "/",
params,
params.mDefaultRelativeKeyWidth,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
adjustedKeys?.get(1)?.popup?.toMoreKeys(params)
))
} else if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
bottomRow.add(KeyParams(
adjustedKeys?.get(1)?.label ?: "<",
params,
params.mDefaultRelativeKeyWidth,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
adjustedKeys?.get(1)?.popup?.toMoreKeys(params)
))
bottomRow.add(keyParams)
bottomRow.add(KeyParams(
adjustedKeys?.get(2)?.label ?: ">",
params,
params.mDefaultRelativeKeyWidth,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
adjustedKeys?.get(2)?.popup?.toMoreKeys(params)
))
} else { // alphabet
if (params.mId.mLanguageSwitchKeyEnabled)
bottomRow.add(getFunctionalKeyParams(FunctionalKey.LANGUAGE_SWITCH))
if (params.mId.mEmojiKeyEnabled)
bottomRow.add(getFunctionalKeyParams(FunctionalKey.EMOJI))
bottomRow.add(keyParams)
if (params.mId.locale.language in languagesThatNeedZwnjKey)
bottomRow.add(getFunctionalKeyParams(FunctionalKey.ZWNJ)) // todo (non-latin): test it
}
} else {
bottomRow.add(keyParams)
}
}
// set space width
val space = bottomRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_SPACEBAR }
space.mRelativeWidth = 1f - bottomRow.filter { it != space }.sumOf { it.mRelativeWidth }
return bottomRow
}
private fun getNumberRow(): ArrayList<KeyParams> {
val row = ArrayList<KeyParams>()
((1..9) + 0).forEachIndexed { i, n ->
row.add(KeyParams(
n.toString(), // todo (non-latin): use language more keys to adjust, possibly in combination with some setting
params,
params.mDefaultRelativeKeyWidth,
Key.LABEL_FLAGS_DISABLE_HINT_LABEL, // todo (later): maybe optional or enable (but then all numbers should have moreKeys)
Key.BACKGROUND_TYPE_NORMAL,
numbersMoreKeys[i] // todo (non-latin): alternative numbers should be in language more keys, which to put where needs to be decided
))
}
return row
}
private fun getFunctionalKeyParams(def: String, label: String? = null, moreKeys: Array<String>? = null): KeyParams {
val split = def.trim().splitOnWhitespace()
val key = FunctionalKey.valueOf(split[0].uppercase())
val width = if (split.size == 2) split[1].substringBefore("%").toFloat() / 100f
else params.mDefaultRelativeKeyWidth
return getFunctionalKeyParams(key, width, label, moreKeys)
}
private fun getFunctionalKeyParams(key: FunctionalKey, relativeWidth: Float? = null, label: String? = null, moreKeys: Array<String>? = null): KeyParams {
// for comma and period: label will override default, moreKeys will be appended
val width = relativeWidth ?: params.mDefaultRelativeKeyWidth
return when (key) {
FunctionalKey.SYMBOL -> KeyParams(
"${getSymbolLabel()}|!code/key_switch_alpha_symbol", // todo (later): in numpad the code is key_symbolNumpad
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
FunctionalKey.COMMA -> KeyParams(
label ?: getDefaultCommaLabel(),
params,
width,
Key.LABEL_FLAGS_HAS_POPUP_HINT, // previously only if normal comma, but always is more correct
if (label?.first()
?.isLetter() == true
) Key.BACKGROUND_TYPE_NORMAL else Key.BACKGROUND_TYPE_FUNCTIONAL,
moreKeys?.let { getCommaMoreKeys() + it } ?: getCommaMoreKeys()
)
FunctionalKey.SPACE -> KeyParams(
"!icon/space_key|!code/key_space", // !icon/space_key_for_number_layout in number layout, but not on tablet
params,
width, // will not be used for normal space (only in number layouts)
0, // todo (later): alignIconToBottom for non-tablet number layout
Key.BACKGROUND_TYPE_SPACEBAR,
null
)
FunctionalKey.PERIOD -> KeyParams(
label ?: ".",
params,
width,
Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT, // todo (later): check what LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT does, maybe remove the flag here
if (label?.first()
?.isLetter() == true
) Key.BACKGROUND_TYPE_NORMAL else Key.BACKGROUND_TYPE_FUNCTIONAL,
moreKeys?.let { getPunctuationMoreKeys() + it } ?: getPunctuationMoreKeys()
)
FunctionalKey.ACTION -> KeyParams(
"${getActionKeyLabel()}|${getActionKeyCode()}",
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE
or Key.LABEL_FLAGS_AUTO_X_SCALE
or Key.LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR
or KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId),
Key.BACKGROUND_TYPE_ACTION,
getActionKeyMoreKeys()
)
FunctionalKey.DELETE -> KeyParams(
"!icon/delete_key|!code/key_delete",
params,
width,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
FunctionalKey.SHIFT -> KeyParams(
"${getShiftLabel()}|!code/key_shift",
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE,
// todo (later): possibly the whole stickOn/Off stuff can be removed, currently it should only have a very slight effect in holo
if (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED)
Key.BACKGROUND_TYPE_STICKY_ON
else Key.BACKGROUND_TYPE_STICKY_OFF,
arrayOf("!noPanelAutoMoreKey!", " |!code/key_capslock")
)
FunctionalKey.EMOJI -> KeyParams(
"!icon/emoji_normal_key|!code/key_emoji",
params,
width,
KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId),
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
// tablet layout has an emoji key that changes to com key in url / mail
FunctionalKey.EMOJI_COM -> if (params.mId.mMode == KeyboardId.MODE_URL || params.mId.mMode == KeyboardId.MODE_EMAIL)
getFunctionalKeyParams(FunctionalKey.COM, width)
else getFunctionalKeyParams(FunctionalKey.EMOJI, width)
FunctionalKey.COM -> KeyParams( // todo: label and moreKeys could be in localeKeyTexts, handled like currency key
".com",
params,
width,
Key.LABEL_FLAGS_AUTO_X_SCALE or Key.LABEL_FLAGS_FONT_NORMAL or Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_PRESERVE_CASE,
Key.BACKGROUND_TYPE_FUNCTIONAL,
arrayOf("!hasLabels!", ".net", ".org", ".gov", ".edu")
)
FunctionalKey.LANGUAGE_SWITCH -> KeyParams(
"!icon/language_switch_key|!code/key_language_switch",
params,
width,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
FunctionalKey.ALPHA -> KeyParams(
"${getAlphabetLabel()}|!code/key_switch_alpha_symbol", // todo (later): in numpad the code is key_alphaNumpad
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
FunctionalKey.NUMPAD -> KeyParams(
"!icon/numpad_key|!code/key_numpad",
params,
width,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
FunctionalKey.ZWNJ -> KeyParams(
"!icon/zwnj_key|\u200C",
params,
width,
Key.LABEL_FLAGS_HAS_POPUP_HINT,
Key.BACKGROUND_TYPE_SPACEBAR,
arrayOf("!icon/zwj_key|\u200D")
)
}
}
private fun getActionKeyLabel(): String {
if (params.mId.isMultiLine && (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED))
return "!icon/enter_key"
val iconName = when (params.mId.imeAction()) {
EditorInfo.IME_ACTION_GO -> KeyboardIconsSet.NAME_GO_KEY
EditorInfo.IME_ACTION_SEARCH -> KeyboardIconsSet.NAME_SEARCH_KEY
EditorInfo.IME_ACTION_SEND -> KeyboardIconsSet.NAME_SEND_KEY
EditorInfo.IME_ACTION_NEXT -> KeyboardIconsSet.NAME_NEXT_KEY
EditorInfo.IME_ACTION_DONE -> KeyboardIconsSet.NAME_DONE_KEY
EditorInfo.IME_ACTION_PREVIOUS -> KeyboardIconsSet.NAME_PREVIOUS_KEY
InputTypeUtils.IME_ACTION_CUSTOM_LABEL -> return params.mId.mCustomActionLabel
else -> return "!icon/enter_key"
}
val replacement = iconName.replaceIconWithLabelIfNoDrawable()
return if (iconName == replacement) // i.e. icon exists
"!icon/$iconName"
else
replacement
}
private fun getActionKeyCode() =
if (params.mId.isMultiLine && (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED))
"!code/key_shift_enter"
else "!code/key_enter"
private fun getActionKeyMoreKeys(): Array<String>? {
val action = params.mId.imeAction()
val navigatePrev = params.mId.navigatePrevious()
val navigateNext = params.mId.navigateNext()
return when {
params.mId.passwordInput() -> when {
navigatePrev && action == EditorInfo.IME_ACTION_NEXT -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
action == EditorInfo.IME_ACTION_NEXT -> null
navigateNext && action == EditorInfo.IME_ACTION_PREVIOUS -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
action == EditorInfo.IME_ACTION_PREVIOUS -> null
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS_NEXT)
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
else -> null
}
// could change definition of numbers to query a range, or have a pre-defined list, but not that crucial
params.mId.mMode in listOf(KeyboardId.MODE_URL, KeyboardId.MODE_EMAIL, KeyboardId.ELEMENT_PHONE, KeyboardId.ELEMENT_NUMBER, KeyboardId.MODE_DATE, KeyboardId.MODE_TIME, KeyboardId.MODE_DATETIME) -> when {
action == EditorInfo.IME_ACTION_NEXT && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
action == EditorInfo.IME_ACTION_NEXT -> null
action == EditorInfo.IME_ACTION_PREVIOUS && navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
action == EditorInfo.IME_ACTION_PREVIOUS -> null
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS_NEXT)
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
else -> null
}
action == EditorInfo.IME_ACTION_NEXT && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS)
action == EditorInfo.IME_ACTION_NEXT -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
action == EditorInfo.IME_ACTION_PREVIOUS && navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_NEXT)
action == EditorInfo.IME_ACTION_PREVIOUS -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS_NEXT)
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_NEXT)
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS)
else -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
}
}
private fun createMoreKeysArray(moreKeysDef: String): Array<String> {
val moreKeys = mutableListOf<String>()
for (moreKey in moreKeysDef.split(",")) {
val iconPrefixRemoved = moreKey.substringAfter("!icon/")
if (iconPrefixRemoved == moreKey) { // i.e. there is no !icon/
moreKeys.add(moreKey)
continue
}
val iconName = iconPrefixRemoved.substringBefore("|")
val replacementText = iconName.replaceIconWithLabelIfNoDrawable()
if (replacementText == iconName) { // i.e. we have the drawable
moreKeys.add(moreKey)
} else {
moreKeys.add("!hasLabels!")
moreKeys.add(replacementText)
}
}
return moreKeys.toTypedArray()
}
private fun String.replaceIconWithLabelIfNoDrawable(): String {
if (params.mIconsSet.getIconDrawable(KeyboardIconsSet.getIconId(this)) != null) return this
val id = context.resources.getIdentifier("label_$this", "string", context.packageName)
val ril = object : RunInLocale<String>() { // todo (later): simpler way of doing this in a single line?
override fun job(res: Resources) = res.getString(id)
}
return ril.runInLocale(context.resources, params.mId.locale)
}
private fun getAlphabetLabel() = params.mLocaleKeyTexts.labelAlphabet
private fun getSymbolLabel() = params.mLocaleKeyTexts.labelSymbols
private fun getShiftLabel(): String {
val elementId = params.mId.mElementId
if (elementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
return params.mLocaleKeyTexts.labelShiftSymbols
if (elementId == KeyboardId.ELEMENT_SYMBOLS)
return getSymbolLabel()
if (elementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || elementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED
|| elementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED || elementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED)
return "!icon/shift_key_shifted"
return "!icon/shift_key"
}
private fun getDefaultCommaLabel(): String {
if (params.mId.mMode == KeyboardId.MODE_URL)
return "/"
if (params.mId.mMode == KeyboardId.MODE_EMAIL)
return "\\@"
return ","
}
private fun getCommaMoreKeys(): Array<String> {
val keys = mutableListOf<String>()
if (!params.mId.mDeviceLocked)
keys.add("!icon/clipboard_normal_key|!code/key_clipboard")
if (!params.mId.mEmojiKeyEnabled)
keys.add("!icon/emoji_normal_key|!code/key_emoji")
if (!params.mId.mLanguageSwitchKeyEnabled)
keys.add("!icon/language_switch_key|!code/key_language_switch")
if (!params.mId.mOneHandedModeEnabled)
keys.add("!icon/start_onehanded_mode_key|!code/key_start_onehanded")
if (!params.mId.mDeviceLocked)
keys.add("!icon/settings_key|!code/key_settings")
return keys.toTypedArray()
}
private fun getPunctuationMoreKeys(): Array<String> {
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS || params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
return arrayOf("")
val moreKeys = params.mLocaleKeyTexts.getMoreKeys("punctuation") ?:
// todo: some (non-latin) languages have different parenthesis keys (maybe rtl has inverted?)
arrayOf("${Key.MORE_KEYS_AUTO_COLUMN_ORDER}8", "\\,", "?", "!", "#", ")", "(", "/", ";", "'", "@", ":", "-", "\"", "+", "\\%", "&")
if (context.resources.getInteger(R.integer.config_screen_metrics) >= 3 && moreKeys.contains("!") && moreKeys.contains("?")) {
// we have a tablet, remove ! and ? keys and reduce number in autoColumnOrder
// this makes use of removal of empty moreKeys in MoreKeySpec.insertAdditionalMoreKeys
moreKeys[moreKeys.indexOf("!")] = ""
moreKeys[moreKeys.indexOf("?")] = ""
val columns = moreKeys[0].substringAfter(Key.MORE_KEYS_AUTO_COLUMN_ORDER).toIntOrNull()
if (columns != null)
moreKeys[0] = "${Key.MORE_KEYS_AUTO_COLUMN_ORDER}${columns - 1}"
}
return moreKeys
}
companion object {
fun createParserForLayout(params: KeyboardParams, context: Context): KeyboardParser? {
val layoutName = params.mId.mSubtype.keyboardLayoutSetName
val layoutFileNames = context.assets.list("layouts") ?: return null
if (layoutFileNames.contains("$layoutName.json"))
return JsonKeyboardParser(params, context)
val simpleLayoutName = getSimpleLayoutName(layoutName)
if (layoutFileNames.contains("$simpleLayoutName.txt"))
return SimpleKeyboardParser(params, context)
return null
}
@JvmStatic // unsupported without JvmStatic
protected fun getSimpleLayoutName(layoutName: String)= when (layoutName) {
"swiss", "german", "serbian_qwertz" -> "qwertz"
"nordic", "spanish" -> "qwerty"
else -> layoutName
}
}
protected enum class FunctionalKey {
EMOJI, LANGUAGE_SWITCH, COM, EMOJI_COM, ACTION, DELETE, PERIOD, COMMA, SPACE, SHIFT, NUMPAD, SYMBOL, ALPHA, ZWNJ
}
}
// moreKeys for numbers, order is 1-9 and then 0
// todo (later): like numbers, for non-latin layouts this depends on language and therefore should not be in the parser
private val numbersMoreKeys = arrayOf(
arrayOf("¹", "½", "","¼", ""),
arrayOf("²", ""),
arrayOf("³", "¾", ""),
arrayOf(""),
arrayOf(""),
null,
arrayOf(""),
null,
null,
arrayOf("", ""),
)
// could make arrays right away, but they need to be copied anyway as moreKeys arrays are modified when creating KeyParams
private const val MORE_KEYS_NAVIGATE_PREVIOUS = "!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard"
private const val MORE_KEYS_NAVIGATE_NEXT = "!icon/clipboard_action_key|!code/key_clipboard,!icon/next_key|!code/key_action_next"
private const val MORE_KEYS_NAVIGATE_PREVIOUS_NEXT = "!fixedColumnOrder!3,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/next_key|!code/key_action_next"
private const val MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS = "!fixedColumnOrder!3,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji"
private const val MORE_KEYS_NAVIGATE_EMOJI = "!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji"
private const val MORE_KEYS_NAVIGATE_EMOJI_NEXT = "!fixedColumnOrder!3,!needsDividers!,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji,!icon/next_key|!code/key_action_next"
private const val MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS_NEXT = "!fixedColumnOrder!4,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji,!icon/next_key|!code/key_action_next"
// farsi|kannada|nepali_romanized|nepali_traditional|telugu"
private val languagesThatNeedZwnjKey = listOf("fa", "ne", "kn", "te")

View file

@ -4,15 +4,16 @@ package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser
import android.content.Context
import org.dslul.openboard.inputmethod.keyboard.Key
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.KeyData
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.toTextKey
import org.dslul.openboard.inputmethod.latin.common.splitOnWhitespace
import org.dslul.openboard.inputmethod.latin.settings.Settings
import java.io.InputStream
import java.util.Locale
import kotlin.math.round
class LocaleKeyTexts(dataStream: InputStream?) {
private val moreKeys = hashMapOf<String, Array<String>>()
private val extraKeys = Array<MutableList<Pair<String, Array<String>?>>?>(5) { null }
private val extraKeys = Array<MutableList<KeyData>?>(5) { null }
var labelSymbols = "\\?123"
var labelAlphabet = "ABC"
var labelShiftSymbols = "=\\<"
@ -54,7 +55,7 @@ class LocaleKeyTexts(dataStream: InputStream?) {
// need tp provide a copy because some functions like MoreKeySpec.insertAdditionalMoreKeys may modify the array
fun getMoreKeys(label: String): Array<String>? = moreKeys[label]?.copyOf()
fun getExtraKeys(row: Int): List<Pair<String, Array<String>?>>? =
fun getExtraKeys(row: Int): List<KeyData>? =
if (row > extraKeys.size) null
else extraKeys[row]
@ -75,10 +76,9 @@ class LocaleKeyTexts(dataStream: InputStream?) {
if (split.size < 2) return
val row = split.first().toIntOrNull() ?: return
val keys = split.last().splitOnWhitespace()
val morekeys = if (keys.size == 1) null else Array(keys.size - 1) { keys[it + 1] }
if (extraKeys[row] == null)
extraKeys[row] = mutableListOf()
extraKeys[row]?.add(keys.first() to morekeys)
extraKeys[row]?.add(keys.first().toTextKey(keys.drop(1)))
}
private fun addLabel(split: List<String>) {
@ -127,31 +127,39 @@ private fun mergeMoreKeys(original: Array<String>, added: List<String>): Array<S
// we had 2 rows, and want it again
return (l + "${Key.MORE_KEYS_AUTO_COLUMN_ORDER}${round(l.size / 2f).toInt()}").toTypedArray()
}
// just drop autoColumnOrder otherwise (maybe not? depends on arising issues)
// just drop autoColumnOrder otherwise
return l.toTypedArray()
}
return moreKeys.toTypedArray()
}
fun addLocaleKeyTextsToParams(context: Context, params: KeyboardParams) {
val locales = Settings.getInstance().current.mSecondaryLocales + params.mId.locale
fun addLocaleKeyTextsToParams(context: Context, params: KeyboardParams, moreKeysSetting: Int) {
val locales = params.mSecondaryLocales + params.mId.locale
params.mLocaleKeyTexts = moreKeysAndLabels.getOrPut(locales.joinToString { it.toString() }) {
val lkt = LocaleKeyTexts(getStreamForLocale(params.mId.locale, context))
locales.forEach { locale ->
if (locale == params.mId.locale) return@forEach
lkt.addFile(getStreamForLocale(locale, context))
}
lkt
createLocaleKeyTexts(context, params, moreKeysSetting)
}
}
private fun createLocaleKeyTexts(context: Context, params: KeyboardParams, moreKeysSetting: Int): LocaleKeyTexts {
val lkt = LocaleKeyTexts(getStreamForLocale(params.mId.locale, context))
if (moreKeysSetting == MORE_KEYS_MORE)
lkt.addFile(context.assets.open("$LANGUAGE_TEXTS_FOLDER/all_more_keys.txt"))
else if (moreKeysSetting == MORE_KEYS_ALL)
lkt.addFile(context.assets.open("$LANGUAGE_TEXTS_FOLDER/more_more_keys.txt"))
params.mSecondaryLocales.forEach { locale ->
if (locale == params.mId.locale) return@forEach
lkt.addFile(getStreamForLocale(locale, context))
}
return lkt
}
private fun getStreamForLocale(locale: Locale, context: Context) =
try {
if (locale.toString() == "zz") context.assets.open("language_key_texts/more_more_keys.txt")
else context.assets.open("language_key_texts/${locale.toString().lowercase()}.txt")
if (locale.toString() == "zz") context.assets.open("$LANGUAGE_TEXTS_FOLDER/more_more_keys.txt")
else context.assets.open("$LANGUAGE_TEXTS_FOLDER/${locale.toString().lowercase()}.txt")
} catch (_: Exception) {
try {
context.assets.open("language_key_texts/${locale.language.lowercase()}.txt")
context.assets.open("$LANGUAGE_TEXTS_FOLDER/${locale.language.lowercase()}.txt")
} catch (_: Exception) {
null
}
@ -226,3 +234,5 @@ private val euroLocales = "bg|ca|cs|da|de|el|en|es|et|eu|fi|fr|ga|gl|hr|hu|it|lb
const val MORE_KEYS_ALL = 2;
const val MORE_KEYS_MORE = 1;
const val MORE_KEYS_NORMAL = 0;
const val LANGUAGE_TEXTS_FOLDER = "language_key_texts"

View file

@ -2,617 +2,50 @@
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser
import android.content.Context
import android.content.res.Resources
import android.view.inputmethod.EditorInfo
import org.dslul.openboard.inputmethod.keyboard.Key
import org.dslul.openboard.inputmethod.keyboard.Key.KeyParams
import org.dslul.openboard.inputmethod.keyboard.KeyboardId
import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardIconsSet
import android.util.Log
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.KeyData
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.toTextKey
import org.dslul.openboard.inputmethod.latin.common.splitOnWhitespace
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils
import org.dslul.openboard.inputmethod.latin.utils.RunInLocale
import org.dslul.openboard.inputmethod.latin.utils.sumOf
/**
* Parser for simple layouts like qwerty or symbol, defined only as rows of (normal) keys with moreKeys.
* Functional keys are pre-defined and can't be changed, with exception of comma, period and similar
* keys in symbol layouts.
* Parser for simple layouts, defined only as rows of (normal) keys with moreKeys.
* There may be a short "extra row" for the configurable keys in the bottom row. This is two keys
* for alphabet, 3 keys for symbols and 4 keys for shift symbols. MoreKeys on period and comma get
* merged with defaults.
* All normal keys have the same width and flags, which likely makes the simple layout definitions
* incompatible with the requirements of certain (non-latin) languages. These languages need to use
* a different (more configurable) layout definition style, and therefore a different parser.
* Also number, phone and numpad layouts are not compatible with this parser.
*/
class SimpleKeyboardParser(private val params: KeyboardParams, private val context: Context) {
class SimpleKeyboardParser(private val params: KeyboardParams, private val context: Context) : KeyboardParser(params, context) {
private val addExtraKeys =
params.mId.locale.language != "eo"
&& params.mId.mSubtype.keyboardLayoutSetName in listOf("nordic", "spanish", "german", "swiss", "serbian_qwertz")
private var addExtraKeys = false
fun parseFromAssets(layoutName: String): ArrayList<ArrayList<KeyParams>> {
val layoutFile = when (layoutName) {
"nordic" -> { addExtraKeys = true; "qwerty" }
"spanish" -> {
if (params.mId.locale.language == "eo") "eo" // this behaves a bit different than before, but probably still fine
else { addExtraKeys = true; "qwerty" }
}
"german", "swiss", "serbian_qwertz" -> { addExtraKeys = true; "qwertz" }
else -> layoutName
}
return parse(context.assets.open("layouts/$layoutFile.txt").reader().readText())
}
override fun getLayoutFromAssets(layoutName: String) =
context.assets.open("layouts/${getSimpleLayoutName(layoutName)}.txt").reader().readText()
fun parse(layoutContent: String): ArrayList<ArrayList<KeyParams>> {
params.readAttributes(context, null)
val keysInRows = ArrayList<ArrayList<KeyParams>>()
val baseKeys: MutableList<List<BaseKey>> = parseCoreLayout(layoutContent)
if (!params.mId.mNumberRowEnabled) {
// todo (later): not all layouts have numbers on first row, so maybe have some layout flag to switch it off (or an option)
// but for latin it's fine, so don't care now
val newFirstRow = baseKeys.first().mapIndexed { index, baseKey ->
if (index < numbers.size)
BaseKey(baseKey.label, baseKey.moreKeys?.let { arrayOf(numbers[index], *it) })
else baseKey
}
baseKeys[0] = newFirstRow
}
val functionalKeysReversed = parseFunctionalKeys().reversed()
// keyboard parsed bottom-up because the number of rows is not fixed, but the functional keys
// are always added to the rows near the bottom
keysInRows.add(getBottomRowAndAdjustBaseKeys(baseKeys))
baseKeys.reversed().forEachIndexed { i, it ->
val row: List<BaseKey> = if (i == 0) {
// add bottom row extra keys
it + context.getString(R.string.key_def_extra_bottom_right)
.split(",").mapNotNull { if (it.isBlank()) null else BaseKey(it.trim()) }
} else {
it
}
// parse functional keys for this row (if any)
val functionalKeysDefs = if (i < functionalKeysReversed.size) functionalKeysReversed[i]
else emptyList<String>() to emptyList()
val functionalKeysLeft = functionalKeysDefs.first.map { getFunctionalKeyParams(it) }
val functionalKeysRight = functionalKeysDefs.second.map { getFunctionalKeyParams(it) }
val paramsRow = ArrayList<KeyParams>(functionalKeysLeft)
// determine key width, maybe scale factor for keys, and spacers to add
val usedKeyWidth = params.mDefaultRelativeKeyWidth * row.size
val functionalKeyWidth = (functionalKeysLeft.sumOf { it.mRelativeWidth }) + (functionalKeysRight.sumOf { it.mRelativeWidth })
val availableWidth = 1f - functionalKeyWidth
var keyWidth: Float
val spacerWidth: Float
if (availableWidth - usedKeyWidth > 0.0001f) { // don't add spacers if only a tiny bit is empty
// width available, add spacer
keyWidth = params.mDefaultRelativeKeyWidth
spacerWidth = (availableWidth - usedKeyWidth) / 2
} else {
// need more width, re-scale
spacerWidth = 0f
keyWidth = availableWidth / row.size
}
if (spacerWidth != 0f) {
paramsRow.add(KeyParams.newSpacer(params, spacerWidth))
}
if (keyWidth < params.mDefaultRelativeKeyWidth * 0.82 && spacerWidth == 0f) {
// keys are very narrow, also rescale the functional keys to make keys a little wider
// 0.82 is just some guess for "too narrow"
// todo (maybe): works reasonably well, but actually functional keys could give some more of their width,
// as long as they end up above mDefaultRelativeKeyWidth
val allKeyScale = 1f / (functionalKeyWidth + row.size * params.mDefaultRelativeKeyWidth)
keyWidth = params.mDefaultRelativeKeyWidth * allKeyScale
functionalKeysLeft.forEach { it.mRelativeWidth *= allKeyScale }
functionalKeysRight.forEach { it.mRelativeWidth *= allKeyScale }
}
for (key in row) {
paramsRow.add(KeyParams(
key.label,
params,
keyWidth, // any reasonable way to scale width if there is a long text? might be allowed in user-defined layout
0, // todo: maybe autoScale / autoXScale if label has more than 2 characters (exception for emojis?)
Key.BACKGROUND_TYPE_NORMAL,
key.moreKeys
))
}
if (spacerWidth != 0f) {
paramsRow.add(KeyParams.newSpacer(params, spacerWidth))
}
functionalKeysRight.forEach { paramsRow.add(it) }
keysInRows.add(0, paramsRow) // we're doing it backwards, so add on top
}
resizeLastNormalRowIfNecessaryForAlignment(keysInRows)
// rescale height if we have more than 4 rows
val heightRescale = if (keysInRows.size > 4) 4f / keysInRows.size else 1f
if (params.mId.mNumberRowEnabled)
keysInRows.add(0, getNumberRow())
if (heightRescale != 1f)
// rescale all keys, so number row doesn't look weird (this is done like in current parsing)
// todo: in symbols view, number row is not rescaled
// so the symbols keyboard is higher than the normal one
// not a new issue, but should be solved in this migration
// how? possibly scale all keyboards to height of main alphabet? (consider suggestion strip)
keysInRows.forEach { it.forEach { it.mRelativeHeight *= heightRescale } }
return keysInRows
}
// resize keys in last row if they are wider than keys in the row above
// this is done so the keys align with the keys above
// done e.g. for nordic and swiss layouts
private fun resizeLastNormalRowIfNecessaryForAlignment(keysInRows: ArrayList<ArrayList<KeyParams>>) {
if (keysInRows.size < 3)
return
val lastNormalRow = keysInRows[keysInRows.lastIndex - 1]
val rowAboveLastNormalRow = keysInRows[keysInRows.lastIndex - 2]
if (lastNormalRow.any { it.isSpacer } || rowAboveLastNormalRow.any { it.isSpacer })
return // annoying to deal with, and probably no resize needed anyway
val lastNormalRowKeyWidth = lastNormalRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }.mRelativeWidth
val rowAboveLastNormalRowKeyWidth = rowAboveLastNormalRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }.mRelativeWidth
if (lastNormalRowKeyWidth <= rowAboveLastNormalRowKeyWidth + 0.0001f)
return // no need
if (lastNormalRow.any { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL && it.mRelativeWidth != lastNormalRowKeyWidth })
return // normal keys have different width, don't deal with this
val numberOfNormalKeys = lastNormalRow.count { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }
val widthBefore = numberOfNormalKeys * lastNormalRowKeyWidth
val widthAfter = numberOfNormalKeys * rowAboveLastNormalRowKeyWidth
val spacerWidth = (widthBefore - widthAfter) / 2
// resize keys and add spacers
lastNormalRow.forEach { if (it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL) it.mRelativeWidth = rowAboveLastNormalRowKeyWidth }
lastNormalRow.add(lastNormalRow.indexOfFirst { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }, KeyParams.newSpacer(params, spacerWidth))
lastNormalRow.add(lastNormalRow.indexOfLast { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL } + 1, KeyParams.newSpacer(params, spacerWidth))
}
private fun parseCoreLayout(layoutContent: String) =
layoutContent.replace("\r\n", "\n").split("\n\n").mapIndexedTo(mutableListOf()) { i, row ->
row.split("\n").mapNotNull {
if (it.isBlank()) return@mapNotNull null
val split = it.splitOnWhitespace()
val moreKeys = if (split.size == 1) {
null
} else if (split.size == 2 && split.last() == "$$$") { // todo: no good reason to ignore it if size > 2
// todo (later): could improve handling and show more currency moreKeys, depending on the moreMoreKeys setting
if (params.mId.passwordInput())
arrayOf("$")
else
arrayOf(getCurrencyKey(params.mId.locale).first)
} else {
Array(split.size - 1) { split[it + 1] }
}
BaseKey(split.first(), moreKeys)
} + if (addExtraKeys)
(params.mLocaleKeyTexts.getExtraKeys(i + 1)?.let { it.map { BaseKey(it.first, it.second) } } ?: emptyList())
else emptyList()
}
private fun parseFunctionalKeys(): List<Pair<List<String>, List<String>>> =
context.getString(R.string.key_def_functional).split("\n").mapNotNull { line ->
if (line.isBlank()) return@mapNotNull null
val p = line.split(";")
p.first().let { if (it.isBlank()) emptyList() else it.split(",") } to
p.last().let { if (it.isBlank()) emptyList() else it.split(",") }
}
private fun getBottomRowAndAdjustBaseKeys(baseKeys: MutableList<List<BaseKey>>): ArrayList<KeyParams> {
val adjustableKeyCount = when (params.mId.mElementId) {
KeyboardId.ELEMENT_SYMBOLS -> 3
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> 4
else -> 2 // must be alphabet, parser doesn't work for other elementIds
}
val adjustedKeys = if (baseKeys.last().size == adjustableKeyCount) baseKeys.last()
else null
if (adjustedKeys != null)
baseKeys.removeLast()
val bottomRow = ArrayList<KeyParams>()
context.getString(R.string.key_def_bottom_row).split(",").forEach {
val key = it.trim().splitOnWhitespace().first()
val adjustKey = when (key) {
KEY_COMMA -> adjustedKeys?.first()
KEY_PERIOD -> adjustedKeys?.last()
else -> null
}
val keyParams = getFunctionalKeyParams(it, adjustKey?.label, adjustKey?.moreKeys)
if (key == KEY_SPACE) { // add the extra keys around space
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) {
bottomRow.add(getFunctionalKeyParams(KEY_NUMPAD))
bottomRow.add(keyParams)
bottomRow.add(KeyParams(
adjustedKeys?.get(1)?.label ?: "/",
params,
params.mDefaultRelativeKeyWidth,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
adjustedKeys?.get(1)?.moreKeys
))
} else if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
bottomRow.add(KeyParams(
adjustedKeys?.get(1)?.label ?: "<",
params,
params.mDefaultRelativeKeyWidth,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
adjustedKeys?.get(1)?.moreKeys
))
bottomRow.add(keyParams)
bottomRow.add(KeyParams(
adjustedKeys?.get(2)?.label ?: ">",
params,
params.mDefaultRelativeKeyWidth,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
adjustedKeys?.get(2)?.moreKeys
))
} else { // alphabet
if (params.mId.mLanguageSwitchKeyEnabled)
bottomRow.add(getFunctionalKeyParams(KEY_LANGUAGE_SWITCH))
if (params.mId.mEmojiKeyEnabled)
bottomRow.add(getFunctionalKeyParams(KEY_EMOJI))
bottomRow.add(keyParams)
// todo (later): add zwnj if necessary (where to get that info? layout file? then likely will not happen in this parser)
}
} else {
bottomRow.add(keyParams)
}
}
// set space width
val space = bottomRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_SPACEBAR }
space.mRelativeWidth = 1f - bottomRow.filter { it != space }.sumOf { it.mRelativeWidth }
return bottomRow
}
// todo: everything below here likely can and should be shared with the planned parser for more complicated layouts
// abstract class?
// interface?
// utils file?
private fun getNumberRow(): ArrayList<KeyParams> {
val row = ArrayList<KeyParams>()
numbers.forEachIndexed { i, n ->
row.add(KeyParams(
n,
params,
params.mDefaultRelativeKeyWidth,
Key.LABEL_FLAGS_DISABLE_HINT_LABEL, // todo (later): maybe optional or enable (but then all numbers should have moreKeys)
Key.BACKGROUND_TYPE_NORMAL,
numbersMoreKeys[i] // todo (later, non-latin): language may add some (either alt numbers, or latin numbers if they are replaced above, see number todo)
))
}
return row
}
// for comma and period: label will override default, moreKeys will be appended
private fun getFunctionalKeyParams(def: String, label: String? = null, moreKeys: Array<String>? = null): KeyParams {
val split = def.trim().splitOnWhitespace()
val key = split[0]
val width = if (split.size == 2) split[1].substringBefore("%").toFloat() / 100f
else params.mDefaultRelativeKeyWidth
return when (key) {
KEY_SYMBOL -> KeyParams(
"${getSymbolLabel()}|!code/key_switch_alpha_symbol", // todo (later): in numpad the code is key_symbolNumpad
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
KEY_COMMA -> KeyParams(
label ?: getDefaultCommaLabel(),
params,
width,
Key.LABEL_FLAGS_HAS_POPUP_HINT, // previously only if normal comma, but always is more correct
if (label?.first()?.isLetter() == true) Key.BACKGROUND_TYPE_NORMAL else Key.BACKGROUND_TYPE_FUNCTIONAL,
moreKeys?.let { getCommaMoreKeys() + it } ?: getCommaMoreKeys()
)
KEY_SPACE -> KeyParams(
"!icon/space_key|!code/key_space", // !icon/space_key_for_number_layout in number layout, but not on tablet
params,
width, // will not be used for normal space (only in number layouts)
0, // todo (later): alignIconToBottom for non-tablet number layout
Key.BACKGROUND_TYPE_SPACEBAR,
null
)
KEY_PERIOD -> KeyParams(
label ?: ".",
params,
width,
Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT, // todo (later): check what LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT does, maybe remove the flag here
if (label?.first()?.isLetter() == true) Key.BACKGROUND_TYPE_NORMAL else Key.BACKGROUND_TYPE_FUNCTIONAL,
moreKeys?.let { getPunctuationMoreKeys() + it } ?: getPunctuationMoreKeys()
)
KEY_ACTION -> KeyParams(
"${getActionKeyLabel()}|${getActionKeyCode()}",
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE
or Key.LABEL_FLAGS_AUTO_X_SCALE
or Key.LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR
or KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId),
Key.BACKGROUND_TYPE_ACTION,
getActionKeyMoreKeys()
)
KEY_DELETE -> KeyParams(
"!icon/delete_key|!code/key_delete",
params,
width,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
KEY_SHIFT -> KeyParams(
"${getShiftLabel()}|!code/key_shift",
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE,
// todo (later): possibly the whole stickOn/Off stuff can be removed, currently it should only have a very slight effect in holo
if (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED)
Key.BACKGROUND_TYPE_STICKY_ON
else Key.BACKGROUND_TYPE_STICKY_OFF,
arrayOf("!noPanelAutoMoreKey!", " |!code/key_capslock")
)
KEY_EMOJI -> KeyParams(
"!icon/emoji_normal_key|!code/key_emoji",
params,
width,
KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId),
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
// tablet layout has an emoji key that changes to com key in url / mail
KEY_EMOJI_COM -> if (params.mId.mMode == KeyboardId.MODE_URL || params.mId.mMode == KeyboardId.MODE_EMAIL)
getFunctionalKeyParams(KEY_COM)
else getFunctionalKeyParams(KEY_EMOJI)
KEY_COM -> KeyParams( // todo: label and moreKeys could be in localeKeyTexts
".com",
params,
width,
Key.LABEL_FLAGS_AUTO_X_SCALE or Key.LABEL_FLAGS_FONT_NORMAL or Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_PRESERVE_CASE,
Key.BACKGROUND_TYPE_FUNCTIONAL,
arrayOf("!hasLabels!", ".net", ".org", ".gov", ".edu")
)
KEY_LANGUAGE_SWITCH -> KeyParams(
"!icon/language_switch_key|!code/key_language_switch",
params,
width,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
KEY_ALPHA -> KeyParams(
"${getAlphabetLabel()}|!code/key_switch_alpha_symbol", // todo (later): in numpad the code is key_alphaNumpad
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
KEY_NUMPAD -> KeyParams(
"!icon/numpad_key|!code/key_numpad",
params,
width,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
else -> throw IllegalArgumentException("unknown key definition \"$key\"")
override fun parseCoreLayout(layoutContent: String): MutableList<List<KeyData>> {
val rowStrings = layoutContent.replace("\r\n", "\n").split("\n\n")
return rowStrings.mapIndexedTo(mutableListOf()) { i, row ->
if (addExtraKeys)
getExtraKeys(i)?.let { parseRow(row) + it } ?: parseRow(row)
else
parseRow(row)
}
}
private fun getActionKeyLabel(): String {
if (params.mId.isMultiLine && (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED))
return "!icon/enter_key"
val iconName = when (params.mId.imeAction()) {
EditorInfo.IME_ACTION_GO -> KeyboardIconsSet.NAME_GO_KEY
EditorInfo.IME_ACTION_SEARCH -> KeyboardIconsSet.NAME_SEARCH_KEY
EditorInfo.IME_ACTION_SEND -> KeyboardIconsSet.NAME_SEND_KEY
EditorInfo.IME_ACTION_NEXT -> KeyboardIconsSet.NAME_NEXT_KEY
EditorInfo.IME_ACTION_DONE -> KeyboardIconsSet.NAME_DONE_KEY
EditorInfo.IME_ACTION_PREVIOUS -> KeyboardIconsSet.NAME_PREVIOUS_KEY
InputTypeUtils.IME_ACTION_CUSTOM_LABEL -> return params.mId.mCustomActionLabel
else -> return "!icon/enter_key"
private fun parseRow(row: String): List<KeyData> =
row.split("\n").mapNotNull {
if (it.isBlank()) null
else parseKey(it)
}
val replacement = iconName.replaceIconWithLabelIfNoDrawable()
return if (iconName == replacement) // i.e. icon exists
"!icon/$iconName"
else
replacement
}
private fun getActionKeyCode() =
if (params.mId.isMultiLine && (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED))
"!code/key_shift_enter"
else "!code/key_enter"
private fun getExtraKeys(rowIndex: Int) = params.mLocaleKeyTexts.getExtraKeys(rowIndex + 1)
private fun getActionKeyMoreKeys(): Array<String>? {
val action = params.mId.imeAction()
val navigatePrev = params.mId.navigatePrevious()
val navigateNext = params.mId.navigateNext()
return when {
params.mId.passwordInput() -> when {
navigatePrev && action == EditorInfo.IME_ACTION_NEXT -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
action == EditorInfo.IME_ACTION_NEXT -> null
navigateNext && action == EditorInfo.IME_ACTION_PREVIOUS -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
action == EditorInfo.IME_ACTION_PREVIOUS -> null
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS_NEXT)
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
else -> null
}
// could change definition of numbers to query a range, or have a pre-defined list, but not that crucial
params.mId.mMode in listOf(KeyboardId.MODE_URL, KeyboardId.MODE_EMAIL, KeyboardId.ELEMENT_PHONE, KeyboardId.ELEMENT_NUMBER, KeyboardId.MODE_DATE, KeyboardId.MODE_TIME, KeyboardId.MODE_DATETIME) -> when {
action == EditorInfo.IME_ACTION_NEXT && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
action == EditorInfo.IME_ACTION_NEXT -> null
action == EditorInfo.IME_ACTION_PREVIOUS && navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
action == EditorInfo.IME_ACTION_PREVIOUS -> null
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS_NEXT)
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
else -> null
}
action == EditorInfo.IME_ACTION_NEXT && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS)
action == EditorInfo.IME_ACTION_NEXT -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
action == EditorInfo.IME_ACTION_PREVIOUS && navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_NEXT)
action == EditorInfo.IME_ACTION_PREVIOUS -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS_NEXT)
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_NEXT)
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS)
else -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
}
}
private fun createMoreKeysArray(moreKeysDef: String): Array<String> {
val moreKeys = mutableListOf<String>()
for (moreKey in moreKeysDef.split(",")) {
val iconPrefixRemoved = moreKey.substringAfter("!icon/")
if (iconPrefixRemoved == moreKey) { // i.e. there is no !icon/
moreKeys.add(moreKey)
continue
}
val iconName = iconPrefixRemoved.substringBefore("|")
val replacementText = iconName.replaceIconWithLabelIfNoDrawable()
if (replacementText == iconName) { // i.e. we have the drawable
moreKeys.add(moreKey)
} else {
moreKeys.add("!hasLabels!") // test what it actually does, but it's probably necessary
moreKeys.add(replacementText)
}
}
return moreKeys.toTypedArray()
}
private fun String.replaceIconWithLabelIfNoDrawable(): String {
if (params.mIconsSet.getIconDrawable(KeyboardIconsSet.getIconId(this)) != null) return this
val id = context.resources.getIdentifier("label_$this", "string", context.packageName)
val ril = object : RunInLocale<String>() { // todo (later): simpler way of doing this in a single line?
override fun job(res: Resources) = res.getString(id)
}
return ril.runInLocale(context.resources, params.mId.locale)
}
private fun getAlphabetLabel() = params.mLocaleKeyTexts.labelAlphabet
private fun getSymbolLabel() = params.mLocaleKeyTexts.labelSymbols
private fun getShiftLabel(): String {
val elementId = params.mId.mElementId
if (elementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
return params.mLocaleKeyTexts.labelShiftSymbols
if (elementId == KeyboardId.ELEMENT_SYMBOLS)
return getSymbolLabel()
if (elementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || elementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED
|| elementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED || elementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED)
return "!icon/shift_key_shifted"
return "!icon/shift_key"
}
private fun getDefaultCommaLabel(): String {
if (params.mId.mMode == KeyboardId.MODE_URL)
return "/"
if (params.mId.mMode == KeyboardId.MODE_EMAIL)
return "\\@"
return ","
}
private fun getCommaMoreKeys(): Array<String> {
val keys = mutableListOf<String>()
if (!params.mId.mDeviceLocked)
keys.add("!icon/clipboard_normal_key|!code/key_clipboard")
if (!params.mId.mEmojiKeyEnabled)
keys.add("!icon/emoji_normal_key|!code/key_emoji")
if (!params.mId.mLanguageSwitchKeyEnabled)
keys.add("!icon/language_switch_key|!code/key_language_switch")
if (!params.mId.mOneHandedModeEnabled)
keys.add("!icon/start_onehanded_mode_key|!code/key_start_onehanded")
if (!params.mId.mDeviceLocked)
keys.add("!icon/settings_key|!code/key_settings")
return keys.toTypedArray()
}
private fun getPunctuationMoreKeys(): Array<String> {
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS || params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
return arrayOf("")
val moreKeys = params.mLocaleKeyTexts.getMoreKeys("punctuation") ?:
// todo: some (non-latin) languages have different parenthesis keys
arrayOf("${Key.MORE_KEYS_AUTO_COLUMN_ORDER}8", "\\,", "?", "!", "#", ")", "(", "/", ";", "'", "@", ":", "-", "\"", "+", "\\%", "&")
if (context.resources.getInteger(R.integer.config_screen_metrics) >= 3 && moreKeys.contains("!") && moreKeys.contains("?")) {
// we have a tablet, remove ! and ? keys and reduce number in autoColumnOrder
// this makes use of removal of empty moreKeys in MoreKeySpec.insertAdditionalMoreKeys
// todo: maybe do this as part of removing unnecessary moreKeys instead of here?
moreKeys[moreKeys.indexOf("!")] = ""
moreKeys[moreKeys.indexOf("?")] = ""
val columns = moreKeys[0].substringAfter(Key.MORE_KEYS_AUTO_COLUMN_ORDER).toIntOrNull()
if (columns != null)
moreKeys[0] = "${Key.MORE_KEYS_AUTO_COLUMN_ORDER}${columns - 1}"
}
return moreKeys
}
companion object {
fun hasLayoutFile(layoutName: String) = layoutName in supportedLayouts
// todo: adjust when changing layout names, and of course when anything changes...
private val supportedLayouts = hashSetOf("qwerty", "qwertz", "halmak", "workman", "bepo", "swiss", "german", "nordic", "spanish", "serbian_qwertz")
private fun parseKey(key: String): KeyData {
val split = key.splitOnWhitespace()
return if (split.size == 1)
split.first().toTextKey()
else
split.first().toTextKey(split.drop(1))
}
}
// class for holding a parsed key of the simple layout
private class BaseKey(
val label: String,
val moreKeys: Array<String>? = null,
)
// todo (later): may depend on language for non-latin layouts... or should the number row always be latin?
// best have number row layout in a file, and get layout file dependent on keyboard layout and some setting
// setting should be sth that goes like "prioritize localized numbers"
// idea for behavior:
// if no extra local number row: number row like now, no number row shows numbers in moreKeys
// for other numbers:
// language-dependent number moreKeys (latin numbers on localized variant, and localized variant on latin numbers)
// show whatever is selected as number row, or as morekeys if number row is off
// but then how to simply put both latin and local number in moreKeys when number row is disabled?
// consider that numbers may have more moreKeys...
// idea: field [numbers] where in the next line the numbers are space-separated in order 1-9 and 0
// would be simpler to handle than moreKeys
private val numbers = (1..9).map { it.toString() } + "0"
// moreKeys for numbers, order is 1-9 and then 0
// todo (later): like numbers, for non-latin layouts this depends on language and therefore should not be in the parser
private val numbersMoreKeys = arrayOf(
arrayOf("¹", "½", "","¼", ""),
arrayOf("²", ""),
arrayOf("³", "¾", ""),
arrayOf(""),
arrayOf(""),
null,
arrayOf(""),
null,
null,
arrayOf("", ""),
)
// could use 1 string per key, and make arrays right away
private const val MORE_KEYS_NAVIGATE_PREVIOUS = "!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard"
private const val MORE_KEYS_NAVIGATE_NEXT = "!icon/clipboard_action_key|!code/key_clipboard,!icon/next_key|!code/key_action_next"
private const val MORE_KEYS_NAVIGATE_PREVIOUS_NEXT = "!fixedColumnOrder!3,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/next_key|!code/key_action_next"
private const val MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS = "!fixedColumnOrder!3,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji"
private const val MORE_KEYS_NAVIGATE_EMOJI = "!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji"
private const val MORE_KEYS_NAVIGATE_EMOJI_NEXT = "!fixedColumnOrder!3,!needsDividers!,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji,!icon/next_key|!code/key_action_next"
private const val MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS_NEXT = "!fixedColumnOrder!4,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji,!icon/next_key|!code/key_action_next"
private const val KEY_EMOJI = "emoji"
private const val KEY_LANGUAGE_SWITCH = "language"
private const val KEY_COM = "com"
private const val KEY_EMOJI_COM = "emoji_com"
private const val KEY_DELETE = "delete"
private const val KEY_ACTION = "action"
private const val KEY_PERIOD = "period"
private const val KEY_COMMA = "comma"
private const val KEY_SPACE = "space"
private const val KEY_SHIFT = "shift"
private const val KEY_NUMPAD = "numpad"
private const val KEY_SYMBOL = "symbol"
private const val KEY_ALPHA = "alphabet"

View file

@ -0,0 +1,128 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* modified
* SPDX-License-Identifier: Apache-2.0
*/
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris
// taken from FlorisBoard, not actually used
object KeyCode {
object Spec {
const val CHARACTERS_MIN = 1
const val CHARACTERS_MAX = 65535
val CHARACTERS = CHARACTERS_MIN..CHARACTERS_MAX
const val INTERNAL_MIN = -9999
const val INTERNAL_MAX = -1
val INTERNAL = INTERNAL_MIN..INTERNAL_MAX
}
const val UNSPECIFIED = 0
const val PHONE_WAIT = 59 // ;
const val PHONE_PAUSE = 44 // ,
const val SPACE = 32
const val ESCAPE = 27
const val ENTER = 10
const val TAB = 9
const val CTRL = -1
const val CTRL_LOCK = -2
const val ALT = -3
const val ALT_LOCK = -4
const val FN = -5
const val FN_LOCK = -6
const val DELETE = -7
const val DELETE_WORD = -8
const val FORWARD_DELETE = -9
const val FORWARD_DELETE_WORD = -10
const val SHIFT = -11
const val CAPS_LOCK = -13
const val ARROW_LEFT = -21
const val ARROW_RIGHT = -22
const val ARROW_UP = -23
const val ARROW_DOWN = -24
const val MOVE_START_OF_PAGE = -25
const val MOVE_END_OF_PAGE = -26
const val MOVE_START_OF_LINE = -27
const val MOVE_END_OF_LINE = -28
const val CLIPBOARD_COPY = -31
const val CLIPBOARD_CUT = -32
const val CLIPBOARD_PASTE = -33
const val CLIPBOARD_SELECT = -34
const val CLIPBOARD_SELECT_ALL = -35
const val CLIPBOARD_CLEAR_HISTORY = -36
const val CLIPBOARD_CLEAR_FULL_HISTORY = -37
const val CLIPBOARD_CLEAR_PRIMARY_CLIP = -38
const val COMPACT_LAYOUT_TO_LEFT = -111
const val COMPACT_LAYOUT_TO_RIGHT = -112
const val SPLIT_LAYOUT = -113
const val MERGE_LAYOUT = -114
const val UNDO = -131
const val REDO = -132
const val VIEW_CHARACTERS = -201
const val VIEW_SYMBOLS = -202
const val VIEW_SYMBOLS2 = -203
const val VIEW_NUMERIC = -204
const val VIEW_NUMERIC_ADVANCED = -205
const val VIEW_PHONE = -206
const val VIEW_PHONE2 = -207
const val IME_UI_MODE_TEXT = -211
const val IME_UI_MODE_MEDIA = -212
const val IME_UI_MODE_CLIPBOARD = -213
const val SYSTEM_INPUT_METHOD_PICKER = -221
const val SYSTEM_PREV_INPUT_METHOD = -222
const val SYSTEM_NEXT_INPUT_METHOD = -223
const val IME_SUBTYPE_PICKER = -224
const val IME_PREV_SUBTYPE = -225
const val IME_NEXT_SUBTYPE = -226
const val LANGUAGE_SWITCH = -227
const val IME_SHOW_UI = -231
const val IME_HIDE_UI = -232
const val VOICE_INPUT = -233
const val TOGGLE_SMARTBAR_VISIBILITY = -241
const val TOGGLE_ACTIONS_OVERFLOW = -242
const val TOGGLE_ACTIONS_EDITOR = -243
const val TOGGLE_INCOGNITO_MODE = -244
const val TOGGLE_AUTOCORRECT = -245
const val URI_COMPONENT_TLD = -255
const val SETTINGS = -301
const val CURRENCY_SLOT_1 = -801
const val CURRENCY_SLOT_2 = -802
const val CURRENCY_SLOT_3 = -803
const val CURRENCY_SLOT_4 = -804
const val CURRENCY_SLOT_5 = -805
const val CURRENCY_SLOT_6 = -806
const val MULTIPLE_CODE_POINTS = -902
const val DRAG_MARKER = -991
const val NOOP = -999
const val CHAR_WIDTH_SWITCHER = -9701
const val CHAR_WIDTH_FULL = -9702
const val CHAR_WIDTH_HALF = -9703
const val KANA_SMALL = 12307
const val KANA_SWITCHER = -9710
const val KANA_HIRA = -9711
const val KANA_KATA = -9712
const val KANA_HALF_KATA = -9713
const val KESHIDA = 1600
const val HALF_SPACE = 8204
const val CJK_SPACE = 12288
}

View file

@ -0,0 +1,367 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.dslul.openboard.inputmethod.keyboard.Key
import org.dslul.openboard.inputmethod.keyboard.Key.KeyParams
import org.dslul.openboard.inputmethod.keyboard.KeyboardId
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
import org.dslul.openboard.inputmethod.latin.common.StringUtils
// taken from FlorisBoard, small modifications
// popup not nullable (maybe change back, but currently that's necessary for number keys)
// added getLabel for creating moreKeys from popups (which may be abstract)
// added toKeyParams for non-abstract KeyData
// compute is using KeyboardParams (for shift state and variation)
// char_width_selector and kana_selector throw an error (not yet supported)
/**
* Basic interface for a key data object. Base for all key data objects across the IME, such as text, emojis and
* selectors. The implementation is as abstract as possible, as different features require different implementations.
*/
interface AbstractKeyData {
/**
* Computes a [KeyData] object for this key data. Returns null if no computation is possible or if the key is
* not relevant based on the result of [params].
*
* @param params The KeyboardParams used to retrieve different states from the parent controller.
*
* @return A [KeyData] object or null if no computation is possible.
*/
fun compute(params: KeyboardParams): KeyData?
/**
* Returns the data described by this key as a string.
*
* @param isForDisplay Specifies if the returned string is intended to be displayed in a UI label (=true) or if
* it should be computed to be sent to an input connection (=false).
*
* @return The computed string for the key data object. Note: some objects may return an empty string here, meaning
* it is always required to check for the string's length before attempting to directly retrieve the first char.
*/
fun asString(isForDisplay: Boolean): String // todo: remove it? not used at all (better only later, maybe useful for getting display label in some languages)
fun getLabel(params: KeyboardParams) = if (this is KeyData) label else compute(params)?.label ?: ""
}
/**
* Interface describing a basic key which can carry a character, an emoji, a special function etc. while being as
* abstract as possible.
*
* @property type The type of the key.
* @property code The Unicode code point of this key, or a special code from [KeyCode].
* @property label The label of the key. This should always be a representative string for [code].
* @property groupId The group which this key belongs to (currently only allows [GROUP_DEFAULT]).
* @property popup The popups for ths key. Can also dynamically be provided via popup extensions.
*/
interface KeyData : AbstractKeyData {
val type: KeyType
val code: Int
val label: String
val groupId: Int
val popup: PopupSet<AbstractKeyData> // not nullable because can't add number otherwise
// groups (currently) not supported
companion object {
/**
* Constant for the default group. If not otherwise specified, any key is automatically
* assigned to this group.
*/
const val GROUP_DEFAULT: Int = 0
/**
* Constant for the Left modifier key group. Any key belonging to this group will get the
* popups specified for "~left" in the popup mapping.
*/
const val GROUP_LEFT: Int = 1
/**
* Constant for the right modifier key group. Any key belonging to this group will get the
* popups specified for "~right" in the popup mapping.
*/
const val GROUP_RIGHT: Int = 2
/**
* Constant for the enter modifier key group. Any key belonging to this group will get the
* popups specified for "~enter" in the popup mapping.
*/
const val GROUP_ENTER: Int = 3
/**
* Constant for the enter modifier key group. Any key belonging to this group will get the
* popups specified for "~kana" in the popup mapping.
*/
const val GROUP_KANA: Int = 97
}
fun isSpaceKey(): Boolean {
return type == KeyType.CHARACTER && (code == KeyCode.SPACE || code == KeyCode.CJK_SPACE
|| code == KeyCode.HALF_SPACE || code == KeyCode.KESHIDA)
}
fun toKeyParams(params: KeyboardParams, width: Float = params.mDefaultRelativeKeyWidth, labelFlags: Int = 0): KeyParams {
require(type == KeyType.CHARACTER) { "currently only KeyType.CHARACTER is supported" }
require(groupId == GROUP_DEFAULT) { "currently only KeyData.GROUP_DEFAULT is supported" }
require(code >= 0) { "functional codes ($code) not (yet) supported" }
require(code != KeyCode.UNSPECIFIED || label.isNotEmpty()) { "key has no code and no label" }
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)
// but also longer labels should work without issues, also for MultiTextKeyData
KeyParams(
label, // todo (when supported): convert special labels to keySpec
params,
width,
labelFlags, // todo (non-latin): label flags... maybe relevant for some languages
Key.BACKGROUND_TYPE_NORMAL, // todo (when supported): determine type
popup.toMoreKeys(params),
)
} else {
KeyParams(
label.ifEmpty { StringUtils.newSingleCodePointString(code) },
code, // todo (when supported): convert codes < 0
params,
width,
labelFlags,
Key.BACKGROUND_TYPE_NORMAL,
popup.toMoreKeys(params),
)
}
}
}
/**
* Allows to select an [AbstractKeyData] based on the current caps state. Note that this type of selector only really
* makes sense in a text context, though technically speaking it can be used anywhere, so this implementation allows
* for any [AbstractKeyData] to be used here. The JSON class identifier for this selector is `case_selector`.
*
* Example usage in a layout JSON file:
* ```
* { "$": "case_selector",
* "lower": { "code": 59, "label": ";" },
* "upper": { "code": 58, "label": ":" }
* }
* ```
*
* @property lower The key data to use if the current caps state is lowercase.
* @property upper The key data to use if the current caps state is uppercase.
*/
@Serializable
@SerialName("case_selector")
class CaseSelector(
val lower: AbstractKeyData,
val upper: AbstractKeyData,
) : AbstractKeyData {
override fun compute(params: KeyboardParams): KeyData? {
return (if (params.mId.isAlphabetShifted) { upper } else { lower }).compute(params)
}
override fun asString(isForDisplay: Boolean): String {
return ""
}
}
/**
* Allows to select an [AbstractKeyData] based on the current shift state. Note that this type of selector only really
* makes sense in a text context, though technically speaking it can be used anywhere, so this implementation allows
* for any [AbstractKeyData] to be used here. The JSON class identifier for this selector is `shift_state_selector`.
*
* Example usage in a layout JSON file:
* ```
* { "$": "shift_state_selector",
* "shiftedManual": { "code": 59, "label": ";" },
* "default": { "code": 58, "label": ":" }
* }
* ```
*
* @property unshifted The key data to use if the current shift state is unshifted, falling back to
* [default] if unspecified.
* @property shifted The key data to use if the current shift state is either manual or
* automatic. Is overridden if [shiftedManual] or [shiftedAutomatic] is specified.
* @property shiftedManual The key data to use if the current shift state is manual,
* falling back to [shifted] or [default] if unspecified.
* @property shiftedAutomatic The key data to use if the current shift state is automatic,
* falling back to [shifted] or [default] if unspecified.
* @property capsLock The key data to use if the current shift state is locked, falling back to
* [default] if unspecified.
* @property default The key data to use if the current shift state is set to a value not specified by this selector.
* If a key data is provided for all shift states possible this key data will never be used.
*/
@Serializable
@SerialName("shift_state_selector")
class ShiftStateSelector(
val unshifted: AbstractKeyData? = null,
val shifted: AbstractKeyData? = null,
val shiftedManual: AbstractKeyData? = null,
val shiftedAutomatic: AbstractKeyData? = null,
val capsLock: AbstractKeyData? = null,
val default: AbstractKeyData? = null,
) : AbstractKeyData {
override fun compute(params: KeyboardParams): KeyData? {
return when (params.mId.mElementId) {
KeyboardId.ELEMENT_ALPHABET, KeyboardId.ELEMENT_SYMBOLS -> unshifted ?: default
KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED -> shiftedManual ?: shifted ?: default
KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED -> shiftedAutomatic ?: shifted ?: default
KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED -> capsLock ?: shifted ?: default
else -> default // or rather unshifted?
}?.compute(params)
}
override fun asString(isForDisplay: Boolean): String {
return ""
}
}
/**
* Allows to select an [AbstractKeyData] based on the current variation. Note that this type of selector only really
* makes sense in a text context, though technically speaking it can be used anywhere, so this implementation allows
* for any [AbstractKeyData] to be used here. The JSON class identifier for this selector is `variation_selector`.
*
* Example usage in a layout JSON file:
* ```
* { "$": "variation_selector",
* "default": { "code": 44, "label": "," },
* "email": { "code": 64, "label": "@" },
* "uri": { "code": 47, "label": "/" }
* }
* ```
*
* @property default The default key data which should be used in case no key variation is known or for the current
* key variation no override key is defined. Can be null, in this case this may mean the variation selector hides
* the key if no direct match is present.
* @property email The key data to use if [KeyboardId.MODE_EMAIL] is active. If this value is
* null, [default] will be used instead.
* @property uri The key data to use if [KeyboardId.MODE_URL] is active. If this value is null,
* [default] will be used instead.
* @property normal The key data to use when? Currently ignored... If this value is null,
* [default] will be used instead.
* @property password The key data to use if [KeyboardId.passwordInput] return true. If this value is
* null, [default] will be used instead.
*/
@Serializable
@SerialName("variation_selector")
data class VariationSelector(
val default: AbstractKeyData? = null,
val email: AbstractKeyData? = null,
val uri: AbstractKeyData? = null,
val normal: AbstractKeyData? = null,
val password: AbstractKeyData? = null,
) : AbstractKeyData {
override fun compute(params: KeyboardParams): KeyData? {
return when {
// todo: what is normal and all?
// KeyVariation.ALL -> default
// KeyVariation.NORMAL -> normal ?: default
params.mId.passwordInput() -> password ?: default
params.mId.mMode == KeyboardId.MODE_EMAIL -> email ?: default
params.mId.mMode == KeyboardId.MODE_URL -> uri ?: default
else -> default
}?.compute(params)
}
override fun asString(isForDisplay: Boolean): String {
return ""
}
}
/**
* Allows to select an [AbstractKeyData] based on the current layout direction. Note that this type of selector only
* really makes sense in a text context, though technically speaking it can be used anywhere, so this implementation
* allows for any [AbstractKeyData] to be used here. The JSON class identifier for this selector is
* `layout_direction_selector`.
*
* Example usage in a layout JSON file:
* ```
* { "$": "layout_direction_selector",
* "ltr": { "code": 59, "label": ";" },
* "rtl": { "code": 58, "label": ":" }
* }
* ```
*
* @property ltr The key data to use if the current layout direction is LTR.
* @property rtl The key data to use if the current layout direction is RTL.
*/
@Serializable
@SerialName("layout_direction_selector")
class LayoutDirectionSelector(
val ltr: AbstractKeyData,
val rtl: AbstractKeyData,
) : AbstractKeyData {
override fun compute(params: KeyboardParams): KeyData? {
return (if (params.mId.mSubtype.isRtlSubtype) { rtl } else { ltr }).compute(params)
}
override fun asString(isForDisplay: Boolean): String {
return ""
}
}
/**
* Allows to select an [AbstractKeyData] based on the character's width. Note that this type of selector only really
* makes sense in a text context, though technically speaking it can be used anywhere, so this implementation allows
* for any [AbstractKeyData] to be used here. The JSON class identifier for this selector is `char_width_selector`.
*
* Example usage in a layout JSON file:
* ```
* { "$": "char_width_selector",
* "full": { "code": 12450, "label": "" },
* "half": { "code": 65393, "label": "" }
* }
* ```
*
* @property full The key data to use if the current character width is full.
* @property half The key data to use if the current character width is half.
*/
@Serializable
@SerialName("char_width_selector")
class CharWidthSelector(
val full: AbstractKeyData?,
val half: AbstractKeyData?,
) : AbstractKeyData {
override fun compute(params: KeyboardParams): KeyData? {
throw UnsupportedOperationException("char_width_selector not (yet) supported")
// val data = if (params.halfWidth) { half } else { full }
// return data?.compute(params)
}
override fun asString(isForDisplay: Boolean): String {
return ""
}
}
/**
* Allows to select an [AbstractKeyData] based on the kana state. Note that this type of selector only really
* makes sense in a text context, though technically speaking it can be used anywhere, so this implementation allows
* for any [AbstractKeyData] to be used here. The JSON class identifier for this selector is `kana_selector`.
*
* Example usage in a layout JSON file:
* ```
* { "$": "kana_selector",
* "hira": { "code": 12354, "label": "" },
* "kata": { "code": 12450, "label": "" }
* }
* ```
*
* @property hira The key data to use if the current kana state is hiragana.
* @property kata The key data to use if the current kana state is katakana.
*/
@Serializable
@SerialName("kana_selector")
class KanaSelector(
val hira: AbstractKeyData,
val kata: AbstractKeyData,
) : AbstractKeyData {
override fun compute(params: KeyboardParams): KeyData? {
throw UnsupportedOperationException("kana_selector not (yet) supported")
// val data = if (evaluator.state.isKanaKata) { kata } else { hira }
// return data.compute(evaluator)
}
override fun asString(isForDisplay: Boolean): String {
return ""
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (C) 2020 Patrick Goldinger
* modified
* SPDX-License-Identifier: Apache-2.0
*/
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
// taken from FlorisBoard, not actually used (only CHARACTER allowed)
/**
* Enum for declaring the type of the key.
* List of possible key types:
* [Wikipedia](https://en.wikipedia.org/wiki/Keyboard_layout#Key_types)
*/
@Serializable(with = KeyTypeSerializer::class)
enum class KeyType {
CHARACTER,
ENTER_EDITING,
FUNCTION,
LOCK,
MODIFIER,
NAVIGATION,
SYSTEM_GUI,
NUMERIC,
PLACEHOLDER,
UNSPECIFIED;
override fun toString(): String {
return super.toString().lowercase()
}
companion object {
fun fromString(string: String): KeyType {
return valueOf(string.uppercase())
}
}
}
class KeyTypeSerializer : KSerializer<KeyType> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("KeyType", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: KeyType) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): KeyType {
return KeyType.fromString(decoder.decodeString())
}
}

View file

@ -0,0 +1,115 @@
/*
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris
import kotlinx.serialization.Serializable
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.getCurrencyKey
// taken from FlorisBoard, small modifications
// mutable set removed (currently the moreKeys assembly is happening in KeyParams)
// toMoreKeys and size added
// popupKeys not used, but might switch to this later
// currently hint would be taken from other, and languageMoreKeys are prioritized
/**
* A popup set for a single key. This set describes, if the key has a [main] character and other [relevant] popups.
*
* Note that a hint character cannot and should not be set in a json extended popup file, rather it
* should only be dynamically set by the LayoutManager.
*/
@Serializable
open class PopupSet<T : AbstractKeyData>(
open val main: T? = null,
open val relevant: List<T> = emptyList()
) {
// todo (idea):
// this is very simple, but essentially what happens in the old system
// could make use of PopupKeys, and also provide a hint depending on user choice
// then language key joining should be done in here too
// also what about getting the moreKeys and key hint from chosen symbol layout?
fun toMoreKeys(params: KeyboardParams) = if (isEmpty) null // need null instead of empty array for KeyParams
else (listOfNotNull(number?.toString(), main?.getLabel(params)) + relevant
// todo: this is not nice and creates unnecessary intermediate lists
// best treat the currency key properly, like florisboard does
.map { it.getLabel(params) }).map {
if (it == "$$$") {
if (params.mId.passwordInput()) "$"
else getCurrencyKey(params.mId.locale).first
}
else it
}.toTypedArray()
private val popupKeys: PopupKeys<T> by lazy {
PopupKeys(null, listOfNotNull(main), relevant)
}
var number: Int? = null
val isEmpty get() = main == null && relevant.isEmpty() && number == null
}
/**
* A fully configured collection of popup keys. It contains a list of keys to be prioritized
* during rendering (ordered by relevance descending) by showing those keys close to the
* popup spawning point.
*
* The keys contain a separate [hint] key to ease rendering the hint label, but the hint, if
* present, also occurs in the [prioritized] list.
*
* The popup keys can be accessed like an array with the addition that negative indexes defined
* within this companion object are allowed (as long as the corresponding [prioritized] list
* contains the corresponding amount of keys.
*/
class PopupKeys<T>(
val hint: T?,
val prioritized: List<T>,
val other: List<T>
) : Collection<T> {
companion object {
const val FIRST_PRIORITIZED = -1
const val SECOND_PRIORITIZED = -2
const val THIRD_PRIORITIZED = -3
}
override val size: Int
get() = prioritized.size + other.size
override fun contains(element: T): Boolean {
return prioritized.contains(element) || other.contains(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
return (prioritized + other).containsAll(elements)
}
override fun isEmpty(): Boolean {
return prioritized.isEmpty() && other.isEmpty()
}
override fun iterator(): Iterator<T> {
return (prioritized + other).listIterator()
}
fun getOrNull(index: Int): T? {
if (index >= other.size || index < -prioritized.size) {
return null
}
return when (index) {
FIRST_PRIORITIZED -> prioritized[0]
SECOND_PRIORITIZED -> prioritized[1]
THIRD_PRIORITIZED -> prioritized[2]
else -> other.getOrNull(index)
}
}
operator fun get(index: Int): T {
val item = getOrNull(index)
if (item == null) {
throw IndexOutOfBoundsException(
"Specified index $index is not an valid entry in this PopupKeys!"
)
} else {
return item
}
}
}

View file

@ -0,0 +1,142 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.dslul.openboard.inputmethod.keyboard.Key
import org.dslul.openboard.inputmethod.keyboard.Key.KeyParams
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
// taken from FlorisBoard, small modifications (see also KeyData)
// internal keys removed (currently no plan to support them)
// added String.toTextKey
/**
* Data class which describes a single key and its attributes.
*
* @property type The type of the key. Some actions require both [code] and [type] to match in order
* to be successfully executed. Defaults to [KeyType.CHARACTER].
* @property code The UTF-8 encoded code of the character. The code defined here is used as the
* data passed to the system. Defaults to 0.
* @property label The string used to display the key in the UI. Is not used for the actual data
* passed to the system. Should normally be the exact same as the [code]. Defaults to an empty
* string.
*/
@Serializable
@SerialName("text_key")
class TextKeyData(
override val type: KeyType = KeyType.CHARACTER,
override val code: Int = KeyCode.UNSPECIFIED,
override val label: String = "",
override val groupId: Int = KeyData.GROUP_DEFAULT,
override val popup: PopupSet<AbstractKeyData> = PopupSet()
) : 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)
// }
// }
return this
}
override fun asString(isForDisplay: Boolean): String {
return buildString {
if (isForDisplay || code == KeyCode.URI_COMPONENT_TLD || code < KeyCode.SPACE) {
if (Unicode.isNonSpacingMark(code) && !label.startsWith("")) {
append("")
}
append(label)
} else {
try { appendCodePoint(code) } catch (_: Throwable) { }
}
}
}
override fun toString(): String {
return "${TextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }"
}
}
@Serializable
@SerialName("auto_text_key")
class AutoTextKeyData(
override val type: KeyType = KeyType.CHARACTER,
override val code: Int = KeyCode.UNSPECIFIED,
override val label: String = "",
override val groupId: Int = KeyData.GROUP_DEFAULT,
override val popup: PopupSet<AbstractKeyData> = PopupSet()
) : 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 {
return buildString {
if (isForDisplay || code == KeyCode.URI_COMPONENT_TLD || code < KeyCode.SPACE) {
if (Unicode.isNonSpacingMark(code) && !label.startsWith("")) {
append("")
}
append(label)
} else {
try { appendCodePoint(code) } catch (_: Throwable) { }
}
}
}
override fun toString(): String {
return "${AutoTextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }"
}
}
@Serializable
@SerialName("multi_text_key")
class MultiTextKeyData(
override val type: KeyType = KeyType.CHARACTER,
val codePoints: IntArray = intArrayOf(),
override val label: String = "",
override val groupId: Int = KeyData.GROUP_DEFAULT,
override val popup: PopupSet<AbstractKeyData> = PopupSet()
) : KeyData {
@Transient override val code: Int = KeyCode.MULTIPLE_CODE_POINTS
override fun compute(params: KeyboardParams): KeyData {
return this
}
override fun asString(isForDisplay: Boolean): String {
return buildString {
if (isForDisplay) {
append(label)
} else {
for (codePoint in codePoints) {
try { appendCodePoint(codePoint) } catch (_: Throwable) { }
}
}
}
}
override fun toString(): String {
return "${MultiTextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }"
}
}
fun String.toTextKey(moreKeys: Collection<String>? = null): TextKeyData =
TextKeyData(
label = this,
popup = moreKeys
?.let { keys -> PopupSet(null, keys.map { it.toTextKey() }) }
?: PopupSet()
)

View file

@ -0,0 +1,47 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris
import android.icu.lang.UCharacter
import android.icu.lang.UCharacterCategory
import android.os.Build
// taken from FlorisBoard, small modifications
// isNonSpacingMark has a fallback for api < 24
/**
* Character codes and comments source:
* https://www.w3.org/International/questions/qa-bidi-unicode-controls#basedirection
*/
@Suppress("unused")
object UnicodeCtrlChar {
/** Sets base direction to LTR and isolates the embedded content from the surrounding text */
const val LeftToRightIsolate = "\u2066"
/** Sets base direction to RTL and isolates the embedded content from the surrounding text */
const val RightToLeftIsolate = "\u2067"
/** Isolates the content and sets the direction according to the first strongly typed directional character */
const val FirstStrongIsolate = "\u2068"
/** Closes a previously opened isolated text block */
const val PopDirectionalIsolate = "\u2069"
val Matcher = """[$LeftToRightIsolate$RightToLeftIsolate$FirstStrongIsolate$PopDirectionalIsolate]""".toRegex()
}
fun String.stripUnicodeCtrlChars(): String {
return this.replace(UnicodeCtrlChar.Matcher, "")
}
object Unicode {
fun isNonSpacingMark(code: Int): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
UCharacter.getType(code).toByte() == UCharacterCategory.NON_SPACING_MARK
} else {
Character.getType(code).toByte() == Character.NON_SPACING_MARK
}
}
}