diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d05d7c4f..3bbeb134f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +## Changed + +- #1369 Add content descriptions to drag handles and custom "Move up"/"Move down" accessibility actions for trigger and action list items, improving TalkBack support for reordering. + ## [4.1.1](https://github.com/sds100/KeyMapper/releases/tag/v4.1.1) #### 15 May 2026 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 f842381aae..e4e00967d4 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 @@ -39,6 +39,9 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color 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.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -62,11 +65,16 @@ fun ActionListItem( onRemoveClick: () -> Unit = {}, onFixClick: () -> Unit = {}, onTestClick: () -> Unit = {}, + onMoveUp: (() -> Unit)? = null, + onMoveDown: (() -> Unit)? = null, ) { val draggableState = rememberDraggableState { dragDropState?.onDrag(Offset(0f, it)) } + 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 @@ -83,7 +91,19 @@ fun ActionListItem( dragDropState?.onDragStart(index, offset) }, onDragStopped = { dragDropState?.onDragInterrupted() }, - ), + ) + .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 @@ -102,7 +122,7 @@ fun ActionListItem( Icon( modifier = Modifier.size(24.dp), imageVector = Icons.Rounded.DragHandle, - contentDescription = null, + contentDescription = stringResource(R.string.drag_handle_for, model.text), tint = MaterialTheme.colorScheme.onSurface, ) } 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 b7fec30f2c..4198aa2287 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 @@ -314,6 +314,16 @@ private fun ActionList( onRemoveClick = { onRemoveClick(model.id) }, onFixClick = { onFixErrorClick(model.id) }, onTestClick = { onTestClick(model.id) }, + onMoveUp = if (isReorderingEnabled && index > 0) { + { onMove(index, index - 1) } + } else { + null + }, + onMoveDown = if (isReorderingEnabled && index < actionList.size - 1) { + { onMove(index, index + 1) } + } else { + null + }, ) } } 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 735ae05da8..2415f99184 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 @@ -482,6 +482,16 @@ private fun TriggerList( onEditClick = { onEditClick(model.id) }, onRemoveClick = { onRemoveClick(model.id) }, onFixClick = onFixErrorClick, + onMoveUp = if (isReorderingEnabled && index > 0) { + { onMove(index, index - 1) } + } else { + null + }, + onMoveDown = if (isReorderingEnabled && index < triggerList.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 8053721ef3..a1e4ee66a4 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 @@ -39,6 +39,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset 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.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -60,11 +63,67 @@ fun TriggerKeyListItem( onEditClick: () -> Unit = {}, onRemoveClick: () -> Unit = {}, onFixClick: (TriggerError) -> Unit = {}, + onMoveUp: (() -> Unit)? = null, + onMoveDown: (() -> Unit)? = null, ) { val draggableState = rememberDraggableState { dragDropState?.onDrag(Offset(0f, it)) } + val primaryText = when (model) { + is TriggerKeyListItemModel.Assistant -> when (model.assistantType) { + AssistantTriggerType.ANY -> stringResource( + R.string.assistant_any_trigger_name, + ) + + AssistantTriggerType.VOICE -> stringResource( + R.string.assistant_voice_trigger_name, + ) + + AssistantTriggerType.DEVICE -> stringResource( + R.string.assistant_device_trigger_name, + ) + } + + is TriggerKeyListItemModel.FloatingButton -> if (model.buttonName.isBlank()) { + stringResource(R.string.trigger_key_floating_button_description_empty) + } else { + stringResource( + R.string.trigger_key_floating_button_description, + model.buttonName, + ) + } + + is TriggerKeyListItemModel.KeyEvent -> model.keyName + + is TriggerKeyListItemModel.EvdevEvent -> model.keyName + + is TriggerKeyListItemModel.FloatingButtonDeleted -> stringResource( + R.string.trigger_error_floating_button_deleted_title, + ) + + is TriggerKeyListItemModel.FingerprintGesture -> when (model.gestureType) { + FingerprintGestureType.SWIPE_UP -> stringResource( + R.string.trigger_key_fingerprint_gesture_up, + ) + + FingerprintGestureType.SWIPE_DOWN -> stringResource( + R.string.trigger_key_fingerprint_gesture_down, + ) + + FingerprintGestureType.SWIPE_LEFT -> stringResource( + R.string.trigger_key_fingerprint_gesture_left, + ) + + FingerprintGestureType.SWIPE_RIGHT -> stringResource( + R.string.trigger_key_fingerprint_gesture_right, + ) + } + } + + 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 @@ -81,7 +140,19 @@ fun TriggerKeyListItem( dragDropState?.onDragStart(index, offset) }, onDragStopped = { dragDropState?.onDragInterrupted() }, - ), + ) + .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 @@ -100,7 +171,7 @@ fun TriggerKeyListItem( Icon( modifier = Modifier.size(24.dp), imageVector = Icons.Rounded.DragHandle, - contentDescription = null, + contentDescription = stringResource(R.string.drag_handle_for, primaryText), tint = MaterialTheme.colorScheme.onSurface, ) } @@ -127,57 +198,6 @@ fun TriggerKeyListItem( } } - val primaryText = when (model) { - is TriggerKeyListItemModel.Assistant -> when (model.assistantType) { - AssistantTriggerType.ANY -> stringResource( - R.string.assistant_any_trigger_name, - ) - - AssistantTriggerType.VOICE -> stringResource( - R.string.assistant_voice_trigger_name, - ) - - AssistantTriggerType.DEVICE -> stringResource( - R.string.assistant_device_trigger_name, - ) - } - - is TriggerKeyListItemModel.FloatingButton -> if (model.buttonName.isBlank()) { - stringResource(R.string.trigger_key_floating_button_description_empty) - } else { - stringResource( - R.string.trigger_key_floating_button_description, - model.buttonName, - ) - } - - is TriggerKeyListItemModel.KeyEvent -> model.keyName - - is TriggerKeyListItemModel.EvdevEvent -> model.keyName - - is TriggerKeyListItemModel.FloatingButtonDeleted -> stringResource( - R.string.trigger_error_floating_button_deleted_title, - ) - - is TriggerKeyListItemModel.FingerprintGesture -> when (model.gestureType) { - FingerprintGestureType.SWIPE_UP -> stringResource( - R.string.trigger_key_fingerprint_gesture_up, - ) - - FingerprintGestureType.SWIPE_DOWN -> stringResource( - R.string.trigger_key_fingerprint_gesture_down, - ) - - FingerprintGestureType.SWIPE_LEFT -> stringResource( - R.string.trigger_key_fingerprint_gesture_left, - ) - - FingerprintGestureType.SWIPE_RIGHT -> stringResource( - R.string.trigger_key_fingerprint_gesture_right, - ) - } - } - Spacer(Modifier.width(8.dp)) if (model.error == null) { diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index a081813537..c7ff462120 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -486,6 +486,8 @@ Drag the handles to adjust priorities. The item at the top is the most important. You must tap the item to enable sorting and toggle ascending/descending. Example: To sort by Actions (ascending) first and Triggers (descending) second: tap and drag Actions to the first position and set it to ascending, then tap and drag Triggers to the second position and set it to descending. Drag handle for %1$s + Move up + Move down Show example Turn on notifications