overhaul style of parsing layouts

and add a cache
This commit is contained in:
Helium314 2024-05-14 23:31:55 +02:00
parent 1bd30f612b
commit ed776c3804
12 changed files with 256 additions and 262 deletions

View file

@ -17,6 +17,7 @@ import helium314.keyboard.keyboard.internal.KeyboardParams;
import helium314.keyboard.keyboard.internal.UniqueKeysCache;
import helium314.keyboard.keyboard.internal.keyboard_parser.LocaleKeyboardInfos;
import helium314.keyboard.keyboard.internal.keyboard_parser.LocaleKeyboardInfosKt;
import helium314.keyboard.keyboard.internal.keyboard_parser.RawKeyboardParser;
import helium314.keyboard.latin.RichInputMethodSubtype;
import helium314.keyboard.latin.utils.InputTypeUtils;
import helium314.keyboard.latin.utils.Log;
@ -99,6 +100,7 @@ public final class KeyboardLayoutSet {
private static void clearKeyboardCache() {
sKeyboardCache.clear();
sUniqueKeysCache.clear();
RawKeyboardParser.INSTANCE.clearCache();
}
KeyboardLayoutSet(final Context context, @NonNull final Params params) {

View file

@ -59,7 +59,7 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
mParams.mPopupKeyTypes.addAll(sv.mPopupKeyTypes)
// add label source only if popup key type enabled
sv.mPopupKeyLabelSources.forEach { if (it in sv.mPopupKeyTypes) mParams.mPopupKeyLabelSources.add(it) }
keysInRows = KeyboardParser.parseLayout(mParams, mContext)
keysInRows = KeyboardParser(mParams, mContext).parseLayout()
determineAbsoluteValues()
} catch (e: Exception) {
Log.e(TAG, "error parsing layout $id ${id.mElementId}", e)

View file

@ -1,72 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.keyboard.internal.keyboard_parser
import android.content.Context
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.AbstractKeyData
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.AutoTextKeyData
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.CaseSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.CharWidthSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KanaSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyData
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.LayoutDirectionSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.MultiTextKeyData
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.ShiftStateSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.TextKeyData
import helium314.keyboard.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] layoutPopupKeys, no choice of which to use or which hint is provided
*/
class JsonKeyboardParser(private val params: KeyboardParams, context: Context) : KeyboardParser(params, context) {
override fun parseCoreLayout(layoutContent: String): MutableList<List<KeyData>> {
val florisKeyData: List<List<AbstractKeyData>> = florisJsonConfig.decodeFromString(layoutContent)
// initially 200 ms parse (debug build on S4 mini)
// after a few parses it's optimized and 20-30 ms
// whole load is 50-70 ms vs 30-55 with simple parser -> it's ok
return florisKeyData.mapTo(mutableListOf()) { it.mapNotNull { it.compute(params) } }
}
}
/*
* Copyright (C) 2021 Patrick Goldinger
* modified
* SPDX-License-Identifier: Apache-2.0
*/
private val florisJsonConfig = Json {
classDiscriminator = "$"
encodeDefaults = true
ignoreUnknownKeys = true
isLenient = true
serializersModule = SerializersModule {
polymorphic(AbstractKeyData::class) {
subclass(TextKeyData::class, TextKeyData.serializer())
subclass(AutoTextKeyData::class, AutoTextKeyData.serializer())
subclass(MultiTextKeyData::class, MultiTextKeyData.serializer())
subclass(CaseSelector::class, CaseSelector.serializer())
subclass(ShiftStateSelector::class, ShiftStateSelector.serializer())
subclass(VariationSelector::class, VariationSelector.serializer())
subclass(LayoutDirectionSelector::class, LayoutDirectionSelector.serializer())
subclass(CharWidthSelector::class, CharWidthSelector.serializer())
subclass(KanaSelector::class, KanaSelector.serializer())
defaultDeserializer { TextKeyData.serializer() }
}
polymorphic(KeyData::class) {
subclass(TextKeyData::class, TextKeyData.serializer())
subclass(AutoTextKeyData::class, AutoTextKeyData.serializer())
subclass(MultiTextKeyData::class, MultiTextKeyData.serializer())
defaultDeserializer { TextKeyData.serializer() }
}
}
}

View file

@ -24,19 +24,16 @@ import helium314.keyboard.latin.common.isEmoji
import helium314.keyboard.latin.define.DebugFlags
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.spellcheck.AndroidSpellCheckerService
import helium314.keyboard.latin.utils.CUSTOM_LAYOUT_PREFIX
import helium314.keyboard.latin.utils.InputTypeUtils
import helium314.keyboard.latin.utils.POPUP_KEYS_LAYOUT
import helium314.keyboard.latin.utils.POPUP_KEYS_NUMBER
import helium314.keyboard.latin.utils.ScriptUtils
import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.getLayoutFile
import helium314.keyboard.latin.utils.removeFirst
import helium314.keyboard.latin.utils.replaceFirst
import helium314.keyboard.latin.utils.runInLocale
import helium314.keyboard.latin.utils.splitAt
import helium314.keyboard.latin.utils.sumOf
import java.io.File
/**
* Abstract parser class that handles creation of keyboard from [KeyData] arranged in rows,
@ -47,7 +44,7 @@ import java.io.File
* By default, all normal keys have the same width and flags, which may cause issues with the
* requirements of certain non-latin languages.
*/
abstract class KeyboardParser(private val params: KeyboardParams, private val context: Context) {
class KeyboardParser(private val params: KeyboardParams, private val context: Context) {
private val infos = layoutInfos(params)
private val defaultLabelFlags = when {
params.mId.isAlphabetKeyboard -> params.mLocaleKeyboardInfos.labelFlags
@ -57,10 +54,7 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
else -> 0
}
abstract fun parseCoreLayout(layoutContent: String): MutableList<List<KeyData>>
// this thing does too much... make it more understandable after everything is implemented
fun parseLayoutString(layoutContent: String): ArrayList<ArrayList<KeyParams>> {
fun parseLayout(): ArrayList<ArrayList<KeyParams>> {
params.readAttributes(context, null)
params.mProximityCharsCorrectionEnabled = infos.enableProximityCharsCorrection
if (infos.touchPositionCorrectionData == null) // need to set correctly, as it's not properly done in readAttributes with attr = null
@ -68,7 +62,7 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
else
params.mTouchPositionCorrection.load(context.resources.getStringArray(infos.touchPositionCorrectionData))
val baseKeys = parseCoreLayout(layoutContent)
val baseKeys = RawKeyboardParser.parseLayout(params, context)
val keysInRows = createRows(baseKeys)
// rescale height if we have anything but the usual 4 rows
val heightRescale = if (keysInRows.size != 4) 4f / keysInRows.size else 1f
@ -79,26 +73,7 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
return keysInRows
}
// this should be ready for customizable functional layouts, but needs cleanup
// todo (later): remove this as part of adding a cache for parsed layouts
private fun getFunctionalKeyLayoutText(): String {
if (params.mId.isNumberLayout) return "[]" // empty list
val layouts = Settings.getLayoutsDir(context).list() ?: emptyArray()
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
if ("functional_keys_symbols_shifted.json" in layouts)
return getLayoutFile("functional_keys_symbols_shifted.json", context).readText()
}
if (!params.mId.isAlphabetKeyboard) {
if ("functional_keys_symbols.json" in layouts)
return getLayoutFile("functional_keys_symbols.json", context).readText()
}
if ("functional_keys.json" in layouts)
return getLayoutFile("functional_keys.json", context).readText()
val fileName = if (Settings.getInstance().isTablet) "functional_keys_tablet.json" else "functional_keys.json"
return context.readAssetsLayoutFile(fileName)
}
private fun createRows(baseKeys: MutableList<List<KeyData>>): ArrayList<ArrayList<KeyParams>> {
private fun createRows(baseKeys: MutableList<MutableList<KeyData>>): ArrayList<ArrayList<KeyParams>> {
// add padding for number layouts in landscape mode (maybe do it some other way later)
if (params.mId.isNumberLayout && params.mId.mElementId != KeyboardId.ELEMENT_NUMPAD
&& context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
@ -112,14 +87,14 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
addSymbolPopupKeys(baseKeys)
if (params.mId.isAlphaOrSymbolKeyboard && params.mId.mNumberRowEnabled)
baseKeys.add(0, params.mLocaleKeyboardInfos.getNumberRow()
.map { it.copy(newLabelFlags = Key.LABEL_FLAGS_DISABLE_HINT_LABEL or defaultLabelFlags) })
.mapTo(mutableListOf()) { it.copy(newLabelFlags = Key.LABEL_FLAGS_DISABLE_HINT_LABEL or defaultLabelFlags) })
val allFunctionalKeys = JsonKeyboardParser(params, context).parseCoreLayout(getFunctionalKeyLayoutText())
val allFunctionalKeys = RawKeyboardParser.parseLayout(params, context, true)
adjustBottomFunctionalRowAndBaseKeys(allFunctionalKeys, baseKeys)
if (allFunctionalKeys.none { it.singleOrNull()?.isKeyPlaceholder() == true })
// add a placeholder so splitAt does what we really want
allFunctionalKeys.add(0, listOf(TextKeyData(type = KeyType.PLACEHOLDER)))
allFunctionalKeys.add(0, mutableListOf(TextKeyData(type = KeyType.PLACEHOLDER)))
val (functionalKeysTop, functionalKeysBottom) = allFunctionalKeys.splitAt { it.singleOrNull()?.isKeyPlaceholder() == true }
@ -235,8 +210,8 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
* does nothing if not isAlphaOrSymbolKeyboard or assumptions not met
* adds an empty row to baseKeys, to have a baseKey row for the bottom functional row
*/
private fun adjustBottomFunctionalRowAndBaseKeys(allFunctionalKeys: MutableList<List<KeyData>>, baseKeys: MutableList<List<KeyData>>) {
val functionalKeysBottom = allFunctionalKeys.lastOrNull()?.toMutableList() ?: return
private fun adjustBottomFunctionalRowAndBaseKeys(allFunctionalKeys: MutableList<MutableList<KeyData>>, baseKeys: MutableList<MutableList<KeyData>>) {
val functionalKeysBottom = allFunctionalKeys.lastOrNull() ?: return
if (!params.mId.isAlphaOrSymbolKeyboard || functionalKeysBottom.isEmpty() || functionalKeysBottom.any { it.isKeyPlaceholder() })
return
if (true /* Settings.getInstance().current.mSingleFunctionalLayout */) { // todo with the customizable functional layout
@ -287,8 +262,7 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
functionalKeysBottom.add(spaceIndex, key1)
}
}
allFunctionalKeys[allFunctionalKeys.lastIndex] = functionalKeysBottom
baseKeys.add(emptyList())
baseKeys.add(mutableListOf())
}
// ideally we would get all functional keys in a nice list of pairs from the start, but at least it works...
@ -331,12 +305,12 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
}
}
private fun addNumberRowOrPopupKeys(baseKeys: MutableList<List<KeyData>>) {
private fun addNumberRowOrPopupKeys(baseKeys: MutableList<MutableList<KeyData>>) {
if (!params.mId.mNumberRowEnabled && params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) {
// replace first symbols row with number row, but use the labels as popupKeys
val numberRow = params.mLocaleKeyboardInfos.getNumberRow()
numberRow.forEachIndexed { index, keyData -> keyData.popup.symbol = baseKeys[0].getOrNull(index)?.label }
baseKeys[0] = numberRow
baseKeys[0] = numberRow.toMutableList()
} else if (!params.mId.mNumberRowEnabled && params.mId.isAlphabetKeyboard && infos.numbersOnTopRow) {
if (baseKeys[0].any { it.popup.main != null || !it.popup.relevant.isNullOrEmpty() } // first row of baseKeys has any layout popup key
&& params.mPopupKeyLabelSources.let {
@ -356,15 +330,9 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
}
}
private fun addSymbolPopupKeys(baseKeys: MutableList<List<KeyData>>) {
val layoutName = getLayoutFileName(params, context, overrideElementId = KeyboardId.ELEMENT_SYMBOLS)
val layout = if (layoutName.startsWith(CUSTOM_LAYOUT_PREFIX)) {
val parser = if (layoutName.endsWith("json")) JsonKeyboardParser(params, context)
else SimpleKeyboardParser(params, context, false)
parser.parseCoreLayout(getLayoutFile(layoutName, context).readText())
} else {
SimpleKeyboardParser(params, context, false).parseCoreLayout(context.readAssetsLayoutFile("$layoutName.txt"))
}
private fun addSymbolPopupKeys(baseKeys: MutableList<MutableList<KeyData>>) {
val layoutName = if (params.mId.locale.script() == ScriptUtils.SCRIPT_ARABIC) LAYOUT_SYMBOLS_ARABIC else LAYOUT_SYMBOLS
val layout = RawKeyboardParser.parseLayout(layoutName, params, context)
layout.forEachIndexed { i, row ->
val baseRow = baseKeys.getOrNull(i) ?: return@forEachIndexed
row.forEachIndexed { j, key ->
@ -492,49 +460,6 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
companion object {
private const val TAG = "KeyboardParser"
// todo: this is somewhat awkward and could be re-organized
// simple and json parser should just parse the core layout
// adding extra keys should be done in KeyboardParser
fun parseLayout(params: KeyboardParams, context: Context): ArrayList<ArrayList<KeyParams>> {
val layoutName = getLayoutFileName(params, context)
if (layoutName.startsWith(CUSTOM_LAYOUT_PREFIX)) {
val parser = if (layoutName.endsWith("json")) JsonKeyboardParser(params, context)
else SimpleKeyboardParser(params, context)
return parser.parseLayoutString(getLayoutFile(layoutName, context).readText())
}
val layoutFileNames = context.assets.list("layouts")!!
if (layoutFileNames.contains("$layoutName.json")) {
return JsonKeyboardParser(params, context).parseLayoutString(context.readAssetsLayoutFile("$layoutName.json"))
}
if (layoutFileNames.contains("$layoutName.txt")) {
return SimpleKeyboardParser(params, context).parseLayoutString(context.readAssetsLayoutFile("$layoutName.txt"))
}
throw IllegalStateException("can't parse layout $layoutName with id ${params.mId} and elementId ${params.mId.mElementId}")
}
private fun Context.readAssetsLayoutFile(name: String) = assets.open("layouts${File.separator}$name").reader().readText()
private fun getLayoutFileName(params: KeyboardParams, context: Context, overrideElementId: Int? = null): String {
var checkForCustom = true
val layoutName = when (overrideElementId ?: params.mId.mElementId) {
KeyboardId.ELEMENT_SYMBOLS -> if (params.mId.locale.script() == ScriptUtils.SCRIPT_ARABIC) LAYOUT_SYMBOLS_ARABIC else LAYOUT_SYMBOLS
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> LAYOUT_SYMBOLS_SHIFTED
KeyboardId.ELEMENT_NUMPAD -> if (context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
LAYOUT_NUMPAD_LANDSCAPE
else
LAYOUT_NUMPAD
KeyboardId.ELEMENT_NUMBER -> LAYOUT_NUMBER
KeyboardId.ELEMENT_PHONE -> LAYOUT_PHONE
KeyboardId.ELEMENT_PHONE_SYMBOLS -> LAYOUT_PHONE_SYMBOLS
else -> {
checkForCustom = false // "custom" is already in keyboardLayoutSetName
params.mId.mSubtype.keyboardLayoutSetName.substringBeforeLast("+")
}
}
return if (checkForCustom) Settings.readLayoutName(layoutName, context)
else layoutName
}
// todo:
// layoutInfos should be stored in method.xml (imeSubtypeExtraValue)
// or somewhere else... some replacement for keyboard_layout_set xml maybe
@ -619,3 +544,7 @@ const val LAYOUT_NUMPAD_LANDSCAPE = "numpad_landscape"
const val LAYOUT_NUMBER = "number"
const val LAYOUT_PHONE = "phone"
const val LAYOUT_PHONE_SYMBOLS = "phone_symbols"
const val FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED = "functional_keys_symbols_shifted"
const val FUNCTIONAL_LAYOUT_SYMBOLS = "functional_keys_symbols"
const val FUNCTIONAL_LAYOUT = "functional_keys"
const val FUNCTIONAL_LAYOUT_TABLET = "functional_keys_tablet"

View file

@ -0,0 +1,199 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.keyboard.internal.keyboard_parser
import android.content.Context
import android.content.res.Configuration
import helium314.keyboard.keyboard.KeyboardId
import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.AbstractKeyData
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.AutoTextKeyData
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.CaseSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.CharWidthSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KanaSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyData
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.LayoutDirectionSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.MultiTextKeyData
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.ShiftStateSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.TextKeyData
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.VariationSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.toTextKey
import helium314.keyboard.latin.common.splitOnWhitespace
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.CUSTOM_LAYOUT_PREFIX
import helium314.keyboard.latin.utils.ScriptUtils
import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.getCustomLayoutFile
import helium314.keyboard.latin.utils.getCustomLayoutsDir
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import java.io.File
object RawKeyboardParser {
private val rawLayoutCache = hashMapOf<String, (KeyboardParams) -> MutableList<MutableList<KeyData>>>()
val symbolAndNumberLayouts = listOf(LAYOUT_SYMBOLS, LAYOUT_SYMBOLS_SHIFTED, LAYOUT_SYMBOLS_ARABIC,
LAYOUT_NUMBER, LAYOUT_NUMPAD, LAYOUT_NUMPAD_LANDSCAPE, LAYOUT_PHONE, LAYOUT_PHONE_SYMBOLS)
// todo: cache is by layout name, this is inefficient for functional keys by default
fun clearCache() = rawLayoutCache.clear()
fun parseLayout(params: KeyboardParams, context: Context, isFunctional: Boolean = false): MutableList<MutableList<KeyData>> {
val layoutName = if (isFunctional) {
if (!params.mId.isAlphaOrSymbolKeyboard) return mutableListOf(mutableListOf())
else getFunctionalLayoutName(params)
} else {
getLayoutName(params, context)
}
return rawLayoutCache.getOrPut(layoutName) {
createCacheLambda(layoutName, context)
}(params)
}
fun parseLayout(layoutName: String, params: KeyboardParams, context: Context): MutableList<MutableList<KeyData>> {
return rawLayoutCache.getOrPut(layoutName) {
createCacheLambda(layoutName, context)
}(params)
}
/**
* Parse 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] layoutPopupKeys, no choice of which to use or which hint is provided
*/
fun parseJsonString(layoutText: String): List<List<AbstractKeyData>> = florisJsonConfig.decodeFromString(layoutText)
/** Parse simple layouts, defined only as rows of (normal) keys with popup keys. */
fun parseSimpleString(layoutText: String): List<List<KeyData>> {
val rowStrings = layoutText.replace("\r\n", "\n").split("\\n\\s*\\n".toRegex()).filter { it.isNotBlank() }
return rowStrings.map { row ->
row.split("\n").mapNotNull { parseKey(it) }
}
}
private fun parseKey(key: String): KeyData? {
if (key.isBlank()) return null
val split = key.splitOnWhitespace()
return if (split.size == 1) split.first().toTextKey()
else split.first().toTextKey(split.drop(1))
}
private fun createCacheLambda(layoutName: String, context: Context): (KeyboardParams) -> MutableList<MutableList<KeyData>> {
val layoutFileName = getLayoutFileName(layoutName, context)
val layoutText = if (layoutFileName.startsWith(CUSTOM_LAYOUT_PREFIX)) getCustomLayoutFile(layoutFileName, context).readText()
else context.assets.open("layouts${File.separator}$layoutFileName").reader().use { it.readText() }
if (layoutFileName.endsWith(".json")) {
val florisKeyData = parseJsonString(layoutText)
return { params ->
florisKeyData.mapTo(mutableListOf()) { row ->
row.mapNotNullTo(mutableListOf()) { it.compute(params) }
}
}
} else {
val simpleKeyData = parseSimpleString(layoutText)
return { params ->
simpleKeyData.mapIndexedTo(mutableListOf()) { i, row ->
val newRow = row.toMutableList()
val extraKeys = params.mId.mSubtype.keyboardLayoutSetName.endsWith("+") && params.mId.isAlphabetKeyboard
if (extraKeys)
params.mLocaleKeyboardInfos.getExtraKeys(i+1)?.let { newRow.addAll(it) }
println("${newRow.size}: ${newRow.map { it.label }}")
newRow
}
}
}
}
private fun getLayoutName(params: KeyboardParams, context: Context) = when (params.mId.mElementId) {
KeyboardId.ELEMENT_SYMBOLS -> if (params.mId.locale.script() == ScriptUtils.SCRIPT_ARABIC) LAYOUT_SYMBOLS_ARABIC else LAYOUT_SYMBOLS
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> LAYOUT_SYMBOLS_SHIFTED
KeyboardId.ELEMENT_NUMPAD -> if (context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
LAYOUT_NUMPAD_LANDSCAPE
else
LAYOUT_NUMPAD
KeyboardId.ELEMENT_NUMBER -> LAYOUT_NUMBER
KeyboardId.ELEMENT_PHONE -> LAYOUT_PHONE
KeyboardId.ELEMENT_PHONE_SYMBOLS -> LAYOUT_PHONE_SYMBOLS
else -> params.mId.mSubtype.keyboardLayoutSetName.substringBeforeLast("+")
}
private fun getFunctionalLayoutName(params: KeyboardParams) = when (params.mId.mElementId) {
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED
KeyboardId.ELEMENT_SYMBOLS -> FUNCTIONAL_LAYOUT_SYMBOLS
else -> if (Settings.getInstance().isTablet) FUNCTIONAL_LAYOUT_TABLET else FUNCTIONAL_LAYOUT
}
/** returns the file name matching the layout name, making sure the file exists (falling back to qwerty.txt) */
private fun getLayoutFileName(layoutName: String, context: Context): String {
val customFiles = getCustomLayoutsDir(context).list()
if (layoutName.startsWith(CUSTOM_LAYOUT_PREFIX)) {
return if (customFiles?.contains(layoutName) == true) layoutName
else "qwerty.txt" // fallback
}
val assetsFiles by lazy { context.assets.list("layouts")!! }
if (layoutName.startsWith("functional")) {
// return custom match if we have one
val customMatch = customFiles?.firstOrNull { it.startsWith("$CUSTOM_LAYOUT_PREFIX$layoutName.") }
if (customMatch != null) return customMatch
if (layoutName == FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED) {
// no custom symbols shifted layout, try custom symbols layout
val customSymbols = customFiles?.firstOrNull { it.startsWith("$CUSTOM_LAYOUT_PREFIX$FUNCTIONAL_LAYOUT_SYMBOLS.") }
if (customSymbols != null) return customSymbols
}
// no custom symbols layout, try custom functional layout
if (Settings.getInstance().isTablet) {
val customTablet = customFiles?.firstOrNull { it.startsWith("$CUSTOM_LAYOUT_PREFIX$FUNCTIONAL_LAYOUT_TABLET.") }
if (customTablet != null) return customTablet
}
val customFunctional = customFiles?.firstOrNull { it.startsWith("$CUSTOM_LAYOUT_PREFIX$FUNCTIONAL_LAYOUT.") }
if (customFunctional != null) return customFunctional
// no custom functional layout, use the default functional layout
return if (Settings.getInstance().isTablet) "$FUNCTIONAL_LAYOUT_TABLET.json"
else "$FUNCTIONAL_LAYOUT.json"
}
return if (layoutName in symbolAndNumberLayouts) {
customFiles?.firstOrNull { it.startsWith("$CUSTOM_LAYOUT_PREFIX$layoutName.")}
?: assetsFiles.first { it.startsWith(layoutName) }
} else {
// can't be custom layout, so it must be in assets
val searchName = layoutName.substringBeforeLast("+") // consider there are layouts ending in "+" for adding extra keys
assetsFiles.firstOrNull { it.startsWith(searchName) } ?: "qwerty.txt" // in case it was removed
}
}
/*
* Copyright (C) 2021 Patrick Goldinger
* modified
* SPDX-License-Identifier: Apache-2.0
*/
private val florisJsonConfig = Json {
classDiscriminator = "$"
encodeDefaults = true
ignoreUnknownKeys = true
isLenient = true
serializersModule = SerializersModule {
polymorphic(AbstractKeyData::class) {
subclass(TextKeyData::class, TextKeyData.serializer())
subclass(AutoTextKeyData::class, AutoTextKeyData.serializer())
subclass(MultiTextKeyData::class, MultiTextKeyData.serializer())
subclass(CaseSelector::class, CaseSelector.serializer())
subclass(ShiftStateSelector::class, ShiftStateSelector.serializer())
subclass(VariationSelector::class, VariationSelector.serializer())
subclass(LayoutDirectionSelector::class, LayoutDirectionSelector.serializer())
subclass(CharWidthSelector::class, CharWidthSelector.serializer())
subclass(KanaSelector::class, KanaSelector.serializer())
defaultDeserializer { TextKeyData.serializer() }
}
polymorphic(KeyData::class) {
subclass(TextKeyData::class, TextKeyData.serializer())
subclass(AutoTextKeyData::class, AutoTextKeyData.serializer())
subclass(MultiTextKeyData::class, MultiTextKeyData.serializer())
defaultDeserializer { TextKeyData.serializer() }
}
}
}
}

View file

@ -1,48 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.keyboard.internal.keyboard_parser
import android.content.Context
import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyData
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.toTextKey
import helium314.keyboard.latin.common.splitOnWhitespace
/**
* Parser for simple layouts, defined only as rows of (normal) keys with popup keys.
* 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. Popup keys on period and comma get
* merged with defaults.
*/
class SimpleKeyboardParser(
private val params: KeyboardParams,
context: Context,
private val addExtraKeys: Boolean = params.mId.mSubtype.keyboardLayoutSetName.endsWith("+") && params.mId.isAlphabetKeyboard
) : KeyboardParser(params, context) {
override fun parseCoreLayout(layoutContent: String): MutableList<List<KeyData>> {
val rowStrings = layoutContent.replace("\r\n", "\n").split("\\n\\s*\\n".toRegex())
return rowStrings.mapIndexedNotNullTo(mutableListOf()) { i, row ->
if (row.isBlank()) return@mapIndexedNotNullTo null
if (addExtraKeys)
getExtraKeys(i)?.let { parseRow(row) + it } ?: parseRow(row)
else
parseRow(row)
}
}
private fun parseRow(row: String): List<KeyData> =
row.split("\n").mapNotNull {
if (it.isBlank()) null
else parseKey(it)
}
private fun getExtraKeys(rowIndex: Int) = params.mLocaleKeyboardInfos.getExtraKeys(rowIndex + 1)
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))
}
}

View file

@ -11,6 +11,7 @@ import helium314.keyboard.latin.settings.USER_DICTIONARY_SUFFIX
import helium314.keyboard.latin.utils.CUSTOM_LAYOUT_PREFIX
import helium314.keyboard.latin.utils.DeviceProtectedUtils
import helium314.keyboard.latin.utils.DictionaryInfoUtils
import helium314.keyboard.latin.utils.getCustomLayoutsDir
import helium314.keyboard.latin.utils.upgradeToolbarPrefs
import java.io.File
@ -50,7 +51,7 @@ fun checkVersionUpgrade(context: Context) {
if (oldVersion == 0) // new install or restoring settings from old app name
upgradesWhenComingFromOldAppName(context)
if (oldVersion <= 1000) { // upgrade old custom layouts name
val layoutsDir = Settings.getLayoutsDir(context)
val layoutsDir = getCustomLayoutsDir(context)
val oldShiftSymbolsFile = File(layoutsDir, "${CUSTOM_LAYOUT_PREFIX}shift_symbols")
if (oldShiftSymbolsFile.exists()) {
oldShiftSymbolsFile.renameTo(File(layoutsDir, "${CUSTOM_LAYOUT_PREFIX}symbols_shifted"))
@ -79,7 +80,7 @@ fun checkVersionUpgrade(context: Context) {
private fun upgradesWhenComingFromOldAppName(context: Context) {
// move layout files
try {
val layoutsDir = Settings.getLayoutsDir(context)
val layoutsDir = getCustomLayoutsDir(context)
File(context.filesDir, "layouts").listFiles()?.forEach {
it.copyTo(File(layoutsDir, it.name), true)
it.delete()

View file

@ -32,6 +32,7 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_PHONE_SYMBOLS
import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_SYMBOLS
import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_SYMBOLS_ARABIC
import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_SYMBOLS_SHIFTED
import helium314.keyboard.keyboard.internal.keyboard_parser.RawKeyboardParser
import helium314.keyboard.latin.AudioAndHapticFeedbackManager
import helium314.keyboard.latin.BuildConfig
import helium314.keyboard.latin.R
@ -46,6 +47,7 @@ import helium314.keyboard.latin.utils.DeviceProtectedUtils
import helium314.keyboard.latin.utils.ExecutorUtils
import helium314.keyboard.latin.utils.JniUtils
import helium314.keyboard.latin.utils.editCustomLayout
import helium314.keyboard.latin.utils.getCustomLayoutsDir
import helium314.keyboard.latin.utils.getStringResourceOrName
import helium314.keyboard.latin.utils.infoDialog
import helium314.keyboard.latin.utils.reloadEnabledSubtypes
@ -157,27 +159,27 @@ class AdvancedSettingsFragment : SubScreenFragment() {
}
private fun showCustomizeLayoutsDialog() {
val layouts = listOf(LAYOUT_SYMBOLS, LAYOUT_SYMBOLS_SHIFTED, LAYOUT_SYMBOLS_ARABIC, LAYOUT_NUMBER, LAYOUT_NUMPAD, LAYOUT_NUMPAD_LANDSCAPE, LAYOUT_PHONE, LAYOUT_PHONE_SYMBOLS)
val layoutNames = layouts.map { it.getStringResourceOrName("layout_", requireContext()) }.toTypedArray()
val layoutNames = RawKeyboardParser.symbolAndNumberLayouts.map { it.getStringResourceOrName("layout_", requireContext()) }.toTypedArray()
AlertDialog.Builder(requireContext())
.setTitle(R.string.customize_symbols_number_layouts)
.setItems(layoutNames) { di, i ->
di.dismiss()
customizeLayout(layouts[i])
customizeSymbolNumberLayout(RawKeyboardParser.symbolAndNumberLayouts[i])
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun customizeLayout(layout: String) {
val customLayoutName = Settings.readLayoutName(layout, context).takeIf { it.startsWith(CUSTOM_LAYOUT_PREFIX) }
private fun customizeSymbolNumberLayout(layoutName: String) {
val customLayoutName = getCustomLayoutsDir(requireContext()).list()
?.firstOrNull { it.startsWith("$CUSTOM_LAYOUT_PREFIX$layoutName.") }
val originalLayout = if (customLayoutName != null) null
else {
requireContext().assets.list("layouts")?.firstOrNull { it.startsWith("$layout.") }
requireContext().assets.list("layouts")?.firstOrNull { it.startsWith("$layoutName.") }
?.let { requireContext().assets.open("layouts" + File.separator + it).reader().readText() }
}
val displayName = layout.getStringResourceOrName("layout_", requireContext())
editCustomLayout(customLayoutName ?: "$CUSTOM_LAYOUT_PREFIX$layout.txt", requireContext(), originalLayout, displayName)
val displayName = layoutName.getStringResourceOrName("layout_", requireContext())
editCustomLayout(customLayoutName ?: "$CUSTOM_LAYOUT_PREFIX$layoutName.txt", requireContext(), originalLayout, displayName)
}
@SuppressLint("ApplySharedPref")

View file

@ -36,7 +36,6 @@ import helium314.keyboard.latin.common.Colors;
import helium314.keyboard.latin.common.LocaleUtils;
import helium314.keyboard.latin.utils.AdditionalSubtypeUtils;
import helium314.keyboard.latin.utils.ColorUtilKt;
import helium314.keyboard.latin.utils.CustomLayoutUtilsKt;
import helium314.keyboard.latin.utils.DeviceProtectedUtils;
import helium314.keyboard.latin.utils.JniUtils;
import helium314.keyboard.latin.utils.Log;
@ -548,22 +547,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
};
}
/** @return custom layout name if there is one for the given layout, else returns "layout" */
public static String readLayoutName(final String layout, final Context context) {
String[] layouts = getLayoutsDir(context).list();
if (layouts != null) {
for (String name : layouts) {
if (name.startsWith(CustomLayoutUtilsKt.CUSTOM_LAYOUT_PREFIX + layout + "."))
return name;
}
}
return layout;
}
public static File getLayoutsDir(final Context context) {
return new File(DeviceProtectedUtils.getFilesDir(context), "layouts");
}
@Nullable public static Drawable readUserBackgroundImage(final Context context, final boolean night) {
if (night && sCachedBackgroundNight != null) return sCachedBackgroundNight;
if (!night && sCachedBackgroundDay != null) return sCachedBackgroundDay;

View file

@ -13,9 +13,8 @@ import helium314.keyboard.keyboard.KeyboardId
import helium314.keyboard.keyboard.KeyboardLayoutSet
import helium314.keyboard.keyboard.KeyboardSwitcher
import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.keyboard.internal.keyboard_parser.JsonKeyboardParser
import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_NORMAL
import helium314.keyboard.keyboard.internal.keyboard_parser.SimpleKeyboardParser
import helium314.keyboard.keyboard.internal.keyboard_parser.RawKeyboardParser
import helium314.keyboard.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams
import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.FileUtils
@ -65,7 +64,7 @@ fun loadCustomLayout(layoutContent: String, layoutName: String, languageTag: Str
.setPositiveButton(android.R.string.ok) { _, _ ->
// name must be encoded to avoid issues with validity of subtype extra string or file name
name = "$CUSTOM_LAYOUT_PREFIX${languageTag}.${encodeBase36(name)}.${if (isJson) "json" else "txt"}"
val file = getLayoutFile(name, context)
val file = getCustomLayoutFile(name, context)
if (file.exists())
file.delete()
file.parentFile?.mkdir()
@ -81,21 +80,21 @@ private fun checkLayout(layoutContent: String, context: Context): Boolean? {
params.mPopupKeyTypes.add(POPUP_KEYS_LAYOUT)
addLocaleKeyTextsToParams(context, params, POPUP_KEYS_NORMAL)
try {
val keys = JsonKeyboardParser(params, context).parseLayoutString(layoutContent)
val keys = RawKeyboardParser.parseJsonString(layoutContent).map { it.mapNotNull { it.compute(params)?.toKeyParams(params) } }
if (!checkKeys(keys))
return null
return true
} catch (e: Exception) { Log.w(TAG, "error parsing custom json layout", e) }
try {
val keys = SimpleKeyboardParser(params, context).parseLayoutString(layoutContent)
val keys = RawKeyboardParser.parseSimpleString(layoutContent).map { it.map { it.toKeyParams(params) } }
if (!checkKeys(keys))
return null
return false
} catch (e: Exception) { Log.w(TAG, "error parsing custom simple layout", e) }
if (layoutContent.startsWith("[")) {
// layout can't be loaded, assume it's json -> try json layout again because of error message readout
// layout can't be loaded, assume it's json -> load json layout again because the error message shown to the user is from the most recent error
try {
JsonKeyboardParser(params, context).parseLayoutString(layoutContent)
RawKeyboardParser.parseJsonString(layoutContent).map { it.mapNotNull { it.compute(params)?.toKeyParams(params) } }
} catch (e: Exception) { Log.w(TAG, "error parsing custom json layout", e) }
}
return null
@ -129,8 +128,10 @@ private fun checkKeys(keys: List<List<Key.KeyParams>>): Boolean {
return true
}
fun getLayoutFile(layoutName: String, context: Context) =
File(Settings.getLayoutsDir(context), layoutName)
fun getCustomLayoutFile(layoutName: String, context: Context) =
File(getCustomLayoutsDir(context), layoutName)
fun getCustomLayoutsDir(context: Context) = File(DeviceProtectedUtils.getFilesDir(context), "layouts")
// undo the name changes in loadCustomLayout when clicking ok
fun getLayoutDisplayName(layoutName: String) =
@ -141,11 +142,11 @@ fun getLayoutDisplayName(layoutName: String) =
}
fun removeCustomLayoutFile(layoutName: String, context: Context) {
getLayoutFile(layoutName, context).delete()
getCustomLayoutFile(layoutName, context).delete()
}
fun editCustomLayout(layoutName: String, context: Context, startContent: String? = null, displayName: CharSequence? = null) {
val file = getLayoutFile(layoutName, context)
val file = getCustomLayoutFile(layoutName, context)
val editText = EditText(context).apply {
setText(startContent ?: file.readText())
}

View file

@ -245,7 +245,7 @@ private fun loadResourceSubtypes(resources: Resources) {
private fun removeInvalidCustomSubtypes(context: Context) {
val prefs = DeviceProtectedUtils.getSharedPreferences(context)
val additionalSubtypes = Settings.readPrefAdditionalSubtypes(prefs, context.resources).split(";")
val customSubtypeFiles by lazy { Settings.getLayoutsDir(context).list() }
val customSubtypeFiles by lazy { getCustomLayoutsDir(context).list() }
val subtypesToRemove = mutableListOf<String>()
additionalSubtypes.forEach {
val name = it.substringAfter(":").substringBefore(":")

View file

@ -12,13 +12,10 @@ import helium314.keyboard.keyboard.internal.KeyboardBuilder
import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.keyboard.internal.TouchPositionCorrection
import helium314.keyboard.keyboard.internal.UniqueKeysCache
import helium314.keyboard.keyboard.internal.keyboard_parser.JsonKeyboardParser
import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_NORMAL
import helium314.keyboard.keyboard.internal.keyboard_parser.SimpleKeyboardParser
import helium314.keyboard.keyboard.internal.keyboard_parser.RawKeyboardParser
import helium314.keyboard.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyType
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.MultiTextKeyData
import helium314.keyboard.latin.LatinIME
import helium314.keyboard.latin.RichInputMethodSubtype
import helium314.keyboard.latin.utils.AdditionalSubtypeUtils.createEmojiCapableAdditionalSubtype
@ -120,7 +117,7 @@ f""", // no newline at the end
val wantedKeyLabels = listOf(listOf("a", "b", "c"), listOf("d", "e", "f"))
layoutStrings.forEachIndexed { i, layout ->
println(i)
val keyLabels = SimpleKeyboardParser(params, latinIME).parseCoreLayout(layout).map { it.map { it.label } }
val keyLabels = RawKeyboardParser.parseSimpleString(layout).map { it.map { it.toKeyParams(params).mLabel } }
assertEquals(wantedKeyLabels, keyLabels)
}
}
@ -268,7 +265,7 @@ f""", // no newline at the end
]
]
""".trimIndent()
val keys = JsonKeyboardParser(params, latinIME).parseCoreLayout(layoutString)
val keys = RawKeyboardParser.parseJsonString(layoutString).map { it.mapNotNull { it.compute(params) } }
keys.first().forEachIndexed { index, keyData ->
println("data: key ${keyData.label}: code ${keyData.code}, popups: ${keyData.popup.getPopupKeyLabels(params)}")
val keyParams = keyData.toKeyParams(params)
@ -298,13 +295,13 @@ f""", // no newline at the end
val editorInfo = EditorInfo()
val subtype = createEmojiCapableAdditionalSubtype(Locale.GERMANY, "qwertz+", true)
val (kb, keys) = buildKeyboard(editorInfo, subtype, KeyboardId.ELEMENT_ALPHABET)
assertEquals(keys[0].size, 11)
assertEquals(keys[1].size, 11)
assertEquals(keys[2].size, 10)
assertEquals(11, keys[0].size)
assertEquals(11, keys[1].size)
assertEquals(10, keys[2].size)
val (kb2, keys2) = buildKeyboard(editorInfo, subtype, KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
assertEquals(keys2[0].size, 11)
assertEquals(keys2[1].size, 11)
assertEquals(keys2[2].size, 10)
assertEquals(11, keys2[0].size)
assertEquals(11, keys2[1].size)
assertEquals(10, keys2[2].size)
}
@Test fun `popup key count does not depend on shift for (for simple layout)`() {