Rework toolbox tile gestures: tap to pick/edit, long-press to clear (v0.5.2)

Single tap now acts immediately instead of just selecting: an empty tile
opens the device picker, a filled tile opens its settings editor (this was
the double-tap behaviour; the highlight-only single-tap and the double-tap
are gone). Long-press clears the tile — but now via a confirmation dialog
("Clear tile") rather than wiping instantly, since clearing discards the
device and its settings with no undo (matches the preset DELETE confirm).
Long-pressing an empty tile does nothing.

Removes the now-unused per-tile `selected` state/highlight and onDoubleTap.
Verified on-device: filled→editor, empty→picker, long-press→confirm(Cancel
keeps the tile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Reactorcoremeltdown
2026-07-18 15:00:17 +02:00
parent 2f671ed372
commit e0fe89d62c
2 changed files with 41 additions and 27 deletions

View File

@@ -22,8 +22,8 @@ android {
// (android.media.midi) and AAudio low-latency audio we rely on.
minSdk = 26
targetSdk = 34
versionCode = 16
versionName = "0.5.1"
versionCode = 17
versionName = "0.5.2"
// We provide our own instrumentation runner if/when tests are added.
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -66,12 +66,10 @@ import kotlinx.coroutines.launch
/**
* The third tab. Top half: a 4x4 grid of 16 slots holding instruments or effects.
* - SINGLE tap -> select the tile (highlighted).
* - DOUBLE tap -> open its edit window (device picker if empty, params if filled).
* - LONG press -> empty the cell.
* - SINGLE tap -> pick a device (empty tile) or open its settings (filled tile).
* - LONG press -> clear the tile (asks to confirm first, since it's destructive).
*
* Bottom half: a stacked two-octave keyboard (one octave per row) that plays the
* currently-selected tile IF it is an instrument, for quick testing.
* Bottom half: a stacked two-octave keyboard (one octave per row) for quick testing.
*
* The parameter editor is generated automatically from the device's [ToolboxType]
* parameter list, so new devices need no bespoke UI.
@@ -81,27 +79,33 @@ fun ToolboxScreen(vm: AppViewModel) {
val c = LocalRetro.current
@Suppress("UNUSED_VARIABLE") val redraw = vm.revision
var selected by remember { mutableStateOf(-1) }
var editing by remember { mutableStateOf<Int?>(null) }
var picking by remember { mutableStateOf<Int?>(null) }
var clearing by remember { mutableStateOf<Int?>(null) }
// Back closes an open device editor / picker before the app-level handler
// Back closes an open device editor / picker / confirm before the app-level handler
// switches tabs.
BackHandler(enabled = editing != null || picking != null) {
if (editing != null) editing = null else picking = null
BackHandler(enabled = editing != null || picking != null || clearing != null) {
when {
clearing != null -> clearing = null
editing != null -> editing = null
else -> picking = null
}
}
// Shared tile gestures (index-based) so both layouts drive the same state.
val onTapTile: (Int) -> Unit = { selected = it }
val onDoubleTapTile: (Int) -> Unit = { i -> if (vm.project.toolbox[i].isEmpty) picking = i else editing = i }
val onLongPressTile: (Int) -> Unit = { i -> vm.project.toolbox[i].clearSlot(); vm.bumpForToolbar() }
// Single tap: pick a device for an empty tile, else open the filled tile's settings.
val onTapTile: (Int) -> Unit = { i -> if (vm.project.toolbox[i].isEmpty) picking = i else editing = i }
// Long press: clear a filled tile — but confirm first (it discards the device and
// its settings, and there's no undo). Long-pressing an empty tile does nothing.
val onLongPressTile: (Int) -> Unit = { i -> if (!vm.project.toolbox[i].isEmpty) clearing = i }
if (isLandscape()) {
// Landscape: a fixed 4x4 grid squashed to fit the height (no scroll) BESIDE
// the audition keyboard, which gets the larger width so its CH/OCT/VEL
// toolbar fits without horizontal scrolling.
Row(Modifier.fillMaxSize().background(c.background)) {
ToolboxTileGrid(vm, selected, onTapTile, onDoubleTapTile, onLongPressTile,
ToolboxTileGrid(vm, onTapTile, onLongPressTile,
Modifier.weight(1f).fillMaxHeight())
Box(Modifier.width(2.dp).fillMaxHeight().background(c.accent))
space.rcmd.android.sizzle.ui.StackedKeyboard(vm, punchIn = false, Modifier.weight(1.3f))
@@ -117,9 +121,8 @@ fun ToolboxScreen(vm: AppViewModel) {
) {
items(vm.project.toolbox) { slot ->
SlotTile(
rev = vm.revision, slot = slot, selected = selected == slot.index,
rev = vm.revision, slot = slot,
onTap = { onTapTile(slot.index) },
onDoubleTap = { onDoubleTapTile(slot.index) },
onLongPress = { onLongPressTile(slot.index) },
modifier = Modifier.fillMaxWidth().aspectRatio(1f),
)
@@ -142,6 +145,23 @@ fun ToolboxScreen(vm: AppViewModel) {
editing?.let { index ->
ParamEditor(vm, vm.project.toolbox[index], onClose = { editing = null })
}
clearing?.let { index ->
val slot = vm.project.toolbox[index]
val label = slot.name.ifBlank { slot.type?.displayName ?: "" }
AlertDialog(
onDismissRequest = { clearing = null },
title = { Text("Clear tile") },
text = { Text("Remove \"$label\" from tile ${index + 1}? This clears the device and its settings and can't be undone.") },
confirmButton = {
TextButton(onClick = {
vm.project.toolbox[index].clearSlot()
vm.bumpForToolbar()
clearing = null
}) { Text("Clear") }
},
dismissButton = { TextButton(onClick = { clearing = null }) { Text("Cancel") } },
)
}
}
/** A fixed 4x4 grid of the 16 slot tiles that fills [modifier]'s bounds — used in
@@ -150,9 +170,7 @@ fun ToolboxScreen(vm: AppViewModel) {
@Composable
private fun ToolboxTileGrid(
vm: AppViewModel,
selected: Int,
onTap: (Int) -> Unit,
onDoubleTap: (Int) -> Unit,
onLongPress: (Int) -> Unit,
modifier: Modifier,
) {
@@ -164,9 +182,7 @@ private fun ToolboxTileGrid(
SlotTile(
rev = vm.revision,
slot = vm.project.toolbox[idx],
selected = selected == idx,
onTap = { onTap(idx) },
onDoubleTap = { onDoubleTap(idx) },
onLongPress = { onLongPress(idx) },
modifier = Modifier.weight(1f).fillMaxHeight(),
)
@@ -182,24 +198,22 @@ private fun ToolboxTileGrid(
private fun SlotTile(
rev: Int,
slot: ToolboxSlot,
selected: Boolean,
onTap: () -> Unit,
onDoubleTap: () -> Unit,
onLongPress: () -> Unit,
modifier: Modifier = Modifier,
) {
@Suppress("UNUSED_PARAMETER") val ignored = rev // param only used to bust skipping
val c = LocalRetro.current
val borderColor = when { selected -> c.playhead; !slot.isEmpty -> c.accent; else -> c.grid }
val fill = when { selected -> c.accent.copy(alpha = 0.28f); !slot.isEmpty -> c.accent.copy(alpha = 0.12f); else -> c.surface }
val borderColor = if (!slot.isEmpty) c.accent else c.grid
val fill = if (!slot.isEmpty) c.accent.copy(alpha = 0.12f) else c.surface
Box(
modifier
.border(if (selected) 2.dp else 1.dp, borderColor, RectangleShape)
.border(1.dp, borderColor, RectangleShape)
.background(fill)
.pointerInput(slot.index, slot.isEmpty) {
// Single tap acts immediately (no double-tap wait); long press clears.
detectTapGestures(
onTap = { onTap() },
onDoubleTap = { onDoubleTap() },
onLongPress = { onLongPress() },
)
},