Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 07e7d706 authored by Chaohui Wang's avatar Chaohui Wang
Browse files

Support SettingsDropdownCheckBox non changeable items

If not changeable, cannot check or uncheck this option.

Bug: 326172568
Test: manual - with gallery
Test: unit test
Change-Id: I671e21c5238c71a8bffd7bf3b4aced2f8977ec71
parent ef8fd887
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -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
@@ -100,7 +100,7 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) {
                EditorMainPageProvider,
                SettingsOutlinedTextFieldPageProvider,
                SettingsExposedDropdownMenuBoxPageProvider,
                SettingsExposedDropdownMenuCheckBoxProvider,
                SettingsDropdownCheckBoxProvider,
                SettingsTextFieldPasswordPageProvider,
                SearchScaffoldPageProvider,
                SuwScaffoldPageProvider,
+1 −1
Original line number Diff line number Diff line
@@ -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(),
+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.
@@ -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
@@ -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
@@ -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),
                    )
                },
            )
        }
    }
@@ -68,8 +127,8 @@ object SettingsExposedDropdownMenuCheckBoxProvider : SettingsPageProvider {

@Preview(showBackground = true)
@Composable
private fun SettingsExposedDropdownMenuCheckBoxPagePreview() {
private fun SettingsDropdownCheckBoxPagePreview() {
    SettingsTheme {
        SettingsExposedDropdownMenuCheckBoxProvider.Page(null)
        SettingsDropdownCheckBoxProvider.Page(null)
    }
}
+184 −0
Original line number Diff line number Diff line
@@ -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
@@ -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)
@@ -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))
        }
    }
}
@@ -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 = {})
        )
    }
}
+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