• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.keyboard.shortcut.ui.composable
18 
19 import androidx.compose.foundation.BorderStroke
20 import androidx.compose.foundation.background
21 import androidx.compose.foundation.layout.Arrangement
22 import androidx.compose.foundation.layout.Box
23 import androidx.compose.foundation.layout.Column
24 import androidx.compose.foundation.layout.Row
25 import androidx.compose.foundation.layout.Spacer
26 import androidx.compose.foundation.layout.height
27 import androidx.compose.foundation.layout.heightIn
28 import androidx.compose.foundation.layout.padding
29 import androidx.compose.foundation.layout.size
30 import androidx.compose.foundation.layout.sizeIn
31 import androidx.compose.foundation.layout.width
32 import androidx.compose.foundation.layout.wrapContentSize
33 import androidx.compose.foundation.shape.RoundedCornerShape
34 import androidx.compose.material.icons.Icons
35 import androidx.compose.material.icons.filled.Add
36 import androidx.compose.material.icons.filled.ErrorOutline
37 import androidx.compose.material3.Icon
38 import androidx.compose.material3.MaterialTheme
39 import androidx.compose.material3.OutlinedTextField
40 import androidx.compose.material3.OutlinedTextFieldDefaults
41 import androidx.compose.material3.Text
42 import androidx.compose.runtime.Composable
43 import androidx.compose.runtime.LaunchedEffect
44 import androidx.compose.runtime.remember
45 import androidx.compose.ui.Alignment
46 import androidx.compose.ui.Modifier
47 import androidx.compose.ui.focus.FocusDirection
48 import androidx.compose.ui.focus.FocusRequester
49 import androidx.compose.ui.focus.focusProperties
50 import androidx.compose.ui.focus.focusRequester
51 import androidx.compose.ui.graphics.Color
52 import androidx.compose.ui.input.key.Key
53 import androidx.compose.ui.input.key.KeyEvent
54 import androidx.compose.ui.input.key.KeyEventType
55 import androidx.compose.ui.input.key.key
56 import androidx.compose.ui.input.key.onPreviewKeyEvent
57 import androidx.compose.ui.input.key.type
58 import androidx.compose.ui.platform.LocalFocusManager
59 import androidx.compose.ui.res.painterResource
60 import androidx.compose.ui.res.stringResource
61 import androidx.compose.ui.semantics.LiveRegionMode
62 import androidx.compose.ui.semantics.contentDescription
63 import androidx.compose.ui.semantics.hideFromAccessibility
64 import androidx.compose.ui.semantics.liveRegion
65 import androidx.compose.ui.semantics.semantics
66 import androidx.compose.ui.text.font.FontWeight
67 import androidx.compose.ui.text.style.TextAlign
68 import androidx.compose.ui.unit.dp
69 import androidx.compose.ui.unit.sp
70 import com.android.compose.ui.graphics.painter.rememberDrawablePainter
71 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
72 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState
73 import com.android.systemui.res.R
74 
75 @Composable
76 fun ShortcutCustomizationDialog(
77     uiState: ShortcutCustomizationUiState,
78     modifier: Modifier = Modifier,
79     onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean,
80     onCancel: () -> Unit,
81     onConfirmSetShortcut: () -> Unit,
82     onConfirmDeleteShortcut: () -> Unit,
83     onConfirmResetShortcut: () -> Unit,
84     onClearSelectedKeyCombination: () -> Unit,
85 ) {
86     when (uiState) {
87         is ShortcutCustomizationUiState.AddShortcutDialog -> {
88             AddShortcutDialog(
89                 modifier,
90                 uiState,
91                 onShortcutKeyCombinationSelected,
92                 onCancel,
93                 onConfirmSetShortcut,
94                 onClearSelectedKeyCombination,
95             )
96         }
97         is ShortcutCustomizationUiState.DeleteShortcutDialog -> {
98             DeleteShortcutDialog(modifier, onCancel, onConfirmDeleteShortcut)
99         }
100         is ShortcutCustomizationUiState.ResetShortcutDialog -> {
101             ResetShortcutDialog(modifier, onCancel, onConfirmResetShortcut)
102         }
103         else -> {
104             /* No-op */
105         }
106     }
107 }
108 
109 @Composable
AddShortcutDialognull110 private fun AddShortcutDialog(
111     modifier: Modifier,
112     uiState: ShortcutCustomizationUiState.AddShortcutDialog,
113     onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean,
114     onCancel: () -> Unit,
115     onConfirmSetShortcut: () -> Unit,
116     onClearSelectedKeyCombination: () -> Unit,
117 ) {
118     Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
119         Title(uiState.shortcutLabel)
120         Description(
121             text = stringResource(id = R.string.shortcut_customize_mode_add_shortcut_description)
122         )
123         PromptShortcutModifier(
124             modifier = Modifier.padding(top = 24.dp).sizeIn(minWidth = 131.dp, minHeight = 48.dp),
125             defaultModifierKey = uiState.defaultCustomShortcutModifierKey,
126         )
127         SelectedKeyCombinationContainer(
128             shouldShowError = uiState.errorMessage.isNotEmpty(),
129             onShortcutKeyCombinationSelected = onShortcutKeyCombinationSelected,
130             pressedKeys = uiState.pressedKeys,
131             contentDescription = uiState.pressedKeysDescription,
132             onConfirmSetShortcut = onConfirmSetShortcut,
133             onClearSelectedKeyCombination = onClearSelectedKeyCombination,
134         )
135         ErrorMessageContainer(uiState.errorMessage)
136         DialogButtons(
137             onCancel,
138             isConfirmButtonEnabled = uiState.pressedKeys.isNotEmpty(),
139             onConfirm = onConfirmSetShortcut,
140             confirmButtonText =
141                 stringResource(R.string.shortcut_helper_customize_dialog_set_shortcut_button_label),
142         )
143     }
144 }
145 
146 @Composable
DeleteShortcutDialognull147 private fun DeleteShortcutDialog(
148     modifier: Modifier,
149     onCancel: () -> Unit,
150     onConfirmDeleteShortcut: () -> Unit,
151 ) {
152     ConfirmationDialog(
153         modifier = modifier,
154         title = stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title),
155         description =
156             stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_description),
157         confirmButtonText =
158             stringResource(R.string.shortcut_helper_customize_dialog_remove_button_label),
159         onCancel = onCancel,
160         onConfirm = onConfirmDeleteShortcut,
161     )
162 }
163 
164 @Composable
ResetShortcutDialognull165 private fun ResetShortcutDialog(
166     modifier: Modifier,
167     onCancel: () -> Unit,
168     onConfirmResetShortcut: () -> Unit,
169 ) {
170     ConfirmationDialog(
171         modifier = modifier,
172         title = stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title),
173         description =
174             stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_description),
175         confirmButtonText =
176             stringResource(R.string.shortcut_helper_customize_dialog_reset_button_label),
177         onCancel = onCancel,
178         onConfirm = onConfirmResetShortcut,
179     )
180 }
181 
182 @Composable
ConfirmationDialognull183 private fun ConfirmationDialog(
184     modifier: Modifier,
185     title: String,
186     description: String,
187     confirmButtonText: String,
188     onConfirm: () -> Unit,
189     onCancel: () -> Unit,
190 ) {
191     Column(modifier) {
192         Title(title = title)
193         Description(text = description)
194         DialogButtons(
195             onCancel = onCancel,
196             onConfirm = onConfirm,
197             confirmButtonText = confirmButtonText,
198         )
199     }
200 }
201 
202 @Composable
DialogButtonsnull203 private fun DialogButtons(
204     onCancel: () -> Unit,
205     isConfirmButtonEnabled: Boolean = true,
206     onConfirm: () -> Unit,
207     confirmButtonText: String,
208 ) {
209     val focusRequester = remember { FocusRequester() }
210     LaunchedEffect(Unit) { focusRequester.requestFocus() }
211 
212     Row(
213         modifier =
214             Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp)
215                 .sizeIn(minWidth = 316.dp, minHeight = 48.dp),
216         verticalAlignment = Alignment.Bottom,
217         horizontalArrangement = Arrangement.End,
218     ) {
219         ShortcutHelperButton(
220             shape = RoundedCornerShape(50.dp),
221             onClick = onCancel,
222             color = Color.Transparent,
223             modifier = Modifier.heightIn(40.dp),
224             contentColor = MaterialTheme.colorScheme.primary,
225             text = stringResource(R.string.shortcut_helper_customize_dialog_cancel_button_label),
226             border = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.outlineVariant),
227         )
228         Spacer(modifier = Modifier.width(8.dp))
229         ShortcutHelperButton(
230             modifier =
231                 Modifier.heightIn(40.dp).focusRequester(focusRequester).focusProperties {
232                     canFocus = true
233                 }, // enable focus on touch/click mode
234             onClick = onConfirm,
235             color = MaterialTheme.colorScheme.primary,
236             contentColor = MaterialTheme.colorScheme.onPrimary,
237             text = confirmButtonText,
238             enabled = isConfirmButtonEnabled,
239         )
240     }
241 }
242 
243 @Composable
ErrorMessageContainernull244 private fun ErrorMessageContainer(errorMessage: String) {
245     if (errorMessage.isNotEmpty()) {
246         Box(
247             modifier =
248                 Modifier.padding(horizontal = 16.dp).sizeIn(minWidth = 332.dp, minHeight = 40.dp)
249         ) {
250             Text(
251                 text = errorMessage,
252                 style = MaterialTheme.typography.bodyMedium,
253                 fontSize = 14.sp,
254                 lineHeight = 20.sp,
255                 fontWeight = FontWeight.W500,
256                 color = MaterialTheme.colorScheme.error,
257                 modifier =
258                     Modifier.padding(start = 24.dp).width(252.dp).semantics {
259                         contentDescription = errorMessage
260                         liveRegion = LiveRegionMode.Polite
261                     },
262             )
263         }
264     }
265 }
266 
267 @Composable
SelectedKeyCombinationContainernull268 private fun SelectedKeyCombinationContainer(
269     shouldShowError: Boolean,
270     onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean,
271     pressedKeys: List<ShortcutKey>,
272     contentDescription: String,
273     onConfirmSetShortcut: () -> Unit,
274     onClearSelectedKeyCombination: () -> Unit,
275 ) {
276     val focusRequester = remember { FocusRequester() }
277     val focusManager = LocalFocusManager.current
278     LaunchedEffect(Unit) { focusRequester.requestFocus() }
279 
280     OutlinedInputField(
281         modifier =
282             Modifier.padding(all = 16.dp)
283                 .sizeIn(minWidth = 332.dp, minHeight = 56.dp)
284                 .focusRequester(focusRequester)
285                 .focusProperties { canFocus = true }
286                 .onPreviewKeyEvent { keyEvent ->
287                     val keyEventProcessed = onShortcutKeyCombinationSelected(keyEvent)
288                     if (keyEventProcessed) {
289                         true
290                     } else {
291                         if (keyEvent.type == KeyEventType.KeyUp) {
292                             when (keyEvent.key) {
293                                 Key.Enter -> {
294                                     onConfirmSetShortcut()
295                                     return@onPreviewKeyEvent true
296                                 }
297                                 Key.Backspace -> {
298                                     onClearSelectedKeyCombination()
299                                     return@onPreviewKeyEvent true
300                                 }
301                                 Key.DirectionDown -> {
302                                     focusManager.moveFocus(FocusDirection.Down)
303                                     return@onPreviewKeyEvent true
304                                 }
305                                 else -> return@onPreviewKeyEvent false
306                             }
307                         } else false
308                     }
309                 },
310         trailingIcon = { ErrorIcon(shouldShowError) },
311         isError = shouldShowError,
312         placeholder = { PressKeyPrompt() },
313         content =
314             if (pressedKeys.isNotEmpty()) {
315                 { PressedKeysTextContainer(pressedKeys) }
316             } else {
317                 null
318             },
319         contentDescription = contentDescription,
320     )
321 }
322 
323 @Composable
ErrorIconnull324 private fun ErrorIcon(shouldShowError: Boolean) {
325     if (shouldShowError) {
326         Icon(
327             imageVector = Icons.Default.ErrorOutline,
328             contentDescription = null,
329             modifier = Modifier.size(20.dp),
330             tint = MaterialTheme.colorScheme.error,
331         )
332     }
333 }
334 
335 @Composable
PressedKeysTextContainernull336 private fun PressedKeysTextContainer(pressedKeys: List<ShortcutKey>) {
337     Row(
338         modifier = Modifier.semantics { hideFromAccessibility() },
339         verticalAlignment = Alignment.CenterVertically,
340     ) {
341         pressedKeys.forEachIndexed { keyIndex, key ->
342             if (keyIndex > 0) {
343                 ShortcutKeySeparator()
344             }
345             if (key is ShortcutKey.Text) {
346                 ShortcutTextKey(key)
347             } else if (key is ShortcutKey.Icon) {
348                 ShortcutIconKey(key)
349             }
350         }
351     }
352 }
353 
354 @Composable
ShortcutKeySeparatornull355 private fun ShortcutKeySeparator() {
356     Text(
357         text = stringResource(id = R.string.shortcut_helper_plus_symbol),
358         style = MaterialTheme.typography.titleSmall,
359         fontSize = 16.sp,
360         lineHeight = 24.sp,
361         color = MaterialTheme.colorScheme.onSurfaceVariant,
362     )
363 }
364 
365 @Composable
ShortcutIconKeynull366 private fun ShortcutIconKey(key: ShortcutKey.Icon) {
367     Icon(
368         painter =
369             when (key) {
370                 is ShortcutKey.Icon.ResIdIcon -> painterResource(key.drawableResId)
371                 is ShortcutKey.Icon.DrawableIcon -> rememberDrawablePainter(drawable = key.drawable)
372             },
373         contentDescription = null,
374         modifier = Modifier.height(24.dp),
375         tint = MaterialTheme.colorScheme.onSurfaceVariant,
376     )
377 }
378 
379 @Composable
PressKeyPromptnull380 private fun PressKeyPrompt() {
381     Text(
382         text = stringResource(id = R.string.shortcut_helper_add_shortcut_dialog_placeholder),
383         style = MaterialTheme.typography.titleSmall,
384         fontSize = 16.sp,
385         lineHeight = 24.sp,
386         color = MaterialTheme.colorScheme.onSurfaceVariant,
387     )
388 }
389 
390 @Composable
ShortcutTextKeynull391 private fun ShortcutTextKey(key: ShortcutKey.Text) {
392     Text(
393         text = key.value,
394         style = MaterialTheme.typography.titleSmall,
395         fontSize = 16.sp,
396         lineHeight = 24.sp,
397         color = MaterialTheme.colorScheme.onSurfaceVariant,
398     )
399 }
400 
401 @Composable
Titlenull402 private fun Title(title: String) {
403     Text(
404         text = title,
405         style = MaterialTheme.typography.headlineSmall,
406         fontSize = 24.sp,
407         modifier =
408             Modifier.padding(horizontal = 24.dp).width(316.dp).wrapContentSize(Alignment.Center),
409         color = MaterialTheme.colorScheme.onSurface,
410         lineHeight = 32.sp,
411         fontWeight = FontWeight.W400,
412         textAlign = TextAlign.Center,
413     )
414 }
415 
416 @Composable
Descriptionnull417 private fun Description(text: String) {
418     Text(
419         text = text,
420         style = MaterialTheme.typography.bodyMedium,
421         modifier =
422             Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp)
423                 .width(316.dp)
424                 .wrapContentSize(Alignment.Center),
425         color = MaterialTheme.colorScheme.onSurfaceVariant,
426         textAlign = TextAlign.Center,
427     )
428 }
429 
430 @Composable
PromptShortcutModifiernull431 private fun PromptShortcutModifier(
432     modifier: Modifier,
433     defaultModifierKey: ShortcutKey.Icon.ResIdIcon,
434 ) {
435     Row(
436         modifier = modifier,
437         horizontalArrangement = Arrangement.spacedBy(2.dp),
438         verticalAlignment = Alignment.CenterVertically,
439     ) {
440         ActionKeyContainer(defaultModifierKey)
441         PlusIconContainer()
442     }
443 }
444 
445 @Composable
ActionKeyContainernull446 private fun ActionKeyContainer(defaultModifierKey: ShortcutKey.Icon.ResIdIcon) {
447     Row(
448         modifier =
449             Modifier.sizeIn(minWidth = 105.dp, minHeight = 48.dp)
450                 .background(
451                     color = MaterialTheme.colorScheme.surface,
452                     shape = RoundedCornerShape(16.dp),
453                 )
454                 .padding(all = 12.dp),
455         horizontalArrangement = Arrangement.spacedBy(8.dp),
456         verticalAlignment = Alignment.CenterVertically,
457     ) {
458         ActionKeyIcon(defaultModifierKey)
459         ActionKeyText()
460     }
461 }
462 
463 @Composable
ActionKeyTextnull464 private fun ActionKeyText() {
465     Text(
466         text = "Action",
467         style = MaterialTheme.typography.titleMedium,
468         fontSize = 16.sp,
469         lineHeight = 24.sp,
470         modifier = Modifier.wrapContentSize(Alignment.Center),
471         color = MaterialTheme.colorScheme.onSurface,
472     )
473 }
474 
475 @Composable
ActionKeyIconnull476 private fun ActionKeyIcon(defaultModifierKey: ShortcutKey.Icon.ResIdIcon) {
477     Icon(
478         painter = painterResource(id = defaultModifierKey.drawableResId),
479         contentDescription = stringResource(R.string.shortcut_helper_content_description_meta_key),
480         modifier = Modifier.size(24.dp).wrapContentSize(Alignment.Center),
481     )
482 }
483 
484 @Composable
PlusIconContainernull485 private fun PlusIconContainer() {
486     Icon(
487         tint = MaterialTheme.colorScheme.onSurface,
488         imageVector = Icons.Default.Add,
489         contentDescription =
490             stringResource(id = R.string.shortcut_helper_content_description_plus_icon),
491         modifier = Modifier.padding(vertical = 12.dp).size(24.dp).wrapContentSize(Alignment.Center),
492     )
493 }
494 
495 @Composable
OutlinedInputFieldnull496 private fun OutlinedInputField(
497     content: @Composable (() -> Unit)?,
498     placeholder: @Composable () -> Unit,
499     trailingIcon: @Composable () -> Unit,
500     isError: Boolean,
501     modifier: Modifier = Modifier,
502     contentDescription: String,
503 ) {
504     OutlinedTextField(
505         value = "",
506         onValueChange = {},
507         placeholder = if (content == null) placeholder else null,
508         prefix = content,
509         singleLine = true,
510         modifier =
511             modifier.semantics(mergeDescendants = true) {
512                 this.contentDescription = contentDescription
513             },
514         trailingIcon = trailingIcon,
515         colors =
516             OutlinedTextFieldDefaults.colors()
517                 .copy(
518                     focusedIndicatorColor = MaterialTheme.colorScheme.primary,
519                     unfocusedIndicatorColor = MaterialTheme.colorScheme.outline,
520                     errorIndicatorColor = MaterialTheme.colorScheme.error,
521                 ),
522         shape = RoundedCornerShape(50.dp),
523         isError = isError,
524     )
525 }
526