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
140 changes: 137 additions & 3 deletions app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ package helium314.keyboard.settings

import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
Expand Down Expand Up @@ -53,6 +58,7 @@ import androidx.compose.ui.unit.dp
import helium314.keyboard.latin.R
import helium314.keyboard.settings.preferences.PreferenceCategory

import helium314.keyboard.latin.utils.getActivity
import helium314.keyboard.latin.utils.prefs

@Composable
Expand Down Expand Up @@ -104,7 +110,35 @@ fun SearchSettingsScreen(
result.filter { it.second.isNotEmpty() }
}

// jump from a long press in the search results: scroll to the setting and highlight it
val listState = androidx.compose.foundation.lazy.rememberLazyListState()
var highlightKey by remember { mutableStateOf<String?>(null) }
// fine-tune sections opened only to find the setting, and the setting to scroll to
var openedForJump by remember { mutableStateOf(emptySet<String>()) }
var scrollKey by remember { mutableStateOf<String?>(null) }
LaunchedEffect(groups) {
val keysOnScreen = groups.flatMap { it.second }
if (scrollKey == null) {
val found = SettingsJump.take(keysOnScreen)
val step = jumpThroughFineTuneSections(context, settings, openedForJump, found)
openedForJump = step.opened
if (step.prefsChanged) (context.getActivity() as? SettingsActivity)?.prefChanged()
if (found == null) return@LaunchedEffect
scrollKey = found
// closing the other sections changes the list, scroll after that
if (step.prefsChanged) return@LaunchedEffect
}
val jumpKey = scrollKey ?: return@LaunchedEffect
scrollKey = null
if (jumpKey !in keysOnScreen) return@LaunchedEffect
val index = groups.indexOfFirst { jumpKey in it.second }
highlightKey = jumpKey
listState.animateScrollToItem(index + if (headerContent != null) 1 else 0)
kotlinx.coroutines.delay(2500)
highlightKey = null
}
LazyColumn(
state = listState,
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
Expand All @@ -128,10 +162,16 @@ fun SearchSettingsScreen(
if (titleRes != null) {
PreferenceCategory(stringResource(titleRes))
}

keys.forEach { key ->
androidx.compose.runtime.key(key) {
SettingsActivity.settingsContainer[key]?.Preference()
val highlight by androidx.compose.animation.animateColorAsState(
if (key == highlightKey) MaterialTheme.colorScheme.primaryContainer
else androidx.compose.ui.graphics.Color.Transparent,
label = "jump highlight"
)
Box(Modifier.background(highlight)) {
SettingsActivity.settingsContainer[key]?.Preference()
}
}
}
}
Expand Down Expand Up @@ -162,10 +202,104 @@ fun SearchSettingsScreen(
}
}
},
itemContent = { it.Preference() }
// long press on a search result opens the screen where the setting is
itemContent = { setting -> JumpOnLongPress(setting.key) { setting.Preference() } }
)
}

private const val FINE_TUNE_SECTION_PREFIX = "expand_fine_tune_"

internal class JumpStep(val opened: Set<String>, val prefsChanged: Boolean)

/**
* A setting in a closed fine-tune section is not in the list. Then all closed sections of the screen are
* opened; once the setting is [found], only the section that contains it stays open. If the setting is
* still missing (hidden for another reason), the sections are closed again.
*/
internal fun jumpThroughFineTuneSections(
context: android.content.Context,
settings: List<Any?>,
opened: Set<String>,
found: String?,
): JumpStep {
val prefs = context.prefs()
fun close(keys: Collection<String>) = prefs.edit().apply { keys.forEach { putBoolean(it, false) } }.apply()
if (found != null) {
if (opened.isEmpty()) return JumpStep(emptySet(), false)
val index = settings.indexOf(found)
val owner = settings.subList(0, index.coerceAtLeast(0)).lastOrNull { it is String && it in opened }
val toClose = opened - setOfNotNull(owner as String?)
close(toClose)
return JumpStep(emptySet(), toClose.isNotEmpty())
}
if (opened.isNotEmpty()) {
close(opened)
return JumpStep(emptySet(), true)
}
if (!SettingsJump.isPending()) return JumpStep(emptySet(), false)
val closed = settings.filterIsInstance<String>()
.filter { it.startsWith(FINE_TUNE_SECTION_PREFIX) && !prefs.getBoolean(it, false) }
if (closed.isEmpty()) return JumpStep(emptySet(), false)
prefs.edit().apply { closed.forEach { putBoolean(it, true) } }.apply()
return JumpStep(closed.toSet(), true)
}

/** A setting to show and highlight on its screen after a long press in the search results */
object SettingsJump {
private const val VALID_MS = 3000L
private var key: String? = null
private var time = 0L

fun request(key: String) {
this.key = key
time = android.os.SystemClock.uptimeMillis()
}

fun isPending() = key != null && android.os.SystemClock.uptimeMillis() - time <= VALID_MS

/** The requested key if it is on this screen; it is then used up */
fun take(keysOnScreen: Collection<String>): String? {
val k = key ?: return null
if (android.os.SystemClock.uptimeMillis() - time > VALID_MS) { key = null; return null }
if (k !in keysOnScreen) return null
key = null
return k
}
}

@Composable
private fun JumpOnLongPress(key: String, content: @Composable () -> Unit) {
val destination = SettingsActivity.settingsContainer.screenOf(key)
if (destination == null) {
content()
return
}
val haptic = androidx.compose.ui.platform.LocalHapticFeedback.current
Box(Modifier.pointerInput(key) {
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
// null on timeout: the finger stayed down without moving, a long press
val endedEarly = withTimeoutOrNull(viewConfiguration.longPressTimeoutMillis) {
while (true) {
val change = awaitPointerEvent(PointerEventPass.Initial).changes.firstOrNull { it.id == down.id } ?: break
if (!change.pressed || (change.position - down.position).getDistance() > viewConfiguration.touchSlop) break
}
true
}
if (endedEarly == null) {
haptic.performHapticFeedback(androidx.compose.ui.hapticfeedback.HapticFeedbackType.LongPress)
SettingsJump.request(key)
SettingsDestination.navigateTo(destination)
// the setting itself must not react to this touch
do {
val event = awaitPointerEvent(PointerEventPass.Initial)
event.changes.forEach { it.consume() }
} while (event.changes.any { it.pressed })
}
}
}) { content() }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T: Any?> SearchScreen(
Expand Down
16 changes: 12 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,15 +27,22 @@ 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 {
putIfAbsent(it.key, it)
}
}
/** 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 @@ -151,10 +158,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
Loading