@@ -3,6 +3,11 @@ package helium314.keyboard.settings
33
44import androidx.activity.compose.BackHandler
55import 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
611import androidx.compose.foundation.layout.Box
712import androidx.compose.foundation.layout.Column
813import androidx.compose.foundation.layout.ColumnScope
@@ -53,6 +58,7 @@ import androidx.compose.ui.unit.dp
5358import helium314.keyboard.latin.R
5459import helium314.keyboard.settings.preferences.PreferenceCategory
5560
61+ import helium314.keyboard.latin.utils.getActivity
5662import 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
171305fun <T : Any ?> SearchScreen (
0 commit comments