From c7dad5a84a82d3412127242c48fda6bd5e99c327 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 14 Sep 2026 23:06:06 +0200 Subject: [PATCH 01/10] fix: bottom sheet Compose previews work again. Your bottom sheet previews stopped rendering because of a Material 3 upgrade. The sheet opens in its own window. I read the 1.4.0 source: ModalBottomSheet now puts its content in a ModalBottomSheetDialog, which on Android is a ComponentDialog, a separate window. Android Studio's preview only draws the preview's own view tree, so it can't draw separate windows. From memory, 1.3.x used a popup that the preview could draw. --- .../base/actions/ActionOptionsBottomSheet.kt | 4 +- .../CreateNotificationActionBottomSheet.kt | 4 +- .../actions/FlashlightActionBottomSheet.kt | 4 +- .../base/actions/HttpRequestBottomSheet.kt | 4 +- .../actions/ModifySettingActionBottomSheet.kt | 4 +- .../base/actions/SmsActionBottomSheet.kt | 4 +- .../actions/StepMediaActionBottomSheet.kt | 4 +- .../base/actions/ToastActionBottomSheet.kt | 4 +- .../base/actions/VolumeActionBottomSheet.kt | 4 +- .../keyevent/FixKeyEventActionBottomSheet.kt | 4 +- .../DisplayResolutionConstraintBottomSheet.kt | 4 +- .../constraints/TimeConstraintBottomSheet.kt | 4 +- .../base/sorting/SortBottomSheetContent.kt | 4 +- .../trigger/TriggerDiscoverBottomSheet.kt | 4 +- .../trigger/TriggerKeyOptionsBottomSheet.kt | 4 +- .../base/trigger/TriggerSetupBottomSheet.kt | 4 +- .../ui/compose/KeyMapperModalBottomSheet.kt | 49 +++++++++++++++++++ 17 files changed, 81 insertions(+), 32 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperModalBottomSheet.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt index 34fcde0e91..11e1e38c02 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -42,6 +41,7 @@ import io.github.sds100.keymapper.base.utils.ui.SliderMaximums import io.github.sds100.keymapper.base.utils.ui.SliderMinimums import io.github.sds100.keymapper.base.utils.ui.SliderStepSizes import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText import io.github.sds100.keymapper.base.utils.ui.compose.openUriSafe @@ -56,7 +56,7 @@ fun ActionOptionsBottomSheet( onDismissRequest: () -> Unit = {}, callback: ActionOptionsBottomSheetCallback, ) { - ModalBottomSheet( + KeyMapperModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt index 68f74a43ad..b3dbb6027d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -36,6 +35,7 @@ import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText import io.github.sds100.keymapper.base.utils.ui.compose.filledTonalButtonColorsError import kotlinx.coroutines.launch @@ -116,7 +116,7 @@ private fun CreateNotificationActionBottomSheet( } } - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt index d7d058aa61..f08939fa88 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt @@ -28,7 +28,6 @@ import androidx.compose.material3.FilledTonalIconToggleButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Slider @@ -46,6 +45,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSliderThumb import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText @@ -348,7 +348,7 @@ private fun FlashlightActionBottomSheet( val scrollState = rememberScrollState() val scope = rememberCoroutineScope() - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt index 2ebe6e1d51..50cceb8171 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -38,6 +37,7 @@ import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperDropdownMenu +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.system.network.HttpMethod import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -110,7 +110,7 @@ private fun HttpRequestBottomSheet( var descriptionError: String? by rememberSaveable { mutableStateOf(null) } var urlError: String? by rememberSaveable { mutableStateOf(null) } - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt index 20fe980ed8..452b706415 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -39,6 +38,7 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.utils.getFullMessage +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.filledTonalButtonColorsError import io.github.sds100.keymapper.common.utils.KMError @@ -118,7 +118,7 @@ private fun ModifySettingActionBottomSheet( } } - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt index b6ad399bb4..2c734df122 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt @@ -16,7 +16,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -41,6 +40,7 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.utils.getFullMessage +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.State @@ -121,7 +121,7 @@ private fun SmsActionBottomSheet( is SmsActionBottomSheetState.ComposeSms -> stringResource(R.string.action_compose_sms) } - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt index b768f34a1c..cb566d215a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -28,6 +27,7 @@ import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText import kotlin.math.roundToInt import kotlinx.coroutines.launch @@ -90,7 +90,7 @@ private fun StepMediaActionBottomSheet( else -> R.string.action_step_forward_media } - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt index 2a0d9a9c56..73d4baabe5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -33,6 +32,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import kotlinx.coroutines.launch @@ -84,7 +84,7 @@ private fun ToastActionBottomSheet( } } - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt index 959951b03c..e60e1e997b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt @@ -16,7 +16,6 @@ import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -37,6 +36,7 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.VolumeStreamStrings import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.system.volume.VolumeStream @@ -99,7 +99,7 @@ private fun VolumeActionBottomSheet( val scrollState = rememberScrollState() val scope = rememberCoroutineScope() - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt index 81ca3c2fc1..fb12ae57cf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedCard import androidx.compose.material3.RadioButton import androidx.compose.material3.SheetState @@ -51,6 +50,7 @@ import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText import io.github.sds100.keymapper.base.utils.ui.compose.ExpertModeRequirementRow import io.github.sds100.keymapper.base.utils.ui.compose.HeaderText import io.github.sds100.keymapper.base.utils.ui.compose.InputMethodRequirementRow +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.filledTonalButtonColorsError @OptIn(ExperimentalMaterial3Api::class) @@ -69,7 +69,7 @@ fun FixKeyEventActionBottomSheet( onDoneClick: () -> Unit = {}, onAutoSwitchImeCheckedChange: (Boolean) -> Unit = {}, ) { - ModalBottomSheet( + KeyMapperModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt index 5424ad8c9a..2f48d82fde 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -31,6 +30,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.common.utils.SizeKM import kotlinx.coroutines.launch @@ -66,7 +66,7 @@ private fun DisplayResolutionConstraintBottomSheet( ) { val scope = rememberCoroutineScope() - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt index e0a1415247..45831fe2d1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -41,6 +40,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow import io.github.sds100.keymapper.common.utils.TimeUtils import java.time.format.FormatStyle @@ -126,7 +126,7 @@ private fun TimeConstraintBottomSheet( ) } - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt b/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt index fa3e7d9a7a..231d71ebd4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt @@ -41,7 +41,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Surface @@ -72,6 +71,7 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.utils.ui.compose.DraggableItem +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.rememberDragDropState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -120,7 +120,7 @@ private fun SortBottomSheet( ) { val coroutineScope = rememberCoroutineScope() - ModalBottomSheet( + KeyMapperModalBottomSheet( modifier = modifier, sheetState = sheetState, onDismissRequest = onDismissRequest, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt index cf17a0def7..ae29e617f7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -18,6 +17,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -30,7 +30,7 @@ fun TriggerDiscoverBottomSheet( ) { val scope = rememberCoroutineScope() - ModalBottomSheet( + KeyMapperModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index 5a90be8d5c..43f8457efa 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -19,7 +19,6 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState import androidx.compose.material3.Text import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo @@ -42,6 +41,7 @@ import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.utils.ui.CheckBoxListItem import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.base.utils.ui.compose.openUriSafe @@ -66,7 +66,7 @@ fun TriggerKeyOptionsBottomSheet( ) { val isCompact = isVerticalCompactLayout() - ModalBottomSheet( + KeyMapperModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt index 1131d4f5a1..0092ce0da5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt @@ -29,7 +29,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Surface @@ -59,6 +58,7 @@ import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText import io.github.sds100.keymapper.base.utils.ui.compose.ExpertModeRequirementRow import io.github.sds100.keymapper.base.utils.ui.compose.HeaderText import io.github.sds100.keymapper.base.utils.ui.compose.InputMethodRequirementRow +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.base.utils.ui.compose.icons.IndeterminateQuestionBox @@ -838,7 +838,7 @@ fun TriggerSetupBottomSheet( ) { val scope = rememberCoroutineScope() - ModalBottomSheet( + KeyMapperModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperModalBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperModalBottomSheet.kt new file mode 100644 index 0000000000..54fefded1d --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperModalBottomSheet.kt @@ -0,0 +1,49 @@ +package io.github.sds100.keymapper.base.utils.ui.compose + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.Surface +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode + +/** + * Use this instead of [ModalBottomSheet] so the sheet content is visible in previews. + * [ModalBottomSheet] shows its content in a separate dialog window, which previews can't draw. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun KeyMapperModalBottomSheet( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + dragHandle: @Composable (() -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit, +) { + if (LocalInspectionMode.current) { + Surface( + modifier = modifier.fillMaxWidth(), + shape = BottomSheetDefaults.ExpandedShape, + color = BottomSheetDefaults.ContainerColor, + ) { + Column { + dragHandle?.invoke() + content() + } + } + } else { + ModalBottomSheet( + onDismissRequest = onDismissRequest, + modifier = modifier, + sheetState = sheetState, + dragHandle = dragHandle, + content = content, + ) + } +} From 3e4184e58bb0a4b46d0f1c1609d2a25650eec685 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 14 Sep 2026 23:06:06 +0200 Subject: [PATCH 02/10] fix: bottom sheet Compose previews work again. Your bottom sheet previews stopped rendering because of a Material 3 upgrade. The sheet opens in its own window. I read the 1.4.0 source: ModalBottomSheet now puts its content in a ModalBottomSheetDialog, which on Android is a ComponentDialog, a separate window. Android Studio's preview only draws the preview's own view tree, so it can't draw separate windows. From memory, 1.3.x used a popup that the preview could draw. --- .../base/actions/ActionOptionsBottomSheet.kt | 4 +- .../CreateNotificationActionBottomSheet.kt | 4 +- .../actions/FlashlightActionBottomSheet.kt | 4 +- .../base/actions/HttpRequestBottomSheet.kt | 4 +- .../actions/ModifySettingActionBottomSheet.kt | 4 +- .../base/actions/SmsActionBottomSheet.kt | 4 +- .../actions/StepMediaActionBottomSheet.kt | 4 +- .../base/actions/ToastActionBottomSheet.kt | 4 +- .../base/actions/VolumeActionBottomSheet.kt | 4 +- .../keyevent/FixKeyEventActionBottomSheet.kt | 4 +- .../DisplayResolutionConstraintBottomSheet.kt | 4 +- .../constraints/TimeConstraintBottomSheet.kt | 4 +- .../base/sorting/SortBottomSheetContent.kt | 4 +- .../trigger/TriggerDiscoverBottomSheet.kt | 4 +- .../trigger/TriggerKeyOptionsBottomSheet.kt | 4 +- .../base/trigger/TriggerSetupBottomSheet.kt | 4 +- .../ui/compose/KeyMapperModalBottomSheet.kt | 49 +++++++++++++++++++ 17 files changed, 81 insertions(+), 32 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperModalBottomSheet.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt index 34fcde0e91..11e1e38c02 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -42,6 +41,7 @@ import io.github.sds100.keymapper.base.utils.ui.SliderMaximums import io.github.sds100.keymapper.base.utils.ui.SliderMinimums import io.github.sds100.keymapper.base.utils.ui.SliderStepSizes import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText import io.github.sds100.keymapper.base.utils.ui.compose.openUriSafe @@ -56,7 +56,7 @@ fun ActionOptionsBottomSheet( onDismissRequest: () -> Unit = {}, callback: ActionOptionsBottomSheetCallback, ) { - ModalBottomSheet( + KeyMapperModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt index 68f74a43ad..b3dbb6027d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -36,6 +35,7 @@ import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText import io.github.sds100.keymapper.base.utils.ui.compose.filledTonalButtonColorsError import kotlinx.coroutines.launch @@ -116,7 +116,7 @@ private fun CreateNotificationActionBottomSheet( } } - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt index d7d058aa61..f08939fa88 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt @@ -28,7 +28,6 @@ import androidx.compose.material3.FilledTonalIconToggleButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Slider @@ -46,6 +45,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSliderThumb import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText @@ -348,7 +348,7 @@ private fun FlashlightActionBottomSheet( val scrollState = rememberScrollState() val scope = rememberCoroutineScope() - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt index 2ebe6e1d51..50cceb8171 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -38,6 +37,7 @@ import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperDropdownMenu +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.system.network.HttpMethod import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -110,7 +110,7 @@ private fun HttpRequestBottomSheet( var descriptionError: String? by rememberSaveable { mutableStateOf(null) } var urlError: String? by rememberSaveable { mutableStateOf(null) } - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt index 20fe980ed8..452b706415 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -39,6 +38,7 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.utils.getFullMessage +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.filledTonalButtonColorsError import io.github.sds100.keymapper.common.utils.KMError @@ -118,7 +118,7 @@ private fun ModifySettingActionBottomSheet( } } - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt index b6ad399bb4..2c734df122 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt @@ -16,7 +16,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -41,6 +40,7 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.utils.getFullMessage +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.State @@ -121,7 +121,7 @@ private fun SmsActionBottomSheet( is SmsActionBottomSheetState.ComposeSms -> stringResource(R.string.action_compose_sms) } - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt index b768f34a1c..cb566d215a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -28,6 +27,7 @@ import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText import kotlin.math.roundToInt import kotlinx.coroutines.launch @@ -90,7 +90,7 @@ private fun StepMediaActionBottomSheet( else -> R.string.action_step_forward_media } - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt index 2a0d9a9c56..73d4baabe5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -33,6 +32,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import kotlinx.coroutines.launch @@ -84,7 +84,7 @@ private fun ToastActionBottomSheet( } } - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt index 959951b03c..e60e1e997b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt @@ -16,7 +16,6 @@ import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -37,6 +36,7 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.VolumeStreamStrings import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.system.volume.VolumeStream @@ -99,7 +99,7 @@ private fun VolumeActionBottomSheet( val scrollState = rememberScrollState() val scope = rememberCoroutineScope() - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt index 81ca3c2fc1..fb12ae57cf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedCard import androidx.compose.material3.RadioButton import androidx.compose.material3.SheetState @@ -51,6 +50,7 @@ import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText import io.github.sds100.keymapper.base.utils.ui.compose.ExpertModeRequirementRow import io.github.sds100.keymapper.base.utils.ui.compose.HeaderText import io.github.sds100.keymapper.base.utils.ui.compose.InputMethodRequirementRow +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.filledTonalButtonColorsError @OptIn(ExperimentalMaterial3Api::class) @@ -69,7 +69,7 @@ fun FixKeyEventActionBottomSheet( onDoneClick: () -> Unit = {}, onAutoSwitchImeCheckedChange: (Boolean) -> Unit = {}, ) { - ModalBottomSheet( + KeyMapperModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt index 5424ad8c9a..2f48d82fde 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -31,6 +30,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.common.utils.SizeKM import kotlinx.coroutines.launch @@ -66,7 +66,7 @@ private fun DisplayResolutionConstraintBottomSheet( ) { val scope = rememberCoroutineScope() - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt index e0a1415247..45831fe2d1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -41,6 +40,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow import io.github.sds100.keymapper.common.utils.TimeUtils import java.time.format.FormatStyle @@ -126,7 +126,7 @@ private fun TimeConstraintBottomSheet( ) } - ModalBottomSheet( + KeyMapperModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt b/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt index fa3e7d9a7a..231d71ebd4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt @@ -41,7 +41,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Surface @@ -72,6 +71,7 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.utils.ui.compose.DraggableItem +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.rememberDragDropState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -120,7 +120,7 @@ private fun SortBottomSheet( ) { val coroutineScope = rememberCoroutineScope() - ModalBottomSheet( + KeyMapperModalBottomSheet( modifier = modifier, sheetState = sheetState, onDismissRequest = onDismissRequest, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt index cf17a0def7..ae29e617f7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -18,6 +17,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -30,7 +30,7 @@ fun TriggerDiscoverBottomSheet( ) { val scope = rememberCoroutineScope() - ModalBottomSheet( + KeyMapperModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index 5a90be8d5c..43f8457efa 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -19,7 +19,6 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState import androidx.compose.material3.Text import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo @@ -42,6 +41,7 @@ import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.utils.ui.CheckBoxListItem import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.base.utils.ui.compose.openUriSafe @@ -66,7 +66,7 @@ fun TriggerKeyOptionsBottomSheet( ) { val isCompact = isVerticalCompactLayout() - ModalBottomSheet( + KeyMapperModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt index 1131d4f5a1..0092ce0da5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt @@ -29,7 +29,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Surface @@ -59,6 +58,7 @@ import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText import io.github.sds100.keymapper.base.utils.ui.compose.ExpertModeRequirementRow import io.github.sds100.keymapper.base.utils.ui.compose.HeaderText import io.github.sds100.keymapper.base.utils.ui.compose.InputMethodRequirementRow +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.base.utils.ui.compose.icons.IndeterminateQuestionBox @@ -838,7 +838,7 @@ fun TriggerSetupBottomSheet( ) { val scope = rememberCoroutineScope() - ModalBottomSheet( + KeyMapperModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperModalBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperModalBottomSheet.kt new file mode 100644 index 0000000000..54fefded1d --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperModalBottomSheet.kt @@ -0,0 +1,49 @@ +package io.github.sds100.keymapper.base.utils.ui.compose + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.Surface +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode + +/** + * Use this instead of [ModalBottomSheet] so the sheet content is visible in previews. + * [ModalBottomSheet] shows its content in a separate dialog window, which previews can't draw. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun KeyMapperModalBottomSheet( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + dragHandle: @Composable (() -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit, +) { + if (LocalInspectionMode.current) { + Surface( + modifier = modifier.fillMaxWidth(), + shape = BottomSheetDefaults.ExpandedShape, + color = BottomSheetDefaults.ContainerColor, + ) { + Column { + dragHandle?.invoke() + content() + } + } + } else { + ModalBottomSheet( + onDismissRequest = onDismissRequest, + modifier = modifier, + sheetState = sheetState, + dragHandle = dragHandle, + content = content, + ) + } +} From 2837e3a14a7faf0140b12aafcc5e7e075b1ca820 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 14 Sep 2026 23:12:47 +0200 Subject: [PATCH 03/10] update Compose BOM --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f10514a86a..bb38faaad3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ androidx-viewpager2 = "1.1.0" dagger-hilt-android = "2.56.2" hilt-navigation-compose = "1.3.0" -compose-bom = "2026.01.01" +compose-bom = "2026.09.00" compose-compiler = "1.5.10" # kotlinCompilerExtensionVersion desugar-jdk-libs = "2.1.5" From 7771181241b13410e316c3688a8255840d8724d6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 14 Sep 2026 23:12:47 +0200 Subject: [PATCH 04/10] update Compose BOM --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f10514a86a..bb38faaad3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ androidx-viewpager2 = "1.1.0" dagger-hilt-android = "2.56.2" hilt-navigation-compose = "1.3.0" -compose-bom = "2026.01.01" +compose-bom = "2026.09.00" compose-compiler = "1.5.10" # kotlinCompilerExtensionVersion desugar-jdk-libs = "2.1.5" From 8ef90bee8e3345f9e5088dbec452c35d13936e0a Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 15 Sep 2026 10:47:30 +0200 Subject: [PATCH 05/10] Revert "fix: bottom sheet Compose previews work again." This reverts commit c7dad5a84a82d3412127242c48fda6bd5e99c327. --- .../base/actions/ActionOptionsBottomSheet.kt | 4 +- .../CreateNotificationActionBottomSheet.kt | 4 +- .../actions/FlashlightActionBottomSheet.kt | 4 +- .../base/actions/HttpRequestBottomSheet.kt | 4 +- .../actions/ModifySettingActionBottomSheet.kt | 4 +- .../base/actions/SmsActionBottomSheet.kt | 4 +- .../actions/StepMediaActionBottomSheet.kt | 4 +- .../base/actions/ToastActionBottomSheet.kt | 4 +- .../base/actions/VolumeActionBottomSheet.kt | 4 +- .../keyevent/FixKeyEventActionBottomSheet.kt | 4 +- .../DisplayResolutionConstraintBottomSheet.kt | 4 +- .../constraints/TimeConstraintBottomSheet.kt | 4 +- .../base/sorting/SortBottomSheetContent.kt | 4 +- .../trigger/TriggerDiscoverBottomSheet.kt | 4 +- .../trigger/TriggerKeyOptionsBottomSheet.kt | 4 +- .../base/trigger/TriggerSetupBottomSheet.kt | 4 +- .../ui/compose/KeyMapperModalBottomSheet.kt | 49 ------------------- 17 files changed, 32 insertions(+), 81 deletions(-) delete mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperModalBottomSheet.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt index 11e1e38c02..34fcde0e91 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt @@ -22,6 +22,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -41,7 +42,6 @@ import io.github.sds100.keymapper.base.utils.ui.SliderMaximums import io.github.sds100.keymapper.base.utils.ui.SliderMinimums import io.github.sds100.keymapper.base.utils.ui.SliderStepSizes import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText import io.github.sds100.keymapper.base.utils.ui.compose.openUriSafe @@ -56,7 +56,7 @@ fun ActionOptionsBottomSheet( onDismissRequest: () -> Unit = {}, callback: ActionOptionsBottomSheetCallback, ) { - KeyMapperModalBottomSheet( + ModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt index b3dbb6027d..68f74a43ad 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -35,7 +36,6 @@ import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText import io.github.sds100.keymapper.base.utils.ui.compose.filledTonalButtonColorsError import kotlinx.coroutines.launch @@ -116,7 +116,7 @@ private fun CreateNotificationActionBottomSheet( } } - KeyMapperModalBottomSheet( + ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt index f08939fa88..d7d058aa61 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt @@ -28,6 +28,7 @@ import androidx.compose.material3.FilledTonalIconToggleButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Slider @@ -45,7 +46,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSliderThumb import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText @@ -348,7 +348,7 @@ private fun FlashlightActionBottomSheet( val scrollState = rememberScrollState() val scope = rememberCoroutineScope() - KeyMapperModalBottomSheet( + ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt index 50cceb8171..2ebe6e1d51 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -37,7 +38,6 @@ import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperDropdownMenu -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.system.network.HttpMethod import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -110,7 +110,7 @@ private fun HttpRequestBottomSheet( var descriptionError: String? by rememberSaveable { mutableStateOf(null) } var urlError: String? by rememberSaveable { mutableStateOf(null) } - KeyMapperModalBottomSheet( + ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt index 452b706415..20fe980ed8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -38,7 +39,6 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.utils.getFullMessage -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.filledTonalButtonColorsError import io.github.sds100.keymapper.common.utils.KMError @@ -118,7 +118,7 @@ private fun ModifySettingActionBottomSheet( } } - KeyMapperModalBottomSheet( + ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt index 2c734df122..b6ad399bb4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt @@ -16,6 +16,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -40,7 +41,6 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.utils.getFullMessage -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.State @@ -121,7 +121,7 @@ private fun SmsActionBottomSheet( is SmsActionBottomSheetState.ComposeSms -> stringResource(R.string.action_compose_sms) } - KeyMapperModalBottomSheet( + ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt index cb566d215a..b768f34a1c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -27,7 +28,6 @@ import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText import kotlin.math.roundToInt import kotlinx.coroutines.launch @@ -90,7 +90,7 @@ private fun StepMediaActionBottomSheet( else -> R.string.action_step_forward_media } - KeyMapperModalBottomSheet( + ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt index 73d4baabe5..2a0d9a9c56 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -32,7 +33,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import kotlinx.coroutines.launch @@ -84,7 +84,7 @@ private fun ToastActionBottomSheet( } } - KeyMapperModalBottomSheet( + ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt index e60e1e997b..959951b03c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt @@ -16,6 +16,7 @@ import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -36,7 +37,6 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.VolumeStreamStrings import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.system.volume.VolumeStream @@ -99,7 +99,7 @@ private fun VolumeActionBottomSheet( val scrollState = rememberScrollState() val scope = rememberCoroutineScope() - KeyMapperModalBottomSheet( + ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt index fb12ae57cf..81ca3c2fc1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedCard import androidx.compose.material3.RadioButton import androidx.compose.material3.SheetState @@ -50,7 +51,6 @@ import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText import io.github.sds100.keymapper.base.utils.ui.compose.ExpertModeRequirementRow import io.github.sds100.keymapper.base.utils.ui.compose.HeaderText import io.github.sds100.keymapper.base.utils.ui.compose.InputMethodRequirementRow -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.filledTonalButtonColorsError @OptIn(ExperimentalMaterial3Api::class) @@ -69,7 +69,7 @@ fun FixKeyEventActionBottomSheet( onDoneClick: () -> Unit = {}, onAutoSwitchImeCheckedChange: (Boolean) -> Unit = {}, ) { - KeyMapperModalBottomSheet( + ModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt index 2f48d82fde..5424ad8c9a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState @@ -30,7 +31,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.common.utils.SizeKM import kotlinx.coroutines.launch @@ -66,7 +66,7 @@ private fun DisplayResolutionConstraintBottomSheet( ) { val scope = rememberCoroutineScope() - KeyMapperModalBottomSheet( + ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt index 45831fe2d1..e0a1415247 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -40,7 +41,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow import io.github.sds100.keymapper.common.utils.TimeUtils import java.time.format.FormatStyle @@ -126,7 +126,7 @@ private fun TimeConstraintBottomSheet( ) } - KeyMapperModalBottomSheet( + ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt b/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt index 231d71ebd4..fa3e7d9a7a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt @@ -41,6 +41,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Surface @@ -71,7 +72,6 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.utils.ui.compose.DraggableItem -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.rememberDragDropState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -120,7 +120,7 @@ private fun SortBottomSheet( ) { val coroutineScope = rememberCoroutineScope() - KeyMapperModalBottomSheet( + ModalBottomSheet( modifier = modifier, sheetState = sheetState, onDismissRequest = onDismissRequest, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt index ae29e617f7..cf17a0def7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -17,7 +18,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -30,7 +30,7 @@ fun TriggerDiscoverBottomSheet( ) { val scope = rememberCoroutineScope() - KeyMapperModalBottomSheet( + ModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index 43f8457efa..5a90be8d5c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState import androidx.compose.material3.Text import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo @@ -41,7 +42,6 @@ import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.utils.ui.CheckBoxListItem import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.base.utils.ui.compose.openUriSafe @@ -66,7 +66,7 @@ fun TriggerKeyOptionsBottomSheet( ) { val isCompact = isVerticalCompactLayout() - KeyMapperModalBottomSheet( + ModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt index 0092ce0da5..1131d4f5a1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt @@ -29,6 +29,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.Surface @@ -58,7 +59,6 @@ import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText import io.github.sds100.keymapper.base.utils.ui.compose.ExpertModeRequirementRow import io.github.sds100.keymapper.base.utils.ui.compose.HeaderText import io.github.sds100.keymapper.base.utils.ui.compose.InputMethodRequirementRow -import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperModalBottomSheet import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.base.utils.ui.compose.icons.IndeterminateQuestionBox @@ -838,7 +838,7 @@ fun TriggerSetupBottomSheet( ) { val scope = rememberCoroutineScope() - KeyMapperModalBottomSheet( + ModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperModalBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperModalBottomSheet.kt deleted file mode 100644 index 54fefded1d..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperModalBottomSheet.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io.github.sds100.keymapper.base.utils.ui.compose - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.BottomSheetDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetState -import androidx.compose.material3.Surface -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalInspectionMode - -/** - * Use this instead of [ModalBottomSheet] so the sheet content is visible in previews. - * [ModalBottomSheet] shows its content in a separate dialog window, which previews can't draw. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun KeyMapperModalBottomSheet( - onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, - sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), - dragHandle: @Composable (() -> Unit)? = null, - content: @Composable ColumnScope.() -> Unit, -) { - if (LocalInspectionMode.current) { - Surface( - modifier = modifier.fillMaxWidth(), - shape = BottomSheetDefaults.ExpandedShape, - color = BottomSheetDefaults.ContainerColor, - ) { - Column { - dragHandle?.invoke() - content() - } - } - } else { - ModalBottomSheet( - onDismissRequest = onDismissRequest, - modifier = modifier, - sheetState = sheetState, - dragHandle = dragHandle, - content = content, - ) - } -} From 43450981874b89aa01645d10a3cbf7797c0c8641 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 15 Sep 2026 11:00:08 +0200 Subject: [PATCH 06/10] fix: show previews for bottom sheets and dialogs by specifying showSystemUi = true in the Preview annotation --- CLAUDE.md | 3 ++ .../base/actions/ActionOptionsBottomSheet.kt | 7 ++- .../CreateNotificationActionBottomSheet.kt | 7 ++- .../actions/FlashlightActionBottomSheet.kt | 13 +++-- .../base/actions/HttpRequestBottomSheet.kt | 7 ++- .../actions/ModifySettingActionBottomSheet.kt | 19 ++++--- .../base/actions/SmsActionBottomSheet.kt | 13 +++-- .../actions/StepMediaActionBottomSheet.kt | 7 ++- .../base/actions/ToastActionBottomSheet.kt | 7 ++- .../base/actions/VolumeActionBottomSheet.kt | 7 ++- .../keyevent/FixKeyEventActionBottomSheet.kt | 10 ++-- .../talkback/PickTalkBackGestureDialog.kt | 2 +- .../DisplayResolutionConstraintBottomSheet.kt | 7 ++- .../constraints/TimeConstraintBottomSheet.kt | 4 +- .../base/expertmode/ExpertModeScreen.kt | 2 +- .../base/home/BackupFilePickerDialog.kt | 4 +- .../keymapper/base/home/ImportDialog.kt | 2 +- .../SetupAccessibilityServiceDialog.kt | 8 +-- .../shortcuts/CreateKeyMapShortcutScreen.kt | 2 +- .../trigger/TriggerDiscoverBottomSheet.kt | 4 +- .../trigger/TriggerKeyOptionsBottomSheet.kt | 16 ++++-- .../base/trigger/TriggerSetupBottomSheet.kt | 49 +++++++++++++------ .../base/utils/ui/UnsavedChangesDialog.kt | 2 +- .../base/utils/ui/compose/SliderOptionText.kt | 2 +- 24 files changed, 138 insertions(+), 66 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 27616596c8..f902d959ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,6 +90,9 @@ Example: `#2025 feat: add button to report bug on home screen` - Use `LocalUriHandler.openUriSafe` extension for URL launching — do not hoist URL launching logic up the call stack - Use import statements; never use fully qualified names in Compose code - Write `@Preview` composables for every screen +- Dialog and modal bottom sheet previews render blank by default because they open a separate + window. Use `@Preview(showSystemUi = true)` for both. For bottom sheets, also create the + preview's `SheetState` with `initialValue = SheetValue.Expanded`. ## Adding a New Action (10-step checklist) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt index 34fcde0e91..f156be2ceb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt @@ -25,6 +25,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope @@ -429,7 +430,7 @@ interface ActionOptionsBottomSheetCallback { } @OptIn(ExperimentalMaterial3Api::class) -@Preview(heightDp = 1000) +@Preview(heightDp = 1000, showSystemUi = true) @Composable private fun Preview() { KeyMapperTheme { @@ -437,6 +438,7 @@ private fun Preview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) ActionOptionsBottomSheet( @@ -489,7 +491,7 @@ private fun Preview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewNoEditButton() { KeyMapperTheme { @@ -497,6 +499,7 @@ private fun PreviewNoEditButton() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) ActionOptionsBottomSheet( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt index 68f74a43ad..c7fd7d6c7a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateNotificationActionBottomSheet.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -285,7 +286,7 @@ private fun CreateNotificationActionBottomSheet( } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun CreateNotificationActionBottomSheetPreview() { KeyMapperTheme { @@ -293,6 +294,7 @@ private fun CreateNotificationActionBottomSheetPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) CreateNotificationActionBottomSheet( @@ -309,7 +311,7 @@ private fun CreateNotificationActionBottomSheetPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun CreateNotificationActionBottomSheetEmptyPreview() { KeyMapperTheme { @@ -317,6 +319,7 @@ private fun CreateNotificationActionBottomSheetEmptyPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) CreateNotificationActionBottomSheet( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt index d7d058aa61..1e2b993770 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/FlashlightActionBottomSheet.kt @@ -31,6 +31,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -453,7 +454,7 @@ data class ChangeFlashlightStrengthActionState( ) @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewBothLenses() { KeyMapperTheme { @@ -461,6 +462,7 @@ private fun PreviewBothLenses() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) EnableFlashlightActionBottomSheet( @@ -489,7 +491,7 @@ private fun PreviewBothLenses() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewOnlyBackLens() { KeyMapperTheme { @@ -497,6 +499,7 @@ private fun PreviewOnlyBackLens() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) EnableFlashlightActionBottomSheet( @@ -520,7 +523,7 @@ private fun PreviewOnlyBackLens() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewOnlyBackLensChangeStrength() { KeyMapperTheme { @@ -528,6 +531,7 @@ private fun PreviewOnlyBackLensChangeStrength() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) ChangeFlashlightStrengthActionBottomSheet( @@ -549,7 +553,7 @@ private fun PreviewOnlyBackLensChangeStrength() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview(apiLevel = Build.VERSION_CODES.R) +@Preview(apiLevel = Build.VERSION_CODES.R, showSystemUi = true) @Composable private fun PreviewUnsupportedAndroidVersion() { KeyMapperTheme { @@ -557,6 +561,7 @@ private fun PreviewUnsupportedAndroidVersion() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) EnableFlashlightActionBottomSheet( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt index 2ebe6e1d51..27818a788b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/HttpRequestBottomSheet.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -291,7 +292,7 @@ private fun HttpRequestBottomSheet( } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewEmpty() { KeyMapperTheme { @@ -299,6 +300,7 @@ private fun PreviewEmpty() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) HttpRequestBottomSheet( sheetState = sheetState, @@ -315,7 +317,7 @@ private fun PreviewEmpty() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewFilled() { KeyMapperTheme { @@ -323,6 +325,7 @@ private fun PreviewFilled() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) HttpRequestBottomSheet( sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt index 20fe980ed8..74f5f3584f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ModifySettingActionBottomSheet.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -308,7 +309,7 @@ private fun ModifySettingActionBottomSheet( } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun Preview() { KeyMapperTheme { @@ -316,6 +317,7 @@ private fun Preview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) ModifySettingActionBottomSheet( @@ -330,7 +332,7 @@ private fun Preview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewEmpty() { KeyMapperTheme { @@ -338,6 +340,7 @@ private fun PreviewEmpty() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) ModifySettingActionBottomSheet( @@ -352,7 +355,7 @@ private fun PreviewEmpty() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewPermissionNotGranted() { KeyMapperTheme { @@ -360,6 +363,7 @@ private fun PreviewPermissionNotGranted() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) ModifySettingActionBottomSheet( @@ -375,7 +379,7 @@ private fun PreviewPermissionNotGranted() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewTestLoading() { KeyMapperTheme { @@ -383,6 +387,7 @@ private fun PreviewTestLoading() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) ModifySettingActionBottomSheet( @@ -398,7 +403,7 @@ private fun PreviewTestLoading() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewTestSuccess() { KeyMapperTheme { @@ -406,6 +411,7 @@ private fun PreviewTestSuccess() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) ModifySettingActionBottomSheet( @@ -421,7 +427,7 @@ private fun PreviewTestSuccess() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewTestError() { KeyMapperTheme { @@ -429,6 +435,7 @@ private fun PreviewTestError() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) ModifySettingActionBottomSheet( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt index b6ad399bb4..a9c8e3bd12 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/SmsActionBottomSheet.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -326,7 +327,7 @@ private fun SmsActionBottomSheet( } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun Preview() { KeyMapperTheme { @@ -334,6 +335,7 @@ private fun Preview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) SmsActionBottomSheet( @@ -349,7 +351,7 @@ private fun Preview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewTestError() { KeyMapperTheme { @@ -357,6 +359,7 @@ private fun PreviewTestError() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) SmsActionBottomSheet( @@ -372,7 +375,7 @@ private fun PreviewTestError() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewTestSuccess() { KeyMapperTheme { @@ -380,6 +383,7 @@ private fun PreviewTestSuccess() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) SmsActionBottomSheet( @@ -395,7 +399,7 @@ private fun PreviewTestSuccess() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewEmpty() { KeyMapperTheme { @@ -403,6 +407,7 @@ private fun PreviewEmpty() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) SmsActionBottomSheet( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt index b768f34a1c..34a683af92 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/StepMediaActionBottomSheet.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -173,7 +174,7 @@ private fun StepMediaActionBottomSheet( } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun StepMediaActionBottomSheetPreview() { KeyMapperTheme { @@ -181,6 +182,7 @@ private fun StepMediaActionBottomSheetPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) StepMediaActionBottomSheet( @@ -195,7 +197,7 @@ private fun StepMediaActionBottomSheetPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun StepMediaActionBottomSheetDefaultPreview() { KeyMapperTheme { @@ -203,6 +205,7 @@ private fun StepMediaActionBottomSheetDefaultPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) StepMediaActionBottomSheet( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt index 2a0d9a9c56..7e766f483f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ToastActionBottomSheet.kt @@ -16,6 +16,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -171,7 +172,7 @@ private fun ToastActionBottomSheet( } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun ToastActionBottomSheetPreview() { KeyMapperTheme { @@ -179,6 +180,7 @@ private fun ToastActionBottomSheetPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) ToastActionBottomSheet( sheetState = sheetState, @@ -191,7 +193,7 @@ private fun ToastActionBottomSheetPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun ToastActionBottomSheetEmptyPreview() { KeyMapperTheme { @@ -199,6 +201,7 @@ private fun ToastActionBottomSheetEmptyPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) ToastActionBottomSheet( sheetState = sheetState, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt index 959951b03c..28e27bc03c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -200,7 +201,7 @@ private fun VolumeActionBottomSheet( } @OptIn(ExperimentalMaterial3Api::class) -@Preview(heightDp = 400) +@Preview(heightDp = 400, showSystemUi = true) @Composable private fun PreviewVolumeActionBottomSheet() { KeyMapperTheme { @@ -208,6 +209,7 @@ private fun PreviewVolumeActionBottomSheet() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) var state by remember { @@ -232,7 +234,7 @@ private fun PreviewVolumeActionBottomSheet() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewVolumeActionBottomSheetDefaultStream() { KeyMapperTheme { @@ -240,6 +242,7 @@ private fun PreviewVolumeActionBottomSheetDefaultStream() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) var state by remember { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt index 81ca3c2fc1..7bce08f242 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt @@ -30,6 +30,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedCard import androidx.compose.material3.RadioButton import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -296,7 +297,7 @@ private fun FixKeyEventActionOptionCard( } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun InputMethodPreview() { KeyMapperTheme { @@ -304,6 +305,7 @@ private fun InputMethodPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) FixKeyEventActionBottomSheet( @@ -321,7 +323,7 @@ private fun InputMethodPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun ExpertModePreview() { KeyMapperTheme { @@ -329,6 +331,7 @@ private fun ExpertModePreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) FixKeyEventActionBottomSheet( @@ -342,7 +345,7 @@ private fun ExpertModePreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun ExpertModeUnsupportedPreview() { KeyMapperTheme { @@ -350,6 +353,7 @@ private fun ExpertModeUnsupportedPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) FixKeyEventActionBottomSheet( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/PickTalkBackGestureDialog.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/PickTalkBackGestureDialog.kt index 9142583a93..32bbe1f439 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/PickTalkBackGestureDialog.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/PickTalkBackGestureDialog.kt @@ -170,7 +170,7 @@ private fun TalkBackGestureItem( } } -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewPickTalkBackGestureDialog() { KeyMapperTheme { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt index 5424ad8c9a..b8795342d3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -192,7 +193,7 @@ private fun DisplayResolutionConstraintBottomSheet( } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewWithModes() { KeyMapperTheme { @@ -200,6 +201,7 @@ private fun PreviewWithModes() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) DisplayResolutionConstraintBottomSheet( @@ -219,7 +221,7 @@ private fun PreviewWithModes() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewCustomOnly() { KeyMapperTheme { @@ -227,6 +229,7 @@ private fun PreviewCustomOnly() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) DisplayResolutionConstraintBottomSheet( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt index e0a1415247..e17627be2f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/TimeConstraintBottomSheet.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TimePicker @@ -267,7 +268,7 @@ private fun TimePickerDialog(state: TimePickerState, onDismiss: () -> Unit, onCo } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun Preview() { KeyMapperTheme { @@ -275,6 +276,7 @@ private fun Preview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) TimeConstraintBottomSheet( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt index e0e77eb0c6..10dcd98621 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt @@ -1221,7 +1221,7 @@ private fun SystemBridgeStartErrorDialog( ) } -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewSystemBridgeStartErrorDialog() { KeyMapperTheme { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/BackupFilePickerDialog.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/BackupFilePickerDialog.kt index 7fdb6b42ae..ac99012d70 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/BackupFilePickerDialog.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/BackupFilePickerDialog.kt @@ -95,7 +95,7 @@ private fun BackupFileListRow(modifier: Modifier = Modifier, name: String, onCli } } -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewBackupFilePickerDialog() { KeyMapperTheme { @@ -112,7 +112,7 @@ private fun PreviewBackupFilePickerDialog() { } } -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewBackupFilePickerDialogEmpty() { KeyMapperTheme { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/ImportDialog.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/ImportDialog.kt index dfcb7ed9fb..146ec5ff3a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/ImportDialog.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/ImportDialog.kt @@ -55,7 +55,7 @@ fun ImportDialog( ) } -@Preview +@Preview(showSystemUi = true) @Composable private fun ImportDialogPreview() { KeyMapperTheme { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/SetupAccessibilityServiceDialog.kt b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/SetupAccessibilityServiceDialog.kt index a893facaeb..afe456f26a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/SetupAccessibilityServiceDialog.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/SetupAccessibilityServiceDialog.kt @@ -192,7 +192,7 @@ private fun RestrictedSettingText(modifier: Modifier = Modifier) { ) } -@Preview +@Preview(showSystemUi = true) @Composable private fun EnableAccessibilityServiceDialogPreview() { KeyMapperTheme { @@ -204,7 +204,7 @@ private fun EnableAccessibilityServiceDialogPreview() { } } -@Preview +@Preview(showSystemUi = true) @Composable private fun EnableAccessibilityServiceDialogRestrictedPreview() { KeyMapperTheme { @@ -216,7 +216,7 @@ private fun EnableAccessibilityServiceDialogRestrictedPreview() { } } -@Preview +@Preview(showSystemUi = true) @Composable private fun RestartAccessibilityServiceDialogPreview() { KeyMapperTheme { @@ -227,7 +227,7 @@ private fun RestartAccessibilityServiceDialogPreview() { } } -@Preview +@Preview(showSystemUi = true) @Composable private fun CantFindAccessibilitySettingsDialogPreview() { KeyMapperTheme { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutScreen.kt index 57dce3fb48..1448d46ea7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutScreen.kt @@ -438,7 +438,7 @@ private fun PreviewEmpty() { } } -@Preview +@Preview(showSystemUi = true) @Composable private fun ShortcutNameDialogPreview() { KeyMapperTheme { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt index cf17a0def7..7eb89a6997 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerDiscoverBottomSheet.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope @@ -60,7 +61,7 @@ fun TriggerDiscoverBottomSheet( } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewNoKeyRecordedComplete() { KeyMapperTheme { @@ -68,6 +69,7 @@ private fun PreviewNoKeyRecordedComplete() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) TriggerDiscoverBottomSheet( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index 5a90be8d5c..24c1b11264 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable @@ -403,7 +404,7 @@ private fun isVerticalCompactLayout(): Boolean { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewKeyEvent() { KeyMapperTheme { @@ -411,6 +412,7 @@ private fun PreviewKeyEvent() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) TriggerKeyOptionsBottomSheet( @@ -441,7 +443,7 @@ private fun PreviewKeyEvent() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview(heightDp = 400, widthDp = 300) +@Preview(heightDp = 400, widthDp = 300, showSystemUi = true) @Composable private fun PreviewKeyEventTiny() { KeyMapperTheme { @@ -449,6 +451,7 @@ private fun PreviewKeyEventTiny() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) TriggerKeyOptionsBottomSheet( @@ -479,7 +482,7 @@ private fun PreviewKeyEventTiny() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewEvdev() { KeyMapperTheme { @@ -487,6 +490,7 @@ private fun PreviewEvdev() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) TriggerKeyOptionsBottomSheet( @@ -505,7 +509,7 @@ private fun PreviewEvdev() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun AssistantPreview() { KeyMapperTheme { @@ -513,6 +517,7 @@ private fun AssistantPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) TriggerKeyOptionsBottomSheet( @@ -526,7 +531,7 @@ private fun AssistantPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun FloatingButtonPreview() { KeyMapperTheme { @@ -534,6 +539,7 @@ private fun FloatingButtonPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) TriggerKeyOptionsBottomSheet( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt index 1131d4f5a1..9530e30cc2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt @@ -32,6 +32,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -909,7 +910,7 @@ fun TriggerSetupBottomSheet( } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PowerButtonPreview() { KeyMapperTheme { @@ -917,6 +918,7 @@ private fun PowerButtonPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) PowerTriggerSetupBottomSheet( @@ -933,7 +935,7 @@ private fun PowerButtonPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun PowerButtonDisabledPreview() { KeyMapperTheme { @@ -941,6 +943,7 @@ private fun PowerButtonDisabledPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) PowerTriggerSetupBottomSheet( @@ -957,7 +960,7 @@ private fun PowerButtonDisabledPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun VolumeButtonPreview() { KeyMapperTheme { @@ -965,6 +968,7 @@ private fun VolumeButtonPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) VolumeTriggerSetupBottomSheet( @@ -982,7 +986,7 @@ private fun VolumeButtonPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun VolumeButtonDisabledPreview() { KeyMapperTheme { @@ -990,6 +994,7 @@ private fun VolumeButtonDisabledPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) VolumeTriggerSetupBottomSheet( @@ -1007,7 +1012,7 @@ private fun VolumeButtonDisabledPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun FingerprintGestureRequirementsMetPreview() { KeyMapperTheme { @@ -1015,6 +1020,7 @@ private fun FingerprintGestureRequirementsMetPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) FingerprintGestureSetupBottomSheet( @@ -1029,7 +1035,7 @@ private fun FingerprintGestureRequirementsMetPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun FingerprintGestureRequirementsNotMetPreview() { KeyMapperTheme { @@ -1037,6 +1043,7 @@ private fun FingerprintGestureRequirementsNotMetPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) FingerprintGestureSetupBottomSheet( @@ -1051,7 +1058,7 @@ private fun FingerprintGestureRequirementsNotMetPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun KeyboardButtonEnabledPreview() { KeyMapperTheme { @@ -1059,6 +1066,7 @@ private fun KeyboardButtonEnabledPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) KeyboardTriggerSetupBottomSheet( @@ -1076,7 +1084,7 @@ private fun KeyboardButtonEnabledPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun KeyboardButtonDisabledPreview() { KeyMapperTheme { @@ -1084,6 +1092,7 @@ private fun KeyboardButtonDisabledPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) KeyboardTriggerSetupBottomSheet( @@ -1101,7 +1110,7 @@ private fun KeyboardButtonDisabledPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun MouseButtonPreview() { KeyMapperTheme { @@ -1109,6 +1118,7 @@ private fun MouseButtonPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) MouseTriggerSetupBottomSheet( @@ -1125,7 +1135,7 @@ private fun MouseButtonPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun MouseButtonDisabledPreview() { KeyMapperTheme { @@ -1133,6 +1143,7 @@ private fun MouseButtonDisabledPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) MouseTriggerSetupBottomSheet( @@ -1149,7 +1160,7 @@ private fun MouseButtonDisabledPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun OtherButtonPreview() { KeyMapperTheme { @@ -1157,6 +1168,7 @@ private fun OtherButtonPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) OtherTriggerSetupBottomSheet( @@ -1174,7 +1186,7 @@ private fun OtherButtonPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun OtherButtonDisabledPreview() { KeyMapperTheme { @@ -1182,6 +1194,7 @@ private fun OtherButtonDisabledPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) OtherTriggerSetupBottomSheet( @@ -1199,7 +1212,7 @@ private fun OtherButtonDisabledPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun GamepadDpadPreview() { KeyMapperTheme { @@ -1207,6 +1220,7 @@ private fun GamepadDpadPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) GamepadTriggerSetupBottomSheet( @@ -1224,7 +1238,7 @@ private fun GamepadDpadPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun GamepadDpadDisabledPreview() { KeyMapperTheme { @@ -1232,6 +1246,7 @@ private fun GamepadDpadDisabledPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) GamepadTriggerSetupBottomSheet( @@ -1249,7 +1264,7 @@ private fun GamepadDpadDisabledPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun GamepadSimpleButtonsPreview() { KeyMapperTheme { @@ -1257,6 +1272,7 @@ private fun GamepadSimpleButtonsPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) GamepadTriggerSetupBottomSheet( @@ -1274,7 +1290,7 @@ private fun GamepadSimpleButtonsPreview() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(showSystemUi = true) @Composable private fun GamepadSimpleButtonsDisabledPreview() { KeyMapperTheme { @@ -1282,6 +1298,7 @@ private fun GamepadSimpleButtonsDisabledPreview() { skipPartiallyExpanded = true, positionalThreshold = { 0f }, velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, ) GamepadTriggerSetupBottomSheet( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/UnsavedChangesDialog.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/UnsavedChangesDialog.kt index b11c34673b..2e36b85efd 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/UnsavedChangesDialog.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/UnsavedChangesDialog.kt @@ -26,7 +26,7 @@ fun UnsavedChangesDialog(onDismiss: () -> Unit, onDiscardClick: () -> Unit) { ) } -@Preview +@Preview(showSystemUi = true) @Composable private fun Preview() { KeyMapperTheme { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SliderOptionText.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SliderOptionText.kt index 185f90c1f6..8bb1949548 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SliderOptionText.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SliderOptionText.kt @@ -238,7 +238,7 @@ private fun PreviewDefault() { } } -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewDialog() { KeyMapperTheme { From 7e533e5f58ab2a19af3df14b1194d68df110092a Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 15 Sep 2026 11:34:21 +0200 Subject: [PATCH 07/10] #701 feat: implement new design for action options bottom sheet and you can now name actions --- CHANGELOG.md | 10 + .../sds100/keymapper/base/actions/Action.kt | 15 + .../base/actions/ActionOptionsBottomSheet.kt | 809 ++++++++++-------- .../keymapper/base/actions/ActionUiHelper.kt | 20 +- .../base/actions/ConfigActionsUseCase.kt | 13 + .../base/actions/ConfigActionsViewModel.kt | 89 +- .../base/home/KeyMapListItemCreator.kt | 9 +- .../sds100/keymapper/base/keymaps/KeyMap.kt | 9 + .../base/utils/ui/compose/ComposeDialogs.kt | 138 +++ base/src/main/res/values/strings.xml | 26 + .../base/actions/ActionEntityMapperTest.kt | 28 + .../base/actions/ConfigActionsUseCaseTest.kt | 62 ++ .../keymapper/data/entities/ActionEntity.kt | 1 + 13 files changed, 845 insertions(+), 384 deletions(-) create mode 100644 base/src/test/java/io/github/sds100/keymapper/base/actions/ActionEntityMapperTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index e5dac9df9c..4aa47a6dd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,19 @@ +## [4.5.0](https://github.com/sds100/KeyMapper/releases/tag/v4.3.2) + +#### TO BE RELEASED + +## Added + +- #701 new action options bottom sheet design. +- #701 you can now give custom names to actions, to make it easier to identify key maps. + ## [4.4.0](https://github.com/sds100/KeyMapper/releases/tag/v4.3.2) #### 12 September 2026 ## Added +- Redesigned the action options with grouped repeat, burst, hold down and delay sections, and actions can now be given a custom name. - #2238 Add a "Reduce app killing" card to Expert Mode on Xiaomi/Redmi/Poco devices with steps to whitelist the app from battery optimisation, disable MIUI optimization, enable autostart, and adjust battery saver settings. - Target Android 17 SDK. - [#2227](https://github.com/keymapperorg/KeyMapper/issues/2227) Add a step to the Expert Mode setup wizard to grant local network access permission, required for ADB on Android 17+. diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/Action.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/Action.kt index 73197e3388..8ee8b43e7e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/Action.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/Action.kt @@ -29,6 +29,11 @@ data class Action( val multiplier: Int? = null, val delayBeforeNextAction: Int? = null, + + /** + * A name set by the user that is shown instead of the generated title. + */ + val customName: String? = null, ) object ActionEntityMapper { @@ -85,6 +90,11 @@ object ActionEntityMapper { .valueOrNull() ?.toIntOrNull() + val customName = entity.extras + .getData(ActionEntity.EXTRA_CUSTOM_NAME) + .valueOrNull() + ?.takeIf { it.isNotBlank() } + return Action( uid = entity.uid, data = data, @@ -98,6 +108,7 @@ object ActionEntityMapper { holdDownDuration = holdDownDuration, delayBeforeNextAction = delayBeforeNextAction, multiplier = multiplier, + customName = customName, ) } @@ -118,6 +129,10 @@ object ActionEntityMapper { add(EntityExtra(ActionEntity.EXTRA_MULTIPLIER, action.multiplier.toString())) } + if (!action.customName.isNullOrBlank()) { + add(EntityExtra(ActionEntity.EXTRA_CUSTOM_NAME, action.customName)) + } + if (keyMap.isHoldingDownActionBeforeRepeatingAllowed(action) && action.holdDownDuration != null ) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt index f156be2ceb..d6a453c6d7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt @@ -9,32 +9,44 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height 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.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.HelpOutline +import androidx.compose.material.icons.rounded.Abc +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Edit +import androidx.compose.material.icons.rounded.HourglassEmpty +import androidx.compose.material.icons.rounded.KeyboardDoubleArrowRight +import androidx.compose.material.icons.rounded.Repeat +import androidx.compose.material.icons.rounded.TouchApp import androidx.compose.material.icons.rounded.Warning +import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.base.R @@ -42,9 +54,10 @@ import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.SliderMaximums import io.github.sds100.keymapper.base.utils.ui.SliderMinimums import io.github.sds100.keymapper.base.utils.ui.SliderStepSizes -import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText +import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText +import io.github.sds100.keymapper.base.utils.ui.compose.TextFieldDialog import io.github.sds100.keymapper.base.utils.ui.compose.openUriSafe import kotlinx.coroutines.launch @@ -68,21 +81,53 @@ fun ActionOptionsBottomSheet( val ctx = LocalContext.current val helpUrl = stringResource(R.string.url_keymap_action_options_guide) val scope = rememberCoroutineScope() - - @Suppress("ktlint:standard:max-line-length") + var showCustomNameDialog by rememberSaveable { mutableStateOf(false) } + + if (showCustomNameDialog) { + TextFieldDialog( + title = stringResource(R.string.action_options_custom_name_dialog_title), + submitButtonText = stringResource(R.string.pos_save), + initialText = state.title, + onSubmitClick = { newText -> + callback.onCustomNameChanged(newText) + null + }, + onDismissRequest = { showCustomNameDialog = false }, + ) + } Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Spacer(modifier = Modifier.height(12.dp)) + Box(modifier = Modifier.fillMaxWidth()) { - Text( - modifier = Modifier.align(Alignment.Center), - textAlign = TextAlign.Center, - text = stringResource(R.string.action_options_title), - style = MaterialTheme.typography.headlineMedium, - ) + Row( + modifier = Modifier + .align(Alignment.Center) + .padding(horizontal = 56.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f, fill = false), + textAlign = TextAlign.Center, + text = state.title, + style = MaterialTheme.typography.headlineMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + softWrap = false, + ) + + IconButton(onClick = { showCustomNameDialog = true }) { + Icon( + imageVector = Icons.Rounded.Edit, + contentDescription = stringResource( + R.string.action_options_custom_name_dialog_title, + ), + ) + } + } IconButton( modifier = Modifier - .align(Alignment.TopEnd) + .align(Alignment.CenterEnd) .padding(horizontal = 8.dp), onClick = { uriHandler.openUriSafe(ctx, helpUrl) }, ) { @@ -93,190 +138,330 @@ fun ActionOptionsBottomSheet( } } + Spacer(modifier = Modifier.height(16.dp)) + + OptionsHeaderRow( + modifier = Modifier.padding(horizontal = 16.dp), + icon = state.actionTypeIcon, + text = stringResource(R.string.action_options_type_header, state.actionTypeTitle), + ) + Spacer(modifier = Modifier.height(8.dp)) - Row( + FlowRow( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { if (state.showEditButton) { - OutlinedButton( - modifier = Modifier.weight(1f), - onClick = callback::onEditClick, - ) { - Text(stringResource(R.string.button_edit_action)) + FilledTonalButton(onClick = callback::onEditClick) { + Text(stringResource(R.string.action_options_customize)) } - Spacer(Modifier.width(16.dp)) } - OutlinedButton(modifier = Modifier.weight(1f), onClick = callback::onReplaceClick) { - Text(stringResource(R.string.button_replace_action)) + FilledTonalButton(onClick = callback::onReplaceClick) { + Text(stringResource(R.string.action_options_swap)) } } if (state.showRepeat) { - Spacer(Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(16.dp)) + RepeatOptions(state = state, callback = callback) + } - CheckBoxText( - modifier = Modifier - .padding(horizontal = 8.dp) - .fillMaxWidth(), - text = stringResource(R.string.flag_repeat_actions), - isChecked = state.isRepeatChecked, - onCheckedChange = callback::onRepeatCheckedChange, - ) + Spacer(modifier = Modifier.height(16.dp)) + BurstOptions(state = state, callback = callback) + + if (state.showHoldDown) { + Spacer(modifier = Modifier.height(16.dp)) + HoldDownOptions(state = state, callback = callback) } - if (state.showRepeatRateWarning) { - Spacer(Modifier.height(8.dp)) + if (state.showDelayBeforeNextAction) { + Spacer(modifier = Modifier.height(16.dp)) + DelayOptions(state = state, callback = callback) + } - Row( - verticalAlignment = Alignment.CenterVertically, + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.End, + ) { + Button( + onClick = { + scope.launch { + sheetState.hide() + onDismissRequest() + } + }, ) { - Spacer(Modifier.width(16.dp)) Icon( - Icons.Rounded.Warning, + modifier = Modifier.size(ButtonDefaults.IconSize), + imageVector = Icons.Rounded.Check, contentDescription = null, - tint = MaterialTheme.colorScheme.error, ) - Spacer(Modifier.width(8.dp)) - Text( - modifier = Modifier.weight(1f), - text = stringResource(R.string.action_repeat_rate_warning), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.labelLarge, - ) - Spacer(Modifier.width(16.dp)) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.button_done)) } } - if (state.showRepeatRate) { - Spacer(Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(16.dp)) + } + } +} - SliderOptionText( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - title = stringResource(R.string.extra_label_repeat_rate), - defaultValue = state.defaultRepeatRate.toFloat(), - value = state.repeatRate.toFloat(), - valueText = { "${it.toInt()} ms" }, - onValueChange = { callback.onRepeatRateChanged(it.toInt()) }, - valueRange = 0f..SliderMaximums.ACTION_REPEAT_RATE.toFloat(), - stepSize = SliderStepSizes.ACTION_REPEAT_RATE, - ) - } +@Composable +private fun RepeatOptions(state: ActionOptionsState, callback: ActionOptionsBottomSheetCallback) { + Column { + OptionsHeaderRow( + modifier = Modifier.padding(horizontal = 16.dp), + icon = Icons.Rounded.Repeat, + text = stringResource(R.string.action_options_repeat_header), + ) - if (state.showRepeatLimit) { - Spacer(Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - val noLimitString = stringResource(R.string.button_slider_repeat_no_limit) + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.action_options_repeat_description), + style = MaterialTheme.typography.bodyMedium, + ) - SliderOptionText( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - title = stringResource(R.string.extra_label_repeat_limit), - defaultValue = state.defaultRepeatLimit.toFloat(), - value = state.repeatLimit.toFloat(), - valueText = { value -> - if (value.toInt() == Int.MAX_VALUE) { - noLimitString - } else { - "${value.toInt()}x" - } - }, - onValueChange = { callback.onRepeatLimitChanged(it.toInt()) }, - valueRange = 1f..SliderMaximums.ACTION_REPEAT_LIMIT.toFloat(), - stepSize = SliderStepSizes.ACTION_REPEAT_LIMIT, - ) - } + val selectedMode: RepeatMode? = if (state.isRepeatChecked) { + state.repeatMode + } else { + null + } - if (state.showRepeatDelay) { - Spacer(Modifier.height(8.dp)) + RadioButtonText( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth(), + text = stringResource(R.string.action_options_dont_repeat), + isSelected = selectedMode == null, + onSelected = { callback.onSelectRepeatMode(null) }, + ) - SliderOptionText( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - title = stringResource(R.string.extra_label_repeat_delay), - defaultValue = state.defaultRepeatDelay.toFloat(), - value = state.repeatDelay.toFloat(), - valueText = { "${it.toInt()} ms" }, - onValueChange = { callback.onRepeatDelayChanged(it.toInt()) }, - valueRange = 0f..SliderMaximums.ACTION_REPEAT_DELAY.toFloat(), - stepSize = SliderStepSizes.ACTION_REPEAT_DELAY, - ) - } + RadioButtonText( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth(), + text = stringResource(R.string.action_options_repeat_limit_reached), + isSelected = selectedMode == RepeatMode.LIMIT_REACHED, + isEnabled = state.allowedRepeatModes.contains(RepeatMode.LIMIT_REACHED), + onSelected = { callback.onSelectRepeatMode(RepeatMode.LIMIT_REACHED) }, + ) - if (state.allowedRepeatModes.isNotEmpty()) { - Spacer(Modifier.height(8.dp)) + val isUntilReleasedAllowed = + state.allowedRepeatModes.contains(RepeatMode.TRIGGER_RELEASED) + + RadioButtonText( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth(), + text = stringResource(R.string.action_options_repeat_until_released), + isSelected = selectedMode == RepeatMode.TRIGGER_RELEASED, + isEnabled = isUntilReleasedAllowed, + onSelected = { callback.onSelectRepeatMode(RepeatMode.TRIGGER_RELEASED) }, + ) + + if (!isUntilReleasedAllowed) { + Text( + // Align with the text of the radio button. + modifier = Modifier.padding(start = 48.dp, end = 16.dp), + text = stringResource(R.string.action_options_repeat_until_released_unavailable), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.labelMedium, + ) + } + + RadioButtonText( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth(), + text = stringResource(R.string.action_options_repeat_until_pressed_again), + isSelected = selectedMode == RepeatMode.TRIGGER_PRESSED_AGAIN, + isEnabled = state.allowedRepeatModes.contains(RepeatMode.TRIGGER_PRESSED_AGAIN), + onSelected = { callback.onSelectRepeatMode(RepeatMode.TRIGGER_PRESSED_AGAIN) }, + ) + + if (state.showRepeatRateWarning) { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Rounded.Warning, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + Spacer(modifier = Modifier.width(8.dp)) Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = stringResource(R.string.stop_repeating_dot_dot_dot), - style = MaterialTheme.typography.titleSmall, + text = stringResource(R.string.action_repeat_rate_warning), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.labelMedium, ) + } + } - FlowRow( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - if (state.allowedRepeatModes.contains(RepeatMode.TRIGGER_RELEASED)) { - RadioButtonText( - isSelected = state.repeatMode == RepeatMode.TRIGGER_RELEASED, - text = stringResource(R.string.stop_repeating_when_trigger_released), - onSelected = { - callback.onSelectRepeatMode(RepeatMode.TRIGGER_RELEASED) - }, - ) - } + if (state.showRepeatDelay) { + Spacer(modifier = Modifier.height(8.dp)) - if (state.allowedRepeatModes.contains(RepeatMode.TRIGGER_PRESSED_AGAIN)) { - RadioButtonText( - isSelected = state.repeatMode == RepeatMode.TRIGGER_PRESSED_AGAIN, - text = stringResource(R.string.stop_repeating_trigger_pressed_again), - onSelected = { - callback.onSelectRepeatMode(RepeatMode.TRIGGER_PRESSED_AGAIN) - }, - ) - } + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.action_options_repeat_delay), + defaultValue = state.defaultRepeatDelay.toFloat(), + value = state.repeatDelay.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { callback.onRepeatDelayChanged(it.toInt()) }, + valueRange = 0f..SliderMaximums.ACTION_REPEAT_DELAY.toFloat(), + stepSize = SliderStepSizes.ACTION_REPEAT_DELAY, + ) + } - if (state.allowedRepeatModes.contains(RepeatMode.LIMIT_REACHED)) { - RadioButtonText( - isSelected = state.repeatMode == RepeatMode.LIMIT_REACHED, - text = stringResource(R.string.stop_repeating_limit_reached), - onSelected = { callback.onSelectRepeatMode(RepeatMode.LIMIT_REACHED) }, - ) + if (state.showRepeatRate) { + Spacer(modifier = Modifier.height(8.dp)) + + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.action_options_repeat_rate), + defaultValue = state.defaultRepeatRate.toFloat(), + value = state.repeatRate.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { callback.onRepeatRateChanged(it.toInt()) }, + valueRange = 0f..SliderMaximums.ACTION_REPEAT_RATE.toFloat(), + stepSize = SliderStepSizes.ACTION_REPEAT_RATE, + ) + } + + if (state.showRepeatLimit) { + Spacer(modifier = Modifier.height(8.dp)) + + val noLimitString = stringResource(R.string.button_slider_repeat_no_limit) + + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.action_options_repeat_limit), + defaultValue = state.defaultRepeatLimit.toFloat(), + value = state.repeatLimit.toFloat(), + valueText = { value -> + if (value.toInt() == Int.MAX_VALUE) { + noLimitString + } else { + "${value.toInt()}x" } + }, + onValueChange = { callback.onRepeatLimitChanged(it.toInt()) }, + valueRange = 1f..SliderMaximums.ACTION_REPEAT_LIMIT.toFloat(), + stepSize = SliderStepSizes.ACTION_REPEAT_LIMIT, + ) + } + } +} - Spacer(Modifier.width(8.dp)) - } - } +@Composable +private fun BurstOptions(state: ActionOptionsState, callback: ActionOptionsBottomSheetCallback) { + Column { + OptionsHeaderRow( + modifier = Modifier.padding(horizontal = 16.dp), + icon = Icons.Rounded.KeyboardDoubleArrowRight, + text = stringResource(R.string.action_options_burst_header), + ) - if (state.showRepeat) { - Spacer(Modifier.height(8.dp)) - HorizontalDivider() - } + Spacer(modifier = Modifier.height(8.dp)) - if (state.showHoldDown) { - Spacer(Modifier.height(8.dp)) + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = if (state.showRepeat && state.isRepeatChecked) { + stringResource(R.string.action_options_burst_description_repeat) + } else { + stringResource(R.string.action_options_burst_description) + }, + style = MaterialTheme.typography.bodyMedium, + ) - CheckBoxText( - modifier = Modifier - .padding(horizontal = 8.dp) - .fillMaxWidth(), - text = stringResource(R.string.flag_hold_down), - isChecked = state.isHoldDownChecked, - onCheckedChange = callback::onHoldDownCheckedChange, - ) - } + Spacer(modifier = Modifier.height(8.dp)) + + val multiplierMin = SliderMinimums.ACTION_MULTIPLIER.toFloat() + val multiplierMax = SliderMaximums.ACTION_MULTIPLIER.toFloat() + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.action_options_burst_size), + defaultValue = state.defaultMultiplier.toFloat(), + value = state.multiplier.toFloat(), + valueText = { "${it.toInt()}x" }, + onValueChange = { callback.onMultiplierChanged(it.toInt()) }, + valueRange = multiplierMin..multiplierMax, + stepSize = SliderStepSizes.ACTION_MULTIPLIER, + ) + } +} + +@Composable +private fun HoldDownOptions(state: ActionOptionsState, callback: ActionOptionsBottomSheetCallback) { + val isRepeating = state.showRepeat && state.isRepeatChecked + + Column { + OptionsHeaderRow( + modifier = Modifier.padding(horizontal = 16.dp), + icon = Icons.Rounded.TouchApp, + text = stringResource(R.string.action_options_hold_down_header), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = if (isRepeating) { + stringResource(R.string.action_options_hold_down_description_repeat) + } else { + stringResource(R.string.action_options_hold_down_description) + }, + style = MaterialTheme.typography.bodyMedium, + ) + + val selectedMode: HoldDownMode? = if (state.isHoldDownChecked) { + state.holdDownMode + } else { + null + } + + RadioButtonText( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth(), + text = stringResource(R.string.action_options_dont_hold_down), + isSelected = selectedMode == null, + onSelected = { callback.onSelectHoldDownMode(null) }, + ) + + if (isRepeating) { + // When repeating, the action is held down for a period of time before each repeat. + RadioButtonText( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth(), + text = stringResource(R.string.action_options_hold_down_period), + isSelected = selectedMode != null, + onSelected = { callback.onSelectHoldDownMode(HoldDownMode.TRIGGER_RELEASED) }, + ) if (state.showHoldDownDuration) { - Spacer(Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) val holdDownDurationMin = SliderMinimums.ACTION_HOLD_DOWN_DURATION.toFloat() val holdDownDurationMax = SliderMaximums.ACTION_HOLD_DOWN_DURATION.toFloat() @@ -293,144 +478,128 @@ fun ActionOptionsBottomSheet( stepSize = SliderStepSizes.ACTION_HOLD_DOWN_DURATION, ) } + } else { + RadioButtonText( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth(), + text = stringResource(R.string.action_options_hold_down_until_released), + isSelected = selectedMode == HoldDownMode.TRIGGER_RELEASED, + onSelected = { callback.onSelectHoldDownMode(HoldDownMode.TRIGGER_RELEASED) }, + ) - if (state.showHoldDownMode) { - Spacer(Modifier.height(8.dp)) - - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = stringResource(R.string.hold_down_until_trigger_is_dot_dot_dot), - style = MaterialTheme.typography.titleSmall, - ) - - FlowRow( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - RadioButtonText( - isSelected = state.holdDownMode == HoldDownMode.TRIGGER_RELEASED, - text = stringResource(R.string.stop_holding_down_when_trigger_released), - onSelected = { - callback.onSelectHoldDownMode(HoldDownMode.TRIGGER_RELEASED) - }, - ) - - RadioButtonText( - isSelected = state.holdDownMode == HoldDownMode.TRIGGER_PRESSED_AGAIN, - text = stringResource(R.string.stop_holding_down_trigger_pressed_again), - onSelected = { - callback.onSelectHoldDownMode(HoldDownMode.TRIGGER_PRESSED_AGAIN) - }, - ) - - Spacer(Modifier.width(8.dp)) - } - } - - if (state.showHoldDown) { - Spacer(Modifier.height(8.dp)) - HorizontalDivider() - } - - if (state.showDelayBeforeNextAction) { - Spacer(Modifier.height(8.dp)) - - val delayBeforeNextActionMin = - SliderMinimums.DELAY_BEFORE_NEXT_ACTION.toFloat() - val delayBeforeNextActionMax = - SliderMaximums.DELAY_BEFORE_NEXT_ACTION.toFloat() - SliderOptionText( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - title = stringResource(R.string.extra_label_delay_before_next_action), - defaultValue = state.defaultDelayBeforeNextAction.toFloat(), - value = state.delayBeforeNextAction.toFloat(), - valueText = { "${it.toInt()} ms" }, - onValueChange = { callback.onDelayBeforeNextActionChanged(it.toInt()) }, - valueRange = delayBeforeNextActionMin..delayBeforeNextActionMax, - stepSize = SliderStepSizes.DELAY_BEFORE_NEXT_ACTION, - ) - } - - Spacer(Modifier.height(8.dp)) - - val actionMultiplierMin = SliderMinimums.ACTION_MULTIPLIER.toFloat() - val actionMultiplierMax = SliderMaximums.ACTION_MULTIPLIER.toFloat() - SliderOptionText( + RadioButtonText( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - title = if (state.showRepeat && state.isRepeatChecked) { - stringResource(R.string.extra_label_action_multiplier_with_repeat) - } else { - stringResource(R.string.extra_label_action_multiplier) + .padding(horizontal = 8.dp) + .fillMaxWidth(), + text = stringResource(R.string.action_options_hold_down_until_pressed_again), + isSelected = selectedMode == HoldDownMode.TRIGGER_PRESSED_AGAIN, + onSelected = { + callback.onSelectHoldDownMode(HoldDownMode.TRIGGER_PRESSED_AGAIN) }, - defaultValue = state.defaultMultiplier.toFloat(), - value = state.multiplier.toFloat(), - valueText = { "${it.toInt()}x" }, - onValueChange = { callback.onMultiplierChanged(it.toInt()) }, - valueRange = actionMultiplierMin..actionMultiplierMax, - stepSize = SliderStepSizes.ACTION_MULTIPLIER, ) + } + } +} - Spacer(Modifier.height(8.dp)) - - HorizontalDivider() - - Spacer(Modifier.height(8.dp)) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) { - Spacer(Modifier.weight(0.5f)) - Spacer(Modifier.width(16.dp)) - - FilledTonalButton( - modifier = Modifier.weight(0.5f), - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - onClick = { - scope.launch { - sheetState.hide() - onDismissRequest() - } - }, - ) { - Spacer(Modifier.width(16.dp)) - Text(stringResource(R.string.button_done)) - Spacer(Modifier.width(16.dp)) - } - } +@Composable +private fun DelayOptions(state: ActionOptionsState, callback: ActionOptionsBottomSheetCallback) { + Column { + OptionsHeaderRow( + modifier = Modifier.padding(horizontal = 16.dp), + icon = Icons.Rounded.HourglassEmpty, + text = stringResource(R.string.action_options_delay_header), + ) - Spacer(Modifier.height(16.dp)) - } + Spacer(modifier = Modifier.height(8.dp)) + + val delayMin = SliderMinimums.DELAY_BEFORE_NEXT_ACTION.toFloat() + val delayMax = SliderMaximums.DELAY_BEFORE_NEXT_ACTION.toFloat() + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.extra_label_delay_before_next_action), + defaultValue = state.defaultDelayBeforeNextAction.toFloat(), + value = state.delayBeforeNextAction.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { callback.onDelayBeforeNextActionChanged(it.toInt()) }, + valueRange = delayMin..delayMax, + stepSize = SliderStepSizes.DELAY_BEFORE_NEXT_ACTION, + ) } } interface ActionOptionsBottomSheetCallback { fun onEditClick() = run { } fun onReplaceClick() = run { } - fun onRepeatCheckedChange(checked: Boolean) = run { } - fun onSelectRepeatMode(repeatMode: RepeatMode) = run { } + fun onCustomNameChanged(name: String) = run { } + + /** + * @param repeatMode null if the action should not repeat. + */ + fun onSelectRepeatMode(repeatMode: RepeatMode?) = run { } fun onRepeatRateChanged(rate: Int) = run { } fun onRepeatLimitChanged(limit: Int) = run { } fun onRepeatDelayChanged(delay: Int) = run { } - fun onHoldDownCheckedChange(checked: Boolean) = run { } + + /** + * @param holdDownMode null if the action should not be held down. + */ + fun onSelectHoldDownMode(holdDownMode: HoldDownMode?) = run { } fun onHoldDownDurationChanged(duration: Int) = run { } - fun onSelectHoldDownMode(holdDownMode: HoldDownMode) = run { } fun onDelayBeforeNextActionChanged(delay: Int) = run { } fun onMultiplierChanged(multiplier: Int) = run { } } +private val previewState = ActionOptionsState( + title = "Input KEYCODE_0", + actionTypeTitle = "Input key event", + actionTypeIcon = Icons.Rounded.Abc, + + showEditButton = true, + showRepeat = true, + isRepeatChecked = true, + showRepeatRateWarning = false, + + showRepeatRate = true, + repeatRate = 400, + defaultRepeatRate = 500, + + showRepeatDelay = true, + repeatDelay = 400, + defaultRepeatDelay = 400, + + showRepeatLimit = true, + repeatLimit = Int.MAX_VALUE, + defaultRepeatLimit = Int.MAX_VALUE, + + allowedRepeatModes = setOf( + RepeatMode.TRIGGER_RELEASED, + RepeatMode.LIMIT_REACHED, + RepeatMode.TRIGGER_PRESSED_AGAIN, + ), + repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN, + + showHoldDown = true, + isHoldDownChecked = true, + + showHoldDownDuration = true, + holdDownDuration = 400, + defaultHoldDownDuration = 400, + + holdDownMode = HoldDownMode.TRIGGER_RELEASED, + + showDelayBeforeNextAction = true, + delayBeforeNextAction = 10000, + defaultDelayBeforeNextAction = 5000, + + multiplier = 4, + defaultMultiplier = 1, +) + @OptIn(ExperimentalMaterial3Api::class) -@Preview(heightDp = 1000, showSystemUi = true) +@Preview(heightDp = 1600, showSystemUi = true) @Composable private fun Preview() { KeyMapperTheme { @@ -443,57 +612,16 @@ private fun Preview() { ActionOptionsBottomSheet( sheetState = sheetState, - state = ActionOptionsState( - showEditButton = true, - showRepeat = true, - isRepeatChecked = true, - showRepeatRateWarning = true, - - showRepeatRate = true, - repeatRate = 400, - defaultRepeatRate = 500, - - showRepeatDelay = true, - repeatDelay = 400, - defaultRepeatDelay = 400, - - showRepeatLimit = true, - repeatLimit = Int.MAX_VALUE, - defaultRepeatLimit = Int.MAX_VALUE, - - allowedRepeatModes = setOf( - RepeatMode.TRIGGER_RELEASED, - RepeatMode.LIMIT_REACHED, - RepeatMode.TRIGGER_PRESSED_AGAIN, - ), - repeatMode = RepeatMode.TRIGGER_RELEASED, - - showHoldDown = true, - isHoldDownChecked = false, - - showHoldDownDuration = true, - holdDownDuration = 400, - defaultHoldDownDuration = 400, - - showHoldDownMode = true, - holdDownMode = HoldDownMode.TRIGGER_PRESSED_AGAIN, - - showDelayBeforeNextAction = true, - delayBeforeNextAction = 10000, - defaultDelayBeforeNextAction = 5000, - - multiplier = 4, - defaultMultiplier = 1, - ), + state = previewState, callback = object : ActionOptionsBottomSheetCallback {}, ) } } @OptIn(ExperimentalMaterial3Api::class) -@Preview(showSystemUi = true) +@Preview(heightDp = 1600, showSystemUi = true) @Composable -private fun PreviewNoEditButton() { +private fun PreviewUntilReleasedUnavailable() { KeyMapperTheme { val sheetState = SheetState( skipPartiallyExpanded = true, @@ -504,47 +632,42 @@ private fun PreviewNoEditButton() { ActionOptionsBottomSheet( sheetState = sheetState, - state = ActionOptionsState( + state = previewState.copy( showEditButton = false, - showRepeat = true, - isRepeatChecked = true, showRepeatRateWarning = true, - - showRepeatRate = true, - repeatRate = 400, - defaultRepeatRate = 500, - - showRepeatDelay = true, - repeatDelay = 400, - defaultRepeatDelay = 400, - - showRepeatLimit = true, - repeatLimit = 10, - defaultRepeatLimit = Int.MAX_VALUE, - allowedRepeatModes = setOf( - RepeatMode.TRIGGER_RELEASED, RepeatMode.LIMIT_REACHED, RepeatMode.TRIGGER_PRESSED_AGAIN, ), - repeatMode = RepeatMode.TRIGGER_RELEASED, - - showHoldDown = true, isHoldDownChecked = false, + ), + callback = object : ActionOptionsBottomSheetCallback {}, + ) + } +} - showHoldDownDuration = true, - holdDownDuration = 400, - defaultHoldDownDuration = 400, +@OptIn(ExperimentalMaterial3Api::class) +@Preview(heightDp = 1600, showSystemUi = true) +@Composable +private fun PreviewNotRepeating() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + positionalThreshold = { 0f }, + velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, + skipHiddenState = true, + ) - showHoldDownMode = true, + ActionOptionsBottomSheet( + sheetState = sheetState, + state = previewState.copy( + isRepeatChecked = false, + showRepeatRate = false, + showRepeatDelay = false, + showRepeatLimit = false, + showHoldDownDuration = false, holdDownMode = HoldDownMode.TRIGGER_PRESSED_AGAIN, - - showDelayBeforeNextAction = true, - delayBeforeNextAction = 10000, - defaultDelayBeforeNextAction = 5000, - - multiplier = 4, - defaultMultiplier = 1, ), callback = object : ActionOptionsBottomSheetCallback {}, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt index cebf056bd6..067b9bafba 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt @@ -31,6 +31,14 @@ class ActionUiHelper( ) : ResourceProvider by resourceProvider, DisplayActionUseCase by displayActionUseCase { + /** + * @return the custom name of the action if it has one, otherwise the generated title. + */ + fun getTitle(action: Action, showDeviceDescriptors: Boolean): String { + return action.customName?.takeIf { it.isNotBlank() } + ?: getTitle(action.data, showDeviceDescriptors) + } + fun getTitle(action: ActionData, showDeviceDescriptors: Boolean): String = when (action) { is ActionData.App -> getAppName(action.packageName).handle( @@ -712,13 +720,17 @@ class ActionUiHelper( R.string.action_expand_notification_drawer, ) - ActionData.StatusBar.ExpandQuickSettings -> getString(R.string.action_expand_quick_settings) + ActionData.StatusBar.ExpandQuickSettings -> getString( + R.string.action_expand_quick_settings, + ) ActionData.StatusBar.ToggleNotifications -> getString( R.string.action_toggle_notification_drawer, ) - ActionData.StatusBar.ToggleQuickSettings -> getString(R.string.action_toggle_quick_settings) + ActionData.StatusBar.ToggleQuickSettings -> getString( + R.string.action_toggle_quick_settings, + ) ActionData.ToggleKeyboard -> getString(R.string.action_toggle_keyboard) @@ -734,7 +746,9 @@ class ActionUiHelper( ActionData.Wifi.Toggle -> getString(R.string.action_toggle_wifi) - ActionData.DismissAllNotifications -> getString(R.string.action_dismiss_all_notifications) + ActionData.DismissAllNotifications -> getString( + R.string.action_dismiss_all_notifications, + ) ActionData.DismissLastNotification -> getString( R.string.action_dismiss_most_recent_notification, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt index 96aee5ec15..a6b623a275 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt @@ -183,6 +183,12 @@ class ConfigActionsUseCaseImpl @Inject constructor( } } + override fun setActionCustomName(uid: String, customName: String?) { + setActionOption(uid) { action -> + action.copy(customName = customName?.trim()?.takeIf { it.isNotEmpty() }) + } + } + private suspend fun getActionShortcuts(json: String?): List { if (json == null) { return emptyList() @@ -222,6 +228,12 @@ class ConfigActionsUseCaseImpl @Inject constructor( repeat = true } + // Actions repeat until the trigger is released by default so do not repeat if + // the release of the trigger can not be detected. + if (!keyMap.isRepeatUntilReleasedAllowed()) { + repeat = false + } + if (data is ActionData.AnswerCall) { configConstraints.addConstraint(ConstraintData.PhoneRinging) } @@ -269,6 +281,7 @@ interface ConfigActionsUseCase : GetDefaultKeyMapOptionsUseCase { fun setActionData(uid: String, data: ActionData) fun setActionMultiplier(uid: String, multiplier: Int) fun setDelayBeforeNextAction(uid: String, delay: Int) + fun setActionCustomName(uid: String, customName: String?) fun setActionRepeatRate(uid: String, repeatRate: Int) fun setActionRepeatLimit(uid: String, repeatLimit: Int) fun setActionStopRepeatingWhenTriggerPressedAgain(uid: String) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt index b606478b92..709e404bac 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.actions +import androidx.compose.ui.graphics.vector.ImageVector import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -213,10 +214,6 @@ class ConfigActionsViewModel @Inject constructor( } } - override fun onRepeatCheckedChange(checked: Boolean) { - actionOptionsUid.value?.let { uid -> config.setActionRepeatEnabled(uid, checked) } - } - override fun onRepeatLimitChanged(limit: Int) { actionOptionsUid.value?.let { uid -> config.setActionRepeatLimit(uid, limit) } } @@ -229,21 +226,23 @@ class ConfigActionsViewModel @Inject constructor( actionOptionsUid.value?.let { uid -> config.setActionRepeatDelay(uid, delay) } } - override fun onHoldDownCheckedChange(checked: Boolean) { - actionOptionsUid.value?.let { uid -> config.setActionHoldDownEnabled(uid, checked) } - } - override fun onHoldDownDurationChanged(duration: Int) { actionOptionsUid.value?.let { uid -> config.setActionHoldDownDuration(uid, duration) } } - override fun onSelectHoldDownMode(holdDownMode: HoldDownMode) { - actionOptionsUid.value?.let { uid -> - config.setActionStopHoldingDownWhenTriggerPressedAgain( - uid, - holdDownMode == HoldDownMode.TRIGGER_PRESSED_AGAIN, - ) + override fun onSelectHoldDownMode(holdDownMode: HoldDownMode?) { + val uid = actionOptionsUid.value ?: return + + if (holdDownMode == null) { + config.setActionHoldDownEnabled(uid, false) + return } + + config.setActionHoldDownEnabled(uid, true) + config.setActionStopHoldingDownWhenTriggerPressedAgain( + uid, + holdDownMode == HoldDownMode.TRIGGER_PRESSED_AGAIN, + ) } override fun onDelayBeforeNextActionChanged(delay: Int) { @@ -254,18 +253,37 @@ class ConfigActionsViewModel @Inject constructor( actionOptionsUid.value?.let { uid -> config.setActionMultiplier(uid, multiplier) } } - override fun onSelectRepeatMode(repeatMode: RepeatMode) { - actionOptionsUid.value?.let { uid -> - when (repeatMode) { - RepeatMode.TRIGGER_RELEASED -> config.setActionStopRepeatingWhenTriggerReleased( - uid, - ) + override fun onSelectRepeatMode(repeatMode: RepeatMode?) { + val uid = actionOptionsUid.value ?: return + + if (repeatMode == null) { + config.setActionRepeatEnabled(uid, false) + return + } - RepeatMode.LIMIT_REACHED -> config.setActionStopRepeatingWhenLimitReached(uid) + config.setActionRepeatEnabled(uid, true) - RepeatMode.TRIGGER_PRESSED_AGAIN -> - config.setActionStopRepeatingWhenTriggerPressedAgain(uid) - } + when (repeatMode) { + RepeatMode.TRIGGER_RELEASED -> config.setActionStopRepeatingWhenTriggerReleased(uid) + + RepeatMode.LIMIT_REACHED -> config.setActionStopRepeatingWhenLimitReached(uid) + + RepeatMode.TRIGGER_PRESSED_AGAIN -> + config.setActionStopRepeatingWhenTriggerPressedAgain(uid) + } + } + + override fun onCustomNameChanged(name: String) { + val uid = actionOptionsUid.value ?: return + + viewModelScope.launch { + val actionData = getActionData(uid) ?: return@launch + val showDeviceDescriptors = displayAction.showDeviceDescriptors.first() + val generatedTitle = uiHelper.getTitle(actionData, showDeviceDescriptors) + + // Submitting the generated title removes the custom name. + val customName = name.trim().takeIf { it.isNotEmpty() && it != generatedTitle } + config.setActionCustomName(uid, customName) } } @@ -318,7 +336,7 @@ class ConfigActionsViewModel @Inject constructor( val multiplier = action.multiplier "${multiplier}x ${uiHelper.getTitle(action.data, showDeviceDescriptors)}" } else { - uiHelper.getTitle(action.data, showDeviceDescriptors) + uiHelper.getTitle(action, showDeviceDescriptors) } val icon: ComposeIconInfo = uiHelper.getIcon(action.data) @@ -382,12 +400,17 @@ class ConfigActionsViewModel @Inject constructor( val allowedRepeatModes = mutableSetOf() - if (keyMap.isChangingRepeatModeAllowed(action)) { - allowedRepeatModes.add(RepeatMode.TRIGGER_RELEASED) + if (keyMap.isRepeatingActionsAllowed()) { + if (keyMap.isRepeatUntilReleasedAllowed()) { + allowedRepeatModes.add(RepeatMode.TRIGGER_RELEASED) + } + allowedRepeatModes.add(RepeatMode.TRIGGER_PRESSED_AGAIN) allowedRepeatModes.add(RepeatMode.LIMIT_REACHED) } + val showDeviceDescriptors = displayAction.showDeviceDescriptors.first() + val defaultRepeatRate = config.defaultRepeatRate.first() val defaultRepeatDelay = config.defaultRepeatDelay.first() val defaultHoldDownDuration = config.defaultHoldDownDuration.first() @@ -404,6 +427,10 @@ class ConfigActionsViewModel @Inject constructor( keyMap.trigger.keys.any { it is EvdevTriggerKey } return ActionOptionsState( + title = uiHelper.getTitle(action, showDeviceDescriptors), + actionTypeTitle = getString(ActionUtils.getTitle(action.data.id)), + actionTypeIcon = ActionUtils.getComposeIcon(action.data.id), + showEditButton = action.data.isEditable(), showRepeat = keyMap.isRepeatingActionsAllowed(), @@ -432,9 +459,6 @@ class ConfigActionsViewModel @Inject constructor( holdDownDuration = action.holdDownDuration ?: defaultHoldDownDuration, defaultHoldDownDuration = defaultHoldDownDuration, - showHoldDownMode = keyMap.isStopHoldingDownActionWhenTriggerPressedAgainAllowed( - action, - ), holdDownMode = if (action.stopHoldDownWhenTriggerPressedAgain) { HoldDownMode.TRIGGER_PRESSED_AGAIN } else { @@ -475,6 +499,10 @@ data class ActionListItemModel( ) data class ActionOptionsState( + val title: String, + val actionTypeTitle: String, + val actionTypeIcon: ImageVector, + val showEditButton: Boolean, val showRepeat: Boolean, @@ -503,7 +531,6 @@ data class ActionOptionsState( val holdDownDuration: Int, val defaultHoldDownDuration: Int, - val showHoldDownMode: Boolean, val holdDownMode: HoldDownMode, val showDelayBeforeNextAction: Boolean, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt index 929038eb47..a9f3d57ebd 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt @@ -125,14 +125,9 @@ class KeyMapListItemCreator( for (action in keyMap.actionList) { val actionTitle: String = if (action.multiplier != null) { - "${action.multiplier}x ${ - actionUiHelper.getTitle( - action.data, - showDeviceDescriptors, - ) - }" + "${action.multiplier}x ${actionUiHelper.getTitle(action, showDeviceDescriptors)}" } else { - actionUiHelper.getTitle(action.data, showDeviceDescriptors) + actionUiHelper.getTitle(action, showDeviceDescriptors) } val chipText = buildString { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt index f75bbf55c6..9ea161792b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt @@ -9,6 +9,8 @@ import io.github.sds100.keymapper.base.constraints.ConstraintEntityMapper import io.github.sds100.keymapper.base.constraints.ConstraintModeEntityMapper import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.detection.KeyMapAlgorithm +import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey +import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerEntityMapper import io.github.sds100.keymapper.base.trigger.TriggerKey @@ -57,6 +59,13 @@ data class KeyMap( fun isChangingRepeatLimitAllowed(action: Action): Boolean = action.repeat && isRepeatingActionsAllowed() + /** + * The release of assistant and fingerprint gesture triggers can not be detected. + */ + fun isRepeatUntilReleasedAllowed(): Boolean = trigger.keys.none { + it is AssistantTriggerKey || it is FingerprintTriggerKey + } + fun isStopHoldingDownActionWhenTriggerPressedAgainAllowed(action: Action): Boolean = action.holdDown && !action.repeat && isHoldingDownActionAllowed(action) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeDialogs.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeDialogs.kt index f05cf9f5f5..01e09b1707 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeDialogs.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeDialogs.kt @@ -10,16 +10,130 @@ 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.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import kotlinx.coroutines.launch + +@Composable +fun TextFieldDialog( + title: String, + submitButtonText: String, + initialText: String, + /** + * Returns an error message. + */ + onSubmitClick: suspend (newText: String) -> String? = { null }, + onDismissRequest: () -> Unit = {}, +) { + val scope = rememberCoroutineScope() + val keyboardController = LocalSoftwareKeyboardController.current + val focusRequester = remember { FocusRequester() } + var textFieldValue: TextFieldValue by remember { + mutableStateOf( + TextFieldValue( + text = initialText, + selection = TextRange(initialText.length), + ), + ) + } + var error: String? by remember { mutableStateOf(null) } + val isError by remember { derivedStateOf { textFieldValue.text.isBlank() || error != null } } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + keyboardController?.show() + } + + val submit = { + scope.launch { + error = onSubmitClick(textFieldValue.text) + + if (error == null) { + onDismissRequest() + } + } + Unit + } + + CustomDialog( + title = title, + confirmButton = { + TextButton( + onClick = submit, + enabled = !isError, + ) { + Text(submitButtonText) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.neg_cancel)) + } + }, + onDismissRequest = onDismissRequest, + ) { + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .focusRequester(focusRequester), + value = textFieldValue, + onValueChange = { + error = null + textFieldValue = it + }, + singleLine = true, + maxLines = 1, + isError = isError, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions( + onDone = { + if (!isError) { + submit() + } + }, + ), + supportingText = { + when { + error != null -> Text(error!!) + + textFieldValue.text.isBlank() -> { + Text(stringResource(R.string.error_cant_be_empty)) + } + } + }, + ) + } +} @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -94,3 +208,27 @@ fun CustomDialogContent( } } } + +@Preview(widthDp = 800, heightDp = 400) +@Composable +private fun TextFieldDialogPreview() { + KeyMapperTheme { + TextFieldDialog( + title = "Title", + submitButtonText = "Submit", + initialText = "Some dialog text", + ) + } +} + +@Preview(widthDp = 800, heightDp = 400) +@Composable +private fun TextFieldDialogEmptyPreview() { + KeyMapperTheme { + TextFieldDialog( + title = "Title", + submitButtonText = "Submit", + initialText = "", + ) + } +} diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index bb6384e0b9..ead561d3d3 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1808,6 +1808,32 @@ Cancel Run these actions when you trigger the key map: Repeating under 20 ms can lag! Adjust for your device and game. + Action name + %s options + Customize + Swap action + Repeat options + This action can be set to repeat multiple times for one trigger. + Don\'t repeat + Repeat a fixed number of times + Repeat continuously until the trigger is released + Not available for side key, assistant or fingerprint gesture triggers! + Repeat continuously until the trigger is pressed again + Before repeating, wait… + Then repeat this action every… + Maximum repeat limit + Burst options + The action can be performed multiple times in a row in a rapid-fire burst. + Every time the action repeats, it can be performed multiple times in a row in a rapid-fire burst. + Burst size + Hold down options + This action can be held down instead of being pressed and released immediately. + This action can be set to be held down for a period of time, regardless of how long you press the trigger. + Don\'t hold down + Hold down for a period of time + Hold down until the trigger is released + Hold down until the trigger is pressed again + Delay options Choose side Brightness diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/ActionEntityMapperTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/ActionEntityMapperTest.kt new file mode 100644 index 0000000000..ab32e2e5f5 --- /dev/null +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/ActionEntityMapperTest.kt @@ -0,0 +1,28 @@ +package io.github.sds100.keymapper.base.actions + +import io.github.sds100.keymapper.base.keymaps.KeyMap +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.nullValue +import org.junit.Test + +class ActionEntityMapperTest { + + @Test + fun `save and restore the custom name of an action`() { + val action = Action(data = ActionData.ConsumeKeyEvent, customName = "My action") + + val entity = ActionEntityMapper.toEntity(KeyMap(actionList = listOf(action))).single() + + assertThat(ActionEntityMapper.fromEntity(entity)?.customName, `is`("My action")) + } + + @Test + fun `do not save a blank custom name`() { + val action = Action(data = ActionData.ConsumeKeyEvent, customName = " ") + + val entity = ActionEntityMapper.toEntity(KeyMap(actionList = listOf(action))).single() + + assertThat(ActionEntityMapper.fromEntity(entity)?.customName, nullValue()) + } +} diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt index 96309f4a02..53a6cf3288 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt @@ -6,6 +6,8 @@ import io.github.sds100.keymapper.base.constraints.ConstraintData import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapStateImpl import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey +import io.github.sds100.keymapper.base.trigger.AssistantTriggerType import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.utils.singleKeyTrigger @@ -17,6 +19,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.nullValue import org.junit.Before import org.junit.Test import org.mockito.kotlin.any @@ -73,6 +76,65 @@ class ConfigActionsUseCaseTest { assertThat(actionList[0].repeat, `is`(false)) } + @Test + fun `add key event action with assistant trigger, do not enable repeat`() = + runTest(testDispatcher) { + configKeyMapState.setKeyMap( + KeyMap( + trigger = singleKeyTrigger( + AssistantTriggerKey( + type = AssistantTriggerType.ANY, + clickType = ClickType.SHORT_PRESS, + ), + ), + ), + ) + + useCase.addAction(ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_W)) + + val actionList = useCase.keyMap.value.dataOrNull()!!.actionList + assertThat(actionList[0].repeat, `is`(false)) + } + + @Test + fun `add key event action with key event trigger, repeat until released`() = + runTest(testDispatcher) { + configKeyMapState.setKeyMap( + KeyMap( + trigger = singleKeyTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + ), + ), + ), + ) + + useCase.addAction(ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_W)) + + val action = useCase.keyMap.value.dataOrNull()!!.actionList[0] + assertThat(action.repeat, `is`(true)) + assertThat(action.repeatMode, `is`(RepeatMode.TRIGGER_RELEASED)) + } + + @Test + fun `set custom name, trim the name and remove it when blank`() = runTest(testDispatcher) { + configKeyMapState.setKeyMap(KeyMap()) + useCase.addAction(ActionData.ConsumeKeyEvent) + + val uid = useCase.keyMap.value.dataOrNull()!!.actionList[0].uid + + useCase.setActionCustomName(uid, " My action ") + assertThat( + useCase.keyMap.value.dataOrNull()!!.actionList[0].customName, + `is`("My action"), + ) + + useCase.setActionCustomName(uid, " ") + assertThat(useCase.keyMap.value.dataOrNull()!!.actionList[0].customName, nullValue()) + } + /** * Issue #852. Add a phone ringing constraint when you add an action * to answer a phone call. diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt index 7d20d6dca6..ff36035c6e 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt @@ -150,6 +150,7 @@ data class ActionEntity( const val EXTRA_DELAY_BEFORE_NEXT_ACTION = "extra_delay_before_next_action" const val EXTRA_HOLD_DOWN_DURATION = "extra_hold_down_duration" const val EXTRA_REPEAT_LIMIT = "extra_repeat_limit" + const val EXTRA_CUSTOM_NAME = "extra_custom_name" const val EXTRA_SETTING_VALUE = "extra_setting_value" const val EXTRA_SETTING_TYPE = "extra_setting_type" From 1f9fa58639d2a31caf43416bd4bd197e59710ff5 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 15 Sep 2026 21:40:15 +0200 Subject: [PATCH 08/10] #701 feat: actions screen is overhauled --- CHANGELOG.md | 5 +- .../sds100/keymapper/base/actions/Action.kt | 10 + .../keymapper/base/actions/ActionListItem.kt | 634 ++++++++++++------ .../base/actions/ActionListItemModel.kt | 37 + .../base/actions/ActionOptionsBottomSheet.kt | 39 -- .../base/actions/ActionOptionsState.kt | 42 ++ .../keymapper/base/actions/ActionUiHelper.kt | 144 ++-- .../keymapper/base/actions/ActionsScreen.kt | 359 ++++++++-- .../base/actions/ConfigActionsUseCase.kt | 5 + .../base/actions/ConfigActionsViewModel.kt | 153 ++--- .../base/detection/DetectKeyMapsUseCase.kt | 5 +- .../base/home/KeyMapListItemCreator.kt | 12 +- .../keymapper/base/home/KeyMapListScreen.kt | 11 +- .../base/utils/ui/compose/CompactChip.kt | 2 + .../base/utils/ui/compose/ComposeChipModel.kt | 4 + .../base/utils/ui/compose/ComposeDialogs.kt | 1 - .../base/utils/ui/compose/SliderOptionText.kt | 40 +- base/src/main/res/values/strings.xml | 13 +- .../base/actions/ActionEntityMapperTest.kt | 29 + .../base/actions/ConfigActionsUseCaseTest.kt | 18 + .../base/keymaps/DetectKeyMapsUseCaseTest.kt | 29 + .../keymapper/data/entities/ActionEntity.kt | 6 + 22 files changed, 1116 insertions(+), 482 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItemModel.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsState.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a2e9e5fed..7579280dc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## [4.5.0](https://github.com/sds100/KeyMapper/releases/tag/v4.3.2) +## [4.5.0](https://github.com/sds100/KeyMapper/releases/tag/v4.5.0) #### TO BE RELEASED @@ -6,6 +6,9 @@ - #701 new action options bottom sheet design. - #701 you can now give custom names to actions, to make it easier to identify key maps. +- #701 redesigned the action list with expandable action cards that show each option and error. +- #701 actions can be turned off individually without deleting them. +- #701 the delay before the next action is now set with a button between actions instead of in the action options. ## [4.4.1](https://github.com/sds100/KeyMapper/releases/tag/v4.4.1) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/Action.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/Action.kt index 8ee8b43e7e..ca467520b1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/Action.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/Action.kt @@ -34,6 +34,11 @@ data class Action( * A name set by the user that is shown instead of the generated title. */ val customName: String? = null, + + /** + * Disabled actions are not performed when the key map is triggered. + */ + val isEnabled: Boolean = true, ) object ActionEntityMapper { @@ -109,6 +114,7 @@ object ActionEntityMapper { delayBeforeNextAction = delayBeforeNextAction, multiplier = multiplier, customName = customName, + isEnabled = !entity.flags.hasFlag(ActionEntity.ACTION_FLAG_DISABLED), ) } @@ -200,6 +206,10 @@ object ActionEntityMapper { flags = flags.withFlag(ActionEntity.ACTION_FLAG_HOLD_DOWN) } + if (!action.isEnabled) { + flags = flags.withFlag(ActionEntity.ACTION_FLAG_DISABLED) + } + return@map ActionEntity( type = base.type, data = base.data, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt index c8493d9604..d7a15df335 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt @@ -1,70 +1,117 @@ package io.github.sds100.keymapper.base.actions +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues 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.material.icons.Icons import androidx.compose.material.icons.outlined.ClearAll +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.PlayArrow import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material.icons.rounded.Add -import androidx.compose.material.icons.rounded.ArrowDownward -import androidx.compose.material.icons.rounded.Clear import androidx.compose.material.icons.rounded.DragHandle -import androidx.compose.material.icons.rounded.PlayCircleOutline +import androidx.compose.material.icons.rounded.Edit +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material.icons.rounded.KeyboardArrowUp +import androidx.compose.material.icons.rounded.KeyboardDoubleArrowRight +import androidx.compose.material.icons.rounded.Repeat +import androidx.compose.material.icons.rounded.TouchApp +import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.accompanist.drawablepainter.rememberDrawablePainter import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.LinkType +import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.base.utils.ui.compose.DragDropState import io.github.sds100.keymapper.base.utils.ui.drawable +/** + * The Material 3 alpha for disabled content. + */ +private const val DISABLED_ALPHA = 0.38f + +private const val EXPAND_ANIMATION_DURATION = 300 + +/** + * The fade and size change share the same duration and easing so the content does not disappear + * before the card has finished changing shape. + */ +private val expandTransition: EnterTransition = + fadeIn(tween(EXPAND_ANIMATION_DURATION, easing = FastOutSlowInEasing)) + + expandVertically( + animationSpec = tween(EXPAND_ANIMATION_DURATION, easing = FastOutSlowInEasing), + expandFrom = Alignment.Top, + ) + +private val collapseTransition: ExitTransition = + fadeOut(tween(EXPAND_ANIMATION_DURATION, easing = FastOutSlowInEasing)) + + shrinkVertically( + animationSpec = tween(EXPAND_ANIMATION_DURATION, easing = FastOutSlowInEasing), + shrinkTowards = Alignment.Top, + ) + @Composable fun ActionListItem( modifier: Modifier = Modifier, model: ActionListItemModel, index: Int, + isExpanded: Boolean, isDraggingEnabled: Boolean = false, isDragging: Boolean, isReorderingEnabled: Boolean, dragDropState: DragDropState? = null, + onExpandedChange: (Boolean) -> Unit = {}, onEditClick: () -> Unit = {}, onRemoveClick: () -> Unit = {}, onFixClick: () -> Unit = {}, onTestClick: () -> Unit = {}, + onRenameClick: () -> Unit = {}, + onEnabledChange: (Boolean) -> Unit = {}, onMoveUp: (() -> Unit)? = null, onMoveDown: (() -> Unit)? = null, ) { @@ -75,45 +122,54 @@ fun ActionListItem( val moveUpLabel = stringResource(R.string.accessibility_action_move_up) val moveDownLabel = stringResource(R.string.accessibility_action_move_down) - Column(modifier = modifier.fillMaxWidth()) { - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 48.dp) - .height(IntrinsicSize.Min) - .padding(start = 16.dp, end = 16.dp) - .semantics { - if (isReorderingEnabled) { - customActions = buildList { - onMoveUp?.let { action -> - add( - CustomAccessibilityAction(moveUpLabel) { - action() - true - }, - ) - } - onMoveDown?.let { action -> - add( - CustomAccessibilityAction(moveDownLabel) { - action() - true - }, - ) - } + // Only grey out a disabled action when it is collapsed so the expanded options stay readable. + val contentAlpha by animateFloatAsState( + targetValue = if (model.isEnabled || isExpanded) 1f else DISABLED_ALPHA, + animationSpec = tween(EXPAND_ANIMATION_DURATION, easing = FastOutSlowInEasing), + ) + + val cardColors = CardDefaults.elevatedCardColors( + containerColor = if (isDragging) { + MaterialTheme.colorScheme.surfaceContainerHighest + } else { + MaterialTheme.colorScheme.surfaceContainer + }, + contentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = contentAlpha), + ) + + ElevatedCard( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .semantics { + if (isReorderingEnabled) { + customActions = buildList { + onMoveUp?.let { action -> + add( + CustomAccessibilityAction(moveUpLabel) { + action() + true + }, + ) + } + onMoveDown?.let { action -> + add( + CustomAccessibilityAction(moveDownLabel) { + action() + true + }, + ) } } - }, - colors = CardDefaults.elevatedCardColors( - containerColor = if (isDragging) { - MaterialTheme.colorScheme.surfaceContainerHighest - } else { - MaterialTheme.colorScheme.surfaceContainer - }, - ), - ) { + } + }, + colors = cardColors, + ) { + Column { Row( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxWidth() + .clickable { onExpandedChange(!isExpanded) }, verticalAlignment = Alignment.CenterVertically, ) { Spacer(Modifier.width(8.dp)) @@ -133,210 +189,408 @@ fun ActionListItem( onDragStopped = { dragDropState?.onDragInterrupted() }, ), imageVector = Icons.Rounded.DragHandle, - contentDescription = stringResource(R.string.drag_handle_for, model.text), + contentDescription = stringResource( + R.string.drag_handle_for, + model.title, + ), tint = MaterialTheme.colorScheme.onSurface, ) } Spacer(Modifier.width(8.dp)) - if (model.error == null) { - when (model.icon) { - is ComposeIconInfo.Vector -> Icon( - modifier = Modifier.size(24.dp), - imageVector = model.icon.imageVector, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - ) - - is ComposeIconInfo.Drawable -> { - val painter = rememberDrawablePainter(model.icon.drawable) - Icon( - modifier = Modifier.size(24.dp), - painter = painter, - contentDescription = null, - tint = Color.Unspecified, - ) - } - } - } - - val primaryText = model.text + ActionIcon(modifier = Modifier.size(20.dp), icon = model.icon) Spacer(Modifier.width(8.dp)) - TextColumn( + HeaderText( modifier = Modifier .weight(1f) .padding(vertical = 8.dp), - primaryText = primaryText, - secondaryText = model.secondaryText, - errorText = model.error, + model = model, + isExpanded = isExpanded, + onRenameClick = onRenameClick, ) CompositionLocalProvider( LocalMinimumInteractiveComponentSize provides 16.dp, ) { - if (model.error != null && model.isErrorFixable) { - FilledTonalButton( - modifier = Modifier.padding(start = 8.dp, end = 8.dp), - onClick = onFixClick, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.error, - contentColor = MaterialTheme.colorScheme.onError, - ), - ) { - Text( - text = stringResource(R.string.button_fix), - ) - } - } - - if (model.error == null) { - IconButton(onClick = onTestClick) { - Icon( - imageVector = Icons.Rounded.PlayCircleOutline, - contentDescription = stringResource(R.string.action_list_item_test), - tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.size(24.dp), - ) - } - } - - IconButton(onClick = onEditClick) { - Icon( - imageVector = Icons.Outlined.Settings, - contentDescription = stringResource(R.string.action_list_item_edit), - tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.size(24.dp), - ) - } - - IconButton(onClick = onRemoveClick) { + IconButton(onClick = { onExpandedChange(!isExpanded) }) { Icon( - imageVector = Icons.Rounded.Clear, - contentDescription = stringResource(R.string.action_list_item_remove), + imageVector = if (isExpanded) { + Icons.Rounded.KeyboardArrowUp + } else { + Icons.Rounded.KeyboardArrowDown + }, + contentDescription = if (isExpanded) { + stringResource(R.string.action_list_item_collapse) + } else { + stringResource(R.string.action_list_item_expand) + }, tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.size(24.dp), ) } } + + Spacer(Modifier.width(4.dp)) + } + + AnimatedVisibility( + visible = isExpanded, + enter = expandTransition, + exit = collapseTransition, + ) { + ExpandedContent( + modifier = Modifier.padding(start = 16.dp, end = 8.dp, bottom = 8.dp), + model = model, + onEditClick = onEditClick, + onRemoveClick = onRemoveClick, + onFixClick = onFixClick, + onTestClick = onTestClick, + onEnabledChange = onEnabledChange, + ) } } + } +} - if (model.linkType == LinkType.HIDDEN) { - // Important! Show an empty spacer so the height of the card remains constant - // while dragging. If the height changes while dragging it can lead to janky - // behavior. - Spacer(Modifier.height(32.dp)) - } else { - Spacer(Modifier.height(4.dp)) +@Composable +private fun ActionIcon(modifier: Modifier = Modifier, icon: ComposeIconInfo) { + when (icon) { + is ComposeIconInfo.Vector -> Icon( + modifier = modifier, + imageVector = icon.imageVector, + contentDescription = null, + ) + is ComposeIconInfo.Drawable -> { + val painter = rememberDrawablePainter(icon.drawable) Icon( - imageVector = when (model.linkType) { - LinkType.ARROW -> Icons.Rounded.ArrowDownward - LinkType.PLUS -> Icons.Rounded.Add - LinkType.HIDDEN -> Icons.Rounded.Add - }, + modifier = modifier, + painter = painter, contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, + tint = Color.Unspecified, + ) + } + } +} + +@Composable +private fun HeaderText( + modifier: Modifier = Modifier, + model: ActionListItemModel, + isExpanded: Boolean, + onRenameClick: () -> Unit, +) { + Column(modifier = modifier) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( modifier = Modifier - .size(24.dp) - .align(Alignment.CenterHorizontally), + .weight(1f, fill = false), + text = model.title, + style = MaterialTheme.typography.bodyMedium, + fontStyle = if (model.isCustomName) FontStyle.Italic else FontStyle.Normal, + maxLines = 2, + overflow = TextOverflow.Ellipsis, ) - Spacer(Modifier.height(4.dp)) + + if (isExpanded) { + CompositionLocalProvider( + LocalMinimumInteractiveComponentSize provides 16.dp, + ) { + IconButton(onClick = onRenameClick) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = Icons.Rounded.Edit, + contentDescription = stringResource(R.string.action_list_item_rename), + ) + } + } + } + } + + AnimatedVisibility( + visible = !isExpanded, + enter = expandTransition, + exit = collapseTransition, + ) { + Column { + if (model.summary != null) { + Text( + text = model.summary, + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + + if (model.error != null && model.isEnabled) { + Text( + text = model.error, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.error, + ) + } + } } } } @Composable -private fun TextColumn( +private fun ExpandedContent( modifier: Modifier = Modifier, - primaryText: String, - secondaryText: String? = null, - errorText: String? = null, + model: ActionListItemModel, + onEditClick: () -> Unit, + onRemoveClick: () -> Unit, + onFixClick: () -> Unit, + onTestClick: () -> Unit, + onEnabledChange: (Boolean) -> Unit, ) { Column( modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Text( - text = primaryText, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 2, - overflow = TextOverflow.Ellipsis, + if (model.error != null) { + Row( + modifier = Modifier.padding(end = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = model.error, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.error, + ) + + if (model.isErrorFixable) { + Spacer(Modifier.width(8.dp)) + + Button( + modifier = Modifier.height(28.dp), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 2.dp), + shape = MaterialTheme.shapes.small, + onClick = onFixClick, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + ) { + Text(stringResource(R.string.button_fix)) + } + } + } + } + + if (model.showRepeat) { + OptionRow( + modifier = Modifier + .fillMaxWidth(), + icon = Icons.Rounded.Repeat, + text = model.repeatText ?: stringResource(R.string.action_list_no_repeat), + isSet = model.repeatText != null, + ) + } + + OptionRow( + modifier = Modifier + .fillMaxWidth(), + icon = Icons.Rounded.KeyboardDoubleArrowRight, + text = model.burstText ?: stringResource(R.string.action_list_no_burst), + isSet = model.burstText != null, ) - if (secondaryText != null) { - Text( - text = secondaryText, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 2, - overflow = TextOverflow.Ellipsis, + + if (model.showHoldDown) { + OptionRow( + modifier = Modifier + .fillMaxWidth(), + icon = Icons.Rounded.TouchApp, + text = model.holdDownText ?: stringResource(R.string.action_list_no_hold_down), + isSet = model.holdDownText != null, ) } - if (errorText != null) { + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { Text( - text = errorText, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error, + text = stringResource(R.string.switch_enabled), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, ) + + Spacer(Modifier.width(16.dp)) + + Switch(checked = model.isEnabled, onCheckedChange = onEnabledChange) + + Spacer(Modifier.weight(1f)) + + IconButton(onClick = onTestClick) { + Icon( + imageVector = Icons.Outlined.PlayArrow, + contentDescription = stringResource(R.string.action_list_item_test), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + + IconButton(onClick = onEditClick) { + Icon( + imageVector = Icons.Outlined.Settings, + contentDescription = stringResource(R.string.action_list_item_edit), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + + IconButton(onClick = onRemoveClick) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = stringResource(R.string.action_list_item_remove), + tint = MaterialTheme.colorScheme.error, + ) + } } } } +@Composable +private fun OptionRow( + modifier: Modifier = Modifier, + icon: ImageVector, + text: String, + isSet: Boolean, +) { + val color = if (isSet) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + } + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(16.dp), + imageVector = icon, + contentDescription = null, + tint = color, + ) + + Spacer(Modifier.width(16.dp)) + + Text( + text = text, + style = MaterialTheme.typography.labelSmall, + color = color, + ) + } +} + @Preview @Composable -private fun NoDragPreview() { - ActionListItem( - model = ActionListItemModel( - id = "id", - text = "Dismiss most recent notification", - secondaryText = "Repeat until released", - error = "Denied notification access permission", - isErrorFixable = true, - icon = ComposeIconInfo.Vector(Icons.Outlined.ClearAll), - ), - isDragging = false, - isReorderingEnabled = false, - index = 0, - ) +private fun CollapsedErrorPreview() { + KeyMapperTheme { + ActionListItem( + model = ActionListItemModel( + id = "id", + title = "Dismiss most recent notification", + summary = "Repeat until released", + error = "Denied notification access permission", + isErrorFixable = true, + icon = ComposeIconInfo.Vector(Icons.Outlined.ClearAll), + ), + isExpanded = false, + isDragging = false, + isReorderingEnabled = true, + index = 0, + ) + } } @Preview @Composable -private fun NoDragOneLinePreview() { - ActionListItem( - model = ActionListItemModel( - id = "id", - text = "Clear all", - secondaryText = null, - error = null, - isErrorFixable = true, - icon = ComposeIconInfo.Vector(Icons.Outlined.ClearAll), - ), - isDragging = false, - isReorderingEnabled = false, - index = 0, - ) +private fun CollapsedErrorDisabledPreview() { + KeyMapperTheme { + ActionListItem( + model = ActionListItemModel( + id = "id", + title = "Dismiss most recent notification", + summary = "Repeat until released", + error = "Denied notification access permission", + isErrorFixable = true, + icon = ComposeIconInfo.Vector(Icons.Outlined.ClearAll), + isEnabled = false, + ), + isExpanded = false, + isDragging = false, + isReorderingEnabled = true, + index = 0, + ) + } } @Preview @Composable -private fun DragDrawablePreview() { +private fun CollapsedOneLinePreview() { + KeyMapperTheme { + ActionListItem( + model = ActionListItemModel( + id = "id", + title = "Clear all", + icon = ComposeIconInfo.Vector(Icons.Outlined.ClearAll), + ), + isExpanded = false, + isDragging = false, + isReorderingEnabled = false, + index = 0, + ) + } +} + +@Preview +@Composable +private fun ExpandedPreview() { + KeyMapperTheme { + ActionListItem( + model = ActionListItemModel( + id = "id", + title = "Open magnifier", + isCustomName = true, + error = "A Key Mapper keyboard must be enabled!", + isErrorFixable = true, + showRepeat = true, + repeatText = "Repeat 5x after 400ms every 50ms until pressed again", + showHoldDown = true, + burstText = null, + holdDownText = "Hold down for 1000ms", + icon = ComposeIconInfo.Vector(Icons.Outlined.ClearAll), + ), + isExpanded = true, + isDragging = false, + isReorderingEnabled = true, + index = 0, + ) + } +} + +@Preview +@Composable +private fun DisabledDrawablePreview() { val drawable = LocalContext.current.drawable(R.mipmap.ic_launcher_round) - ActionListItem( - model = ActionListItemModel( - id = "id", - text = "Dismiss most recent notification", - secondaryText = "Repeat until released", - error = null, - isErrorFixable = true, - icon = ComposeIconInfo.Drawable(drawable), - ), - isDragging = false, - isReorderingEnabled = true, - index = 0, - ) + KeyMapperTheme { + ActionListItem( + model = ActionListItemModel( + id = "id", + title = "Dismiss most recent notification", + summary = "Repeat until released", + isEnabled = false, + icon = ComposeIconInfo.Drawable(drawable), + ), + isExpanded = false, + isDragging = false, + isReorderingEnabled = true, + index = 0, + ) + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItemModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItemModel.kt new file mode 100644 index 0000000000..cff9fbb8b0 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItemModel.kt @@ -0,0 +1,37 @@ +package io.github.sds100.keymapper.base.actions + +import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo + +data class ActionListItemModel( + val id: String, + val icon: ComposeIconInfo, + val title: String, + val isCustomName: Boolean = false, + val isEnabled: Boolean = true, + /** + * A summary of the options that is shown when the item is collapsed. + */ + val summary: String? = null, + val error: String? = null, + val isErrorFixable: Boolean = true, + val showRepeat: Boolean = false, + /** + * Null if the action does not repeat. + */ + val repeatText: String? = null, + /** + * Null if the action is not performed in a burst. + */ + val burstText: String? = null, + val showHoldDown: Boolean = false, + /** + * Null if the action is not held down. + */ + val holdDownText: String? = null, + /** + * Whether to show the chip for setting the delay before the next action. A delay after the + * last action does nothing. + */ + val showDelayChip: Boolean = false, + val delayBeforeNextAction: Int? = null, +) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt index d6a453c6d7..df5d5eb93d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt @@ -18,7 +18,6 @@ import androidx.compose.material.icons.automirrored.rounded.HelpOutline import androidx.compose.material.icons.rounded.Abc import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Edit -import androidx.compose.material.icons.rounded.HourglassEmpty import androidx.compose.material.icons.rounded.KeyboardDoubleArrowRight import androidx.compose.material.icons.rounded.Repeat import androidx.compose.material.icons.rounded.TouchApp @@ -178,11 +177,6 @@ fun ActionOptionsBottomSheet( HoldDownOptions(state = state, callback = callback) } - if (state.showDelayBeforeNextAction) { - Spacer(modifier = Modifier.height(16.dp)) - DelayOptions(state = state, callback = callback) - } - Spacer(modifier = Modifier.height(16.dp)) Row( @@ -502,34 +496,6 @@ private fun HoldDownOptions(state: ActionOptionsState, callback: ActionOptionsBo } } -@Composable -private fun DelayOptions(state: ActionOptionsState, callback: ActionOptionsBottomSheetCallback) { - Column { - OptionsHeaderRow( - modifier = Modifier.padding(horizontal = 16.dp), - icon = Icons.Rounded.HourglassEmpty, - text = stringResource(R.string.action_options_delay_header), - ) - - Spacer(modifier = Modifier.height(8.dp)) - - val delayMin = SliderMinimums.DELAY_BEFORE_NEXT_ACTION.toFloat() - val delayMax = SliderMaximums.DELAY_BEFORE_NEXT_ACTION.toFloat() - SliderOptionText( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - title = stringResource(R.string.extra_label_delay_before_next_action), - defaultValue = state.defaultDelayBeforeNextAction.toFloat(), - value = state.delayBeforeNextAction.toFloat(), - valueText = { "${it.toInt()} ms" }, - onValueChange = { callback.onDelayBeforeNextActionChanged(it.toInt()) }, - valueRange = delayMin..delayMax, - stepSize = SliderStepSizes.DELAY_BEFORE_NEXT_ACTION, - ) - } -} - interface ActionOptionsBottomSheetCallback { fun onEditClick() = run { } fun onReplaceClick() = run { } @@ -548,7 +514,6 @@ interface ActionOptionsBottomSheetCallback { */ fun onSelectHoldDownMode(holdDownMode: HoldDownMode?) = run { } fun onHoldDownDurationChanged(duration: Int) = run { } - fun onDelayBeforeNextActionChanged(delay: Int) = run { } fun onMultiplierChanged(multiplier: Int) = run { } } @@ -590,10 +555,6 @@ private val previewState = ActionOptionsState( holdDownMode = HoldDownMode.TRIGGER_RELEASED, - showDelayBeforeNextAction = true, - delayBeforeNextAction = 10000, - defaultDelayBeforeNextAction = 5000, - multiplier = 4, defaultMultiplier = 1, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsState.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsState.kt new file mode 100644 index 0000000000..525005f0ef --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsState.kt @@ -0,0 +1,42 @@ +package io.github.sds100.keymapper.base.actions + +import androidx.compose.ui.graphics.vector.ImageVector + +data class ActionOptionsState( + val title: String, + val actionTypeTitle: String, + val actionTypeIcon: ImageVector, + + val showEditButton: Boolean, + + val showRepeat: Boolean, + val isRepeatChecked: Boolean, + + val showRepeatRate: Boolean, + val showRepeatRateWarning: Boolean, + val repeatRate: Int, + val defaultRepeatRate: Int, + + val showRepeatDelay: Boolean, + val repeatDelay: Int, + val defaultRepeatDelay: Int, + + val showRepeatLimit: Boolean, + val repeatLimit: Int, + val defaultRepeatLimit: Int, + + val allowedRepeatModes: Set, + val repeatMode: RepeatMode, + + val showHoldDown: Boolean, + val isHoldDownChecked: Boolean, + + val showHoldDownDuration: Boolean, + val holdDownDuration: Int, + val defaultHoldDownDuration: Int, + + val holdDownMode: HoldDownMode, + + val multiplier: Int, + val defaultMultiplier: Int, +) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt index 067b9bafba..1296d5b774 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt @@ -905,76 +905,116 @@ class ActionUiHelper( } fun getOptionLabels(keyMap: KeyMap, action: Action) = buildList { - if (keyMap.isRepeatingActionsAllowed() && action.repeat) { - val repeatDescription = buildString { - append(getString(R.string.flag_repeat_build_description_start)) + getRepeatDescription(keyMap, action)?.let { add(it) } - val repeatLimit = when { - action.repeatLimit != null -> action.repeatLimit + if (keyMap.isHoldingDownActionAllowed(action) && + action.holdDown && + !action.stopHoldDownWhenTriggerPressedAgain + ) { + add(getString(R.string.flag_hold_down)) + } - action.repeatMode == RepeatMode.LIMIT_REACHED -> 1 + if (keyMap.isHoldingDownActionAllowed(action) && + action.holdDown && + action.stopHoldDownWhenTriggerPressedAgain + ) { + add(getString(R.string.flag_hold_down_until_pressed_again)) + } + } - // and is null - else -> null - } + /** + * @return null if the action does not repeat. + */ + fun getRepeatDescription(keyMap: KeyMap, action: Action): String? { + if (!keyMap.isRepeatingActionsAllowed() || !action.repeat) { + return null + } - if (repeatLimit != null) { - append(" ") - append(getString(R.string.flag_repeat_build_description_limit, repeatLimit)) - } + return buildString { + append(getString(R.string.flag_repeat_build_description_start)) - if (action.repeatRate != null) { - append(" ") - append( - getString( - R.string.flag_repeat_build_description_repeat_rate, - action.repeatRate, - ), - ) - } + val repeatLimit = when { + action.repeatLimit != null -> action.repeatLimit - if (action.repeatDelay != null) { - append(" ") - append( - getString( - R.string.flag_repeat_build_description_repeat_delay, - action.repeatDelay, - ), - ) - } + action.repeatMode == RepeatMode.LIMIT_REACHED -> 1 + + // and is null + else -> null + } + if (repeatLimit != null) { append(" ") + append(getString(R.string.flag_repeat_build_description_limit, repeatLimit)) + } - when (action.repeatMode) { - RepeatMode.TRIGGER_RELEASED -> { - append(getString(R.string.flag_repeat_build_description_until_released)) - } + if (action.repeatRate != null) { + append(" ") + append( + getString( + R.string.flag_repeat_build_description_repeat_rate, + action.repeatRate, + ), + ) + } - RepeatMode.TRIGGER_PRESSED_AGAIN -> { - append( - getString(R.string.flag_repeat_build_description_until_pressed_again), - ) - } + if (action.repeatDelay != null) { + append(" ") + append( + getString( + R.string.flag_repeat_build_description_repeat_delay, + action.repeatDelay, + ), + ) + } + + append(" ") + + when (action.repeatMode) { + RepeatMode.TRIGGER_RELEASED -> { + append(getString(R.string.flag_repeat_build_description_until_released)) + } - else -> Unit + RepeatMode.TRIGGER_PRESSED_AGAIN -> { + append( + getString(R.string.flag_repeat_build_description_until_pressed_again), + ) } + + else -> Unit } + } + } + + /** + * @return null if the action is not performed in a burst. + */ + fun getBurstDescription(action: Action): String? { + val multiplier = action.multiplier ?: return null - add(repeatDescription) + if (multiplier <= 1) { + return null } - if (keyMap.isHoldingDownActionAllowed(action) && - action.holdDown && - !action.stopHoldDownWhenTriggerPressedAgain - ) { - add(getString(R.string.flag_hold_down)) + return getString(R.string.action_list_burst, multiplier) + } + + /** + * @return null if the action is not held down. + */ + fun getHoldDownDescription(keyMap: KeyMap, action: Action): String? { + if (!keyMap.isHoldingDownActionAllowed(action) || !action.holdDown) { + return null } - if (keyMap.isHoldingDownActionAllowed(action) && - action.holdDown && - action.stopHoldDownWhenTriggerPressedAgain - ) { - add(getString(R.string.flag_hold_down_until_pressed_again)) + return when { + keyMap.isHoldingDownActionBeforeRepeatingAllowed(action) && + action.holdDownDuration != null -> + getString(R.string.action_list_hold_down_duration, action.holdDownDuration) + + action.stopHoldDownWhenTriggerPressedAgain -> + getString(R.string.flag_hold_down_until_pressed_again) + + else -> getString(R.string.flag_hold_down) } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt index 9781d71951..99c392f3f9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt @@ -8,30 +8,40 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowDownward import androidx.compose.material.icons.rounded.FlashlightOn +import androidx.compose.material.icons.rounded.MoreTime import androidx.compose.material.icons.rounded.Pinch +import androidx.compose.material.icons.rounded.Timelapse import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLocale import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -44,14 +54,25 @@ import io.github.sds100.keymapper.base.keymaps.ShortcutModel import io.github.sds100.keymapper.base.keymaps.ShortcutRow import io.github.sds100.keymapper.base.onboarding.OnboardingTipModel import io.github.sds100.keymapper.base.onboarding.TipCard -import io.github.sds100.keymapper.base.utils.ui.LinkType +import io.github.sds100.keymapper.base.utils.ui.SliderMaximums +import io.github.sds100.keymapper.base.utils.ui.SliderMinimums +import io.github.sds100.keymapper.base.utils.ui.SliderStepSizes import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.base.utils.ui.compose.CustomDialog import io.github.sds100.keymapper.base.utils.ui.compose.DraggableItem +import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText +import io.github.sds100.keymapper.base.utils.ui.compose.TextFieldDialog import io.github.sds100.keymapper.base.utils.ui.compose.rememberDragDropState import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.system.camera.CameraLens import kotlinx.coroutines.flow.update +/** + * The height of the row between action cards. The last card has a spacer of the same height so + * the height of the items stays constant while dragging. + */ +private val linkRowHeight = 48.dp + @OptIn(ExperimentalMaterial3Api::class) @Composable fun ActionsScreen(modifier: Modifier = Modifier, viewModel: ConfigActionsViewModel) { @@ -95,15 +116,11 @@ fun ActionsScreen(modifier: Modifier = Modifier, viewModel: ConfigActionsViewMod modifier = modifier, state = state, tipModel = actionTipModel, + callback = viewModel, onRemoveClick = viewModel::onRemoveClick, - onEditClick = viewModel::onEditClick, - onMoveAction = viewModel::onMoveAction, - onFixErrorClick = viewModel::onFixError, - onClickShortcut = viewModel::onClickShortcut, - onTestClick = viewModel::onTestClick, onAddClick = viewModel::onAddActionClick, - onActionTipDismiss = viewModel::onActionTipDismissClick, - onTipButtonClick = viewModel::onTipButtonClick, + onDelayChange = viewModel::onDelayChanged, + onRenameAction = viewModel::onRenameAction, ) } @@ -112,22 +129,21 @@ private fun ActionsScreen( modifier: Modifier = Modifier, state: State, tipModel: OnboardingTipModel? = null, + callback: ActionListCallback = object : ActionListCallback {}, onAddClick: () -> Unit = {}, onRemoveClick: (String) -> Unit = {}, - onEditClick: (String) -> Unit = {}, - onMoveAction: (fromIndex: Int, toIndex: Int) -> Unit = { _, _ -> }, - onFixErrorClick: (String) -> Unit = {}, - onTestClick: (String) -> Unit = {}, - onClickShortcut: (ActionData) -> Unit = {}, - onActionTipDismiss: () -> Unit = {}, - onTipButtonClick: (String) -> Unit = {}, + onDelayChange: (String, Int) -> Unit = { _, _ -> }, + onRenameAction: (String, String) -> Unit = { _, _ -> }, ) { - var showDeleteDialog by rememberSaveable { mutableStateOf(false) } var actionToDelete by rememberSaveable { mutableStateOf(null) } + var actionToRename by rememberSaveable { mutableStateOf(null) } + var actionToSetDelay by rememberSaveable { mutableStateOf(null) } + + val actions = ((state as? State.Data)?.data as? ConfigActionsState.Loaded)?.actions.orEmpty() - if (showDeleteDialog && actionToDelete != null) { + if (actionToDelete != null) { AlertDialog( - onDismissRequest = { showDeleteDialog = false }, + onDismissRequest = { actionToDelete = null }, title = { Text(stringResource(R.string.action_list_delete_dialog_title)) }, @@ -136,20 +152,48 @@ private fun ActionsScreen( TextButton( onClick = { onRemoveClick(actionToDelete!!) - showDeleteDialog = false + actionToDelete = null }, ) { Text(stringResource(R.string.action_list_delete_yes)) } }, dismissButton = { - TextButton(onClick = { showDeleteDialog = false }) { + TextButton(onClick = { actionToDelete = null }) { Text(stringResource(R.string.action_list_delete_cancel)) } }, ) } + val renameModel = actions.find { it.id == actionToRename } + + if (renameModel != null) { + TextFieldDialog( + title = stringResource(R.string.action_options_custom_name_dialog_title), + submitButtonText = stringResource(R.string.pos_save), + initialText = renameModel.title, + onSubmitClick = { newText -> + onRenameAction(renameModel.id, newText) + null + }, + onDismissRequest = { actionToRename = null }, + ) + } + + val delayModel = actions.find { it.id == actionToSetDelay } + + if (delayModel != null) { + DelayBeforeNextActionDialog( + initialDelay = delayModel.delayBeforeNextAction, + onSaveClick = { delay -> + onDelayChange(delayModel.id, delay) + actionToSetDelay = null + }, + onDismissRequest = { actionToSetDelay = null }, + ) + } + when (state) { State.Loading -> Loading() @@ -159,17 +203,10 @@ private fun ActionsScreen( modifier = Modifier.weight(1f), state = state.data, tipModel = tipModel, - onRemoveClick = { - actionToDelete = it - showDeleteDialog = true - }, - onEditClick = onEditClick, - onFixErrorClick = onFixErrorClick, - onMove = onMoveAction, - onClickShortcut = onClickShortcut, - onTestClick = onTestClick, - onActionTipDismiss, - onTipButtonClick, + callback = callback, + onRemoveClick = { actionToDelete = it }, + onDelayClick = { actionToSetDelay = it }, + onRenameClick = { actionToRename = it }, ) FilledTonalButton( @@ -196,32 +233,44 @@ private fun Loading(modifier: Modifier = Modifier) { } } +interface ActionListCallback { + fun onEditClick(id: String) = run { } + fun onFixErrorClick(id: String) = run { } + fun onMove(fromIndex: Int, toIndex: Int) = run { } + fun onClickShortcut(data: ActionData) = run { } + fun onTestClick(id: String) = run { } + fun onActionTipDismiss() = run { } + fun onTipButtonClick(id: String) = run { } + fun onEnabledChange(id: String, enabled: Boolean) = run { } +} + @Composable private fun ActionList( modifier: Modifier = Modifier, state: ConfigActionsState, tipModel: OnboardingTipModel?, + callback: ActionListCallback, onRemoveClick: (String) -> Unit, - onEditClick: (String) -> Unit, - onFixErrorClick: (String) -> Unit, - onMove: (fromIndex: Int, toIndex: Int) -> Unit, - onClickShortcut: (ActionData) -> Unit, - onTestClick: (String) -> Unit, - onActionTipDismiss: () -> Unit, - onTipButtonClick: (String) -> Unit, + onDelayClick: (String) -> Unit, + onRenameClick: (String) -> Unit, ) { val lazyListState = rememberLazyListState() + // A list rather than a set so it can be saved in a Bundle. + var expandedIds by rememberSaveable { mutableStateOf(listOf()) } + val dragDropState = if (state is ConfigActionsState.Loaded) { rememberDragDropState( lazyListState = lazyListState, - onMove = onMove, + onMove = callback::onMove, // Do not drag and drop the row of shortcuts ignoreLastItems = if (state.shortcuts.isEmpty()) { 0 } else { 1 }, + // Collapse all the items so they are a similar height while dragging. + onStart = { expandedIds = emptyList() }, ) } else { null @@ -248,9 +297,9 @@ private fun ActionList( title = tip.title, message = tip.message, isDismissable = tip.isDismissable, - onDismiss = onActionTipDismiss, + onDismiss = callback::onActionTipDismiss, buttonText = tip.buttonText, - onButtonClick = { onTipButtonClick(tip.id) }, + onButtonClick = { callback.onTipButtonClick(tip.id) }, ) Spacer(Modifier.height(8.dp)) @@ -276,35 +325,66 @@ private fun ActionList( key = { _, item -> item.id }, contentType = { _, _ -> "action" }, ) { index, model -> + // Automatically expand the item when an error appears so the user can fix it. + LaunchedEffect(model.id, model.error) { + if (model.isEnabled && model.error != null && model.id !in expandedIds) { + expandedIds = expandedIds + model.id + } + } + DraggableItem( dragDropState = dragDropState!!, index = index, ) { isDragging -> - ActionListItem( - modifier = Modifier.fillMaxWidth(), - model = model, - index = index, - isDraggingEnabled = state.actions.size > 1, - isDragging = isDragging, - isReorderingEnabled = state.isReorderingEnabled, - dragDropState = dragDropState, - onEditClick = { onEditClick(model.id) }, - onRemoveClick = { onRemoveClick(model.id) }, - onFixClick = { onFixErrorClick(model.id) }, - onTestClick = { onTestClick(model.id) }, - onMoveUp = if (state.isReorderingEnabled && index > 0) { - { onMove(index, index - 1) } - } else { - null - }, - onMoveDown = if (state.isReorderingEnabled && - index < state.actions.size - 1 - ) { - { onMove(index, index + 1) } + Column { + ActionListItem( + modifier = Modifier.fillMaxWidth(), + model = model, + index = index, + isExpanded = model.id in expandedIds, + isDraggingEnabled = state.actions.size > 1, + isDragging = isDragging, + isReorderingEnabled = state.isReorderingEnabled, + dragDropState = dragDropState, + onExpandedChange = { expanded -> + expandedIds = if (expanded) { + expandedIds + model.id + } else { + expandedIds - model.id + } + }, + onEditClick = { callback.onEditClick(model.id) }, + onRemoveClick = { onRemoveClick(model.id) }, + onFixClick = { callback.onFixErrorClick(model.id) }, + onTestClick = { callback.onTestClick(model.id) }, + onRenameClick = { onRenameClick(model.id) }, + onEnabledChange = { callback.onEnabledChange(model.id, it) }, + onMoveUp = if (state.isReorderingEnabled && index > 0) { + { callback.onMove(index, index - 1) } + } else { + null + }, + onMoveDown = if (state.isReorderingEnabled && + index < state.actions.size - 1 + ) { + { callback.onMove(index, index + 1) } + } else { + null + }, + ) + + if (model.showDelayChip) { + ActionLinkRow( + delayBeforeNextAction = model.delayBeforeNextAction, + onDelayClick = { onDelayClick(model.id) }, + ) } else { - null - }, - ) + // Important! Keep the height of the item constant while dragging. + // If the height changes while dragging it can lead to janky + // behavior. + Spacer(Modifier.height(linkRowHeight)) + } + } } } } @@ -325,7 +405,7 @@ private fun ActionList( .fillMaxWidth() .padding(horizontal = 32.dp), shortcuts = state.shortcuts, - onClick = { onClickShortcut(it) }, + onClick = { callback.onClickShortcut(it) }, ) } } @@ -333,6 +413,121 @@ private fun ActionList( } } +@Composable +private fun ActionLinkRow( + modifier: Modifier = Modifier, + delayBeforeNextAction: Int?, + onDelayClick: () -> Unit, +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(linkRowHeight) + .padding(horizontal = 16.dp), + ) { + Icon( + modifier = Modifier + .size(24.dp) + .align(Alignment.Center), + imageVector = Icons.Rounded.ArrowDownward, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + + AssistChip( + modifier = Modifier.align(Alignment.CenterEnd), + onClick = onDelayClick, + label = { + if (delayBeforeNextAction == null) { + Text(stringResource(R.string.action_list_add_delay)) + } else { + if (delayBeforeNextAction < 1000) { + Text(stringResource(R.string.action_title_wait_ms, delayBeforeNextAction)) + } else { + val seconds = delayBeforeNextAction / 1000f + val secondsText = if (seconds % 1f == 0f) { + seconds.toInt().toString() + } else { + String.format(LocalLocale.current.platformLocale, "%.1f", seconds) + } + + Text( + stringResource( + R.string.action_title_wait_secs, + secondsText, + ), + ) + } + } + }, + leadingIcon = { + Icon( + modifier = Modifier.size(AssistChipDefaults.IconSize), + imageVector = if (delayBeforeNextAction == null) { + Icons.Rounded.MoreTime + } else { + Icons.Rounded.Timelapse + }, + contentDescription = null, + ) + }, + colors = AssistChipDefaults.assistChipColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + leadingIconContentColor = MaterialTheme.colorScheme.onSurface, + ), + border = null, + ) + } +} + +@Composable +private fun DelayBeforeNextActionDialog( + initialDelay: Int?, + onSaveClick: (Int) -> Unit, + onDismissRequest: () -> Unit, +) { + var delay by rememberSaveable { + mutableIntStateOf(initialDelay ?: SliderStepSizes.DELAY_BEFORE_NEXT_ACTION) + } + + CustomDialog( + title = stringResource(R.string.action_options_delay_header), + confirmButton = { + TextButton(onClick = { onSaveClick(delay) }) { + Text(stringResource(R.string.pos_save)) + } + }, + dismissButton = { + if (initialDelay == null) { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.neg_cancel)) + } + } else { + TextButton(onClick = { onSaveClick(0) }) { + Text(stringResource(R.string.action_list_delay_remove)) + } + } + }, + onDismissRequest = onDismissRequest, + ) { + val delayMin = SliderMinimums.DELAY_BEFORE_NEXT_ACTION.toFloat() + val delayMax = SliderMaximums.DELAY_BEFORE_NEXT_ACTION.toFloat() + + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + title = null, + defaultValue = delayMin, + value = delay.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { delay = it.toInt() }, + valueRange = delayMin..delayMax, + stepSize = SliderStepSizes.DELAY_BEFORE_NEXT_ACTION, + ) + } +} + @Preview @Composable private fun EmptyPreview() { @@ -391,19 +586,25 @@ private fun LoadedPreview() { ActionListItemModel( id = "1", icon = ComposeIconInfo.Vector(Icons.Rounded.FlashlightOn), - text = "Toggle Back flashlight", - secondaryText = "Repeat until released", + title = "Toggle Back flashlight", + summary = "Repeat 5x until pressed again • Hold down", error = "Flashlight not found", isErrorFixable = true, - linkType = LinkType.ARROW, + showDelayChip = true, ), ActionListItemModel( id = "2", + icon = ComposeIconInfo.Vector(Icons.Rounded.Pinch), + title = "Open magnifier", + isCustomName = true, + showDelayChip = true, + delayBeforeNextAction = 100, + ), + ActionListItemModel( + id = "3", icon = ComposeIconInfo.Vector(Icons.Rounded.FlashlightOn), - text = "Toggle Back flashlight", - secondaryText = "Repeat until released", - error = null, - isErrorFixable = true, + title = "Toggle Back flashlight", + isEnabled = false, ), ), shortcuts = setOf( @@ -427,3 +628,15 @@ private fun LoadedPreview() { ) } } + +@Preview(showSystemUi = true) +@Composable +private fun DelayDialogPreview() { + KeyMapperTheme { + DelayBeforeNextActionDialog( + initialDelay = 400, + onSaveClick = {}, + onDismissRequest = {}, + ) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt index a6b623a275..e383a5267c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt @@ -189,6 +189,10 @@ class ConfigActionsUseCaseImpl @Inject constructor( } } + override fun setActionEnabled(uid: String, enabled: Boolean) { + setActionOption(uid) { action -> action.copy(isEnabled = enabled) } + } + private suspend fun getActionShortcuts(json: String?): List { if (json == null) { return emptyList() @@ -282,6 +286,7 @@ interface ConfigActionsUseCase : GetDefaultKeyMapOptionsUseCase { fun setActionMultiplier(uid: String, multiplier: Int) fun setDelayBeforeNextAction(uid: String, delay: Int) fun setActionCustomName(uid: String, customName: String?) + fun setActionEnabled(uid: String, enabled: Boolean) fun setActionRepeatRate(uid: String, repeatRate: Int) fun setActionRepeatLimit(uid: String, repeatLimit: Int) fun setActionStopRepeatingWhenTriggerPressedAgain(uid: String) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt index 709e404bac..2e56831119 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.base.actions -import androidx.compose.ui.graphics.vector.ImageVector import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -19,7 +18,6 @@ import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.base.utils.ui.DialogProvider -import io.github.sds100.keymapper.base.utils.ui.LinkType import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.base.utils.ui.ViewModelHelper import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo @@ -54,12 +52,13 @@ class ConfigActionsViewModel @Inject constructor( private val onboardingUseCase: OnboardingUseCase, setupAccessibilityServiceDelegate: SetupAccessibilityServiceDelegate, fixKeyEventActionDelegate: FixKeyEventActionDelegate, - onboardingTipDelegate: OnboardingTipDelegate, + private val onboardingTipDelegate: OnboardingTipDelegate, resourceProvider: ResourceProvider, navigationProvider: NavigationProvider, dialogProvider: DialogProvider, ) : ViewModel(), ActionOptionsBottomSheetCallback, + ActionListCallback, SetupAccessibilityServiceDelegate by setupAccessibilityServiceDelegate, ResourceProvider by resourceProvider, DialogProvider by dialogProvider, @@ -114,13 +113,13 @@ class ConfigActionsViewModel @Inject constructor( return config.keyMap.first().dataOrNull()?.actionList?.singleOrNull { it.uid == uid }?.data } - fun onClickShortcut(action: ActionData) { + override fun onClickShortcut(action: ActionData) { viewModelScope.launch { config.addAction(action) } } - fun onFixError(actionUid: String) { + override fun onFixErrorClick(actionUid: String) { viewModelScope.launch { val actionData = getActionData(actionUid) ?: return@launch val error = @@ -168,7 +167,7 @@ class ConfigActionsViewModel @Inject constructor( } } - fun onMoveAction(fromIndex: Int, toIndex: Int) { + override fun onMove(fromIndex: Int, toIndex: Int) { config.moveAction(fromIndex, toIndex) } @@ -176,11 +175,11 @@ class ConfigActionsViewModel @Inject constructor( config.removeAction(actionUid) } - fun onEditClick(actionUid: String) { + override fun onEditClick(actionUid: String) { actionOptionsUid.value = actionUid } - fun onTestClick(actionUid: String) { + override fun onTestClick(actionUid: String) { viewModelScope.launch { val actionData = getActionData(actionUid) ?: return@launch attemptTestAction(actionData) @@ -245,8 +244,20 @@ class ConfigActionsViewModel @Inject constructor( ) } - override fun onDelayBeforeNextActionChanged(delay: Int) { - actionOptionsUid.value?.let { uid -> config.setDelayBeforeNextAction(uid, delay) } + fun onDelayChanged(actionUid: String, delay: Int) { + config.setDelayBeforeNextAction(actionUid, delay) + } + + override fun onEnabledChange(actionUid: String, enabled: Boolean) { + config.setActionEnabled(actionUid, enabled) + } + + override fun onActionTipDismiss() { + onActionTipDismissClick() + } + + override fun onTipButtonClick(id: String) { + onboardingTipDelegate.onTipButtonClick(id) } override fun onMultiplierChanged(multiplier: Int) { @@ -275,7 +286,10 @@ class ConfigActionsViewModel @Inject constructor( override fun onCustomNameChanged(name: String) { val uid = actionOptionsUid.value ?: return + onRenameAction(uid, name) + } + fun onRenameAction(uid: String, name: String) { viewModelScope.launch { val actionData = getActionData(uid) ?: return@launch val showDeviceDescriptors = displayAction.showDeviceDescriptors.first() @@ -330,60 +344,36 @@ class ConfigActionsViewModel @Inject constructor( ): List { val actionErrors = errorSnapshot.getErrors(keyMap.actionList.map { it.data }) - return keyMap.actionList.mapIndexed { index, action -> - - val title: String = if (action.multiplier != null && action.multiplier > 1) { - val multiplier = action.multiplier - "${multiplier}x ${uiHelper.getTitle(action.data, showDeviceDescriptors)}" - } else { - uiHelper.getTitle(action, showDeviceDescriptors) - } + val midDot = getString(R.string.middot) + return keyMap.actionList.mapIndexed { index, action -> val icon: ComposeIconInfo = uiHelper.getIcon(action.data) val error: KMError? = actionErrors[action.data] - val extraInfo = buildString { - val midDot = getString(R.string.middot) - - uiHelper.getOptionLabels(keyMap, action).forEachIndexed { index, label -> - if (index != 0) { - append(" $midDot ") - } - - append(label) - } - - action.delayBeforeNextAction.apply { - if (keyMap.isDelayBeforeNextActionAllowed() && - action.delayBeforeNextAction != null - ) { - if (this@buildString.isNotBlank()) { - append(" $midDot ") - } - - append( - getString( - R.string.action_title_wait, - action.delayBeforeNextAction, - ), - ) - } - } - }.takeIf { it.isNotBlank() } + val repeatText = uiHelper.getRepeatDescription(keyMap, action) + val burstText = uiHelper.getBurstDescription(action) + val holdDownText = uiHelper.getHoldDownDescription(keyMap, action) - val linkType = when { - index < keyMap.actionList.lastIndex -> LinkType.ARROW - else -> LinkType.HIDDEN - } + val summary = listOfNotNull(repeatText, burstText, holdDownText) + .joinToString(" $midDot ") + .takeIf { it.isNotBlank() } ActionListItemModel( id = action.uid, icon = icon, - text = title, - secondaryText = extraInfo, + title = uiHelper.getTitle(action, showDeviceDescriptors), + isCustomName = !action.customName.isNullOrBlank(), + isEnabled = action.isEnabled, + summary = summary, error = error?.getFullMessage(this), isErrorFixable = error?.isFixable ?: true, - linkType = linkType, + showRepeat = keyMap.isRepeatingActionsAllowed(), + repeatText = repeatText, + burstText = burstText, + showHoldDown = keyMap.isHoldingDownActionAllowed(action), + holdDownText = holdDownText, + showDelayChip = index < keyMap.actionList.lastIndex, + delayBeforeNextAction = action.delayBeforeNextAction, ) } } @@ -465,10 +455,6 @@ class ConfigActionsViewModel @Inject constructor( HoldDownMode.TRIGGER_RELEASED }, - showDelayBeforeNextAction = keyMap.isDelayBeforeNextActionAllowed(), - delayBeforeNextAction = action.delayBeforeNextAction ?: 0, - defaultDelayBeforeNextAction = 0, - multiplier = action.multiplier ?: 1, defaultMultiplier = 1, ) @@ -487,56 +473,3 @@ sealed class ConfigActionsState { override val shortcuts: Set> = emptySet(), ) : ConfigActionsState() } - -data class ActionListItemModel( - val id: String, - val icon: ComposeIconInfo, - val text: String, - val secondaryText: String?, - val error: String? = null, - val isErrorFixable: Boolean = true, - val linkType: LinkType = LinkType.HIDDEN, -) - -data class ActionOptionsState( - val title: String, - val actionTypeTitle: String, - val actionTypeIcon: ImageVector, - - val showEditButton: Boolean, - - val showRepeat: Boolean, - val isRepeatChecked: Boolean, - - val showRepeatRate: Boolean, - val showRepeatRateWarning: Boolean, - val repeatRate: Int, - val defaultRepeatRate: Int, - - val showRepeatDelay: Boolean, - val repeatDelay: Int, - val defaultRepeatDelay: Int, - - val showRepeatLimit: Boolean, - val repeatLimit: Int, - val defaultRepeatLimit: Int, - - val allowedRepeatModes: Set, - val repeatMode: RepeatMode, - - val showHoldDown: Boolean, - val isHoldDownChecked: Boolean, - - val showHoldDownDuration: Boolean, - val holdDownDuration: Int, - val defaultHoldDownDuration: Int, - - val holdDownMode: HoldDownMode, - - val showDelayBeforeNextAction: Boolean, - val delayBeforeNextAction: Int, - val defaultDelayBeforeNextAction: Int, - - val multiplier: Int, - val defaultMultiplier: Int, -) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt index 01d28b35b1..7754d823ae 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt @@ -81,7 +81,10 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( if (groupUid == null) { add( DetectKeyMapModel( - keyMap = keyMap, + // Disabled actions must never be performed. + keyMap = keyMap.copy( + actionList = keyMap.actionList.filter { it.isEnabled }, + ), groupConstraintStates = constraintStates, ), ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt index a9f3d57ebd..f70861d8e3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt @@ -148,7 +148,7 @@ class KeyMapListItemCreator( append( getString( - R.string.action_title_wait, + R.string.action_title_wait_ms, action.delayBeforeNextAction, ), ) @@ -158,8 +158,14 @@ class KeyMapListItemCreator( val icon: ComposeIconInfo = actionUiHelper.getIcon(action.data) val error: KMError? = actionErrors[action.data] - val chip = if (error == null) { - ComposeChipModel.Normal(id = action.uid, text = chipText, icon = icon) + // Disabled actions are never performed so do not show their errors. + val chip = if (error == null || !action.isEnabled) { + ComposeChipModel.Normal( + id = action.uid, + text = chipText, + icon = icon, + isEnabled = action.isEnabled, + ) } else { ComposeChipModel.Error(action.uid, chipText, error, isFixable = error.isFixable) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt index 4688989732..a46e7ef45c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt @@ -473,20 +473,26 @@ private fun OptionsDescription(modifier: Modifier = Modifier, options: List Unit) { when (model) { is ComposeChipModel.Normal -> { + // Grey out disabled chips using the Material 3 alpha for disabled content. + val contentAlpha = if (model.isEnabled) 1f else 0.38f + CompactChip( text = model.text, + contentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = contentAlpha), icon = model.icon?.let { icon -> { when (icon) { is ComposeIconInfo.Drawable -> Icon( - modifier = Modifier.fillMaxHeight(), + modifier = Modifier + .fillMaxHeight(), painter = rememberDrawablePainter(icon.drawable), contentDescription = null, tint = Color.Unspecified, ) is ComposeIconInfo.Vector -> Icon( - modifier = Modifier.fillMaxHeight(), + modifier = Modifier + .fillMaxHeight(), imageVector = icon.imageVector, contentDescription = null, tint = MaterialTheme.colorScheme.onSurface, @@ -653,6 +659,7 @@ private fun sampleList(): List { drawable = context.drawable(R.drawable.ic_launcher_web), ), "Open Key Mapper", + isEnabled = false, ), ), constraintMode = ConstraintMode.AND, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt index 040b285a01..3eb294318b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt @@ -42,6 +42,7 @@ fun CompactChip( Surface( modifier = modifier.height(chipHeight), color = containerColor, + contentColor = contentColor, border = borderStroke, shape = AssistChipDefaults.shape, ) { @@ -51,6 +52,7 @@ fun CompactChip( Surface( modifier = modifier.height(chipHeight), color = containerColor, + contentColor = contentColor, border = borderStroke, shape = AssistChipDefaults.shape, onClick = onClick, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeChipModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeChipModel.kt index 15f44addaf..992fda2688 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeChipModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeChipModel.kt @@ -8,6 +8,10 @@ sealed class ComposeChipModel { override val id: String, val icon: ComposeIconInfo?, override val text: String, + /** + * Disabled chips are greyed out. + */ + val isEnabled: Boolean = true, ) : ComposeChipModel() data class Error( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeDialogs.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeDialogs.kt index ae8118a8ad..10a9cf2f08 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeDialogs.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeDialogs.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Spacer 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.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.AlertDialogDefaults diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SliderOptionText.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SliderOptionText.kt index 8bb1949548..9fdfd8433e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SliderOptionText.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SliderOptionText.kt @@ -44,7 +44,7 @@ import kotlin.math.roundToInt @Composable fun SliderOptionText( modifier: Modifier = Modifier, - title: String, + title: String?, value: Float, defaultValue: Float, valueText: (Float) -> String, @@ -77,13 +77,15 @@ fun SliderOptionText( } Column(modifier = modifier) { - Text( - modifier = Modifier, - text = title, - style = MaterialTheme.typography.titleSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + if (title != null) { + Text( + modifier = Modifier, + text = title, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } Row(verticalAlignment = Alignment.CenterVertically) { val interactionSource = remember { MutableInteractionSource() } @@ -138,7 +140,7 @@ fun SliderOptionText( private fun ValueDialog( initialValue: Int?, placeholderValue: String?, - title: String, + title: String?, onDismissRequest: () -> Unit, onSaveClick: (Int?) -> Unit, ) { @@ -218,6 +220,26 @@ private fun Preview() { } } +@Preview +@Composable +private fun PreviewNoTitle() { + KeyMapperTheme { + Surface { + SliderOptionText( + modifier = Modifier.width(400.dp), + title = null, + value = 50f, + defaultValue = 500f, + valueText = { "${it.roundToInt()} ms" }, + isEnabled = true, + onValueChange = {}, + valueRange = 0f..1000f, + stepSize = 50, + ) + } + } +} + @Preview @Composable private fun PreviewDefault() { diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 0d340c3325..81b4dd3c53 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -70,7 +70,8 @@ Reset Once you have enabled the device admin, you must DEACTIVATE it if you want to uninstall Key Mapper. - Wait %sms + Wait %sms + Wait %ss Start activity: %s Start service: %s @@ -1802,6 +1803,16 @@ Remove Edit Test + Expand + Collapse + Rename + Add delay + Remove + No repeat + No burst + Burst %dx + No hold down + Hold down for %dms Add actions to set what the key map should do once triggered. Recently used actions Action options diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/ActionEntityMapperTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/ActionEntityMapperTest.kt index ab32e2e5f5..7ecd30102e 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/ActionEntityMapperTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/ActionEntityMapperTest.kt @@ -1,6 +1,8 @@ package io.github.sds100.keymapper.base.actions import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.common.utils.hasFlag +import io.github.sds100.keymapper.data.entities.ActionEntity import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.nullValue @@ -25,4 +27,31 @@ class ActionEntityMapperTest { assertThat(ActionEntityMapper.fromEntity(entity)?.customName, nullValue()) } + + @Test + fun `action is enabled when the disabled flag is not set`() { + val entity = + ActionEntity(type = ActionEntity.Type.SYSTEM_ACTION, data = "consume_key_event") + + assertThat(ActionEntityMapper.fromEntity(entity)?.isEnabled, `is`(true)) + } + + @Test + fun `save and restore a disabled action`() { + val action = Action(data = ActionData.ConsumeKeyEvent, isEnabled = false) + + val entity = ActionEntityMapper.toEntity(KeyMap(actionList = listOf(action))).single() + + assertThat(entity.flags.hasFlag(ActionEntity.ACTION_FLAG_DISABLED), `is`(true)) + assertThat(ActionEntityMapper.fromEntity(entity)?.isEnabled, `is`(false)) + } + + @Test + fun `do not set the disabled flag for an enabled action`() { + val action = Action(data = ActionData.ConsumeKeyEvent, isEnabled = true) + + val entity = ActionEntityMapper.toEntity(KeyMap(actionList = listOf(action))).single() + + assertThat(entity.flags.hasFlag(ActionEntity.ACTION_FLAG_DISABLED), `is`(false)) + } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt index 53a6cf3288..b33fa2cdf9 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt @@ -135,6 +135,24 @@ class ConfigActionsUseCaseTest { assertThat(useCase.keyMap.value.dataOrNull()!!.actionList[0].customName, nullValue()) } + @Test + fun `disable an action, only that action is disabled`() = runTest(testDispatcher) { + configKeyMapState.setKeyMap(KeyMap()) + useCase.addAction(ActionData.ConsumeKeyEvent) + useCase.addAction(ActionData.GoHome) + + val uid = useCase.keyMap.value.dataOrNull()!!.actionList[0].uid + + useCase.setActionEnabled(uid, false) + + val actionList = useCase.keyMap.value.dataOrNull()!!.actionList + assertThat(actionList[0].isEnabled, `is`(false)) + assertThat(actionList[1].isEnabled, `is`(true)) + + useCase.setActionEnabled(uid, true) + assertThat(useCase.keyMap.value.dataOrNull()!!.actionList[0].isEnabled, `is`(true)) + } + /** * Issue #852. Add a phone ringing constraint when you add an action * to answer a phone call. diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DetectKeyMapsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DetectKeyMapsUseCaseTest.kt index a5f20562cf..45cc078e17 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DetectKeyMapsUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DetectKeyMapsUseCaseTest.kt @@ -1,5 +1,7 @@ package io.github.sds100.keymapper.base.keymaps +import io.github.sds100.keymapper.base.actions.Action +import io.github.sds100.keymapper.base.actions.ActionData import io.github.sds100.keymapper.base.constraints.Constraint import io.github.sds100.keymapper.base.constraints.ConstraintData import io.github.sds100.keymapper.base.constraints.ConstraintMode @@ -211,6 +213,33 @@ class DetectKeyMapsUseCaseTest { assertThat(models, Matchers.empty()) } + @Test + fun `Key map has disabled actions then remove the disabled actions`() { + val enabledAction = Action(data = ActionData.GoHome) + val disabledAction = Action(data = ActionData.ConsumeKeyEvent, isEnabled = false) + val keyMap = KeyMap(actionList = listOf(enabledAction, disabledAction)) + + val models = DetectKeyMapsUseCaseImpl.processKeyMapsAndGroups( + keyMaps = listOf(keyMap), + groups = emptyList(), + ) + + assertThat(models.single().keyMap.actionList, Matchers.contains(enabledAction)) + } + + @Test + fun `Key map has only disabled actions then action list is empty`() { + val disabledAction = Action(data = ActionData.ConsumeKeyEvent, isEnabled = false) + val keyMap = KeyMap(actionList = listOf(disabledAction)) + + val models = DetectKeyMapsUseCaseImpl.processKeyMapsAndGroups( + keyMaps = listOf(keyMap), + groups = emptyList(), + ) + + assertThat(models.single().keyMap.actionList, Matchers.empty()) + } + @Test fun `One key map in child group then include`() { val keyMap = KeyMap(groupUid = "child") diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt index ff36035c6e..2ec96727b1 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt @@ -142,6 +142,12 @@ data class ActionEntity( const val ACTION_FLAG_SHELL_COMMAND_USE_ROOT = 16 const val ACTION_FLAG_SHELL_COMMAND_USE_ADB = 32 + /** + * Stored as disabled rather than enabled so actions saved before this flag existed + * are enabled by default. + */ + const val ACTION_FLAG_DISABLED = 64 + const val EXTRA_CUSTOM_STOP_REPEAT_BEHAVIOUR = "extra_custom_stop_repeat_behaviour" const val EXTRA_CUSTOM_HOLD_DOWN_BEHAVIOUR = "extra_custom_hold_down_behaviour" const val EXTRA_REPEAT_DELAY = "extra_hold_down_until_repeat_delay" From 14af216ac088a400233365a87bd823148600561a Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 15 Sep 2026 22:07:49 +0200 Subject: [PATCH 09/10] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7579280dc3..83e23a6290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - #701 new action options bottom sheet design. - #701 you can now give custom names to actions, to make it easier to identify key maps. - #701 redesigned the action list with expandable action cards that show each option and error. -- #701 actions can be turned off individually without deleting them. +- #1465 actions can be turned off individually without deleting them. - #701 the delay before the next action is now set with a button between actions instead of in the action options. ## [4.4.1](https://github.com/sds100/KeyMapper/releases/tag/v4.4.1) From b3656908d1da1ca0956cef2a26ce16345dd57d20 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 15 Sep 2026 22:53:38 +0200 Subject: [PATCH 10/10] fix drag and drop of actions in the list --- .../keymapper/base/actions/ActionListItem.kt | 4 +- .../keymapper/base/actions/ActionsScreen.kt | 38 +-- .../base/sorting/SortBottomSheetContent.kt | 18 +- .../base/trigger/BaseTriggerScreen.kt | 20 +- .../base/trigger/TriggerKeyListItem.kt | 4 +- .../base/utils/ui/compose/DragDropState.kt | 313 +++++++++++++----- .../base/utils/ui/compose/DraggableItem.kt | 9 +- 7 files changed, 274 insertions(+), 132 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt index d7a15df335..7ef9564460 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt @@ -183,8 +183,8 @@ fun ActionListItem( enabled = isDraggingEnabled, orientation = Orientation.Vertical, startDragImmediately = true, - onDragStarted = { offset -> - dragDropState?.onDragStart(index, offset) + onDragStarted = { + dragDropState?.onDragStart(model.id) }, onDragStopped = { dragDropState?.onDragInterrupted() }, ), diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt index 99c392f3f9..08d9e71fe6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -259,22 +260,15 @@ private fun ActionList( // A list rather than a set so it can be saved in a Bundle. var expandedIds by rememberSaveable { mutableStateOf(listOf()) } - val dragDropState = if (state is ConfigActionsState.Loaded) { - rememberDragDropState( - lazyListState = lazyListState, - onMove = callback::onMove, - // Do not drag and drop the row of shortcuts - ignoreLastItems = if (state.shortcuts.isEmpty()) { - 0 - } else { - 1 - }, - // Collapse all the items so they are a similar height while dragging. - onStart = { expandedIds = emptyList() }, - ) - } else { - null - } + val actions = (state as? ConfigActionsState.Loaded)?.actions.orEmpty() + val actionIds = remember(actions) { actions.map { it.id } } + + // Only the actions can be dragged. Not the tip or the row of shortcuts. + val dragDropState = rememberDragDropState( + lazyListState = lazyListState, + keys = actionIds, + onMove = callback::onMove, + ) // Use dragContainer rather than .draggable() modifier because that causes // dragging the first item to be always be dropped in the next position. @@ -320,8 +314,10 @@ private fun ActionList( } is ConfigActionsState.Loaded -> { + val orderedActions = dragDropState.ordered(state.actions) { it.id } + itemsIndexed( - state.actions, + orderedActions, key = { _, item -> item.id }, contentType = { _, _ -> "action" }, ) { index, model -> @@ -333,8 +329,8 @@ private fun ActionList( } DraggableItem( - dragDropState = dragDropState!!, - index = index, + dragDropState = dragDropState, + key = model.id, ) { isDragging -> Column { ActionListItem( @@ -342,7 +338,7 @@ private fun ActionList( model = model, index = index, isExpanded = model.id in expandedIds, - isDraggingEnabled = state.actions.size > 1, + isDraggingEnabled = orderedActions.size > 1, isDragging = isDragging, isReorderingEnabled = state.isReorderingEnabled, dragDropState = dragDropState, @@ -365,7 +361,7 @@ private fun ActionList( null }, onMoveDown = if (state.isReorderingEnabled && - index < state.actions.size - 1 + index < orderedActions.size - 1 ) { { callback.onMove(index, index + 1) } } else { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt b/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt index fa3e7d9a7a..c095902c40 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt @@ -286,8 +286,10 @@ private fun SortDraggableList( onSortFieldClick: (SortField) -> Unit, ) { val lazyListState = rememberLazyListState() + val sortFields = remember(sortFieldOrderList) { sortFieldOrderList.map { it.field } } val dragDropState = rememberDragDropState( lazyListState = lazyListState, + keys = sortFields, onMove = onMove, ) @@ -296,13 +298,13 @@ private fun SortDraggableList( state = lazyListState, ) { itemsIndexed( - items = sortFieldOrderList, + items = dragDropState.ordered(sortFieldOrderList) { it.field }, key = { _, item -> item.field }, ) { index, item -> DraggableItem( dragDropState = dragDropState, - index = index, + key = item.field, ) { isDragging -> SortFieldListItem( index = index + 1, @@ -311,17 +313,7 @@ private fun SortDraggableList( onToggle = { onSortFieldClick(item.field) }, isDragging = isDragging, onDrag = { dragDropState.onDrag(it) }, - onDragStarted = { offset -> - // Calculate the offset of the item in the list - val lazyItem = lazyListState.layoutInfo.visibleItemsInfo - .firstOrNull { it.index == index } ?: return@SortFieldListItem - - val initialOffset = lazyItem.offset - - val finalOffset = offset + Offset(0f, initialOffset.toFloat()) - - dragDropState.onDragStart(finalOffset) - }, + onDragStarted = { dragDropState.onDragStart(item.field) }, onDragStopped = { dragDropState.onDragInterrupted() }, ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt index e32b627772..ed8c073118 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -437,16 +438,15 @@ private fun TriggerList( onAddMoreClick: () -> Unit, ) { val lazyListState = rememberLazyListState() + val triggerKeyIds = remember(triggerList) { triggerList.map { it.id } } + + // Only the trigger keys can be dragged. Not the "add more" button. val dragDropState = rememberDragDropState( lazyListState = lazyListState, + keys = triggerKeyIds, onMove = onMove, - // Do not drag and drop the "add more" button - ignoreLastItems = if (triggerList.isEmpty()) { - 0 - } else { - 1 - }, ) + val orderedTriggerList = dragDropState.ordered(triggerList) { it.id } // Use dragContainer rather than .draggable() modifier because that causes // dragging the first item to be always be dropped in the next position. @@ -457,19 +457,19 @@ private fun TriggerList( horizontalAlignment = Alignment.CenterHorizontally, ) { itemsIndexed( - triggerList, + orderedTriggerList, key = { _, item -> item.id }, contentType = { _, _ -> "key" }, ) { index, model -> DraggableItem( dragDropState = dragDropState, - index = index, + key = model.id, ) { isDragging -> TriggerKeyListItem( modifier = Modifier.fillMaxWidth(), model = model, index = index, - isDraggingEnabled = triggerList.size > 1, + isDraggingEnabled = orderedTriggerList.size > 1, isDragging = isDragging, isReorderingEnabled = isReorderingEnabled, dragDropState = dragDropState, @@ -481,7 +481,7 @@ private fun TriggerList( } else { null }, - onMoveDown = if (isReorderingEnabled && index < triggerList.size - 1) { + onMoveDown = if (isReorderingEnabled && index < orderedTriggerList.size - 1) { { onMove(index, index + 1) } } else { null diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt index 6370ac90b2..adfb51b65f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt @@ -127,8 +127,8 @@ fun TriggerKeyListItem( enabled = isDraggingEnabled, orientation = Orientation.Vertical, startDragImmediately = true, - onDragStarted = { offset -> - dragDropState?.onDragStart(index, offset) + onDragStarted = { + dragDropState?.onDragStart(model.id) }, onDragStopped = { dragDropState?.onDragInterrupted() }, ), diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/DragDropState.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/DragDropState.kt index bece2124d7..020f15b5a5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/DragDropState.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/DragDropState.kt @@ -6,51 +6,101 @@ import androidx.compose.animation.core.spring import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListLayoutInfo import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +/** + * The distance past the edge of the list at which auto scrolling reaches its maximum speed. + */ +private val autoScrollEdgeDistance = 64.dp + +/** + * Maximum auto scroll speed per second. + */ +private val autoScrollMaxSpeed = 1000.dp + @Composable fun rememberDragDropState( lazyListState: LazyListState, /** - * Ignore the last N items in the list. Do not allow dragging and dropping these items or - * placing other items in these positions. + * The keys of the items that can be dragged, in their current order. Other items in the list, + * such as headers and footers, can not be dragged and items can not be dropped on them. */ - ignoreLastItems: Int = 0, - onMove: (Int, Int) -> Unit, + keys: List, + /** + * Called once when the drag ends with the indices in [keys]. While dragging, the order is + * only changed locally, so use [DragDropState.ordered] to display the items. + */ + onMove: (fromIndex: Int, toIndex: Int) -> Unit, onStart: () -> Unit = {}, onEnd: () -> Unit = {}, ): DragDropState { val scope = rememberCoroutineScope() + val currentOnMove by rememberUpdatedState(onMove) + val currentOnStart by rememberUpdatedState(onStart) + val currentOnEnd by rememberUpdatedState(onEnd) + val state = remember(lazyListState) { DragDropState( state = lazyListState, - ignoreLastItems = ignoreLastItems, - onStart = onStart, - onMove = onMove, - onEnd = onEnd, scope = scope, + onStart = { currentOnStart() }, + onMove = { fromIndex, toIndex -> currentOnMove(fromIndex, toIndex) }, + onEnd = { currentOnEnd() }, ) } - LaunchedEffect(state) { - while (true) { - val diff = state.scrollChannel.receive() - lazyListState.scrollBy(diff) + SideEffect { + state.updateKeys(keys) + } + + val density = LocalDensity.current + val edgeDistancePx = with(density) { autoScrollEdgeDistance.toPx() } + val maxSpeedPx = with(density) { autoScrollMaxSpeed.toPx() } + + LaunchedEffect(state, edgeDistancePx, maxSpeedPx) { + snapshotFlow { state.draggingItemKey != null }.collectLatest { isDragging -> + if (!isDragging) { + return@collectLatest + } + + var lastFrameNanos = withFrameNanos { it } + + while (true) { + val frameNanos = withFrameNanos { it } + val seconds = (frameNanos - lastFrameNanos) / 1_000_000_000f + lastFrameNanos = frameNanos + + val overflow = state.draggingItemOverflow() + if (overflow != 0f) { + val fraction = (overflow / edgeDistancePx).coerceIn(-1f, 1f) + lazyListState.scrollBy(fraction * maxSpeedPx * seconds) + } + + // Check again after scrolling because other items move under the dragged item. + state.moveToTarget() + } } } @@ -58,20 +108,29 @@ fun rememberDragDropState( } /** - * This is copied from an official demo for drag and drop at https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt + * Originally based on the official demo for drag and drop at https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt + * + * Items are tracked by their key rather than their index so the list can contain other items, + * such as headers and footers. The new order is kept locally until the drag ends so the list + * does not depend on the new order propagating back from a ViewModel while dragging. */ class DragDropState internal constructor( private val state: LazyListState, - private val ignoreLastItems: Int, private val scope: CoroutineScope, private val onStart: () -> Unit, private val onMove: (Int, Int) -> Unit, private val onEnd: () -> Unit, ) { - var draggingItemIndex by mutableStateOf(null) + var draggingItemKey by mutableStateOf(null) private set - internal val scrollChannel = Channel() + private var keys: List = emptyList() + + /** + * The order of the keys while dragging, and after the drag ends until the new order is + * received in [updateKeys]. + */ + private var pendingOrder by mutableStateOf?>(null) private var draggingItemDraggedDelta by mutableFloatStateOf(0f) private var draggingItemInitialOffset by mutableIntStateOf(0) @@ -81,44 +140,81 @@ class DragDropState internal constructor( } ?: 0f private val draggingItemLayoutInfo: LazyListItemInfo? - get() = state.layoutInfo.visibleItemsInfo - .firstOrNull { it.index == draggingItemIndex } + get() = draggingItemKey?.let { key -> + state.layoutInfo.visibleItemsInfo.firstOrNull { it.key == key } + } + + /** + * The layout info when the order was last changed. Do not change the order again until the + * list has been laid out with the new order, otherwise the item can move back and forth. + */ + private var layoutInfoAtLastMove: LazyListLayoutInfo? = null - internal var previousIndexOfDraggedItem by mutableStateOf(null) + internal var previousKeyOfDraggedItem by mutableStateOf(null) private set internal var previousItemOffset = Animatable(0f) private set - fun onDragStart(index: Int, offset: Offset) { - // Calculate the offset of the item in the list - val lazyItem = state.layoutInfo.visibleItemsInfo - .firstOrNull { it.index == index } - ?: return + /** + * Sort the items in the order they should be displayed while dragging. + */ + fun ordered(items: List, key: (T) -> Any): List { + val order = pendingOrder ?: return items + val positions = order.withIndex().associate { it.value to it.index } + return items.sortedBy { positions[key(it)] ?: Int.MAX_VALUE } + } + + internal fun updateKeys(newKeys: List) { + if (newKeys == keys) { + return + } + + keys = newKeys - val initialOffset = lazyItem.offset + // The moved list has been received so the local order is no longer needed. + if (draggingItemKey == null) { + pendingOrder = null + } + } - val finalOffset = offset + Offset(0f, initialOffset.toFloat()) + /** + * Start dragging the item with this key. Use this when dragging with a drag handle. + */ + fun onDragStart(key: Any) { + if (key !in keys) { + return + } - onDragStart(finalOffset) + val item = state.layoutInfo.visibleItemsInfo.firstOrNull { it.key == key } ?: return + startDragging(item) } + /** + * Start dragging the item at this offset in the list. + */ fun onDragStart(offset: Offset) { - // check if the touch position is on drag handle - state.layoutInfo.visibleItemsInfo - .firstOrNull { item -> - item.index < state.layoutInfo.totalItemsCount - ignoreLastItems && - offset.y.toInt() in item.offset..(item.offset + item.size) - }?.also { - draggingItemIndex = it.index - draggingItemInitialOffset = it.offset - } + val item = state.layoutInfo.visibleItemsInfo.firstOrNull { item -> + item.key in keys && offset.y.toInt() in item.offset..item.offsetEnd + } ?: return + + startDragging(item) + } + + private fun startDragging(item: LazyListItemInfo) { + pendingOrder = ordered(keys) { it } + draggingItemKey = item.key + draggingItemInitialOffset = item.offset + draggingItemDraggedDelta = 0f + layoutInfoAtLastMove = null onStart.invoke() } fun onDragInterrupted() { - if (draggingItemIndex != null) { - previousIndexOfDraggedItem = draggingItemIndex + val key = draggingItemKey + + if (key != null) { + previousKeyOfDraggedItem = key val startOffset = draggingItemOffset scope.launch { previousItemOffset.snapTo(startOffset) @@ -129,68 +225,123 @@ class DragDropState internal constructor( visibilityThreshold = 1f, ), ) - previousIndexOfDraggedItem = null + previousKeyOfDraggedItem = null + } + + val fromIndex = keys.indexOf(key) + val toIndex = ordered(keys) { it }.indexOf(key) + + if (fromIndex != -1 && toIndex != -1 && fromIndex != toIndex) { + // Keep the local order until the new keys are received. + onMove.invoke(fromIndex, toIndex) + } else { + pendingOrder = null } } + draggingItemDraggedDelta = 0f - draggingItemIndex = null + draggingItemKey = null draggingItemInitialOffset = 0 + layoutInfoAtLastMove = null onEnd.invoke() } fun onDrag(offset: Offset) { draggingItemDraggedDelta += offset.y + moveToTarget() + } + /** + * Move the dragged item to the position of the item underneath its middle. + */ + internal fun moveToTarget() { val draggingItem = draggingItemLayoutInfo ?: return + val layoutInfo = state.layoutInfo + val order = pendingOrder ?: return + + if (layoutInfo === layoutInfoAtLastMove) { + return + } + val startOffset = draggingItem.offset + draggingItemOffset - val endOffset = startOffset + draggingItem.size - val middleOffset = startOffset + (endOffset - startOffset) / 2f + val middleOffset = startOffset + draggingItem.size / 2f + + val targetItem = layoutInfo.visibleItemsInfo.find { item -> + item.key != draggingItem.key && + item.key in keys && + middleOffset.toInt() in item.offset..item.offsetEnd + } ?: return - val targetItem = state.layoutInfo.visibleItemsInfo.find { item -> - middleOffset.toInt() in item.offset..item.offsetEnd && - draggingItem.index != item.index + // Only move once the middle of the dragged item has passed the middle of the target. + // Otherwise items of different heights swap back and forth. + val targetMiddle = targetItem.offset + targetItem.size / 2f + val hasPassedTarget = if (targetItem.offset > draggingItem.offset) { + middleOffset > targetMiddle + } else { + middleOffset < targetMiddle } - val itemCount = state.layoutInfo.totalItemsCount - if (targetItem != null) { - val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) { - draggingItem.index - } else if (draggingItem.index == state.firstVisibleItemIndex) { - targetItem.index - } else { - null - } + if (!hasPassedTarget) { + return + } - if (draggingItem.index < itemCount - ignoreLastItems && - targetItem.index < itemCount - ignoreLastItems - ) { - if (scrollToIndex != null) { - scope.launch { - // this is needed to neutralize automatic keeping the first item first. - state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset) - onMove.invoke(draggingItem.index, targetItem.index) - } - } else { - onMove.invoke(draggingItem.index, targetItem.index) - } - draggingItemIndex = targetItem.index - } + val fromIndex = order.indexOf(draggingItem.key) + val toIndex = order.indexOf(targetItem.key) + + if (fromIndex == -1 || toIndex == -1) { + return + } + + // Where the dragged item will be laid out after moving if the other items stay in place. + val newOffset = if (targetItem.offset > draggingItem.offset) { + targetItem.offsetEnd - draggingItem.size } else { - val overscroll = when { - draggingItemDraggedDelta > 0 -> - (endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast( - 0f, - ) + targetItem.offset + } + val minOffset = layoutInfo.viewportStartOffset + val maxOffset = + (layoutInfo.viewportEndOffset - draggingItem.size).coerceAtLeast(minOffset) - draggingItemDraggedDelta < 0 -> - (startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f) + if (newOffset !in minOffset..maxOffset) { + // Moving a small item past a large item can move it out of the list, which stops + // it being laid out so the drag can not continue. Scroll so it stays in the list. + // The dragged item will be at the target's index after moving. + state.requestScrollToItem(targetItem.index, -newOffset.coerceIn(minOffset, maxOffset)) + } else if (draggingItem.index == state.firstVisibleItemIndex || + targetItem.index == state.firstVisibleItemIndex + ) { + // The list keeps the first visible item in place when the order changes, which + // scrolls the list when the first visible item is moved. Keep the scroll position. + state.requestScrollToItem( + state.firstVisibleItemIndex, + state.firstVisibleItemScrollOffset, + ) + } - else -> 0f - } - if (overscroll != 0f) { - scrollChannel.trySend(overscroll) - } + pendingOrder = order.toMutableList().apply { add(toIndex, removeAt(fromIndex)) } + layoutInfoAtLastMove = layoutInfo + } + + /** + * How far the dragged item is past the edge of the list in the direction it is being dragged. + * Positive when past the end and negative when past the start. + */ + internal fun draggingItemOverflow(): Float { + val draggingItem = draggingItemLayoutInfo ?: return 0f + val layoutInfo = state.layoutInfo + + val startOffset = draggingItem.offset + draggingItemOffset + val endOffset = startOffset + draggingItem.size + + return when { + draggingItemDraggedDelta > 0 -> + (endOffset - layoutInfo.viewportEndOffset).coerceAtLeast(0f) + + draggingItemDraggedDelta < 0 -> + (startOffset - layoutInfo.viewportStartOffset).coerceAtMost(0f) + + else -> 0f } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/DraggableItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/DraggableItem.kt index d40e0cb3cd..7d05a62527 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/DraggableItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/DraggableItem.kt @@ -11,18 +11,21 @@ import androidx.compose.ui.zIndex @Composable fun LazyItemScope.DraggableItem( dragDropState: DragDropState, - index: Int, + /** + * The same key that is used for this item in the lazy list. + */ + key: Any, modifier: Modifier = Modifier, content: @Composable BoxScope.(isDragging: Boolean) -> Unit, ) { - val dragging = index == dragDropState.draggingItemIndex + val dragging = key == dragDropState.draggingItemKey val draggingModifier = if (dragging) { Modifier .zIndex(1f) .graphicsLayer { translationY = dragDropState.draggingItemOffset } - } else if (index == dragDropState.previousIndexOfDraggedItem) { + } else if (key == dragDropState.previousKeyOfDraggedItem) { Modifier .zIndex(1f) .graphicsLayer {