Loading packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +2 −2 Original line number Diff line number Diff line Loading @@ -28,7 +28,7 @@ import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider import com.android.settingslib.spa.gallery.dialog.NavDialogProvider import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuBoxPageProvider import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuCheckBoxProvider import com.android.settingslib.spa.gallery.editor.SettingsDropdownCheckBoxProvider import com.android.settingslib.spa.gallery.home.HomePageProvider import com.android.settingslib.spa.gallery.itemList.ItemListPageProvider import com.android.settingslib.spa.gallery.itemList.ItemOperatePageProvider Loading Loading @@ -100,7 +100,7 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { EditorMainPageProvider, SettingsOutlinedTextFieldPageProvider, SettingsExposedDropdownMenuBoxPageProvider, SettingsExposedDropdownMenuCheckBoxProvider, SettingsDropdownCheckBoxProvider, SettingsTextFieldPasswordPageProvider, SearchScaffoldPageProvider, SuwScaffoldPageProvider, Loading packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt +1 −1 Original line number Diff line number Diff line Loading @@ -37,7 +37,7 @@ object EditorMainPageProvider : SettingsPageProvider { .build(), SettingsExposedDropdownMenuBoxPageProvider.buildInjectEntry().setLink(fromPage = owner) .build(), SettingsExposedDropdownMenuCheckBoxProvider.buildInjectEntry().setLink(fromPage = owner) SettingsDropdownCheckBoxProvider.buildInjectEntry().setLink(fromPage = owner) .build(), SettingsTextFieldPasswordPageProvider.buildInjectEntry().setLink(fromPage = owner) .build(), Loading packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuCheckBoxProvider.kt→packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownCheckBoxProvider.kt +134 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. Loading @@ -18,7 +18,7 @@ package com.android.settingslib.spa.gallery.editor import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.common.SettingsEntryBuilder Loading @@ -26,18 +26,16 @@ import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuCheckBox import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckBox import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckOption import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.scaffold.RegularScaffold private const val TITLE = "Sample SettingsExposedDropdownMenuCheckBox" private const val TITLE = "Sample SettingsDropdownCheckBox" object SettingsExposedDropdownMenuCheckBoxProvider : SettingsPageProvider { override val name = "SettingsExposedDropdownMenuCheckBox" private const val exposedDropdownMenuCheckBoxLabel = "ExposedDropdownMenuCheckBoxLabel" private val options = listOf("item1", "item2", "item3") private val selectedOptionsState1 = mutableStateListOf(0, 1) object SettingsDropdownCheckBoxProvider : SettingsPageProvider { override val name = "SettingsDropdownCheckBox" override fun getTitle(arguments: Bundle?): String { return TITLE Loading @@ -46,12 +44,73 @@ object SettingsExposedDropdownMenuCheckBoxProvider : SettingsPageProvider { @Composable override fun Page(arguments: Bundle?) { RegularScaffold(title = TITLE) { SettingsExposedDropdownMenuCheckBox( label = exposedDropdownMenuCheckBoxLabel, options = options, selectedOptionsState = remember { selectedOptionsState1 }, enabled = true, onSelectedOptionStateChange = {}, SettingsDropdownCheckBox( label = "SettingsDropdownCheckBox", options = remember { listOf( SettingsDropdownCheckOption("Item 1"), SettingsDropdownCheckOption("Item 2"), SettingsDropdownCheckOption("Item 3"), ) }, ) SettingsDropdownCheckBox( label = "Empty list", options = emptyList(), ) SettingsDropdownCheckBox( label = "Disabled", options = remember { listOf( SettingsDropdownCheckOption("Item 1", selected = mutableStateOf(true)), SettingsDropdownCheckOption("Item 2"), SettingsDropdownCheckOption("Item 3"), ) }, enabled = false, ) SettingsDropdownCheckBox( label = "With disabled item", options = remember { listOf( SettingsDropdownCheckOption("Item 1"), SettingsDropdownCheckOption("Item 2"), SettingsDropdownCheckOption( text = "Disabled item 1", changeable = false, selected = mutableStateOf(true), ), SettingsDropdownCheckOption("Disabled item 2", changeable = false), ) }, ) SettingsDropdownCheckBox( label = "With select all", options = remember { listOf( SettingsDropdownCheckOption("All", isSelectAll = true), SettingsDropdownCheckOption("Item 1"), SettingsDropdownCheckOption("Item 2"), SettingsDropdownCheckOption("Item 3"), ) }, ) SettingsDropdownCheckBox( label = "With disabled item and select all", options = remember { listOf( SettingsDropdownCheckOption("All", isSelectAll = true, changeable = false), SettingsDropdownCheckOption("Item 1"), SettingsDropdownCheckOption("Item 2"), SettingsDropdownCheckOption( text = "Disabled item 1", changeable = false, selected = mutableStateOf(true), ), SettingsDropdownCheckOption("Disabled item 2", changeable = false), ) }, ) } } Loading @@ -68,8 +127,8 @@ object SettingsExposedDropdownMenuCheckBoxProvider : SettingsPageProvider { @Preview(showBackground = true) @Composable private fun SettingsExposedDropdownMenuCheckBoxPagePreview() { private fun SettingsDropdownCheckBoxPagePreview() { SettingsTheme { SettingsExposedDropdownMenuCheckBoxProvider.Page(null) SettingsDropdownCheckBoxProvider.Page(null) } } packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt→packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt +184 −0 Original line number Diff line number Diff line Loading @@ -29,13 +29,12 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged Loading @@ -43,25 +42,48 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckOption.Companion.changeable data class SettingsDropdownCheckOption( /** The displayed text of this option. */ val text: String, /** If true, check / uncheck this item will check / uncheck all enabled options. */ val isSelectAll: Boolean = false, /** If not changeable, cannot check or uncheck this option. */ val changeable: Boolean = true, /** The selected state of this option. */ val selected: MutableState<Boolean> = mutableStateOf(false), /** Get called when the option is clicked, no matter if it's changeable. */ val onClick: () -> Unit = {}, ) { companion object { val List<SettingsDropdownCheckOption>.changeable: Boolean get() = filter { !it.isSelectAll }.any { it.changeable } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsExposedDropdownMenuCheckBox( fun SettingsDropdownCheckBox( label: String, options: List<String>, selectedOptionsState: SnapshotStateList<Int>, emptyVal: String = "", enabled: Boolean, options: List<SettingsDropdownCheckOption>, emptyText: String = "", enabled: Boolean = true, errorMessage: String? = null, onSelectedOptionStateChange: () -> Unit, onSelectedStateChange: () -> Unit = {}, ) { var dropDownWidth by remember { mutableIntStateOf(0) } var expanded by remember { mutableStateOf(false) } val allIndex = options.indexOf("*") val changeable = enabled && options.changeable ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { expanded = it }, onExpandedChange = { expanded = changeable && it }, modifier = Modifier .width(350.dp) .padding(SettingsDimension.textFieldPadding) Loading @@ -72,78 +94,76 @@ fun SettingsExposedDropdownMenuCheckBox( modifier = Modifier .menuAnchor() .fillMaxWidth(), value = if (selectedOptionsState.size == 0) emptyVal else if (selectedOptionsState.contains(allIndex)) "*" else selectedOptionsState.joinToString { options[it] }, value = getDisplayText(options) ?: emptyText, onValueChange = {}, label = { Text(text = label) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon( expanded = expanded ) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, readOnly = true, enabled = enabled, enabled = changeable, isError = errorMessage != null, supportingText = { if (errorMessage != null) { Text(text = errorMessage) } } supportingText = errorMessage?.let { { Text(text = it) } }, ) if (options.isNotEmpty()) { ExposedDropdownMenu( expanded = expanded, modifier = Modifier.width(with(LocalDensity.current) { dropDownWidth.toDp() }), onDismissRequest = { expanded = false }, ) { options.forEachIndexed { index, option -> CheckboxItem( selectedOptionsState, index, allIndex, onSelectedOptionStateChange, option, ) for (option in options) { CheckboxItem(option) { option.onClick() if (option.changeable) { checkboxItemOnClick(options, option) onSelectedStateChange() } } } } } } private fun getDisplayText(options: List<SettingsDropdownCheckOption>): String? { val selectedOptions = options.filter { it.selected.value } if (selectedOptions.isEmpty()) return null return selectedOptions.filter { it.isSelectAll }.ifEmpty { selectedOptions } .joinToString { it.text } } private fun checkboxItemOnClick( options: List<SettingsDropdownCheckOption>, clickedOption: SettingsDropdownCheckOption, ) { if (!clickedOption.changeable) return val newChecked = !clickedOption.selected.value if (clickedOption.isSelectAll) { for (option in options.filter { it.changeable }) option.selected.value = newChecked } else { clickedOption.selected.value = newChecked } val (selectAllOptions, regularOptions) = options.partition { it.isSelectAll } val isAllRegularOptionsChecked = regularOptions.all { it.selected.value } selectAllOptions.forEach { it.selected.value = isAllRegularOptionsChecked } } @Composable private fun CheckboxItem( selectedOptionsState: SnapshotStateList<Int>, index: Int, allIndex: Int, onSelectedOptionStateChange: () -> Unit, option: String option: SettingsDropdownCheckOption, onClick: (SettingsDropdownCheckOption) -> Unit, ) { TextButton( onClick = { onClick(option) }, modifier = Modifier.fillMaxWidth(), onClick = { if (selectedOptionsState.contains(index)) { if (index == allIndex) { selectedOptionsState.clear() } else { selectedOptionsState.remove(index) selectedOptionsState.remove(allIndex) } } else { selectedOptionsState.add(index) } onSelectedOptionStateChange() }) { ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround), verticalAlignment = Alignment.CenterVertically ) { Checkbox( checked = selectedOptionsState.contains(index), checked = option.selected.value, onCheckedChange = null, enabled = option.changeable, ) Text(text = option) Text(text = option.text, modifier = Modifier.alphaForEnabled(option.changeable)) } } } Loading @@ -151,14 +171,14 @@ private fun CheckboxItem( @Preview @Composable private fun ActionButtonsPreview() { val options = listOf("item1", "item2", "item3") val selectedOptionsState = remember { mutableStateListOf(0, 1) } val item1 = SettingsDropdownCheckOption("item1") val item2 = SettingsDropdownCheckOption("item2") val item3 = SettingsDropdownCheckOption("item3") val options = listOf(item1, item2, item3) SettingsTheme { SettingsExposedDropdownMenuCheckBox( SettingsDropdownCheckBox( label = "label", options = options, selectedOptionsState = selectedOptionsState, enabled = true, onSelectedOptionStateChange = {}) ) } } packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBoxTest.kt 0 → 100644 +145 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settingslib.spa.widget.editor import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.semantics.Role import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasText import androidx.compose.ui.test.isPopup import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.testutils.hasRole import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SettingsDropdownCheckBoxTest { @get:Rule val composeTestRule = createComposeRule() @Test fun dropdownCheckBox_displayed() { val item1 = SettingsDropdownCheckOption("item1") val item2 = SettingsDropdownCheckOption("item2") val item3 = SettingsDropdownCheckOption("item3") composeTestRule.setContent { SettingsDropdownCheckBox( label = LABEL, options = listOf(item1, item2, item3), ) } composeTestRule.onNodeWithText(LABEL).assertIsDisplayed() } @Test fun dropdownCheckBox_expanded() { val item1 = SettingsDropdownCheckOption("item1") val item2 = SettingsDropdownCheckOption("item2") val item3 = SettingsDropdownCheckOption("item3") composeTestRule.setContent { SettingsDropdownCheckBox( label = LABEL, options = listOf(item1, item2, item3), ) } composeTestRule.onOption(item3).assertDoesNotExist() composeTestRule.onNodeWithText(LABEL).performClick() composeTestRule.onOption(item3).assertIsDisplayed() } @Test fun dropdownCheckBox_valueAdded() { val item1 = SettingsDropdownCheckOption("item1") val item2 = SettingsDropdownCheckOption("item2") val item3 = SettingsDropdownCheckOption("item3") composeTestRule.setContent { SettingsDropdownCheckBox( label = LABEL, options = listOf(item1, item2, item3), ) } composeTestRule.onDropdownBox(item3.text).assertDoesNotExist() composeTestRule.onNodeWithText(LABEL).performClick() composeTestRule.onOption(item3).performClick() composeTestRule.onDropdownBox(item3.text).assertIsDisplayed() assertThat(item3.selected.value).isTrue() } @Test fun dropdownCheckBox_valueDeleted() { val item1 = SettingsDropdownCheckOption("item1") val item2 = SettingsDropdownCheckOption("item2", selected = mutableStateOf(true)) val item3 = SettingsDropdownCheckOption("item3") composeTestRule.setContent { SettingsDropdownCheckBox( label = LABEL, options = listOf(item1, item2, item3), ) } composeTestRule.onDropdownBox(item2.text).assertIsDisplayed() composeTestRule.onNodeWithText(LABEL).performClick() composeTestRule.onOption(item2).performClick() composeTestRule.onDropdownBox(item2.text).assertDoesNotExist() assertThat(item2.selected.value).isFalse() } @Test fun dropdownCheckBox_withSelectAll() { val selectAll = SettingsDropdownCheckOption("All", isSelectAll = true) val item1 = SettingsDropdownCheckOption("item1") val item2 = SettingsDropdownCheckOption("item2") composeTestRule.setContent { SettingsDropdownCheckBox( label = LABEL, options = listOf(selectAll, item1, item2), ) } composeTestRule.onNodeWithText(LABEL).performClick() composeTestRule.onOption(selectAll).performClick() composeTestRule.onDropdownBox(selectAll.text).assertIsDisplayed() composeTestRule.onDropdownBox(item1.text).assertDoesNotExist() composeTestRule.onDropdownBox(item2.text).assertDoesNotExist() assertThat(item1.selected.value).isTrue() assertThat(item2.selected.value).isTrue() } private companion object { const val LABEL = "Label" } } private fun ComposeContentTestRule.onDropdownBox(text: String) = onNode(hasRole(Role.DropdownList) and hasText(text)) private fun ComposeContentTestRule.onOption(option: SettingsDropdownCheckOption) = onNode(hasAnyAncestor(isPopup()) and hasText(option.text)) Loading
packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +2 −2 Original line number Diff line number Diff line Loading @@ -28,7 +28,7 @@ import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider import com.android.settingslib.spa.gallery.dialog.NavDialogProvider import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuBoxPageProvider import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuCheckBoxProvider import com.android.settingslib.spa.gallery.editor.SettingsDropdownCheckBoxProvider import com.android.settingslib.spa.gallery.home.HomePageProvider import com.android.settingslib.spa.gallery.itemList.ItemListPageProvider import com.android.settingslib.spa.gallery.itemList.ItemOperatePageProvider Loading Loading @@ -100,7 +100,7 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { EditorMainPageProvider, SettingsOutlinedTextFieldPageProvider, SettingsExposedDropdownMenuBoxPageProvider, SettingsExposedDropdownMenuCheckBoxProvider, SettingsDropdownCheckBoxProvider, SettingsTextFieldPasswordPageProvider, SearchScaffoldPageProvider, SuwScaffoldPageProvider, Loading
packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt +1 −1 Original line number Diff line number Diff line Loading @@ -37,7 +37,7 @@ object EditorMainPageProvider : SettingsPageProvider { .build(), SettingsExposedDropdownMenuBoxPageProvider.buildInjectEntry().setLink(fromPage = owner) .build(), SettingsExposedDropdownMenuCheckBoxProvider.buildInjectEntry().setLink(fromPage = owner) SettingsDropdownCheckBoxProvider.buildInjectEntry().setLink(fromPage = owner) .build(), SettingsTextFieldPasswordPageProvider.buildInjectEntry().setLink(fromPage = owner) .build(), Loading
packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuCheckBoxProvider.kt→packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownCheckBoxProvider.kt +134 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. Loading @@ -18,7 +18,7 @@ package com.android.settingslib.spa.gallery.editor import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.common.SettingsEntryBuilder Loading @@ -26,18 +26,16 @@ import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuCheckBox import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckBox import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckOption import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.scaffold.RegularScaffold private const val TITLE = "Sample SettingsExposedDropdownMenuCheckBox" private const val TITLE = "Sample SettingsDropdownCheckBox" object SettingsExposedDropdownMenuCheckBoxProvider : SettingsPageProvider { override val name = "SettingsExposedDropdownMenuCheckBox" private const val exposedDropdownMenuCheckBoxLabel = "ExposedDropdownMenuCheckBoxLabel" private val options = listOf("item1", "item2", "item3") private val selectedOptionsState1 = mutableStateListOf(0, 1) object SettingsDropdownCheckBoxProvider : SettingsPageProvider { override val name = "SettingsDropdownCheckBox" override fun getTitle(arguments: Bundle?): String { return TITLE Loading @@ -46,12 +44,73 @@ object SettingsExposedDropdownMenuCheckBoxProvider : SettingsPageProvider { @Composable override fun Page(arguments: Bundle?) { RegularScaffold(title = TITLE) { SettingsExposedDropdownMenuCheckBox( label = exposedDropdownMenuCheckBoxLabel, options = options, selectedOptionsState = remember { selectedOptionsState1 }, enabled = true, onSelectedOptionStateChange = {}, SettingsDropdownCheckBox( label = "SettingsDropdownCheckBox", options = remember { listOf( SettingsDropdownCheckOption("Item 1"), SettingsDropdownCheckOption("Item 2"), SettingsDropdownCheckOption("Item 3"), ) }, ) SettingsDropdownCheckBox( label = "Empty list", options = emptyList(), ) SettingsDropdownCheckBox( label = "Disabled", options = remember { listOf( SettingsDropdownCheckOption("Item 1", selected = mutableStateOf(true)), SettingsDropdownCheckOption("Item 2"), SettingsDropdownCheckOption("Item 3"), ) }, enabled = false, ) SettingsDropdownCheckBox( label = "With disabled item", options = remember { listOf( SettingsDropdownCheckOption("Item 1"), SettingsDropdownCheckOption("Item 2"), SettingsDropdownCheckOption( text = "Disabled item 1", changeable = false, selected = mutableStateOf(true), ), SettingsDropdownCheckOption("Disabled item 2", changeable = false), ) }, ) SettingsDropdownCheckBox( label = "With select all", options = remember { listOf( SettingsDropdownCheckOption("All", isSelectAll = true), SettingsDropdownCheckOption("Item 1"), SettingsDropdownCheckOption("Item 2"), SettingsDropdownCheckOption("Item 3"), ) }, ) SettingsDropdownCheckBox( label = "With disabled item and select all", options = remember { listOf( SettingsDropdownCheckOption("All", isSelectAll = true, changeable = false), SettingsDropdownCheckOption("Item 1"), SettingsDropdownCheckOption("Item 2"), SettingsDropdownCheckOption( text = "Disabled item 1", changeable = false, selected = mutableStateOf(true), ), SettingsDropdownCheckOption("Disabled item 2", changeable = false), ) }, ) } } Loading @@ -68,8 +127,8 @@ object SettingsExposedDropdownMenuCheckBoxProvider : SettingsPageProvider { @Preview(showBackground = true) @Composable private fun SettingsExposedDropdownMenuCheckBoxPagePreview() { private fun SettingsDropdownCheckBoxPagePreview() { SettingsTheme { SettingsExposedDropdownMenuCheckBoxProvider.Page(null) SettingsDropdownCheckBoxProvider.Page(null) } }
packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt→packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt +184 −0 Original line number Diff line number Diff line Loading @@ -29,13 +29,12 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged Loading @@ -43,25 +42,48 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckOption.Companion.changeable data class SettingsDropdownCheckOption( /** The displayed text of this option. */ val text: String, /** If true, check / uncheck this item will check / uncheck all enabled options. */ val isSelectAll: Boolean = false, /** If not changeable, cannot check or uncheck this option. */ val changeable: Boolean = true, /** The selected state of this option. */ val selected: MutableState<Boolean> = mutableStateOf(false), /** Get called when the option is clicked, no matter if it's changeable. */ val onClick: () -> Unit = {}, ) { companion object { val List<SettingsDropdownCheckOption>.changeable: Boolean get() = filter { !it.isSelectAll }.any { it.changeable } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsExposedDropdownMenuCheckBox( fun SettingsDropdownCheckBox( label: String, options: List<String>, selectedOptionsState: SnapshotStateList<Int>, emptyVal: String = "", enabled: Boolean, options: List<SettingsDropdownCheckOption>, emptyText: String = "", enabled: Boolean = true, errorMessage: String? = null, onSelectedOptionStateChange: () -> Unit, onSelectedStateChange: () -> Unit = {}, ) { var dropDownWidth by remember { mutableIntStateOf(0) } var expanded by remember { mutableStateOf(false) } val allIndex = options.indexOf("*") val changeable = enabled && options.changeable ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { expanded = it }, onExpandedChange = { expanded = changeable && it }, modifier = Modifier .width(350.dp) .padding(SettingsDimension.textFieldPadding) Loading @@ -72,78 +94,76 @@ fun SettingsExposedDropdownMenuCheckBox( modifier = Modifier .menuAnchor() .fillMaxWidth(), value = if (selectedOptionsState.size == 0) emptyVal else if (selectedOptionsState.contains(allIndex)) "*" else selectedOptionsState.joinToString { options[it] }, value = getDisplayText(options) ?: emptyText, onValueChange = {}, label = { Text(text = label) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon( expanded = expanded ) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, readOnly = true, enabled = enabled, enabled = changeable, isError = errorMessage != null, supportingText = { if (errorMessage != null) { Text(text = errorMessage) } } supportingText = errorMessage?.let { { Text(text = it) } }, ) if (options.isNotEmpty()) { ExposedDropdownMenu( expanded = expanded, modifier = Modifier.width(with(LocalDensity.current) { dropDownWidth.toDp() }), onDismissRequest = { expanded = false }, ) { options.forEachIndexed { index, option -> CheckboxItem( selectedOptionsState, index, allIndex, onSelectedOptionStateChange, option, ) for (option in options) { CheckboxItem(option) { option.onClick() if (option.changeable) { checkboxItemOnClick(options, option) onSelectedStateChange() } } } } } } private fun getDisplayText(options: List<SettingsDropdownCheckOption>): String? { val selectedOptions = options.filter { it.selected.value } if (selectedOptions.isEmpty()) return null return selectedOptions.filter { it.isSelectAll }.ifEmpty { selectedOptions } .joinToString { it.text } } private fun checkboxItemOnClick( options: List<SettingsDropdownCheckOption>, clickedOption: SettingsDropdownCheckOption, ) { if (!clickedOption.changeable) return val newChecked = !clickedOption.selected.value if (clickedOption.isSelectAll) { for (option in options.filter { it.changeable }) option.selected.value = newChecked } else { clickedOption.selected.value = newChecked } val (selectAllOptions, regularOptions) = options.partition { it.isSelectAll } val isAllRegularOptionsChecked = regularOptions.all { it.selected.value } selectAllOptions.forEach { it.selected.value = isAllRegularOptionsChecked } } @Composable private fun CheckboxItem( selectedOptionsState: SnapshotStateList<Int>, index: Int, allIndex: Int, onSelectedOptionStateChange: () -> Unit, option: String option: SettingsDropdownCheckOption, onClick: (SettingsDropdownCheckOption) -> Unit, ) { TextButton( onClick = { onClick(option) }, modifier = Modifier.fillMaxWidth(), onClick = { if (selectedOptionsState.contains(index)) { if (index == allIndex) { selectedOptionsState.clear() } else { selectedOptionsState.remove(index) selectedOptionsState.remove(allIndex) } } else { selectedOptionsState.add(index) } onSelectedOptionStateChange() }) { ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround), verticalAlignment = Alignment.CenterVertically ) { Checkbox( checked = selectedOptionsState.contains(index), checked = option.selected.value, onCheckedChange = null, enabled = option.changeable, ) Text(text = option) Text(text = option.text, modifier = Modifier.alphaForEnabled(option.changeable)) } } } Loading @@ -151,14 +171,14 @@ private fun CheckboxItem( @Preview @Composable private fun ActionButtonsPreview() { val options = listOf("item1", "item2", "item3") val selectedOptionsState = remember { mutableStateListOf(0, 1) } val item1 = SettingsDropdownCheckOption("item1") val item2 = SettingsDropdownCheckOption("item2") val item3 = SettingsDropdownCheckOption("item3") val options = listOf(item1, item2, item3) SettingsTheme { SettingsExposedDropdownMenuCheckBox( SettingsDropdownCheckBox( label = "label", options = options, selectedOptionsState = selectedOptionsState, enabled = true, onSelectedOptionStateChange = {}) ) } }
packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBoxTest.kt 0 → 100644 +145 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settingslib.spa.widget.editor import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.semantics.Role import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasText import androidx.compose.ui.test.isPopup import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.testutils.hasRole import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SettingsDropdownCheckBoxTest { @get:Rule val composeTestRule = createComposeRule() @Test fun dropdownCheckBox_displayed() { val item1 = SettingsDropdownCheckOption("item1") val item2 = SettingsDropdownCheckOption("item2") val item3 = SettingsDropdownCheckOption("item3") composeTestRule.setContent { SettingsDropdownCheckBox( label = LABEL, options = listOf(item1, item2, item3), ) } composeTestRule.onNodeWithText(LABEL).assertIsDisplayed() } @Test fun dropdownCheckBox_expanded() { val item1 = SettingsDropdownCheckOption("item1") val item2 = SettingsDropdownCheckOption("item2") val item3 = SettingsDropdownCheckOption("item3") composeTestRule.setContent { SettingsDropdownCheckBox( label = LABEL, options = listOf(item1, item2, item3), ) } composeTestRule.onOption(item3).assertDoesNotExist() composeTestRule.onNodeWithText(LABEL).performClick() composeTestRule.onOption(item3).assertIsDisplayed() } @Test fun dropdownCheckBox_valueAdded() { val item1 = SettingsDropdownCheckOption("item1") val item2 = SettingsDropdownCheckOption("item2") val item3 = SettingsDropdownCheckOption("item3") composeTestRule.setContent { SettingsDropdownCheckBox( label = LABEL, options = listOf(item1, item2, item3), ) } composeTestRule.onDropdownBox(item3.text).assertDoesNotExist() composeTestRule.onNodeWithText(LABEL).performClick() composeTestRule.onOption(item3).performClick() composeTestRule.onDropdownBox(item3.text).assertIsDisplayed() assertThat(item3.selected.value).isTrue() } @Test fun dropdownCheckBox_valueDeleted() { val item1 = SettingsDropdownCheckOption("item1") val item2 = SettingsDropdownCheckOption("item2", selected = mutableStateOf(true)) val item3 = SettingsDropdownCheckOption("item3") composeTestRule.setContent { SettingsDropdownCheckBox( label = LABEL, options = listOf(item1, item2, item3), ) } composeTestRule.onDropdownBox(item2.text).assertIsDisplayed() composeTestRule.onNodeWithText(LABEL).performClick() composeTestRule.onOption(item2).performClick() composeTestRule.onDropdownBox(item2.text).assertDoesNotExist() assertThat(item2.selected.value).isFalse() } @Test fun dropdownCheckBox_withSelectAll() { val selectAll = SettingsDropdownCheckOption("All", isSelectAll = true) val item1 = SettingsDropdownCheckOption("item1") val item2 = SettingsDropdownCheckOption("item2") composeTestRule.setContent { SettingsDropdownCheckBox( label = LABEL, options = listOf(selectAll, item1, item2), ) } composeTestRule.onNodeWithText(LABEL).performClick() composeTestRule.onOption(selectAll).performClick() composeTestRule.onDropdownBox(selectAll.text).assertIsDisplayed() composeTestRule.onDropdownBox(item1.text).assertDoesNotExist() composeTestRule.onDropdownBox(item2.text).assertDoesNotExist() assertThat(item1.selected.value).isTrue() assertThat(item2.selected.value).isTrue() } private companion object { const val LABEL = "Label" } } private fun ComposeContentTestRule.onDropdownBox(text: String) = onNode(hasRole(Role.DropdownList) and hasText(text)) private fun ComposeContentTestRule.onOption(option: SettingsDropdownCheckOption) = onNode(hasAnyAncestor(isPopup()) and hasText(option.text))