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.
|
// (android.media.midi) and AAudio low-latency audio we rely on.
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 16
|
versionCode = 17
|
||||||
versionName = "0.5.1"
|
versionName = "0.5.2"
|
||||||
|
|
||||||
// We provide our own instrumentation runner if/when tests are added.
|
// We provide our own instrumentation runner if/when tests are added.
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
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.
|
* The third tab. Top half: a 4x4 grid of 16 slots holding instruments or effects.
|
||||||
* - SINGLE tap -> select the tile (highlighted).
|
* - SINGLE tap -> pick a device (empty tile) or open its settings (filled tile).
|
||||||
* - DOUBLE tap -> open its edit window (device picker if empty, params if filled).
|
* - LONG press -> clear the tile (asks to confirm first, since it's destructive).
|
||||||
* - LONG press -> empty the cell.
|
|
||||||
*
|
*
|
||||||
* Bottom half: a stacked two-octave keyboard (one octave per row) that plays the
|
* Bottom half: a stacked two-octave keyboard (one octave per row) for quick testing.
|
||||||
* currently-selected tile IF it is an instrument, for quick testing.
|
|
||||||
*
|
*
|
||||||
* The parameter editor is generated automatically from the device's [ToolboxType]
|
* The parameter editor is generated automatically from the device's [ToolboxType]
|
||||||
* parameter list, so new devices need no bespoke UI.
|
* parameter list, so new devices need no bespoke UI.
|
||||||
@@ -81,27 +79,33 @@ fun ToolboxScreen(vm: AppViewModel) {
|
|||||||
val c = LocalRetro.current
|
val c = LocalRetro.current
|
||||||
@Suppress("UNUSED_VARIABLE") val redraw = vm.revision
|
@Suppress("UNUSED_VARIABLE") val redraw = vm.revision
|
||||||
|
|
||||||
var selected by remember { mutableStateOf(-1) }
|
|
||||||
var editing by remember { mutableStateOf<Int?>(null) }
|
var editing by remember { mutableStateOf<Int?>(null) }
|
||||||
var picking 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.
|
// switches tabs.
|
||||||
BackHandler(enabled = editing != null || picking != null) {
|
BackHandler(enabled = editing != null || picking != null || clearing != null) {
|
||||||
if (editing != null) editing = null else picking = 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.
|
// Shared tile gestures (index-based) so both layouts drive the same state.
|
||||||
val onTapTile: (Int) -> Unit = { selected = it }
|
// Single tap: pick a device for an empty tile, else open the filled tile's settings.
|
||||||
val onDoubleTapTile: (Int) -> Unit = { i -> if (vm.project.toolbox[i].isEmpty) picking = i else editing = i }
|
val onTapTile: (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() }
|
// 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()) {
|
if (isLandscape()) {
|
||||||
// Landscape: a fixed 4x4 grid squashed to fit the height (no scroll) BESIDE
|
// 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
|
// the audition keyboard, which gets the larger width so its CH/OCT/VEL
|
||||||
// toolbar fits without horizontal scrolling.
|
// toolbar fits without horizontal scrolling.
|
||||||
Row(Modifier.fillMaxSize().background(c.background)) {
|
Row(Modifier.fillMaxSize().background(c.background)) {
|
||||||
ToolboxTileGrid(vm, selected, onTapTile, onDoubleTapTile, onLongPressTile,
|
ToolboxTileGrid(vm, onTapTile, onLongPressTile,
|
||||||
Modifier.weight(1f).fillMaxHeight())
|
Modifier.weight(1f).fillMaxHeight())
|
||||||
Box(Modifier.width(2.dp).fillMaxHeight().background(c.accent))
|
Box(Modifier.width(2.dp).fillMaxHeight().background(c.accent))
|
||||||
space.rcmd.android.sizzle.ui.StackedKeyboard(vm, punchIn = false, Modifier.weight(1.3f))
|
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 ->
|
items(vm.project.toolbox) { slot ->
|
||||||
SlotTile(
|
SlotTile(
|
||||||
rev = vm.revision, slot = slot, selected = selected == slot.index,
|
rev = vm.revision, slot = slot,
|
||||||
onTap = { onTapTile(slot.index) },
|
onTap = { onTapTile(slot.index) },
|
||||||
onDoubleTap = { onDoubleTapTile(slot.index) },
|
|
||||||
onLongPress = { onLongPressTile(slot.index) },
|
onLongPress = { onLongPressTile(slot.index) },
|
||||||
modifier = Modifier.fillMaxWidth().aspectRatio(1f),
|
modifier = Modifier.fillMaxWidth().aspectRatio(1f),
|
||||||
)
|
)
|
||||||
@@ -142,6 +145,23 @@ fun ToolboxScreen(vm: AppViewModel) {
|
|||||||
editing?.let { index ->
|
editing?.let { index ->
|
||||||
ParamEditor(vm, vm.project.toolbox[index], onClose = { editing = null })
|
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
|
/** 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
|
@Composable
|
||||||
private fun ToolboxTileGrid(
|
private fun ToolboxTileGrid(
|
||||||
vm: AppViewModel,
|
vm: AppViewModel,
|
||||||
selected: Int,
|
|
||||||
onTap: (Int) -> Unit,
|
onTap: (Int) -> Unit,
|
||||||
onDoubleTap: (Int) -> Unit,
|
|
||||||
onLongPress: (Int) -> Unit,
|
onLongPress: (Int) -> Unit,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -164,9 +182,7 @@ private fun ToolboxTileGrid(
|
|||||||
SlotTile(
|
SlotTile(
|
||||||
rev = vm.revision,
|
rev = vm.revision,
|
||||||
slot = vm.project.toolbox[idx],
|
slot = vm.project.toolbox[idx],
|
||||||
selected = selected == idx,
|
|
||||||
onTap = { onTap(idx) },
|
onTap = { onTap(idx) },
|
||||||
onDoubleTap = { onDoubleTap(idx) },
|
|
||||||
onLongPress = { onLongPress(idx) },
|
onLongPress = { onLongPress(idx) },
|
||||||
modifier = Modifier.weight(1f).fillMaxHeight(),
|
modifier = Modifier.weight(1f).fillMaxHeight(),
|
||||||
)
|
)
|
||||||
@@ -182,24 +198,22 @@ private fun ToolboxTileGrid(
|
|||||||
private fun SlotTile(
|
private fun SlotTile(
|
||||||
rev: Int,
|
rev: Int,
|
||||||
slot: ToolboxSlot,
|
slot: ToolboxSlot,
|
||||||
selected: Boolean,
|
|
||||||
onTap: () -> Unit,
|
onTap: () -> Unit,
|
||||||
onDoubleTap: () -> Unit,
|
|
||||||
onLongPress: () -> Unit,
|
onLongPress: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@Suppress("UNUSED_PARAMETER") val ignored = rev // param only used to bust skipping
|
@Suppress("UNUSED_PARAMETER") val ignored = rev // param only used to bust skipping
|
||||||
val c = LocalRetro.current
|
val c = LocalRetro.current
|
||||||
val borderColor = when { selected -> c.playhead; !slot.isEmpty -> c.accent; else -> c.grid }
|
val borderColor = if (!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 fill = if (!slot.isEmpty) c.accent.copy(alpha = 0.12f) else c.surface
|
||||||
Box(
|
Box(
|
||||||
modifier
|
modifier
|
||||||
.border(if (selected) 2.dp else 1.dp, borderColor, RectangleShape)
|
.border(1.dp, borderColor, RectangleShape)
|
||||||
.background(fill)
|
.background(fill)
|
||||||
.pointerInput(slot.index, slot.isEmpty) {
|
.pointerInput(slot.index, slot.isEmpty) {
|
||||||
|
// Single tap acts immediately (no double-tap wait); long press clears.
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onTap = { onTap() },
|
onTap = { onTap() },
|
||||||
onDoubleTap = { onDoubleTap() },
|
|
||||||
onLongPress = { onLongPress() },
|
onLongPress = { onLongPress() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user