mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-04-19 05:39:10 +00:00
add keyboard parser for more complicated layouts
parsing mostly taken from FlorisBoard, thus json files are compatible some small modifications added json files for missing latin layouts
This commit is contained in:
parent
dc0490a0e1
commit
ca3790b519
18 changed files with 1776 additions and 635 deletions
38
app/src/main/assets/layouts/azerty.json
Normal file
38
app/src/main/assets/layouts/azerty.json
Normal file
|
@ -0,0 +1,38 @@
|
|||
[
|
||||
[
|
||||
{ "label": "a", "popup": { "main": { "label": "%" } } },
|
||||
{ "label": "z", "popup": { "main": { "label": "\\" } } },
|
||||
{ "label": "e", "popup": { "main": { "label": "|" } } },
|
||||
{ "label": "r", "popup": { "main": { "label": "=" } } },
|
||||
{ "label": "t", "popup": { "main": { "label": "[" } } },
|
||||
{ "label": "y", "popup": { "main": { "label": "]" } } },
|
||||
{ "label": "u", "popup": { "main": { "label": "<" } } },
|
||||
{ "label": "i", "popup": { "main": { "label": ">" } } },
|
||||
{ "label": "o", "popup": { "main": { "label": "{" } } },
|
||||
{ "label": "p", "popup": { "main": { "label": "}" } } }
|
||||
],
|
||||
[
|
||||
{ "label": "q", "popup": { "main": { "label": "@" } } },
|
||||
{ "label": "s", "popup": { "main": { "label": "#" } } },
|
||||
{ "label": "d", "popup": { "main": { "label": "$$$" } } },
|
||||
{ "label": "f", "popup": { "main": { "label": "_" } } },
|
||||
{ "label": "g", "popup": { "main": { "label": "&" } } },
|
||||
{ "label": "h", "popup": { "main": { "label": "-" } } },
|
||||
{ "label": "j", "popup": { "main": { "label": "+" } } },
|
||||
{ "label": "k", "popup": { "main": { "label": "(" } } },
|
||||
{ "label": "l", "popup": { "main": { "label": ")" } } },
|
||||
{ "label": "m", "popup": { "main": { "label": "/" } } }
|
||||
],
|
||||
[
|
||||
{ "label": "w", "popup": { "main": { "label": "*" } } },
|
||||
{ "label": "x", "popup": { "main": { "label": "\"" } } },
|
||||
{ "label": "c", "popup": { "main": { "label": "'" } } },
|
||||
{ "label": "v", "popup": { "main": { "label": ":" } } },
|
||||
{ "label": "b", "popup": { "main": { "label": ";" } } },
|
||||
{ "label": "n", "popup": { "main": { "label": "!" } } },
|
||||
{ "$": "shift_state_selector",
|
||||
"shiftedManual": { "label": "?" },
|
||||
"default": { "label": "'" }, "popup": { "main": { "label": "?" } }
|
||||
}
|
||||
]
|
||||
]
|
40
app/src/main/assets/layouts/colemak.json
Normal file
40
app/src/main/assets/layouts/colemak.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
[
|
||||
[
|
||||
{ "label": "q", "popup": { "main": { "label": "%" } } },
|
||||
{ "label": "w", "popup": { "main": { "label": "\\" } } },
|
||||
{ "label": "f", "popup": { "main": { "label": "|" } } },
|
||||
{ "label": "p", "popup": { "main": { "label": "=" } } },
|
||||
{ "label": "g", "popup": { "main": { "label": "[" } } },
|
||||
{ "label": "j", "popup": { "main": { "label": "]" } } },
|
||||
{ "label": "l", "popup": { "main": { "label": "<" } } },
|
||||
{ "label": "u", "popup": { "main": { "label": ">" } } },
|
||||
{ "label": "y", "popup": { "main": { "label": "{" } } },
|
||||
{ "$": "shift_state_selector",
|
||||
"shiftedManual": { "label": ":" },
|
||||
"default": { "label": ";", "popup": { "relevant": [
|
||||
{ "label": ":" }, { "label": "}" }
|
||||
] } }
|
||||
}
|
||||
],
|
||||
[
|
||||
{ "label": "a", "popup": { "main": { "label": "@" } } },
|
||||
{ "label": "r", "popup": { "main": { "label": "#" } } },
|
||||
{ "label": "s", "popup": { "main": { "label": "$$$" } } },
|
||||
{ "label": "t", "popup": { "main": { "label": "_" } } },
|
||||
{ "label": "d", "popup": { "main": { "label": "&" } } },
|
||||
{ "label": "h", "popup": { "main": { "label": "-" } } },
|
||||
{ "label": "n", "popup": { "main": { "label": "+" } } },
|
||||
{ "label": "e", "popup": { "main": { "label": "(" } } },
|
||||
{ "label": "i", "popup": { "main": { "label": ")" } } },
|
||||
{ "label": "o", "popup": { "main": { "label": "…" } } }
|
||||
],
|
||||
[
|
||||
{ "label": "z", "popup": { "main": { "label": "*" } } },
|
||||
{ "label": "x", "popup": { "main": { "label": "\"" } } },
|
||||
{ "label": "c", "popup": { "main": { "label": "'" } } },
|
||||
{ "label": "v", "popup": { "main": { "label": ":" } } },
|
||||
{ "label": "b", "popup": { "main": { "label": ";" } } },
|
||||
{ "label": "k", "popup": { "main": { "label": "!" } } },
|
||||
{ "label": "m", "popup": { "main": { "label": "?" } } }
|
||||
]
|
||||
]
|
40
app/src/main/assets/layouts/colemak_dh.json
Normal file
40
app/src/main/assets/layouts/colemak_dh.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
[
|
||||
[
|
||||
{ "label": "q", "popup": { "main": { "label": "%" } } },
|
||||
{ "label": "w", "popup": { "main": { "label": "\\" } } },
|
||||
{ "label": "f", "popup": { "main": { "label": "|" } } },
|
||||
{ "label": "p", "popup": { "main": { "label": "=" } } },
|
||||
{ "label": "b", "popup": { "main": { "label": "[" } } },
|
||||
{ "label": "j", "popup": { "main": { "label": "]" } } },
|
||||
{ "label": "l", "popup": { "main": { "label": "<" } } },
|
||||
{ "label": "u", "popup": { "main": { "label": ">" } } },
|
||||
{ "label": "y", "popup": { "main": { "label": "{" } } },
|
||||
{ "$": "shift_state_selector",
|
||||
"shiftedManual": { "label": ":" },
|
||||
"default": { "label": ";", "popup": { "relevant": [
|
||||
{ "label": ":" }, { "label": "}" }
|
||||
] } }
|
||||
}
|
||||
],
|
||||
[
|
||||
{ "label": "a", "popup": { "main": { "label": "@" } } },
|
||||
{ "label": "r", "popup": { "main": { "label": "#" } } },
|
||||
{ "label": "s", "popup": { "main": { "label": "$$$" } } },
|
||||
{ "label": "t", "popup": { "main": { "label": "_" } } },
|
||||
{ "label": "g", "popup": { "main": { "label": "&" } } },
|
||||
{ "label": "m", "popup": { "main": { "label": "-" } } },
|
||||
{ "label": "n", "popup": { "main": { "label": "+" } } },
|
||||
{ "label": "e", "popup": { "main": { "label": "(" } } },
|
||||
{ "label": "i", "popup": { "main": { "label": ")" } } },
|
||||
{ "label": "o", "popup": { "main": { "label": "…" } } }
|
||||
],
|
||||
[
|
||||
{ "label": "z", "popup": { "main": { "label": "*" } } },
|
||||
{ "label": "x", "popup": { "main": { "label": "\"" } } },
|
||||
{ "label": "c", "popup": { "main": { "label": "'" } } },
|
||||
{ "label": "d", "popup": { "main": { "label": ":" } } },
|
||||
{ "label": "v", "popup": { "main": { "label": ";" } } },
|
||||
{ "label": "k", "popup": { "main": { "label": "!" } } },
|
||||
{ "label": "h", "popup": { "main": { "label": "?" } } }
|
||||
]
|
||||
]
|
55
app/src/main/assets/layouts/dvorak.json
Normal file
55
app/src/main/assets/layouts/dvorak.json
Normal file
|
@ -0,0 +1,55 @@
|
|||
[
|
||||
[
|
||||
{ "$": "shift_state_selector",
|
||||
"shifted": { "label": "\"" },
|
||||
"default": { "$": "variation_selector",
|
||||
"uri": { "label": "/" },
|
||||
"email": { "label": "@" },
|
||||
"default": { "label": "'", "popup": { "relevant": [
|
||||
{ "label": "!" },
|
||||
{ "label": "\"" }
|
||||
] } }
|
||||
}
|
||||
},
|
||||
{ "$": "shift_state_selector",
|
||||
"shifted": { "label": "<" },
|
||||
"default": { "label": "," }
|
||||
},
|
||||
{ "$": "shift_state_selector",
|
||||
"shifted": { "label": ">" },
|
||||
"default": { "label": "." }
|
||||
},
|
||||
{ "label": "p" },
|
||||
{ "label": "y" },
|
||||
{ "label": "f" },
|
||||
{ "label": "g" },
|
||||
{ "label": "c" },
|
||||
{ "label": "r" },
|
||||
{ "label": "l" }
|
||||
],
|
||||
[
|
||||
{ "label": "a" },
|
||||
{ "label": "o" },
|
||||
{ "label": "e" },
|
||||
{ "label": "u" },
|
||||
{ "label": "i" },
|
||||
{ "label": "d" },
|
||||
{ "label": "h" },
|
||||
{ "label": "t" },
|
||||
{ "label": "n" },
|
||||
{ "label": "s" }
|
||||
],
|
||||
[
|
||||
{ "label": "j" },
|
||||
{ "label": "k" },
|
||||
{ "label": "x" },
|
||||
{ "label": "b" },
|
||||
{ "label": "m" },
|
||||
{ "label": "w" },
|
||||
{ "label": "v" }
|
||||
],
|
||||
[
|
||||
{ "label": "q" },
|
||||
{ "label": "z" }
|
||||
]
|
||||
]
|
|
@ -956,7 +956,7 @@ public class Key implements Comparable<Key> {
|
|||
|
||||
// params that remains constant
|
||||
public final int mCode;
|
||||
@Nullable final String mLabel;
|
||||
@Nullable public final String mLabel;
|
||||
@Nullable final String mHintLabel;
|
||||
final int mLabelFlags;
|
||||
final int mIconId;
|
||||
|
@ -1156,6 +1156,17 @@ public class Key implements Comparable<Key> {
|
|||
mEnabled = true;
|
||||
}
|
||||
|
||||
public KeyParams(
|
||||
@NonNull final String keySpec,
|
||||
@NonNull final KeyboardParams params,
|
||||
final float relativeWidth,
|
||||
final int labelFlags,
|
||||
final int backgroundType,
|
||||
@Nullable final String[] layoutMoreKeys
|
||||
) {
|
||||
this(keySpec, KeySpecParser.getCode(keySpec), params, relativeWidth, labelFlags, backgroundType, layoutMoreKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* constructor that does not require attrs, style or absolute key dimension / position
|
||||
* setDimensionsFromRelativeSize needs to be called before creating the key
|
||||
|
@ -1163,6 +1174,7 @@ public class Key implements Comparable<Key> {
|
|||
public KeyParams(
|
||||
// todo (much later): replace keySpec? these encoded icons and codes are not really great
|
||||
@NonNull final String keySpec, // key text or some special string for KeySpecParser, e.g. "!icon/shift_key|!code/key_shift" (avoid using !text, should be removed)
|
||||
final int code,
|
||||
@NonNull final KeyboardParams params,
|
||||
final float relativeWidth,
|
||||
final int labelFlags,
|
||||
|
@ -1200,7 +1212,6 @@ public class Key implements Comparable<Key> {
|
|||
mMoreKeys = null;
|
||||
}
|
||||
|
||||
final int code = KeySpecParser.getCode(keySpec);
|
||||
if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) {
|
||||
mLabel = params.mId.mCustomActionLabel;
|
||||
} else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) {
|
||||
|
@ -1223,7 +1234,7 @@ public class Key implements Comparable<Key> {
|
|||
if (hintLabelAlwaysFromFirstLongPressKey) {
|
||||
hintLabel = mMoreKeys == null ? null : mMoreKeys[0].mLabel;
|
||||
} else {
|
||||
hintLabel = layoutMoreKeys == null ? null : layoutMoreKeys[0];
|
||||
hintLabel = layoutMoreKeys == null ? null : layoutMoreKeys[0]; // note that some entries may have been changed to other string or null
|
||||
if (hintLabel != null && hintLabel.length() > 1 && hintLabel.startsWith("!")) // this is not great, but other than removing com key label this is definitely ok
|
||||
hintLabel = null;
|
||||
if (hintLabel != null && hintLabel.length() == 2 && hintLabel.startsWith("\\"))
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -8,18 +8,19 @@ package org.dslul.openboard.inputmethod.keyboard.internal
|
|||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import org.dslul.openboard.inputmethod.annotations.UsedForTesting
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key.KeyParams
|
||||
import org.dslul.openboard.inputmethod.keyboard.Keyboard
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardId
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.MORE_KEYS_ALL
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.MORE_KEYS_MORE
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.SimpleKeyboardParser
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.KeyboardParser
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.XmlKeyboardParser
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams
|
||||
import org.dslul.openboard.inputmethod.latin.BuildConfig
|
||||
import org.dslul.openboard.inputmethod.latin.R
|
||||
import org.dslul.openboard.inputmethod.latin.common.Constants
|
||||
import org.dslul.openboard.inputmethod.latin.define.DebugFlags
|
||||
import org.dslul.openboard.inputmethod.latin.settings.Settings
|
||||
import org.dslul.openboard.inputmethod.latin.utils.sumOf
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
|
@ -47,24 +48,23 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
|
|||
mParams.mAllowRedundantMoreKeys = enabled
|
||||
}
|
||||
|
||||
fun loadSimpleKeyboard(id: KeyboardId): KeyboardBuilder<KP> {
|
||||
fun loadFromAssets(id: KeyboardId): KeyboardBuilder<KP>? {
|
||||
mParams.mId = id
|
||||
addLocaleKeyTextsToParams(mContext, mParams)
|
||||
when (Settings.getInstance().current.mShowMoreKeys) {
|
||||
MORE_KEYS_ALL -> mParams.mLocaleKeyTexts.addFile(mContext.assets.open("language_key_texts/all_more_keys.txt"))
|
||||
MORE_KEYS_MORE -> mParams.mLocaleKeyTexts.addFile(mContext.assets.open("language_key_texts/more_more_keys.txt"))
|
||||
addLocaleKeyTextsToParams(mContext, mParams, Settings.getInstance().current.mShowMoreKeys)
|
||||
try {
|
||||
val parser = KeyboardParser.createParserForLayout(mParams, mContext) ?: return null
|
||||
Log.d(TAG, "parsing $id using ${parser::class.simpleName}")
|
||||
keysInRows = parser.parseLayoutFromAssets(id.mSubtype.keyboardLayoutSetName)
|
||||
} catch (e: Throwable) {
|
||||
if (DebugFlags.DEBUG_ENABLED || BuildConfig.DEBUG)
|
||||
Toast.makeText(mContext, "error loading keyboard: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, "loading $id from assets failed", e)
|
||||
return null
|
||||
}
|
||||
keysInRows = SimpleKeyboardParser(mParams, mContext).parseFromAssets(id.mSubtype.keyboardLayoutSetName)
|
||||
determineAbsoluteValues()
|
||||
return this
|
||||
|
||||
// todo: further plan
|
||||
// add a parser for more complex layouts, and slowly extend it with whatever is needed
|
||||
// try to make the format compatible with florisboard, or just take it if it has all we need
|
||||
// if so, then make sure additional stuff unwanted in this app (if there is some) does not cause errors
|
||||
// probably need to deal with different functional key definition style, but only if allowing numpad and similar layouts
|
||||
// initially it's just alternative key for shifted layout
|
||||
// so dvorak and azerty and colemak and others can be migrated
|
||||
// migrate symbol layouts to this style
|
||||
// simplified if possible, but json should be fine too
|
||||
// migrate keypad layouts to this style
|
||||
|
@ -144,15 +144,14 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
|
|||
}
|
||||
|
||||
fun loadFromXml(xmlId: Int, id: KeyboardId): KeyboardBuilder<KP> {
|
||||
mParams.mId = id
|
||||
if (Settings.getInstance().current.mUseNewKeyboardParsing
|
||||
&& id.isAlphabetKeyboard
|
||||
&& this::class == KeyboardBuilder::class // otherwise this will apply to moreKeys and moreSuggestions
|
||||
&& SimpleKeyboardParser.hasLayoutFile(mParams.mId.mSubtype.keyboardLayoutSetName)
|
||||
&& this::class == KeyboardBuilder::class // otherwise this will apply to moreKeys and moreSuggestions, and then some parameters are off
|
||||
) {
|
||||
loadSimpleKeyboard(id)
|
||||
return this
|
||||
if (loadFromAssets(id) != null)
|
||||
return this
|
||||
}
|
||||
mParams.mId = id
|
||||
// loading a keyboard should set default params like mParams.readAttributes(mContext, attrs);
|
||||
// attrs may be null, then default values are used (looks good for "normal" keyboards)
|
||||
try {
|
||||
|
|
|
@ -24,6 +24,8 @@ import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
|
@ -81,6 +83,8 @@ public class KeyboardParams {
|
|||
public final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
|
||||
@NonNull
|
||||
public final KeyStylesSet mKeyStyles = new KeyStylesSet(mTextsSet);
|
||||
@NonNull // todo: not good, this only works because params are currently always created for the active subtype
|
||||
public final List<Locale> mSecondaryLocales = Settings.getInstance().getCurrent().mSecondaryLocales;
|
||||
|
||||
@NonNull
|
||||
private final UniqueKeysCache mUniqueKeysCache;
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.AbstractKeyData
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.AutoTextKeyData
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.CaseSelector
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.CharWidthSelector
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.KanaSelector
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.KeyData
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.LayoutDirectionSelector
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.MultiTextKeyData
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.ShiftStateSelector
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.TextKeyData
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.VariationSelector
|
||||
|
||||
/**
|
||||
* Parser for json layout files as used in FlorisBoard, see floris directory for classes taken from FlorisBoard.
|
||||
* Some differences to the FlorisBoard keys:
|
||||
* (currently) only normal keys supported
|
||||
* if label or code are missing one is created from the other
|
||||
* auto_text_key ignored (i.e. interpreted like the default TextKey)
|
||||
* codes of multi_text_key not used, only the label
|
||||
* (currently) popups is always read to [number, main, relevant] layoutMoreKeys, no choice of which to use or which hint is provided
|
||||
*/
|
||||
class JsonKeyboardParser(private val params: KeyboardParams, private val context: Context) : KeyboardParser(params, context) {
|
||||
|
||||
override fun getLayoutFromAssets(layoutName: String) =
|
||||
context.assets.open("layouts/$layoutName.json").reader().readText()
|
||||
|
||||
override fun parseCoreLayout(layoutContent: String): MutableList<List<KeyData>> {
|
||||
val florisKeyData: List<List<AbstractKeyData>> = florisJsonConfig.decodeFromString(layoutContent)
|
||||
// initially 200 ms parse (debug build on S4 mini)
|
||||
// after a few parses it's optimized and 20-30 ms
|
||||
// whole load is 50-70 ms vs 30-55 with simple parser -> it's ok
|
||||
return florisKeyData.mapTo(mutableListOf()) { it.mapNotNull { it.compute(params) } }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
* modified
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
private val florisJsonConfig = Json {
|
||||
classDiscriminator = "$"
|
||||
encodeDefaults = true
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
serializersModule = SerializersModule {
|
||||
polymorphic(AbstractKeyData::class) {
|
||||
subclass(TextKeyData::class, TextKeyData.serializer())
|
||||
subclass(AutoTextKeyData::class, AutoTextKeyData.serializer())
|
||||
subclass(MultiTextKeyData::class, MultiTextKeyData.serializer())
|
||||
subclass(CaseSelector::class, CaseSelector.serializer())
|
||||
subclass(ShiftStateSelector::class, ShiftStateSelector.serializer())
|
||||
subclass(VariationSelector::class, VariationSelector.serializer())
|
||||
subclass(LayoutDirectionSelector::class, LayoutDirectionSelector.serializer())
|
||||
subclass(CharWidthSelector::class, CharWidthSelector.serializer())
|
||||
subclass(KanaSelector::class, KanaSelector.serializer())
|
||||
defaultDeserializer { TextKeyData.serializer() }
|
||||
}
|
||||
polymorphic(KeyData::class) {
|
||||
subclass(TextKeyData::class, TextKeyData.serializer())
|
||||
subclass(AutoTextKeyData::class, AutoTextKeyData.serializer())
|
||||
subclass(MultiTextKeyData::class, MultiTextKeyData.serializer())
|
||||
defaultDeserializer { TextKeyData.serializer() }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,576 @@
|
|||
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key.KeyParams
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardId
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardIconsSet
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.KeyData
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.toTextKey
|
||||
import org.dslul.openboard.inputmethod.latin.R
|
||||
import org.dslul.openboard.inputmethod.latin.common.splitOnWhitespace
|
||||
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils
|
||||
import org.dslul.openboard.inputmethod.latin.utils.RunInLocale
|
||||
import org.dslul.openboard.inputmethod.latin.utils.sumOf
|
||||
|
||||
/**
|
||||
* Abstract parser class that handles creation of keyboard from [KeyData] arranged in rows,
|
||||
* provided by the extending class.
|
||||
*
|
||||
* Functional keys are pre-defined and can't be changed, with exception of comma, period and similar
|
||||
* keys in symbol layouts.
|
||||
* By default, all normal keys have the same width and flags, which may cause issues with the
|
||||
* requirements of certain non-latin languages. todo: add labelFlags to Json parser, or determine automatically?
|
||||
*
|
||||
* Currently the number, phone and numpad layouts are not compatible with this parser.
|
||||
*/
|
||||
abstract class KeyboardParser(private val params: KeyboardParams, private val context: Context) {
|
||||
|
||||
protected abstract fun getLayoutFromAssets(layoutName: String): String
|
||||
|
||||
protected abstract fun parseCoreLayout(layoutContent: String): MutableList<List<KeyData>>
|
||||
|
||||
fun parseLayoutFromAssets(layoutName: String): ArrayList<ArrayList<KeyParams>> =
|
||||
parseLayoutString(getLayoutFromAssets(layoutName))
|
||||
|
||||
fun parseLayoutString(layoutContent: String): ArrayList<ArrayList<KeyParams>> {
|
||||
params.readAttributes(context, null)
|
||||
val keysInRows = ArrayList<ArrayList<KeyParams>>()
|
||||
|
||||
val baseKeys: MutableList<List<KeyData>> = parseCoreLayout(layoutContent)
|
||||
if (!params.mId.mNumberRowEnabled) {
|
||||
// todo (non-latin): not all layouts have numbers on first row, so maybe have some layout flag to switch it off (or an option)
|
||||
((1..9) + 0).forEachIndexed { i, n -> baseKeys.first().getOrNull(i)?.popup?.number = n }
|
||||
}
|
||||
val functionalKeysReversed = parseFunctionalKeys().reversed()
|
||||
|
||||
// keyboard parsed bottom-up because the number of rows is not fixed, but the functional keys
|
||||
// are always added to the rows near the bottom
|
||||
keysInRows.add(getBottomRowAndAdjustBaseKeys(baseKeys))
|
||||
|
||||
baseKeys.reversed().forEachIndexed { i, it ->
|
||||
val row: List<KeyData> = if (i == 0) {
|
||||
// add bottom row extra keys
|
||||
it + context.getString(R.string.key_def_extra_bottom_right)
|
||||
.split(",").mapNotNull { if (it.isBlank()) null else it.trim().toTextKey() }
|
||||
} else {
|
||||
it
|
||||
}
|
||||
// parse functional keys for this row (if any)
|
||||
val functionalKeysDefs = if (i < functionalKeysReversed.size) functionalKeysReversed[i]
|
||||
else emptyList<String>() to emptyList()
|
||||
val functionalKeysLeft = functionalKeysDefs.first.map { getFunctionalKeyParams(it) }
|
||||
val functionalKeysRight = functionalKeysDefs.second.map { getFunctionalKeyParams(it) }
|
||||
val paramsRow = ArrayList<KeyParams>(functionalKeysLeft)
|
||||
|
||||
// determine key width, maybe scale factor for keys, and spacers to add
|
||||
val usedKeyWidth = params.mDefaultRelativeKeyWidth * row.size
|
||||
val functionalKeyWidth = (functionalKeysLeft.sumOf { it.mRelativeWidth }) + (functionalKeysRight.sumOf { it.mRelativeWidth })
|
||||
val availableWidth = 1f - functionalKeyWidth
|
||||
var keyWidth: Float
|
||||
val spacerWidth: Float
|
||||
if (availableWidth - usedKeyWidth > 0.0001f) { // don't add spacers if only a tiny bit is empty
|
||||
// width available, add spacer
|
||||
keyWidth = params.mDefaultRelativeKeyWidth
|
||||
spacerWidth = (availableWidth - usedKeyWidth) / 2
|
||||
} else {
|
||||
// need more width, re-scale
|
||||
spacerWidth = 0f
|
||||
keyWidth = availableWidth / row.size
|
||||
}
|
||||
if (spacerWidth != 0f) {
|
||||
paramsRow.add(KeyParams.newSpacer(params, spacerWidth))
|
||||
}
|
||||
if (keyWidth < params.mDefaultRelativeKeyWidth * 0.82 && spacerWidth == 0f) {
|
||||
// keys are very narrow, also rescale the functional keys to make keys a little wider
|
||||
// 0.82 is just some guess for "too narrow"
|
||||
// todo (idea): works reasonably well, but actually functional keys could give some more of their width
|
||||
val allKeyScale = 1f / (functionalKeyWidth + row.size * params.mDefaultRelativeKeyWidth)
|
||||
keyWidth = params.mDefaultRelativeKeyWidth * allKeyScale
|
||||
functionalKeysLeft.forEach { it.mRelativeWidth *= allKeyScale }
|
||||
functionalKeysRight.forEach { it.mRelativeWidth *= allKeyScale }
|
||||
}
|
||||
|
||||
for (key in row) {
|
||||
// todo: maybe autoScale / autoXScale if label has more than 2 characters (exception for emojis?)
|
||||
// but that could also be determined in toKeyParams
|
||||
val keyParams = key.compute(params)?.toKeyParams(params, keyWidth) ?: continue
|
||||
paramsRow.add(keyParams)
|
||||
}
|
||||
if (spacerWidth != 0f) {
|
||||
paramsRow.add(KeyParams.newSpacer(params, spacerWidth))
|
||||
}
|
||||
functionalKeysRight.forEach { paramsRow.add(it) }
|
||||
keysInRows.add(0, paramsRow) // we're doing it backwards, so add on top
|
||||
}
|
||||
resizeLastNormalRowIfNecessaryForAlignment(keysInRows)
|
||||
// rescale height if we have more than 4 rows
|
||||
val heightRescale = if (keysInRows.size > 4) 4f / keysInRows.size else 1f
|
||||
if (params.mId.mNumberRowEnabled)
|
||||
keysInRows.add(0, getNumberRow())
|
||||
if (heightRescale != 1f) {
|
||||
// rescale all keys, so number row doesn't look weird (this is done like in current parsing)
|
||||
// todo: in symbols view, number row is not rescaled
|
||||
// so the symbols keyboard is higher than the normal one
|
||||
// not a new issue, but should be solved in this migration
|
||||
// how? possibly scale all keyboards to height of main alphabet? (consider suggestion strip)
|
||||
keysInRows.forEach { it.forEach { it.mRelativeHeight *= heightRescale } }
|
||||
}
|
||||
|
||||
return keysInRows
|
||||
}
|
||||
|
||||
// resize keys in last row if they are wider than keys in the row above
|
||||
// this is done so the keys align with the keys above, like in original layouts
|
||||
// done e.g. for nordic and swiss layouts
|
||||
private fun resizeLastNormalRowIfNecessaryForAlignment(keysInRows: ArrayList<ArrayList<KeyParams>>) {
|
||||
if (keysInRows.size < 3)
|
||||
return
|
||||
val lastNormalRow = keysInRows[keysInRows.lastIndex - 1]
|
||||
val rowAboveLastNormalRow = keysInRows[keysInRows.lastIndex - 2]
|
||||
if (lastNormalRow.any { it.isSpacer } || rowAboveLastNormalRow.any { it.isSpacer })
|
||||
return // annoying to deal with, and probably no resize needed anyway
|
||||
val lastNormalRowKeyWidth = lastNormalRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }.mRelativeWidth
|
||||
val rowAboveLastNormalRowKeyWidth = rowAboveLastNormalRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }.mRelativeWidth
|
||||
if (lastNormalRowKeyWidth <= rowAboveLastNormalRowKeyWidth + 0.0001f)
|
||||
return // no need
|
||||
if (lastNormalRow.any { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL && it.mRelativeWidth != lastNormalRowKeyWidth })
|
||||
return // normal keys have different width, don't deal with this
|
||||
val numberOfNormalKeys = lastNormalRow.count { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }
|
||||
val widthBefore = numberOfNormalKeys * lastNormalRowKeyWidth
|
||||
val widthAfter = numberOfNormalKeys * rowAboveLastNormalRowKeyWidth
|
||||
val spacerWidth = (widthBefore - widthAfter) / 2
|
||||
// resize keys and add spacers
|
||||
lastNormalRow.forEach { if (it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL) it.mRelativeWidth = rowAboveLastNormalRowKeyWidth }
|
||||
lastNormalRow.add(lastNormalRow.indexOfFirst { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }, KeyParams.newSpacer(params, spacerWidth))
|
||||
lastNormalRow.add(lastNormalRow.indexOfLast { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL } + 1, KeyParams.newSpacer(params, spacerWidth))
|
||||
}
|
||||
|
||||
private fun parseFunctionalKeys(): List<Pair<List<String>, List<String>>> =
|
||||
context.getString(R.string.key_def_functional).split("\n").mapNotNull { line ->
|
||||
if (line.isBlank()) return@mapNotNull null
|
||||
val p = line.split(";")
|
||||
p.first().let { if (it.isBlank()) emptyList() else it.split(",") } to
|
||||
p.last().let { if (it.isBlank()) emptyList() else it.split(",") }
|
||||
}
|
||||
|
||||
private fun getBottomRowAndAdjustBaseKeys(baseKeys: MutableList<List<KeyData>>): ArrayList<KeyParams> {
|
||||
val adjustableKeyCount = when (params.mId.mElementId) {
|
||||
KeyboardId.ELEMENT_SYMBOLS -> 3
|
||||
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> 4
|
||||
else -> 2 // must be alphabet, parser doesn't work for other elementIds
|
||||
}
|
||||
val adjustedKeys = if (baseKeys.last().size == adjustableKeyCount) baseKeys.last()
|
||||
else null
|
||||
if (adjustedKeys != null)
|
||||
baseKeys.removeLast()
|
||||
val bottomRow = ArrayList<KeyParams>()
|
||||
context.getString(R.string.key_def_bottom_row).split(",").forEach {
|
||||
val key = it.trim().splitOnWhitespace().first()
|
||||
val adjustKey = when (key) {
|
||||
"comma" -> adjustedKeys?.first()
|
||||
"period" -> adjustedKeys?.last()
|
||||
else -> null
|
||||
}
|
||||
val keyParams = getFunctionalKeyParams(it, adjustKey?.label, adjustKey?.popup?.toMoreKeys(params))
|
||||
if (key == "space") { // add the extra keys around space
|
||||
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) {
|
||||
bottomRow.add(getFunctionalKeyParams(FunctionalKey.NUMPAD))
|
||||
bottomRow.add(keyParams)
|
||||
bottomRow.add(KeyParams(
|
||||
adjustedKeys?.get(1)?.label ?: "/",
|
||||
params,
|
||||
params.mDefaultRelativeKeyWidth,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
adjustedKeys?.get(1)?.popup?.toMoreKeys(params)
|
||||
))
|
||||
} else if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
|
||||
bottomRow.add(KeyParams(
|
||||
adjustedKeys?.get(1)?.label ?: "<",
|
||||
params,
|
||||
params.mDefaultRelativeKeyWidth,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
adjustedKeys?.get(1)?.popup?.toMoreKeys(params)
|
||||
))
|
||||
bottomRow.add(keyParams)
|
||||
bottomRow.add(KeyParams(
|
||||
adjustedKeys?.get(2)?.label ?: ">",
|
||||
params,
|
||||
params.mDefaultRelativeKeyWidth,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
adjustedKeys?.get(2)?.popup?.toMoreKeys(params)
|
||||
))
|
||||
} else { // alphabet
|
||||
if (params.mId.mLanguageSwitchKeyEnabled)
|
||||
bottomRow.add(getFunctionalKeyParams(FunctionalKey.LANGUAGE_SWITCH))
|
||||
if (params.mId.mEmojiKeyEnabled)
|
||||
bottomRow.add(getFunctionalKeyParams(FunctionalKey.EMOJI))
|
||||
bottomRow.add(keyParams)
|
||||
if (params.mId.locale.language in languagesThatNeedZwnjKey)
|
||||
bottomRow.add(getFunctionalKeyParams(FunctionalKey.ZWNJ)) // todo (non-latin): test it
|
||||
}
|
||||
} else {
|
||||
bottomRow.add(keyParams)
|
||||
}
|
||||
}
|
||||
// set space width
|
||||
val space = bottomRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_SPACEBAR }
|
||||
space.mRelativeWidth = 1f - bottomRow.filter { it != space }.sumOf { it.mRelativeWidth }
|
||||
return bottomRow
|
||||
}
|
||||
|
||||
private fun getNumberRow(): ArrayList<KeyParams> {
|
||||
val row = ArrayList<KeyParams>()
|
||||
((1..9) + 0).forEachIndexed { i, n ->
|
||||
row.add(KeyParams(
|
||||
n.toString(), // todo (non-latin): use language more keys to adjust, possibly in combination with some setting
|
||||
params,
|
||||
params.mDefaultRelativeKeyWidth,
|
||||
Key.LABEL_FLAGS_DISABLE_HINT_LABEL, // todo (later): maybe optional or enable (but then all numbers should have moreKeys)
|
||||
Key.BACKGROUND_TYPE_NORMAL,
|
||||
numbersMoreKeys[i] // todo (non-latin): alternative numbers should be in language more keys, which to put where needs to be decided
|
||||
))
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
private fun getFunctionalKeyParams(def: String, label: String? = null, moreKeys: Array<String>? = null): KeyParams {
|
||||
val split = def.trim().splitOnWhitespace()
|
||||
val key = FunctionalKey.valueOf(split[0].uppercase())
|
||||
val width = if (split.size == 2) split[1].substringBefore("%").toFloat() / 100f
|
||||
else params.mDefaultRelativeKeyWidth
|
||||
return getFunctionalKeyParams(key, width, label, moreKeys)
|
||||
}
|
||||
|
||||
private fun getFunctionalKeyParams(key: FunctionalKey, relativeWidth: Float? = null, label: String? = null, moreKeys: Array<String>? = null): KeyParams {
|
||||
// for comma and period: label will override default, moreKeys will be appended
|
||||
val width = relativeWidth ?: params.mDefaultRelativeKeyWidth
|
||||
return when (key) {
|
||||
FunctionalKey.SYMBOL -> KeyParams(
|
||||
"${getSymbolLabel()}|!code/key_switch_alpha_symbol", // todo (later): in numpad the code is key_symbolNumpad
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
FunctionalKey.COMMA -> KeyParams(
|
||||
label ?: getDefaultCommaLabel(),
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_HAS_POPUP_HINT, // previously only if normal comma, but always is more correct
|
||||
if (label?.first()
|
||||
?.isLetter() == true
|
||||
) Key.BACKGROUND_TYPE_NORMAL else Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
moreKeys?.let { getCommaMoreKeys() + it } ?: getCommaMoreKeys()
|
||||
)
|
||||
FunctionalKey.SPACE -> KeyParams(
|
||||
"!icon/space_key|!code/key_space", // !icon/space_key_for_number_layout in number layout, but not on tablet
|
||||
params,
|
||||
width, // will not be used for normal space (only in number layouts)
|
||||
0, // todo (later): alignIconToBottom for non-tablet number layout
|
||||
Key.BACKGROUND_TYPE_SPACEBAR,
|
||||
null
|
||||
)
|
||||
FunctionalKey.PERIOD -> KeyParams(
|
||||
label ?: ".",
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT, // todo (later): check what LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT does, maybe remove the flag here
|
||||
if (label?.first()
|
||||
?.isLetter() == true
|
||||
) Key.BACKGROUND_TYPE_NORMAL else Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
moreKeys?.let { getPunctuationMoreKeys() + it } ?: getPunctuationMoreKeys()
|
||||
)
|
||||
FunctionalKey.ACTION -> KeyParams(
|
||||
"${getActionKeyLabel()}|${getActionKeyCode()}",
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE
|
||||
or Key.LABEL_FLAGS_AUTO_X_SCALE
|
||||
or Key.LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
|
||||
or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR
|
||||
or KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId),
|
||||
Key.BACKGROUND_TYPE_ACTION,
|
||||
getActionKeyMoreKeys()
|
||||
)
|
||||
FunctionalKey.DELETE -> KeyParams(
|
||||
"!icon/delete_key|!code/key_delete",
|
||||
params,
|
||||
width,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
FunctionalKey.SHIFT -> KeyParams(
|
||||
"${getShiftLabel()}|!code/key_shift",
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE,
|
||||
// todo (later): possibly the whole stickOn/Off stuff can be removed, currently it should only have a very slight effect in holo
|
||||
if (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED)
|
||||
Key.BACKGROUND_TYPE_STICKY_ON
|
||||
else Key.BACKGROUND_TYPE_STICKY_OFF,
|
||||
arrayOf("!noPanelAutoMoreKey!", " |!code/key_capslock")
|
||||
)
|
||||
FunctionalKey.EMOJI -> KeyParams(
|
||||
"!icon/emoji_normal_key|!code/key_emoji",
|
||||
params,
|
||||
width,
|
||||
KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId),
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
// tablet layout has an emoji key that changes to com key in url / mail
|
||||
FunctionalKey.EMOJI_COM -> if (params.mId.mMode == KeyboardId.MODE_URL || params.mId.mMode == KeyboardId.MODE_EMAIL)
|
||||
getFunctionalKeyParams(FunctionalKey.COM, width)
|
||||
else getFunctionalKeyParams(FunctionalKey.EMOJI, width)
|
||||
FunctionalKey.COM -> KeyParams( // todo: label and moreKeys could be in localeKeyTexts, handled like currency key
|
||||
".com",
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_AUTO_X_SCALE or Key.LABEL_FLAGS_FONT_NORMAL or Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_PRESERVE_CASE,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
arrayOf("!hasLabels!", ".net", ".org", ".gov", ".edu")
|
||||
)
|
||||
FunctionalKey.LANGUAGE_SWITCH -> KeyParams(
|
||||
"!icon/language_switch_key|!code/key_language_switch",
|
||||
params,
|
||||
width,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
FunctionalKey.ALPHA -> KeyParams(
|
||||
"${getAlphabetLabel()}|!code/key_switch_alpha_symbol", // todo (later): in numpad the code is key_alphaNumpad
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
FunctionalKey.NUMPAD -> KeyParams(
|
||||
"!icon/numpad_key|!code/key_numpad",
|
||||
params,
|
||||
width,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
FunctionalKey.ZWNJ -> KeyParams(
|
||||
"!icon/zwnj_key|\u200C",
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_HAS_POPUP_HINT,
|
||||
Key.BACKGROUND_TYPE_SPACEBAR,
|
||||
arrayOf("!icon/zwj_key|\u200D")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getActionKeyLabel(): String {
|
||||
if (params.mId.isMultiLine && (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED))
|
||||
return "!icon/enter_key"
|
||||
val iconName = when (params.mId.imeAction()) {
|
||||
EditorInfo.IME_ACTION_GO -> KeyboardIconsSet.NAME_GO_KEY
|
||||
EditorInfo.IME_ACTION_SEARCH -> KeyboardIconsSet.NAME_SEARCH_KEY
|
||||
EditorInfo.IME_ACTION_SEND -> KeyboardIconsSet.NAME_SEND_KEY
|
||||
EditorInfo.IME_ACTION_NEXT -> KeyboardIconsSet.NAME_NEXT_KEY
|
||||
EditorInfo.IME_ACTION_DONE -> KeyboardIconsSet.NAME_DONE_KEY
|
||||
EditorInfo.IME_ACTION_PREVIOUS -> KeyboardIconsSet.NAME_PREVIOUS_KEY
|
||||
InputTypeUtils.IME_ACTION_CUSTOM_LABEL -> return params.mId.mCustomActionLabel
|
||||
else -> return "!icon/enter_key"
|
||||
}
|
||||
val replacement = iconName.replaceIconWithLabelIfNoDrawable()
|
||||
return if (iconName == replacement) // i.e. icon exists
|
||||
"!icon/$iconName"
|
||||
else
|
||||
replacement
|
||||
}
|
||||
|
||||
private fun getActionKeyCode() =
|
||||
if (params.mId.isMultiLine && (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED))
|
||||
"!code/key_shift_enter"
|
||||
else "!code/key_enter"
|
||||
|
||||
private fun getActionKeyMoreKeys(): Array<String>? {
|
||||
val action = params.mId.imeAction()
|
||||
val navigatePrev = params.mId.navigatePrevious()
|
||||
val navigateNext = params.mId.navigateNext()
|
||||
return when {
|
||||
params.mId.passwordInput() -> when {
|
||||
navigatePrev && action == EditorInfo.IME_ACTION_NEXT -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
|
||||
action == EditorInfo.IME_ACTION_NEXT -> null
|
||||
navigateNext && action == EditorInfo.IME_ACTION_PREVIOUS -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
|
||||
action == EditorInfo.IME_ACTION_PREVIOUS -> null
|
||||
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS_NEXT)
|
||||
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
|
||||
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
|
||||
else -> null
|
||||
}
|
||||
// could change definition of numbers to query a range, or have a pre-defined list, but not that crucial
|
||||
params.mId.mMode in listOf(KeyboardId.MODE_URL, KeyboardId.MODE_EMAIL, KeyboardId.ELEMENT_PHONE, KeyboardId.ELEMENT_NUMBER, KeyboardId.MODE_DATE, KeyboardId.MODE_TIME, KeyboardId.MODE_DATETIME) -> when {
|
||||
action == EditorInfo.IME_ACTION_NEXT && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
|
||||
action == EditorInfo.IME_ACTION_NEXT -> null
|
||||
action == EditorInfo.IME_ACTION_PREVIOUS && navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
|
||||
action == EditorInfo.IME_ACTION_PREVIOUS -> null
|
||||
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS_NEXT)
|
||||
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
|
||||
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
|
||||
else -> null
|
||||
}
|
||||
action == EditorInfo.IME_ACTION_NEXT && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS)
|
||||
action == EditorInfo.IME_ACTION_NEXT -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
|
||||
action == EditorInfo.IME_ACTION_PREVIOUS && navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_NEXT)
|
||||
action == EditorInfo.IME_ACTION_PREVIOUS -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
|
||||
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS_NEXT)
|
||||
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_NEXT)
|
||||
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS)
|
||||
else -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMoreKeysArray(moreKeysDef: String): Array<String> {
|
||||
val moreKeys = mutableListOf<String>()
|
||||
for (moreKey in moreKeysDef.split(",")) {
|
||||
val iconPrefixRemoved = moreKey.substringAfter("!icon/")
|
||||
if (iconPrefixRemoved == moreKey) { // i.e. there is no !icon/
|
||||
moreKeys.add(moreKey)
|
||||
continue
|
||||
}
|
||||
val iconName = iconPrefixRemoved.substringBefore("|")
|
||||
val replacementText = iconName.replaceIconWithLabelIfNoDrawable()
|
||||
if (replacementText == iconName) { // i.e. we have the drawable
|
||||
moreKeys.add(moreKey)
|
||||
} else {
|
||||
moreKeys.add("!hasLabels!")
|
||||
moreKeys.add(replacementText)
|
||||
}
|
||||
}
|
||||
return moreKeys.toTypedArray()
|
||||
}
|
||||
|
||||
private fun String.replaceIconWithLabelIfNoDrawable(): String {
|
||||
if (params.mIconsSet.getIconDrawable(KeyboardIconsSet.getIconId(this)) != null) return this
|
||||
val id = context.resources.getIdentifier("label_$this", "string", context.packageName)
|
||||
val ril = object : RunInLocale<String>() { // todo (later): simpler way of doing this in a single line?
|
||||
override fun job(res: Resources) = res.getString(id)
|
||||
}
|
||||
return ril.runInLocale(context.resources, params.mId.locale)
|
||||
}
|
||||
|
||||
private fun getAlphabetLabel() = params.mLocaleKeyTexts.labelAlphabet
|
||||
|
||||
private fun getSymbolLabel() = params.mLocaleKeyTexts.labelSymbols
|
||||
|
||||
private fun getShiftLabel(): String {
|
||||
val elementId = params.mId.mElementId
|
||||
if (elementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
|
||||
return params.mLocaleKeyTexts.labelShiftSymbols
|
||||
if (elementId == KeyboardId.ELEMENT_SYMBOLS)
|
||||
return getSymbolLabel()
|
||||
if (elementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || elementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED
|
||||
|| elementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED || elementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED)
|
||||
return "!icon/shift_key_shifted"
|
||||
return "!icon/shift_key"
|
||||
}
|
||||
|
||||
private fun getDefaultCommaLabel(): String {
|
||||
if (params.mId.mMode == KeyboardId.MODE_URL)
|
||||
return "/"
|
||||
if (params.mId.mMode == KeyboardId.MODE_EMAIL)
|
||||
return "\\@"
|
||||
return ","
|
||||
}
|
||||
|
||||
private fun getCommaMoreKeys(): Array<String> {
|
||||
val keys = mutableListOf<String>()
|
||||
if (!params.mId.mDeviceLocked)
|
||||
keys.add("!icon/clipboard_normal_key|!code/key_clipboard")
|
||||
if (!params.mId.mEmojiKeyEnabled)
|
||||
keys.add("!icon/emoji_normal_key|!code/key_emoji")
|
||||
if (!params.mId.mLanguageSwitchKeyEnabled)
|
||||
keys.add("!icon/language_switch_key|!code/key_language_switch")
|
||||
if (!params.mId.mOneHandedModeEnabled)
|
||||
keys.add("!icon/start_onehanded_mode_key|!code/key_start_onehanded")
|
||||
if (!params.mId.mDeviceLocked)
|
||||
keys.add("!icon/settings_key|!code/key_settings")
|
||||
return keys.toTypedArray()
|
||||
}
|
||||
|
||||
private fun getPunctuationMoreKeys(): Array<String> {
|
||||
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS || params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
|
||||
return arrayOf("…")
|
||||
val moreKeys = params.mLocaleKeyTexts.getMoreKeys("punctuation") ?:
|
||||
// todo: some (non-latin) languages have different parenthesis keys (maybe rtl has inverted?)
|
||||
arrayOf("${Key.MORE_KEYS_AUTO_COLUMN_ORDER}8", "\\,", "?", "!", "#", ")", "(", "/", ";", "'", "@", ":", "-", "\"", "+", "\\%", "&")
|
||||
if (context.resources.getInteger(R.integer.config_screen_metrics) >= 3 && moreKeys.contains("!") && moreKeys.contains("?")) {
|
||||
// we have a tablet, remove ! and ? keys and reduce number in autoColumnOrder
|
||||
// this makes use of removal of empty moreKeys in MoreKeySpec.insertAdditionalMoreKeys
|
||||
moreKeys[moreKeys.indexOf("!")] = ""
|
||||
moreKeys[moreKeys.indexOf("?")] = ""
|
||||
val columns = moreKeys[0].substringAfter(Key.MORE_KEYS_AUTO_COLUMN_ORDER).toIntOrNull()
|
||||
if (columns != null)
|
||||
moreKeys[0] = "${Key.MORE_KEYS_AUTO_COLUMN_ORDER}${columns - 1}"
|
||||
}
|
||||
return moreKeys
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createParserForLayout(params: KeyboardParams, context: Context): KeyboardParser? {
|
||||
val layoutName = params.mId.mSubtype.keyboardLayoutSetName
|
||||
val layoutFileNames = context.assets.list("layouts") ?: return null
|
||||
if (layoutFileNames.contains("$layoutName.json"))
|
||||
return JsonKeyboardParser(params, context)
|
||||
val simpleLayoutName = getSimpleLayoutName(layoutName)
|
||||
if (layoutFileNames.contains("$simpleLayoutName.txt"))
|
||||
return SimpleKeyboardParser(params, context)
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmStatic // unsupported without JvmStatic
|
||||
protected fun getSimpleLayoutName(layoutName: String)= when (layoutName) {
|
||||
"swiss", "german", "serbian_qwertz" -> "qwertz"
|
||||
"nordic", "spanish" -> "qwerty"
|
||||
else -> layoutName
|
||||
}
|
||||
}
|
||||
|
||||
protected enum class FunctionalKey {
|
||||
EMOJI, LANGUAGE_SWITCH, COM, EMOJI_COM, ACTION, DELETE, PERIOD, COMMA, SPACE, SHIFT, NUMPAD, SYMBOL, ALPHA, ZWNJ
|
||||
}
|
||||
|
||||
}
|
||||
// moreKeys for numbers, order is 1-9 and then 0
|
||||
// todo (later): like numbers, for non-latin layouts this depends on language and therefore should not be in the parser
|
||||
private val numbersMoreKeys = arrayOf(
|
||||
arrayOf("¹", "½", "⅓","¼", "⅛"),
|
||||
arrayOf("²", "⅔"),
|
||||
arrayOf("³", "¾", "⅜"),
|
||||
arrayOf("⁴"),
|
||||
arrayOf("⅝"),
|
||||
null,
|
||||
arrayOf("⅞"),
|
||||
null,
|
||||
null,
|
||||
arrayOf("ⁿ", "∅"),
|
||||
)
|
||||
|
||||
// could make arrays right away, but they need to be copied anyway as moreKeys arrays are modified when creating KeyParams
|
||||
private const val MORE_KEYS_NAVIGATE_PREVIOUS = "!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard"
|
||||
private const val MORE_KEYS_NAVIGATE_NEXT = "!icon/clipboard_action_key|!code/key_clipboard,!icon/next_key|!code/key_action_next"
|
||||
private const val MORE_KEYS_NAVIGATE_PREVIOUS_NEXT = "!fixedColumnOrder!3,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/next_key|!code/key_action_next"
|
||||
private const val MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS = "!fixedColumnOrder!3,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji"
|
||||
private const val MORE_KEYS_NAVIGATE_EMOJI = "!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji"
|
||||
private const val MORE_KEYS_NAVIGATE_EMOJI_NEXT = "!fixedColumnOrder!3,!needsDividers!,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji,!icon/next_key|!code/key_action_next"
|
||||
private const val MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS_NEXT = "!fixedColumnOrder!4,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji,!icon/next_key|!code/key_action_next"
|
||||
|
||||
// farsi|kannada|nepali_romanized|nepali_traditional|telugu"
|
||||
private val languagesThatNeedZwnjKey = listOf("fa", "ne", "kn", "te")
|
|
@ -4,15 +4,16 @@ package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser
|
|||
import android.content.Context
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.KeyData
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.toTextKey
|
||||
import org.dslul.openboard.inputmethod.latin.common.splitOnWhitespace
|
||||
import org.dslul.openboard.inputmethod.latin.settings.Settings
|
||||
import java.io.InputStream
|
||||
import java.util.Locale
|
||||
import kotlin.math.round
|
||||
|
||||
class LocaleKeyTexts(dataStream: InputStream?) {
|
||||
private val moreKeys = hashMapOf<String, Array<String>>()
|
||||
private val extraKeys = Array<MutableList<Pair<String, Array<String>?>>?>(5) { null }
|
||||
private val extraKeys = Array<MutableList<KeyData>?>(5) { null }
|
||||
var labelSymbols = "\\?123"
|
||||
var labelAlphabet = "ABC"
|
||||
var labelShiftSymbols = "=\\<"
|
||||
|
@ -54,7 +55,7 @@ class LocaleKeyTexts(dataStream: InputStream?) {
|
|||
// need tp provide a copy because some functions like MoreKeySpec.insertAdditionalMoreKeys may modify the array
|
||||
fun getMoreKeys(label: String): Array<String>? = moreKeys[label]?.copyOf()
|
||||
|
||||
fun getExtraKeys(row: Int): List<Pair<String, Array<String>?>>? =
|
||||
fun getExtraKeys(row: Int): List<KeyData>? =
|
||||
if (row > extraKeys.size) null
|
||||
else extraKeys[row]
|
||||
|
||||
|
@ -75,10 +76,9 @@ class LocaleKeyTexts(dataStream: InputStream?) {
|
|||
if (split.size < 2) return
|
||||
val row = split.first().toIntOrNull() ?: return
|
||||
val keys = split.last().splitOnWhitespace()
|
||||
val morekeys = if (keys.size == 1) null else Array(keys.size - 1) { keys[it + 1] }
|
||||
if (extraKeys[row] == null)
|
||||
extraKeys[row] = mutableListOf()
|
||||
extraKeys[row]?.add(keys.first() to morekeys)
|
||||
extraKeys[row]?.add(keys.first().toTextKey(keys.drop(1)))
|
||||
}
|
||||
|
||||
private fun addLabel(split: List<String>) {
|
||||
|
@ -127,31 +127,39 @@ private fun mergeMoreKeys(original: Array<String>, added: List<String>): Array<S
|
|||
// we had 2 rows, and want it again
|
||||
return (l + "${Key.MORE_KEYS_AUTO_COLUMN_ORDER}${round(l.size / 2f).toInt()}").toTypedArray()
|
||||
}
|
||||
// just drop autoColumnOrder otherwise (maybe not? depends on arising issues)
|
||||
// just drop autoColumnOrder otherwise
|
||||
return l.toTypedArray()
|
||||
}
|
||||
return moreKeys.toTypedArray()
|
||||
}
|
||||
|
||||
fun addLocaleKeyTextsToParams(context: Context, params: KeyboardParams) {
|
||||
val locales = Settings.getInstance().current.mSecondaryLocales + params.mId.locale
|
||||
fun addLocaleKeyTextsToParams(context: Context, params: KeyboardParams, moreKeysSetting: Int) {
|
||||
val locales = params.mSecondaryLocales + params.mId.locale
|
||||
params.mLocaleKeyTexts = moreKeysAndLabels.getOrPut(locales.joinToString { it.toString() }) {
|
||||
val lkt = LocaleKeyTexts(getStreamForLocale(params.mId.locale, context))
|
||||
locales.forEach { locale ->
|
||||
if (locale == params.mId.locale) return@forEach
|
||||
lkt.addFile(getStreamForLocale(locale, context))
|
||||
}
|
||||
lkt
|
||||
createLocaleKeyTexts(context, params, moreKeysSetting)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLocaleKeyTexts(context: Context, params: KeyboardParams, moreKeysSetting: Int): LocaleKeyTexts {
|
||||
val lkt = LocaleKeyTexts(getStreamForLocale(params.mId.locale, context))
|
||||
if (moreKeysSetting == MORE_KEYS_MORE)
|
||||
lkt.addFile(context.assets.open("$LANGUAGE_TEXTS_FOLDER/all_more_keys.txt"))
|
||||
else if (moreKeysSetting == MORE_KEYS_ALL)
|
||||
lkt.addFile(context.assets.open("$LANGUAGE_TEXTS_FOLDER/more_more_keys.txt"))
|
||||
params.mSecondaryLocales.forEach { locale ->
|
||||
if (locale == params.mId.locale) return@forEach
|
||||
lkt.addFile(getStreamForLocale(locale, context))
|
||||
}
|
||||
return lkt
|
||||
}
|
||||
|
||||
private fun getStreamForLocale(locale: Locale, context: Context) =
|
||||
try {
|
||||
if (locale.toString() == "zz") context.assets.open("language_key_texts/more_more_keys.txt")
|
||||
else context.assets.open("language_key_texts/${locale.toString().lowercase()}.txt")
|
||||
if (locale.toString() == "zz") context.assets.open("$LANGUAGE_TEXTS_FOLDER/more_more_keys.txt")
|
||||
else context.assets.open("$LANGUAGE_TEXTS_FOLDER/${locale.toString().lowercase()}.txt")
|
||||
} catch (_: Exception) {
|
||||
try {
|
||||
context.assets.open("language_key_texts/${locale.language.lowercase()}.txt")
|
||||
context.assets.open("$LANGUAGE_TEXTS_FOLDER/${locale.language.lowercase()}.txt")
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
@ -226,3 +234,5 @@ private val euroLocales = "bg|ca|cs|da|de|el|en|es|et|eu|fi|fr|ga|gl|hr|hu|it|lb
|
|||
const val MORE_KEYS_ALL = 2;
|
||||
const val MORE_KEYS_MORE = 1;
|
||||
const val MORE_KEYS_NORMAL = 0;
|
||||
|
||||
const val LANGUAGE_TEXTS_FOLDER = "language_key_texts"
|
||||
|
|
|
@ -2,617 +2,50 @@
|
|||
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key.KeyParams
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardId
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardIconsSet
|
||||
import android.util.Log
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
|
||||
import org.dslul.openboard.inputmethod.latin.R
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.KeyData
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.toTextKey
|
||||
import org.dslul.openboard.inputmethod.latin.common.splitOnWhitespace
|
||||
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils
|
||||
import org.dslul.openboard.inputmethod.latin.utils.RunInLocale
|
||||
import org.dslul.openboard.inputmethod.latin.utils.sumOf
|
||||
|
||||
/**
|
||||
* Parser for simple layouts like qwerty or symbol, defined only as rows of (normal) keys with moreKeys.
|
||||
* Functional keys are pre-defined and can't be changed, with exception of comma, period and similar
|
||||
* keys in symbol layouts.
|
||||
* Parser for simple layouts, defined only as rows of (normal) keys with moreKeys.
|
||||
* There may be a short "extra row" for the configurable keys in the bottom row. This is two keys
|
||||
* for alphabet, 3 keys for symbols and 4 keys for shift symbols. MoreKeys on period and comma get
|
||||
* merged with defaults.
|
||||
* All normal keys have the same width and flags, which likely makes the simple layout definitions
|
||||
* incompatible with the requirements of certain (non-latin) languages. These languages need to use
|
||||
* a different (more configurable) layout definition style, and therefore a different parser.
|
||||
* Also number, phone and numpad layouts are not compatible with this parser.
|
||||
*/
|
||||
class SimpleKeyboardParser(private val params: KeyboardParams, private val context: Context) {
|
||||
class SimpleKeyboardParser(private val params: KeyboardParams, private val context: Context) : KeyboardParser(params, context) {
|
||||
private val addExtraKeys =
|
||||
params.mId.locale.language != "eo"
|
||||
&& params.mId.mSubtype.keyboardLayoutSetName in listOf("nordic", "spanish", "german", "swiss", "serbian_qwertz")
|
||||
|
||||
private var addExtraKeys = false
|
||||
fun parseFromAssets(layoutName: String): ArrayList<ArrayList<KeyParams>> {
|
||||
val layoutFile = when (layoutName) {
|
||||
"nordic" -> { addExtraKeys = true; "qwerty" }
|
||||
"spanish" -> {
|
||||
if (params.mId.locale.language == "eo") "eo" // this behaves a bit different than before, but probably still fine
|
||||
else { addExtraKeys = true; "qwerty" }
|
||||
}
|
||||
"german", "swiss", "serbian_qwertz" -> { addExtraKeys = true; "qwertz" }
|
||||
else -> layoutName
|
||||
}
|
||||
return parse(context.assets.open("layouts/$layoutFile.txt").reader().readText())
|
||||
}
|
||||
override fun getLayoutFromAssets(layoutName: String) =
|
||||
context.assets.open("layouts/${getSimpleLayoutName(layoutName)}.txt").reader().readText()
|
||||
|
||||
fun parse(layoutContent: String): ArrayList<ArrayList<KeyParams>> {
|
||||
params.readAttributes(context, null)
|
||||
val keysInRows = ArrayList<ArrayList<KeyParams>>()
|
||||
|
||||
val baseKeys: MutableList<List<BaseKey>> = parseCoreLayout(layoutContent)
|
||||
if (!params.mId.mNumberRowEnabled) {
|
||||
// todo (later): not all layouts have numbers on first row, so maybe have some layout flag to switch it off (or an option)
|
||||
// but for latin it's fine, so don't care now
|
||||
val newFirstRow = baseKeys.first().mapIndexed { index, baseKey ->
|
||||
if (index < numbers.size)
|
||||
BaseKey(baseKey.label, baseKey.moreKeys?.let { arrayOf(numbers[index], *it) })
|
||||
else baseKey
|
||||
}
|
||||
baseKeys[0] = newFirstRow
|
||||
}
|
||||
val functionalKeysReversed = parseFunctionalKeys().reversed()
|
||||
|
||||
// keyboard parsed bottom-up because the number of rows is not fixed, but the functional keys
|
||||
// are always added to the rows near the bottom
|
||||
keysInRows.add(getBottomRowAndAdjustBaseKeys(baseKeys))
|
||||
|
||||
baseKeys.reversed().forEachIndexed { i, it ->
|
||||
val row: List<BaseKey> = if (i == 0) {
|
||||
// add bottom row extra keys
|
||||
it + context.getString(R.string.key_def_extra_bottom_right)
|
||||
.split(",").mapNotNull { if (it.isBlank()) null else BaseKey(it.trim()) }
|
||||
} else {
|
||||
it
|
||||
}
|
||||
// parse functional keys for this row (if any)
|
||||
val functionalKeysDefs = if (i < functionalKeysReversed.size) functionalKeysReversed[i]
|
||||
else emptyList<String>() to emptyList()
|
||||
val functionalKeysLeft = functionalKeysDefs.first.map { getFunctionalKeyParams(it) }
|
||||
val functionalKeysRight = functionalKeysDefs.second.map { getFunctionalKeyParams(it) }
|
||||
val paramsRow = ArrayList<KeyParams>(functionalKeysLeft)
|
||||
|
||||
// determine key width, maybe scale factor for keys, and spacers to add
|
||||
val usedKeyWidth = params.mDefaultRelativeKeyWidth * row.size
|
||||
val functionalKeyWidth = (functionalKeysLeft.sumOf { it.mRelativeWidth }) + (functionalKeysRight.sumOf { it.mRelativeWidth })
|
||||
val availableWidth = 1f - functionalKeyWidth
|
||||
var keyWidth: Float
|
||||
val spacerWidth: Float
|
||||
if (availableWidth - usedKeyWidth > 0.0001f) { // don't add spacers if only a tiny bit is empty
|
||||
// width available, add spacer
|
||||
keyWidth = params.mDefaultRelativeKeyWidth
|
||||
spacerWidth = (availableWidth - usedKeyWidth) / 2
|
||||
} else {
|
||||
// need more width, re-scale
|
||||
spacerWidth = 0f
|
||||
keyWidth = availableWidth / row.size
|
||||
}
|
||||
if (spacerWidth != 0f) {
|
||||
paramsRow.add(KeyParams.newSpacer(params, spacerWidth))
|
||||
}
|
||||
if (keyWidth < params.mDefaultRelativeKeyWidth * 0.82 && spacerWidth == 0f) {
|
||||
// keys are very narrow, also rescale the functional keys to make keys a little wider
|
||||
// 0.82 is just some guess for "too narrow"
|
||||
// todo (maybe): works reasonably well, but actually functional keys could give some more of their width,
|
||||
// as long as they end up above mDefaultRelativeKeyWidth
|
||||
val allKeyScale = 1f / (functionalKeyWidth + row.size * params.mDefaultRelativeKeyWidth)
|
||||
keyWidth = params.mDefaultRelativeKeyWidth * allKeyScale
|
||||
functionalKeysLeft.forEach { it.mRelativeWidth *= allKeyScale }
|
||||
functionalKeysRight.forEach { it.mRelativeWidth *= allKeyScale }
|
||||
}
|
||||
|
||||
for (key in row) {
|
||||
paramsRow.add(KeyParams(
|
||||
key.label,
|
||||
params,
|
||||
keyWidth, // any reasonable way to scale width if there is a long text? might be allowed in user-defined layout
|
||||
0, // todo: maybe autoScale / autoXScale if label has more than 2 characters (exception for emojis?)
|
||||
Key.BACKGROUND_TYPE_NORMAL,
|
||||
key.moreKeys
|
||||
))
|
||||
}
|
||||
if (spacerWidth != 0f) {
|
||||
paramsRow.add(KeyParams.newSpacer(params, spacerWidth))
|
||||
}
|
||||
functionalKeysRight.forEach { paramsRow.add(it) }
|
||||
keysInRows.add(0, paramsRow) // we're doing it backwards, so add on top
|
||||
}
|
||||
resizeLastNormalRowIfNecessaryForAlignment(keysInRows)
|
||||
// rescale height if we have more than 4 rows
|
||||
val heightRescale = if (keysInRows.size > 4) 4f / keysInRows.size else 1f
|
||||
if (params.mId.mNumberRowEnabled)
|
||||
keysInRows.add(0, getNumberRow())
|
||||
if (heightRescale != 1f)
|
||||
// rescale all keys, so number row doesn't look weird (this is done like in current parsing)
|
||||
// todo: in symbols view, number row is not rescaled
|
||||
// so the symbols keyboard is higher than the normal one
|
||||
// not a new issue, but should be solved in this migration
|
||||
// how? possibly scale all keyboards to height of main alphabet? (consider suggestion strip)
|
||||
keysInRows.forEach { it.forEach { it.mRelativeHeight *= heightRescale } }
|
||||
|
||||
return keysInRows
|
||||
}
|
||||
|
||||
// resize keys in last row if they are wider than keys in the row above
|
||||
// this is done so the keys align with the keys above
|
||||
// done e.g. for nordic and swiss layouts
|
||||
private fun resizeLastNormalRowIfNecessaryForAlignment(keysInRows: ArrayList<ArrayList<KeyParams>>) {
|
||||
if (keysInRows.size < 3)
|
||||
return
|
||||
val lastNormalRow = keysInRows[keysInRows.lastIndex - 1]
|
||||
val rowAboveLastNormalRow = keysInRows[keysInRows.lastIndex - 2]
|
||||
if (lastNormalRow.any { it.isSpacer } || rowAboveLastNormalRow.any { it.isSpacer })
|
||||
return // annoying to deal with, and probably no resize needed anyway
|
||||
val lastNormalRowKeyWidth = lastNormalRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }.mRelativeWidth
|
||||
val rowAboveLastNormalRowKeyWidth = rowAboveLastNormalRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }.mRelativeWidth
|
||||
if (lastNormalRowKeyWidth <= rowAboveLastNormalRowKeyWidth + 0.0001f)
|
||||
return // no need
|
||||
if (lastNormalRow.any { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL && it.mRelativeWidth != lastNormalRowKeyWidth })
|
||||
return // normal keys have different width, don't deal with this
|
||||
val numberOfNormalKeys = lastNormalRow.count { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }
|
||||
val widthBefore = numberOfNormalKeys * lastNormalRowKeyWidth
|
||||
val widthAfter = numberOfNormalKeys * rowAboveLastNormalRowKeyWidth
|
||||
val spacerWidth = (widthBefore - widthAfter) / 2
|
||||
// resize keys and add spacers
|
||||
lastNormalRow.forEach { if (it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL) it.mRelativeWidth = rowAboveLastNormalRowKeyWidth }
|
||||
lastNormalRow.add(lastNormalRow.indexOfFirst { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }, KeyParams.newSpacer(params, spacerWidth))
|
||||
lastNormalRow.add(lastNormalRow.indexOfLast { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL } + 1, KeyParams.newSpacer(params, spacerWidth))
|
||||
}
|
||||
|
||||
private fun parseCoreLayout(layoutContent: String) =
|
||||
layoutContent.replace("\r\n", "\n").split("\n\n").mapIndexedTo(mutableListOf()) { i, row ->
|
||||
row.split("\n").mapNotNull {
|
||||
if (it.isBlank()) return@mapNotNull null
|
||||
val split = it.splitOnWhitespace()
|
||||
val moreKeys = if (split.size == 1) {
|
||||
null
|
||||
} else if (split.size == 2 && split.last() == "$$$") { // todo: no good reason to ignore it if size > 2
|
||||
// todo (later): could improve handling and show more currency moreKeys, depending on the moreMoreKeys setting
|
||||
if (params.mId.passwordInput())
|
||||
arrayOf("$")
|
||||
else
|
||||
arrayOf(getCurrencyKey(params.mId.locale).first)
|
||||
} else {
|
||||
Array(split.size - 1) { split[it + 1] }
|
||||
}
|
||||
BaseKey(split.first(), moreKeys)
|
||||
} + if (addExtraKeys)
|
||||
(params.mLocaleKeyTexts.getExtraKeys(i + 1)?.let { it.map { BaseKey(it.first, it.second) } } ?: emptyList())
|
||||
else emptyList()
|
||||
}
|
||||
|
||||
private fun parseFunctionalKeys(): List<Pair<List<String>, List<String>>> =
|
||||
context.getString(R.string.key_def_functional).split("\n").mapNotNull { line ->
|
||||
if (line.isBlank()) return@mapNotNull null
|
||||
val p = line.split(";")
|
||||
p.first().let { if (it.isBlank()) emptyList() else it.split(",") } to
|
||||
p.last().let { if (it.isBlank()) emptyList() else it.split(",") }
|
||||
}
|
||||
|
||||
private fun getBottomRowAndAdjustBaseKeys(baseKeys: MutableList<List<BaseKey>>): ArrayList<KeyParams> {
|
||||
val adjustableKeyCount = when (params.mId.mElementId) {
|
||||
KeyboardId.ELEMENT_SYMBOLS -> 3
|
||||
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> 4
|
||||
else -> 2 // must be alphabet, parser doesn't work for other elementIds
|
||||
}
|
||||
val adjustedKeys = if (baseKeys.last().size == adjustableKeyCount) baseKeys.last()
|
||||
else null
|
||||
if (adjustedKeys != null)
|
||||
baseKeys.removeLast()
|
||||
val bottomRow = ArrayList<KeyParams>()
|
||||
context.getString(R.string.key_def_bottom_row).split(",").forEach {
|
||||
val key = it.trim().splitOnWhitespace().first()
|
||||
val adjustKey = when (key) {
|
||||
KEY_COMMA -> adjustedKeys?.first()
|
||||
KEY_PERIOD -> adjustedKeys?.last()
|
||||
else -> null
|
||||
}
|
||||
val keyParams = getFunctionalKeyParams(it, adjustKey?.label, adjustKey?.moreKeys)
|
||||
if (key == KEY_SPACE) { // add the extra keys around space
|
||||
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) {
|
||||
bottomRow.add(getFunctionalKeyParams(KEY_NUMPAD))
|
||||
bottomRow.add(keyParams)
|
||||
bottomRow.add(KeyParams(
|
||||
adjustedKeys?.get(1)?.label ?: "/",
|
||||
params,
|
||||
params.mDefaultRelativeKeyWidth,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
adjustedKeys?.get(1)?.moreKeys
|
||||
))
|
||||
} else if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
|
||||
bottomRow.add(KeyParams(
|
||||
adjustedKeys?.get(1)?.label ?: "<",
|
||||
params,
|
||||
params.mDefaultRelativeKeyWidth,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
adjustedKeys?.get(1)?.moreKeys
|
||||
))
|
||||
bottomRow.add(keyParams)
|
||||
bottomRow.add(KeyParams(
|
||||
adjustedKeys?.get(2)?.label ?: ">",
|
||||
params,
|
||||
params.mDefaultRelativeKeyWidth,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
adjustedKeys?.get(2)?.moreKeys
|
||||
))
|
||||
} else { // alphabet
|
||||
if (params.mId.mLanguageSwitchKeyEnabled)
|
||||
bottomRow.add(getFunctionalKeyParams(KEY_LANGUAGE_SWITCH))
|
||||
if (params.mId.mEmojiKeyEnabled)
|
||||
bottomRow.add(getFunctionalKeyParams(KEY_EMOJI))
|
||||
bottomRow.add(keyParams)
|
||||
// todo (later): add zwnj if necessary (where to get that info? layout file? then likely will not happen in this parser)
|
||||
}
|
||||
} else {
|
||||
bottomRow.add(keyParams)
|
||||
}
|
||||
}
|
||||
// set space width
|
||||
val space = bottomRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_SPACEBAR }
|
||||
space.mRelativeWidth = 1f - bottomRow.filter { it != space }.sumOf { it.mRelativeWidth }
|
||||
return bottomRow
|
||||
}
|
||||
|
||||
// todo: everything below here likely can and should be shared with the planned parser for more complicated layouts
|
||||
// abstract class?
|
||||
// interface?
|
||||
// utils file?
|
||||
|
||||
private fun getNumberRow(): ArrayList<KeyParams> {
|
||||
val row = ArrayList<KeyParams>()
|
||||
numbers.forEachIndexed { i, n ->
|
||||
row.add(KeyParams(
|
||||
n,
|
||||
params,
|
||||
params.mDefaultRelativeKeyWidth,
|
||||
Key.LABEL_FLAGS_DISABLE_HINT_LABEL, // todo (later): maybe optional or enable (but then all numbers should have moreKeys)
|
||||
Key.BACKGROUND_TYPE_NORMAL,
|
||||
numbersMoreKeys[i] // todo (later, non-latin): language may add some (either alt numbers, or latin numbers if they are replaced above, see number todo)
|
||||
))
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
// for comma and period: label will override default, moreKeys will be appended
|
||||
private fun getFunctionalKeyParams(def: String, label: String? = null, moreKeys: Array<String>? = null): KeyParams {
|
||||
val split = def.trim().splitOnWhitespace()
|
||||
val key = split[0]
|
||||
val width = if (split.size == 2) split[1].substringBefore("%").toFloat() / 100f
|
||||
else params.mDefaultRelativeKeyWidth
|
||||
return when (key) {
|
||||
KEY_SYMBOL -> KeyParams(
|
||||
"${getSymbolLabel()}|!code/key_switch_alpha_symbol", // todo (later): in numpad the code is key_symbolNumpad
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
KEY_COMMA -> KeyParams(
|
||||
label ?: getDefaultCommaLabel(),
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_HAS_POPUP_HINT, // previously only if normal comma, but always is more correct
|
||||
if (label?.first()?.isLetter() == true) Key.BACKGROUND_TYPE_NORMAL else Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
moreKeys?.let { getCommaMoreKeys() + it } ?: getCommaMoreKeys()
|
||||
)
|
||||
KEY_SPACE -> KeyParams(
|
||||
"!icon/space_key|!code/key_space", // !icon/space_key_for_number_layout in number layout, but not on tablet
|
||||
params,
|
||||
width, // will not be used for normal space (only in number layouts)
|
||||
0, // todo (later): alignIconToBottom for non-tablet number layout
|
||||
Key.BACKGROUND_TYPE_SPACEBAR,
|
||||
null
|
||||
)
|
||||
KEY_PERIOD -> KeyParams(
|
||||
label ?: ".",
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT, // todo (later): check what LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT does, maybe remove the flag here
|
||||
if (label?.first()?.isLetter() == true) Key.BACKGROUND_TYPE_NORMAL else Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
moreKeys?.let { getPunctuationMoreKeys() + it } ?: getPunctuationMoreKeys()
|
||||
)
|
||||
KEY_ACTION -> KeyParams(
|
||||
"${getActionKeyLabel()}|${getActionKeyCode()}",
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE
|
||||
or Key.LABEL_FLAGS_AUTO_X_SCALE
|
||||
or Key.LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
|
||||
or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR
|
||||
or KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId),
|
||||
Key.BACKGROUND_TYPE_ACTION,
|
||||
getActionKeyMoreKeys()
|
||||
)
|
||||
KEY_DELETE -> KeyParams(
|
||||
"!icon/delete_key|!code/key_delete",
|
||||
params,
|
||||
width,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
KEY_SHIFT -> KeyParams(
|
||||
"${getShiftLabel()}|!code/key_shift",
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE,
|
||||
// todo (later): possibly the whole stickOn/Off stuff can be removed, currently it should only have a very slight effect in holo
|
||||
if (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED)
|
||||
Key.BACKGROUND_TYPE_STICKY_ON
|
||||
else Key.BACKGROUND_TYPE_STICKY_OFF,
|
||||
arrayOf("!noPanelAutoMoreKey!", " |!code/key_capslock")
|
||||
)
|
||||
KEY_EMOJI -> KeyParams(
|
||||
"!icon/emoji_normal_key|!code/key_emoji",
|
||||
params,
|
||||
width,
|
||||
KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId),
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
// tablet layout has an emoji key that changes to com key in url / mail
|
||||
KEY_EMOJI_COM -> if (params.mId.mMode == KeyboardId.MODE_URL || params.mId.mMode == KeyboardId.MODE_EMAIL)
|
||||
getFunctionalKeyParams(KEY_COM)
|
||||
else getFunctionalKeyParams(KEY_EMOJI)
|
||||
KEY_COM -> KeyParams( // todo: label and moreKeys could be in localeKeyTexts
|
||||
".com",
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_AUTO_X_SCALE or Key.LABEL_FLAGS_FONT_NORMAL or Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_PRESERVE_CASE,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
arrayOf("!hasLabels!", ".net", ".org", ".gov", ".edu")
|
||||
)
|
||||
KEY_LANGUAGE_SWITCH -> KeyParams(
|
||||
"!icon/language_switch_key|!code/key_language_switch",
|
||||
params,
|
||||
width,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
KEY_ALPHA -> KeyParams(
|
||||
"${getAlphabetLabel()}|!code/key_switch_alpha_symbol", // todo (later): in numpad the code is key_alphaNumpad
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
KEY_NUMPAD -> KeyParams(
|
||||
"!icon/numpad_key|!code/key_numpad",
|
||||
params,
|
||||
width,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
else -> throw IllegalArgumentException("unknown key definition \"$key\"")
|
||||
override fun parseCoreLayout(layoutContent: String): MutableList<List<KeyData>> {
|
||||
val rowStrings = layoutContent.replace("\r\n", "\n").split("\n\n")
|
||||
return rowStrings.mapIndexedTo(mutableListOf()) { i, row ->
|
||||
if (addExtraKeys)
|
||||
getExtraKeys(i)?.let { parseRow(row) + it } ?: parseRow(row)
|
||||
else
|
||||
parseRow(row)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getActionKeyLabel(): String {
|
||||
if (params.mId.isMultiLine && (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED))
|
||||
return "!icon/enter_key"
|
||||
val iconName = when (params.mId.imeAction()) {
|
||||
EditorInfo.IME_ACTION_GO -> KeyboardIconsSet.NAME_GO_KEY
|
||||
EditorInfo.IME_ACTION_SEARCH -> KeyboardIconsSet.NAME_SEARCH_KEY
|
||||
EditorInfo.IME_ACTION_SEND -> KeyboardIconsSet.NAME_SEND_KEY
|
||||
EditorInfo.IME_ACTION_NEXT -> KeyboardIconsSet.NAME_NEXT_KEY
|
||||
EditorInfo.IME_ACTION_DONE -> KeyboardIconsSet.NAME_DONE_KEY
|
||||
EditorInfo.IME_ACTION_PREVIOUS -> KeyboardIconsSet.NAME_PREVIOUS_KEY
|
||||
InputTypeUtils.IME_ACTION_CUSTOM_LABEL -> return params.mId.mCustomActionLabel
|
||||
else -> return "!icon/enter_key"
|
||||
private fun parseRow(row: String): List<KeyData> =
|
||||
row.split("\n").mapNotNull {
|
||||
if (it.isBlank()) null
|
||||
else parseKey(it)
|
||||
}
|
||||
val replacement = iconName.replaceIconWithLabelIfNoDrawable()
|
||||
return if (iconName == replacement) // i.e. icon exists
|
||||
"!icon/$iconName"
|
||||
else
|
||||
replacement
|
||||
}
|
||||
|
||||
private fun getActionKeyCode() =
|
||||
if (params.mId.isMultiLine && (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED))
|
||||
"!code/key_shift_enter"
|
||||
else "!code/key_enter"
|
||||
private fun getExtraKeys(rowIndex: Int) = params.mLocaleKeyTexts.getExtraKeys(rowIndex + 1)
|
||||
|
||||
private fun getActionKeyMoreKeys(): Array<String>? {
|
||||
val action = params.mId.imeAction()
|
||||
val navigatePrev = params.mId.navigatePrevious()
|
||||
val navigateNext = params.mId.navigateNext()
|
||||
return when {
|
||||
params.mId.passwordInput() -> when {
|
||||
navigatePrev && action == EditorInfo.IME_ACTION_NEXT -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
|
||||
action == EditorInfo.IME_ACTION_NEXT -> null
|
||||
navigateNext && action == EditorInfo.IME_ACTION_PREVIOUS -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
|
||||
action == EditorInfo.IME_ACTION_PREVIOUS -> null
|
||||
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS_NEXT)
|
||||
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
|
||||
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
|
||||
else -> null
|
||||
}
|
||||
// could change definition of numbers to query a range, or have a pre-defined list, but not that crucial
|
||||
params.mId.mMode in listOf(KeyboardId.MODE_URL, KeyboardId.MODE_EMAIL, KeyboardId.ELEMENT_PHONE, KeyboardId.ELEMENT_NUMBER, KeyboardId.MODE_DATE, KeyboardId.MODE_TIME, KeyboardId.MODE_DATETIME) -> when {
|
||||
action == EditorInfo.IME_ACTION_NEXT && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
|
||||
action == EditorInfo.IME_ACTION_NEXT -> null
|
||||
action == EditorInfo.IME_ACTION_PREVIOUS && navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
|
||||
action == EditorInfo.IME_ACTION_PREVIOUS -> null
|
||||
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS_NEXT)
|
||||
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
|
||||
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
|
||||
else -> null
|
||||
}
|
||||
action == EditorInfo.IME_ACTION_NEXT && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS)
|
||||
action == EditorInfo.IME_ACTION_NEXT -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
|
||||
action == EditorInfo.IME_ACTION_PREVIOUS && navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_NEXT)
|
||||
action == EditorInfo.IME_ACTION_PREVIOUS -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
|
||||
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS_NEXT)
|
||||
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_NEXT)
|
||||
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS)
|
||||
else -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMoreKeysArray(moreKeysDef: String): Array<String> {
|
||||
val moreKeys = mutableListOf<String>()
|
||||
for (moreKey in moreKeysDef.split(",")) {
|
||||
val iconPrefixRemoved = moreKey.substringAfter("!icon/")
|
||||
if (iconPrefixRemoved == moreKey) { // i.e. there is no !icon/
|
||||
moreKeys.add(moreKey)
|
||||
continue
|
||||
}
|
||||
val iconName = iconPrefixRemoved.substringBefore("|")
|
||||
val replacementText = iconName.replaceIconWithLabelIfNoDrawable()
|
||||
if (replacementText == iconName) { // i.e. we have the drawable
|
||||
moreKeys.add(moreKey)
|
||||
} else {
|
||||
moreKeys.add("!hasLabels!") // test what it actually does, but it's probably necessary
|
||||
moreKeys.add(replacementText)
|
||||
}
|
||||
}
|
||||
return moreKeys.toTypedArray()
|
||||
}
|
||||
|
||||
private fun String.replaceIconWithLabelIfNoDrawable(): String {
|
||||
if (params.mIconsSet.getIconDrawable(KeyboardIconsSet.getIconId(this)) != null) return this
|
||||
val id = context.resources.getIdentifier("label_$this", "string", context.packageName)
|
||||
val ril = object : RunInLocale<String>() { // todo (later): simpler way of doing this in a single line?
|
||||
override fun job(res: Resources) = res.getString(id)
|
||||
}
|
||||
return ril.runInLocale(context.resources, params.mId.locale)
|
||||
}
|
||||
|
||||
private fun getAlphabetLabel() = params.mLocaleKeyTexts.labelAlphabet
|
||||
|
||||
private fun getSymbolLabel() = params.mLocaleKeyTexts.labelSymbols
|
||||
|
||||
private fun getShiftLabel(): String {
|
||||
val elementId = params.mId.mElementId
|
||||
if (elementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
|
||||
return params.mLocaleKeyTexts.labelShiftSymbols
|
||||
if (elementId == KeyboardId.ELEMENT_SYMBOLS)
|
||||
return getSymbolLabel()
|
||||
if (elementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || elementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED
|
||||
|| elementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED || elementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED)
|
||||
return "!icon/shift_key_shifted"
|
||||
return "!icon/shift_key"
|
||||
}
|
||||
|
||||
private fun getDefaultCommaLabel(): String {
|
||||
if (params.mId.mMode == KeyboardId.MODE_URL)
|
||||
return "/"
|
||||
if (params.mId.mMode == KeyboardId.MODE_EMAIL)
|
||||
return "\\@"
|
||||
return ","
|
||||
}
|
||||
|
||||
private fun getCommaMoreKeys(): Array<String> {
|
||||
val keys = mutableListOf<String>()
|
||||
if (!params.mId.mDeviceLocked)
|
||||
keys.add("!icon/clipboard_normal_key|!code/key_clipboard")
|
||||
if (!params.mId.mEmojiKeyEnabled)
|
||||
keys.add("!icon/emoji_normal_key|!code/key_emoji")
|
||||
if (!params.mId.mLanguageSwitchKeyEnabled)
|
||||
keys.add("!icon/language_switch_key|!code/key_language_switch")
|
||||
if (!params.mId.mOneHandedModeEnabled)
|
||||
keys.add("!icon/start_onehanded_mode_key|!code/key_start_onehanded")
|
||||
if (!params.mId.mDeviceLocked)
|
||||
keys.add("!icon/settings_key|!code/key_settings")
|
||||
return keys.toTypedArray()
|
||||
}
|
||||
|
||||
private fun getPunctuationMoreKeys(): Array<String> {
|
||||
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS || params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
|
||||
return arrayOf("…")
|
||||
val moreKeys = params.mLocaleKeyTexts.getMoreKeys("punctuation") ?:
|
||||
// todo: some (non-latin) languages have different parenthesis keys
|
||||
arrayOf("${Key.MORE_KEYS_AUTO_COLUMN_ORDER}8", "\\,", "?", "!", "#", ")", "(", "/", ";", "'", "@", ":", "-", "\"", "+", "\\%", "&")
|
||||
if (context.resources.getInteger(R.integer.config_screen_metrics) >= 3 && moreKeys.contains("!") && moreKeys.contains("?")) {
|
||||
// we have a tablet, remove ! and ? keys and reduce number in autoColumnOrder
|
||||
// this makes use of removal of empty moreKeys in MoreKeySpec.insertAdditionalMoreKeys
|
||||
// todo: maybe do this as part of removing unnecessary moreKeys instead of here?
|
||||
moreKeys[moreKeys.indexOf("!")] = ""
|
||||
moreKeys[moreKeys.indexOf("?")] = ""
|
||||
val columns = moreKeys[0].substringAfter(Key.MORE_KEYS_AUTO_COLUMN_ORDER).toIntOrNull()
|
||||
if (columns != null)
|
||||
moreKeys[0] = "${Key.MORE_KEYS_AUTO_COLUMN_ORDER}${columns - 1}"
|
||||
}
|
||||
return moreKeys
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun hasLayoutFile(layoutName: String) = layoutName in supportedLayouts
|
||||
// todo: adjust when changing layout names, and of course when anything changes...
|
||||
private val supportedLayouts = hashSetOf("qwerty", "qwertz", "halmak", "workman", "bepo", "swiss", "german", "nordic", "spanish", "serbian_qwertz")
|
||||
private fun parseKey(key: String): KeyData {
|
||||
val split = key.splitOnWhitespace()
|
||||
return if (split.size == 1)
|
||||
split.first().toTextKey()
|
||||
else
|
||||
split.first().toTextKey(split.drop(1))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// class for holding a parsed key of the simple layout
|
||||
private class BaseKey(
|
||||
val label: String,
|
||||
val moreKeys: Array<String>? = null,
|
||||
)
|
||||
|
||||
// todo (later): may depend on language for non-latin layouts... or should the number row always be latin?
|
||||
// best have number row layout in a file, and get layout file dependent on keyboard layout and some setting
|
||||
// setting should be sth that goes like "prioritize localized numbers"
|
||||
// idea for behavior:
|
||||
// if no extra local number row: number row like now, no number row shows numbers in moreKeys
|
||||
// for other numbers:
|
||||
// language-dependent number moreKeys (latin numbers on localized variant, and localized variant on latin numbers)
|
||||
// show whatever is selected as number row, or as morekeys if number row is off
|
||||
// but then how to simply put both latin and local number in moreKeys when number row is disabled?
|
||||
// consider that numbers may have more moreKeys...
|
||||
// idea: field [numbers] where in the next line the numbers are space-separated in order 1-9 and 0
|
||||
// would be simpler to handle than moreKeys
|
||||
private val numbers = (1..9).map { it.toString() } + "0"
|
||||
|
||||
// moreKeys for numbers, order is 1-9 and then 0
|
||||
// todo (later): like numbers, for non-latin layouts this depends on language and therefore should not be in the parser
|
||||
private val numbersMoreKeys = arrayOf(
|
||||
arrayOf("¹", "½", "⅓","¼", "⅛"),
|
||||
arrayOf("²", "⅔"),
|
||||
arrayOf("³", "¾", "⅜"),
|
||||
arrayOf("⁴"),
|
||||
arrayOf("⅝"),
|
||||
null,
|
||||
arrayOf("⅞"),
|
||||
null,
|
||||
null,
|
||||
arrayOf("ⁿ", "∅"),
|
||||
)
|
||||
|
||||
// could use 1 string per key, and make arrays right away
|
||||
private const val MORE_KEYS_NAVIGATE_PREVIOUS = "!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard"
|
||||
private const val MORE_KEYS_NAVIGATE_NEXT = "!icon/clipboard_action_key|!code/key_clipboard,!icon/next_key|!code/key_action_next"
|
||||
private const val MORE_KEYS_NAVIGATE_PREVIOUS_NEXT = "!fixedColumnOrder!3,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/next_key|!code/key_action_next"
|
||||
private const val MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS = "!fixedColumnOrder!3,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji"
|
||||
private const val MORE_KEYS_NAVIGATE_EMOJI = "!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji"
|
||||
private const val MORE_KEYS_NAVIGATE_EMOJI_NEXT = "!fixedColumnOrder!3,!needsDividers!,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji,!icon/next_key|!code/key_action_next"
|
||||
private const val MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS_NEXT = "!fixedColumnOrder!4,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji,!icon/next_key|!code/key_action_next"
|
||||
|
||||
private const val KEY_EMOJI = "emoji"
|
||||
private const val KEY_LANGUAGE_SWITCH = "language"
|
||||
private const val KEY_COM = "com"
|
||||
private const val KEY_EMOJI_COM = "emoji_com"
|
||||
private const val KEY_DELETE = "delete"
|
||||
private const val KEY_ACTION = "action"
|
||||
private const val KEY_PERIOD = "period"
|
||||
private const val KEY_COMMA = "comma"
|
||||
private const val KEY_SPACE = "space"
|
||||
private const val KEY_SHIFT = "shift"
|
||||
private const val KEY_NUMPAD = "numpad"
|
||||
private const val KEY_SYMBOL = "symbol"
|
||||
private const val KEY_ALPHA = "alphabet"
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,367 @@
|
|||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
* modified
|
||||
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
|
||||
*/
|
||||
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key.KeyParams
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardId
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
|
||||
import org.dslul.openboard.inputmethod.latin.common.StringUtils
|
||||
|
||||
// taken from FlorisBoard, small modifications
|
||||
// popup not nullable (maybe change back, but currently that's necessary for number keys)
|
||||
// added getLabel for creating moreKeys from popups (which may be abstract)
|
||||
// added toKeyParams for non-abstract KeyData
|
||||
// compute is using KeyboardParams (for shift state and variation)
|
||||
// char_width_selector and kana_selector throw an error (not yet supported)
|
||||
/**
|
||||
* Basic interface for a key data object. Base for all key data objects across the IME, such as text, emojis and
|
||||
* selectors. The implementation is as abstract as possible, as different features require different implementations.
|
||||
*/
|
||||
interface AbstractKeyData {
|
||||
/**
|
||||
* Computes a [KeyData] object for this key data. Returns null if no computation is possible or if the key is
|
||||
* not relevant based on the result of [params].
|
||||
*
|
||||
* @param params The KeyboardParams used to retrieve different states from the parent controller.
|
||||
*
|
||||
* @return A [KeyData] object or null if no computation is possible.
|
||||
*/
|
||||
fun compute(params: KeyboardParams): KeyData?
|
||||
|
||||
/**
|
||||
* Returns the data described by this key as a string.
|
||||
*
|
||||
* @param isForDisplay Specifies if the returned string is intended to be displayed in a UI label (=true) or if
|
||||
* it should be computed to be sent to an input connection (=false).
|
||||
*
|
||||
* @return The computed string for the key data object. Note: some objects may return an empty string here, meaning
|
||||
* it is always required to check for the string's length before attempting to directly retrieve the first char.
|
||||
*/
|
||||
fun asString(isForDisplay: Boolean): String // todo: remove it? not used at all (better only later, maybe useful for getting display label in some languages)
|
||||
|
||||
fun getLabel(params: KeyboardParams) = if (this is KeyData) label else compute(params)?.label ?: ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface describing a basic key which can carry a character, an emoji, a special function etc. while being as
|
||||
* abstract as possible.
|
||||
*
|
||||
* @property type The type of the key.
|
||||
* @property code The Unicode code point of this key, or a special code from [KeyCode].
|
||||
* @property label The label of the key. This should always be a representative string for [code].
|
||||
* @property groupId The group which this key belongs to (currently only allows [GROUP_DEFAULT]).
|
||||
* @property popup The popups for ths key. Can also dynamically be provided via popup extensions.
|
||||
*/
|
||||
interface KeyData : AbstractKeyData {
|
||||
val type: KeyType
|
||||
val code: Int
|
||||
val label: String
|
||||
val groupId: Int
|
||||
val popup: PopupSet<AbstractKeyData> // not nullable because can't add number otherwise
|
||||
|
||||
// groups (currently) not supported
|
||||
companion object {
|
||||
/**
|
||||
* Constant for the default group. If not otherwise specified, any key is automatically
|
||||
* assigned to this group.
|
||||
*/
|
||||
const val GROUP_DEFAULT: Int = 0
|
||||
|
||||
/**
|
||||
* Constant for the Left modifier key group. Any key belonging to this group will get the
|
||||
* popups specified for "~left" in the popup mapping.
|
||||
*/
|
||||
const val GROUP_LEFT: Int = 1
|
||||
|
||||
/**
|
||||
* Constant for the right modifier key group. Any key belonging to this group will get the
|
||||
* popups specified for "~right" in the popup mapping.
|
||||
*/
|
||||
const val GROUP_RIGHT: Int = 2
|
||||
|
||||
/**
|
||||
* Constant for the enter modifier key group. Any key belonging to this group will get the
|
||||
* popups specified for "~enter" in the popup mapping.
|
||||
*/
|
||||
const val GROUP_ENTER: Int = 3
|
||||
|
||||
/**
|
||||
* Constant for the enter modifier key group. Any key belonging to this group will get the
|
||||
* popups specified for "~kana" in the popup mapping.
|
||||
*/
|
||||
const val GROUP_KANA: Int = 97
|
||||
}
|
||||
|
||||
fun isSpaceKey(): Boolean {
|
||||
return type == KeyType.CHARACTER && (code == KeyCode.SPACE || code == KeyCode.CJK_SPACE
|
||||
|| code == KeyCode.HALF_SPACE || code == KeyCode.KESHIDA)
|
||||
}
|
||||
|
||||
fun toKeyParams(params: KeyboardParams, width: Float = params.mDefaultRelativeKeyWidth, labelFlags: Int = 0): KeyParams {
|
||||
require(type == KeyType.CHARACTER) { "currently only KeyType.CHARACTER is supported" }
|
||||
require(groupId == GROUP_DEFAULT) { "currently only KeyData.GROUP_DEFAULT is supported" }
|
||||
require(code >= 0) { "functional codes ($code) not (yet) supported" }
|
||||
require(code != KeyCode.UNSPECIFIED || label.isNotEmpty()) { "key has no code and no label" }
|
||||
|
||||
return if (code == KeyCode.UNSPECIFIED || code == KeyCode.MULTIPLE_CODE_POINTS) {
|
||||
// code will be determined from label if possible (i.e. label is single code point)
|
||||
// but also longer labels should work without issues, also for MultiTextKeyData
|
||||
KeyParams(
|
||||
label, // todo (when supported): convert special labels to keySpec
|
||||
params,
|
||||
width,
|
||||
labelFlags, // todo (non-latin): label flags... maybe relevant for some languages
|
||||
Key.BACKGROUND_TYPE_NORMAL, // todo (when supported): determine type
|
||||
popup.toMoreKeys(params),
|
||||
)
|
||||
} else {
|
||||
KeyParams(
|
||||
label.ifEmpty { StringUtils.newSingleCodePointString(code) },
|
||||
code, // todo (when supported): convert codes < 0
|
||||
params,
|
||||
width,
|
||||
labelFlags,
|
||||
Key.BACKGROUND_TYPE_NORMAL,
|
||||
popup.toMoreKeys(params),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to select an [AbstractKeyData] based on the current caps state. Note that this type of selector only really
|
||||
* makes sense in a text context, though technically speaking it can be used anywhere, so this implementation allows
|
||||
* for any [AbstractKeyData] to be used here. The JSON class identifier for this selector is `case_selector`.
|
||||
*
|
||||
* Example usage in a layout JSON file:
|
||||
* ```
|
||||
* { "$": "case_selector",
|
||||
* "lower": { "code": 59, "label": ";" },
|
||||
* "upper": { "code": 58, "label": ":" }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @property lower The key data to use if the current caps state is lowercase.
|
||||
* @property upper The key data to use if the current caps state is uppercase.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("case_selector")
|
||||
class CaseSelector(
|
||||
val lower: AbstractKeyData,
|
||||
val upper: AbstractKeyData,
|
||||
) : AbstractKeyData {
|
||||
override fun compute(params: KeyboardParams): KeyData? {
|
||||
return (if (params.mId.isAlphabetShifted) { upper } else { lower }).compute(params)
|
||||
}
|
||||
|
||||
override fun asString(isForDisplay: Boolean): String {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to select an [AbstractKeyData] based on the current shift state. Note that this type of selector only really
|
||||
* makes sense in a text context, though technically speaking it can be used anywhere, so this implementation allows
|
||||
* for any [AbstractKeyData] to be used here. The JSON class identifier for this selector is `shift_state_selector`.
|
||||
*
|
||||
* Example usage in a layout JSON file:
|
||||
* ```
|
||||
* { "$": "shift_state_selector",
|
||||
* "shiftedManual": { "code": 59, "label": ";" },
|
||||
* "default": { "code": 58, "label": ":" }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @property unshifted The key data to use if the current shift state is unshifted, falling back to
|
||||
* [default] if unspecified.
|
||||
* @property shifted The key data to use if the current shift state is either manual or
|
||||
* automatic. Is overridden if [shiftedManual] or [shiftedAutomatic] is specified.
|
||||
* @property shiftedManual The key data to use if the current shift state is manual,
|
||||
* falling back to [shifted] or [default] if unspecified.
|
||||
* @property shiftedAutomatic The key data to use if the current shift state is automatic,
|
||||
* falling back to [shifted] or [default] if unspecified.
|
||||
* @property capsLock The key data to use if the current shift state is locked, falling back to
|
||||
* [default] if unspecified.
|
||||
* @property default The key data to use if the current shift state is set to a value not specified by this selector.
|
||||
* If a key data is provided for all shift states possible this key data will never be used.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("shift_state_selector")
|
||||
class ShiftStateSelector(
|
||||
val unshifted: AbstractKeyData? = null,
|
||||
val shifted: AbstractKeyData? = null,
|
||||
val shiftedManual: AbstractKeyData? = null,
|
||||
val shiftedAutomatic: AbstractKeyData? = null,
|
||||
val capsLock: AbstractKeyData? = null,
|
||||
val default: AbstractKeyData? = null,
|
||||
) : AbstractKeyData {
|
||||
override fun compute(params: KeyboardParams): KeyData? {
|
||||
return when (params.mId.mElementId) {
|
||||
KeyboardId.ELEMENT_ALPHABET, KeyboardId.ELEMENT_SYMBOLS -> unshifted ?: default
|
||||
KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED -> shiftedManual ?: shifted ?: default
|
||||
KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED -> shiftedAutomatic ?: shifted ?: default
|
||||
KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED -> capsLock ?: shifted ?: default
|
||||
else -> default // or rather unshifted?
|
||||
}?.compute(params)
|
||||
}
|
||||
|
||||
override fun asString(isForDisplay: Boolean): String {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to select an [AbstractKeyData] based on the current variation. Note that this type of selector only really
|
||||
* makes sense in a text context, though technically speaking it can be used anywhere, so this implementation allows
|
||||
* for any [AbstractKeyData] to be used here. The JSON class identifier for this selector is `variation_selector`.
|
||||
*
|
||||
* Example usage in a layout JSON file:
|
||||
* ```
|
||||
* { "$": "variation_selector",
|
||||
* "default": { "code": 44, "label": "," },
|
||||
* "email": { "code": 64, "label": "@" },
|
||||
* "uri": { "code": 47, "label": "/" }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @property default The default key data which should be used in case no key variation is known or for the current
|
||||
* key variation no override key is defined. Can be null, in this case this may mean the variation selector hides
|
||||
* the key if no direct match is present.
|
||||
* @property email The key data to use if [KeyboardId.MODE_EMAIL] is active. If this value is
|
||||
* null, [default] will be used instead.
|
||||
* @property uri The key data to use if [KeyboardId.MODE_URL] is active. If this value is null,
|
||||
* [default] will be used instead.
|
||||
* @property normal The key data to use when? Currently ignored... If this value is null,
|
||||
* [default] will be used instead.
|
||||
* @property password The key data to use if [KeyboardId.passwordInput] return true. If this value is
|
||||
* null, [default] will be used instead.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("variation_selector")
|
||||
data class VariationSelector(
|
||||
val default: AbstractKeyData? = null,
|
||||
val email: AbstractKeyData? = null,
|
||||
val uri: AbstractKeyData? = null,
|
||||
val normal: AbstractKeyData? = null,
|
||||
val password: AbstractKeyData? = null,
|
||||
) : AbstractKeyData {
|
||||
override fun compute(params: KeyboardParams): KeyData? {
|
||||
return when {
|
||||
// todo: what is normal and all?
|
||||
// KeyVariation.ALL -> default
|
||||
// KeyVariation.NORMAL -> normal ?: default
|
||||
params.mId.passwordInput() -> password ?: default
|
||||
params.mId.mMode == KeyboardId.MODE_EMAIL -> email ?: default
|
||||
params.mId.mMode == KeyboardId.MODE_URL -> uri ?: default
|
||||
else -> default
|
||||
}?.compute(params)
|
||||
}
|
||||
|
||||
override fun asString(isForDisplay: Boolean): String {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to select an [AbstractKeyData] based on the current layout direction. Note that this type of selector only
|
||||
* really makes sense in a text context, though technically speaking it can be used anywhere, so this implementation
|
||||
* allows for any [AbstractKeyData] to be used here. The JSON class identifier for this selector is
|
||||
* `layout_direction_selector`.
|
||||
*
|
||||
* Example usage in a layout JSON file:
|
||||
* ```
|
||||
* { "$": "layout_direction_selector",
|
||||
* "ltr": { "code": 59, "label": ";" },
|
||||
* "rtl": { "code": 58, "label": ":" }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @property ltr The key data to use if the current layout direction is LTR.
|
||||
* @property rtl The key data to use if the current layout direction is RTL.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("layout_direction_selector")
|
||||
class LayoutDirectionSelector(
|
||||
val ltr: AbstractKeyData,
|
||||
val rtl: AbstractKeyData,
|
||||
) : AbstractKeyData {
|
||||
override fun compute(params: KeyboardParams): KeyData? {
|
||||
return (if (params.mId.mSubtype.isRtlSubtype) { rtl } else { ltr }).compute(params)
|
||||
}
|
||||
|
||||
override fun asString(isForDisplay: Boolean): String {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to select an [AbstractKeyData] based on the character's width. Note that this type of selector only really
|
||||
* makes sense in a text context, though technically speaking it can be used anywhere, so this implementation allows
|
||||
* for any [AbstractKeyData] to be used here. The JSON class identifier for this selector is `char_width_selector`.
|
||||
*
|
||||
* Example usage in a layout JSON file:
|
||||
* ```
|
||||
* { "$": "char_width_selector",
|
||||
* "full": { "code": 12450, "label": "ア" },
|
||||
* "half": { "code": 65393, "label": "ア" }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @property full The key data to use if the current character width is full.
|
||||
* @property half The key data to use if the current character width is half.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("char_width_selector")
|
||||
class CharWidthSelector(
|
||||
val full: AbstractKeyData?,
|
||||
val half: AbstractKeyData?,
|
||||
) : AbstractKeyData {
|
||||
override fun compute(params: KeyboardParams): KeyData? {
|
||||
throw UnsupportedOperationException("char_width_selector not (yet) supported")
|
||||
// val data = if (params.halfWidth) { half } else { full }
|
||||
// return data?.compute(params)
|
||||
}
|
||||
|
||||
override fun asString(isForDisplay: Boolean): String {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to select an [AbstractKeyData] based on the kana state. Note that this type of selector only really
|
||||
* makes sense in a text context, though technically speaking it can be used anywhere, so this implementation allows
|
||||
* for any [AbstractKeyData] to be used here. The JSON class identifier for this selector is `kana_selector`.
|
||||
*
|
||||
* Example usage in a layout JSON file:
|
||||
* ```
|
||||
* { "$": "kana_selector",
|
||||
* "hira": { "code": 12354, "label": "あ" },
|
||||
* "kata": { "code": 12450, "label": "ア" }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @property hira The key data to use if the current kana state is hiragana.
|
||||
* @property kata The key data to use if the current kana state is katakana.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("kana_selector")
|
||||
class KanaSelector(
|
||||
val hira: AbstractKeyData,
|
||||
val kata: AbstractKeyData,
|
||||
) : AbstractKeyData {
|
||||
override fun compute(params: KeyboardParams): KeyData? {
|
||||
throw UnsupportedOperationException("kana_selector not (yet) supported")
|
||||
// val data = if (evaluator.state.isKanaKata) { kata } else { hira }
|
||||
// return data.compute(evaluator)
|
||||
}
|
||||
|
||||
override fun asString(isForDisplay: Boolean): String {
|
||||
return ""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
* modified
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
// taken from FlorisBoard, not actually used (only CHARACTER allowed)
|
||||
/**
|
||||
* Enum for declaring the type of the key.
|
||||
* List of possible key types:
|
||||
* [Wikipedia](https://en.wikipedia.org/wiki/Keyboard_layout#Key_types)
|
||||
*/
|
||||
@Serializable(with = KeyTypeSerializer::class)
|
||||
enum class KeyType {
|
||||
CHARACTER,
|
||||
ENTER_EDITING,
|
||||
FUNCTION,
|
||||
LOCK,
|
||||
MODIFIER,
|
||||
NAVIGATION,
|
||||
SYSTEM_GUI,
|
||||
NUMERIC,
|
||||
PLACEHOLDER,
|
||||
UNSPECIFIED;
|
||||
|
||||
override fun toString(): String {
|
||||
return super.toString().lowercase()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String): KeyType {
|
||||
return valueOf(string.uppercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class KeyTypeSerializer : KSerializer<KeyType> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("KeyType", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: KeyType) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): KeyType {
|
||||
return KeyType.fromString(decoder.decodeString())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* modified
|
||||
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
|
||||
*/
|
||||
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.getCurrencyKey
|
||||
|
||||
// taken from FlorisBoard, small modifications
|
||||
// mutable set removed (currently the moreKeys assembly is happening in KeyParams)
|
||||
// toMoreKeys and size added
|
||||
// popupKeys not used, but might switch to this later
|
||||
// currently hint would be taken from other, and languageMoreKeys are prioritized
|
||||
/**
|
||||
* A popup set for a single key. This set describes, if the key has a [main] character and other [relevant] popups.
|
||||
*
|
||||
* Note that a hint character cannot and should not be set in a json extended popup file, rather it
|
||||
* should only be dynamically set by the LayoutManager.
|
||||
*/
|
||||
@Serializable
|
||||
open class PopupSet<T : AbstractKeyData>(
|
||||
open val main: T? = null,
|
||||
open val relevant: List<T> = emptyList()
|
||||
) {
|
||||
// todo (idea):
|
||||
// this is very simple, but essentially what happens in the old system
|
||||
// could make use of PopupKeys, and also provide a hint depending on user choice
|
||||
// then language key joining should be done in here too
|
||||
// also what about getting the moreKeys and key hint from chosen symbol layout?
|
||||
fun toMoreKeys(params: KeyboardParams) = if (isEmpty) null // need null instead of empty array for KeyParams
|
||||
else (listOfNotNull(number?.toString(), main?.getLabel(params)) + relevant
|
||||
// todo: this is not nice and creates unnecessary intermediate lists
|
||||
// best treat the currency key properly, like florisboard does
|
||||
.map { it.getLabel(params) }).map {
|
||||
if (it == "$$$") {
|
||||
if (params.mId.passwordInput()) "$"
|
||||
else getCurrencyKey(params.mId.locale).first
|
||||
}
|
||||
else it
|
||||
}.toTypedArray()
|
||||
|
||||
private val popupKeys: PopupKeys<T> by lazy {
|
||||
PopupKeys(null, listOfNotNull(main), relevant)
|
||||
}
|
||||
var number: Int? = null
|
||||
val isEmpty get() = main == null && relevant.isEmpty() && number == null
|
||||
}
|
||||
|
||||
/**
|
||||
* A fully configured collection of popup keys. It contains a list of keys to be prioritized
|
||||
* during rendering (ordered by relevance descending) by showing those keys close to the
|
||||
* popup spawning point.
|
||||
*
|
||||
* The keys contain a separate [hint] key to ease rendering the hint label, but the hint, if
|
||||
* present, also occurs in the [prioritized] list.
|
||||
*
|
||||
* The popup keys can be accessed like an array with the addition that negative indexes defined
|
||||
* within this companion object are allowed (as long as the corresponding [prioritized] list
|
||||
* contains the corresponding amount of keys.
|
||||
*/
|
||||
class PopupKeys<T>(
|
||||
val hint: T?,
|
||||
val prioritized: List<T>,
|
||||
val other: List<T>
|
||||
) : Collection<T> {
|
||||
companion object {
|
||||
const val FIRST_PRIORITIZED = -1
|
||||
const val SECOND_PRIORITIZED = -2
|
||||
const val THIRD_PRIORITIZED = -3
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = prioritized.size + other.size
|
||||
|
||||
override fun contains(element: T): Boolean {
|
||||
return prioritized.contains(element) || other.contains(element)
|
||||
}
|
||||
|
||||
override fun containsAll(elements: Collection<T>): Boolean {
|
||||
return (prioritized + other).containsAll(elements)
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return prioritized.isEmpty() && other.isEmpty()
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<T> {
|
||||
return (prioritized + other).listIterator()
|
||||
}
|
||||
|
||||
fun getOrNull(index: Int): T? {
|
||||
if (index >= other.size || index < -prioritized.size) {
|
||||
return null
|
||||
}
|
||||
return when (index) {
|
||||
FIRST_PRIORITIZED -> prioritized[0]
|
||||
SECOND_PRIORITIZED -> prioritized[1]
|
||||
THIRD_PRIORITIZED -> prioritized[2]
|
||||
else -> other.getOrNull(index)
|
||||
}
|
||||
}
|
||||
|
||||
operator fun get(index: Int): T {
|
||||
val item = getOrNull(index)
|
||||
if (item == null) {
|
||||
throw IndexOutOfBoundsException(
|
||||
"Specified index $index is not an valid entry in this PopupKeys!"
|
||||
)
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
* modified
|
||||
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
|
||||
*/
|
||||
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key.KeyParams
|
||||
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
|
||||
|
||||
// taken from FlorisBoard, small modifications (see also KeyData)
|
||||
// internal keys removed (currently no plan to support them)
|
||||
// added String.toTextKey
|
||||
/**
|
||||
* Data class which describes a single key and its attributes.
|
||||
*
|
||||
* @property type The type of the key. Some actions require both [code] and [type] to match in order
|
||||
* to be successfully executed. Defaults to [KeyType.CHARACTER].
|
||||
* @property code The UTF-8 encoded code of the character. The code defined here is used as the
|
||||
* data passed to the system. Defaults to 0.
|
||||
* @property label The string used to display the key in the UI. Is not used for the actual data
|
||||
* passed to the system. Should normally be the exact same as the [code]. Defaults to an empty
|
||||
* string.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("text_key")
|
||||
class TextKeyData(
|
||||
override val type: KeyType = KeyType.CHARACTER,
|
||||
override val code: Int = KeyCode.UNSPECIFIED,
|
||||
override val label: String = "",
|
||||
override val groupId: Int = KeyData.GROUP_DEFAULT,
|
||||
override val popup: PopupSet<AbstractKeyData> = PopupSet()
|
||||
) : KeyData {
|
||||
override fun compute(params: KeyboardParams): KeyData {
|
||||
// if (evaluator.isSlot(this)) { // todo: currency key stuff probably should be taken from florisboard too
|
||||
// return evaluator.slotData(this)?.let { data ->
|
||||
// TextKeyData(type, data.code, data.label, groupId, popup).compute(params)
|
||||
// }
|
||||
// }
|
||||
return this
|
||||
}
|
||||
|
||||
override fun asString(isForDisplay: Boolean): String {
|
||||
return buildString {
|
||||
if (isForDisplay || code == KeyCode.URI_COMPONENT_TLD || code < KeyCode.SPACE) {
|
||||
if (Unicode.isNonSpacingMark(code) && !label.startsWith("◌")) {
|
||||
append("◌")
|
||||
}
|
||||
append(label)
|
||||
} else {
|
||||
try { appendCodePoint(code) } catch (_: Throwable) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "${TextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("auto_text_key")
|
||||
class AutoTextKeyData(
|
||||
override val type: KeyType = KeyType.CHARACTER,
|
||||
override val code: Int = KeyCode.UNSPECIFIED,
|
||||
override val label: String = "",
|
||||
override val groupId: Int = KeyData.GROUP_DEFAULT,
|
||||
override val popup: PopupSet<AbstractKeyData> = PopupSet()
|
||||
) : KeyData {
|
||||
// state and recompute not needed, as upcasing is done when creating KeyParams
|
||||
|
||||
override fun compute(params: KeyboardParams): KeyData {
|
||||
// if (evaluator.isSlot(this)) { // todo: see above
|
||||
// return evaluator.slotData(this)?.let { data ->
|
||||
// TextKeyData(type, data.code, data.label, groupId, popup).compute(evaluator)
|
||||
// }
|
||||
// }
|
||||
return this
|
||||
}
|
||||
|
||||
override fun asString(isForDisplay: Boolean): String {
|
||||
return buildString {
|
||||
if (isForDisplay || code == KeyCode.URI_COMPONENT_TLD || code < KeyCode.SPACE) {
|
||||
if (Unicode.isNonSpacingMark(code) && !label.startsWith("◌")) {
|
||||
append("◌")
|
||||
}
|
||||
append(label)
|
||||
} else {
|
||||
try { appendCodePoint(code) } catch (_: Throwable) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "${AutoTextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("multi_text_key")
|
||||
class MultiTextKeyData(
|
||||
override val type: KeyType = KeyType.CHARACTER,
|
||||
val codePoints: IntArray = intArrayOf(),
|
||||
override val label: String = "",
|
||||
override val groupId: Int = KeyData.GROUP_DEFAULT,
|
||||
override val popup: PopupSet<AbstractKeyData> = PopupSet()
|
||||
) : KeyData {
|
||||
@Transient override val code: Int = KeyCode.MULTIPLE_CODE_POINTS
|
||||
|
||||
override fun compute(params: KeyboardParams): KeyData {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun asString(isForDisplay: Boolean): String {
|
||||
return buildString {
|
||||
if (isForDisplay) {
|
||||
append(label)
|
||||
} else {
|
||||
for (codePoint in codePoints) {
|
||||
try { appendCodePoint(codePoint) } catch (_: Throwable) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "${MultiTextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }"
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toTextKey(moreKeys: Collection<String>? = null): TextKeyData =
|
||||
TextKeyData(
|
||||
label = this,
|
||||
popup = moreKeys
|
||||
?.let { keys -> PopupSet(null, keys.map { it.toTextKey() }) }
|
||||
?: PopupSet()
|
||||
)
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue