diff --git a/app/src/main/java/helium314/keyboard/latin/common/Constants.java b/app/src/main/java/helium314/keyboard/latin/common/Constants.java index c272df43e..4dedec0ec 100644 --- a/app/src/main/java/helium314/keyboard/latin/common/Constants.java +++ b/app/src/main/java/helium314/keyboard/latin/common/Constants.java @@ -92,6 +92,9 @@ public final class Constants { /** Overrides the general "more popups" setting */ public static final String MORE_POPUPS = "MorePopups"; + /** Overrides the general "localized number row" setting */ + public static final String LOCALIZED_NUMBER_ROW = "LocalizedNumberRow"; + private ExtraValue() { // This utility class is not publicly instantiable. } diff --git a/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt index 0ac9b258b..b4d78dd95 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt @@ -1,6 +1,7 @@ package helium314.keyboard.latin.utils import android.content.Context +import helium314.keyboard.latin.R import helium314.keyboard.latin.settings.Defaults.default import helium314.keyboard.latin.utils.LayoutType.Companion.folder import helium314.keyboard.latin.utils.ScriptUtils.script @@ -12,10 +13,11 @@ object LayoutUtils { if (layoutType != LayoutType.MAIN) return context.assets.list(layoutType.folder)?.map { it.substringBefore(".") }.orEmpty() if (locale == null) - return SubtypeSettings.getAllAvailableSubtypes().mapTo(HashSet()) { it.mainLayoutName()?.substringBefore("+") ?: "qwerty" } - if (locale.script() == ScriptUtils.SCRIPT_LATIN) - return SubtypeSettings.getAllAvailableSubtypes().filter { it.isAsciiCapable && !LayoutUtilsCustom.isCustomLayout(it.mainLayoutName() ?: "qwerty") } + return SubtypeSettings.getAllAvailableSubtypes() .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() } } diff --git a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtils.kt index dc0dd96a2..f5c2824dc 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtils.kt @@ -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.constructLocale import helium314.keyboard.latin.define.DebugFlags +import helium314.keyboard.latin.utils.LayoutType.Companion.toExtraValue import helium314.keyboard.latin.utils.ScriptUtils.script import org.xmlpull.v1.XmlPullParser import java.util.Locale @@ -64,22 +65,23 @@ fun getResourceSubtypes(resources: Resources): List { /** Workaround for SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale ignoring custom layout names */ // 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) if (LayoutUtilsCustom.isCustomLayout(layoutName)) return "${LocaleUtils.getLocaleDisplayNameInSystemLocale(locale(), context)} (${LayoutUtilsCustom.getDisplayName(layoutName)})" 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. * Resulting InputMethodSubtypes are equal if SettingsSubtypes are equal */ fun toAdditionalSubtype(): InputMethodSubtype? { 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")) { // Skip unknown keyboard layout subtype. This may happen when predefined keyboard // layout has been removed. @@ -89,7 +91,40 @@ data class SettingsSubtype(val locale: Locale, val extraValue: String) { 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 { 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? // 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 + // 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) }.joinToString(",") require(!filteredExtraValue.contains(Separators.SETS) && !filteredExtraValue.contains(Separators.SET)) diff --git a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt index 1a6702b9f..0c712ae94 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt @@ -51,6 +51,7 @@ object SubtypeUtilsAdditional { fun createEmojiCapableAdditionalSubtype(locale: Locale, mainLayoutName: String, asciiCapable: Boolean) = createAdditionalSubtype(locale, "${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN${Separators.KV}$mainLayoutName", asciiCapable, true) + // todo: consider using SettingsSubtype fun addAdditionalSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) { val oldAdditionalSubtypesString = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!! val additionalSubtypes = createAdditionalSubtypes(oldAdditionalSubtypesString).toMutableSet() @@ -67,6 +68,27 @@ object SubtypeUtilsAdditional { 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 { if (prefSubtypes.isEmpty()) return emptyList() diff --git a/app/src/main/java/helium314/keyboard/settings/SettingsNavHost.kt b/app/src/main/java/helium314/keyboard/settings/SettingsNavHost.kt index 60b3b0b70..875480563 100644 --- a/app/src/main/java/helium314/keyboard/settings/SettingsNavHost.kt +++ b/app/src/main/java/helium314/keyboard/settings/SettingsNavHost.kt @@ -16,9 +16,10 @@ import helium314.keyboard.settings.screens.AppearanceScreen import helium314.keyboard.settings.screens.ColorsScreen import helium314.keyboard.settings.screens.DebugScreen 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.PreferencesScreen +import helium314.keyboard.settings.screens.SecondaryLayoutScreen import helium314.keyboard.settings.screens.TextCorrectionScreen import helium314.keyboard.settings.screens.ToolbarScreen import kotlinx.coroutines.CoroutineScope @@ -92,10 +93,10 @@ fun SettingsNavHost( // ) } composable(SettingsDestination.Languages) { -// LanguageScreen(onClickBack = ::goBack) + LanguageScreen(onClickBack = ::goBack) } composable(SettingsDestination.Layouts) { - LayoutScreen(onClickBack = ::goBack) + SecondaryLayoutScreen(onClickBack = ::goBack) } composable(SettingsDestination.Colors) { ColorsScreen(isNight = false, onClickBack = ::goBack) diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/MultiListPickerDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/MultiListPickerDialog.kt new file mode 100644 index 000000000..24a05148b --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/dialogs/MultiListPickerDialog.kt @@ -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 MultiListPickerDialog( + onDismissRequest: () -> Unit, + items: List, + onConfirmed: (Collection) -> Unit, + modifier: Modifier = Modifier, + title: (@Composable () -> Unit)? = null, + initialSelection: List = 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" }, + ) +} diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/SubtypeDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/SubtypeDialog.kt new file mode 100644 index 000000000..374a7d9f8 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/dialogs/SubtypeDialog.kt @@ -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 DropDownField( + items: List, + 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 { + 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() +} diff --git a/app/src/main/java/helium314/keyboard/settings/screens/LanguageScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/LanguageScreen.kt new file mode 100644 index 000000000..f19fde7df --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/screens/LanguageScreen.kt @@ -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 { + 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( + { 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) +} diff --git a/app/src/main/java/helium314/keyboard/settings/screens/MainSettingsScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/MainSettingsScreen.kt index 42f87b878..cd596c64b 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/MainSettingsScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/MainSettingsScreen.kt @@ -23,7 +23,10 @@ import helium314.keyboard.latin.settings.LanguageSettingsFragment import helium314.keyboard.latin.settings.PreferencesSettingsFragment import helium314.keyboard.latin.settings.ToolbarSettingsFragment 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.prefs import helium314.keyboard.latin.utils.switchTo import helium314.keyboard.settings.preferences.Preference import helium314.keyboard.settings.preferences.PreferenceCategory @@ -49,9 +52,11 @@ fun MainSettingsScreen( title = stringResource(R.string.ime_settings), settings = emptyList(), ) { + val enabledSubtypes = SubtypeSettings.getEnabledSubtypes(ctx.prefs(), true) Column(Modifier.verticalScroll(rememberScrollState())) { Preference( name = stringResource(R.string.language_and_layouts_title), + description = enabledSubtypes.joinToString(", ") { it.displayName(ctx) }, onClick = onClickLanguage, icon = R.drawable.ic_settings_languages_foreground ) { @@ -118,7 +123,7 @@ fun MainSettingsScreen( ) } Preference( - name = stringResource(R.string.keyboard_layout_set), + name = stringResource(R.string.settings_screen_secondary_layouts), onClick = onClickLayouts, icon = R.drawable.ic_ime_switcher ) { diff --git a/app/src/main/java/helium314/keyboard/settings/screens/SecondaryLayoutScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/SecondaryLayoutScreen.kt index 72df95862..f6ffc995c 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/SecondaryLayoutScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/SecondaryLayoutScreen.kt @@ -28,20 +28,16 @@ import helium314.keyboard.settings.preferences.Preference fun SecondaryLayoutScreen( onClickBack: () -> Unit, ) { - // todo: enable main layouts - // which layouts to show? all is too much, maybe limit to latin and layouts for enabled locales (and system locales?) + // no main layouts in here + // could be added later, but need to decide how to do it (showing all main layouts is too much) SearchSettingsScreen( 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 } ) } -fun createLayoutSettings(context: Context) = listOf( - 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 -> +fun createLayoutSettings(context: Context) = LayoutType.entries.filter { it != LayoutType.MAIN }.map { layoutType -> Setting(context, Settings.PREF_LAYOUT_PREFIX + layoutType, layoutType.displayNameId) { setting -> val ctx = LocalContext.current val prefs = ctx.prefs() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 904937a2c..bca7242c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -539,8 +539,12 @@ disposition rather than other common dispositions for Latin languages. [CHAR LIM Customize symbols and number layouts Customize functional key layouts + + Secondary layouts Functional keys + + Functional keys (large screen) Functional keys (Symbols) diff --git a/app/src/test/java/helium314/keyboard/LayoutTest.kt b/app/src/test/java/helium314/keyboard/LayoutTest.kt new file mode 100644 index 000000000..5248ba588 --- /dev/null +++ b/app/src/test/java/helium314/keyboard/LayoutTest.kt @@ -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)) + } +} \ No newline at end of file