Skip to content

Commit ce5d426

Browse files
4di-helloclaude
andcommitted
Settings search: long press on a result opens its screen
A tap on a search result still changes the setting. A long press opens the screen that has the setting, scrolls to it and highlights it briefly, so it can be found again without searching. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VuiaMhLXwhErf9shhrr1uD
1 parent 0db0936 commit ce5d426

2 files changed

Lines changed: 149 additions & 7 deletions

File tree

‎app/src/main/java/helium314/keyboard/settings/SearchScreen.kt‎

Lines changed: 137 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ package helium314.keyboard.settings
33

44
import androidx.activity.compose.BackHandler
55
import androidx.compose.animation.AnimatedVisibility
6+
import androidx.compose.foundation.background
7+
import androidx.compose.foundation.gestures.awaitEachGesture
8+
import androidx.compose.foundation.gestures.awaitFirstDown
9+
import androidx.compose.ui.input.pointer.PointerEventPass
10+
import androidx.compose.ui.input.pointer.pointerInput
611
import androidx.compose.foundation.layout.Box
712
import androidx.compose.foundation.layout.Column
813
import androidx.compose.foundation.layout.ColumnScope
@@ -53,6 +58,7 @@ import androidx.compose.ui.unit.dp
5358
import helium314.keyboard.latin.R
5459
import helium314.keyboard.settings.preferences.PreferenceCategory
5560

61+
import helium314.keyboard.latin.utils.getActivity
5662
import helium314.keyboard.latin.utils.prefs
5763

5864
@Composable
@@ -104,7 +110,35 @@ fun SearchSettingsScreen(
104110
result.filter { it.second.isNotEmpty() }
105111
}
106112

113+
// jump from a long press in the search results: scroll to the setting and highlight it
114+
val listState = androidx.compose.foundation.lazy.rememberLazyListState()
115+
var highlightKey by remember { mutableStateOf<String?>(null) }
116+
// fine-tune sections opened only to find the setting, and the setting to scroll to
117+
var openedForJump by remember { mutableStateOf(emptySet<String>()) }
118+
var scrollKey by remember { mutableStateOf<String?>(null) }
119+
LaunchedEffect(groups) {
120+
val keysOnScreen = groups.flatMap { it.second }
121+
if (scrollKey == null) {
122+
val found = SettingsJump.take(keysOnScreen)
123+
val step = jumpThroughFineTuneSections(context, settings, openedForJump, found)
124+
openedForJump = step.opened
125+
if (step.prefsChanged) (context.getActivity() as? SettingsActivity)?.prefChanged()
126+
if (found == null) return@LaunchedEffect
127+
scrollKey = found
128+
// closing the other sections changes the list, scroll after that
129+
if (step.prefsChanged) return@LaunchedEffect
130+
}
131+
val jumpKey = scrollKey ?: return@LaunchedEffect
132+
scrollKey = null
133+
if (jumpKey !in keysOnScreen) return@LaunchedEffect
134+
val index = groups.indexOfFirst { jumpKey in it.second }
135+
highlightKey = jumpKey
136+
listState.animateScrollToItem(index + if (headerContent != null) 1 else 0)
137+
kotlinx.coroutines.delay(2500)
138+
highlightKey = null
139+
}
107140
LazyColumn(
141+
state = listState,
108142
modifier = Modifier
109143
.padding(innerPadding)
110144
.fillMaxSize(),
@@ -128,10 +162,16 @@ fun SearchSettingsScreen(
128162
if (titleRes != null) {
129163
PreferenceCategory(stringResource(titleRes))
130164
}
131-
132165
keys.forEach { key ->
133166
androidx.compose.runtime.key(key) {
134-
SettingsActivity.settingsContainer[key]?.Preference()
167+
val highlight by androidx.compose.animation.animateColorAsState(
168+
if (key == highlightKey) MaterialTheme.colorScheme.primaryContainer
169+
else androidx.compose.ui.graphics.Color.Transparent,
170+
label = "jump highlight"
171+
)
172+
Box(Modifier.background(highlight)) {
173+
SettingsActivity.settingsContainer[key]?.Preference()
174+
}
135175
}
136176
}
137177
}
@@ -162,10 +202,104 @@ fun SearchSettingsScreen(
162202
}
163203
}
164204
},
165-
itemContent = { it.Preference() }
205+
// long press on a search result opens the screen where the setting is
206+
itemContent = { setting -> JumpOnLongPress(setting.key) { setting.Preference() } }
166207
)
167208
}
168209

210+
private const val FINE_TUNE_SECTION_PREFIX = "expand_fine_tune_"
211+
212+
internal class JumpStep(val opened: Set<String>, val prefsChanged: Boolean)
213+
214+
/**
215+
* A setting in a closed fine-tune section is not in the list. Then all closed sections of the screen are
216+
* opened; once the setting is [found], only the section that contains it stays open. If the setting is
217+
* still missing (hidden for another reason), the sections are closed again.
218+
*/
219+
internal fun jumpThroughFineTuneSections(
220+
context: android.content.Context,
221+
settings: List<Any?>,
222+
opened: Set<String>,
223+
found: String?,
224+
): JumpStep {
225+
val prefs = context.prefs()
226+
fun close(keys: Collection<String>) = prefs.edit().apply { keys.forEach { putBoolean(it, false) } }.apply()
227+
if (found != null) {
228+
if (opened.isEmpty()) return JumpStep(emptySet(), false)
229+
val index = settings.indexOf(found)
230+
val owner = settings.subList(0, index.coerceAtLeast(0)).lastOrNull { it is String && it in opened }
231+
val toClose = opened - setOfNotNull(owner as String?)
232+
close(toClose)
233+
return JumpStep(emptySet(), toClose.isNotEmpty())
234+
}
235+
if (opened.isNotEmpty()) {
236+
close(opened)
237+
return JumpStep(emptySet(), true)
238+
}
239+
if (!SettingsJump.isPending()) return JumpStep(emptySet(), false)
240+
val closed = settings.filterIsInstance<String>()
241+
.filter { it.startsWith(FINE_TUNE_SECTION_PREFIX) && !prefs.getBoolean(it, false) }
242+
if (closed.isEmpty()) return JumpStep(emptySet(), false)
243+
prefs.edit().apply { closed.forEach { putBoolean(it, true) } }.apply()
244+
return JumpStep(closed.toSet(), true)
245+
}
246+
247+
/** A setting to show and highlight on its screen after a long press in the search results */
248+
object SettingsJump {
249+
private const val VALID_MS = 3000L
250+
private var key: String? = null
251+
private var time = 0L
252+
253+
fun request(key: String) {
254+
this.key = key
255+
time = android.os.SystemClock.uptimeMillis()
256+
}
257+
258+
fun isPending() = key != null && android.os.SystemClock.uptimeMillis() - time <= VALID_MS
259+
260+
/** The requested key if it is on this screen; it is then used up */
261+
fun take(keysOnScreen: Collection<String>): String? {
262+
val k = key ?: return null
263+
if (android.os.SystemClock.uptimeMillis() - time > VALID_MS) { key = null; return null }
264+
if (k !in keysOnScreen) return null
265+
key = null
266+
return k
267+
}
268+
}
269+
270+
@Composable
271+
private fun JumpOnLongPress(key: String, content: @Composable () -> Unit) {
272+
val destination = SettingsActivity.settingsContainer.screenOf(key)
273+
if (destination == null) {
274+
content()
275+
return
276+
}
277+
val haptic = androidx.compose.ui.platform.LocalHapticFeedback.current
278+
Box(Modifier.pointerInput(key) {
279+
awaitEachGesture {
280+
val down = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
281+
// null on timeout: the finger stayed down without moving, a long press
282+
val endedEarly = withTimeoutOrNull(viewConfiguration.longPressTimeoutMillis) {
283+
while (true) {
284+
val change = awaitPointerEvent(PointerEventPass.Initial).changes.firstOrNull { it.id == down.id } ?: break
285+
if (!change.pressed || (change.position - down.position).getDistance() > viewConfiguration.touchSlop) break
286+
}
287+
true
288+
}
289+
if (endedEarly == null) {
290+
haptic.performHapticFeedback(androidx.compose.ui.hapticfeedback.HapticFeedbackType.LongPress)
291+
SettingsJump.request(key)
292+
SettingsDestination.navigateTo(destination)
293+
// the setting itself must not react to this touch
294+
do {
295+
val event = awaitPointerEvent(PointerEventPass.Initial)
296+
event.changes.forEach { it.consume() }
297+
} while (event.changes.any { it.pressed })
298+
}
299+
}
300+
}) { content() }
301+
}
302+
169303
@OptIn(ExperimentalMaterial3Api::class)
170304
@Composable
171305
fun <T: Any?> SearchScreen(

‎app/src/main/java/helium314/keyboard/settings/SettingsContainer.kt‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,22 @@ import helium314.keyboard.settings.screens.createToolbarSettings
2727
import helium314.keyboard.latin.voice.createVoiceSettings
2828

2929
class SettingsContainer(context: Context) {
30-
private val list = createSettings(context)
30+
private val settingsWithScreens = createSettings(context)
31+
private val list = settingsWithScreens.map { it.first }
3132
private val map: Map<String, Setting> = HashMap<String, Setting>(list.size).apply {
3233
list.forEach {
3334
putIfAbsent(it.key, it)
3435
}
3536
}
37+
/** settings screen (destination) that shows a setting, for jumping there from the search */
38+
private val screens: Map<String, String> = HashMap<String, String>().apply {
39+
settingsWithScreens.forEach { (setting, destination) -> if (destination != null) putIfAbsent(setting.key, destination) }
40+
}
3641

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

44+
fun screenOf(key: String): String? = screens[key]
45+
3946
// filtering could be more elaborate, but should be good enough for a start
4047
// always have all settings in search, because:
4148
// don't show disabled settings -> users confused
@@ -151,10 +158,11 @@ private val modules = listOf(
151158
SettingsModule(SettingsWithoutKey.SCREEN_NAV_UPDATES, SettingsDestination.Updates, R.string.settings_screen_updates, iconRes = R.drawable.ic_settings_updates)
152159
)
153160

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

0 commit comments

Comments
 (0)