add language screen (not finished)

This commit is contained in:
Helium314 2025-02-16 22:58:19 +01:00
parent 12f1e20d9f
commit 85382de881
12 changed files with 709 additions and 20 deletions

View file

@ -92,6 +92,9 @@ public final class Constants {
/** Overrides the general "more popups" setting */ /** Overrides the general "more popups" setting */
public static final String MORE_POPUPS = "MorePopups"; public static final String MORE_POPUPS = "MorePopups";
/** Overrides the general "localized number row" setting */
public static final String LOCALIZED_NUMBER_ROW = "LocalizedNumberRow";
private ExtraValue() { private ExtraValue() {
// This utility class is not publicly instantiable. // This utility class is not publicly instantiable.
} }

View file

@ -1,6 +1,7 @@
package helium314.keyboard.latin.utils package helium314.keyboard.latin.utils
import android.content.Context import android.content.Context
import helium314.keyboard.latin.R
import helium314.keyboard.latin.settings.Defaults.default import helium314.keyboard.latin.settings.Defaults.default
import helium314.keyboard.latin.utils.LayoutType.Companion.folder import helium314.keyboard.latin.utils.LayoutType.Companion.folder
import helium314.keyboard.latin.utils.ScriptUtils.script import helium314.keyboard.latin.utils.ScriptUtils.script
@ -12,10 +13,11 @@ object LayoutUtils {
if (layoutType != LayoutType.MAIN) if (layoutType != LayoutType.MAIN)
return context.assets.list(layoutType.folder)?.map { it.substringBefore(".") }.orEmpty() return context.assets.list(layoutType.folder)?.map { it.substringBefore(".") }.orEmpty()
if (locale == null) if (locale == null)
return SubtypeSettings.getAllAvailableSubtypes().mapTo(HashSet()) { it.mainLayoutName()?.substringBefore("+") ?: "qwerty" } return SubtypeSettings.getAllAvailableSubtypes()
if (locale.script() == ScriptUtils.SCRIPT_LATIN)
return SubtypeSettings.getAllAvailableSubtypes().filter { it.isAsciiCapable && !LayoutUtilsCustom.isCustomLayout(it.mainLayoutName() ?: "qwerty") }
.mapTo(HashSet()) { it.mainLayoutName()?.substringBefore("+") ?: "qwerty" } .mapTo(HashSet()) { it.mainLayoutName()?.substringBefore("+") ?: "qwerty" }
.apply { addAll(context.resources.getStringArray(R.array.predefined_layouts)) }
if (locale.script() == ScriptUtils.SCRIPT_LATIN)
return context.resources.getStringArray(R.array.predefined_layouts).toList()
return SubtypeSettings.getSubtypesForLocale(locale).mapNotNullTo(HashSet()) { it.mainLayoutName() } return SubtypeSettings.getSubtypesForLocale(locale).mapNotNullTo(HashSet()) { it.mainLayoutName() }
} }

View file

@ -11,6 +11,7 @@ import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAY
import helium314.keyboard.latin.common.LocaleUtils import helium314.keyboard.latin.common.LocaleUtils
import helium314.keyboard.latin.common.LocaleUtils.constructLocale import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.define.DebugFlags import helium314.keyboard.latin.define.DebugFlags
import helium314.keyboard.latin.utils.LayoutType.Companion.toExtraValue
import helium314.keyboard.latin.utils.ScriptUtils.script import helium314.keyboard.latin.utils.ScriptUtils.script
import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParser
import java.util.Locale import java.util.Locale
@ -64,22 +65,23 @@ fun getResourceSubtypes(resources: Resources): List<InputMethodSubtype> {
/** Workaround for SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale ignoring custom layout names */ /** Workaround for SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale ignoring custom layout names */
// todo (later): this should be done properly and in SubtypeLocaleUtils // todo (later): this should be done properly and in SubtypeLocaleUtils
fun InputMethodSubtype.displayName(context: Context): CharSequence { fun InputMethodSubtype.displayName(context: Context): String {
val layoutName = SubtypeLocaleUtils.getMainLayoutName(this) val layoutName = SubtypeLocaleUtils.getMainLayoutName(this)
if (LayoutUtilsCustom.isCustomLayout(layoutName)) if (LayoutUtilsCustom.isCustomLayout(layoutName))
return "${LocaleUtils.getLocaleDisplayNameInSystemLocale(locale(), context)} (${LayoutUtilsCustom.getDisplayName(layoutName)})" return "${LocaleUtils.getLocaleDisplayNameInSystemLocale(locale(), context)} (${LayoutUtilsCustom.getDisplayName(layoutName)})"
return SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(this) return SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(this)
} }
data class SettingsSubtype(val locale: Locale, val extraValue: String) { // some kind of intermediate between the string stored in preferences and an InputMethodSubtype
data class SettingsSubtype(val locale: Locale, val extraValues: String) {
fun toPref() = locale.toLanguageTag() + Separators.SET + extraValue fun toPref() = locale.toLanguageTag() + Separators.SET + extraValues
/** Creates an additional subtype from the SettingsSubtype. /** Creates an additional subtype from the SettingsSubtype.
* Resulting InputMethodSubtypes are equal if SettingsSubtypes are equal */ * Resulting InputMethodSubtypes are equal if SettingsSubtypes are equal */
fun toAdditionalSubtype(): InputMethodSubtype? { fun toAdditionalSubtype(): InputMethodSubtype? {
val asciiCapable = locale.script() == ScriptUtils.SCRIPT_LATIN val asciiCapable = locale.script() == ScriptUtils.SCRIPT_LATIN
val subtype = SubtypeUtilsAdditional.createAdditionalSubtype(locale, extraValue, asciiCapable, true) val subtype = SubtypeUtilsAdditional.createAdditionalSubtype(locale, extraValues, asciiCapable, true)
if (subtype.nameResId == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT && !LayoutUtilsCustom.isCustomLayout(mainLayoutName() ?: "qwerty")) { if (subtype.nameResId == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT && !LayoutUtilsCustom.isCustomLayout(mainLayoutName() ?: "qwerty")) {
// Skip unknown keyboard layout subtype. This may happen when predefined keyboard // Skip unknown keyboard layout subtype. This may happen when predefined keyboard
// layout has been removed. // layout has been removed.
@ -89,7 +91,40 @@ data class SettingsSubtype(val locale: Locale, val extraValue: String) {
return subtype return subtype
} }
fun mainLayoutName() = LayoutType.getMainLayoutFromExtraValue(extraValue) fun mainLayoutName() = LayoutType.getMainLayoutFromExtraValue(extraValues)
fun layoutName(type: LayoutType) = LayoutType.getLayoutMap(getExtraValueOf(KEYBOARD_LAYOUT_SET) ?: "")[type]
fun with(extraValueKey: String, extraValue: String?): SettingsSubtype {
val newList = extraValues.split(",")
.filterNot { it.startsWith("$extraValueKey=") || it == extraValueKey }
val newValue = if (extraValue == null) extraValueKey else "$extraValueKey=$extraValue"
val newValues = (newList + newValue).joinToString(",")
return copy(extraValues = newValues)
}
fun without(extraValueKey: String): SettingsSubtype {
val newValues = extraValues.split(",")
.filterNot { it.startsWith("$extraValueKey=") || it == extraValueKey }
.joinToString(",")
return copy(extraValues = newValues)
}
fun getExtraValueOf(extraValueKey: String): String? = extraValues.split(",")
.firstOrNull { it.startsWith("$extraValueKey=") }?.substringAfter("$extraValueKey=")
fun withLayout(type: LayoutType, name: String): SettingsSubtype {
val map = LayoutType.getLayoutMap(getExtraValueOf(KEYBOARD_LAYOUT_SET) ?: "")
map[type] = name
return with(KEYBOARD_LAYOUT_SET, map.toExtraValue())
}
fun withoutLayout(type: LayoutType): SettingsSubtype {
val map = LayoutType.getLayoutMap(getExtraValueOf(KEYBOARD_LAYOUT_SET) ?: "")
map.remove(type)
return if (map.isEmpty()) without(KEYBOARD_LAYOUT_SET)
else with(KEYBOARD_LAYOUT_SET, map.toExtraValue())
}
companion object { companion object {
fun String.toSettingsSubtype() = fun String.toSettingsSubtype() =
@ -109,6 +144,8 @@ data class SettingsSubtype(val locale: Locale, val extraValue: String) {
// todo: this is in "old" additional subtypes, but where was it set? // todo: this is in "old" additional subtypes, but where was it set?
// must have been by app in 2.3, but not any more? // must have been by app in 2.3, but not any more?
// anyway, a. we can easily create it again, and b. it may contain "bad" characters messing up the extra value // anyway, a. we can easily create it again, and b. it may contain "bad" characters messing up the extra value
// removing UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME changes the name of some layouts,
// e.g. from "English (United States)" to "English (US)"
|| it.startsWith(ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME) || it.startsWith(ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME)
}.joinToString(",") }.joinToString(",")
require(!filteredExtraValue.contains(Separators.SETS) && !filteredExtraValue.contains(Separators.SET)) require(!filteredExtraValue.contains(Separators.SETS) && !filteredExtraValue.contains(Separators.SET))

View file

@ -51,6 +51,7 @@ object SubtypeUtilsAdditional {
fun createEmojiCapableAdditionalSubtype(locale: Locale, mainLayoutName: String, asciiCapable: Boolean) = fun createEmojiCapableAdditionalSubtype(locale: Locale, mainLayoutName: String, asciiCapable: Boolean) =
createAdditionalSubtype(locale, "${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN${Separators.KV}$mainLayoutName", asciiCapable, true) createAdditionalSubtype(locale, "${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN${Separators.KV}$mainLayoutName", asciiCapable, true)
// todo: consider using SettingsSubtype
fun addAdditionalSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) { fun addAdditionalSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) {
val oldAdditionalSubtypesString = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!! val oldAdditionalSubtypesString = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!
val additionalSubtypes = createAdditionalSubtypes(oldAdditionalSubtypesString).toMutableSet() val additionalSubtypes = createAdditionalSubtypes(oldAdditionalSubtypesString).toMutableSet()
@ -67,6 +68,27 @@ object SubtypeUtilsAdditional {
Settings.writePrefAdditionalSubtypes(prefs, newAdditionalSubtypesString) Settings.writePrefAdditionalSubtypes(prefs, newAdditionalSubtypesString)
} }
// updates additional subtypes, enabled subtypes, and selected subtype
fun changeAdditionalSubtype(from: SettingsSubtype, to: SettingsSubtype, prefs: SharedPreferences) {
val new = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!
.split(Separators.SETS).mapTo(sortedSetOf()) {
if (it == from.toPref()) to.toPref() else it
}
prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, new.joinToString(Separators.SETS)).apply()
val fromSubtype = from.toAdditionalSubtype() // will be null if we edit a resource subtype
val toSubtype = to.toAdditionalSubtype() // should never be null
if (SubtypeSettings.getSelectedSubtype(prefs) == fromSubtype && toSubtype != null) {
SubtypeSettings.setSelectedSubtype(prefs, toSubtype)
}
if (SubtypeSettings.getEnabledSubtypes(prefs, false).contains(fromSubtype)) {
if (fromSubtype != null)
SubtypeSettings.removeEnabledSubtype(prefs, fromSubtype)
if (toSubtype != null)
SubtypeSettings.addEnabledSubtype(prefs, toSubtype)
}
}
fun createAdditionalSubtypes(prefSubtypes: String): List<InputMethodSubtype> { fun createAdditionalSubtypes(prefSubtypes: String): List<InputMethodSubtype> {
if (prefSubtypes.isEmpty()) if (prefSubtypes.isEmpty())
return emptyList() return emptyList()

View file

@ -16,9 +16,10 @@ import helium314.keyboard.settings.screens.AppearanceScreen
import helium314.keyboard.settings.screens.ColorsScreen import helium314.keyboard.settings.screens.ColorsScreen
import helium314.keyboard.settings.screens.DebugScreen import helium314.keyboard.settings.screens.DebugScreen
import helium314.keyboard.settings.screens.GestureTypingScreen import helium314.keyboard.settings.screens.GestureTypingScreen
import helium314.keyboard.settings.screens.LayoutScreen import helium314.keyboard.settings.screens.LanguageScreen
import helium314.keyboard.settings.screens.MainSettingsScreen import helium314.keyboard.settings.screens.MainSettingsScreen
import helium314.keyboard.settings.screens.PreferencesScreen import helium314.keyboard.settings.screens.PreferencesScreen
import helium314.keyboard.settings.screens.SecondaryLayoutScreen
import helium314.keyboard.settings.screens.TextCorrectionScreen import helium314.keyboard.settings.screens.TextCorrectionScreen
import helium314.keyboard.settings.screens.ToolbarScreen import helium314.keyboard.settings.screens.ToolbarScreen
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -92,10 +93,10 @@ fun SettingsNavHost(
// ) // )
} }
composable(SettingsDestination.Languages) { composable(SettingsDestination.Languages) {
// LanguageScreen(onClickBack = ::goBack) LanguageScreen(onClickBack = ::goBack)
} }
composable(SettingsDestination.Layouts) { composable(SettingsDestination.Layouts) {
LayoutScreen(onClickBack = ::goBack) SecondaryLayoutScreen(onClickBack = ::goBack)
} }
composable(SettingsDestination.Colors) { composable(SettingsDestination.Colors) {
ColorsScreen(isNight = false, onClickBack = ::goBack) ColorsScreen(isNight = false, onClickBack = ::goBack)

View file

@ -0,0 +1,94 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings.dialogs
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
// modified version of ListPickerDialog for selecting multiple items
@Composable
fun <T: Any> MultiListPickerDialog(
onDismissRequest: () -> Unit,
items: List<T>,
onConfirmed: (Collection<T>) -> Unit,
modifier: Modifier = Modifier,
title: (@Composable () -> Unit)? = null,
initialSelection: List<T> = emptyList(),
getItemName: (@Composable (T) -> String) = { it.toString() },
) {
var selected by remember { mutableStateOf(initialSelection.toSet()) }
val state = rememberLazyListState()
ThreeButtonAlertDialog(
onDismissRequest = onDismissRequest,
onConfirmed = { onConfirmed(selected) },
confirmButtonText = stringResource(android.R.string.ok),
modifier = modifier,
title = title,
text = {
CompositionLocalProvider(
LocalTextStyle provides MaterialTheme.typography.bodyLarge
) {
LazyColumn(state = state) {
items(items) { item ->
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable {
selected = if (item in selected) selected - item else selected + item
}
.padding(horizontal = 16.dp)
.heightIn(min = 40.dp)
) {
Text(
text = getItemName(item),
modifier = Modifier.weight(1f),
)
Switch(
checked = item in selected,
onCheckedChange = {
selected = if (it) selected + item else selected - item
}
)
}
}
}
}
},
)
}
@Preview
@Composable
private fun Preview() {
val items = remember { (0..<5).toList() }
MultiListPickerDialog(
onDismissRequest = {},
items = items,
onConfirmed = {},
title = { Text("Select something") },
initialSelection = listOf(2, 4),
getItemName = { "Item $it" },
)
}

View file

@ -0,0 +1,362 @@
package helium314.keyboard.settings.dialogs
import android.content.Context
import android.view.inputmethod.InputMethodSubtype
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
import helium314.keyboard.keyboard.internal.keyboard_parser.hasLocalizedNumberRow
import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.Constants.Separators
import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue
import helium314.keyboard.latin.common.LocaleUtils
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.LayoutType.Companion.displayNameId
import helium314.keyboard.latin.utils.LayoutUtils
import helium314.keyboard.latin.utils.LayoutUtilsCustom
import helium314.keyboard.latin.utils.ScriptUtils.SCRIPT_LATIN
import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.SettingsSubtype
import helium314.keyboard.latin.utils.SettingsSubtype.Companion.toSettingsSubtype
import helium314.keyboard.latin.utils.SubtypeLocaleUtils
import helium314.keyboard.latin.utils.SubtypeSettings
import helium314.keyboard.latin.utils.SubtypeUtilsAdditional
import helium314.keyboard.latin.utils.getDictionaryLocales
import helium314.keyboard.latin.utils.getStringResourceOrName
import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.settings.screens.GetIcon
import java.util.Locale
// todo:
// save when "editing" a resource subtypes is not working
// default buttons missing
// string resources
@Composable
fun SubtypeDialog(
// could also use InputMethodSubtype if there is any advantage
// but as soon as anything is changed we will need an additional subtype anyway...
onDismissRequest: () -> Unit,
subtype: InputMethodSubtype,
onConfirmed: (SettingsSubtype) -> Unit,
) {
// todo: make sure the values are always correct (e.g. if using rememberSaveable and rotating)
val ctx = LocalContext.current
val prefs = ctx.prefs()
var currentSubtype by remember { mutableStateOf(subtype.toSettingsSubtype()) }
val availableLocalesForScript = getAvailableSecondaryLocales(ctx, currentSubtype.locale).sortedBy { it.toLanguageTag() }
var showSecondaryLocaleDialog by remember { mutableStateOf(false) }
var showKeyOrderDialog by remember { mutableStateOf(false) }
var showHintOrderDialog by remember { mutableStateOf(false) }
var showMorePopupsDialog by remember { mutableStateOf(false) }
val scrollState = rememberScrollState()
ThreeButtonAlertDialog(
onDismissRequest = onDismissRequest,
onConfirmed = { onConfirmed(currentSubtype) },
neutralButtonText = if (SubtypeSettings.isAdditionalSubtype(subtype)) null else stringResource(R.string.delete),
onNeutral = {
SubtypeUtilsAdditional.removeAdditionalSubtype(prefs, subtype)
SubtypeSettings.removeEnabledSubtype(prefs, subtype)
}, // maybe confirm dialog?
title = { Text(SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype)) },
text = {
Column(
modifier = Modifier.verticalScroll(scrollState),
verticalArrangement = Arrangement.SpaceBetween
) {
WithDescription("main layout") {
val appLayouts = LayoutUtils.getAvailableLayouts(LayoutType.MAIN, ctx, currentSubtype.locale)
val customLayouts = LayoutUtilsCustom.getLayoutFiles(LayoutType.MAIN, ctx, currentSubtype.locale).map { it.name }
DropDownField(
items = appLayouts + customLayouts,
selectedItem = currentSubtype.mainLayoutName() ?: "qwerty", // todo: what about qwerty+ and similar?
onSelected = {
currentSubtype = currentSubtype.withLayout(LayoutType.MAIN, it)
}
) {
// todo: displayName can be complicated and may require an inputmehtodsubtype...
// maybe search for stuff in resource subtypes?
Text(it)
// todo: edit button? or only for selected layout? and delete button?
}
}
WithDescription(stringResource(R.string.secondary_locale)) {
TextButton(onClick = { showSecondaryLocaleDialog = true }, Modifier.fillMaxWidth()) {
val text = currentSubtype.getExtraValueOf(ExtraValue.SECONDARY_LOCALES)
?.split(Separators.KV)?.joinToString(", ") {
LocaleUtils.getLocaleDisplayNameInSystemLocale(it.constructLocale(), ctx)
} ?: "none"
Text(text, style = MaterialTheme.typography.bodyLarge)
}
}
TextButton(onClick = { showSecondaryLocaleDialog = true }) {
val text = currentSubtype.getExtraValueOf(ExtraValue.SECONDARY_LOCALES)
?.split(Separators.KV)?.joinToString(", ") {
LocaleUtils.getLocaleDisplayNameInSystemLocale(it.constructLocale(), ctx)
} ?: ""
Column(Modifier.fillMaxWidth()) {
Text(stringResource(R.string.secondary_locale))
Text(text, style = MaterialTheme.typography.bodyLarge)
}
}
WithDescription("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")
}
TextButton(onClick = { showKeyOrderDialog = true })
{ Text(stringResource(R.string.popup_order), Modifier.fillMaxWidth(), style = MaterialTheme.typography.bodyLarge) }
TextButton(onClick = { showHintOrderDialog = true })
{ Text(stringResource(R.string.hint_source), Modifier.fillMaxWidth(), style = MaterialTheme.typography.bodyLarge) }
if (currentSubtype.locale.script() == SCRIPT_LATIN)
WithDescription(stringResource(R.string.show_popup_keys_title)) {
val explicitValue = currentSubtype.getExtraValueOf(ExtraValue.MORE_POPUPS)
val value = explicitValue ?: prefs.getString(Settings.PREF_MORE_POPUP_KEYS, Defaults.PREF_MORE_POPUP_KEYS)
val textResId = when (value) { // todo: this should not be duplicated... see below
"normal" -> R.string.show_popup_keys_normal
"more" -> R.string.show_popup_keys_more
"all" -> R.string.show_popup_keys_all
else -> R.string.show_popup_keys_main
}
TextButton(onClick = { showMorePopupsDialog = true }, Modifier.fillMaxWidth())
{ Text(stringResource(textResId)) }
}
if (hasLocalizedNumberRow(currentSubtype.locale, ctx))
Row {
Text(stringResource(R.string.localized_number_row), Modifier.weight(1f))
Switch(
checked = currentSubtype.getExtraValueOf(ExtraValue.LOCALIZED_NUMBER_ROW)?.toBoolean()
?: prefs.getBoolean(Settings.PREF_LOCALIZED_NUMBER_ROW, Defaults.PREF_LOCALIZED_NUMBER_ROW),
onCheckedChange = {
currentSubtype = currentSubtype.with(ExtraValue.LOCALIZED_NUMBER_ROW, it.toString())
}
)
// todo: default button?
}
LayoutType.entries.forEach { type ->
if (type == LayoutType.MAIN) return@forEach
// todo: also some default button, to be shown when necessary, uses currentSubtype.withoutLayout(type)
WithDescription(stringResource(type.displayNameId)) {
val explicitLayout = currentSubtype.layoutName(type)
val layout = explicitLayout ?: Settings.readDefaultLayoutName(type, prefs)
val defaultLayouts = LayoutUtils.getAvailableLayouts(type, ctx)
val customLayouts = LayoutUtilsCustom.getLayoutFiles(type, ctx).map { it.name }
DropDownField(
items = defaultLayouts + customLayouts,
selectedItem = layout,
onSelected = {
currentSubtype = currentSubtype.withLayout(type, it)
}
) {
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
}
}
}
}
}
)
if (showSecondaryLocaleDialog)
MultiListPickerDialog(
onDismissRequest = { showSecondaryLocaleDialog = false },
onConfirmed = {
val newValue = it.joinToString(Separators.KV) { it.toLanguageTag() }
currentSubtype = if (newValue.isEmpty()) currentSubtype.without(ExtraValue.SECONDARY_LOCALES)
else currentSubtype.with(ExtraValue.SECONDARY_LOCALES, newValue)
},
items = availableLocalesForScript,
initialSelection = currentSubtype.getExtraValueOf(ExtraValue.SECONDARY_LOCALES)
?.split(Separators.KV)?.map { it.constructLocale() }.orEmpty(),
getItemName = { LocaleUtils.getLocaleDisplayNameInSystemLocale(it, ctx) }
)
if (showKeyOrderDialog) {
val setting = currentSubtype.getExtraValueOf(ExtraValue.POPUP_ORDER)
PopupOrderDialog(
onDismissRequest = { showKeyOrderDialog = false },
initialValue = setting ?: prefs.getString(Settings.PREF_POPUP_KEYS_ORDER, Defaults.PREF_POPUP_KEYS_ORDER)!!,
title = stringResource(R.string.popup_order),
showDefault = setting != null,
onConfirmed = {
if (it == null) currentSubtype = currentSubtype.without(ExtraValue.POPUP_ORDER)
else currentSubtype = currentSubtype.with(ExtraValue.POPUP_ORDER, it)
}
)
}
if (showHintOrderDialog) {
val setting = currentSubtype.getExtraValueOf(ExtraValue.HINT_ORDER)
PopupOrderDialog(
onDismissRequest = { showHintOrderDialog = false },
initialValue = setting ?: prefs.getString(Settings.PREF_POPUP_KEYS_LABELS_ORDER, Defaults.PREF_POPUP_KEYS_LABELS_ORDER)!!,
title = stringResource(R.string.hint_source),
showDefault = setting != null,
onConfirmed = {
if (it == null) currentSubtype = currentSubtype.without(ExtraValue.HINT_ORDER)
else currentSubtype = currentSubtype.with(ExtraValue.HINT_ORDER, it)
}
)
}
if (showMorePopupsDialog) {
// todo: default button in here? or next to the pref?
val items = listOf("normal", "main", "more", "all")
val explicitValue = currentSubtype.getExtraValueOf(ExtraValue.MORE_POPUPS)
val value = explicitValue ?: prefs.getString(Settings.PREF_MORE_POPUP_KEYS, Defaults.PREF_MORE_POPUP_KEYS)
ListPickerDialog(
onDismissRequest = { showMorePopupsDialog = false },
items = items,
getItemName = {
val textResId = when (it) { // todo: this should not be duplicated... now we have it twice here, and in advanced settings
"normal" -> R.string.show_popup_keys_normal
"more" -> R.string.show_popup_keys_more
"all" -> R.string.show_popup_keys_all
else -> R.string.show_popup_keys_main
}
stringResource(textResId)
},
selectedItem = value,
onItemSelected = { currentSubtype = currentSubtype.with(ExtraValue.MORE_POPUPS, it) }
)
}
}
// from ReorderSwitchPreference
@Composable
private fun PopupOrderDialog(
onDismissRequest: () -> Unit,
initialValue: String,
onConfirmed: (String?) -> Unit,
title: String,
showDefault: Boolean
) {
class KeyAndState(var name: String, var state: Boolean)
val items = initialValue.split(Separators.ENTRY).map {
KeyAndState(it.substringBefore(Separators.KV), it.substringAfter(Separators.KV).toBoolean())
}
val ctx = LocalContext.current
ReorderDialog(
onConfirmed = { reorderedItems ->
val value = reorderedItems.joinToString(Separators.ENTRY) { it.name + Separators.KV + it.state }
onConfirmed(value)
},
onDismissRequest = onDismissRequest,
onNeutral = { onDismissRequest(); onConfirmed(null) },
neutralButtonText = if (showDefault) stringResource(R.string.button_default) else null,
items = items,
title = { Text(title) },
displayItem = { item ->
var checked by rememberSaveable { mutableStateOf(item.state) }
Row(verticalAlignment = Alignment.CenterVertically) {
KeyboardIconsSet.instance.GetIcon(item.name)
val text = item.name.lowercase().getStringResourceOrName("", ctx)
Text(text, Modifier.weight(1f))
Switch(
checked = checked,
onCheckedChange = { item.state = it; checked = it }
)
}
},
getKey = { it.name }
)
}
@Composable
private fun WithDescription(
description: String,
content: @Composable () -> Unit,
) {
Column {
Text(description, style = MaterialTheme.typography.bodySmall)
content()
}
}
@Composable
private fun <T>DropDownField(
items: List<T>,
selectedItem: T,
onSelected: (T) -> Unit,
itemContent: @Composable (T) -> Unit,
) {
var expanded by remember { mutableStateOf(false) }
Box(
Modifier.clickable { expanded = !expanded }
//.border(2.dp, MaterialTheme.colorScheme.onSecondary)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 4.dp)
) {
Box(Modifier.weight(1f)) {
itemContent(selectedItem)
}
IconButton(
onClick = { expanded = !expanded },
enabled = items.size > 1
) {
Icon(
painterResource(R.drawable.ic_arrow_left),
null,
Modifier.rotate(-90f)
)
}
}
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
items.forEach {
DropdownMenuItem(
text = { itemContent(it) },
onClick = { expanded = false; onSelected(it) }
)
}
}
}
// get locales with same script as main locale, but different language
// todo: do we need any sort of force-ascii like in old variant?
// now we use hi-Latn and sr-Latn for the relevant subtypes, so it should be fine
// only potential issue is the Latn-default if we don't have the script for a locale,
// but in that case we should rather add the script to ScriptUtils
private fun getAvailableSecondaryLocales(context: Context, mainLocale: Locale): List<Locale> {
val locales = getDictionaryLocales(context)
locales.removeAll {
// it.language == mainLocale.language || it.script() != mainLocale.script()
it == mainLocale || it.script() != mainLocale.script() // todo: check whether this is fine, otherwise go back to the variant above
}
return locales.toList()
}

View file

@ -0,0 +1,150 @@
package helium314.keyboard.settings.screens
import android.content.Context
import android.os.Build
import android.view.inputmethod.InputMethodSubtype
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.Constants.Separators
import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue
import helium314.keyboard.latin.common.LocaleUtils
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.common.splitOnWhitespace
import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.USER_DICTIONARY_SUFFIX
import helium314.keyboard.latin.utils.DictionaryInfoUtils
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.SettingsSubtype.Companion.toSettingsSubtype
import helium314.keyboard.latin.utils.SubtypeSettings
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.prefs
import helium314.keyboard.settings.SearchScreen
import helium314.keyboard.settings.SettingsActivity
import helium314.keyboard.settings.dialogs.SubtypeDialog
@Composable
fun LanguageScreen(
onClickBack: () -> Unit,
) {
val ctx = LocalContext.current
var sortedSubtypes by remember { mutableStateOf(getSortedSubtypes(ctx)) }
val prefs = ctx.prefs()
val b = (LocalContext.current.getActivity() as? SettingsActivity)?.prefChanged?.collectAsState()
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?
SearchScreen(
onClickBack = onClickBack,
title = {
Column {
Text(stringResource(R.string.language_and_layouts_title))
Text(stringResource(
R.string.text_tap_languages),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
filteredItems = { term ->
// todo: maybe better performance with display name cache?
sortedSubtypes.filter {
it.displayName(ctx).replace("(", "")
.splitOnWhitespace().any { it.startsWith(term, true) }
}
},
itemContent = { item ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { selectedSubtype = item }
.padding(vertical = 6.dp, horizontal = 16.dp)
) {
Column(modifier = Modifier.weight(1f)) {
Text(item.displayName(ctx), style = MaterialTheme.typography.bodyLarge)
val description = item.getExtraValueOf(ExtraValue.SECONDARY_LOCALES)?.split(Separators.KV)
?.joinToString(", ") { LocaleUtils.getLocaleDisplayNameInSystemLocale(it.constructLocale(), ctx) }
if (description != null)
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Switch(
checked = item in SubtypeSettings.getEnabledSubtypes(prefs),
onCheckedChange = {
if (it) SubtypeSettings.addEnabledSubtype(prefs, item)
else SubtypeSettings.removeEnabledSubtype(prefs, item)
}
)
}
}
)
if (selectedSubtype != null) {
val oldSubtype = selectedSubtype!!
SubtypeDialog(
onDismissRequest = { selectedSubtype = null },
onConfirmed = {
// todo: this does not work when "modifying" a resource subtype
SubtypeUtilsAdditional.changeAdditionalSubtype(oldSubtype.toSettingsSubtype(), it, prefs)
sortedSubtypes = getSortedSubtypes(ctx)
},
subtype = oldSubtype
)
}
}
// todo: sorting is slow, need to cache displayName (overall or just in getSortedSubtypes), and then it should be fine
private fun getSortedSubtypes(context: Context): List<InputMethodSubtype> {
val systemLocales = SubtypeSettings.getSystemLocales()
val enabledSubtypes = SubtypeSettings.getEnabledSubtypes(context.prefs(), true)
val localesWithDictionary = DictionaryInfoUtils.getCachedDirectoryList(context)?.mapNotNull { dir ->
if (!dir.isDirectory)
return@mapNotNull null
if (dir.list()?.any { it.endsWith(USER_DICTIONARY_SUFFIX) } == true)
dir.name.constructLocale()
else null
}.orEmpty()
val defaultAdditionalSubtypes = Defaults.PREF_ADDITIONAL_SUBTYPES.split(Separators.SETS).map {
it.substringBefore(Separators.SET) to (it.substringAfter(Separators.SET) + ",AsciiCapable,EmojiCapable,isAdditionalSubtype")
}
fun isDefaultSubtype(subtype: InputMethodSubtype): Boolean =
defaultAdditionalSubtypes.any { it.first == subtype.locale().language && it.second == subtype.extraValue }
val subtypeSortComparator = compareBy<InputMethodSubtype>(
{ it !in enabledSubtypes },
{ it.locale() !in localesWithDictionary },
{ it.locale() !in systemLocales},
{ !(SubtypeSettings.isAdditionalSubtype(it) && !isDefaultSubtype(it) ) },
{
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) it.languageTag == "zz"
else it.locale == "zz"
},
{ it.displayName(context) }
)
return SubtypeSettings.getAllAvailableSubtypes().sortedWith(subtypeSortComparator)
}

View file

@ -23,7 +23,10 @@ import helium314.keyboard.latin.settings.LanguageSettingsFragment
import helium314.keyboard.latin.settings.PreferencesSettingsFragment import helium314.keyboard.latin.settings.PreferencesSettingsFragment
import helium314.keyboard.latin.settings.ToolbarSettingsFragment import helium314.keyboard.latin.settings.ToolbarSettingsFragment
import helium314.keyboard.latin.utils.JniUtils import helium314.keyboard.latin.utils.JniUtils
import helium314.keyboard.latin.utils.SubtypeSettings
import helium314.keyboard.latin.utils.displayName
import helium314.keyboard.latin.utils.getActivity import helium314.keyboard.latin.utils.getActivity
import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.latin.utils.switchTo import helium314.keyboard.latin.utils.switchTo
import helium314.keyboard.settings.preferences.Preference import helium314.keyboard.settings.preferences.Preference
import helium314.keyboard.settings.preferences.PreferenceCategory import helium314.keyboard.settings.preferences.PreferenceCategory
@ -49,9 +52,11 @@ fun MainSettingsScreen(
title = stringResource(R.string.ime_settings), title = stringResource(R.string.ime_settings),
settings = emptyList(), settings = emptyList(),
) { ) {
val enabledSubtypes = SubtypeSettings.getEnabledSubtypes(ctx.prefs(), true)
Column(Modifier.verticalScroll(rememberScrollState())) { Column(Modifier.verticalScroll(rememberScrollState())) {
Preference( Preference(
name = stringResource(R.string.language_and_layouts_title), name = stringResource(R.string.language_and_layouts_title),
description = enabledSubtypes.joinToString(", ") { it.displayName(ctx) },
onClick = onClickLanguage, onClick = onClickLanguage,
icon = R.drawable.ic_settings_languages_foreground icon = R.drawable.ic_settings_languages_foreground
) { ) {
@ -118,7 +123,7 @@ fun MainSettingsScreen(
) )
} }
Preference( Preference(
name = stringResource(R.string.keyboard_layout_set), name = stringResource(R.string.settings_screen_secondary_layouts),
onClick = onClickLayouts, onClick = onClickLayouts,
icon = R.drawable.ic_ime_switcher icon = R.drawable.ic_ime_switcher
) { ) {

View file

@ -28,20 +28,16 @@ import helium314.keyboard.settings.preferences.Preference
fun SecondaryLayoutScreen( fun SecondaryLayoutScreen(
onClickBack: () -> Unit, onClickBack: () -> Unit,
) { ) {
// todo: enable main layouts // no main layouts in here
// which layouts to show? all is too much, maybe limit to latin and layouts for enabled locales (and system locales?) // could be added later, but need to decide how to do it (showing all main layouts is too much)
SearchSettingsScreen( SearchSettingsScreen(
onClickBack = onClickBack, onClickBack = onClickBack,
title = stringResource(R.string.keyboard_layout_set), title = stringResource(R.string.settings_screen_secondary_layouts),
settings = LayoutType.entries.filter { it != LayoutType.MAIN }.map { Settings.PREF_LAYOUT_PREFIX + it.name } settings = LayoutType.entries.filter { it != LayoutType.MAIN }.map { Settings.PREF_LAYOUT_PREFIX + it.name }
) )
} }
fun createLayoutSettings(context: Context) = listOf( fun createLayoutSettings(context: Context) = LayoutType.entries.filter { it != LayoutType.MAIN }.map { layoutType ->
Setting(context, Settings.PREF_LAYOUT_PREFIX + LayoutType.MAIN, R.string.customize_functional_key_layouts) { // todo: title
// todo: actual content
},
) + LayoutType.entries.filter { it != LayoutType.MAIN }.map { layoutType ->
Setting(context, Settings.PREF_LAYOUT_PREFIX + layoutType, layoutType.displayNameId) { setting -> Setting(context, Settings.PREF_LAYOUT_PREFIX + layoutType, layoutType.displayNameId) { setting ->
val ctx = LocalContext.current val ctx = LocalContext.current
val prefs = ctx.prefs() val prefs = ctx.prefs()

View file

@ -539,8 +539,12 @@ disposition rather than other common dispositions for Latin languages. [CHAR LIM
<string name="customize_symbols_number_layouts">Customize symbols and number layouts</string> <string name="customize_symbols_number_layouts">Customize symbols and number layouts</string>
<!-- Title for customizing functional key layouts --> <!-- Title for customizing functional key layouts -->
<string name="customize_functional_key_layouts">Customize functional key layouts</string> <string name="customize_functional_key_layouts">Customize functional key layouts</string>
<!-- Settings screen title for secondary layouts (functional keys, symbols, numpad, ...) -->
<string name="settings_screen_secondary_layouts">Secondary layouts</string>
<!-- Name for functional keys layout --> <!-- Name for functional keys layout -->
<string name="layout_functional_keys" tools:keep="@string/layout_functional_keys">Functional keys</string> <string name="layout_functional_keys" tools:keep="@string/layout_functional_keys">Functional keys</string>
<!-- Name for functional keys layout for tablets -->
<string name="layout_functional_keys_tablet" tools:keep="@string/layout_functional_keys_tablet">Functional keys (large screen)</string>
<!-- Name for functional keys layout when showing symbols layout --> <!-- Name for functional keys layout when showing symbols layout -->
<string name="layout_functional_keys_symbols" tools:keep="@string/layout_functional_keys_symbols">Functional keys (Symbols)</string> <string name="layout_functional_keys_symbols" tools:keep="@string/layout_functional_keys_symbols">Functional keys (Symbols)</string>
<!-- Name for functional keys layout when showing more symbols layout --> <!-- Name for functional keys layout when showing more symbols layout -->

View file

@ -0,0 +1,13 @@
package helium314.keyboard
import helium314.keyboard.latin.utils.LayoutType
import kotlin.test.Test
import kotlin.test.assertEquals
class LayoutTest {
// todo: add more
@Test fun extraValueToMainLayout() {
val extraValue = "KeyboardLayoutSet=MAIN:qwertz+,SupportTouchPositionCorrection"
assertEquals("qwertz+", LayoutType.getMainLayoutFromExtraValue(extraValue))
}
}