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.UniqueKeysCache;
import helium314.keyboard.keyboard.internal.keyboard_parser.LocaleKeyboardInfos; import helium314.keyboard.keyboard.internal.keyboard_parser.LocaleKeyboardInfos;
import helium314.keyboard.keyboard.internal.keyboard_parser.LocaleKeyboardInfosKt; 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.RichInputMethodSubtype;
import helium314.keyboard.latin.utils.InputTypeUtils; import helium314.keyboard.latin.utils.InputTypeUtils;
import helium314.keyboard.latin.utils.Log; import helium314.keyboard.latin.utils.Log;
@ -99,6 +100,7 @@ public final class KeyboardLayoutSet {
private static void clearKeyboardCache() { private static void clearKeyboardCache() {
sKeyboardCache.clear(); sKeyboardCache.clear();
sUniqueKeysCache.clear(); sUniqueKeysCache.clear();
RawKeyboardParser.INSTANCE.clearCache();
} }
KeyboardLayoutSet(final Context context, @NonNull final Params params) { 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) mParams.mPopupKeyTypes.addAll(sv.mPopupKeyTypes)
// add label source only if popup key type enabled // add label source only if popup key type enabled
sv.mPopupKeyLabelSources.forEach { if (it in sv.mPopupKeyTypes) mParams.mPopupKeyLabelSources.add(it) } sv.mPopupKeyLabelSources.forEach { if (it in sv.mPopupKeyTypes) mParams.mPopupKeyLabelSources.add(it) }
keysInRows = KeyboardParser.parseLayout(mParams, mContext) keysInRows = KeyboardParser(mParams, mContext).parseLayout()
determineAbsoluteValues() determineAbsoluteValues()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "error parsing layout $id ${id.mElementId}", e) 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.define.DebugFlags
import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.spellcheck.AndroidSpellCheckerService 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.InputTypeUtils
import helium314.keyboard.latin.utils.POPUP_KEYS_LAYOUT import helium314.keyboard.latin.utils.POPUP_KEYS_LAYOUT
import helium314.keyboard.latin.utils.POPUP_KEYS_NUMBER import helium314.keyboard.latin.utils.POPUP_KEYS_NUMBER
import helium314.keyboard.latin.utils.ScriptUtils import helium314.keyboard.latin.utils.ScriptUtils
import helium314.keyboard.latin.utils.ScriptUtils.script import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.getLayoutFile
import helium314.keyboard.latin.utils.removeFirst import helium314.keyboard.latin.utils.removeFirst
import helium314.keyboard.latin.utils.replaceFirst import helium314.keyboard.latin.utils.replaceFirst
import helium314.keyboard.latin.utils.runInLocale import helium314.keyboard.latin.utils.runInLocale
import helium314.keyboard.latin.utils.splitAt import helium314.keyboard.latin.utils.splitAt
import helium314.keyboard.latin.utils.sumOf import helium314.keyboard.latin.utils.sumOf
import java.io.File
/** /**
* Abstract parser class that handles creation of keyboard from [KeyData] arranged in rows, * 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 * By default, all normal keys have the same width and flags, which may cause issues with the
* requirements of certain non-latin languages. * 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 infos = layoutInfos(params)
private val defaultLabelFlags = when { private val defaultLabelFlags = when {
params.mId.isAlphabetKeyboard -> params.mLocaleKeyboardInfos.labelFlags params.mId.isAlphabetKeyboard -> params.mLocaleKeyboardInfos.labelFlags
@ -57,10 +54,7 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
else -> 0 else -> 0
} }
abstract fun parseCoreLayout(layoutContent: String): MutableList<List<KeyData>> fun parseLayout(): ArrayList<ArrayList<KeyParams>> {
// this thing does too much... make it more understandable after everything is implemented
fun parseLayoutString(layoutContent: String): ArrayList<ArrayList<KeyParams>> {
params.readAttributes(context, null) params.readAttributes(context, null)
params.mProximityCharsCorrectionEnabled = infos.enableProximityCharsCorrection params.mProximityCharsCorrectionEnabled = infos.enableProximityCharsCorrection
if (infos.touchPositionCorrectionData == null) // need to set correctly, as it's not properly done in readAttributes with attr = null 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 else
params.mTouchPositionCorrection.load(context.resources.getStringArray(infos.touchPositionCorrectionData)) params.mTouchPositionCorrection.load(context.resources.getStringArray(infos.touchPositionCorrectionData))
val baseKeys = parseCoreLayout(layoutContent) val baseKeys = RawKeyboardParser.parseLayout(params, context)
val keysInRows = createRows(baseKeys) val keysInRows = createRows(baseKeys)
// rescale height if we have anything but the usual 4 rows // rescale height if we have anything but the usual 4 rows
val heightRescale = if (keysInRows.size != 4) 4f / keysInRows.size else 1f 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 return keysInRows
} }
// this should be ready for customizable functional layouts, but needs cleanup private fun createRows(baseKeys: MutableList<MutableList<KeyData>>): ArrayList<ArrayList<KeyParams>> {
// 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>> {
// add padding for number layouts in landscape mode (maybe do it some other way later) // 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 if (params.mId.isNumberLayout && params.mId.mElementId != KeyboardId.ELEMENT_NUMPAD
&& context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { && context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
@ -112,14 +87,14 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
addSymbolPopupKeys(baseKeys) addSymbolPopupKeys(baseKeys)
if (params.mId.isAlphaOrSymbolKeyboard && params.mId.mNumberRowEnabled) if (params.mId.isAlphaOrSymbolKeyboard && params.mId.mNumberRowEnabled)
baseKeys.add(0, params.mLocaleKeyboardInfos.getNumberRow() 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) adjustBottomFunctionalRowAndBaseKeys(allFunctionalKeys, baseKeys)
if (allFunctionalKeys.none { it.singleOrNull()?.isKeyPlaceholder() == true }) if (allFunctionalKeys.none { it.singleOrNull()?.isKeyPlaceholder() == true })
// add a placeholder so splitAt does what we really want // 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 } 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 * 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 * 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>>) { private fun adjustBottomFunctionalRowAndBaseKeys(allFunctionalKeys: MutableList<MutableList<KeyData>>, baseKeys: MutableList<MutableList<KeyData>>) {
val functionalKeysBottom = allFunctionalKeys.lastOrNull()?.toMutableList() ?: return val functionalKeysBottom = allFunctionalKeys.lastOrNull() ?: return
if (!params.mId.isAlphaOrSymbolKeyboard || functionalKeysBottom.isEmpty() || functionalKeysBottom.any { it.isKeyPlaceholder() }) if (!params.mId.isAlphaOrSymbolKeyboard || functionalKeysBottom.isEmpty() || functionalKeysBottom.any { it.isKeyPlaceholder() })
return return
if (true /* Settings.getInstance().current.mSingleFunctionalLayout */) { // todo with the customizable functional layout 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) functionalKeysBottom.add(spaceIndex, key1)
} }
} }
allFunctionalKeys[allFunctionalKeys.lastIndex] = functionalKeysBottom baseKeys.add(mutableListOf())
baseKeys.add(emptyList())
} }
// ideally we would get all functional keys in a nice list of pairs from the start, but at least it works... // 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) { if (!params.mId.mNumberRowEnabled && params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) {
// replace first symbols row with number row, but use the labels as popupKeys // replace first symbols row with number row, but use the labels as popupKeys
val numberRow = params.mLocaleKeyboardInfos.getNumberRow() val numberRow = params.mLocaleKeyboardInfos.getNumberRow()
numberRow.forEachIndexed { index, keyData -> keyData.popup.symbol = baseKeys[0].getOrNull(index)?.label } 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) { } 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 if (baseKeys[0].any { it.popup.main != null || !it.popup.relevant.isNullOrEmpty() } // first row of baseKeys has any layout popup key
&& params.mPopupKeyLabelSources.let { && params.mPopupKeyLabelSources.let {
@ -356,15 +330,9 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
} }
} }
private fun addSymbolPopupKeys(baseKeys: MutableList<List<KeyData>>) { private fun addSymbolPopupKeys(baseKeys: MutableList<MutableList<KeyData>>) {
val layoutName = getLayoutFileName(params, context, overrideElementId = KeyboardId.ELEMENT_SYMBOLS) val layoutName = if (params.mId.locale.script() == ScriptUtils.SCRIPT_ARABIC) LAYOUT_SYMBOLS_ARABIC else LAYOUT_SYMBOLS
val layout = if (layoutName.startsWith(CUSTOM_LAYOUT_PREFIX)) { val layout = RawKeyboardParser.parseLayout(layoutName, params, context)
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"))
}
layout.forEachIndexed { i, row -> layout.forEachIndexed { i, row ->
val baseRow = baseKeys.getOrNull(i) ?: return@forEachIndexed val baseRow = baseKeys.getOrNull(i) ?: return@forEachIndexed
row.forEachIndexed { j, key -> row.forEachIndexed { j, key ->
@ -492,49 +460,6 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
companion object { companion object {
private const val TAG = "KeyboardParser" 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: // todo:
// layoutInfos should be stored in method.xml (imeSubtypeExtraValue) // layoutInfos should be stored in method.xml (imeSubtypeExtraValue)
// or somewhere else... some replacement for keyboard_layout_set xml maybe // 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_NUMBER = "number"
const val LAYOUT_PHONE = "phone" const val LAYOUT_PHONE = "phone"
const val LAYOUT_PHONE_SYMBOLS = "phone_symbols" 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.CUSTOM_LAYOUT_PREFIX
import helium314.keyboard.latin.utils.DeviceProtectedUtils import helium314.keyboard.latin.utils.DeviceProtectedUtils
import helium314.keyboard.latin.utils.DictionaryInfoUtils import helium314.keyboard.latin.utils.DictionaryInfoUtils
import helium314.keyboard.latin.utils.getCustomLayoutsDir
import helium314.keyboard.latin.utils.upgradeToolbarPrefs import helium314.keyboard.latin.utils.upgradeToolbarPrefs
import java.io.File import java.io.File
@ -50,7 +51,7 @@ fun checkVersionUpgrade(context: Context) {
if (oldVersion == 0) // new install or restoring settings from old app name if (oldVersion == 0) // new install or restoring settings from old app name
upgradesWhenComingFromOldAppName(context) upgradesWhenComingFromOldAppName(context)
if (oldVersion <= 1000) { // upgrade old custom layouts name 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") val oldShiftSymbolsFile = File(layoutsDir, "${CUSTOM_LAYOUT_PREFIX}shift_symbols")
if (oldShiftSymbolsFile.exists()) { if (oldShiftSymbolsFile.exists()) {
oldShiftSymbolsFile.renameTo(File(layoutsDir, "${CUSTOM_LAYOUT_PREFIX}symbols_shifted")) oldShiftSymbolsFile.renameTo(File(layoutsDir, "${CUSTOM_LAYOUT_PREFIX}symbols_shifted"))
@ -79,7 +80,7 @@ fun checkVersionUpgrade(context: Context) {
private fun upgradesWhenComingFromOldAppName(context: Context) { private fun upgradesWhenComingFromOldAppName(context: Context) {
// move layout files // move layout files
try { try {
val layoutsDir = Settings.getLayoutsDir(context) val layoutsDir = getCustomLayoutsDir(context)
File(context.filesDir, "layouts").listFiles()?.forEach { File(context.filesDir, "layouts").listFiles()?.forEach {
it.copyTo(File(layoutsDir, it.name), true) it.copyTo(File(layoutsDir, it.name), true)
it.delete() 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
import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_SYMBOLS_ARABIC 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.LAYOUT_SYMBOLS_SHIFTED
import helium314.keyboard.keyboard.internal.keyboard_parser.RawKeyboardParser
import helium314.keyboard.latin.AudioAndHapticFeedbackManager import helium314.keyboard.latin.AudioAndHapticFeedbackManager
import helium314.keyboard.latin.BuildConfig import helium314.keyboard.latin.BuildConfig
import helium314.keyboard.latin.R 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.ExecutorUtils
import helium314.keyboard.latin.utils.JniUtils import helium314.keyboard.latin.utils.JniUtils
import helium314.keyboard.latin.utils.editCustomLayout import helium314.keyboard.latin.utils.editCustomLayout
import helium314.keyboard.latin.utils.getCustomLayoutsDir
import helium314.keyboard.latin.utils.getStringResourceOrName import helium314.keyboard.latin.utils.getStringResourceOrName
import helium314.keyboard.latin.utils.infoDialog import helium314.keyboard.latin.utils.infoDialog
import helium314.keyboard.latin.utils.reloadEnabledSubtypes import helium314.keyboard.latin.utils.reloadEnabledSubtypes
@ -157,27 +159,27 @@ class AdvancedSettingsFragment : SubScreenFragment() {
} }
private fun showCustomizeLayoutsDialog() { 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 = RawKeyboardParser.symbolAndNumberLayouts.map { it.getStringResourceOrName("layout_", requireContext()) }.toTypedArray()
val layoutNames = layouts.map { it.getStringResourceOrName("layout_", requireContext()) }.toTypedArray()
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setTitle(R.string.customize_symbols_number_layouts) .setTitle(R.string.customize_symbols_number_layouts)
.setItems(layoutNames) { di, i -> .setItems(layoutNames) { di, i ->
di.dismiss() di.dismiss()
customizeLayout(layouts[i]) customizeSymbolNumberLayout(RawKeyboardParser.symbolAndNumberLayouts[i])
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
} }
private fun customizeLayout(layout: String) { private fun customizeSymbolNumberLayout(layoutName: String) {
val customLayoutName = Settings.readLayoutName(layout, context).takeIf { it.startsWith(CUSTOM_LAYOUT_PREFIX) } val customLayoutName = getCustomLayoutsDir(requireContext()).list()
?.firstOrNull { it.startsWith("$CUSTOM_LAYOUT_PREFIX$layoutName.") }
val originalLayout = if (customLayoutName != null) null val originalLayout = if (customLayoutName != null) null
else { 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() } ?.let { requireContext().assets.open("layouts" + File.separator + it).reader().readText() }
} }
val displayName = layout.getStringResourceOrName("layout_", requireContext()) val displayName = layoutName.getStringResourceOrName("layout_", requireContext())
editCustomLayout(customLayoutName ?: "$CUSTOM_LAYOUT_PREFIX$layout.txt", requireContext(), originalLayout, displayName) editCustomLayout(customLayoutName ?: "$CUSTOM_LAYOUT_PREFIX$layoutName.txt", requireContext(), originalLayout, displayName)
} }
@SuppressLint("ApplySharedPref") @SuppressLint("ApplySharedPref")

View file

@ -36,7 +36,6 @@ import helium314.keyboard.latin.common.Colors;
import helium314.keyboard.latin.common.LocaleUtils; import helium314.keyboard.latin.common.LocaleUtils;
import helium314.keyboard.latin.utils.AdditionalSubtypeUtils; import helium314.keyboard.latin.utils.AdditionalSubtypeUtils;
import helium314.keyboard.latin.utils.ColorUtilKt; import helium314.keyboard.latin.utils.ColorUtilKt;
import helium314.keyboard.latin.utils.CustomLayoutUtilsKt;
import helium314.keyboard.latin.utils.DeviceProtectedUtils; import helium314.keyboard.latin.utils.DeviceProtectedUtils;
import helium314.keyboard.latin.utils.JniUtils; import helium314.keyboard.latin.utils.JniUtils;
import helium314.keyboard.latin.utils.Log; 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) { @Nullable public static Drawable readUserBackgroundImage(final Context context, final boolean night) {
if (night && sCachedBackgroundNight != null) return sCachedBackgroundNight; if (night && sCachedBackgroundNight != null) return sCachedBackgroundNight;
if (!night && sCachedBackgroundDay != null) return sCachedBackgroundDay; 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.KeyboardLayoutSet
import helium314.keyboard.keyboard.KeyboardSwitcher import helium314.keyboard.keyboard.KeyboardSwitcher
import helium314.keyboard.keyboard.internal.KeyboardParams 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.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.addLocaleKeyTextsToParams
import helium314.keyboard.latin.R import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.FileUtils import helium314.keyboard.latin.common.FileUtils
@ -65,7 +64,7 @@ fun loadCustomLayout(layoutContent: String, layoutName: String, languageTag: Str
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
// name must be encoded to avoid issues with validity of subtype extra string or file name // 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"}" 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()) if (file.exists())
file.delete() file.delete()
file.parentFile?.mkdir() file.parentFile?.mkdir()
@ -81,21 +80,21 @@ private fun checkLayout(layoutContent: String, context: Context): Boolean? {
params.mPopupKeyTypes.add(POPUP_KEYS_LAYOUT) params.mPopupKeyTypes.add(POPUP_KEYS_LAYOUT)
addLocaleKeyTextsToParams(context, params, POPUP_KEYS_NORMAL) addLocaleKeyTextsToParams(context, params, POPUP_KEYS_NORMAL)
try { try {
val keys = JsonKeyboardParser(params, context).parseLayoutString(layoutContent) val keys = RawKeyboardParser.parseJsonString(layoutContent).map { it.mapNotNull { it.compute(params)?.toKeyParams(params) } }
if (!checkKeys(keys)) if (!checkKeys(keys))
return null return null
return true return true
} catch (e: Exception) { Log.w(TAG, "error parsing custom json layout", e) } } catch (e: Exception) { Log.w(TAG, "error parsing custom json layout", e) }
try { try {
val keys = SimpleKeyboardParser(params, context).parseLayoutString(layoutContent) val keys = RawKeyboardParser.parseSimpleString(layoutContent).map { it.map { it.toKeyParams(params) } }
if (!checkKeys(keys)) if (!checkKeys(keys))
return null return null
return false return false
} catch (e: Exception) { Log.w(TAG, "error parsing custom simple layout", e) } } catch (e: Exception) { Log.w(TAG, "error parsing custom simple layout", e) }
if (layoutContent.startsWith("[")) { 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 { 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) } } catch (e: Exception) { Log.w(TAG, "error parsing custom json layout", e) }
} }
return null return null
@ -129,8 +128,10 @@ private fun checkKeys(keys: List<List<Key.KeyParams>>): Boolean {
return true return true
} }
fun getLayoutFile(layoutName: String, context: Context) = fun getCustomLayoutFile(layoutName: String, context: Context) =
File(Settings.getLayoutsDir(context), layoutName) File(getCustomLayoutsDir(context), layoutName)
fun getCustomLayoutsDir(context: Context) = File(DeviceProtectedUtils.getFilesDir(context), "layouts")
// undo the name changes in loadCustomLayout when clicking ok // undo the name changes in loadCustomLayout when clicking ok
fun getLayoutDisplayName(layoutName: String) = fun getLayoutDisplayName(layoutName: String) =
@ -141,11 +142,11 @@ fun getLayoutDisplayName(layoutName: String) =
} }
fun removeCustomLayoutFile(layoutName: String, context: Context) { 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) { 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 { val editText = EditText(context).apply {
setText(startContent ?: file.readText()) setText(startContent ?: file.readText())
} }

View file

@ -245,7 +245,7 @@ private fun loadResourceSubtypes(resources: Resources) {
private fun removeInvalidCustomSubtypes(context: Context) { private fun removeInvalidCustomSubtypes(context: Context) {
val prefs = DeviceProtectedUtils.getSharedPreferences(context) val prefs = DeviceProtectedUtils.getSharedPreferences(context)
val additionalSubtypes = Settings.readPrefAdditionalSubtypes(prefs, context.resources).split(";") 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>() val subtypesToRemove = mutableListOf<String>()
additionalSubtypes.forEach { additionalSubtypes.forEach {
val name = it.substringAfter(":").substringBefore(":") 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.KeyboardParams
import helium314.keyboard.keyboard.internal.TouchPositionCorrection import helium314.keyboard.keyboard.internal.TouchPositionCorrection
import helium314.keyboard.keyboard.internal.UniqueKeysCache import helium314.keyboard.keyboard.internal.UniqueKeysCache
import helium314.keyboard.keyboard.internal.keyboard_parser.JsonKeyboardParser
import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_NORMAL import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_NORMAL
import helium314.keyboard.keyboard.internal.keyboard_parser.SimpleKeyboardParser import helium314.keyboard.keyboard.internal.keyboard_parser.RawKeyboardParser
import helium314.keyboard.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams import helium314.keyboard.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyType
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.MultiTextKeyData
import helium314.keyboard.latin.LatinIME import helium314.keyboard.latin.LatinIME
import helium314.keyboard.latin.RichInputMethodSubtype import helium314.keyboard.latin.RichInputMethodSubtype
import helium314.keyboard.latin.utils.AdditionalSubtypeUtils.createEmojiCapableAdditionalSubtype import helium314.keyboard.latin.utils.AdditionalSubtypeUtils.createEmojiCapableAdditionalSubtype
@ -120,7 +117,7 @@ f""", // no newline at the end
val wantedKeyLabels = listOf(listOf("a", "b", "c"), listOf("d", "e", "f")) val wantedKeyLabels = listOf(listOf("a", "b", "c"), listOf("d", "e", "f"))
layoutStrings.forEachIndexed { i, layout -> layoutStrings.forEachIndexed { i, layout ->
println(i) 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) assertEquals(wantedKeyLabels, keyLabels)
} }
} }
@ -268,7 +265,7 @@ f""", // no newline at the end
] ]
] ]
""".trimIndent() """.trimIndent()
val keys = JsonKeyboardParser(params, latinIME).parseCoreLayout(layoutString) val keys = RawKeyboardParser.parseJsonString(layoutString).map { it.mapNotNull { it.compute(params) } }
keys.first().forEachIndexed { index, keyData -> keys.first().forEachIndexed { index, keyData ->
println("data: key ${keyData.label}: code ${keyData.code}, popups: ${keyData.popup.getPopupKeyLabels(params)}") println("data: key ${keyData.label}: code ${keyData.code}, popups: ${keyData.popup.getPopupKeyLabels(params)}")
val keyParams = keyData.toKeyParams(params) val keyParams = keyData.toKeyParams(params)
@ -298,13 +295,13 @@ f""", // no newline at the end
val editorInfo = EditorInfo() val editorInfo = EditorInfo()
val subtype = createEmojiCapableAdditionalSubtype(Locale.GERMANY, "qwertz+", true) val subtype = createEmojiCapableAdditionalSubtype(Locale.GERMANY, "qwertz+", true)
val (kb, keys) = buildKeyboard(editorInfo, subtype, KeyboardId.ELEMENT_ALPHABET) val (kb, keys) = buildKeyboard(editorInfo, subtype, KeyboardId.ELEMENT_ALPHABET)
assertEquals(keys[0].size, 11) assertEquals(11, keys[0].size)
assertEquals(keys[1].size, 11) assertEquals(11, keys[1].size)
assertEquals(keys[2].size, 10) assertEquals(10, keys[2].size)
val (kb2, keys2) = buildKeyboard(editorInfo, subtype, KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) val (kb2, keys2) = buildKeyboard(editorInfo, subtype, KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
assertEquals(keys2[0].size, 11) assertEquals(11, keys2[0].size)
assertEquals(keys2[1].size, 11) assertEquals(11, keys2[1].size)
assertEquals(keys2[2].size, 10) assertEquals(10, keys2[2].size)
} }
@Test fun `popup key count does not depend on shift for (for simple layout)`() { @Test fun `popup key count does not depend on shift for (for simple layout)`() {