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:
@@ -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"
|
||||
|
||||
@@ -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() },
|
||||
)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user