From ca3790b5198590b62e65f6bf46b34b34530eeea7 Mon Sep 17 00:00:00 2001 From: Helium314 Date: Fri, 24 Nov 2023 12:19:29 +0100 Subject: [PATCH] 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 --- app/src/main/assets/layouts/azerty.json | 38 ++ app/src/main/assets/layouts/colemak.json | 40 ++ app/src/main/assets/layouts/colemak_dh.json | 40 ++ app/src/main/assets/layouts/dvorak.json | 55 ++ .../openboard/inputmethod/keyboard/Key.java | 17 +- .../inputmethod/keyboard/KeyboardId.java | 5 + .../keyboard/internal/KeyboardBuilder.kt | 39 +- .../keyboard/internal/KeyboardParams.java | 4 + .../keyboard_parser/JsonKeyboardParser.kt | 75 +++ .../keyboard_parser/KeyboardParser.kt | 576 ++++++++++++++++ .../keyboard_parser/LocaleKeyTexts.kt | 44 +- .../keyboard_parser/SimpleKeyboardParser.kt | 623 +----------------- .../keyboard_parser/floris/KeyCode.kt | 128 ++++ .../keyboard_parser/floris/KeyData.kt | 367 +++++++++++ .../keyboard_parser/floris/KeyType.kt | 56 ++ .../keyboard_parser/floris/PopupSet.kt | 115 ++++ .../keyboard_parser/floris/TextKeyData.kt | 142 ++++ .../keyboard_parser/floris/Unicode.kt | 47 ++ 18 files changed, 1776 insertions(+), 635 deletions(-) create mode 100644 app/src/main/assets/layouts/azerty.json create mode 100644 app/src/main/assets/layouts/colemak.json create mode 100644 app/src/main/assets/layouts/colemak_dh.json create mode 100644 app/src/main/assets/layouts/dvorak.json create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/JsonKeyboardParser.kt create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/KeyboardParser.kt create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/KeyCode.kt create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/KeyData.kt create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/KeyType.kt create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/PopupSet.kt create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/TextKeyData.kt create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/Unicode.kt diff --git a/app/src/main/assets/layouts/azerty.json b/app/src/main/assets/layouts/azerty.json new file mode 100644 index 000000000..c5224ddf7 --- /dev/null +++ b/app/src/main/assets/layouts/azerty.json @@ -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": "?" } } + } + ] +] diff --git a/app/src/main/assets/layouts/colemak.json b/app/src/main/assets/layouts/colemak.json new file mode 100644 index 000000000..670685bc6 --- /dev/null +++ b/app/src/main/assets/layouts/colemak.json @@ -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": "?" } } } + ] +] diff --git a/app/src/main/assets/layouts/colemak_dh.json b/app/src/main/assets/layouts/colemak_dh.json new file mode 100644 index 000000000..d38841268 --- /dev/null +++ b/app/src/main/assets/layouts/colemak_dh.json @@ -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": "?" } } } + ] +] diff --git a/app/src/main/assets/layouts/dvorak.json b/app/src/main/assets/layouts/dvorak.json new file mode 100644 index 000000000..b4b318a71 --- /dev/null +++ b/app/src/main/assets/layouts/dvorak.json @@ -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" } + ] +] diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java index 8e113d41e..0ab4f35e5 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java @@ -956,7 +956,7 @@ public class Key implements Comparable { // 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 { 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 { 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 { 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 { 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("\\")) diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardId.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardId.java index d8ba9152d..f6a5973d0 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardId.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardId.java @@ -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); } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardBuilder.kt b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardBuilder.kt index 6cd8e1844..6bb193b6c 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardBuilder.kt +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardBuilder.kt @@ -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(protected val mContext: Context, mParams.mAllowRedundantMoreKeys = enabled } - fun loadSimpleKeyboard(id: KeyboardId): KeyboardBuilder { + fun loadFromAssets(id: KeyboardId): KeyboardBuilder? { 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(protected val mContext: Context, } fun loadFromXml(xmlId: Int, id: KeyboardId): KeyboardBuilder { - 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 { diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardParams.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardParams.java index 36504cd07..90e43b7d7 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardParams.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardParams.java @@ -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 mSecondaryLocales = Settings.getInstance().getCurrent().mSecondaryLocales; @NonNull private final UniqueKeysCache mUniqueKeysCache; diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/JsonKeyboardParser.kt b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/JsonKeyboardParser.kt new file mode 100644 index 000000000..8ec902f02 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/JsonKeyboardParser.kt @@ -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> { + val florisKeyData: List> = 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() } + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/KeyboardParser.kt b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/KeyboardParser.kt new file mode 100644 index 000000000..dfea01619 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/KeyboardParser.kt @@ -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> + + fun parseLayoutFromAssets(layoutName: String): ArrayList> = + parseLayoutString(getLayoutFromAssets(layoutName)) + + fun parseLayoutString(layoutContent: String): ArrayList> { + params.readAttributes(context, null) + val keysInRows = ArrayList>() + + val baseKeys: MutableList> = 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 = 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() to emptyList() + val functionalKeysLeft = functionalKeysDefs.first.map { getFunctionalKeyParams(it) } + val functionalKeysRight = functionalKeysDefs.second.map { getFunctionalKeyParams(it) } + val paramsRow = ArrayList(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>) { + 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, List>> = + 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>): ArrayList { + 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() + 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 { + val row = ArrayList() + ((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? = 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? = 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? { + 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 { + val moreKeys = mutableListOf() + 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() { // 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 { + val keys = mutableListOf() + 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 { + 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") diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/LocaleKeyTexts.kt b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/LocaleKeyTexts.kt index ebd2d0537..9c623ce23 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/LocaleKeyTexts.kt +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/LocaleKeyTexts.kt @@ -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>() - private val extraKeys = Array?>>?>(5) { null } + private val extraKeys = Array?>(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? = moreKeys[label]?.copyOf() - fun getExtraKeys(row: Int): List?>>? = + fun getExtraKeys(row: Int): List? = 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) { @@ -127,31 +127,39 @@ private fun mergeMoreKeys(original: Array, added: List): Array - 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" diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/SimpleKeyboardParser.kt b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/SimpleKeyboardParser.kt index 36774464b..3c8d28f9f 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/SimpleKeyboardParser.kt +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/SimpleKeyboardParser.kt @@ -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> { - 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> { - params.readAttributes(context, null) - val keysInRows = ArrayList>() - - val baseKeys: MutableList> = 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 = 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() to emptyList() - val functionalKeysLeft = functionalKeysDefs.first.map { getFunctionalKeyParams(it) } - val functionalKeysRight = functionalKeysDefs.second.map { getFunctionalKeyParams(it) } - val paramsRow = ArrayList(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>) { - 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, List>> = - 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>): ArrayList { - 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() - 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 { - val row = ArrayList() - 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? = 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> { + 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 = + 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? { - 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 { - val moreKeys = mutableListOf() - 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() { // 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 { - val keys = mutableListOf() - 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 { - 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? = 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" diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/KeyCode.kt b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/KeyCode.kt new file mode 100644 index 000000000..bc5c0f64d --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/KeyCode.kt @@ -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 +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/KeyData.kt b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/KeyData.kt new file mode 100644 index 000000000..4c0186a49 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/KeyData.kt @@ -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 // 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 "" + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/KeyType.kt b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/KeyType.kt new file mode 100644 index 000000000..4d51d9fae --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/KeyType.kt @@ -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 { + 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()) + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/PopupSet.kt b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/PopupSet.kt new file mode 100644 index 000000000..8c6964da2 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/PopupSet.kt @@ -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( + open val main: T? = null, + open val relevant: List = 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 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( + val hint: T?, + val prioritized: List, + val other: List +) : Collection { + 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): Boolean { + return (prioritized + other).containsAll(elements) + } + + override fun isEmpty(): Boolean { + return prioritized.isEmpty() && other.isEmpty() + } + + override fun iterator(): Iterator { + 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 + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/TextKeyData.kt b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/TextKeyData.kt new file mode 100644 index 000000000..8b80a3495 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/TextKeyData.kt @@ -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 = 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 = 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 = 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? = null): TextKeyData = + TextKeyData( + label = this, + popup = moreKeys + ?.let { keys -> PopupSet(null, keys.map { it.toTextKey() }) } + ?: PopupSet() + ) diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/Unicode.kt b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/Unicode.kt new file mode 100644 index 000000000..159515245 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/floris/Unicode.kt @@ -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 + } + } +}