diff --git a/preference/api/jvm/preference.api b/preference/api/jvm/preference.api index 87ff2d4..362a4c8 100644 --- a/preference/api/jvm/preference.api +++ b/preference/api/jvm/preference.api @@ -37,8 +37,9 @@ public final class me/zhanghai/compose/preference/ComposableSingletons$ListPrefe public final class me/zhanghai/compose/preference/ComposableSingletons$MultiSelectListPreferenceKt { public static final field INSTANCE Lme/zhanghai/compose/preference/ComposableSingletons$MultiSelectListPreferenceKt; public fun ()V - public final fun getLambda$-769147719$ComposePreference_preference ()Lkotlin/jvm/functions/Function3; - public final fun getLambda$830545936$ComposePreference_preference ()Lkotlin/jvm/functions/Function3; + public final fun getLambda$-506150531$ComposePreference_preference ()Lkotlin/jvm/functions/Function3; + public final fun getLambda$-872365676$ComposePreference_preference ()Lkotlin/jvm/functions/Function3; + public final fun getLambda$1012851952$ComposePreference_preference ()Lkotlin/jvm/functions/Function3; } public final class me/zhanghai/compose/preference/ComposableSingletons$TextFieldPreferenceKt { @@ -92,15 +93,16 @@ public final class me/zhanghai/compose/preference/MultiSelectListPreferenceDefau public static final field $stable I public static final field INSTANCE Lme/zhanghai/compose/preference/MultiSelectListPreferenceDefaults; public final fun item (Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function5; + public final fun searchFilter ()Lkotlin/jvm/functions/Function3; } public final class me/zhanghai/compose/preference/MultiSelectListPreferenceKt { - public static final fun MultiSelectListPreference (Landroidx/compose/runtime/MutableState;Ljava/util/List;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;II)V - public static final fun MultiSelectListPreference (Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/util/List;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;II)V - public static final fun multiSelectListPreference (Landroidx/compose/foundation/lazy/LazyListScope;Ljava/lang/String;Ljava/util/Set;Ljava/util/List;Lkotlin/jvm/functions/Function3;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;)V - public static final fun multiSelectListPreference (Landroidx/compose/foundation/lazy/LazyListScope;Ljava/lang/String;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/util/List;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;)V - public static synthetic fun multiSelectListPreference$default (Landroidx/compose/foundation/lazy/LazyListScope;Ljava/lang/String;Ljava/util/Set;Ljava/util/List;Lkotlin/jvm/functions/Function3;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;ILjava/lang/Object;)V - public static synthetic fun multiSelectListPreference$default (Landroidx/compose/foundation/lazy/LazyListScope;Ljava/lang/String;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/util/List;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;ILjava/lang/Object;)V + public static final fun MultiSelectListPreference (Landroidx/compose/runtime/MutableState;Ljava/util/List;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;ZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;III)V + public static final fun MultiSelectListPreference (Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/util/List;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;ZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;III)V + public static final fun multiSelectListPreference (Landroidx/compose/foundation/lazy/LazyListScope;Ljava/lang/String;Ljava/util/Set;Ljava/util/List;Lkotlin/jvm/functions/Function3;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;ZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function5;)V + public static final fun multiSelectListPreference (Landroidx/compose/foundation/lazy/LazyListScope;Ljava/lang/String;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/util/List;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;ZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function5;)V + public static synthetic fun multiSelectListPreference$default (Landroidx/compose/foundation/lazy/LazyListScope;Ljava/lang/String;Ljava/util/Set;Ljava/util/List;Lkotlin/jvm/functions/Function3;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;ZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function5;ILjava/lang/Object;)V + public static synthetic fun multiSelectListPreference$default (Landroidx/compose/foundation/lazy/LazyListScope;Ljava/lang/String;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/util/List;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;ZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function5;ILjava/lang/Object;)V } public final class me/zhanghai/compose/preference/MutableMapPreferences : me/zhanghai/compose/preference/MutablePreferences { diff --git a/preference/src/commonMain/kotlin/MultiSelectListPreference.kt b/preference/src/commonMain/kotlin/MultiSelectListPreference.kt index cd1321c..bd8a65f 100644 --- a/preference/src/commonMain/kotlin/MultiSelectListPreference.kt +++ b/preference/src/commonMain/kotlin/MultiSelectListPreference.kt @@ -16,6 +16,8 @@ package me.zhanghai.compose.preference +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -27,11 +29,17 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Checkbox +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -41,6 +49,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import composepreference.preference.generated.resources.Res import composepreference.preference.generated.resources.cancel @@ -60,6 +69,13 @@ public inline fun LazyListScope.multiSelectListPreference( noinline icon: @Composable ((Set) -> Unit)? = null, noinline summary: @Composable ((Set) -> Unit)? = null, noinline valueToText: @Composable (T) -> AnnotatedString = { AnnotatedString(it.toString()) }, + searchable: Boolean = false, + noinline searchFilter: (value: T, text: String, query: String) -> Boolean = + MultiSelectListPreferenceDefaults.searchFilter(), + noinline searchPlaceholder: @Composable (() -> Unit)? = null, + noinline searchLeadingIcon: @Composable (() -> Unit)? = null, + noinline searchEmptyResult: @Composable (() -> Unit)? = null, + pinSelectedValues: Boolean = false, noinline item: @Composable (value: T, currentValues: Set, onToggle: (Boolean) -> Unit) -> Unit = @@ -77,6 +93,12 @@ public inline fun LazyListScope.multiSelectListPreference( icon = icon?.let { { it(value) } }, summary = summary?.let { { it(value) } }, valueToText = valueToText, + searchable = searchable, + searchFilter = searchFilter, + searchPlaceholder = searchPlaceholder, + searchLeadingIcon = searchLeadingIcon, + searchEmptyResult = searchEmptyResult, + pinSelectedValues = pinSelectedValues, item = item, ) } @@ -93,6 +115,13 @@ public fun LazyListScope.multiSelectListPreference( icon: @Composable (() -> Unit)? = null, summary: @Composable (() -> Unit)? = null, valueToText: @Composable (T) -> AnnotatedString = { AnnotatedString(it.toString()) }, + searchable: Boolean = false, + searchFilter: (value: T, text: String, query: String) -> Boolean = + MultiSelectListPreferenceDefaults.searchFilter(), + searchPlaceholder: @Composable (() -> Unit)? = null, + searchLeadingIcon: @Composable (() -> Unit)? = null, + searchEmptyResult: @Composable (() -> Unit)? = null, + pinSelectedValues: Boolean = false, item: @Composable (value: T, currentValues: Set, onToggle: (Boolean) -> Unit) -> Unit = MultiSelectListPreferenceDefaults.item(valueToText), ) { @@ -107,6 +136,12 @@ public fun LazyListScope.multiSelectListPreference( icon = icon, summary = summary, valueToText = valueToText, + searchable = searchable, + searchFilter = searchFilter, + searchPlaceholder = searchPlaceholder, + searchLeadingIcon = searchLeadingIcon, + searchEmptyResult = searchEmptyResult, + pinSelectedValues = pinSelectedValues, item = item, ) } @@ -122,6 +157,13 @@ public fun MultiSelectListPreference( icon: @Composable (() -> Unit)? = null, summary: @Composable (() -> Unit)? = null, valueToText: @Composable (T) -> AnnotatedString = { AnnotatedString(it.toString()) }, + searchable: Boolean = false, + searchFilter: (value: T, text: String, query: String) -> Boolean = + MultiSelectListPreferenceDefaults.searchFilter(), + searchPlaceholder: @Composable (() -> Unit)? = null, + searchLeadingIcon: @Composable (() -> Unit)? = null, + searchEmptyResult: @Composable (() -> Unit)? = null, + pinSelectedValues: Boolean = false, item: @Composable (value: T, currentValues: Set, onToggle: (Boolean) -> Unit) -> Unit = MultiSelectListPreferenceDefaults.item(valueToText), ) { @@ -136,6 +178,12 @@ public fun MultiSelectListPreference( icon = icon, summary = summary, valueToText = valueToText, + searchable = searchable, + searchFilter = searchFilter, + searchPlaceholder = searchPlaceholder, + searchLeadingIcon = searchLeadingIcon, + searchEmptyResult = searchEmptyResult, + pinSelectedValues = pinSelectedValues, item = item, ) } @@ -151,6 +199,13 @@ public fun MultiSelectListPreference( icon: @Composable (() -> Unit)? = null, summary: @Composable (() -> Unit)? = null, valueToText: @Composable (T) -> AnnotatedString = { AnnotatedString(it.toString()) }, + searchable: Boolean = false, + searchFilter: (value: T, text: String, query: String) -> Boolean = + MultiSelectListPreferenceDefaults.searchFilter(), + searchPlaceholder: @Composable (() -> Unit)? = null, + searchLeadingIcon: @Composable (() -> Unit)? = null, + searchEmptyResult: @Composable (() -> Unit)? = null, + pinSelectedValues: Boolean = false, item: @Composable (value: T, currentValues: Set, onToggle: (Boolean) -> Unit) -> Unit = MultiSelectListPreferenceDefaults.item(valueToText), ) { @@ -166,6 +221,25 @@ public fun MultiSelectListPreference( } if (openDialog) { var dialogValue by rememberSaveable { mutableStateOf(value) } + // Snapshot of the selection when the dialog was opened, so that unchecking a pinned value + // doesn't move it around until the dialog is reopened. + val pinnedValues = rememberSaveable { + if (pinSelectedValues) values.filter { it in value } else emptyList() + } + var query by rememberSaveable { mutableStateOf("") } + val isFiltering = searchable && query.isNotEmpty() + val filteredPinnedValues = + if (isFiltering) { + pinnedValues.filter { searchFilter(it, valueToText(it).text, query) } + } else { + pinnedValues + } + val filteredValues = + if (isFiltering) { + values.filter { searchFilter(it, valueToText(it).text, query) } + } else { + values + } PreferenceAlertDialog( onDismissRequest = { openDialog = false }, title = title, @@ -183,23 +257,86 @@ public fun MultiSelectListPreference( } }, ) { + val onToggle: (T, Boolean) -> Unit = { itemValue, checked -> + dialogValue = if (checked) dialogValue + itemValue else dialogValue - itemValue + } val lazyListState = rememberLazyListState() - LazyColumn( - modifier = Modifier.fillMaxWidth().verticalScrollIndicators(lazyListState), - state = lazyListState, - ) { - items(values) { itemValue -> - item(itemValue, dialogValue) { - dialogValue = if (it) dialogValue + itemValue else dialogValue - itemValue + val list: @Composable (Modifier) -> Unit = { modifier -> + LazyColumn( + modifier = modifier.fillMaxWidth().verticalScrollIndicators(lazyListState), + state = lazyListState, + ) { + // Pinned values are duplicates of their items in the full list below. + items(filteredPinnedValues) { itemValue -> + item(itemValue, dialogValue) { onToggle(itemValue, it) } + } + if (filteredPinnedValues.isNotEmpty()) { + item { + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + } + } + items(filteredValues) { itemValue -> + item(itemValue, dialogValue) { onToggle(itemValue, it) } + } + if (filteredValues.isEmpty() && searchEmptyResult != null) { + item { MultiSelectListPreferenceSearchEmptyResult(searchEmptyResult) } } } } + if (searchable) { + Column(modifier = Modifier.fillMaxWidth()) { + MultiSelectListPreferenceSearchField( + query = query, + onQueryChange = { query = it }, + placeholder = searchPlaceholder, + leadingIcon = searchLeadingIcon, + ) + // Fill the available height so that the dialog doesn't resize while filtering. + list(Modifier.weight(1f)) + } + } else { + list(Modifier) + } } } } +@Composable +private fun MultiSelectListPreferenceSearchField( + query: String, + onQueryChange: (String) -> Unit, + placeholder: @Composable (() -> Unit)?, + leadingIcon: @Composable (() -> Unit)?, +) { + OutlinedTextField( + value = query, + onValueChange = onQueryChange, + modifier = Modifier.fillMaxWidth().padding(start = 24.dp, end = 24.dp, bottom = 8.dp), + placeholder = placeholder, + leadingIcon = leadingIcon, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + ) +} + +@Composable +private fun MultiSelectListPreferenceSearchEmptyResult(content: @Composable () -> Unit) { + Box(modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp)) { + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant, + LocalTextStyle provides MaterialTheme.typography.bodyLarge, + content = content, + ) + } +} + @PublishedApi internal object MultiSelectListPreferenceDefaults { + fun searchFilter(): (value: T, text: String, query: String) -> Boolean = + { _, text, query -> + text.contains(query, ignoreCase = true) + } + fun item( valueToText: @Composable (T) -> AnnotatedString ): @Composable (value: T, currentValues: Set, onToggle: (Boolean) -> Unit) -> Unit = diff --git a/sample/src/commonMain/kotlin/SampleApp.kt b/sample/src/commonMain/kotlin/SampleApp.kt index 5d2c8b4..f1f363b 100644 --- a/sample/src/commonMain/kotlin/SampleApp.kt +++ b/sample/src/commonMain/kotlin/SampleApp.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -139,6 +140,34 @@ fun SampleScreen() { title = { Text(text = "Multi-select list preference") }, summary = { Text(text = it.sorted().joinToString(", ")) }, ) + multiSelectListPreference( + key = "searchable_multi_select_list_preference", + defaultValue = setOf("Beta", "Eta"), + values = + listOf( + "Alpha", + "Beta", + "Gamma", + "Delta", + "Epsilon", + "Zeta", + "Eta", + "Theta", + "Iota", + "Kappa", + "Lambda", + "Mu", + ), + title = { Text(text = "Searchable multi-select list preference") }, + summary = { Text(text = it.sorted().joinToString(", ")) }, + searchable = true, + searchPlaceholder = { Text(text = "Search") }, + searchLeadingIcon = { + Icon(imageVector = Icons.Outlined.Search, contentDescription = null) + }, + searchEmptyResult = { Text(text = "No results") }, + pinSelectedValues = true, + ) textFieldPreference( key = "text_field_preference", defaultValue = "Value",