diff options
| author | 2024-05-31 13:25:12 +0100 | |
|---|---|---|
| committer | 2024-06-06 10:48:32 +0100 | |
| commit | 85f37909701cbb1cddeb26efead5edb1f7d0ce6f (patch) | |
| tree | 7055216c8e9b92afef0f2bf31143db4db9dfa502 | |
| parent | f7caa4dfabb88bae908153e1cfb32f275dc19305 (diff) | |
Shortcut Helper - Implement UI for shortcut details
- Data classes and the data of the shortcuts is all still temporary
Fixes: 335387428
Test: Manually - See screenshots
Flag: com.android.systemui.keyboard_shortcut_helper_rewrite
Change-Id: I6e128bcb358b6639168ae27a5de0aa7a36fd9b77
3 files changed, 462 insertions, 48 deletions
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index cac2df5b45d9..951ffedceed9 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3525,6 +3525,14 @@ use. The helper shows shortcuts in categories, which can be collapsed or expanded. [CHAR LIMIT=NONE] --> <string name="shortcut_helper_content_description_expand_icon">Expand icon</string> + <!-- Word that separates different possible key combinations of a shortcut. For example the + "Go to home screen" shortcut could be triggered using "home button" OR "ctrl + h". + This is that "or" separator. + The keyboard shortcut helper is a component that shows the user which keyboard shortcuts + they can use. + [CHAR LIMIT=NONE] + --> + <string name="shortcut_helper_key_combinations_or_separator">or</string> <!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] --> <string name="keyboard_backlight_dialog_title">Keyboard backlight</string> diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt index 52ccc219353e..271e79b8d060 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt @@ -16,13 +16,16 @@ package com.android.systemui.keyboard.shortcut.ui.composable -import androidx.annotation.StringRes 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.BoxScope import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.FlowRowScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -32,21 +35,19 @@ 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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items 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.automirrored.filled.OpenInNew -import androidx.compose.material.icons.filled.Accessibility -import androidx.compose.material.icons.filled.Apps import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material.icons.filled.Keyboard import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Tv -import androidx.compose.material.icons.filled.VerticalSplit import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationDrawerItemColors @@ -56,6 +57,7 @@ import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -69,10 +71,13 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.util.fastForEach @@ -81,8 +86,13 @@ import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.systemui.res.R @Composable -fun ShortcutHelper(modifier: Modifier = Modifier, onKeyboardSettingsClicked: () -> Unit) { - if (shouldUseSinglePane()) { +fun ShortcutHelper( + onKeyboardSettingsClicked: () -> Unit, + modifier: Modifier = Modifier, + categories: List<ShortcutHelperCategory> = ShortcutHelperTemporaryData.categories, + useSinglePane: @Composable () -> Boolean = { shouldUseSinglePane() }, +) { + if (useSinglePane()) { ShortcutHelperSinglePane(modifier, categories, onKeyboardSettingsClicked) } else { ShortcutHelperTwoPane(modifier, categories, onKeyboardSettingsClicked) @@ -91,7 +101,8 @@ fun ShortcutHelper(modifier: Modifier = Modifier, onKeyboardSettingsClicked: () @Composable private fun shouldUseSinglePane() = - LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact + LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact || + LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Compact @Composable private fun ShortcutHelperSinglePane( @@ -209,11 +220,31 @@ private fun RotatingExpandCollapseIcon(isExpanded: Boolean) { @Composable private fun ShortcutCategoryDetailsSinglePane(category: ShortcutHelperCategory) { - Box(modifier = Modifier.fillMaxWidth().heightIn(min = 300.dp)) { - Text( - modifier = Modifier.align(Alignment.Center), - text = stringResource(category.labelResId), - ) + Column(Modifier.padding(horizontal = 16.dp)) { + category.subCategories.fastForEach { subCategory -> + ShortcutSubCategorySinglePane(subCategory) + } + } +} + +@Composable +private fun ShortcutSubCategorySinglePane(subCategory: SubCategory) { + // This @Composable is expected to be in a Column. + SubCategoryTitle(subCategory.label) + subCategory.shortcuts.fastForEachIndexed { index, shortcut -> + if (index > 0) { + HorizontalDivider() + } + ShortcutSinglePane(shortcut) + } +} + +@Composable +private fun ShortcutSinglePane(shortcut: Shortcut) { + Column(Modifier.padding(vertical = 24.dp)) { + ShortcutDescriptionText(shortcut = shortcut) + Spacer(modifier = Modifier.height(12.dp)) + ShortcutKeyCombinations(shortcut = shortcut) } } @@ -223,6 +254,7 @@ private fun ShortcutHelperTwoPane( categories: List<ShortcutHelperCategory>, onKeyboardSettingsClicked: () -> Unit, ) { + var selectedCategory by remember { mutableStateOf(categories.first()) } Column(modifier = modifier.fillMaxSize().padding(start = 24.dp, end = 24.dp, top = 26.dp)) { TitleBar() Spacer(modifier = Modifier.height(12.dp)) @@ -230,39 +262,189 @@ private fun ShortcutHelperTwoPane( StartSidePanel( modifier = Modifier.fillMaxWidth(fraction = 0.32f), categories = categories, + selectedCategory = selectedCategory, + onCategoryClicked = { selectedCategory = it }, onKeyboardSettingsClicked = onKeyboardSettingsClicked, ) Spacer(modifier = Modifier.width(24.dp)) - EndSidePanel(Modifier.fillMaxSize()) + EndSidePanel(Modifier.fillMaxSize(), selectedCategory) + } + } +} + +@Composable +private fun EndSidePanel(modifier: Modifier, category: ShortcutHelperCategory) { + LazyColumn(modifier.nestedScroll(rememberNestedScrollInteropConnection())) { + items(items = category.subCategories, key = { item -> item.label }) { + SubCategoryContainerDualPane(it) + Spacer(modifier = Modifier.height(8.dp)) } } } @Composable +private fun SubCategoryContainerDualPane(subCategory: SubCategory) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceBright + ) { + Column(Modifier.padding(horizontal = 32.dp, vertical = 24.dp)) { + SubCategoryTitle(subCategory.label) + Spacer(Modifier.height(24.dp)) + subCategory.shortcuts.fastForEachIndexed { index, shortcut -> + if (index > 0) { + HorizontalDivider() + } + ShortcutViewDualPane(shortcut) + } + } + } +} + +@Composable +private fun SubCategoryTitle(title: String) { + Text( + title, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary, + ) +} + +@Composable +private fun ShortcutViewDualPane(shortcut: Shortcut) { + Row(Modifier.padding(vertical = 16.dp)) { + ShortcutDescriptionText( + modifier = Modifier.weight(0.25f).align(Alignment.CenterVertically), + shortcut = shortcut, + ) + ShortcutKeyCombinations( + modifier = Modifier.weight(0.75f), + shortcut = shortcut, + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun ShortcutKeyCombinations( + modifier: Modifier = Modifier, + shortcut: Shortcut, +) { + FlowRow(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { + shortcut.commands.forEachIndexed { index, command -> + if (index > 0) { + ShortcutOrSeparator(spacing = 16.dp) + } + ShortcutCommand(command) + } + } +} + +@Composable +private fun ShortcutCommand(command: ShortcutCommand) { + // This @Composable is expected to be in a Row or FlowRow. + command.keys.forEachIndexed { keyIndex, key -> + if (keyIndex > 0) { + Spacer(Modifier.width(4.dp)) + } + ShortcutKeyContainer { + if (key is ShortcutKey.Text) { + ShortcutTextKey(key) + } else if (key is ShortcutKey.Icon) { + ShortcutIconKey(key) + } + } + } +} + +@Composable +private fun ShortcutKeyContainer(shortcutKeyContent: @Composable BoxScope.() -> Unit) { + Box( + modifier = + Modifier.height(36.dp) + .background( + color = MaterialTheme.colorScheme.surfaceContainer, + shape = RoundedCornerShape(12.dp) + ), + ) { + shortcutKeyContent() + } +} + +@Composable +private fun BoxScope.ShortcutTextKey(key: ShortcutKey.Text) { + Text( + text = key.value, + modifier = Modifier.align(Alignment.Center).padding(horizontal = 12.dp), + style = MaterialTheme.typography.titleSmall, + ) +} + +@Composable +private fun BoxScope.ShortcutIconKey(key: ShortcutKey.Icon) { + Icon( + imageVector = key.value, + contentDescription = null, + modifier = Modifier.align(Alignment.Center).padding(6.dp) + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun FlowRowScope.ShortcutOrSeparator(spacing: Dp) { + Spacer(Modifier.width(spacing)) + Text( + text = stringResource(R.string.shortcut_helper_key_combinations_or_separator), + modifier = Modifier.align(Alignment.CenterVertically), + style = MaterialTheme.typography.titleSmall, + ) + Spacer(Modifier.width(spacing)) +} + +@Composable +private fun ShortcutDescriptionText( + shortcut: Shortcut, + modifier: Modifier = Modifier, +) { + Text( + modifier = modifier, + text = shortcut.label, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) +} + +@Composable private fun StartSidePanel( modifier: Modifier, categories: List<ShortcutHelperCategory>, onKeyboardSettingsClicked: () -> Unit, + selectedCategory: ShortcutHelperCategory, + onCategoryClicked: (ShortcutHelperCategory) -> Unit, ) { Column(modifier) { ShortcutsSearchBar() Spacer(modifier = Modifier.heightIn(16.dp)) - CategoriesPanelTwoPane(categories) + CategoriesPanelTwoPane(categories, selectedCategory, onCategoryClicked) Spacer(modifier = Modifier.weight(1f)) KeyboardSettings(onKeyboardSettingsClicked) } } @Composable -private fun CategoriesPanelTwoPane(categories: List<ShortcutHelperCategory>) { - var selected by remember { mutableStateOf(categories.first()) } +private fun CategoriesPanelTwoPane( + categories: List<ShortcutHelperCategory>, + selectedCategory: ShortcutHelperCategory, + onCategoryClicked: (ShortcutHelperCategory) -> Unit +) { Column { categories.fastForEach { CategoryItemTwoPane( label = stringResource(it.labelResId), icon = it.icon, - selected = selected == it, - onClick = { selected = it } + selected = selectedCategory == it, + onClick = { onCategoryClicked(it) } ) } } @@ -305,15 +487,6 @@ private fun CategoryItemTwoPane( } @Composable -fun EndSidePanel(modifier: Modifier) { - Surface( - modifier = modifier, - shape = RoundedCornerShape(28.dp), - color = MaterialTheme.colorScheme.surfaceBright - ) {} -} - -@Composable @OptIn(ExperimentalMaterial3Api::class) private fun TitleBar() { CenterAlignedTopAppBar( @@ -333,6 +506,7 @@ private fun TitleBar() { private fun ShortcutsSearchBar() { var query by remember { mutableStateOf("") } SearchBar( + modifier = Modifier.fillMaxWidth(), colors = SearchBarDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceBright), query = query, active = false, @@ -372,25 +546,6 @@ private fun KeyboardSettings(onClick: () -> Unit) { } } -/** Temporary data class just to populate the UI. */ -private data class ShortcutHelperCategory( - @StringRes val labelResId: Int, - val icon: ImageVector, -) - -// Temporarily populating the categories directly in the UI. -private val categories = - listOf( - ShortcutHelperCategory(R.string.shortcut_helper_category_system, Icons.Default.Tv), - ShortcutHelperCategory( - R.string.shortcut_helper_category_multitasking, - Icons.Default.VerticalSplit - ), - ShortcutHelperCategory(R.string.shortcut_helper_category_input, Icons.Default.Keyboard), - ShortcutHelperCategory(R.string.shortcut_helper_category_app_shortcuts, Icons.Default.Apps), - ShortcutHelperCategory(R.string.shortcut_helper_category_a11y, Icons.Default.Accessibility), - ) - object ShortcutHelper { object Shapes { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperTemporaryData.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperTemporaryData.kt new file mode 100644 index 000000000000..fa2388f6287d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperTemporaryData.kt @@ -0,0 +1,251 @@ +/* + * 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.systemui.keyboard.shortcut.ui.composable + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Accessibility +import androidx.compose.material.icons.filled.Apps +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material.icons.filled.Keyboard +import androidx.compose.material.icons.filled.KeyboardCommandKey +import androidx.compose.material.icons.filled.RadioButtonUnchecked +import androidx.compose.material.icons.filled.Tv +import androidx.compose.material.icons.filled.VerticalSplit +import androidx.compose.ui.graphics.vector.ImageVector +import com.android.systemui.res.R + +/** Temporary data classes and data below just to populate the UI. */ +data class ShortcutHelperCategory( + @StringRes val labelResId: Int, + val icon: ImageVector, + val subCategories: List<SubCategory>, +) + +data class SubCategory( + val label: String, + val shortcuts: List<Shortcut>, +) + +data class Shortcut(val label: String, val commands: List<ShortcutCommand>) + +data class ShortcutCommand(val keys: List<ShortcutKey>) + +sealed interface ShortcutKey { + data class Text(val value: String) : ShortcutKey + + data class Icon(val value: ImageVector) : ShortcutKey +} + +// DSL Builder Functions +private fun shortcutHelperCategory( + labelResId: Int, + icon: ImageVector, + block: ShortcutHelperCategoryBuilder.() -> Unit +): ShortcutHelperCategory = ShortcutHelperCategoryBuilder(labelResId, icon).apply(block).build() + +private fun ShortcutHelperCategoryBuilder.subCategory( + label: String, + block: SubCategoryBuilder.() -> Unit +) { + subCategories.add(SubCategoryBuilder(label).apply(block).build()) +} + +private fun SubCategoryBuilder.shortcut(label: String, block: ShortcutBuilder.() -> Unit) { + shortcuts.add(ShortcutBuilder(label).apply(block).build()) +} + +private fun ShortcutBuilder.command(block: ShortcutCommandBuilder.() -> Unit) { + commands.add(ShortcutCommandBuilder().apply(block).build()) +} + +private fun ShortcutCommandBuilder.key(value: String) { + keys.add(ShortcutKey.Text(value)) +} + +private fun ShortcutCommandBuilder.key(value: ImageVector) { + keys.add(ShortcutKey.Icon(value)) +} + +private class ShortcutHelperCategoryBuilder( + private val labelResId: Int, + private val icon: ImageVector +) { + val subCategories = mutableListOf<SubCategory>() + + fun build() = ShortcutHelperCategory(labelResId, icon, subCategories) +} + +private class SubCategoryBuilder(private val label: String) { + val shortcuts = mutableListOf<Shortcut>() + + fun build() = SubCategory(label, shortcuts) +} + +private class ShortcutBuilder(private val label: String) { + val commands = mutableListOf<ShortcutCommand>() + + fun build() = Shortcut(label, commands) +} + +private class ShortcutCommandBuilder { + val keys = mutableListOf<ShortcutKey>() + + fun build() = ShortcutCommand(keys) +} + +object ShortcutHelperTemporaryData { + + // Some shortcuts and their strings below are made up just to populate the UI for now. + // For this reason they are not in translatable resources yet. + val categories = + listOf( + shortcutHelperCategory(R.string.shortcut_helper_category_system, Icons.Default.Tv) { + subCategory("System controls") { + shortcut("Go to home screen") { + command { key(Icons.Default.RadioButtonUnchecked) } + command { + key(Icons.Default.KeyboardCommandKey) + key("H") + } + command { + key(Icons.Default.KeyboardCommandKey) + key("Return") + } + } + shortcut("View recent apps") { + command { + key(Icons.Default.KeyboardCommandKey) + key("Tab") + } + } + shortcut("All apps search") { + command { key(Icons.Default.KeyboardCommandKey) } + } + } + subCategory("System apps") { + shortcut("Go back") { + command { key(Icons.Default.ArrowBackIosNew) } + command { + key(Icons.Default.KeyboardCommandKey) + key("Left arrow") + } + command { + key(Icons.Default.KeyboardCommandKey) + key("ESC") + } + command { + key(Icons.Default.KeyboardCommandKey) + key("Backspace") + } + } + shortcut("View notifications") { + command { + key(Icons.Default.KeyboardCommandKey) + key("N") + } + } + shortcut("Take a screenshot") { + command { key(Icons.Default.KeyboardCommandKey) } + command { key("CTRL") } + command { key("S") } + } + shortcut("Open Settings") { + command { + key(Icons.Default.KeyboardCommandKey) + key("I") + } + } + } + }, + shortcutHelperCategory( + R.string.shortcut_helper_category_multitasking, + Icons.Default.VerticalSplit + ) { + subCategory("Multitasking & windows") { + shortcut("Take a screenshot") { + command { key(Icons.Default.KeyboardCommandKey) } + command { key("CTRL") } + command { key("S") } + } + } + }, + shortcutHelperCategory( + R.string.shortcut_helper_category_input, + Icons.Default.Keyboard + ) { + subCategory("Input") { + shortcut("Open Settings") { + command { + key(Icons.Default.KeyboardCommandKey) + key("I") + } + } + shortcut("View notifications") { + command { + key(Icons.Default.KeyboardCommandKey) + key("N") + } + } + } + }, + shortcutHelperCategory( + R.string.shortcut_helper_category_app_shortcuts, + Icons.Default.Apps + ) { + subCategory("App shortcuts") { + shortcut("Open Settings") { + command { + key(Icons.Default.KeyboardCommandKey) + key("I") + } + } + shortcut("Go back") { + command { key(Icons.Default.ArrowBackIosNew) } + command { + key(Icons.Default.KeyboardCommandKey) + key("Left arrow") + } + command { + key(Icons.Default.KeyboardCommandKey) + key("ESC") + } + command { + key(Icons.Default.KeyboardCommandKey) + key("Backspace") + } + } + } + }, + shortcutHelperCategory( + R.string.shortcut_helper_category_a11y, + Icons.Default.Accessibility + ) { + subCategory("Accessibility shortcuts") { + shortcut("View recent apps") { + command { + key(Icons.Default.KeyboardCommandKey) + key("Tab") + } + } + shortcut("All apps search") { + command { key(Icons.Default.KeyboardCommandKey) } + } + } + } + ) +} |