diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/impls/OneConfigUIScreen.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/impls/OneConfigUIScreen.kt index 2a8be4e03..91d1721b5 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/impls/OneConfigUIScreen.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/impls/OneConfigUIScreen.kt @@ -258,7 +258,7 @@ class OneConfigUIScreen @JvmOverloads constructor( } private fun warmRoute(route: Any) { - runCatching { if (shell.navigation.isReady) shell.navigation.navigate(route) } + runCatching { if (shell.navigation.isReady) shell.navigation.navigate(route, clearSearch = false) } } private fun markClosed() { diff --git a/minecraft/src/main/resources/assets/oneconfig/lang/en_us.json b/minecraft/src/main/resources/assets/oneconfig/lang/en_us.json index a11cfb981..63bf73d9d 100644 --- a/minecraft/src/main/resources/assets/oneconfig/lang/en_us.json +++ b/minecraft/src/main/resources/assets/oneconfig/lang/en_us.json @@ -82,7 +82,7 @@ "oneconfig.preferences.show_opening_page_animation.description": "Plays the page animation when the menu is first opened.", "oneconfig.preferences.instant_search.title": "Instant Search", - "oneconfig.preferences.instant_search.description": "Focuses the search bar when the OneConfig menu opens.", + "oneconfig.preferences.instant_search.description": "Focuses the search bar when the OneConfig menu opens or you change pages.", "oneconfig.preferences.search_keybind.title": "Search Keybind", "oneconfig.preferences.search_keybind.description": "Focuses the search bar in the OneConfig menu and HUD editor.", diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/OneConfigInterface.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/OneConfigInterface.kt index bc6f7fd21..c469d27e4 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/OneConfigInterface.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/OneConfigInterface.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.Density import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner @@ -33,6 +34,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeoutOrNull import org.apache.logging.log4j.LogManager import org.polyfrost.oneconfig.internal.OneConfigConfig +import org.polyfrost.oneconfig.internal.ui.components.LocalSearchFieldFocused import org.polyfrost.oneconfig.internal.ui.components.RetainedVisibility import org.polyfrost.oneconfig.internal.ui.hud.screens.HudDragLayer import org.polyfrost.oneconfig.internal.ui.navigation.graph.ModsGraph @@ -128,7 +130,13 @@ fun OneConfigInterface( else if (opened) onCloseRequest() } - val requestClose: () -> Unit = { visible = false } + val searchFocused = remember { mutableStateOf(false) } + val focusManager = LocalFocusManager.current + val requestClose: () -> Unit = { + // commit any field still being edited before configs save, but keep search focused for reopening + if (!searchFocused.value) focusManager.clearFocus() + visible = false + } val requestOpen: () -> Unit = { visible = true } SideEffect { @@ -138,6 +146,7 @@ fun OneConfigInterface( CompositionLocalProvider( LocalCloseRequest provides requestClose, + LocalSearchFieldFocused provides searchFocused, LocalShell provides shell, LocalShellNavController provides navController, ) { diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Button.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Button.kt index 3ac45b0d1..72124e503 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Button.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Button.kt @@ -8,6 +8,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.unit.dp import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme @@ -46,7 +48,9 @@ fun IconButton( Icon( iconName, color = iconColor, - modifier = Modifier.onClick(interactionSource, enabled, onClick).then(modifier) + modifier = Modifier.onClick(interactionSource, enabled, onClick) + .then(if (enabled) Modifier.pointerHoverIcon(PointerIcon.Hand) else Modifier) + .then(modifier) // inset the glyph so the outline doesn't hug it; size bordered buttons 8dp larger to keep the icon size .then( if (hoverBorder) Modifier.border(1.dp, borderColor, LocalTheme.current.buttonShape).padding(4.dp) diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Common.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Common.kt index 61863634a..cf481262d 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Common.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Common.kt @@ -4,16 +4,26 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.focus.FocusProperties +import androidx.compose.ui.focus.FocusPropertiesModifierNode import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.node.CompositionLocalConsumerModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.PointerInputModifierNode +import androidx.compose.ui.node.currentValueOf +import androidx.compose.ui.unit.IntSize import org.polyfrost.oneconfig.internal.ui.sound.UiSoundEvent import org.polyfrost.oneconfig.internal.ui.sound.UiSounds @@ -23,15 +33,52 @@ fun rememberInteractionSource() = remember { MutableInteractionSource() } fun Modifier.onClick(interactionSource: MutableInteractionSource, onClick: () -> Unit) = onClick(interactionSource, true, onClick) -fun Modifier.onClick(interactionSource: MutableInteractionSource, enabled: Boolean, onClick: () -> Unit) = clickable( - interactionSource = interactionSource, - indication = null, - enabled = enabled, - onClick = { - UiSounds.play(UiSoundEvent.CLICK) - onClick() +fun Modifier.onClick(interactionSource: MutableInteractionSource, enabled: Boolean, onClick: () -> Unit) = + // when disabled it would apply to focusable children instead + (if (enabled) keepSearchFocus() else this).clickable( + interactionSource = interactionSource, + indication = null, + enabled = enabled, + onClick = { + UiSounds.play(UiSoundEvent.CLICK) + onClick() + } + ) + +val LocalSearchFieldFocused = staticCompositionLocalOf?> { null } + +/** Clicking this doesn't take focus from the search field */ +fun Modifier.keepSearchFocus(): Modifier = this then KeepSearchFocusElement + +private object KeepSearchFocusElement : ModifierNodeElement() { + override fun create() = KeepSearchFocusNode() + + override fun update(node: KeepSearchFocusNode) {} + + override fun equals(other: Any?) = other === this + + override fun hashCode() = System.identityHashCode(this) +} + +private class KeepSearchFocusNode : + Modifier.Node(), PointerInputModifierNode, FocusPropertiesModifierNode, CompositionLocalConsumerModifierNode { + + // set only while a press is handled, so tab can still focus this + private var pressing = false + + override fun onPointerEvent(pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize) { + if (pass == PointerEventPass.Initial) pressing = pointerEvent.type == PointerEventType.Press + else if (pass == PointerEventPass.Final) pressing = false + } + + override fun onCancelPointerInput() { + pressing = false + } + + override fun applyFocusProperties(focusProperties: FocusProperties) { + if (pressing && currentValueOf(LocalSearchFieldFocused)?.value == true) focusProperties.canFocus = false } -) +} /** * Swallows press and release events before children see them so anything below cannot be clicked, dragged diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Header.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Header.kt index 417754fbf..244ab7251 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Header.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Header.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon @@ -318,6 +319,7 @@ fun GlobalSearchBar() { val isFocused by interactionSource.collectIsFocusedAsState() val focusRequester = remember { FocusRequester() } + val searchFocused = LocalSearchFieldFocused.current LaunchedEffect(searchText.text) { search.update(searchText.text) } LaunchedEffect(search.query) { @@ -366,7 +368,9 @@ fun GlobalSearchBar() { cursorBrush = SolidColor(LocalTheme.current.textColorSecondary), textStyle = searchTextStyle, interactionSource = interactionSource, - modifier = Modifier.trackTextInputFocus().focusRequester(focusRequester) + modifier = Modifier.trackTextInputFocus() + .focusRequester(focusRequester) + .onFocusChanged { searchFocused?.value = it.isFocused } ) { innerTextField -> Box( modifier = Modifier diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Mods.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Mods.kt index 3676220bf..af4154cde 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Mods.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Mods.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.Arrangement @@ -56,7 +57,6 @@ import androidx.compose.ui.graphics.drawOutline import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.lerp -import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerInput @@ -275,11 +275,11 @@ fun ColumnScope.ModsGrid(category: ModCategory) { .weight(1f) // see overlayActive above: the overlay card has to be able to leave the grid while it exists .then(if (overlayActive) Modifier else Modifier.clipToBounds()) - // cards can't take focus, so clear it on press to unfocus the search field + // cards and their buttons consume their clicks, so only a click on empty space unfocuses the search field .pointerInput(focusManager) { awaitEachGesture { - awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial) - focusManager.clearFocus() + awaitFirstDown(requireUnconsumed = false) + if (waitForUpOrCancellation() != null) focusManager.clearFocus() } } .reorderContainer(reorderState), diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Themes.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Themes.kt index 8c3d23815..b161fd46e 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Themes.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Themes.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import org.polyfrost.oneconfig.internal.ui.api.ConfigRegistry import org.polyfrost.oneconfig.internal.ui.components.Text +import org.polyfrost.oneconfig.internal.ui.components.keepSearchFocus import org.polyfrost.oneconfig.internal.ui.components.rememberInteractionSource import org.polyfrost.oneconfig.internal.ui.shell.PageGutter import org.polyfrost.oneconfig.internal.ui.themes.Accent @@ -78,6 +79,7 @@ private fun ThemeCard(theme: UITheme) { modifier = Modifier .background(LocalTheme.current.modCardBackground, LocalTheme.current.backgroundShape) .border(borderWidth, borderColor, LocalTheme.current.backgroundShape) + .keepSearchFocus() .clickable(interactionSource, indication = null) { // Use clickable because onClick always plays CLICK. ThemeRegistry.activate chooses CLICK or OPEN ThemeRegistry.activate(theme) diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/ShellController.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/ShellController.kt index 0f9b3764c..fff887b42 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/ShellController.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/ShellController.kt @@ -344,6 +344,7 @@ class ShellNavigation internal constructor( seedRouteCategory(route) updateArrows() search.bind(host.currentBackStackEntry?.id, query) + if (clearSearch) instantSearch() } fun discard(route: Any, fallback: Any) { @@ -409,6 +410,7 @@ class ShellNavigation internal constructor( applyCategory(currentEntry) search.bind(host.currentBackStackEntry?.id, currentEntry.searchQuery) lastRoute = currentEntry.route + instantSearch() updateArrows() } @@ -430,6 +432,11 @@ class ShellNavigation internal constructor( updateArrows() host.navigate(next.route) search.bind(host.currentBackStackEntry?.id, next.searchQuery) + instantSearch() + } + + private fun instantSearch() { + if (OneConfigConfig.instantSearch) search.requestFocus(SearchFocus.SelectAll) } /** A route that names a category such as an external deep link overrides the remembered tab */ diff --git a/modules/internal/src/main/resources/assets/oneconfig/en_default.lang b/modules/internal/src/main/resources/assets/oneconfig/en_default.lang index d1d4aa389..488eaa6d7 100644 --- a/modules/internal/src/main/resources/assets/oneconfig/en_default.lang +++ b/modules/internal/src/main/resources/assets/oneconfig/en_default.lang @@ -61,7 +61,7 @@ oneconfig.preferences.remember_last_page.always=Always oneconfig.preferences.show_opening_page_animation.title=Show opening page animation oneconfig.preferences.show_opening_page_animation.description=Plays the page animation when the menu is first opened. oneconfig.preferences.instant_search.title=Instant Search -oneconfig.preferences.instant_search.description=Focuses the search bar when the OneConfig menu opens. +oneconfig.preferences.instant_search.description=Focuses the search bar when the OneConfig menu opens or you change pages. oneconfig.preferences.search_keybind.title=Search Keybind oneconfig.preferences.search_keybind.description=Focuses the search bar in the OneConfig menu and HUD editor. oneconfig.preferences.flip_top_option_order.title=Flip Top Option Order