store custom latin layouts with scripts instead of language tag, so they can be used across languages

This commit is contained in:
Helium314 2025-02-15 18:31:25 +01:00
parent 5ccc117ae1
commit bccb10ea39
11 changed files with 64 additions and 47 deletions

View file

@ -13,8 +13,8 @@ android {
applicationId = "helium314.keyboard" applicationId = "helium314.keyboard"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 2304 versionCode = 2305
versionName = "2.3+dev3" versionName = "2.3+dev4"
ndk { ndk {
abiFilters.clear() abiFilters.clear()
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")) abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64"))

View file

@ -19,6 +19,9 @@ import helium314.keyboard.latin.utils.DictionaryInfoUtils
import helium314.keyboard.latin.utils.LayoutType import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.LayoutType.Companion.folder import helium314.keyboard.latin.utils.LayoutType.Companion.folder
import helium314.keyboard.latin.utils.LayoutUtilsCustom import helium314.keyboard.latin.utils.LayoutUtilsCustom
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.ScriptUtils.SCRIPT_LATIN
import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.ToolbarKey import helium314.keyboard.latin.utils.ToolbarKey
import helium314.keyboard.latin.utils.defaultPinnedToolbarPref import helium314.keyboard.latin.utils.defaultPinnedToolbarPref
import helium314.keyboard.latin.utils.prefs import helium314.keyboard.latin.utils.prefs
@ -344,16 +347,33 @@ fun checkVersionUpgrade(context: Context) {
val dir = File(folder, LayoutType.MAIN.folder) val dir = File(folder, LayoutType.MAIN.folder)
dir.mkdirs() dir.mkdirs()
file.renameTo(File(dir, file.name)) file.renameTo(File(dir, file.name))
// todo: maybe rename to custom.latn.name. instead of custom.en-GB.name. for latin script?
// just make sure the subtypes are still working when the file name is different (need to upgrade PREF_ADDITIONAL_SUBTYPES)
// also consider name collision when a user has layout with the same name for 2 languages
// decode name, append number, encode name
} }
} }
} }
if (prefs.contains(Settings.PREF_ADDITIONAL_SUBTYPES)) if (prefs.contains(Settings.PREF_ADDITIONAL_SUBTYPES))
prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, "")!!.replace(":", "§")).apply() prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, "")!!.replace(":", "§")).apply()
} }
if (oldVersion <= 2304) {
// rename layout files for latin scripts, and adjust layouts stored in prefs accordingly
LayoutUtilsCustom.getCustomLayoutFiles(LayoutType.MAIN, context).forEach {
val locale = it.name.substringAfter("custom.").substringBefore(".").constructLocale()
if (locale.script() != SCRIPT_LATIN) return@forEach
// change language tag to SCRIPT_LATIN, but
// avoid overwriting if 2 layouts have a different language tag, but the same name
val layoutDisplayName = LayoutUtilsCustom.getSecondaryLayoutDisplayName(it.name)
var newFile = File(it.parentFile!!, LayoutUtilsCustom.getMainLayoutName(layoutDisplayName, locale))
var i = 1
while (newFile.exists()) // make sure name is not already in use, e.g. custom.en.abcd. and custom.it.abcd. would both be custom.Latn.abcd
newFile = File(it.parentFile!!, LayoutUtilsCustom.getMainLayoutName(layoutDisplayName + i++, locale))
it.renameTo(newFile)
// modify prefs
listOf(Settings.PREF_ENABLED_SUBTYPES, Settings.PREF_SELECTED_SUBTYPE, Settings.PREF_ADDITIONAL_SUBTYPES).forEach { key ->
val value = prefs.getString(key, "")!!
if (it.name in value)
prefs.edit().putString(key, value.replace(it.name, newFile.name)).apply()
}
}
}
upgradeToolbarPrefs(prefs) upgradeToolbarPrefs(prefs)
LayoutUtilsCustom.onCustomLayoutFileListChanged() // just to be sure LayoutUtilsCustom.onCustomLayoutFileListChanged() // just to be sure
prefs.edit { putInt(Settings.PREF_VERSION_CODE, BuildConfig.VERSION_CODE) } prefs.edit { putInt(Settings.PREF_VERSION_CODE, BuildConfig.VERSION_CODE) }

View file

@ -202,14 +202,14 @@ class LanguageSettingsDialog(
fun delete() { fun delete() {
binding.subtypes.removeView(row) binding.subtypes.removeView(row)
infos.remove(subtype) infos.remove(subtype)
if (isCustom) //if (isCustom)
LayoutUtilsCustom.removeCustomLayoutFile(layoutSetName, context) // LayoutUtilsCustom.removeCustomLayoutFile(layoutSetName, context)
SubtypeUtilsAdditional.removeAdditionalSubtype(prefs, subtype.subtype) SubtypeUtilsAdditional.removeAdditionalSubtype(prefs, subtype.subtype)
removeEnabledSubtype(prefs, subtype.subtype) removeEnabledSubtype(prefs, subtype.subtype)
reloadSetting() reloadSetting()
} }
if (isCustom) { if (isCustom) {
confirmDialog(context, context.getString(R.string.delete_layout, LayoutUtilsCustom.getCustomLayoutDisplayName(layoutSetName)), context.getString(R.string.delete)) { delete() } confirmDialog(context, context.getString(R.string.delete_layout, LayoutUtilsCustom.getSecondaryLayoutDisplayName(layoutSetName)), context.getString(R.string.delete)) { delete() }
} else { } else {
delete() delete()
} }

View file

@ -14,11 +14,14 @@ object LayoutUtils {
if (locale == null) if (locale == null)
return getAllAvailableSubtypes().mapTo(HashSet()) { it.mainLayoutName()?.substringBefore("+") ?: "qwerty" } return getAllAvailableSubtypes().mapTo(HashSet()) { it.mainLayoutName()?.substringBefore("+") ?: "qwerty" }
if (locale.script() == ScriptUtils.SCRIPT_LATIN) if (locale.script() == ScriptUtils.SCRIPT_LATIN)
return getAllAvailableSubtypes().filter { it.isAsciiCapable && LayoutUtilsCustom.isCustomLayout(it.mainLayoutName() ?: "qwerty") } return getAllAvailableSubtypes().filter { it.isAsciiCapable && !LayoutUtilsCustom.isCustomLayout(it.mainLayoutName() ?: "qwerty") }
.mapTo(HashSet()) { it.mainLayoutName()?.substringBefore("+") ?: "qwerty" } .mapTo(HashSet()) { it.mainLayoutName()?.substringBefore("+") ?: "qwerty" }
return getSubtypesForLocale(locale).mapNotNullTo(HashSet()) { it.mainLayoutName() } return getSubtypesForLocale(locale).mapNotNullTo(HashSet()) { it.mainLayoutName() }
} }
fun getLMainLayoutsForLocales(locales: List<Locale>, context: Context): Collection<String> =
locales.flatMapTo(HashSet()) { getAvailableLayouts(LayoutType.MAIN, context, it) }.sorted()
fun getContent(layoutType: LayoutType, layoutName: String, context: Context): String { fun getContent(layoutType: LayoutType, layoutName: String, context: Context): String {
val layouts = context.assets.list(layoutType.folder)!! val layouts = context.assets.list(layoutType.folder)!!
layouts.firstOrNull { it.startsWith("$layoutName.") } layouts.firstOrNull { it.startsWith("$layoutName.") }

View file

@ -21,10 +21,12 @@ import helium314.keyboard.latin.common.FileUtils
import helium314.keyboard.latin.common.decodeBase36 import helium314.keyboard.latin.common.decodeBase36
import helium314.keyboard.latin.common.encodeBase36 import helium314.keyboard.latin.common.encodeBase36
import helium314.keyboard.latin.utils.LayoutType.Companion.folder import helium314.keyboard.latin.utils.LayoutType.Companion.folder
import helium314.keyboard.latin.utils.ScriptUtils.script
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.EnumMap import java.util.EnumMap
import java.util.Locale
object LayoutUtilsCustom { object LayoutUtilsCustom {
fun loadCustomLayout(uri: Uri?, languageTag: String, context: Context, onAdded: (String) -> Unit) { fun loadCustomLayout(uri: Uri?, languageTag: String, context: Context, onAdded: (String) -> Unit) {
@ -70,7 +72,7 @@ object LayoutUtilsCustom {
.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)}." name = "$CUSTOM_LAYOUT_PREFIX${languageTag}.${encodeBase36(name)}."
val file = getCustomLayoutFile(name, context) val file = getCustomLayoutFile(name, LayoutType.MAIN, context)
if (file.exists()) if (file.exists())
file.delete() file.delete()
file.parentFile?.mkdir() file.parentFile?.mkdir()
@ -149,42 +151,37 @@ object LayoutUtilsCustom {
return true return true
} }
/** don't rename or delete the file without calling [onCustomLayoutFileListChanged] */ fun getCustomLayoutFiles(layoutType: LayoutType, context: Context, locale: Locale? = null): List<File> {
fun getCustomLayoutFile(layoutName: String, context: Context) = // todo: remove val layouts = customLayoutMap.getOrPut(layoutType) {
File(getCustomLayoutsDir(context), layoutName)
// cache to avoid frequently listing files
/** don't rename or delete files without calling [onCustomLayoutFileListChanged] */
fun getCustomLayoutFiles(context: Context): List<File> { // todo: remove, AND USE THE NEW THING FOR SUBTYPE SETTINGS
customLayouts?.let { return it }
val layouts = getCustomLayoutsDir(context).listFiles()?.toList() ?: emptyList()
customLayouts = layouts
return layouts
}
fun getCustomLayoutFiles(layoutType: LayoutType, context: Context): List<File> =
customLayoutMap.getOrPut(layoutType) {
File(DeviceProtectedUtils.getFilesDir(context), layoutType.folder).listFiles()?.toList() ?: emptyList() File(DeviceProtectedUtils.getFilesDir(context), layoutType.folder).listFiles()?.toList() ?: emptyList()
} }
if (layoutType != LayoutType.MAIN || locale == null)
private val customLayoutMap = EnumMap<LayoutType, List<File>>(LayoutType::class.java) return layouts
if (locale.script() == ScriptUtils.SCRIPT_LATIN)
return layouts.filter { it.name.startsWith(CUSTOM_LAYOUT_PREFIX + ScriptUtils.SCRIPT_LATIN + ".") }
return layouts.filter { it.name.startsWith(CUSTOM_LAYOUT_PREFIX + locale.toLanguageTag() + ".") }
}
fun onCustomLayoutFileListChanged() { fun onCustomLayoutFileListChanged() {
customLayouts = null
customLayoutMap.clear() customLayoutMap.clear()
} }
private fun getCustomLayoutsDir(context: Context) = File(DeviceProtectedUtils.getFilesDir(context), "layouts") fun getSecondaryLayoutDisplayName(layoutName: String) =
fun getCustomLayoutDisplayName(layoutName: String) =
try { try {
decodeBase36(layoutName.substringAfter(CUSTOM_LAYOUT_PREFIX).substringBeforeLast(".")) if (layoutName.count { it == '.' } == 3) // main layout: "custom.<locale or script>.<name>.", other: custom.<name>.
decodeBase36(layoutName.substringAfter(CUSTOM_LAYOUT_PREFIX).substringAfter(".").substringBeforeLast("."))
else decodeBase36(layoutName.substringAfter(CUSTOM_LAYOUT_PREFIX).substringBeforeLast("."))
} catch (_: NumberFormatException) { } catch (_: NumberFormatException) {
layoutName layoutName
} }
fun getCustomLayoutName(displayName: String) = CUSTOM_LAYOUT_PREFIX + encodeBase36(displayName) + "." fun getCustomLayoutName(displayName: String) = CUSTOM_LAYOUT_PREFIX + encodeBase36(displayName) + "."
fun getMainLayoutName(displayName: String, locale: Locale) =
if (locale.script() == ScriptUtils.SCRIPT_LATIN)
CUSTOM_LAYOUT_PREFIX + ScriptUtils.SCRIPT_LATIN + "." + encodeBase36(displayName) + "."
else CUSTOM_LAYOUT_PREFIX + locale.toLanguageTag() + "." + encodeBase36(displayName) + "."
fun isCustomLayout(layoutName: String) = layoutName.startsWith(CUSTOM_LAYOUT_PREFIX) fun isCustomLayout(layoutName: String) = layoutName.startsWith(CUSTOM_LAYOUT_PREFIX)
fun getCustomLayoutFile(layoutName: String, layoutType: LayoutType, context: Context): File { fun getCustomLayoutFile(layoutName: String, layoutType: LayoutType, context: Context): File {
@ -193,17 +190,13 @@ object LayoutUtilsCustom {
return file return file
} }
fun removeCustomLayoutFile(layoutName: String, context: Context) {
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 = getCustomLayoutFile(layoutName, context) val file = getCustomLayoutFile(layoutName, LayoutType.MAIN, context)
val editText = EditText(context).apply { val editText = EditText(context).apply {
setText(startContent ?: file.readText()) setText(startContent ?: file.readText())
} }
val builder = AlertDialog.Builder(context) val builder = AlertDialog.Builder(context)
.setTitle(getCustomLayoutDisplayName(layoutName)) .setTitle(getSecondaryLayoutDisplayName(layoutName))
.setView(editText) .setView(editText)
.setPositiveButton(R.string.save) { _, _ -> .setPositiveButton(R.string.save) { _, _ ->
val content = editText.text.toString() val content = editText.text.toString()
@ -236,5 +229,6 @@ object LayoutUtilsCustom {
// this goes into prefs and file names, so do not change! // this goes into prefs and file names, so do not change!
const val CUSTOM_LAYOUT_PREFIX = "custom." const val CUSTOM_LAYOUT_PREFIX = "custom."
private const val TAG = "LayoutUtilsCustom" private const val TAG = "LayoutUtilsCustom"
private var customLayouts: List<File>? = null private val customLayoutMap = EnumMap<LayoutType, List<File>>(LayoutType::class.java)
} }

View file

@ -267,7 +267,7 @@ public final class SubtypeLocaleUtils {
@Nullable @Nullable
public static String getMainLayoutDisplayName(@NonNull final String layoutName) { public static String getMainLayoutDisplayName(@NonNull final String layoutName) {
if (LayoutUtilsCustom.INSTANCE.isCustomLayout(layoutName)) if (LayoutUtilsCustom.INSTANCE.isCustomLayout(layoutName))
return LayoutUtilsCustom.INSTANCE.getCustomLayoutDisplayName(layoutName); return LayoutUtilsCustom.INSTANCE.getSecondaryLayoutDisplayName(layoutName);
return sKeyboardLayoutToDisplayNameMap.get(layoutName); return sKeyboardLayoutToDisplayNameMap.get(layoutName);
} }

View file

@ -57,7 +57,7 @@ fun getMatchingLayoutSetNameForLocale(locale: Locale): String {
fun addEnabledSubtype(prefs: SharedPreferences, newSubtype: InputMethodSubtype) { fun addEnabledSubtype(prefs: SharedPreferences, newSubtype: InputMethodSubtype) {
require(initialized) require(initialized)
val subtypeString = newSubtype.prefString() val subtypeString = newSubtype.prefString()
val oldSubtypeStrings = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, "")!!.split(SUBTYPE_SEPARATOR) val oldSubtypeStrings = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!.split(SUBTYPE_SEPARATOR)
val newString = (oldSubtypeStrings + subtypeString).filter { it.isNotBlank() }.toSortedSet().joinToString(SUBTYPE_SEPARATOR) val newString = (oldSubtypeStrings + subtypeString).filter { it.isNotBlank() }.toSortedSet().joinToString(SUBTYPE_SEPARATOR)
prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, newString) } prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, newString) }

View file

@ -26,6 +26,6 @@ fun InputMethodSubtype.mainLayoutName(): String? {
fun InputMethodSubtype.displayName(context: Context): CharSequence { fun InputMethodSubtype.displayName(context: Context): CharSequence {
val layoutName = SubtypeLocaleUtils.getMainLayoutName(this) val layoutName = SubtypeLocaleUtils.getMainLayoutName(this)
if (LayoutUtilsCustom.isCustomLayout(layoutName)) if (LayoutUtilsCustom.isCustomLayout(layoutName))
return "${LocaleUtils.getLocaleDisplayNameInSystemLocale(locale(), context)} (${LayoutUtilsCustom.getCustomLayoutDisplayName(layoutName)})" return "${LocaleUtils.getLocaleDisplayNameInSystemLocale(locale(), context)} (${LayoutUtilsCustom.getSecondaryLayoutDisplayName(layoutName)})"
return SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(this) return SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(this)
} }

View file

@ -44,7 +44,7 @@ fun LayoutEditDialog(
val startIsCustom = LayoutUtilsCustom.isCustomLayout(initialLayoutName) val startIsCustom = LayoutUtilsCustom.isCustomLayout(initialLayoutName)
var displayNameValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { var displayNameValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue( mutableStateOf(TextFieldValue(
if (startIsCustom) LayoutUtilsCustom.getCustomLayoutDisplayName(initialLayoutName) if (startIsCustom) LayoutUtilsCustom.getSecondaryLayoutDisplayName(initialLayoutName)
else initialLayoutName.getStringResourceOrName("layout_", ctx) else initialLayoutName.getStringResourceOrName("layout_", ctx)
)) ))
} }

View file

@ -206,7 +206,7 @@ private fun LayoutItemRow(
} }
) )
Text( Text(
text = if (isCustom) LayoutUtilsCustom.getCustomLayoutDisplayName(layoutName) text = if (isCustom) LayoutUtilsCustom.getSecondaryLayoutDisplayName(layoutName)
else layoutName.getStringResourceOrName("layout_", ctx), else layoutName.getStringResourceOrName("layout_", ctx),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@ -219,7 +219,7 @@ private fun LayoutItemRow(
if (showDeleteDialog) if (showDeleteDialog)
ConfirmationDialog( ConfirmationDialog(
onDismissRequest = { showDeleteDialog = false }, onDismissRequest = { showDeleteDialog = false },
text = { Text(stringResource(R.string.delete_layout, LayoutUtilsCustom.getCustomLayoutDisplayName(layoutName))) }, text = { Text(stringResource(R.string.delete_layout, LayoutUtilsCustom.getSecondaryLayoutDisplayName(layoutName))) },
confirmButtonText = stringResource(R.string.delete), confirmButtonText = stringResource(R.string.delete),
onConfirmed = { onConfirmed = {
showDeleteDialog = false showDeleteDialog = false

View file

@ -50,7 +50,7 @@ fun createLayoutSettings(context: Context) = listOf(
Log.v("irrelevant", "stupid way to trigger recomposition on preference change") Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
var showDialog by rememberSaveable { mutableStateOf(false) } var showDialog by rememberSaveable { mutableStateOf(false) }
val currentLayout = Settings.readDefaultLayoutName(layoutType, prefs) val currentLayout = Settings.readDefaultLayoutName(layoutType, prefs)
val displayName = if (LayoutUtilsCustom.isCustomLayout(currentLayout)) LayoutUtilsCustom.getCustomLayoutDisplayName(currentLayout) val displayName = if (LayoutUtilsCustom.isCustomLayout(currentLayout)) LayoutUtilsCustom.getSecondaryLayoutDisplayName(currentLayout)
else currentLayout.getStringResourceOrName("layout_", ctx) else currentLayout.getStringResourceOrName("layout_", ctx)
Preference( Preference(
name = setting.title, name = setting.title,