add setting for customizing functional key layouts

This commit is contained in:
Helium314 2024-05-22 22:34:17 +02:00
parent 17aa321fbd
commit 0459971e3a
10 changed files with 150 additions and 61 deletions

View file

@ -197,8 +197,8 @@ class KeyboardParser(private val params: KeyboardParams, private val context: Co
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
// remove unwanted keys (emoji, numpad, language switch)
if (!Settings.getInstance().current.mHasCustomFunctionalLayout) {
// remove keys that should only exist on specific layouts or depend on setting (emoji, numpad, language switch)
if (!Settings.getInstance().current.mShowsEmojiKey || !params.mId.isAlphabetKeyboard)
functionalKeysBottom.removeFirst { it.label == KeyLabel.EMOJI }
if (!Settings.getInstance().current.isLanguageSwitchKeyEnabled || !params.mId.isAlphabetKeyboard)
@ -314,7 +314,3 @@ 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

@ -22,8 +22,9 @@ 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.getCustomFunctionalLayoutName
import helium314.keyboard.latin.utils.getCustomLayoutFile
import helium314.keyboard.latin.utils.getCustomLayoutsDir
import helium314.keyboard.latin.utils.getCustomLayoutFiles
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
@ -35,13 +36,12 @@ object RawKeyboardParser {
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 getFunctionalLayoutName(params, context)
} else {
getLayoutName(params, context)
}
@ -121,43 +121,24 @@ object RawKeyboardParser {
else -> params.mId.mSubtype.keyboardLayoutSetName.substringBeforeLast("+")
}
// todo (later, see also keyboardParser): use Settings.getInstance().current.mSingleFunctionalLayout
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
private fun getFunctionalLayoutName(params: KeyboardParams, context: Context): String {
if (Settings.getInstance().current.mHasCustomFunctionalLayout) {
getCustomFunctionalLayoutName(params.mId.mElementId, params.mId.mSubtype.rawSubtype, context)
?.let { return it }
}
return if (Settings.getInstance().isTablet) "functional_keys_tablet" else "functional_keys"
}
/** 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()
val customFiles = getCustomLayoutFiles(context).map { it.name }
if (layoutName.startsWith(CUSTOM_LAYOUT_PREFIX)) {
return if (customFiles?.contains(layoutName) == true) layoutName
else "qwerty.txt" // fallback
return customFiles.firstOrNull { it.startsWith(layoutName)}
?: if (layoutName.contains("functional")) "functional_keys.json" else "qwerty.txt" // fallback to defaults
}
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.")}
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

View file

@ -11,7 +11,8 @@ 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.getCustomLayoutFile
import helium314.keyboard.latin.utils.onCustomLayoutFileListChanged
import helium314.keyboard.latin.utils.upgradeToolbarPrefs
import java.io.File
@ -51,10 +52,9 @@ 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 = getCustomLayoutsDir(context)
val oldShiftSymbolsFile = File(layoutsDir, "${CUSTOM_LAYOUT_PREFIX}shift_symbols")
val oldShiftSymbolsFile = getCustomLayoutFile("${CUSTOM_LAYOUT_PREFIX}shift_symbols", context)
if (oldShiftSymbolsFile.exists()) {
oldShiftSymbolsFile.renameTo(File(layoutsDir, "${CUSTOM_LAYOUT_PREFIX}symbols_shifted"))
oldShiftSymbolsFile.renameTo(getCustomLayoutFile("${CUSTOM_LAYOUT_PREFIX}symbols_shifted", context))
}
// rename subtype setting, and clean old subtypes that might remain in some cases
@ -73,6 +73,7 @@ fun checkVersionUpgrade(context: Context) {
putString(Settings.PREF_SELECTED_SUBTYPE, selectedSubtype)
}
}
onCustomLayoutFileListChanged() // just to be sure
prefs.edit { putInt(Settings.PREF_VERSION_CODE, BuildConfig.VERSION_CODE) }
}
@ -80,9 +81,8 @@ fun checkVersionUpgrade(context: Context) {
private fun upgradesWhenComingFromOldAppName(context: Context) {
// move layout files
try {
val layoutsDir = getCustomLayoutsDir(context)
File(context.filesDir, "layouts").listFiles()?.forEach {
it.copyTo(File(layoutsDir, it.name), true)
it.copyTo(getCustomLayoutFile(it.name, context), true)
it.delete()
}
} catch (_: Exception) {}

View file

@ -41,12 +41,15 @@ import helium314.keyboard.latin.common.FileUtils
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.settings.SeekBarDialogPreference.ValueProxy
import helium314.keyboard.latin.utils.AdditionalSubtypeUtils
import helium314.keyboard.latin.utils.CUSTOM_FUNCTIONAL_LAYOUT_NORMAL
import helium314.keyboard.latin.utils.CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS
import helium314.keyboard.latin.utils.CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED
import helium314.keyboard.latin.utils.CUSTOM_LAYOUT_PREFIX
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.getCustomLayoutFiles
import helium314.keyboard.latin.utils.getStringResourceOrName
import helium314.keyboard.latin.utils.infoDialog
import helium314.keyboard.latin.utils.reloadEnabledSubtypes
@ -127,7 +130,11 @@ class AdvancedSettingsFragment : SubScreenFragment() {
findPreference<Preference>("backup_restore")?.setOnPreferenceClickListener { showBackupRestoreDialog() }
findPreference<Preference>("custom_symbols_number_layouts")?.setOnPreferenceClickListener {
showCustomizeLayoutsDialog()
showCustomizeSymbolNumberLayoutsDialog()
true
}
findPreference<Preference>("custom_functional_key_layouts")?.setOnPreferenceClickListener {
showCustomizeFunctionalKeyLayoutsDialog()
true
}
}
@ -145,7 +152,7 @@ class AdvancedSettingsFragment : SubScreenFragment() {
}
}
private fun showCustomizeLayoutsDialog() {
private fun showCustomizeSymbolNumberLayoutsDialog() {
val layoutNames = RawKeyboardParser.symbolAndNumberLayouts.map { it.getStringResourceOrName("layout_", requireContext()) }.toTypedArray()
AlertDialog.Builder(requireContext())
.setTitle(R.string.customize_symbols_number_layouts)
@ -158,8 +165,8 @@ class AdvancedSettingsFragment : SubScreenFragment() {
}
private fun customizeSymbolNumberLayout(layoutName: String) {
val customLayoutName = getCustomLayoutsDir(requireContext()).list()
?.firstOrNull { it.startsWith("$CUSTOM_LAYOUT_PREFIX$layoutName.") }
val customLayoutName = getCustomLayoutFiles(requireContext()).map { it.name }
.firstOrNull { it.startsWith("$CUSTOM_LAYOUT_PREFIX$layoutName.") }
val originalLayout = if (customLayoutName != null) null
else {
requireContext().assets.list("layouts")?.firstOrNull { it.startsWith("$layoutName.") }
@ -169,6 +176,32 @@ class AdvancedSettingsFragment : SubScreenFragment() {
editCustomLayout(customLayoutName ?: "$CUSTOM_LAYOUT_PREFIX$layoutName.txt", requireContext(), originalLayout, displayName)
}
private fun showCustomizeFunctionalKeyLayoutsDialog() {
val list = listOf(CUSTOM_FUNCTIONAL_LAYOUT_NORMAL, CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS, CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED)
.map { it.substringBeforeLast(".") }
val layoutNames = list.map { it.substringAfter(CUSTOM_LAYOUT_PREFIX).getStringResourceOrName("layout_", requireContext()) }.toTypedArray()
AlertDialog.Builder(requireContext())
.setTitle(R.string.customize_functional_key_layouts)
.setItems(layoutNames) { di, i ->
di.dismiss()
customizeFunctionalKeysLayout(list[i])
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun customizeFunctionalKeysLayout(layoutName: String) {
val customLayoutName = getCustomLayoutFiles(requireContext()).map { it.name }
.firstOrNull { it.startsWith("$layoutName.") }
val originalLayout = if (customLayoutName != null) null
else {
val defaultLayoutName = if (Settings.getInstance().isTablet) "functional_keys_tablet.json" else "functional_keys.json"
requireContext().assets.open("layouts" + File.separator + defaultLayoutName).reader().readText()
}
val displayName = layoutName.substringAfter(CUSTOM_LAYOUT_PREFIX).getStringResourceOrName("layout_", requireContext())
editCustomLayout(customLayoutName ?: "$layoutName.json", requireContext(), originalLayout, displayName)
}
@SuppressLint("ApplySharedPref")
private fun onClickLoadLibrary(): Boolean {
// get architecture for telling user which file to use

View file

@ -25,6 +25,7 @@ import helium314.keyboard.latin.R;
import helium314.keyboard.latin.RichInputMethodManager;
import helium314.keyboard.latin.common.Colors;
import helium314.keyboard.latin.permissions.PermissionsUtil;
import helium314.keyboard.latin.utils.CustomLayoutUtilsKt;
import helium314.keyboard.latin.utils.InputTypeUtils;
import helium314.keyboard.latin.utils.Log;
import helium314.keyboard.latin.utils.PopupKeysUtilsKt;
@ -122,6 +123,7 @@ public class SettingsValues {
public final SettingsValuesForSuggestion mSettingsValuesForSuggestion;
public final boolean mIncognitoModeEnabled;
public final boolean mLongPressSymbolsForNumpad;
public final boolean mHasCustomFunctionalLayout;
// User-defined colors
public final Colors mColors;
@ -237,6 +239,7 @@ public class SettingsValues {
mSpacingAndPunctuations = new SpacingAndPunctuations(res, mUrlDetectionEnabled);
mBottomPaddingScale = prefs.getFloat(Settings.PREF_BOTTOM_PADDING_SCALE, DEFAULT_SIZE_SCALE);
mLongPressSymbolsForNumpad = prefs.getBoolean(Settings.PREFS_LONG_PRESS_SYMBOLS_FOR_NUMPAD, false);
mHasCustomFunctionalLayout = CustomLayoutUtilsKt.hasCustomFunctionalLayout(selectedSubtype, context);
}
public boolean isApplicationSpecifiedCompletionsOn() {

View file

@ -5,6 +5,7 @@ import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import android.text.InputType
import android.view.inputmethod.InputMethodSubtype
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.core.widget.doAfterTextChanged
@ -17,6 +18,7 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_NORMAL
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.Constants
import helium314.keyboard.latin.common.FileUtils
import java.io.File
import java.io.IOException
@ -112,7 +114,7 @@ private fun checkKeys(keys: List<List<Key.KeyParams>>): Boolean {
Log.w(TAG, "too many keys in one row")
return false
}
if (keys.any { row -> row.any { ((it.mLabel?.length ?: 0) > 6) } }) {
if (keys.any { row -> row.any { ((it.mLabel?.length ?: 0) > 20) } }) {
Log.w(TAG, "too long text on key")
return false
}
@ -127,10 +129,24 @@ private fun checkKeys(keys: List<List<Key.KeyParams>>): Boolean {
return true
}
/** don't rename or delete the file without calling [onCustomLayoutFileListChanged] */
fun getCustomLayoutFile(layoutName: String, context: Context) =
File(getCustomLayoutsDir(context), layoutName)
fun getCustomLayoutsDir(context: Context) = File(DeviceProtectedUtils.getFilesDir(context), "layouts")
// cache to avoid frequently listing files
/** don't rename or delete files without calling [onCustomLayoutFileListChanged] */
fun getCustomLayoutFiles(context: Context): List<File> {
customLayouts?.let { return it }
val layouts = getCustomLayoutsDir(context).listFiles()?.toList() ?: emptyList()
customLayouts = layouts
return layouts
}
fun onCustomLayoutFileListChanged() {
customLayouts = null
}
private fun getCustomLayoutsDir(context: Context) = File(DeviceProtectedUtils.getFilesDir(context), "layouts")
// undo the name changes in loadCustomLayout when clicking ok
fun getLayoutDisplayName(layoutName: String) =
@ -164,6 +180,7 @@ fun editCustomLayout(layoutName: String, context: Context, startContent: String?
file.writeText(content)
if (isJson != wasJson) // unlikely to be needed, but better be safe
file.renameTo(File(file.absolutePath.substringBeforeLast(".") + "." + if (isJson) "json" else "txt"))
onCustomLayoutFileListChanged()
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(context)
}
}
@ -173,6 +190,7 @@ fun editCustomLayout(layoutName: String, context: Context, startContent: String?
builder.setNeutralButton(R.string.delete) { _, _ ->
confirmDialog(context, context.getString(R.string.delete_layout, displayName), context.getString(R.string.delete)) {
file.delete()
onCustomLayoutFileListChanged()
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(context)
}
}
@ -182,6 +200,45 @@ fun editCustomLayout(layoutName: String, context: Context, startContent: String?
builder.show()
}
fun hasCustomFunctionalLayout(subtype: InputMethodSubtype, context: Context): Boolean {
val anyCustomFunctionalLayout = getCustomFunctionalLayoutName(KeyboardId.ELEMENT_ALPHABET, subtype, context)
?: getCustomFunctionalLayoutName(KeyboardId.ELEMENT_SYMBOLS, subtype, context)
?: getCustomFunctionalLayoutName(KeyboardId.ELEMENT_SYMBOLS_SHIFTED, subtype, context)
return anyCustomFunctionalLayout != null
}
fun getCustomFunctionalLayoutName(elementId: Int, subtype: InputMethodSubtype, context: Context): String? {
val customFunctionalLayoutNames = getCustomLayoutFiles(context).filter { it.name.contains("functional") }.map { it.name.substringBeforeLast(".") + "." }
if (customFunctionalLayoutNames.isEmpty()) return null
val languageTag = subtype.locale().toLanguageTag()
val mainLayoutName = subtype.getExtraValueOf(Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET) ?: "qwerty"
if (elementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
findMatchingLayout(customFunctionalLayoutNames.filter { it.startsWith(CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED) }, mainLayoutName, languageTag)
?.let { return it }
}
if (elementId == KeyboardId.ELEMENT_SYMBOLS) {
findMatchingLayout(customFunctionalLayoutNames.filter { it.startsWith(CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS) }, mainLayoutName, languageTag)
?.let { return it }
}
return findMatchingLayout(customFunctionalLayoutNames.filter { it.startsWith(CUSTOM_FUNCTIONAL_LAYOUT_NORMAL) }, mainLayoutName, languageTag)
}
// todo (when adding custom layouts per locale or main layout): adjust mainLayoutName for custom layouts?
// remove language tag and file ending (currently name is e.g. custom.en-US.abcdfdsg3.json, and we could use abcdfdsg3 only)
// this way, custom layouts with same name could use same custom functional layouts
// currently there is no way to set the language tag or main layout name, so changes don't break backwards compatibility
private fun findMatchingLayout(layoutNames: List<String>, mainLayoutName: String, languageTag: String): String? {
// first find layout with matching locale and main layout
return layoutNames.firstOrNull { it.endsWith(".$languageTag.$mainLayoutName.") }
// then find matching main layout
?: layoutNames.firstOrNull { it.endsWith(".$mainLayoutName.") }
// then find matching language
?: layoutNames.firstOrNull { it.endsWith(".$languageTag.") }
// then find "normal" functional layout (make use of the '.' separator
?: layoutNames.firstOrNull { it.count { it == '.' } == 2 }
}
private fun encodeBase36(string: String): String = BigInteger(string.toByteArray()).toString(36)
private fun decodeBase36(string: String) = BigInteger(string, 36).toByteArray().decodeToString()
@ -189,3 +246,8 @@ private fun decodeBase36(string: String) = BigInteger(string, 36).toByteArray().
// this goes into prefs and file names, so do not change!
const val CUSTOM_LAYOUT_PREFIX = "custom."
private const val TAG = "CustomLayoutUtils"
private var customLayouts: List<File>? = null
const val CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED = "${CUSTOM_LAYOUT_PREFIX}functional_keys_symbols_shifted."
const val CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS = "${CUSTOM_LAYOUT_PREFIX}functional_keys_symbols."
const val CUSTOM_FUNCTIONAL_LAYOUT_NORMAL = "${CUSTOM_LAYOUT_PREFIX}functional_keys."

View file

@ -245,12 +245,12 @@ 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 { getCustomLayoutsDir(context).list() }
val customSubtypeFiles by lazy { getCustomLayoutFiles(context).map { it.name } }
val subtypesToRemove = mutableListOf<String>()
additionalSubtypes.forEach {
val name = it.substringAfter(":").substringBefore(":")
if (!name.startsWith(CUSTOM_LAYOUT_PREFIX)) return@forEach
if (customSubtypeFiles?.contains(name) != true)
if (name !in customSubtypeFiles)
subtypesToRemove.add(it)
}
if (subtypesToRemove.isEmpty()) return