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"
minSdk = 21
targetSdk = 34
versionCode = 2304
versionName = "2.3+dev3"
versionCode = 2305
versionName = "2.3+dev4"
ndk {
abiFilters.clear()
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.Companion.folder
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.defaultPinnedToolbarPref
import helium314.keyboard.latin.utils.prefs
@ -344,16 +347,33 @@ fun checkVersionUpgrade(context: Context) {
val dir = File(folder, LayoutType.MAIN.folder)
dir.mkdirs()
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))
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)
LayoutUtilsCustom.onCustomLayoutFileListChanged() // just to be sure
prefs.edit { putInt(Settings.PREF_VERSION_CODE, BuildConfig.VERSION_CODE) }

View file

@ -202,14 +202,14 @@ class LanguageSettingsDialog(
fun delete() {
binding.subtypes.removeView(row)
infos.remove(subtype)
if (isCustom)
LayoutUtilsCustom.removeCustomLayoutFile(layoutSetName, context)
//if (isCustom)
// LayoutUtilsCustom.removeCustomLayoutFile(layoutSetName, context)
SubtypeUtilsAdditional.removeAdditionalSubtype(prefs, subtype.subtype)
removeEnabledSubtype(prefs, subtype.subtype)
reloadSetting()
}
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 {
delete()
}

View file

@ -14,11 +14,14 @@ object LayoutUtils {
if (locale == null)
return getAllAvailableSubtypes().mapTo(HashSet()) { it.mainLayoutName()?.substringBefore("+") ?: "qwerty" }
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" }
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 {
val layouts = context.assets.list(layoutType.folder)!!
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.encodeBase36
import helium314.keyboard.latin.utils.LayoutType.Companion.folder
import helium314.keyboard.latin.utils.ScriptUtils.script
import kotlinx.serialization.SerializationException
import java.io.File
import java.io.IOException
import java.util.EnumMap
import java.util.Locale
object LayoutUtilsCustom {
fun loadCustomLayout(uri: Uri?, languageTag: String, context: Context, onAdded: (String) -> Unit) {
@ -70,7 +72,7 @@ object LayoutUtilsCustom {
.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)}."
val file = getCustomLayoutFile(name, context)
val file = getCustomLayoutFile(name, LayoutType.MAIN, context)
if (file.exists())
file.delete()
file.parentFile?.mkdir()
@ -149,42 +151,37 @@ object LayoutUtilsCustom {
return true
}
/** don't rename or delete the file without calling [onCustomLayoutFileListChanged] */
fun getCustomLayoutFile(layoutName: String, context: Context) = // todo: remove
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) {
fun getCustomLayoutFiles(layoutType: LayoutType, context: Context, locale: Locale? = null): List<File> {
val layouts = customLayoutMap.getOrPut(layoutType) {
File(DeviceProtectedUtils.getFilesDir(context), layoutType.folder).listFiles()?.toList() ?: emptyList()
}
private val customLayoutMap = EnumMap<LayoutType, List<File>>(LayoutType::class.java)
if (layoutType != LayoutType.MAIN || locale == null)
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() {
customLayouts = null
customLayoutMap.clear()
}
private fun getCustomLayoutsDir(context: Context) = File(DeviceProtectedUtils.getFilesDir(context), "layouts")
fun getCustomLayoutDisplayName(layoutName: String) =
fun getSecondaryLayoutDisplayName(layoutName: String) =
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) {
layoutName
}
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 getCustomLayoutFile(layoutName: String, layoutType: LayoutType, context: Context): File {
@ -193,17 +190,13 @@ object LayoutUtilsCustom {
return file
}
fun removeCustomLayoutFile(layoutName: String, context: Context) {
getCustomLayoutFile(layoutName, context).delete()
}
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 {
setText(startContent ?: file.readText())
}
val builder = AlertDialog.Builder(context)
.setTitle(getCustomLayoutDisplayName(layoutName))
.setTitle(getSecondaryLayoutDisplayName(layoutName))
.setView(editText)
.setPositiveButton(R.string.save) { _, _ ->
val content = editText.text.toString()
@ -236,5 +229,6 @@ object LayoutUtilsCustom {
// this goes into prefs and file names, so do not change!
const val CUSTOM_LAYOUT_PREFIX = "custom."
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
public static String getMainLayoutDisplayName(@NonNull final String layoutName) {
if (LayoutUtilsCustom.INSTANCE.isCustomLayout(layoutName))
return LayoutUtilsCustom.INSTANCE.getCustomLayoutDisplayName(layoutName);
return LayoutUtilsCustom.INSTANCE.getSecondaryLayoutDisplayName(layoutName);
return sKeyboardLayoutToDisplayNameMap.get(layoutName);
}

View file

@ -57,7 +57,7 @@ fun getMatchingLayoutSetNameForLocale(locale: Locale): String {
fun addEnabledSubtype(prefs: SharedPreferences, newSubtype: InputMethodSubtype) {
require(initialized)
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)
prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, newString) }

View file

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

View file

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

View file

@ -206,7 +206,7 @@ private fun LayoutItemRow(
}
)
Text(
text = if (isCustom) LayoutUtilsCustom.getCustomLayoutDisplayName(layoutName)
text = if (isCustom) LayoutUtilsCustom.getSecondaryLayoutDisplayName(layoutName)
else layoutName.getStringResourceOrName("layout_", ctx),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f),
@ -219,7 +219,7 @@ private fun LayoutItemRow(
if (showDeleteDialog)
ConfirmationDialog(
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),
onConfirmed = {
showDeleteDialog = false

View file

@ -50,7 +50,7 @@ fun createLayoutSettings(context: Context) = listOf(
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
var showDialog by rememberSaveable { mutableStateOf(false) }
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)
Preference(
name = setting.title,