allow editing main layouts in subtype dialog

This commit is contained in:
Helium314 2025-02-22 12:21:26 +01:00
parent f15f65daa8
commit 7494d85aea
9 changed files with 150 additions and 48 deletions

View file

@ -375,10 +375,10 @@ fun checkVersionUpgrade(context: Context) {
// change language tag to SCRIPT_LATIN, but
// avoid overwriting if 2 layouts have a different language tag, but the same name
val layoutDisplayName = LayoutUtilsCustom.getDisplayName(it.name)
var newFile = File(it.parentFile!!, LayoutUtilsCustom.getMainLayoutName(layoutDisplayName, locale))
var newFile = File(it.parentFile!!, LayoutUtilsCustom.getLayoutName(layoutDisplayName, LayoutType.MAIN, 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))
newFile = File(it.parentFile!!, LayoutUtilsCustom.getLayoutName(layoutDisplayName + i++, LayoutType.MAIN, locale))
it.renameTo(newFile)
// modify prefs
listOf(Settings.PREF_ENABLED_SUBTYPES, Settings.PREF_SELECTED_SUBTYPE, Settings.PREF_ADDITIONAL_SUBTYPES).forEach { key ->

View file

@ -183,12 +183,16 @@ object LayoutUtilsCustom {
layoutName
}
fun getSecondaryLayoutName(displayName: String) = CUSTOM_LAYOUT_PREFIX + encodeBase36(displayName) + "."
fun getMainLayoutName(displayName: String, locale: Locale) =
if (locale.script() == ScriptUtils.SCRIPT_LATIN)
/** @return layoutName for given [displayName]. If [layoutType ]is MAIN, non-null [locale] must be supplied */
fun getLayoutName(displayName: String, layoutType: LayoutType, locale: Locale? = null): String {
if (layoutType != LayoutType.MAIN)
return CUSTOM_LAYOUT_PREFIX + encodeBase36(displayName) + "."
if (locale == null) throw IllegalArgumentException("locale for main layout not specified")
return 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)

View file

@ -132,14 +132,14 @@ object SubtypeSettings {
Settings.PREF_ENABLED_SUBTYPES to Defaults.PREF_ENABLED_SUBTYPES,
Settings.PREF_SELECTED_SUBTYPE to Defaults.PREF_SELECTED_SUBTYPE
).forEach { (key, default) ->
val new = prefs.getString(key, default)!!.split(Separators.SETS).joinToString(Separators.SETS) {
val new = prefs.getString(key, default)!!.split(Separators.SETS).mapTo(mutableSetOf()) {
val subtype = it.toSettingsSubtype()
if (subtype.layoutName(type) == from) {
if (to == null) subtype.withoutLayout(type).toPref()
else subtype.withLayout(type, to).toPref()
}
else subtype.toPref()
}
}.joinToString(Separators.SETS)
prefs.edit().putString(key, new).apply()
}
if (Settings.readDefaultLayoutName(type, prefs) == from)

View file

@ -73,6 +73,10 @@ object SubtypeUtilsAdditional {
// updates additional subtypes, enabled subtypes, and selected subtype
fun changeAdditionalSubtype(from: SettingsSubtype, to: SettingsSubtype, context: Context) {
val prefs = context.prefs()
// read now because there may be an intermediate state where the subtype is invalid and thus removed
val isSelected = prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype() == from
val isEnabled = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!.split(Separators.SETS)
.any { it.toSettingsSubtype() == from }
val new = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!
.split(Separators.SETS).mapNotNullTo(sortedSetOf()) {
if (it == from.toPref()) null else it
@ -81,11 +85,11 @@ object SubtypeUtilsAdditional {
val fromSubtype = from.toAdditionalSubtype() // will be null if we edit a resource subtype
val toSubtype = to.toAdditionalSubtype() // should never be null
val selectedSubtype = prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype()
if (selectedSubtype == from && toSubtype != null) {
if (isSelected && toSubtype != null) {
SubtypeSettings.setSelectedSubtype(prefs, toSubtype)
}
if (fromSubtype != null && SubtypeSettings.removeEnabledSubtype(context, fromSubtype) && toSubtype != null) {
if (fromSubtype != null && isEnabled && toSubtype != null) {
SubtypeSettings.removeEnabledSubtype(context, fromSubtype)
SubtypeSettings.addEnabledSubtype(prefs, toSubtype)
}
}

View file

@ -24,7 +24,6 @@ import helium314.keyboard.latin.common.FileUtils
import helium314.keyboard.latin.define.DebugFlags
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.ExecutorUtils
import helium314.keyboard.latin.utils.SubtypeSettings
import helium314.keyboard.latin.utils.cleanUnusedMainDicts
import helium314.keyboard.latin.utils.prefs
import kotlinx.coroutines.flow.MutableStateFlow
@ -40,6 +39,7 @@ import java.util.zip.ZipOutputStream
// https://developer.android.com/codelabs/android-baseline-profiles-improve
// https://developer.android.com/codelabs/jetpack-compose-performance#2
// https://developer.android.com/topic/performance/baselineprofiles/overview
// todo: consider viewModel, at least for LanguageScreen and ColorsScreen it might help making them less awkward and complicated
class SettingsActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
private val prefs by lazy { this.prefs() }
val prefChanged = MutableStateFlow(0) // simple counter, as the only relevant information is that something changed

View file

@ -25,11 +25,14 @@ import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.LayoutUtilsCustom
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.SubtypeSettings
import helium314.keyboard.latin.utils.getActivity
import helium314.keyboard.latin.utils.getStringResourceOrName
import helium314.keyboard.settings.SettingsActivity
import helium314.keyboard.settings.keyboardNeedsReload
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.Locale
@Composable
fun LayoutEditDialog(
@ -37,7 +40,9 @@ fun LayoutEditDialog(
layoutType: LayoutType,
initialLayoutName: String,
startContent: String? = null,
isNameValid: (String) -> Boolean
locale: Locale? = null,
onEdited: (newLayoutName: String) -> Unit = { },
isNameValid: ((String) -> Boolean)?
) {
val ctx = LocalContext.current
val scope = rememberCoroutineScope()
@ -50,8 +55,8 @@ fun LayoutEditDialog(
}
val nameValid = displayNameValue.text.isNotBlank()
&& (
(startIsCustom && LayoutUtilsCustom.getSecondaryLayoutName(displayNameValue.text) == initialLayoutName)
|| isNameValid(LayoutUtilsCustom.getSecondaryLayoutName(displayNameValue.text))
(startIsCustom && LayoutUtilsCustom.getLayoutName(displayNameValue.text, layoutType, locale) == initialLayoutName)
|| isNameValid?.let { it(LayoutUtilsCustom.getLayoutName(displayNameValue.text, layoutType, locale)) } == true
)
TextInputDialog(
@ -60,26 +65,31 @@ fun LayoutEditDialog(
onDismissRequest()
},
onConfirmed = {
val newLayoutName = LayoutUtilsCustom.getSecondaryLayoutName(displayNameValue.text)
val newLayoutName = LayoutUtilsCustom.getLayoutName(displayNameValue.text, layoutType, locale)
if (startIsCustom && initialLayoutName != newLayoutName) {
LayoutUtilsCustom.getLayoutFile(initialLayoutName, layoutType, ctx).delete()
SubtypeSettings.onRenameLayout(layoutType, initialLayoutName, newLayoutName, ctx)
}
LayoutUtilsCustom.getLayoutFile(newLayoutName, layoutType, ctx).writeText(it)
LayoutUtilsCustom.onLayoutFileChanged()
onEdited(newLayoutName)
(ctx.getActivity() as? SettingsActivity)?.prefChanged?.value = 555
keyboardNeedsReload = true
},
confirmButtonText = stringResource(R.string.save),
initialText = startContent ?: LayoutUtilsCustom.getLayoutFile(initialLayoutName, layoutType, ctx).readText(),
singleLine = false,
title = {
TextField(
value = displayNameValue,
onValueChange = { displayNameValue = it },
isError = !nameValid,
supportingText = { if (!nameValid) Text(stringResource(R.string.name_invalid)) },
trailingIcon = { if (!nameValid) Icon(painterResource(R.drawable.ic_close), null) },
)
if (isNameValid == null)
Text(displayNameValue.text)
else
TextField(
value = displayNameValue,
onValueChange = { displayNameValue = it },
isError = !nameValid,
supportingText = { if (!nameValid) Text(stringResource(R.string.name_invalid)) },
trailingIcon = { if (!nameValid) Icon(painterResource(R.drawable.ic_close), null) },
)
},
checkTextValid = {
val valid = LayoutUtilsCustom.checkLayout(it, ctx)

View file

@ -94,7 +94,7 @@ fun LayoutPickerDialog(
LazyColumn(state = state) {
items(layouts) { item ->
if (item == "") {
AddLayoutRow({ newLayoutDialog = it to "" }, customLayouts)
AddLayoutRow({ newLayoutDialog = it to "" }, layoutType, customLayouts)
} else {
LayoutItemRow(
onDismissRequest = onDismissRequest,
@ -127,7 +127,7 @@ fun LayoutPickerDialog(
}
@Composable
private fun AddLayoutRow(onNewLayout: (String) -> Unit, userLayouts: Collection<String>) {
private fun AddLayoutRow(onNewLayout: (String) -> Unit, layoutType: LayoutType, userLayouts: Collection<String>) {
var textValue by remember { mutableStateOf(TextFieldValue()) }
Row(
verticalAlignment = Alignment.CenterVertically,
@ -141,7 +141,7 @@ private fun AddLayoutRow(onNewLayout: (String) -> Unit, userLayouts: Collection<
singleLine = true
)
IconButton(
enabled = textValue.text.isNotEmpty() && LayoutUtilsCustom.getSecondaryLayoutName(textValue.text) !in userLayouts,
enabled = textValue.text.isNotEmpty() && LayoutUtilsCustom.getLayoutName(textValue.text, layoutType) !in userLayouts,
onClick = { onNewLayout(textValue.text) }
) { Icon(painterResource(R.drawable.ic_edit), null) }
}

View file

@ -61,17 +61,19 @@ import helium314.keyboard.latin.utils.SubtypeUtilsAdditional
import helium314.keyboard.latin.utils.getDictionaryLocales
import helium314.keyboard.latin.utils.getSecondaryLocales
import helium314.keyboard.latin.utils.getStringResourceOrName
import helium314.keyboard.latin.utils.mainLayoutName
import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.settings.layoutFilePicker
import helium314.keyboard.settings.layoutIntent
import helium314.keyboard.settings.screens.GetIcon
import java.util.Locale
// todo:
// fix the display name (why is the layout always added now e.g. after adding a secondary locale, when it's not there initially?)
// some way of editing, adding and removing main layouts
// here or in layouts screen?
// layouts screen: that would be the purpose, but there is no default for main, and it's language dependent
// here we need at least an indicator which layouts are custom (edit or delete buttons)
// sometimes the layout is not enabled after changing something
// dropdown content is not refreshed on deleting a layout, maybe need to do the pref listener trick
// layout edit dialog does not care about ime padding when called from here
// rotating closes the dialog
// split this up a little, it's too long... and maybe we could re-use parts other dialogs?
@Composable
fun SubtypeDialog(
onDismissRequest: () -> Unit,
@ -91,12 +93,12 @@ fun SubtypeDialog(
ThreeButtonAlertDialog(
onDismissRequest = onDismissRequest,
onConfirmed = { onConfirmed(currentSubtype) },
neutralButtonText = if (SubtypeSettings.isAdditionalSubtype(subtype)) null else stringResource(R.string.delete),
neutralButtonText = if (SubtypeSettings.isAdditionalSubtype(subtype)) stringResource(R.string.delete) else null,
onNeutral = {
onDismissRequest()
SubtypeUtilsAdditional.removeAdditionalSubtype(prefs, subtype)
SubtypeSettings.removeEnabledSubtype(ctx, subtype)
}, // maybe confirm dialog?
},
title = { Text(SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype)) },
text = {
Column(
@ -106,6 +108,11 @@ fun SubtypeDialog(
WithSmallTitle(stringResource(R.string.keyboard_layout_set)) {
val appLayouts = LayoutUtils.getAvailableLayouts(LayoutType.MAIN, ctx, currentSubtype.locale)
val customLayouts = LayoutUtilsCustom.getLayoutFiles(LayoutType.MAIN, ctx, currentSubtype.locale).map { it.name }
var showAddLayoutDialog by remember { mutableStateOf(false) }
var showLayoutEditDialog: Pair<String, String?>? by remember { mutableStateOf(null) }
val layoutPicker = layoutFilePicker { content, name ->
showLayoutEditDialog = (name ?: "new layout") to content
}
DropDownField(
items = appLayouts + customLayouts,
selectedItem = currentSubtype.mainLayoutName() ?: "qwerty",
@ -113,9 +120,73 @@ fun SubtypeDialog(
currentSubtype = currentSubtype.withLayout(LayoutType.MAIN, it)
}
) {
Text(SubtypeLocaleUtils.getDisplayNameInSystemLocale(it, currentSubtype.locale))
// todo: edit button? or only for selected layout? and delete button?
// yes, even just to make clear what is custom
var showLayoutDeleteDialog by remember { mutableStateOf(false) }
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(SubtypeLocaleUtils.getDisplayNameInSystemLocale(it, currentSubtype.locale))
Row (verticalAlignment = Alignment.CenterVertically) {
Icon(painterResource(R.drawable.ic_edit), stringResource(R.string.edit_layout), Modifier.clickable { showLayoutEditDialog = it to null })
if (it in customLayouts && subtype.mainLayoutName() != it) // don't allow current main layout
Icon(painterResource(R.drawable.ic_bin), stringResource(R.string.delete), Modifier.clickable { showLayoutDeleteDialog = true })
}
}
if (showLayoutDeleteDialog) {
// todo: if layout used by other subtypes: either disable button, or explicitly mention in text
ConfirmationDialog(
onDismissRequest = { showLayoutDeleteDialog = false },
confirmButtonText = stringResource(R.string.delete),
title = { Text(stringResource(R.string.delete_layout, LayoutUtilsCustom.getDisplayName(it))) },
onConfirmed = {
if (it == currentSubtype.mainLayoutName())
currentSubtype = currentSubtype.withoutLayout(LayoutType.MAIN)
LayoutUtilsCustom.deleteLayout(it, LayoutType.MAIN, ctx)
}
)
}
}
// todo: should be in same row as DropDownField
// maybe make the default button more customizable
IconButton(
{ showAddLayoutDialog = true }
) { Icon(painterResource(R.drawable.ic_plus), stringResource(R.string.button_title_add_custom_layout)) }
if (showLayoutEditDialog != null) {
val layoutName = showLayoutEditDialog!!.first
val startContent = showLayoutEditDialog?.second
?: if (layoutName in appLayouts) LayoutUtils.getContent(LayoutType.MAIN, layoutName, ctx)
else null
LayoutEditDialog(
onDismissRequest = { showLayoutEditDialog = null },
layoutType = LayoutType.MAIN,
initialLayoutName = layoutName,
startContent = startContent,
locale = currentSubtype.locale,
// only can edit name for new custom layout
isNameValid = if (layoutName in customLayouts) null else ({ it !in customLayouts }),
onEdited = {
if (layoutName !in customLayouts)
currentSubtype = currentSubtype.withLayout(LayoutType.MAIN, it)
}
)
}
if (showAddLayoutDialog) {
// todo: maybe supply link to discussion section for layouts
// todo: no html for compose, so message is broken
// try annotatedString
val link = "<a href='$LAYOUT_FORMAT_URL'>" + ctx.getString(R.string.dictionary_link_text) + "</a>"
ConfirmationDialog(
onDismissRequest = { showAddLayoutDialog = false },
title = { Text(stringResource(R.string.button_title_add_custom_layout)) },
text = { Text(stringResource(R.string.message_add_custom_layout, link)) },
onConfirmed = { showLayoutEditDialog = "new layout" to "" },
neutralButtonText = stringResource(R.string.button_load_custom),
onNeutral = {
showAddLayoutDialog = false
layoutPicker.launch(layoutIntent)
}
)
}
}
if (availableLocalesForScript.size > 1) {
@ -128,12 +199,6 @@ fun SubtypeDialog(
}
}
}
WithSmallTitle("dictionaries") {
// todo: maybe remove here and use a separate screen for dictionary management
// would be clearer, as dicts are per language (and no intention to change it)
Text("not yet implemented")
}
// todo: this looks strange without the title
Row {
TextButton(onClick = { showKeyOrderDialog = true }, Modifier.weight(1f))
{ Text(stringResource(R.string.popup_order), style = MaterialTheme.typography.bodyLarge) }
@ -200,9 +265,22 @@ fun SubtypeDialog(
) {
val displayName = if (LayoutUtilsCustom.isCustomLayout(it)) LayoutUtilsCustom.getDisplayName(it)
else it.getStringResourceOrName("layout_", ctx)
Text(displayName)
// content is name, and if it's user layout there is an edit button
// also maybe there should be an "add" button similar to the old settings
var showLayoutEditDialog by remember { mutableStateOf(false) }
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(displayName, Modifier.padding(end = 8.dp))
if (LayoutUtilsCustom.isCustomLayout(it))
Icon(painterResource(R.drawable.ic_edit), stringResource(R.string.edit_layout), Modifier.clickable { showLayoutEditDialog = true })
}
if (showLayoutEditDialog)
LayoutEditDialog(
onDismissRequest = { showLayoutEditDialog = false },
layoutType = type,
initialLayoutName = it,
isNameValid = null
)
}
}
}
@ -325,7 +403,6 @@ private fun <T>DropDownField(
var expanded by remember { mutableStateOf(false) }
Box(
Modifier.clickable { expanded = !expanded }
//.border(2.dp, MaterialTheme.colorScheme.onSecondary)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
@ -376,3 +453,5 @@ private fun DefaultButton(
private fun getAvailableSecondaryLocales(context: Context, mainLocale: Locale): List<Locale> =
getDictionaryLocales(context).filter { it != mainLocale && it.script() == mainLocale.script() }
private const val LAYOUT_FORMAT_URL = "https://github.com/Helium314/HeliBoard/blob/main/layouts.md"

View file

@ -39,6 +39,7 @@ import helium314.keyboard.latin.utils.SubtypeUtilsAdditional
import helium314.keyboard.latin.utils.displayName
import helium314.keyboard.latin.utils.getActivity
import helium314.keyboard.latin.utils.locale
import helium314.keyboard.latin.utils.mainLayoutName
import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.settings.SearchScreen
import helium314.keyboard.settings.SettingsActivity
@ -55,6 +56,7 @@ fun LanguageScreen(
if ((b?.value ?: 0) < 0)
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
var selectedSubtype: InputMethodSubtype? by remember { mutableStateOf(null) } // todo: rememberSaveable? maybe with SettingsSubtype?
val enabledSubtypes = SubtypeSettings.getEnabledSubtypes(prefs)
SearchScreen(
onClickBack = onClickBack,
title = {
@ -93,7 +95,7 @@ fun LanguageScreen(
)
}
Switch(
checked = item in SubtypeSettings.getEnabledSubtypes(prefs),
checked = item in enabledSubtypes,
onCheckedChange = {
if (it) SubtypeSettings.addEnabledSubtype(prefs, item)
else SubtypeSettings.removeEnabledSubtype(ctx, item)
@ -105,7 +107,10 @@ fun LanguageScreen(
if (selectedSubtype != null) {
val oldSubtype = selectedSubtype!!
SubtypeDialog(
onDismissRequest = { selectedSubtype = null },
onDismissRequest = {
selectedSubtype = null
sortedSubtypes = getSortedSubtypes(ctx) // todo: not good, only necessary when a main layout is deleted
},
onConfirmed = {
SubtypeUtilsAdditional.changeAdditionalSubtype(oldSubtype.toSettingsSubtype(), it, ctx)
sortedSubtypes = getSortedSubtypes(ctx)