Skip to content

Commit 8b5b22e

Browse files
Caps Lock synchronization between Mac and Beeb
Fixes issue where Mac Caps Lock key did not affect the emulated BBC Micro's Caps Lock state or LED indicator, and also goes to some lengths to ensure the states remain synchronized even when the BeebEm is not in focus. Key changes: 1. Fixed Alt key mapping bug - Was incorrectly mapped to VK_CAPITAL (Caps Lock) - Now correctly mapped to VK_MENU (Windows Alt key code) 2. Implemented Caps Lock handler with 50 ms pulse - Sends DOWN+UP pulse when Mac Caps Lock toggles - 50 ms delay = 40 ms Windows default + 25% safety margin 3. Added state synchronization - On startup: Syncs after BBC LED initialization detected which indicates the point in BBC MOS initialization after which Caps Lock state changes will be recognized - On focus gain: Re-syncs when window becomes active - Prevents desync when Caps Lock changed while app unfocused Testing note: macOS debounces Caps Lock key events internally to prevent accidental engagement of Caps Lock. Very rapid presses are filtered by macOS itself, and won't reach BeebEm. The physical LED on the macOS Caps Lock reflects the debounced state. Implementation notes: - BBC Caps Lock at keyboard matrix row 4, column 0 - IC32 bit 6 controls BBC keyboard Caps Lock LED (active-low) - BBC MOS toggles Caps Lock state on key DOWN only - Uses GCD dispatch_after for async key-up event after delay
1 parent 6345580 commit 8b5b22e

File tree

3 files changed

+126
-15
lines changed

3 files changed

+126
-15
lines changed

XCode/Project/Src/MacBridge/BeebEm-Bridging-Header.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ void beeb_consumer();
1616
void beeb_handlekeys(long message, long wParam, long lParam); // long eventkind, unsigned long keycode, char charCode
1717
void beeb_handlemouse(long message, long wParam, long lParam);
1818

19+
// Exposed to BeebViewController (for Caps Lock synchronization)
20+
void beeb_syncCapsLockState(int macCapsLockIsOn); // C bool: 0=false, non-zero=true
21+
void beeb_resetModifierTracking(long currentModifiers);
22+
1923
void beeb_handlejoystick(long message, long wParam, long lParam);
2024

2125

XCode/Project/Src/MacBridge/BeebEm-Bridging-Keyboard.cpp

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,48 @@ int remapKeys(int k)
444444
/*
445445
*/
446446

447+
// File-scoped variable to track last seen modifier state
448+
// Moved from inside beeb_handlekeys to allow reset function access
449+
static long last_wParam = 0;
450+
451+
// Send a complete Caps Lock key DOWN/UP cycle to the BBC emulator
452+
// This triggers the BBC MOS to toggle its Caps Lock state
453+
static void pressCapsLock()
454+
{
455+
// Send DOWN - BBC MOS detects key press and toggles Caps Lock state
456+
int row, col;
457+
mainWin->TranslateKey(VK_CAPITAL, false, row, col);
458+
459+
// Schedule UP for 50ms later (40ms Windows default + 25% reliability margin)
460+
// This delay ensures BBC MOS has time to scan keyboard and process the key press
461+
// The UP releases the key so the next toggle will be detected as a new press
462+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC),
463+
dispatch_get_main_queue(), ^{
464+
int up_row, up_col;
465+
mainWin->TranslateKey(VK_CAPITAL, true, up_row, up_col);
466+
});
467+
}
468+
469+
// Sync BBC Caps Lock state to match Mac Caps Lock state
470+
// Called on startup and when window gains focus
471+
extern "C" void beeb_syncCapsLockState(int macCapsLockIsOn)
472+
{
473+
// Read current BBC Caps Lock state from LEDs global
474+
bool macCapsOn = (macCapsLockIsOn != 0);
475+
bool bbcCapsLockIsOn = LEDs.CapsLock;
476+
477+
// If states differ, send toggle to BBC to bring into sync
478+
if (macCapsOn != bbcCapsLockIsOn) {
479+
pressCapsLock();
480+
}
481+
}
482+
483+
// Reset modifier tracking to current Mac state
484+
// Called when window gains focus to prevent stale state issues
485+
extern "C" void beeb_resetModifierTracking(long currentModifiers)
486+
{
487+
last_wParam = currentModifiers;
488+
}
447489

448490
// SWIFT calls this to
449491
extern "C" void beeb_handlekeys(long message, long wParam, long lParam)
@@ -488,10 +530,8 @@ extern "C" void beeb_handlekeys(long message, long wParam, long lParam)
488530
}
489531
break;
490532
case kEventRawKeyModifiersChanged:
533+
{
491534
// fprintf(stderr, "Key modifier : code = %016x\n", wParam);
492-
493-
static long last_wParam = 0;
494-
495535
long diff_wParam = wParam ^ last_wParam; // XOR - to find what changed
496536

497537
// bitpatterns
@@ -524,15 +564,54 @@ extern "C" void beeb_handlekeys(long message, long wParam, long lParam)
524564
mainWin->TranslateKey(VK_CONTROL, (wParam & CTRLMASK)==0, row, col);
525565
}
526566

527-
// APPLE ALT KEY
528-
if ((diff_wParam & ALTMASK)!=0) // left and right caps key
567+
// APPLE ALT/OPTION KEY
568+
if ((diff_wParam & ALTMASK)!=0) // left and right alt/option key
529569
{
530570
// UP when mask is 0, DOWN if mask is 1
531-
mainWin->TranslateKey(VK_CAPITAL, (wParam & ALTMASK)==0, row, col);
571+
// Maps to Windows VK_MENU (the Alt key - confusing name,
572+
// but VK_MENU is indeed the Windows Alt key)
573+
mainWin->TranslateKey(VK_MENU, (wParam & ALTMASK)==0, row, col);
574+
}
575+
576+
// APPLE CAPS LOCK KEY
577+
//
578+
// Implementation Notes (discovered through systematic research and testing):
579+
//
580+
// 1. macOS Caps Lock behavior:
581+
// - Toggle key with persistent state (LED on physical key)
582+
// - We receive modifier change events when state toggles (ON to OFF)
583+
// - Events report the NEW state, not press/release
584+
//
585+
// 2. BBC Micro Caps Lock behavior:
586+
// - Momentary key at keyboard matrix position row 4, column 0
587+
// - BBC MOS scans keyboard matrix and toggles IC32 bit 6 on key PRESS
588+
// - IC32 bit 6 controls Caps Lock LED (active-low: 0=ON, 1=OFF)
589+
// - The MOS toggles Caps Lock state on key DOWN only, not on key UP
590+
//
591+
// 3. Discovered through testing:
592+
// - Sending KEY DOWN triggers BBC MOS to toggle Caps Lock LED
593+
// - Sending KEY UP has no effect (TranslateKey returns row=-1, col=-1)
594+
//
595+
// 4. State synchronization implications:
596+
// - Mac and BBC Caps Lock states can get out of phase when Mac key is
597+
// toggled outside of BeebEm, e.g. while BeebEm window is unfocused
598+
// so we take special action on focus gain to resynchronise states
599+
//
600+
// 5. Known limitations:
601+
// - INKEY(-65) cannot detect "key held". This is a limitation of how
602+
// Caps Lock works in macOS.
603+
// - Games needing held Caps Lock should use "Map A,S to Caps,Ctrl" option
604+
// - Initial state may be out of sync (BBC boots with LED ON due to IC32State=0x00)
605+
// so we take special action on startup to sync states
606+
//
607+
if ((diff_wParam & CAPSMASK)!=0)
608+
{
609+
pressCapsLock();
532610
}
533611

534612
last_wParam = wParam;
535-
break;
613+
}
614+
break;
536615
}
537616
}
538617

@@ -628,7 +707,6 @@ extern "C" void beeb_handlejoystick(long message, long wParam, long lParam)
628707
mainWin->AppProc(MM_JOY1BUTTONDOWN, buttons1, 0);
629708
}
630709
break;
631-
break;
632710
}
633711

634712
}

XCode/Project/Swift/BeebControllers/BeebViewController.swift

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ class BeebViewController: NSViewController {
3838
var screenFilename : String?
3939

4040
var BeebReady : Bool = false
41-
41+
42+
// Tracks if BBC has initialized LEDs (indicates boot complete) and there it
43+
// safe to assume that Caps Lock sync can be performed
44+
private var bbcLedsInitialized = false
45+
4246
// var timer: Timer = Timer()
4347

4448

@@ -317,6 +321,17 @@ extension BeebViewController
317321

318322
func LEDs_update()
319323
{
324+
// Detect first LED update - indicates BBC MOS has initialized the
325+
// keyboard LEDs and will be responsive to Caps Lock changes
326+
// This is the right moment to sync Caps Lock states
327+
if !bbcLedsInitialized && !CBridge.leds.isEmpty {
328+
bbcLedsInitialized = true
329+
330+
// BBC has now initialized - safe to sync Caps Lock
331+
let macCapsLockIsOn = NSEvent.modifierFlags.contains(.capsLock)
332+
beeb_syncCapsLockState(macCapsLockIsOn ? 1 : 0)
333+
}
334+
320335
WIPlabel.stringValue = CBridge.windowTitle
321336

322337
if #available(OSX 10.14, *) {
@@ -345,10 +360,24 @@ extension BeebViewController
345360

346361

347362
extension BeebViewController: NSWindowDelegate {
348-
349-
// This method is called just before the window is closed.
350-
func windowWillClose(_ notification: Notification) {
351-
// if the BeebViewController goes, then just stop the app.
352-
NSApp.terminate(self)
353-
}
363+
364+
// This method is called when the window gains focus (becomes key window)
365+
func windowDidBecomeKey(_ notification: Notification) {
366+
// Sync Caps Lock states when window gains focus
367+
// This ensures Mac and BBC Caps Lock stay synchronized even if
368+
// the user changed Mac Caps Lock while BeebEm was not focused
369+
let currentModifiers = NSEvent.modifierFlags
370+
let macCapsLockIsOn = currentModifiers.contains(.capsLock)
371+
372+
beeb_syncCapsLockState(macCapsLockIsOn ? 1 : 0)
373+
374+
// Reset modifier tracking to prevent stale state detection
375+
beeb_resetModifierTracking(Int(currentModifiers.rawValue))
376+
}
377+
378+
// This method is called just before the window is closed.
379+
func windowWillClose(_ notification: Notification) {
380+
// if the BeebViewController goes, then just stop the app.
381+
NSApp.terminate(self)
382+
}
354383
}

0 commit comments

Comments
 (0)