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? // search only in current pref screen, except when in main?
// try getting rid of appcompat stuff (activity, dialogs, ...) // try getting rid of appcompat stuff (activity, dialogs, ...)
// rearrange settings screens? now it should be very simple to do (definitely separate PR) // 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: // preliminary results:
// looks ok (ugly M3 switches) // 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.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@ -35,6 +36,7 @@ fun <T: Any> ListPickerDialog(
selectedItem: T? = null, selectedItem: T? = null,
getItemName: (@Composable (T) -> String) = { it.toString() }, getItemName: (@Composable (T) -> String) = { it.toString() },
confirmImmediately: Boolean = true, confirmImmediately: Boolean = true,
showRadioButtons: Boolean = true,
) { ) {
var selected by remember { mutableStateOf(selectedItem) } var selected by remember { mutableStateOf(selectedItem) }
val state = rememberLazyListState() val state = rememberLazyListState()
@ -68,12 +70,14 @@ fun <T: Any> ListPickerDialog(
selected = item selected = item
} }
.padding(horizontal = 24.dp) .padding(horizontal = 24.dp)
.heightIn(min = 40.dp)
) { ) {
Text( Text(
text = getItemName(item), text = getItemName(item),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
if (showRadioButtons)
RadioButton( RadioButton(
selected = selected == item, selected = selected == item,
onClick = { onClick = {

View file

@ -39,10 +39,9 @@ fun TextInputDialog(
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
var value by remember { 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() } LaunchedEffect(initialText) { focusRequester.requestFocus() }
ThreeButtonAlertDialog( ThreeButtonAlertDialog(

View file

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

View file

@ -2,12 +2,15 @@ package helium314.keyboard.settings.screens
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.widget.Toast
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale 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.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview 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.BuildConfig
import helium314.keyboard.latin.R import helium314.keyboard.latin.R
import helium314.keyboard.latin.SystemBroadcastReceiver import helium314.keyboard.latin.SystemBroadcastReceiver
import helium314.keyboard.latin.common.splitOnWhitespace
import helium314.keyboard.latin.settings.DebugSettings import helium314.keyboard.latin.settings.DebugSettings
import helium314.keyboard.latin.settings.Settings 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.latin.utils.prefs
import helium314.keyboard.settings.AllPrefs import helium314.keyboard.settings.AllPrefs
import helium314.keyboard.settings.ListPreference import helium314.keyboard.settings.ListPreference
@ -33,8 +49,14 @@ import helium314.keyboard.settings.SettingsDestination
import helium314.keyboard.settings.SliderPreference import helium314.keyboard.settings.SliderPreference
import helium314.keyboard.settings.SwitchPreference import helium314.keyboard.settings.SwitchPreference
import helium314.keyboard.settings.Theme 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.dialogs.TextInputDialog
import helium314.keyboard.settings.keyboardNeedsReload import helium314.keyboard.settings.keyboardNeedsReload
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
@Composable @Composable
fun AdvancedSettingsScreen( fun AdvancedSettingsScreen(
@ -159,13 +181,25 @@ fun createAdvancedPrefs(context: Context) = listOf(
default = false 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) } var showDialog by remember { mutableStateOf(false) }
Preference( Preference(
name = it.title, name = def.title,
onClick = { showDialog = true } 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 -> PrefDef(context, Settings.PREF_MORE_POPUP_KEYS, R.string.show_popup_keys_title) { def ->
val items = listOf( 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_more) to "more",
stringResource(R.string.show_popup_keys_all) to "all", stringResource(R.string.show_popup_keys_all) to "all",
) )
ListPreference(def, items, "main") ListPreference(def, items, "main") { KeyboardLayoutSet.onSystemLocaleChanged() }
// todo: on value changed -> 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) } var showDialog by remember { mutableStateOf(false) }
val ctx = LocalContext.current
var layout: String? by remember { mutableStateOf(null) }
Preference( Preference(
name = it.title, name = def.title,
onClick = { showDialog = true } onClick = { showDialog = true }
) )
if (showDialog) // todo: first the selection dialog, then the edit dialog if (showDialog) {
TextInputDialog( ListPickerDialog(
onDismissRequest = { showDialog = false }, onDismissRequest = { showDialog = false },
onConfirmed = { }, // todo showRadioButtons = false,
initialText = LocalContext.current.assets.open("layouts/dvorak.json").bufferedReader().readText() 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) } var showDialog by remember { mutableStateOf(false) }
val ctx = LocalContext.current
var layout: String? by remember { mutableStateOf(null) }
Preference( Preference(
name = it.title, name = def.title,
onClick = { showDialog = true } 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) { PrefDef(context, NonSettingsPrefs.BACKUP_RESTORE, R.string.backup_restore_title) {
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }