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

Commit 75e4420c authored by Josh's avatar Josh
Browse files

Capturing user's selected custom key combination for shortcuts

When user presses a key combination we capture it, and display on the
UI.

Test: ShortcutCustomizationViewModelTest
Flag: com.android.systemui.keyboard_shortcut_helper_shortcut_customizer
Bug: 373631227
Change-Id: Iac6718b5377108849e915ea8121be7a8da44c890
parent 293f2dbf
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -3818,6 +3818,10 @@
         assigning a new custom key combination to a shortcut in shortcut helper. The helper is a
         component that shows the user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] -->
    <string name="shortcut_helper_customize_dialog_error_message">Key combination already in use. Try another key.</string>
    <!-- Plus sign, used in keyboard shortcut helper to combine keys for shortcut. E.g. Ctrl + A
         The helper is a component that shows the user which keyboard shortcuts they can use.
         [CHAR LIMIT=NONE] -->
    <string name="shortcut_helper_plus_symbol">+</string>


    <!-- Keyboard touchpad tutorial scheduler-->
+13 −3
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.keyboard.shortcut.ui

import android.app.Dialog
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
@@ -33,6 +34,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialogFactory
import com.android.systemui.statusbar.phone.create
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.awaitCancellation

class ShortcutCustomizationDialogStarter
@AssistedInject
@@ -57,6 +59,7 @@ constructor(
                dialog = null
            }
        }
        awaitCancellation()
    }

    fun onShortcutCustomizationRequested(requestInfo: ShortcutCustomizationRequestInfo) {
@@ -66,14 +69,21 @@ constructor(
    private fun createAddShortcutDialog(): Dialog {
        return dialogFactory.create(dialogDelegate = ShortcutCustomizationDialogDelegate()) { dialog
            ->
            val uiState by viewModel.shortcutCustomizationUiState.collectAsStateWithLifecycle()
            val uiState by
                viewModel.shortcutCustomizationUiState.collectAsStateWithLifecycle(
                    initialValue = ShortcutCustomizationUiState.Inactive
                )
            AssignNewShortcutDialog(
                uiState = uiState,
                modifier = Modifier.width(364.dp).wrapContentHeight().padding(vertical = 24.dp),
                onKeyPress = { viewModel.onKeyPressed(it) },
                onCancel = { dialog.dismiss() },
            )
            dialog.setOnDismissListener { viewModel.onAddShortcutDialogDismissed() }
            dialog.setOnDismissListener { viewModel.onDialogDismissed() }

            // By default, apps cannot intercept action key. The system always handles it. This
            // flag is needed to enable customisation dialog window to intercept action key
            dialog.window?.addPrivateFlags(PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS)
        }
    }

+72 −15
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ 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.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -45,12 +46,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.android.compose.ui.graphics.painter.rememberDrawablePainter
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState
import com.android.systemui.res.R
@@ -81,6 +83,7 @@ fun AssignNewShortcutDialog(
            SelectedKeyCombinationContainer(
                shouldShowErrorMessage = uiState.shouldShowErrorMessage,
                onKeyPress = onKeyPress,
                pressedKeys = uiState.pressedKeys,
            )
            KeyCombinationAlreadyInUseErrorMessage(uiState.shouldShowErrorMessage)
            DialogButtons(onCancel, isValidKeyCombination = uiState.isValidKeyCombination)
@@ -137,10 +140,9 @@ fun KeyCombinationAlreadyInUseErrorMessage(shouldShowErrorMessage: Boolean) {

@Composable
fun SelectedKeyCombinationContainer(
    keyCombination: String =
        stringResource(R.string.shortcut_helper_add_shortcut_dialog_placeholder),
    shouldShowErrorMessage: Boolean,
    onKeyPress: (KeyEvent) -> Boolean,
    pressedKeys: List<ShortcutKey>,
) {
    val interactionSource = remember { MutableInteractionSource() }
    val isFocused by interactionSource.collectIsFocusedAsState()
@@ -157,22 +159,18 @@ fun SelectedKeyCombinationContainer(
            Modifier.padding(all = 16.dp)
                .sizeIn(minWidth = 332.dp, minHeight = 56.dp)
                .border(width = 2.dp, color = outlineColor, shape = RoundedCornerShape(50.dp))
                .onPreviewKeyEvent { onKeyPress(it) },
                .onKeyEvent { onKeyPress(it) },
        interactionSource = interactionSource,
    ) {
        Row(
            modifier = Modifier.padding(start = 24.dp, top = 16.dp, end = 16.dp, bottom = 16.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Text(
                text = keyCombination,
                style = MaterialTheme.typography.headlineSmall,
                fontSize = 16.sp,
                lineHeight = 24.sp,
                fontWeight = FontWeight.W500,
                color = MaterialTheme.colorScheme.onSurfaceVariant,
                modifier = Modifier.width(252.dp),
            )
            if (pressedKeys.isEmpty()) {
                PressKeyPrompt()
            } else {
                PressedKeysTextContainer(pressedKeys)
            }
            Spacer(modifier = Modifier.weight(1f))
            if (shouldShowErrorMessage) {
                Icon(
@@ -186,6 +184,67 @@ fun SelectedKeyCombinationContainer(
    }
}

@Composable
private fun RowScope.PressedKeysTextContainer(pressedKeys: List<ShortcutKey>) {
    pressedKeys.forEachIndexed { keyIndex, key ->
        if (keyIndex > 0) {
            ShortcutKeySeparator()
        }
        if (key is ShortcutKey.Text) {
            ShortcutTextKey(key)
        } else if (key is ShortcutKey.Icon) {
            ShortcutIconKey(key)
        }
    }
}

@Composable
private fun ShortcutKeySeparator() {
    Text(
        text = stringResource(id = R.string.shortcut_helper_plus_symbol),
        style = MaterialTheme.typography.titleSmall,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        color = MaterialTheme.colorScheme.onSurfaceVariant,
    )
}

@Composable
private fun RowScope.ShortcutIconKey(key: ShortcutKey.Icon) {
    Icon(
        painter =
            when (key) {
                is ShortcutKey.Icon.ResIdIcon -> painterResource(key.drawableResId)
                is ShortcutKey.Icon.DrawableIcon -> rememberDrawablePainter(drawable = key.drawable)
            },
        contentDescription = null,
        modifier = Modifier.align(Alignment.CenterVertically).height(24.dp),
        tint = MaterialTheme.colorScheme.onSurfaceVariant,
    )
}

@Composable
private fun PressKeyPrompt() {
    Text(
        text = stringResource(id = R.string.shortcut_helper_add_shortcut_dialog_placeholder),
        style = MaterialTheme.typography.titleSmall,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        color = MaterialTheme.colorScheme.onSurfaceVariant,
    )
}

@Composable
private fun ShortcutTextKey(key: ShortcutKey.Text) {
    Text(
        text = key.value,
        style = MaterialTheme.typography.titleSmall,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        color = MaterialTheme.colorScheme.onSurfaceVariant,
    )
}

@Composable
private fun Title(title: String, modifier: Modifier = Modifier) {
    Text(
@@ -203,8 +262,6 @@ private fun Description(modifier: Modifier = Modifier) {
    Text(
        text = stringResource(id = R.string.shortcut_helper_customize_mode_sub_title),
        style = MaterialTheme.typography.bodyMedium,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        modifier = modifier.wrapContentSize(Alignment.Center),
        color = MaterialTheme.colorScheme.onSurfaceVariant,
    )
+1 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ sealed interface ShortcutCustomizationUiState {
        val isValidKeyCombination: Boolean,
        val defaultCustomShortcutModifierKey: ShortcutKey.Icon.ResIdIcon,
        val isDialogShowing: Boolean,
        val pressedKeys: List<ShortcutKey> = emptyList(),
    ) : ShortcutCustomizationUiState

    data object Inactive : ShortcutCustomizationUiState
+57 −4
Original line number Diff line number Diff line
@@ -17,14 +17,22 @@
package com.android.systemui.keyboard.shortcut.ui.viewmodel

import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isMetaPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.nativeKeyCode
import androidx.compose.ui.input.key.type
import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutCustomizationInteractor
import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update

class ShortcutCustomizationViewModel
@@ -35,7 +43,21 @@ constructor(private val shortcutCustomizationInteractor: ShortcutCustomizationIn
    private val _shortcutCustomizationUiState =
        MutableStateFlow<ShortcutCustomizationUiState>(ShortcutCustomizationUiState.Inactive)

    val shortcutCustomizationUiState = _shortcutCustomizationUiState.asStateFlow()
    val shortcutCustomizationUiState =
        shortcutCustomizationInteractor.pressedKeys
            .map { keys ->
                // Note that Action Key is excluded as it's already displayed on the UI
                keys.filter {
                    it != shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey()
                }
            }
            .combine(_shortcutCustomizationUiState) { keys, uiState ->
                if (uiState is ShortcutCustomizationUiState.AddShortcutDialog) {
                    uiState.copy(pressedKeys = keys)
                } else {
                    uiState
                }
            }

    fun onShortcutCustomizationRequested(requestInfo: ShortcutCustomizationRequestInfo) {
        when (requestInfo) {
@@ -48,6 +70,7 @@ constructor(private val shortcutCustomizationInteractor: ShortcutCustomizationIn
                        defaultCustomShortcutModifierKey =
                            shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey(),
                        isDialogShowing = false,
                        pressedKeys = emptyList(),
                    )
                _shortcutBeingCustomized.value = requestInfo
            }
@@ -62,18 +85,48 @@ constructor(private val shortcutCustomizationInteractor: ShortcutCustomizationIn
        }
    }

    fun onAddShortcutDialogDismissed() {
    fun onDialogDismissed() {
        _shortcutBeingCustomized.value = null
        _shortcutCustomizationUiState.value = ShortcutCustomizationUiState.Inactive
        shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null)
    }

    fun onKeyPressed(keyEvent: KeyEvent): Boolean {
        // TODO Not yet implemented b/373638584
        if ((keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown)) {
            updatePressedKeys(keyEvent)
            return true
        }
        return false
    }

    private fun updatePressedKeys(keyEvent: KeyEvent) {
        val isModifier = SUPPORTED_MODIFIERS.contains(keyEvent.key)
        val keyCombination =
            KeyCombination(
                modifiers = keyEvent.nativeKeyEvent.modifiers,
                keyCode = if (!isModifier) keyEvent.key.nativeKeyCode else null,
            )
        shortcutCustomizationInteractor.updateUserSelectedKeyCombination(keyCombination)
    }

    @AssistedFactory
    interface Factory {
        fun create(): ShortcutCustomizationViewModel
    }

    companion object {
        private val SUPPORTED_MODIFIERS =
            listOf(
                Key.MetaLeft,
                Key.MetaRight,
                Key.CtrlRight,
                Key.CtrlLeft,
                Key.AltLeft,
                Key.AltRight,
                Key.ShiftLeft,
                Key.ShiftRight,
                Key.Function,
                Key.Symbol,
            )
    }
}
Loading