add layout screen for choosing and editing default (non-main) layouts

This commit is contained in:
Helium314 2025-02-15 10:52:56 +01:00
parent 6b86ea236b
commit a3e85bc664
24 changed files with 478 additions and 82 deletions

View file

@ -17,11 +17,10 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.floris.TextKeyData
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.VariationSelector
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.toTextKey
import helium314.keyboard.latin.common.splitOnWhitespace
import helium314.keyboard.latin.settings.Defaults.default
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.CUSTOM_LAYOUT_PREFIX
import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.LayoutType.Companion.folder
import helium314.keyboard.latin.utils.LayoutUtils
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.getCustomLayoutFiles
import helium314.keyboard.latin.utils.prefs
@ -40,7 +39,7 @@ object LayoutParser {
if (layoutType == LayoutType.FUNCTIONAL && !params.mId.isAlphaOrSymbolKeyboard)
return mutableListOf(mutableListOf()) // no functional keys
val layoutName = if (layoutType == LayoutType.MAIN) params.mId.mSubtype.mainLayoutName
else params.mId.mSubtype.layouts[layoutType] ?: Settings.getLayoutName(layoutType, context.prefs())
else params.mId.mSubtype.layouts[layoutType] ?: Settings.readDefaultLayoutName(layoutType, context.prefs())
return layoutCache.getOrPut(layoutType.name + layoutName) {
createCacheLambda(layoutType, layoutName, context)
}(params)
@ -105,11 +104,7 @@ object LayoutParser {
if (layoutName.startsWith(CUSTOM_LAYOUT_PREFIX))
getCustomLayoutFiles(layoutType, context)
.firstOrNull { it.name.startsWith(layoutName) }?.let { return it.readText() }
val layouts = context.assets.list(layoutType.folder)!!
layouts.firstOrNull { it.startsWith("$layoutName.") }
?.let { return context.assets.open(layoutType.folder + it).reader().readText() }
val fallback = layouts.first { it.startsWith(layoutType.default) } // must exist!
return context.assets.open(layoutType.folder + fallback).reader().readText()
return LayoutUtils.getContent(layoutType, layoutName, context)
}
// allow commenting lines by starting them with "//"

View file

@ -41,7 +41,7 @@ object Defaults {
LayoutType.PHONE -> "phone"
LayoutType.PHONE_SYMBOLS -> "phone_symbols"
LayoutType.EMOJI_BOTTOM -> "emoji_bottom_row"
LayoutType.CLIPBOARD_BOTTOM -> "clipboard_bottom_row"
LayoutType.CLIPBOARD_BOTTOM -> "clip_bottom_row"
}
const val PREF_THEME_STYLE = KeyboardTheme.STYLE_MATERIAL

View file

@ -209,7 +209,7 @@ class LanguageSettingsDialog(
reloadSetting()
}
if (isCustom) {
confirmDialog(context, context.getString(R.string.delete_layout, getLayoutDisplayName(layoutSetName)), context.getString(R.string.delete)) { delete() }
confirmDialog(context, context.getString(R.string.delete_layout, getCustomLayoutDisplayName(layoutSetName)), context.getString(R.string.delete)) { delete() }
} else {
delete()
}

View file

@ -551,10 +551,15 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return new File(DeviceProtectedUtils.getFilesDir(context), "custom_font");
}
public static String getLayoutName(final LayoutType type, final SharedPreferences prefs) {
// "default" layout as in this is used if nothing else is specified in the subtype
public static String readDefaultLayoutName(final LayoutType type, final SharedPreferences prefs) {
return prefs.getString(PREF_LAYOUT_PREFIX + type.name(), Defaults.INSTANCE.getDefault(type));
}
public static void writeDefaultLayoutName(final String name, final LayoutType type, final SharedPreferences prefs) {
prefs.edit().putString(PREF_LAYOUT_PREFIX + type.name(), name).apply();
}
@Nullable
public Typeface getCustomTypeface() {
if (!sCustomTypefaceLoaded) {

View file

@ -25,7 +25,6 @@ import helium314.keyboard.latin.R;
import helium314.keyboard.latin.RichInputMethodManager;
import helium314.keyboard.latin.common.Colors;
import helium314.keyboard.latin.permissions.PermissionsUtil;
import helium314.keyboard.latin.utils.CustomLayoutUtilsKt;
import helium314.keyboard.latin.utils.InputTypeUtils;
import helium314.keyboard.latin.utils.JniUtils;
import helium314.keyboard.latin.utils.Log;

View file

@ -79,6 +79,7 @@ public final class AdditionalSubtypeUtils {
return createAdditionalSubtypeInternal(locale, keyboardLayoutSetName, asciiCapable, true);
}
// todo: adjust so we can store more stuff in extra values
private static String getPrefSubtype(final InputMethodSubtype subtype) {
final String keyboardLayoutSetName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=MAIN:" + keyboardLayoutSetName;
@ -121,7 +122,7 @@ public final class AdditionalSubtypeUtils {
final boolean asciiCapable = ScriptUtils.script(locale).equals(ScriptUtils.SCRIPT_LATIN);
// Here we assume that all the additional subtypes are EmojiCapable
final InputMethodSubtype subtype = createEmojiCapableAdditionalSubtype(locale, keyboardLayoutSetName, asciiCapable);
if (subtype.getNameResId() == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT && !keyboardLayoutSetName.startsWith(CustomLayoutUtilsKt.CUSTOM_LAYOUT_PREFIX)) {
if (subtype.getNameResId() == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT && !LayoutUtilsCustomKt.isCustomLayout(keyboardLayoutSetName)) {
// Skip unknown keyboard layout subtype. This may happen when predefined keyboard
// layout has been removed.
return null;

View file

@ -1,5 +1,6 @@
package helium314.keyboard.latin.utils
import helium314.keyboard.latin.R
import java.io.File
import java.util.EnumMap
@ -20,5 +21,20 @@ enum class LayoutType {
}
val LayoutType.folder get() = "layouts${File.separator}${name.lowercase()}${File.separator}"
val LayoutType.displayNameId get() = when (this) {
MAIN -> TODO()
SYMBOLS -> R.string.layout_symbols
MORE_SYMBOLS -> R.string.layout_symbols_shifted
FUNCTIONAL -> R.string.layout_functional_keys
NUMBER -> R.string.layout_number
NUMBER_ROW -> R.string.layout_number_row
NUMPAD -> R.string.layout_numpad
NUMPAD_LANDSCAPE -> R.string.layout_numpad_landscape
PHONE -> R.string.layout_phone
PHONE_SYMBOLS -> R.string.layout_phone_symbols
EMOJI_BOTTOM -> R.string.layout_emoji_bottom_row
CLIPBOARD_BOTTOM -> R.string.layout_clip_bottom_row
}
}
}

View file

@ -0,0 +1,29 @@
package helium314.keyboard.latin.utils
import android.content.Context
import helium314.keyboard.latin.settings.Defaults.default
import helium314.keyboard.latin.utils.LayoutType.Companion.folder
import helium314.keyboard.latin.utils.ScriptUtils.script
import java.util.Locale
// for layouts provided by the app
object LayoutUtils {
fun getAvailableLayouts(layoutType: LayoutType, context: Context, locale: Locale? = null): Collection<String> {
if (layoutType != LayoutType.MAIN)
return context.assets.list(layoutType.folder)?.map { it.substringBefore(".") }.orEmpty()
if (locale == null)
return getAllAvailableSubtypes().mapTo(HashSet()) { it.mainLayoutName()?.substringBefore("+") ?: "qwerty" }
if (locale.script() == ScriptUtils.SCRIPT_LATIN)
return getAllAvailableSubtypes().filter { it.isAsciiCapable && it.mainLayoutName()?.startsWith(CUSTOM_LAYOUT_PREFIX) == false }
.mapTo(HashSet()) { it.mainLayoutName()?.substringBefore("+") ?: "qwerty" }
return getSubtypesForLocale(locale).mapNotNullTo(HashSet()) { it.mainLayoutName() }
}
fun getContent(layoutType: LayoutType, layoutName: String, context: Context): String {
val layouts = context.assets.list(layoutType.folder)!!
layouts.firstOrNull { it.startsWith("$layoutName.") }
?.let { return context.assets.open(layoutType.folder + it).reader().readText() }
val fallback = layouts.first { it.startsWith(layoutType.default) } // must exist!
return context.assets.open(layoutType.folder + fallback).reader().readText()
}
}

View file

@ -5,7 +5,6 @@ import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import android.text.InputType
import android.view.inputmethod.InputMethodSubtype
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.core.widget.doAfterTextChanged
@ -26,6 +25,8 @@ import java.io.IOException
import java.math.BigInteger
import java.util.EnumMap
// todo: object like LayoutUtils
fun loadCustomLayout(uri: Uri?, languageTag: String, context: Context, onAdded: (String) -> Unit) {
if (uri == null)
return infoDialog(context, context.getString(R.string.layout_error, "layout file not found"))
@ -175,14 +176,23 @@ fun onCustomLayoutFileListChanged() {
private fun getCustomLayoutsDir(context: Context) = File(DeviceProtectedUtils.getFilesDir(context), "layouts")
// undo the name changes in loadCustomLayout when clicking ok
fun getLayoutDisplayName(layoutName: String) =
fun getCustomLayoutDisplayName(layoutName: String) =
try {
decodeBase36(layoutName.substringAfter(CUSTOM_LAYOUT_PREFIX).substringAfter(".").substringBeforeLast("."))
decodeBase36(layoutName.substringAfter(CUSTOM_LAYOUT_PREFIX).substringBeforeLast("."))
} catch (_: NumberFormatException) {
layoutName
}
fun getCustomLayoutName(displayName: String) = CUSTOM_LAYOUT_PREFIX + encodeBase36(displayName) + "."
fun isCustomLayout(layoutName: String) = layoutName.startsWith(CUSTOM_LAYOUT_PREFIX)
fun getCustomLayoutFile(layoutName: String, layoutType: LayoutType, context: Context): File {
val file = File(DeviceProtectedUtils.getFilesDir(context), layoutType.folder + layoutName)
file.parentFile?.mkdirs()
return file
}
fun removeCustomLayoutFile(layoutName: String, context: Context) {
getCustomLayoutFile(layoutName, context).delete()
}
@ -193,7 +203,7 @@ fun editCustomLayout(layoutName: String, context: Context, startContent: String?
setText(startContent ?: file.readText())
}
val builder = AlertDialog.Builder(context)
.setTitle(getLayoutDisplayName(layoutName))
.setTitle(getCustomLayoutDisplayName(layoutName))
.setView(editText)
.setPositiveButton(R.string.save) { _, _ ->
val content = editText.text.toString()

View file

@ -266,8 +266,8 @@ public final class SubtypeLocaleUtils {
@Nullable
public static String getKeyboardLayoutSetDisplayName(@NonNull final String layoutName) {
if (layoutName.startsWith(CustomLayoutUtilsKt.CUSTOM_LAYOUT_PREFIX))
return CustomLayoutUtilsKt.getLayoutDisplayName(layoutName);
if (LayoutUtilsCustomKt.isCustomLayout(layoutName))
return LayoutUtilsCustomKt.getCustomLayoutDisplayName(layoutName);
return sKeyboardLayoutToDisplayNameMap.get(layoutName);
}

View file

@ -22,6 +22,8 @@ import helium314.keyboard.latin.utils.ScriptUtils.script
import org.xmlpull.v1.XmlPullParser
import java.util.*
// todo: move some parts, to subtypeUtils, and only keep actual settings?
/** @return enabled subtypes. If no subtypes are enabled, but a contextForFallback is provided,
* subtypes for system locales will be returned, or en-US if none found. */
fun getEnabledSubtypes(prefs: SharedPreferences, fallback: Boolean = false): List<InputMethodSubtype> {
@ -148,6 +150,8 @@ fun hasMatchingSubtypeForLocale(locale: Locale): Boolean {
return !resourceSubtypesByLocale[locale].isNullOrEmpty()
}
fun getSubtypesForLocale(locale: Locale): List<InputMethodSubtype> = resourceSubtypesByLocale[locale].orEmpty()
fun getAvailableSubtypeLocales(): Collection<Locale> {
require(initialized)
return resourceSubtypesByLocale.keys

View file

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

View file

@ -11,6 +11,7 @@ import helium314.keyboard.settings.screens.createAdvancedSettings
import helium314.keyboard.settings.screens.createAppearanceSettings
import helium314.keyboard.settings.screens.createCorrectionSettings
import helium314.keyboard.settings.screens.createGestureTypingSettings
import helium314.keyboard.settings.screens.createLayoutSettings
import helium314.keyboard.settings.screens.createPreferencesSettings
import helium314.keyboard.settings.screens.createToolbarSettings
@ -63,7 +64,8 @@ class Setting(
// intentionally not putting individual debug settings in here so user knows the context
private fun createSettings(context: Context) = createAboutSettings(context) + createAppearanceSettings(context) +
createCorrectionSettings(context) + createPreferencesSettings(context) + createToolbarSettings(context) +
createAdvancedSettings(context) + if (JniUtils.sHaveGestureLib) createGestureTypingSettings(context) else emptyList()
createLayoutSettings(context) + createAdvancedSettings(context) +
if (JniUtils.sHaveGestureLib) createGestureTypingSettings(context) else emptyList()
object SettingsWithoutKey {
const val EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary"

View file

@ -16,6 +16,7 @@ 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.MainSettingsScreen
import helium314.keyboard.settings.screens.PreferencesScreen
import helium314.keyboard.settings.screens.TextCorrectionScreen
@ -57,6 +58,7 @@ fun SettingsNavHost(
onClickAdvanced = { navController.navigate(SettingsDestination.Advanced) },
onClickAppearance = { navController.navigate(SettingsDestination.Appearance) },
onClickLanguage = { navController.navigate(SettingsDestination.Languages) },
onClickLayouts = { navController.navigate(SettingsDestination.Layouts) },
onClickBack = ::goBack,
)
}
@ -92,6 +94,9 @@ fun SettingsNavHost(
composable(SettingsDestination.Languages) {
// LanguageScreen(onClickBack = ::goBack)
}
composable(SettingsDestination.Layouts) {
LayoutScreen(onClickBack = ::goBack)
}
composable(SettingsDestination.Colors) {
ColorsScreen(isNight = false, onClickBack = ::goBack)
}
@ -117,6 +122,7 @@ object SettingsDestination {
const val ColorsNight = "colors_night"
const val PersonalDictionary = "personal_dictionary"
const val Languages = "languages"
const val Layouts = "layouts"
val navTarget = MutableStateFlow(Settings)
private val navScope = CoroutineScope(Dispatchers.Default)

View file

@ -181,7 +181,7 @@ private fun AddColorRow(onDismissRequest: () -> Unit, userColors: Collection<Str
}
@Composable
fun ColorItemRow(onDismissRequest: () -> Unit, item: String, isSelected: Boolean, isUser: Boolean, targetScreen: String, prefKey: String) {
private fun ColorItemRow(onDismissRequest: () -> Unit, item: String, isSelected: Boolean, isUser: Boolean, targetScreen: String, prefKey: String) {
val ctx = LocalContext.current
val prefs = ctx.prefs()
Row(
@ -217,6 +217,7 @@ fun ColorItemRow(onDismissRequest: () -> Unit, item: String, isSelected: Boolean
IconButton(
onClick = {
onDismissRequest()
// todo: maybe no need to set it as default when using the navigation specials
prefs.edit().putString(prefKey, item).apply()
SettingsDestination.navigateTo(targetScreen)
keyboardNeedsReload = true
@ -272,7 +273,7 @@ private fun loadColorString(colorString: String, prefs: SharedPreferences): Bool
@Preview
@Composable
private fun PreviewListPickerDialog() {
private fun Preview() {
ColorThemePickerDialog(
onDismissRequest = {},
setting = Setting(LocalContext.current, "", R.string.settings) {},

View file

@ -3,7 +3,9 @@ package helium314.keyboard.settings.dialogs
import android.widget.Toast
import androidx.compose.foundation.layout.imePadding
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -12,50 +14,78 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.window.DialogProperties
import helium314.keyboard.latin.R
import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.checkLayout
import helium314.keyboard.latin.utils.getCustomLayoutFile
import helium314.keyboard.latin.utils.getLayoutDisplayName
import helium314.keyboard.latin.utils.getCustomLayoutDisplayName
import helium314.keyboard.latin.utils.getCustomLayoutName
import helium314.keyboard.latin.utils.getStringResourceOrName
import helium314.keyboard.latin.utils.isCustomLayout
import helium314.keyboard.latin.utils.onCustomLayoutFileListChanged
import helium314.keyboard.settings.keyboardNeedsReload
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
// todo: make it wider!
// maybe make it a completely separate dialog, not even using the 3-button thing?
// though we could provide with parameter, and maybe some sort of reduce-padding option
@Composable
fun LayoutEditDialog(
onDismissRequest: () -> Unit,
layoutName: String,
layoutType: LayoutType,
initialLayoutName: String,
startContent: String? = null,
displayName: String? = null
isNameValid: (String) -> Boolean
) {
val ctx = LocalContext.current
val file = getCustomLayoutFile(layoutName, ctx)
val scope = rememberCoroutineScope()
var job: Job? = null
var showDeleteConfirmation by rememberSaveable { mutableStateOf(false) }
val startIsCustom = isCustomLayout(initialLayoutName)
var displayNameValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(
if (startIsCustom) getCustomLayoutDisplayName(initialLayoutName)
else initialLayoutName.getStringResourceOrName("layout_", ctx)
))
}
val nameValid = displayNameValue.text.isNotBlank()
&& (
(startIsCustom && getCustomLayoutName(displayNameValue.text) == initialLayoutName)
|| isNameValid(getCustomLayoutName(displayNameValue.text))
)
TextInputDialog(
onDismissRequest = onDismissRequest,
onDismissRequest = {
job?.cancel()
onDismissRequest()
},
onConfirmed = {
file.parentFile?.mkdir()
file.writeText(it)
val newLayoutName = getCustomLayoutName(displayNameValue.text)
if (startIsCustom && initialLayoutName != newLayoutName)
getCustomLayoutFile(initialLayoutName, layoutType, ctx).delete()
getCustomLayoutFile(newLayoutName, layoutType, ctx).writeText(it)
onCustomLayoutFileListChanged()
keyboardNeedsReload = true
},
confirmButtonText = stringResource(R.string.save),
neutralButtonText = if (displayName != null && file.exists()) stringResource(R.string.delete) else null,
onNeutral = {
if (!file.exists()) return@TextInputDialog
file.delete()
onCustomLayoutFileListChanged()
keyboardNeedsReload = true
},
initialText = startContent ?: file.readText(),
initialText = startContent ?: getCustomLayoutFile(initialLayoutName, layoutType, ctx).readText(),
singleLine = false,
title = { Text(displayName ?: getLayoutDisplayName(layoutName)) },
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) },
// textStyle = MaterialTheme.typography.titleMedium, // todo: only makes it a tiny bit smaller, find a better way
)
},
checkTextValid = {
val valid = checkLayout(it, ctx)
job?.cancel()
@ -68,23 +98,12 @@ fun LayoutEditDialog(
Toast.makeText(ctx, ctx.getString(R.string.layout_error, message), Toast.LENGTH_LONG).show()
}
}
valid
valid && nameValid // don't allow saving with invalid name, but inform user about issues with layout content
},
modifier = Modifier.imePadding(),
// decorFitsSystemWindows = false is necessary so the dialog is not covered by keyboard
// but this also stops the background from being darkened... great idea to combine both
// todo: also it results in an ugly effect when adding a new layout... need to find something else
properties = DialogProperties(decorFitsSystemWindows = false)
)
if (showDeleteConfirmation)
ConfirmationDialog(
onDismissRequest = { showDeleteConfirmation = false },
onConfirmed = {
onDismissRequest()
file.delete()
onCustomLayoutFileListChanged()
keyboardNeedsReload = true
},
text = { Text(stringResource(R.string.delete_layout, displayName ?: "")) },
confirmButtonText = stringResource(R.string.delete)
)
}

View file

@ -0,0 +1,248 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings.dialogs
import android.app.Activity
import android.content.Intent
import android.provider.OpenableColumns
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import helium314.keyboard.latin.R
import helium314.keyboard.latin.settings.Defaults.default
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.LayoutUtils
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.checkLayout
import helium314.keyboard.latin.utils.getActivity
import helium314.keyboard.latin.utils.getCustomLayoutDisplayName
import helium314.keyboard.latin.utils.getCustomLayoutFiles
import helium314.keyboard.latin.utils.getCustomLayoutName
import helium314.keyboard.latin.utils.getStringResourceOrName
import helium314.keyboard.latin.utils.onCustomLayoutFileListChanged
import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.settings.Setting
import helium314.keyboard.settings.SettingsActivity
import helium314.keyboard.settings.keyboardNeedsReload
// modified copy of ColorPickerDialog, later check whether stuff can be re-used
@Composable
fun LayoutPickerDialog(
onDismissRequest: () -> Unit,
setting: Setting,
layoutType: LayoutType,
) {
val ctx = LocalContext.current
val prefs = ctx.prefs()
val b = (ctx.getActivity() as? SettingsActivity)?.prefChanged?.collectAsState()
if ((b?.value ?: 0) < 0)
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
val currentLayout = Settings.readDefaultLayoutName(layoutType, prefs)
val internalLayouts = LayoutUtils.getAvailableLayouts(layoutType, ctx)
// todo: getCustomLayoutFiles does not work nicely for main layout, but currently this dialog is not used for them
val customLayouts = getCustomLayoutFiles(layoutType, ctx).map { it.name }.sorted()
val layouts = internalLayouts + customLayouts + ""
val state = rememberLazyListState()
LaunchedEffect(currentLayout) {
val index = layouts.indexOfFirst { it == currentLayout }
if (index != -1) state.scrollToItem(index, -state.layoutInfo.viewportSize.height / 3)
}
var errorDialog by rememberSaveable { mutableStateOf(false) }
var newLayoutDialog: Pair<String, String?>? by rememberSaveable { mutableStateOf(null) }
val loadFilePicker = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
val uri = it.data?.data ?: return@rememberLauncherForActivityResult
val cr = ctx.getActivity()?.contentResolver ?: return@rememberLauncherForActivityResult
val name = cr.query(uri, null, null, null, null)?.use { c ->
if (!c.moveToFirst()) return@use null
val index = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (index < 0) null
else c.getString(index)
}
cr.openInputStream(uri)?.use {
val content = it.reader().readText()
errorDialog = !checkLayout(content, ctx)
if (!errorDialog)
newLayoutDialog = (name ?: layoutType.default) to content
}
}
ThreeButtonAlertDialog(
onDismissRequest = onDismissRequest,
cancelButtonText = stringResource(R.string.dialog_close),
onConfirmed = { },
confirmButtonText = null,
neutralButtonText = stringResource(R.string.button_load_custom),
onNeutral = {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("text/*", "application/octet-stream", "application/json"))
.setType("*/*")
loadFilePicker.launch(intent) },
title = { Text(setting.title) },
text = {
CompositionLocalProvider(
LocalTextStyle provides MaterialTheme.typography.bodyLarge
) {
LazyColumn(state = state) {
items(layouts) { item ->
if (item == "") {
AddLayoutRow({ newLayoutDialog = it to "" }, customLayouts)
} else {
LayoutItemRow(
onDismissRequest = onDismissRequest,
onClickEdit = { newLayoutDialog = it },
onDelete = { deletedLayout ->
if (item == deletedLayout) {
prefs.edit().remove(Settings.PREF_LAYOUT_PREFIX + layoutType.name).apply()
keyboardNeedsReload = true
}
getCustomLayoutFiles(layoutType, ctx).firstOrNull { it.name == deletedLayout }?.delete()
onCustomLayoutFileListChanged()
},
layoutType = layoutType,
layoutName = item,
isSelected = item == currentLayout,
isCustom = item in customLayouts
)
}
}
}
}
},
)
if (errorDialog)
InfoDialog(stringResource(R.string.file_read_error)) { errorDialog = false }
if (newLayoutDialog != null) {
LayoutEditDialog(
onDismissRequest = { newLayoutDialog = null },
layoutType = layoutType,
initialLayoutName = newLayoutDialog?.first ?: layoutType.default,
startContent = newLayoutDialog?.second,
isNameValid = { it.isNotBlank() && it !in customLayouts }
)
}
}
@Composable
private fun AddLayoutRow(onNewLayout: (String) -> Unit, userLayouts: Collection<String>) {
var textValue by remember { mutableStateOf(TextFieldValue()) }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 10.dp)
) {
Icon(painterResource(R.drawable.ic_plus), stringResource(R.string.add))
TextField(
value = textValue,
onValueChange = { textValue = it },
modifier = Modifier.weight(1f),
singleLine = true
)
IconButton(
enabled = textValue.text.isNotEmpty() && getCustomLayoutName(textValue.text) !in userLayouts,
onClick = { onNewLayout(textValue.text) }
) { Icon(painterResource(R.drawable.ic_edit), null) }
}
}
@Composable
private fun LayoutItemRow(
onDismissRequest: () -> Unit,
onClickEdit: (Pair<String, String?>) -> Unit,
onDelete: (String) -> Unit,
layoutType: LayoutType,
layoutName: String,
isSelected: Boolean,
isCustom: Boolean,
) {
val ctx = LocalContext.current
val prefs = ctx.prefs()
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable {
onDismissRequest()
Settings.writeDefaultLayoutName(layoutName, layoutType, prefs)
keyboardNeedsReload = true
}
.padding(start = 6.dp)
.heightIn(min = 40.dp)
) {
RadioButton(
selected = isSelected,
onClick = {
onDismissRequest()
Settings.writeDefaultLayoutName(layoutName, layoutType, prefs)
keyboardNeedsReload = true
}
)
Text(
text = if (isCustom) getCustomLayoutDisplayName(layoutName)
else layoutName.getStringResourceOrName("layout_", ctx),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f),
)
if (isCustom) {
var showDeleteDialog by remember { mutableStateOf(false) }
IconButton(
onClick = { showDeleteDialog = true }
) { Icon(painterResource(R.drawable.ic_bin), null) }
if (showDeleteDialog)
ConfirmationDialog(
onDismissRequest = { showDeleteDialog = false },
text = { Text(stringResource(R.string.delete_layout, getCustomLayoutDisplayName(layoutName))) },
confirmButtonText = stringResource(R.string.delete),
onConfirmed = {
showDeleteDialog = false
onDelete(layoutName)
}
)
}
IconButton(
onClick = { onClickEdit(layoutName to (if (isCustom) null else LayoutUtils.getContent(layoutType, layoutName, ctx))) }
) { Icon(painterResource(R.drawable.ic_edit), null) }
}
}
@Preview
@Composable
private fun Preview() {
LayoutPickerDialog(
onDismissRequest = {},
setting = Setting(LocalContext.current, "", R.string.settings) {},
layoutType = LayoutType.SYMBOLS
)
}

View file

@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@ -48,7 +49,7 @@ fun ThreeButtonAlertDialog(
properties = properties
) {
Box(
modifier = modifier.sizeIn(minWidth = 280.dp, maxWidth = 560.dp),
modifier = modifier.widthIn(min = 280.dp, max = 560.dp),
propagateMinConstraints = true
) {
Surface(

View file

@ -14,7 +14,7 @@ import helium314.keyboard.settings.Setting
import helium314.keyboard.settings.dialogs.LayoutEditDialog
import helium314.keyboard.settings.dialogs.ListPickerDialog
import java.io.File
/*
@Composable
fun LayoutEditPreference(
setting: Setting,
@ -57,3 +57,4 @@ fun LayoutEditPreference(
)
}
}
*/

View file

@ -27,11 +27,6 @@ import helium314.keyboard.latin.common.splitOnWhitespace
import helium314.keyboard.latin.settings.DebugSettings
import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.CUSTOM_FUNCTIONAL_LAYOUT_NORMAL
import helium314.keyboard.latin.utils.CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS
import helium314.keyboard.latin.utils.CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED
import helium314.keyboard.latin.utils.CUSTOM_LAYOUT_PREFIX
import helium314.keyboard.latin.utils.getStringResourceOrName
import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.settings.SettingsContainer
import helium314.keyboard.settings.preferences.ListPreference
@ -47,7 +42,6 @@ import helium314.keyboard.settings.Theme
import helium314.keyboard.settings.dialogs.TextInputDialog
import helium314.keyboard.settings.keyboardNeedsReload
import helium314.keyboard.settings.preferences.BackupRestorePreference
import helium314.keyboard.settings.preferences.LayoutEditPreference
import helium314.keyboard.settings.preferences.LoadGestureLibPreference
@Composable
@ -192,23 +186,6 @@ fun createAdvancedSettings(context: Context) = listOf(
)
ListPreference(it, items, Defaults.PREF_MORE_POPUP_KEYS) { KeyboardLayoutSet.onSystemLocaleChanged() }
},
/* Setting(context, SettingsWithoutKey.CUSTOM_SYMBOLS_NUMBER_LAYOUTS, R.string.customize_symbols_number_layouts) { setting ->
LayoutEditPreference(
setting = setting,
items = RawKeyboardParser.symbolAndNumberLayouts,
getItemName = { it.getStringResourceOrName("layout_", LocalContext.current) },
getDefaultLayout = { LocalContext.current.assets.list("layouts")?.firstOrNull { it.startsWith("$it.") } }
)
},
Setting(context, SettingsWithoutKey.CUSTOM_FUNCTIONAL_LAYOUTS, R.string.customize_functional_key_layouts) { setting ->
LayoutEditPreference(
setting = setting,
items = listOf(CUSTOM_FUNCTIONAL_LAYOUT_NORMAL, CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS, CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED)
.map { it.substringBeforeLast(".") },
getItemName = { it.substringAfter(CUSTOM_LAYOUT_PREFIX).getStringResourceOrName("layout_", LocalContext.current) },
getDefaultLayout = { if (Settings.getInstance().isTablet) "functional_keys_tablet.json" else "functional_keys.json" }
)
},*/ // todo: these settings are disabled for now -> remove them and use a layoutScreen instead
Setting(context, SettingsWithoutKey.BACKUP_RESTORE, R.string.backup_restore_title) {
BackupRestorePreference(it)
},

View file

@ -136,7 +136,7 @@ fun ColorsScreen(
nameField = it
},
isError = !nameValid,
// supportingText = { if (!nameValid) Text("name already in use") } // this is cutting off bottom half of the actual text...
// supportingText = { if (!nameValid) Text(stringResource(R.string.name_invalid) } // todo: this is cutting off bottom half of the actual text...
trailingIcon = { if (!nameValid) Icon(painterResource(R.drawable.ic_close), null) }
)
},

View file

@ -0,0 +1,68 @@
package helium314.keyboard.settings.screens
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import helium314.keyboard.latin.R
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.Log
import helium314.keyboard.latin.utils.getActivity
import helium314.keyboard.latin.utils.getCustomLayoutDisplayName
import helium314.keyboard.latin.utils.getStringResourceOrName
import helium314.keyboard.latin.utils.isCustomLayout
import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.settings.SearchSettingsScreen
import helium314.keyboard.settings.Setting
import helium314.keyboard.settings.SettingsActivity
import helium314.keyboard.settings.dialogs.LayoutPickerDialog
import helium314.keyboard.settings.preferences.Preference
@Composable
fun LayoutScreen(
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?)
SearchSettingsScreen(
onClickBack = onClickBack,
title = stringResource(R.string.keyboard_layout_set),
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 ->
Setting(context, Settings.PREF_LAYOUT_PREFIX + layoutType, layoutType.displayNameId) { setting ->
val ctx = LocalContext.current
val prefs = ctx.prefs()
val b = (ctx.getActivity() as? SettingsActivity)?.prefChanged?.collectAsState()
if ((b?.value ?: 0) < 0)
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
var showDialog by rememberSaveable { mutableStateOf(false) }
val currentLayout = Settings.readDefaultLayoutName(layoutType, prefs)
val displayName = if (isCustomLayout(currentLayout)) getCustomLayoutDisplayName(currentLayout)
else currentLayout.getStringResourceOrName("layout_", ctx)
Preference(
name = setting.title,
description = displayName,
onClick = { showDialog = true }
)
if (showDialog)
LayoutPickerDialog(
onDismissRequest = { showDialog = false },
setting = setting,
layoutType = layoutType
)
}
}

View file

@ -40,6 +40,7 @@ fun MainSettingsScreen(
onClickAdvanced: () -> Unit,
onClickAppearance: () -> Unit,
onClickLanguage: () -> Unit,
onClickLayouts: () -> Unit,
onClickBack: () -> Unit,
) {
val ctx = LocalContext.current
@ -116,6 +117,17 @@ fun MainSettingsScreen(
contentDescription = null
)
}
Preference(
name = stringResource(R.string.keyboard_layout_set),
onClick = onClickLayouts,
icon = R.drawable.ic_ime_switcher
) {
Icon(
painter = painterResource(R.drawable.ic_arrow_left),
modifier = Modifier.scale(-1f, 1f),
contentDescription = null
)
}
Preference(
name = stringResource(R.string.settings_screen_advanced),
onClick = onClickAdvanced,
@ -181,7 +193,7 @@ fun MainSettingsScreen(
private fun PreviewScreen() {
Theme(true) {
Surface {
MainSettingsScreen({}, {}, {}, {}, {}, {}, {}, {}, {})
MainSettingsScreen({}, {}, {}, {}, {}, {}, {}, {}, {}, {})
}
}
}

View file

@ -987,6 +987,8 @@ New dictionary:
<string name="customize_icons">Customize icons</string>
<!-- Confirmation message when resetting all custom icons -->
<string name="customize_icons_reset_message">Really reset all customized icons?</string>
<!-- Confirmation message when deleting a something (used for custom colors) -->
<!-- Confirmation message when deleting a something (currently used for custom colors) -->
<string name="delete_confirmation">Really delete %s?</string>
<!-- Message when chosen name is invalid (empty or already taked) -->
<string name="name_invalid">Invalid name</string>
</resources>