Add Reset button, fix disk swap regression, and add sticky Shift

- Reset button (left of Sound) ejects disk and power-cycles the C64
    to BASIC without reloading — loadedADisks is cleared so episode
    buttons trigger autostart again on next press
  - A-side episode disks that were already loaded now hot-swap instead
    of resetting the machine; use Reset explicitly when a cold start
    is needed (fixes unintended reset when returning from hero disk)
  - Virtual keyboard Shift is now a toggle: tap SHF to lock shift on
    (key stays highlighted), tap again to release — no multi-touch
    required to type capital letters
This commit is contained in:
ml
2026-06-14 08:15:33 +02:00
parent 65597b8fce
commit e2cbf2b37d
5 changed files with 98 additions and 17 deletions
@@ -25,6 +25,9 @@ class C64Engine {
/** Hot-swap disk in drive 8 without resetting (use for B-side / hero disks). */ /** Hot-swap disk in drive 8 without resetting (use for B-side / hero disks). */
external fun attachDisk(path: String): Boolean external fun attachDisk(path: String): Boolean
/** Eject the disk and hard-reset the C64 to a BASIC ready prompt. */
external fun resetMachine(): Boolean
/** Run the emulator for one video frame (~20 000 CPU cycles). */ /** Run the emulator for one video frame (~20 000 CPU cycles). */
external fun runFrame() external fun runFrame()
@@ -91,6 +91,15 @@ class C64KeyboardView @JvmOverloads constructor(
private val heldKeys = mutableMapOf<Int, C64Key>() private val heldKeys = mutableMapOf<Int, C64Key>()
/* Sticky Shift: tapping either SHF key toggles LSHIFT on/off.
* When true, the SHF keys are drawn highlighted and LSHIFT stays held. */
private var shiftActive = false
private val lshiftKey = C64Key("SHF", C64Engine.KEY_LSHIFT)
private fun isShiftKey(key: C64Key) =
key.codes.size == 1 &&
(key.codes[0] == C64Engine.KEY_LSHIFT || key.codes[0] == C64Engine.KEY_RSHIFT)
private val keyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private val keyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#2C2C6C") color = Color.parseColor("#2C2C6C")
} }
@@ -127,12 +136,12 @@ class C64KeyboardView @JvmOverloads constructor(
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
val held = heldKeys.values.toSet() val held = heldKeys.values.toSet()
// Scale text so it fits roughly in half the row height
textPaint.textSize = (height / rows.size) * 0.38f textPaint.textSize = (height / rows.size) * 0.38f
for (row in keyRects) { for (row in keyRects) {
for ((rect, key) in row) { for ((rect, key) in row) {
canvas.drawRoundRect(rect, 5f, 5f, if (key in held) pressedPaint else keyPaint) val pressed = key in held || (shiftActive && isShiftKey(key))
canvas.drawRoundRect(rect, 5f, 5f, if (pressed) pressedPaint else keyPaint)
drawKeyLabel(canvas, rect, key.label) drawKeyLabel(canvas, rect, key.label)
} }
} }
@@ -157,8 +166,14 @@ class C64KeyboardView @JvmOverloads constructor(
when (event.actionMasked) { when (event.actionMasked) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
val key = keyAt(ex, ey) ?: return true val key = keyAt(ex, ey) ?: return true
if (isShiftKey(key)) {
// Toggle sticky shift; always use LSHIFT so there's no phantom held key.
shiftActive = !shiftActive
onKeyEvent?.invoke(lshiftKey, shiftActive)
} else {
heldKeys[pid] = key heldKeys[pid] = key
onKeyEvent?.invoke(key, true) onKeyEvent?.invoke(key, true)
}
invalidate() invalidate()
} }
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP,
@@ -80,6 +80,7 @@ class MainActivity : AppCompatActivity() {
private val episodeButtons = mutableMapOf<DiskSlot, Button>() private val episodeButtons = mutableMapOf<DiskSlot, Button>()
private val loadedADisks = mutableSetOf<Int>() private val loadedADisks = mutableSetOf<Int>()
private var activeSlot: DiskSlot? = null private var activeSlot: DiskSlot? = null
private var currentDiskPath: String? = null // path last passed to engine.loadDisk()
// ---- Disk import (multi-select) ----------------------------------------- // ---- Disk import (multi-select) -----------------------------------------
@@ -157,6 +158,18 @@ class MainActivity : AppCompatActivity() {
btnSound.text = if (soundEnabled) "Sound: ON" else "Sound: OFF" btnSound.text = if (soundEnabled) "Sound: ON" else "Sound: OFF"
} }
val btnReset = findViewById<Button>(R.id.btn_reset)
btnReset.setOnClickListener {
val ok = engine.resetMachine()
if (ok) {
loadedADisks.clear()
activeSlot = null
refreshDiskButtons()
drawerLayout.closeDrawer(GravityCompat.START)
}
appendLog(if (ok) "Reset: C64 reset to BASIC" else "Reset failed")
}
for (slot in diskSlots) { for (slot in diskSlots) {
val btn = findViewById<Button>(slot.btnId) val btn = findViewById<Button>(slot.btnId)
episodeButtons[slot] = btn episodeButtons[slot] = btn
@@ -291,12 +304,18 @@ class MainActivity : AppCompatActivity() {
appendLog("Disk not found: ${slot.fileName}") appendLog("Disk not found: ${slot.fileName}")
return return
} }
// A-side episode disks restart the machine; B-side and hero disks are hot-swapped. // Only reset + autostart for the first load of an A-side episode disk.
val resetMachine = slot.part == 'A' // Returning to an already-loaded episode (or any B-side / hero disk) is a hot-swap.
// Use the explicit Reset button to restart the machine on demand.
val resetMachine = slot.part == 'A' && slot.episode !in loadedADisks
val ok = if (resetMachine) engine.loadDisk(file.absolutePath) val ok = if (resetMachine) engine.loadDisk(file.absolutePath)
else engine.attachDisk(file.absolutePath) else engine.attachDisk(file.absolutePath)
if (ok) { if (ok) {
if (slot.part == 'A') loadedADisks.add(slot.episode) if (resetMachine) {
loadedADisks.add(slot.episode)
currentDiskPath = file.absolutePath
findViewById<Button>(R.id.btn_reset).isEnabled = true
}
activeSlot = slot activeSlot = slot
refreshDiskButtons() refreshDiskButtons()
} }
@@ -36,14 +36,16 @@ static uint32_t g_framebuf[FRAME_W * FRAME_H];
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
static int g_ready = 0; static int g_ready = 0;
/* Pending disk operations — written by Android thread, consumed by /* Pending disk/reset operations — written by Android thread, consumed by
* video_canvas_refresh on the VICE thread (both attach and autostart APIs * video_canvas_refresh on the VICE thread (machine_trigger_reset and disk APIs
* must be called from the VICE thread to avoid corrupting internal state). * must be called from the VICE thread to avoid corrupting internal state).
* g_pending_disk → autostart_disk (resets the machine, used for A-side disks) * g_pending_disk → autostart_disk (resets the machine, used for A-side disks)
* g_pending_attach → file_system_attach_disk (hot-swap, used for B-side / hero disks) */ * g_pending_attach → file_system_attach_disk (hot-swap, used for B-side / hero disks)
* g_pending_reset → detach disk + hard reset to BASIC prompt */
static pthread_mutex_t g_pending_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t g_pending_lock = PTHREAD_MUTEX_INITIALIZER;
static char g_pending_disk[512]; static char g_pending_disk[512];
static char g_pending_attach[512]; static char g_pending_attach[512];
static volatile int g_pending_reset = 0;
/* ========================================================================= /* =========================================================================
* VICE integration (compiled only when libvice.a is linked in) * VICE integration (compiled only when libvice.a is linked in)
@@ -352,7 +354,20 @@ void video_canvas_refresh(video_canvas_t *canvas,
unsigned int xs, unsigned int ys, unsigned int xs, unsigned int ys,
unsigned int xi, unsigned int yi, unsigned int xi, unsigned int yi,
unsigned int w, unsigned int h) { unsigned int w, unsigned int h) {
/* Process any pending disk operations on the VICE thread. */ /* Process any pending operations on the VICE thread. */
/* Hard reset: eject disk and return to BASIC prompt. */
if (g_pending_reset) {
g_pending_reset = 0;
pthread_mutex_lock(&g_pending_lock);
g_pending_disk[0] = '\0';
g_pending_attach[0] = '\0';
pthread_mutex_unlock(&g_pending_lock);
file_system_detach_disk(8, 0);
machine_trigger_reset(MACHINE_RESET_MODE_POWER_CYCLE);
g_io_frames = g_load_frames = g_attach_frames = 0;
}
pthread_mutex_lock(&g_pending_lock); pthread_mutex_lock(&g_pending_lock);
char autostart_path[512] = {0}; char autostart_path[512] = {0};
char attach_path[512] = {0}; char attach_path[512] = {0};
@@ -649,6 +664,16 @@ JNI_FN(jboolean, attachDisk)(JNIEnv *env, jobject obj, jstring path) {
#endif #endif
} }
JNI_FN(jboolean, resetMachine)(JNIEnv *env, jobject obj) {
(void)env; (void)obj;
#ifdef HAVE_VICE_SRC
g_pending_reset = 1;
return JNI_TRUE;
#else
return JNI_FALSE;
#endif
}
JNI_FN(jboolean, getDriveLed)(JNIEnv *env, jobject obj) { JNI_FN(jboolean, getDriveLed)(JNIEnv *env, jobject obj) {
(void)env; (void)obj; (void)env; (void)obj;
#ifdef HAVE_VICE_SRC #ifdef HAVE_VICE_SRC
@@ -235,16 +235,35 @@
android:text="Import Disks" android:text="Import Disks"
android:textSize="12sp" /> android:textSize="12sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btn_reset"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:text="Reset"
android:textSize="12sp"
android:enabled="false" />
<Button <Button
android:id="@+id/btn_sound" android:id="@+id/btn_sound"
style="?attr/materialButtonOutlinedStyle" style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Sound: OFF" android:text="Sound: OFF"
android:textSize="12sp" /> android:textSize="12sp" />
</LinearLayout> </LinearLayout>
</LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout> </androidx.drawerlayout.widget.DrawerLayout>
<!-- Splash screen overlay — covers everything until VICE is ready --> <!-- Splash screen overlay — covers everything until VICE is ready -->