From 0f53f7ecdf0c0ab8b21cb198344f2a4b3a178e37 Mon Sep 17 00:00:00 2001 From: Jonas Haugesen Date: Thu, 10 Sep 2026 11:45:14 +0200 Subject: [PATCH] Open the search when typing on a physical keyboard Typing on a physical keyboard while the home screen is shown now opens the search page and inserts the typed character, so the launcher can be used without touching the screen. It works for any printable key, appends the characters in order even when the user types faster than the launcher can recompose, and the search bar can still be tapped to edit the query with the on-screen keyboard as usual. While the search is driven by the keyboard, the search bar's text field is left unfocused on purpose: a focused text field would make the system show the on-screen keyboard, which is not wanted when there is a physical keyboard. The handler therefore inserts and deletes characters itself, including backspace and enter (to launch the best match). The behaviour is controlled by a new "Search on typing" preference in Settings -> Search, which is enabled by default. Implementation notes: - The key events are intercepted in SharedLauncherActivity.dispatchKeyEvent, which is the only place that sees hardware key events while no view has focus. The scaffold installs the handler while it is shown. - Key events are not forwarded to the search bar's text field, so the characters are inserted into the search view model directly. - SearchBar keeps a TextFieldValue instead of a String so that the cursor moves to the end of the text when the value is changed from the outside. Without this the next typed character ended up in front of the text. --- .gitignore | 3 +- app/app/build.gradle.kts | 25 +++- .../mm20/launcher2/ui/component/SearchBar.kt | 23 +++- .../ui/launcher/SharedLauncherActivity.kt | 30 ++++ .../ui/launcher/scaffold/LauncherScaffold.kt | 35 ++++- .../scaffold/components/SearchComponent.kt | 4 +- .../launcher2/ui/launcher/search/SearchVM.kt | 6 + .../launcher/searchbar/TypeToSearchHandler.kt | 130 ++++++++++++++++++ .../sheets/LauncherBottomSheetManager.kt | 10 ++ .../settings/search/SearchSettingsScreen.kt | 10 ++ .../settings/search/SearchSettingsScreenVM.kt | 7 + core/i18n/src/main/res/values-da/strings.xml | 2 + core/i18n/src/main/res/values/strings.xml | 2 + .../preferences/LauncherSettingsData.kt | 5 + .../preferences/ui/SearchUiSettings.kt | 12 ++ 15 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/TypeToSearchHandler.kt diff --git a/.gitignore b/.gitignore index 33233646b..3d1e7d181 100644 --- a/.gitignore +++ b/.gitignore @@ -295,4 +295,5 @@ fabric.properties .idea/other.xml .idea/studiobot.xml -.kotlin \ No newline at end of file +.kotlin +keystore.properties diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index ae9abe1cc..f57aa1718 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -1,5 +1,6 @@ import java.time.LocalDate import java.time.format.DateTimeFormatter +import java.util.Properties import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { @@ -33,8 +34,8 @@ android { applicationId = "de.mm20.launcher2" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2026053100 - versionName = "1.40.2" + versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2026091000 + versionName = "1.40.2-typing.1" signingConfig = signingConfigs.getByName("debug") } @@ -45,11 +46,31 @@ android { keyAlias = System.getenv("SIGNING_KEY_ALIAS") keyPassword = System.getenv("SIGNING_KEY_PASSWORD") } + + // Signing config for fork builds. Credentials are read from keystore.properties in the + // project root, or from the KEYSTORE_FILE, KEYSTORE_PASSWORD, KEY_ALIAS and KEY_PASSWORD + // environment variables. + create("local") { + val props = Properties() + val propsFile = rootProject.file("keystore.properties") + if (propsFile.exists()) { + propsFile.inputStream().use { props.load(it) } + } + storeFile = file( + props.getProperty("storeFile") + ?: System.getenv("KEYSTORE_FILE") + ?: "keystore.jks" + ) + storePassword = props.getProperty("storePassword") ?: System.getenv("KEYSTORE_PASSWORD") + keyAlias = props.getProperty("keyAlias") ?: System.getenv("KEY_ALIAS") + keyPassword = props.getProperty("keyPassword") ?: System.getenv("KEY_PASSWORD") + } } buildTypes { release { applicationIdSuffix = ".release" + signingConfig = signingConfigs.getByName("local") isMinifyEnabled = true isShrinkResources = true proguardFiles( diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt index 95f4dcbf7..5480adcf4 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt @@ -25,6 +25,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -39,8 +40,10 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import de.mm20.launcher2.preferences.SearchBarStyle import de.mm20.launcher2.ui.R @@ -133,6 +136,17 @@ fun SearchBar( else 1f } + // The String flavour of BasicTextField keeps the old selection when the text is changed from + // the outside, which puts the cursor in front of the existing text. Examples are searches that + // were started by typing on a physical keyboard. A TextFieldValue lets us move the cursor to the + // end of the text instead. + val textFieldValue = remember { + mutableStateOf(TextFieldValue(value, TextRange(value.length))) + } + if (textFieldValue.value.text != value) { + textFieldValue.value = TextFieldValue(value, TextRange(value.length)) + } + LauncherCard( modifier = modifier .alpha(opacity), @@ -168,7 +182,7 @@ fun SearchBar( BasicTextField( modifier = Modifier .onFocusChanged { - if (it.hasFocus) onFocus() + if (it.hasFocus) onFocus() else onUnfocus() } .focusRequester(focusRequester) .fillMaxWidth() @@ -179,8 +193,11 @@ fun SearchBar( color = contentColor ), singleLine = true, - value = value, - onValueChange = onValueChange, + value = textFieldValue.value, + onValueChange = { + textFieldValue.value = it + onValueChange(it.text) + }, cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), keyboardOptions = KeyboardOptions( imeAction = ImeAction.Go, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt index 0a9f6581d..30e76daf5 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt @@ -6,6 +6,7 @@ import android.content.pm.ActivityInfo import android.content.res.Configuration import android.content.res.Resources import android.os.Bundle +import android.view.KeyEvent import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels @@ -33,6 +34,7 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle @@ -466,6 +468,34 @@ abstract class SharedLauncherActivity( var pauseTime = 0L + /** + * Key event handler that is installed by the launcher scaffold while it is shown. It allows + * the launcher to react to key events that are not dispatched to any view, e.g. to open the + * search when the user starts typing on a physical keyboard. + * + * The handler runs on the main thread and returns true if it has consumed the event. + */ + internal var launcherKeyEventHandler: ((KeyEvent) -> Boolean)? = null + + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + val handler = launcherKeyEventHandler + if (handler != null) { + // dispatchKeyEvent must not throw, otherwise unhandled input would crash the launcher. + val handled = runCatching { handler(event) }.getOrDefault(false) + if (handled) return true + } + return super.dispatchKeyEvent(event) + } + + /** + * Hide the on-screen keyboard. Used when the search was opened by typing on a physical + * keyboard, where the soft keyboard would only cover the search results. + */ + internal fun hideSoftKeyboard() { + WindowCompat.getInsetsController(window, window.decorView) + .hide(WindowInsetsCompat.Type.ime()) + } + /** * True if the scaffold was on home screen when the activity was paused. */ diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt index 3cd38da1b..4d329457e 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt @@ -50,6 +50,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf @@ -109,6 +110,7 @@ import de.mm20.launcher2.ui.launcher.scaffold.components.SearchComponent import de.mm20.launcher2.ui.launcher.search.SearchVM import de.mm20.launcher2.ui.launcher.search.filters.KeyboardFilterBar import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar +import de.mm20.launcher2.ui.launcher.searchbar.rememberTypeToSearchHandler import de.mm20.launcher2.ui.theme.transparency.transparency import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeSource @@ -325,6 +327,20 @@ internal class LauncherScaffoldState( var isSearchBarFocused by mutableStateOf(config.homeComponent is SearchComponent) + /** + * True while the search bar's text field actually has input focus in the Compose hierarchy. + * Unlike [isSearchBarFocused], which only says that the search bar *should* be focused, this is + * reported by the text field itself and is used to decide whether the text field is able to + * handle key events from a physical keyboard. + */ + var isSearchBarTextFieldFocused by mutableStateOf(false) + + /** + * True while the search is driven by the physical keyboard. The search bar's text field is not + * focused in that case, so that the on-screen keyboard stays hidden. + */ + var isTypingSearch by mutableStateOf(false) + val statusBarScrim by derivedStateOf { !isAtTop } @@ -1053,6 +1069,19 @@ internal fun LauncherScaffold( ) } + // Open the search when the user starts typing on a physical keyboard. The handler is + // installed on the activity, so it also receives key events while no text field is focused. + val typeToSearchHandler = rememberTypeToSearchHandler(state, searchVM) + DisposableEffect(typeToSearchHandler) { + val launcherActivity = activity as? SharedLauncherActivity + launcherActivity?.launcherKeyEventHandler = typeToSearchHandler + onDispose { + if (launcherActivity?.launcherKeyEventHandler === typeToSearchHandler) { + launcherActivity.launcherKeyEventHandler = null + } + } + } + LaunchedEffect(state.isAtTop, state.isAtBottom) { if (state.currentProgress > 0f && state.currentProgress < 1f) { return@LaunchedEffect @@ -1372,10 +1401,14 @@ internal fun LauncherScaffold( level = { state.searchBarLevel }, bottomSearchBar = config.searchBarPosition == SearchBarPosition.Bottom, onFocusChange = { + state.isSearchBarTextFieldFocused = it if (it) { + // The user took over with the touch screen, so the text field behaves + // as usual from now on. + state.isTypingSearch = false scope.launch { state.onSearchBarTap() } + state.isSearchBarFocused = true } - state.isSearchBarFocused = it }, onKeyboardActionGo = if (launchOnEnter) { { searchVM.launchBestMatchOrAction(activity) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/SearchComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/SearchComponent.kt index 175020259..a94549243 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/SearchComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/components/SearchComponent.kt @@ -105,7 +105,9 @@ internal class SearchComponent( override fun onPreActivate(state: LauncherScaffoldState) { super.onPreActivate(state) - if (openKeyboard) { + // When the search was started by typing on a physical keyboard, the text field stays + // unfocused so that the on-screen keyboard stays hidden. + if (openKeyboard && !state.isTypingSearch) { state.isSearchBarFocused = true } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index 709d64daa..833d65a33 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -74,6 +74,12 @@ class SearchVM : ViewModel(), KoinComponent { val launchOnEnter = searchUiSettings.launchOnEnter .stateIn(viewModelScope, SharingStarted.Eagerly, false) + /** + * Whether typing on a physical keyboard while the home screen is shown opens the search. + */ + val searchOnTyping = searchUiSettings.searchOnTyping + .stateIn(viewModelScope, SharingStarted.Eagerly, true) + private val searchService: SearchService by inject() val searchQuery = mutableStateOf("") diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/TypeToSearchHandler.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/TypeToSearchHandler.kt new file mode 100644 index 000000000..7706f373f --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/TypeToSearchHandler.kt @@ -0,0 +1,130 @@ +package de.mm20.launcher2.ui.launcher.searchbar + +import android.view.KeyEvent +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import de.mm20.launcher2.ui.launcher.SharedLauncherActivity +import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffoldState +import de.mm20.launcher2.ui.launcher.scaffold.components.SearchComponent +import de.mm20.launcher2.ui.launcher.search.SearchVM +import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager +import de.mm20.launcher2.ui.overlays.LocalOverlayManager +import kotlinx.coroutines.launch + +/** + * The character that this key event types, or `null` if it is not a printable key press. + * + * Modifier keys, navigation keys, the backspace and delete keys as well as auto-repeat events + * return `null`. + */ +internal fun KeyEvent.typedCharacter(): Char? { + if (action != KeyEvent.ACTION_DOWN) return null + // Auto-repeat events are handled separately, see [isBackspace]. + if (repeatCount > 0) return null + if (isBackspace()) return null + val unicode = unicodeChar + if (unicode == 0) return null + // Control characters (enter, tab, escape, ctrl shortcuts, ...) are not typed into the search. + if (Character.isISOControl(unicode)) return null + return unicode.toChar() +} + +/** + * True for the backspace and delete keys. Auto-repeat is allowed, so that holding the key keeps + * deleting characters. + */ +internal fun KeyEvent.isBackspace(): Boolean { + if (action != KeyEvent.ACTION_DOWN) return false + return keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL +} + +/** + * True for the enter key. Used to launch the highlighted search result, like the "go" key on a + * software keyboard does. + */ +internal fun KeyEvent.isEnter(): Boolean { + if (action != KeyEvent.ACTION_DOWN) return false + if (repeatCount > 0) return false + return keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER +} + +/** + * Creates the key event handler that opens the search and starts typing when the user presses a key + * on a physical keyboard while the launcher home screen is shown. + * + * The handler is installed on the launcher activity, see + * [SharedLauncherActivity.launcherKeyEventHandler]. It is called for every key event before the + * event is dispatched to the view hierarchy, so it also works while no text field has focus. + * + * While the search is driven by the physical keyboard, the search bar's text field is deliberately + * left unfocused: a focused text field would make the system show the on-screen keyboard, and the + * user is typing on a physical keyboard anyway. The handler therefore inserts and deletes the + * characters itself. As soon as the user taps the search bar, the text field takes over and behaves + * like it always does. + * + * @return a handler that returns `true` if it has consumed the key event. + */ +@Composable +internal fun rememberTypeToSearchHandler( + state: LauncherScaffoldState, + searchVM: SearchVM, +): (KeyEvent) -> Boolean { + val scope = rememberCoroutineScope() + val activity = LocalActivity.current as? SharedLauncherActivity + val sheetManager = LocalBottomSheetManager.current + val overlayManager = LocalOverlayManager.current + + return remember(state, searchVM, activity, sheetManager, overlayManager) { + fun handle(event: KeyEvent): Boolean { + // Read the preference here rather than capturing it in the composition, so that the + // current value is always used. + if (!searchVM.searchOnTyping.value || state.isLocked) return false + + // The text field has the input focus and handles key events itself. This gives the user + // the usual editing behaviour, e.g. a text cursor that can be moved. + if (state.isSearchBarTextFieldFocused) return false + + // Only start a search from the home screen or while the search page is already open + // (the user may have scrolled the results away from the search bar). + val searchOpen = state.currentComponent is SearchComponent + val onHomeScreen = !state.isSettledOnSecondaryPage && state.currentProgress == 0f + if (!searchOpen && !onHomeScreen) return false + + // Never steal keys from a text field in a bottom sheet or an overlay. + if (sheetManager.isAnySheetShown()) return false + if (overlayManager.overlays.isNotEmpty()) return false + + val text = searchVM.searchQuery.value + val char = event.typedCharacter() + val newText = when { + char != null -> if (searchOpen) text + char else char.toString() + // Backspace and delete are only handled for a search that the keyboard started. + event.isBackspace() && state.isTypingSearch -> text.dropLast(1) + event.isEnter() && state.isTypingSearch -> { + if (searchVM.launchOnEnter.value) { + searchVM.launchBestMatchOrAction( + activity ?: return false + ) + } + return true + } + + else -> return false + } + + if (newText == text && char == null) return false + + if (!state.isTypingSearch) { + state.isTypingSearch = true + // In case the on-screen keyboard is still visible from an earlier session. + activity?.hideSoftKeyboard() + } + if (!searchOpen) scope.launch { state.onSearchBarTap() } + searchVM.search(newText) + return true + } + ::handle + } +} diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheetManager.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheetManager.kt index 03eaa14f7..c2e6eead1 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheetManager.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheetManager.kt @@ -83,6 +83,16 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) : failedGestureSheetShown.value = null } + /** + * True if any bottom sheet is currently shown. + */ + fun isAnySheetShown(): Boolean = + customizeSearchableSheetShown.value != null + || editFavoritesSheetShown.value + || hiddenItemsSheetShown.value + || editTagSheetShown.value != null + || failedGestureSheetShown.value != null + companion object { private const val PROVIDER = "bottom_sheet_manager" private const val FAVORITES = "favorites" diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt index 6da660205..f955f8156 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt @@ -89,6 +89,7 @@ fun SearchSettingsScreen() { val autoFocus by viewModel.autoFocus.collectAsStateWithLifecycle(null) val launchOnEnter by viewModel.launchOnEnter.collectAsStateWithLifecycle(null) + val searchOnTyping by viewModel.searchOnTyping.collectAsStateWithLifecycle(null) val reverseSearchResults by viewModel.reverseSearchResults.collectAsStateWithLifecycle(null) val filterBar by viewModel.filterBar.collectAsStateWithLifecycle(null) @@ -384,6 +385,15 @@ fun SearchSettingsScreen() { viewModel.setLaunchOnEnter(it) } ) + SwitchPreference( + title = stringResource(R.string.preference_search_bar_search_on_typing), + iconPadding = true, + summary = stringResource(R.string.preference_search_bar_search_on_typing_summary), + value = searchOnTyping == true, + onValueChanged = { + viewModel.setSearchOnTyping(it) + } + ) } } item { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt index 6bebc3e08..08f001cec 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt @@ -135,6 +135,13 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent { searchUiSettings.setLaunchOnEnter(launchOnEnter) } + val searchOnTyping = searchUiSettings.searchOnTyping + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + + fun setSearchOnTyping(searchOnTyping: Boolean) { + searchUiSettings.setSearchOnTyping(searchOnTyping) + } + val hasAppShortcutPermission = permissionsManager.hasPermission(PermissionGroup.AppShortcuts) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) val appShortcuts = shortcutSearchSettings.enabled diff --git a/core/i18n/src/main/res/values-da/strings.xml b/core/i18n/src/main/res/values-da/strings.xml index eda41a3f6..3e72eccf1 100644 --- a/core/i18n/src/main/res/values-da/strings.xml +++ b/core/i18n/src/main/res/values-da/strings.xml @@ -425,6 +425,8 @@ Du har ikke tilknyttet en Nextcloud-konto endnu Flere oplysninger om denne udgave af denne app Start fremhævet match eller hurtig handling, når du trykker gå + Søg ved indtastning + Åbn søgningen og begynd at skrive, når du trykker på et fysisk tastatur Send besked Arbejde diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 0c62a7884..9c55ae9f6 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -675,6 +675,8 @@ Automatically show the keyboard when opening the app drawer Launch on enter Launch highlighted match or quick action upon tapping go + Search on typing + Open the search and start typing when you press a key on a physical keyboard Excluded search results Manage excluded apps and search results Show reveal button diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt index fa6398e35..ca8267d7d 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt @@ -116,6 +116,11 @@ data class LauncherSettingsData internal constructor( val searchBarColors: SearchBarColors = SearchBarColors.Auto, val searchBarKeyboard: Boolean = true, val searchLaunchOnEnter: Boolean = true, + /** + * Open the search and start typing when a printable key is pressed on a physical keyboard + * while the home screen is shown. + */ + val searchOnTyping: Boolean = true, val searchBarBottom: Boolean = false, val searchBarFixed: Boolean = false, diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/SearchUiSettings.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/SearchUiSettings.kt index 3b3e4828a..6d0689e9d 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/SearchUiSettings.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/SearchUiSettings.kt @@ -52,6 +52,18 @@ class SearchUiSettings internal constructor( } } + /** + * Whether typing on a physical keyboard while the home screen is shown opens the search. + */ + val searchOnTyping + get() = launcherDataStore.data.map { it.searchOnTyping }.distinctUntilChanged() + + fun setSearchOnTyping(searchOnTyping: Boolean) { + launcherDataStore.update { + it.copy(searchOnTyping = searchOnTyping) + } + } + val reversedResults get() = launcherDataStore.data.map { it.searchResultsReversed }.distinctUntilChanged()