Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 75 additions & 2 deletions app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,16 @@ import helium314.keyboard.latin.R
import helium314.keyboard.settings.preferences.PreferenceCategory

import helium314.keyboard.latin.utils.prefs
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import helium314.keyboard.settings.preferences.LocalPreferenceLongClick
import kotlinx.coroutines.delay

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SearchSettingsScreen(
onClickBack: () -> Unit,
Expand Down Expand Up @@ -104,7 +113,46 @@ fun SearchSettingsScreen(
result.filter { it.second.isNotEmpty() }
}

// jump from a long press in the search results (see SettingsJump): show the setting
// here, opening its fine-tune section if needed, scroll to it and highlight it
val listState = rememberLazyListState()
var highlightKey by remember { mutableStateOf<String?>(null) }
val jumpTarget = jumpTarget()
val jumpOpened = jumpOpenedSections()
val jump = jumpState()
// the setting to scroll to once the list shows it; separate from the jump state, so that
// ending the jump does not cancel the scrolling
var scrollKey by remember { mutableStateOf<String?>(null) }
LaunchedEffect(groups, jumpTarget) {
val target = jumpTarget ?: return@LaunchedEffect
val shown = groups.flatMap { it.second }
val prefs = context.prefs()
when (val step = nextJumpStep(target, settings, shown, jumpOpened) { prefs.getBoolean(it, false) }) {
// the screen shows more settings then, and this runs again
is JumpStep.OpenSections -> jump.setOpenedSections(step.sections)
JumpStep.GiveUp -> { jump.setOpenedSections(emptySet()); jump.done() }
is JumpStep.Show -> {
// closing the other sections changes the list, this runs again after that
if (step.keepOpen != jumpOpened) jump.setOpenedSections(step.keepOpen)
else { scrollKey = target; jump.done() }
}
}
}
LaunchedEffect(scrollKey) {
val target = scrollKey ?: return@LaunchedEffect
val index = groups.indexOfFirst { target in it.second }
if (index >= 0) {
// to the card with the setting, then the setting itself is brought into view (see below)
listState.animateScrollToItem(index + if (headerContent != null) 1 else 0)
highlightKey = target
delay(2500)
highlightKey = null
}
scrollKey = null
}

LazyColumn(
state = listState,
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
Expand All @@ -131,7 +179,17 @@ fun SearchSettingsScreen(

keys.forEach { key ->
androidx.compose.runtime.key(key) {
SettingsActivity.settingsContainer[key]?.Preference()
val highlight by animateColorAsState(
if (key == highlightKey) MaterialTheme.colorScheme.primaryContainer
else androidx.compose.ui.graphics.Color.Transparent,
label = "jump highlight"
)
// a setting further down in a large card is not on screen after scrolling to the card
val bringIntoView = remember { BringIntoViewRequester() }
LaunchedEffect(highlightKey) { if (key == highlightKey) bringIntoView.bringIntoView() }
Box(Modifier.bringIntoViewRequester(bringIntoView).background(highlight)) {
SettingsActivity.settingsContainer[key]?.Preference()
}
}
}
}
Expand Down Expand Up @@ -162,10 +220,25 @@ fun SearchSettingsScreen(
}
}
},
itemContent = { it.Preference() }
// long press on a search result opens the screen where the setting is
itemContent = { setting -> SearchResult(setting) }
)
}

/** A search result: a long press opens the screen of the setting and shows it there */
@Composable
private fun SearchResult(setting: Setting) {
val navController = LocalSettingsNavController.current
val destination = SettingsActivity.settingsContainer.screenOf(setting.key)
if (navController == null || destination == null) {
setting.Preference()
return
}
CompositionLocalProvider(LocalPreferenceLongClick provides { jumpToSetting(navController, destination, setting.key) }) {
setting.Preference()
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T: Any?> SearchScreen(
Expand Down
17 changes: 13 additions & 4 deletions app/src/main/java/helium314/keyboard/settings/SettingsContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import helium314.keyboard.settings.screens.createToolbarSettings
import helium314.keyboard.latin.voice.createVoiceSettings

class SettingsContainer(context: Context) {
private val list = createSettings(context)
private val settingsWithScreens = createSettings(context)
private val list = settingsWithScreens.map { it.first }
private val map: Map<String, Setting> = HashMap<String, Setting>(list.size).apply {
list.forEach {
if (!containsKey(it.key)) {
Expand All @@ -36,8 +37,15 @@ class SettingsContainer(context: Context) {
}
}

/** settings screen (destination) that shows a setting, for jumping there from the search */
private val screens: Map<String, String> = HashMap<String, String>().apply {
settingsWithScreens.forEach { (setting, destination) -> if (destination != null) putIfAbsent(setting.key, destination) }
}

operator fun get(key: Any): Setting? = map[key]

fun screenOf(key: String): String? = screens[key]

// filtering could be more elaborate, but should be good enough for a start
// always have all settings in search, because:
// don't show disabled settings -> users confused
Expand Down Expand Up @@ -153,10 +161,11 @@ private val modules = listOf(
SettingsModule(SettingsWithoutKey.SCREEN_NAV_UPDATES, SettingsDestination.Updates, R.string.settings_screen_updates, iconRes = R.drawable.ic_settings_updates)
)

private fun createSettings(context: Context): List<Setting> = buildList {
/** all settings, each with the destination of the screen that shows it (null for the navigation entries) */
private fun createSettings(context: Context): List<Pair<Setting, String?>> = buildList {
modules.forEach { module ->
add(module.createNavigationSetting(context))
module.provider?.invoke(context)?.let { addAll(it) }
add(module.createNavigationSetting(context) to null)
module.provider?.invoke(context)?.forEach { add(it to module.destination) }
}
}

Expand Down
102 changes: 102 additions & 0 deletions app/src/main/java/helium314/keyboard/settings/SettingsJump.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings

import android.content.SharedPreferences
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController

/*
* Jump from a long press in the settings search to the screen of a setting, which then scrolls to the
* setting and highlights it. What the screen is asked to show, and the fine-tune sections opened for it,
* belong to the back stack entry of that screen: nothing is written to the preferences, nothing is kept
* in a global object, and it ends when the screen is left.
*/

private const val JUMP_TO = "settings_jump_to"
private const val JUMP_OPENED_SECTIONS = "settings_jump_opened_sections"
internal const val FINE_TUNE_SECTION_PREFIX = "expand_fine_tune_"

/** The nav controller of the settings, used by the search results to open the screen of a setting */
val LocalSettingsNavController = staticCompositionLocalOf<NavController?> { null }

/** Opens [destination] and asks it to show the setting [key] */
fun jumpToSetting(navController: NavController, destination: String, key: String) {
navController.navigate(destination)
navController.currentBackStackEntry?.savedStateHandle?.set(JUMP_TO, key)
}

/** State of the screen this is called in: null outside of the navigation (e.g. in a preview) */
@Composable
private fun screenState(): SavedStateHandle? = (LocalViewModelStoreOwner.current as? NavBackStackEntry)?.savedStateHandle

/** The setting this screen is asked to show, if any */
@Composable
internal fun jumpTarget(): String? {
val state = screenState() ?: return null
val target by state.getStateFlow<String?>(JUMP_TO, null).collectAsState()
return target
}

/** Fine-tune sections opened on this screen to show a setting; they are not saved */
@Composable
fun jumpOpenedSections(): Set<String> {
val state = screenState() ?: return emptySet()
val opened by state.getStateFlow(JUMP_OPENED_SECTIONS, ArrayList<String>()).collectAsState()
return opened.toSet()
}

/** Whether a fine-tune section is open: as the user left it, or opened to show a setting (see [jumpOpenedSections]) */
@Composable
fun fineTuneExpanded(prefs: SharedPreferences, key: String, default: Boolean): Boolean =
prefs.getBoolean(key, default) || key in jumpOpenedSections()

/** Changes of this screen's jump state, for use outside of composition */
class JumpState internal constructor(private val state: SavedStateHandle?) {
fun setOpenedSections(sections: Collection<String>) { state?.set(JUMP_OPENED_SECTIONS, ArrayList(sections)) }
/** The user opens or closes the section [key]: from now on it is as the user wants */
fun userToggledSection(key: String) {
val opened = state?.get<ArrayList<String>>(JUMP_OPENED_SECTIONS) ?: return
if (key in opened) setOpenedSections(opened - key)
}
fun done() { state?.remove<String>(JUMP_TO) }
}

@Composable
fun jumpState() = JumpState(screenState())

/** What to do next to show [target] on a screen */
internal sealed interface JumpStep {
/** Open these closed fine-tune sections, the setting may be in one of them */
data class OpenSections(val sections: Set<String>) : JumpStep
/** The setting is shown; of the sections opened for the jump, keep only [keepOpen] (the one with the setting) */
data class Show(val keepOpen: Set<String>) : JumpStep
/** The setting is not on this screen, or hidden for another reason: close the opened sections again */
data object GiveUp : JumpStep
}

/**
* [settings]: the screen's list (setting keys and section titles), [shown]: the keys in the list now,
* [opened]: sections opened for this jump, [isOpen]: whether a section is open as the user left it.
*/
internal fun nextJumpStep(
target: String,
settings: List<Any?>,
shown: Collection<String>,
opened: Set<String>,
isOpen: (String) -> Boolean,
): JumpStep {
if (target in shown) {
val index = settings.indexOf(target)
val owner = settings.subList(0, index.coerceAtLeast(0)).lastOrNull { it is String && it in opened } as String?
return JumpStep.Show(setOfNotNull(owner))
}
if (opened.isNotEmpty()) return JumpStep.GiveUp
val closed = settings.filterIsInstance<String>().filter { it.startsWith(FINE_TUNE_SECTION_PREFIX) && !isOpen(it) }
return if (closed.isEmpty()) JumpStep.GiveUp else JumpStep.OpenSections(closed.toSet())
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
Expand Down Expand Up @@ -65,6 +66,8 @@ fun SettingsNavHost(
if (!navController.popBackStack()) onClickBack()
}

// the search results open the screen of a setting with it, see SettingsJump
CompositionLocalProvider(LocalSettingsNavController provides navController) {
NavHost(
navController = navController,
startDestination = startDestination ?: SettingsDestination.Settings,
Expand Down Expand Up @@ -223,6 +226,7 @@ fun SettingsNavHost(
helium314.keyboard.settings.screens.CjkSettingsScreen(onClickBack = ::goBack)
}
}
}
}

object SettingsDestination {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import helium314.keyboard.latin.utils.getActivity
import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.settings.Setting
import helium314.keyboard.settings.SettingsActivity
import helium314.keyboard.settings.jumpOpenedSections
import helium314.keyboard.settings.jumpState
import kotlinx.coroutines.flow.MutableStateFlow

@Composable
Expand All @@ -26,15 +28,19 @@ fun ExpandablePreference(
val prefs = ctx.prefs()
val activity = ctx.getActivity() as? SettingsActivity
val prefChanged = (activity?.prefChanged ?: MutableStateFlow(0)).collectAsState()
val expanded = remember(prefChanged.value) {
val saved = remember(prefChanged.value) {
prefs.getBoolean(setting.key, defaultExpanded)
}
// may be open only to show a setting from the search, without changing what is saved
val expanded = saved || setting.key in jumpOpenedSections()
val jump = jumpState()
Preference(
name = setting.title,
description = setting.description,
onClick = {
val newExpanded = !expanded
prefs.edit { putBoolean(setting.key, newExpanded) }
jump.userToggledSection(setting.key)
activity?.prefChanged()
}
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
package helium314.keyboard.settings.preferences

import androidx.annotation.DrawableRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand All @@ -22,9 +24,11 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.draw.clip
import androidx.compose.foundation.background
import androidx.compose.ui.text.style.Hyphens
Expand Down Expand Up @@ -54,6 +58,10 @@ fun PreferenceCategory(
}
}

/** A long press action for the [Preference] shown inside, e.g. "show in its screen" for search results */
val LocalPreferenceLongClick = compositionLocalOf<(() -> Unit)?> { null }

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Preference(
name: String,
Expand All @@ -64,10 +72,19 @@ fun Preference(
@DrawableRes icon: Int? = null,
value: @Composable (RowScope.() -> Unit)? = null,
) {
val onLongClick = LocalPreferenceLongClick.current
Row(
modifier = modifier
.fillMaxWidth()
.clickable(enabled = enabled) { onClick() }
.then(
if (onLongClick == null) Modifier.clickable(enabled = enabled) { onClick() }
else Modifier.combinedClickable(
enabled = enabled,
onLongClickLabel = stringResource(R.string.settings_search_show_in_screen),
onLongClick = onLongClick,
onClick = onClick,
)
)
.heightIn(min = 44.dp)
.padding(vertical = 10.dp, horizontal = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
Expand Down Expand Up @@ -105,6 +122,7 @@ fun Preference(
}
if (value != null) {
CompositionLocalProvider(
LocalPreferenceLongClick provides null,
LocalTextStyle provides LocalTextStyle.current.copy(
textAlign = TextAlign.End,
hyphens = Hyphens.Auto
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.checkTimestampFormat
import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.settings.fineTuneExpanded
import helium314.keyboard.settings.dialogs.ConfirmationDialog
import helium314.keyboard.settings.NextScreenIcon
import helium314.keyboard.settings.SettingsContainer
Expand Down Expand Up @@ -83,7 +84,7 @@ fun AdvancedSettingsScreen(
val activity = ctx.getActivity() as? SettingsActivity
val prefChanged = (activity?.prefChanged ?: MutableStateFlow(0)).collectAsState()

val fineTuneAdvancedExpanded = prefs.getBoolean(Settings.PREF_EXPAND_FINE_TUNE_ADVANCED, Defaults.PREF_EXPAND_FINE_TUNE_ADVANCED)
val fineTuneAdvancedExpanded = fineTuneExpanded(prefs, Settings.PREF_EXPAND_FINE_TUNE_ADVANCED, Defaults.PREF_EXPAND_FINE_TUNE_ADVANCED)

val items = remember(fineTuneAdvancedExpanded, prefChanged.value) {
listOfNotNull(
Expand Down
Loading
Loading