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()