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

Commit 61538502 authored by Joshua Mokut's avatar Joshua Mokut Committed by Android (Google) Code Review
Browse files

Merge "Refactored ShortcutHelper UI file to smaller files." into main

parents a3faa4f7 9837074d
Loading
Loading
Loading
Loading
+2 −1148

File changed.

Preview size limit exceeded, changes collapsed.

+122 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.systemui.keyboard.shortcut.ui.composable

import androidx.compose.foundation.layout.Arrangement
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFirstOrNull
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState

@Composable
fun ShortcutHelperDualPane(
    onSearchQueryChanged: (String) -> Unit,
    selectedCategoryType: ShortcutCategoryType?,
    onCategorySelected: (ShortcutCategoryType?) -> Unit,
    onKeyboardSettingsClicked: () -> Unit,
    onCustomizationModeToggled: (isCustomizing: Boolean) -> Unit,
    uiState: ShortcutsUiState.Active,
    modifier: Modifier = Modifier,
    onShortcutCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
) {
    val selectedCategory =
        uiState.shortcutCategories.fastFirstOrNull { it.type == selectedCategoryType }

    Column(modifier = modifier.fillMaxSize().padding(horizontal = 24.dp)) {
        Row(
            modifier = Modifier.fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween,
        ) {
            // Keep title centered whether customize button is visible or not.
            Spacer(modifier = Modifier.weight(1f))
            Box(modifier = Modifier.width(412.dp), contentAlignment = Alignment.Center) {
                TitleBar(uiState.isCustomizationModeEnabled)
            }
            if (uiState.isShortcutCustomizerFlagEnabled) {
                CustomizationButtonsContainer(
                    modifier = Modifier.weight(1f),
                    isCustomizing = uiState.isCustomizationModeEnabled,
                    onToggleCustomizationMode = {
                        onCustomizationModeToggled(!uiState.isCustomizationModeEnabled)
                    },
                    onReset = {
                        onShortcutCustomizationRequested(ShortcutCustomizationRequestInfo.Reset)
                    },
                    shouldShowResetButton = uiState.shouldShowResetButton,
                )
            } else {
                Spacer(modifier = Modifier.weight(1f))
            }
        }
        Spacer(modifier = Modifier.height(12.dp))
        Row(Modifier.fillMaxWidth()) {
            StartSidePanel(
                onSearchQueryChanged = onSearchQueryChanged,
                modifier = Modifier.width(240.dp).semantics { isTraversalGroup = true },
                categories = uiState.shortcutCategories,
                onKeyboardSettingsClicked = onKeyboardSettingsClicked,
                selectedCategory = selectedCategoryType,
                onCategoryClicked = { onCategorySelected(it.type) },
            )
            Spacer(modifier = Modifier.width(24.dp))
            EndSidePanel(
                uiState,
                onCustomizationModeToggled,
                selectedCategory,
                Modifier.fillMaxSize().padding(top = 8.dp).semantics { isTraversalGroup = true },
                onShortcutCustomizationRequested,
            )
        }
    }
}

@Composable
private fun CustomizationButtonsContainer(
    isCustomizing: Boolean,
    shouldShowResetButton: Boolean,
    onToggleCustomizationMode: () -> Unit,
    onReset: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Row(modifier = modifier, horizontalArrangement = Arrangement.End) {
        if (isCustomizing) {
            if (shouldShowResetButton) {
                ResetButton(onClick = onReset)
                Spacer(Modifier.width(8.dp))
            }
            DoneButton(onClick = onToggleCustomizationMode)
        } else {
            CustomizeButton(onClick = onToggleCustomizationMode)
        }
    }
}
+245 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.systemui.keyboard.shortcut.ui.composable

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import com.android.systemui.keyboard.shortcut.ui.model.IconSource
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCategoryUi
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
import com.android.systemui.res.R

@Composable
fun EndSidePanel(
    uiState: ShortcutsUiState.Active,
    onCustomizationModeToggled: (isCustomizing: Boolean) -> Unit,
    category: ShortcutCategoryUi?,
    modifier: Modifier = Modifier,
    onShortcutCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit = {},
) {
    val listState = rememberLazyListState()

    LaunchedEffect(key1 = category) { if (category != null) listState.animateScrollToItem(0) }
    if (category == null) {
        NoSearchResultsText(horizontalPadding = 24.dp, fillHeight = false)
        return
    }
    LazyColumn(
        modifier = modifier,
        state = listState,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        stickyHeader {
            Column {
                AnimatedVisibility(
                    category.type == AppCategories &&
                        uiState.shouldShowCustomAppsShortcutLimitHeader
                ) {
                    AppCustomShortcutLimitContainer(Modifier.padding(8.dp))
                }
            }
        }
        items(category.subCategories) { subcategory ->
            SubCategoryContainerDualPane(
                uiState.searchQuery,
                subcategory,
                isCustomizing =
                    uiState.isCustomizationModeEnabled && category.type.includeInCustomization,
                onShortcutCustomizationRequested = { requestInfo ->
                    onShortcutCustomizationRequestedInSubCategory(
                        requestInfo,
                        onShortcutCustomizationRequested,
                        category.type,
                    )
                },
                uiState.allowExtendedAppShortcutsCustomization,
            )
            Spacer(modifier = Modifier.height(8.dp))
        }

        if (
            category.type == AppCategories &&
                !uiState.isCustomizationModeEnabled &&
                uiState.isExtendedAppCategoryFlagEnabled &&
                uiState.allowExtendedAppShortcutsCustomization
        ) {
            item {
                ShortcutHelperButton(
                    onClick = { onCustomizationModeToggled(/* isCustomizing= */ true) },
                    contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
                    color = MaterialTheme.colorScheme.secondaryContainer,
                    iconSource = IconSource(imageVector = Icons.Default.Add),
                    modifier = Modifier.heightIn(40.dp),
                    text = stringResource(R.string.shortcut_helper_add_shortcut_button_label),
                )
            }
        }
    }
}

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun AppCustomShortcutLimitContainer(modifier: Modifier = Modifier) {
    Row(
        modifier =
            modifier
                .fillMaxWidth()
                .background(
                    color = MaterialTheme.colorScheme.secondaryContainer,
                    shape = RoundedCornerShape(40.dp),
                )
                .padding(16.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(12.dp),
    ) {
        Surface(
            shape = CircleShape,
            modifier = Modifier.size(40.dp),
            color = MaterialTheme.colorScheme.secondary,
        ) {
            Icon(
                imageVector = Icons.Default.Info,
                tint = MaterialTheme.colorScheme.onSecondary,
                modifier = Modifier.size(24.dp).padding(8.dp),
                contentDescription = null,
            )
        }

        Column(
            horizontalAlignment = Alignment.Start,
            verticalArrangement = Arrangement.spacedBy(2.dp),
        ) {
            Text(
                text = stringResource(R.string.shortcut_helper_app_custom_shortcut_limit_exceeded),
                color = MaterialTheme.colorScheme.onSecondaryContainer,
                style = MaterialTheme.typography.titleMediumEmphasized,
                textAlign = TextAlign.Center,
            )
            Text(
                text =
                    stringResource(
                        R.string.shortcut_helper_app_custom_shortcut_limit_exceeded_instruction
                    ),
                style = MaterialTheme.typography.labelMedium,
                color = MaterialTheme.colorScheme.onSecondaryContainer,
                textAlign = TextAlign.Center,
            )
        }
    }
}

@Composable
private fun SubCategoryContainerDualPane(
    searchQuery: String,
    subCategory: ShortcutSubCategory,
    isCustomizing: Boolean,
    onShortcutCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit,
    allowExtendedAppShortcutsCustomization: Boolean,
) {
    Surface(
        modifier = Modifier.fillMaxWidth(),
        shape = RoundedCornerShape(28.dp),
        color = MaterialTheme.colorScheme.surfaceBright,
    ) {
        Column(Modifier.padding(16.dp)) {
            SubCategoryTitle(subCategory.label, Modifier.padding(8.dp))
            Spacer(Modifier.height(8.dp))
            subCategory.shortcuts.fastForEachIndexed { index, shortcut ->
                if (index > 0) {
                    HorizontalDivider(
                        modifier = Modifier.padding(horizontal = 8.dp),
                        color = MaterialTheme.colorScheme.surfaceContainerHigh,
                    )
                }
                Shortcut(
                    modifier = Modifier.padding(vertical = 8.dp),
                    searchQuery = searchQuery,
                    shortcut = shortcut,
                    isCustomizing = isCustomizing && shortcut.isCustomizable,
                    onShortcutCustomizationRequested = { requestInfo ->
                        when (requestInfo) {
                            is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add ->
                                onShortcutCustomizationRequested(
                                    requestInfo.copy(subCategoryLabel = subCategory.label)
                                )

                            is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete ->
                                onShortcutCustomizationRequested(
                                    requestInfo.copy(subCategoryLabel = subCategory.label)
                                )

                            ShortcutCustomizationRequestInfo.Reset ->
                                onShortcutCustomizationRequested(requestInfo)
                        }
                    },
                    allowExtendedAppShortcutsCustomization = allowExtendedAppShortcutsCustomization,
                )
            }
        }
    }
}

private fun onShortcutCustomizationRequestedInSubCategory(
    requestInfo: ShortcutCustomizationRequestInfo,
    onShortcutCustomizationRequested: (ShortcutCustomizationRequestInfo) -> Unit,
    categoryType: ShortcutCategoryType,
) {
    when (requestInfo) {
        is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add ->
            onShortcutCustomizationRequested(requestInfo.copy(categoryType = categoryType))

        is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete ->
            onShortcutCustomizationRequested(requestInfo.copy(categoryType = categoryType))

        ShortcutCustomizationRequestInfo.Reset -> onShortcutCustomizationRequested(requestInfo)
    }
}
+580 −0

File added.

Preview size limit exceeded, changes collapsed.

+232 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.systemui.keyboard.shortcut.ui.composable

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelperSinglePane.Shapes
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCategoryUi
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
import com.android.systemui.res.R

@Composable
fun ShortcutHelperSinglePane(
    uiState: ShortcutsUiState.Active,
    onSearchQueryChanged: (String) -> Unit,
    selectedCategoryType: ShortcutCategoryType?,
    onCategorySelected: (ShortcutCategoryType?) -> Unit,
    onKeyboardSettingsClicked: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier =
            modifier
                .fillMaxSize()
                .verticalScroll(rememberScrollState())
                .padding(start = 16.dp, end = 16.dp, top = 26.dp)
    ) {
        TitleBar()
        Spacer(modifier = Modifier.height(6.dp))
        ShortcutsSearchBar(onSearchQueryChanged)
        Spacer(modifier = Modifier.height(16.dp))
        if (uiState.shortcutCategories.isEmpty()) {
            Box(modifier = Modifier.weight(1f)) {
                NoSearchResultsText(horizontalPadding = 16.dp, fillHeight = true)
            }
        } else {
            CategoriesPanelSinglePane(uiState, selectedCategoryType, onCategorySelected)
            Spacer(modifier = Modifier.weight(1f))
        }
        KeyboardSettings(onClick = onKeyboardSettingsClicked)
    }
}

@Composable
private fun CategoriesPanelSinglePane(
    uiState: ShortcutsUiState.Active,
    selectedCategoryType: ShortcutCategoryType?,
    onCategorySelected: (ShortcutCategoryType?) -> Unit,
) {
    Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
        uiState.shortcutCategories.fastForEachIndexed { index, category ->
            val isExpanded = selectedCategoryType == category.type
            val itemShape =
                if (uiState.shortcutCategories.size == 1) {
                    Shapes.singlePaneSingleCategory
                } else if (index == 0) {
                    Shapes.singlePaneFirstCategory
                } else if (index == uiState.shortcutCategories.lastIndex) {
                    Shapes.singlePaneLastCategory
                } else {
                    Shapes.singlePaneCategory
                }
            CategoryItemSinglePane(
                searchQuery = uiState.searchQuery,
                category = category,
                isExpanded = isExpanded,
                onClick = {
                    onCategorySelected(
                        if (isExpanded) {
                            null
                        } else {
                            category.type
                        }
                    )
                },
                shape = itemShape,
            )
        }
    }
}

@Composable
private fun CategoryItemSinglePane(
    searchQuery: String,
    category: ShortcutCategoryUi,
    isExpanded: Boolean,
    onClick: () -> Unit,
    shape: Shape,
) {
    Surface(color = MaterialTheme.colorScheme.surfaceBright, shape = shape, onClick = onClick) {
        Column {
            Row(
                verticalAlignment = Alignment.CenterVertically,
                modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp).padding(horizontal = 16.dp),
            ) {
                ShortcutCategoryIcon(modifier = Modifier.size(24.dp), source = category.iconSource)
                Spacer(modifier = Modifier.width(16.dp))
                Text(category.label)
                Spacer(modifier = Modifier.weight(1f))
                RotatingExpandCollapseIcon(isExpanded)
            }
            AnimatedVisibility(visible = isExpanded) {
                ShortcutCategoryDetailsSinglePane(searchQuery, category)
            }
        }
    }
}

@Composable
private fun RotatingExpandCollapseIcon(isExpanded: Boolean) {
    val expandIconRotationDegrees by
        animateFloatAsState(
            targetValue =
                if (isExpanded) {
                    180f
                } else {
                    0f
                },
            label = "Expand icon rotation animation",
        )
    Icon(
        modifier =
            Modifier.background(
                    color = MaterialTheme.colorScheme.surfaceContainerHigh,
                    shape = CircleShape,
                )
                .graphicsLayer { rotationZ = expandIconRotationDegrees },
        imageVector = Icons.Default.ExpandMore,
        contentDescription =
            if (isExpanded) {
                stringResource(R.string.shortcut_helper_content_description_collapse_icon)
            } else {
                stringResource(R.string.shortcut_helper_content_description_expand_icon)
            },
        tint = MaterialTheme.colorScheme.onSurface,
    )
}

@Composable
private fun ShortcutCategoryDetailsSinglePane(searchQuery: String, category: ShortcutCategoryUi) {
    Column(Modifier.padding(horizontal = 16.dp)) {
        category.subCategories.fastForEach { subCategory ->
            ShortcutSubCategorySinglePane(searchQuery, subCategory)
        }
    }
}

@Composable
private fun ShortcutSubCategorySinglePane(searchQuery: String, subCategory: ShortcutSubCategory) {
    // This @Composable is expected to be in a Column.
    SubCategoryTitle(subCategory.label)
    subCategory.shortcuts.fastForEachIndexed { index, shortcut ->
        if (index > 0) {
            HorizontalDivider(color = MaterialTheme.colorScheme.surfaceContainerHigh)
        }
        Shortcut(Modifier.padding(vertical = 24.dp), searchQuery, shortcut)
    }
}

private object ShortcutHelperSinglePane {

    object Shapes {
        val singlePaneFirstCategory =
            RoundedCornerShape(
                topStart = Dimensions.SinglePaneCategoryCornerRadius,
                topEnd = Dimensions.SinglePaneCategoryCornerRadius,
            )
        val singlePaneLastCategory =
            RoundedCornerShape(
                bottomStart = Dimensions.SinglePaneCategoryCornerRadius,
                bottomEnd = Dimensions.SinglePaneCategoryCornerRadius,
            )
        val singlePaneSingleCategory =
            RoundedCornerShape(size = Dimensions.SinglePaneCategoryCornerRadius)
        val singlePaneCategory = RectangleShape
    }

    object Dimensions {
        val SinglePaneCategoryCornerRadius = 28.dp
    }
}
Loading