add layout customizer dialog

This commit is contained in:
Helium314 2025-02-05 20:21:16 +01:00
parent 6e3bedaf5c
commit fff66913b8
6 changed files with 170 additions and 28 deletions

View file

@ -81,6 +81,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
// search only in current pref screen, except when in main?
// try getting rid of appcompat stuff (activity, dialogs, ...)
// rearrange settings screens? now it should be very simple to do (definitely separate PR)
// actually lenient json parsing is not good in a certain way: we should show an error if a json property is unknown
// syntax highlighting for json? should show basic json errors
// preliminary results:
// looks ok (ugly M3 switches)

View file

@ -0,0 +1,50 @@
package helium314.keyboard.settings.dialogs
import android.widget.Toast
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import helium314.keyboard.latin.R
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 kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun CustomizeLayoutDialog(
layoutName: String,
onDismissRequest: () -> Unit,
startContent: String? = null,
displayName: String? = null,
) {
val ctx = LocalContext.current
val file = getCustomLayoutFile(layoutName, ctx)
val scope = rememberCoroutineScope()
var job: Job? = null
TextInputDialog(
onDismissRequest = onDismissRequest,
onConfirmed = { },
initialText = startContent ?: file.readText(),
title = { Text(displayName ?: getLayoutDisplayName(layoutName)) },
checkTextValid = {
val valid = checkLayout(it, ctx)
job?.cancel()
if (!valid) {
job = scope.launch {
delay(3000)
val message = Log.getLog(10)
.lastOrNull { it.tag == "CustomLayoutUtils" }?.message
?.split("\n")?.take(2)?.joinToString("\n")
Toast.makeText(ctx, ctx.getString(R.string.layout_error, message), Toast.LENGTH_LONG).show()
}
}
valid
},
singleLine = false,
)
}

View file

@ -3,6 +3,7 @@ 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
@ -35,6 +36,7 @@ fun <T: Any> ListPickerDialog(
selectedItem: T? = null,
getItemName: (@Composable (T) -> String) = { it.toString() },
confirmImmediately: Boolean = true,
showRadioButtons: Boolean = true,
) {
var selected by remember { mutableStateOf(selectedItem) }
val state = rememberLazyListState()
@ -68,22 +70,24 @@ fun <T: Any> ListPickerDialog(
selected = item
}
.padding(horizontal = 24.dp)
.heightIn(min = 40.dp)
) {
Text(
text = getItemName(item),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f),
)
RadioButton(
selected = selected == item,
onClick = {
if (confirmImmediately) {
onDismissRequest()
onItemSelected(item)
if (showRadioButtons)
RadioButton(
selected = selected == item,
onClick = {
if (confirmImmediately) {
onDismissRequest()
onItemSelected(item)
}
selected = item
}
selected = item
}
)
)
}
}
}

View file

@ -39,10 +39,9 @@ fun TextInputDialog(
val focusRequester = remember { FocusRequester() }
var value by remember {
mutableStateOf(TextFieldValue(initialText, selection = TextRange(initialText.length)))
mutableStateOf(TextFieldValue(initialText, selection = TextRange(if (singleLine) initialText.length else 0)))
}
// todo: this is not working any more?
LaunchedEffect(initialText) { focusRequester.requestFocus() }
ThreeButtonAlertDialog(

View file

@ -3,14 +3,14 @@ package helium314.keyboard.settings.dialogs
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.DialogProperties
@ -45,7 +45,12 @@ fun ThreeButtonAlertDialog(
}
},
modifier = modifier,
title = title,
title = {
// avoid way too large title (headlineSmall)
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleLarge) {
title?.invoke()
}
},
text = text,
shape = MaterialTheme.shapes.medium,
containerColor = MaterialTheme.colorScheme.surface,

View file

@ -2,12 +2,15 @@ package helium314.keyboard.settings.screens
import android.content.Context
import android.os.Build
import android.widget.Toast
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
@ -15,11 +18,24 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import helium314.keyboard.keyboard.KeyboardLayoutSet
import helium314.keyboard.keyboard.internal.keyboard_parser.RawKeyboardParser
import helium314.keyboard.latin.BuildConfig
import helium314.keyboard.latin.R
import helium314.keyboard.latin.SystemBroadcastReceiver
import helium314.keyboard.latin.common.splitOnWhitespace
import helium314.keyboard.latin.settings.DebugSettings
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.Log
import helium314.keyboard.latin.utils.checkLayout
import helium314.keyboard.latin.utils.getCustomLayoutFile
import helium314.keyboard.latin.utils.getCustomLayoutFiles
import helium314.keyboard.latin.utils.getLayoutDisplayName
import helium314.keyboard.latin.utils.getStringResourceOrName
import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.settings.AllPrefs
import helium314.keyboard.settings.ListPreference
@ -33,8 +49,14 @@ import helium314.keyboard.settings.SettingsDestination
import helium314.keyboard.settings.SliderPreference
import helium314.keyboard.settings.SwitchPreference
import helium314.keyboard.settings.Theme
import helium314.keyboard.settings.dialogs.CustomizeLayoutDialog
import helium314.keyboard.settings.dialogs.ListPickerDialog
import helium314.keyboard.settings.dialogs.TextInputDialog
import helium314.keyboard.settings.keyboardNeedsReload
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
@Composable
fun AdvancedSettingsScreen(
@ -159,13 +181,25 @@ fun createAdvancedPrefs(context: Context) = listOf(
default = false
)
},
PrefDef(context, Settings.PREF_CUSTOM_CURRENCY_KEY, R.string.customize_currencies) {
PrefDef(context, Settings.PREF_CUSTOM_CURRENCY_KEY, R.string.customize_currencies) { def ->
var showDialog by remember { mutableStateOf(false) }
Preference(
name = it.title,
name = def.title,
onClick = { showDialog = true }
)
// if (showDialog) todo: show the currency customizer
if (showDialog) {
val prefs = LocalContext.current.prefs()
TextInputDialog(
onDismissRequest = { showDialog = false },
textInputLabel = { Text(stringResource(R.string.customize_currencies_detail)) },
initialText = prefs.getString(def.key, "")!!,
onConfirmed = { prefs.edit().putString(def.key, it).apply(); KeyboardLayoutSet.onSystemLocaleChanged() },
title = { Text(stringResource(R.string.customize_currencies)) },
neutralButtonText = if (prefs.contains(def.key)) stringResource(R.string.button_default) else null,
onNeutral = { prefs.edit().remove(def.key).apply(); KeyboardLayoutSet.onSystemLocaleChanged() },
checkTextValid = { it.splitOnWhitespace().none { it.length > 8 } }
)
}
},
PrefDef(context, Settings.PREF_MORE_POPUP_KEYS, R.string.show_popup_keys_title) { def ->
val items = listOf(
@ -174,29 +208,77 @@ fun createAdvancedPrefs(context: Context) = listOf(
stringResource(R.string.show_popup_keys_more) to "more",
stringResource(R.string.show_popup_keys_all) to "all",
)
ListPreference(def, items, "main")
// todo: on value changed -> KeyboardLayoutSet.onSystemLocaleChanged()
ListPreference(def, items, "main") { KeyboardLayoutSet.onSystemLocaleChanged() }
},
PrefDef(context, NonSettingsPrefs.CUSTOM_SYMBOLS_NUMBER_LAYOUTS, R.string.customize_symbols_number_layouts) {
PrefDef(context, NonSettingsPrefs.CUSTOM_SYMBOLS_NUMBER_LAYOUTS, R.string.customize_symbols_number_layouts) { def ->
var showDialog by remember { mutableStateOf(false) }
val ctx = LocalContext.current
var layout: String? by remember { mutableStateOf(null) }
Preference(
name = it.title,
name = def.title,
onClick = { showDialog = true }
)
if (showDialog) // todo: first the selection dialog, then the edit dialog
TextInputDialog(
if (showDialog) {
ListPickerDialog(
onDismissRequest = { showDialog = false },
onConfirmed = { }, // todo
initialText = LocalContext.current.assets.open("layouts/dvorak.json").bufferedReader().readText()
showRadioButtons = false,
confirmImmediately = true,
items = RawKeyboardParser.symbolAndNumberLayouts,
getItemName = { it.getStringResourceOrName("layout_", ctx) },
onItemSelected = { layout = it },
title = { Text(def.title) }
)
}
if (layout != null) {
val customLayoutName = getCustomLayoutFiles(ctx).firstOrNull { it.name.startsWith("$CUSTOM_LAYOUT_PREFIX$layout.")}?.name
val originalLayout = if (customLayoutName != null) null
else {
ctx.assets.list("layouts")?.firstOrNull { it.startsWith("$layout.") }
?.let { ctx.assets.open("layouts" + File.separator + it).reader().readText() }
}
CustomizeLayoutDialog(
layoutName = customLayoutName ?: "$CUSTOM_LAYOUT_PREFIX$layout.",
startContent = originalLayout,
displayName = layout?.getStringResourceOrName("layout_", ctx),
onDismissRequest = { layout = null}
)
}
},
PrefDef(context, NonSettingsPrefs.CUSTOM_FUNCTIONAL_LAYOUTS, R.string.customize_functional_key_layouts) {
PrefDef(context, NonSettingsPrefs.CUSTOM_FUNCTIONAL_LAYOUTS, R.string.customize_functional_key_layouts) { def ->
var showDialog by remember { mutableStateOf(false) }
val ctx = LocalContext.current
var layout: String? by remember { mutableStateOf(null) }
Preference(
name = it.title,
name = def.title,
onClick = { showDialog = true }
)
// if (showDialog) todo: show the customizer
if (showDialog) {
ListPickerDialog(
onDismissRequest = { showDialog = false },
showRadioButtons = false,
confirmImmediately = true,
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_", ctx) },
onItemSelected = { layout = it },
title = { Text(def.title) }
)
}
if (layout != null) {
val customLayoutName = getCustomLayoutFiles(ctx).map { it.name }
.firstOrNull { it.startsWith("$layout.") }
val originalLayout = if (customLayoutName != null) null
else {
val defaultLayoutName = if (Settings.getInstance().isTablet) "functional_keys_tablet.json" else "functional_keys.json"
ctx.assets.open("layouts" + File.separator + defaultLayoutName).reader().readText()
}
CustomizeLayoutDialog(
layoutName = customLayoutName ?: "$CUSTOM_LAYOUT_PREFIX$layout.",
startContent = originalLayout,
displayName = layout?.substringAfter(CUSTOM_LAYOUT_PREFIX)?.getStringResourceOrName("layout_", ctx),
onDismissRequest = { layout = null}
)
}
},
PrefDef(context, NonSettingsPrefs.BACKUP_RESTORE, R.string.backup_restore_title) {
var showDialog by remember { mutableStateOf(false) }