diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 60388fa..65a592c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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" diff --git a/app/src/main/java/space/rcmd/android/sizzle/ui/toolbox/ToolboxScreen.kt b/app/src/main/java/space/rcmd/android/sizzle/ui/toolbox/ToolboxScreen.kt index e61cd24..d357345 100644 --- a/app/src/main/java/space/rcmd/android/sizzle/ui/toolbox/ToolboxScreen.kt +++ b/app/src/main/java/space/rcmd/android/sizzle/ui/toolbox/ToolboxScreen.kt @@ -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(null) } var picking by remember { mutableStateOf(null) } + var clearing by remember { mutableStateOf(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() }, ) },