Open the search when typing on a physical keyboard
Some checks failed
Trigger F-Droid repository rebuild / trigger (release) Has been cancelled
Some checks failed
Trigger F-Droid repository rebuild / trigger (release) Has been cancelled
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.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -295,4 +295,5 @@ fabric.properties
|
||||
.idea/other.xml
|
||||
.idea/studiobot.xml
|
||||
|
||||
.kotlin
|
||||
.kotlin
|
||||
keystore.properties
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -425,6 +425,8 @@
|
||||
<string name="no_account_nextcloud">Du har ikke tilknyttet en Nextcloud-konto endnu</string>
|
||||
<string name="preference_screen_buildinfo_summary">Flere oplysninger om denne udgave af denne app</string>
|
||||
<string name="preference_search_bar_launch_on_enter_summary">Start fremhævet match eller hurtig handling, når du trykker gå</string>
|
||||
<string name="preference_search_bar_search_on_typing">Søg ved indtastning</string>
|
||||
<string name="preference_search_bar_search_on_typing_summary">Åbn søgningen og begynd at skrive, når du trykker på et fysisk tastatur</string>
|
||||
<string name="search_action_message">Send besked</string>
|
||||
<string name="apps_profile_work">Arbejde</string>
|
||||
<plurals name="calendar_widget_running_events">
|
||||
|
||||
@@ -675,6 +675,8 @@
|
||||
<string name="preference_search_bar_auto_focus_summary">Automatically show the keyboard when opening the app drawer</string>
|
||||
<string name="preference_search_bar_launch_on_enter">Launch on enter</string>
|
||||
<string name="preference_search_bar_launch_on_enter_summary">Launch highlighted match or quick action upon tapping go</string>
|
||||
<string name="preference_search_bar_search_on_typing">Search on typing</string>
|
||||
<string name="preference_search_bar_search_on_typing_summary">Open the search and start typing when you press a key on a physical keyboard</string>
|
||||
<string name="preference_hidden_items">Excluded search results</string>
|
||||
<string name="preference_hidden_items_summary">Manage excluded apps and search results</string>
|
||||
<string name="preference_hidden_items_reveal_button">Show reveal button</string>
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user