Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions XCode/Project/Src/MacBridge/BeebEm-Bridging-Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ void beeb_consumer();
void beeb_handlekeys(long message, long wParam, long lParam); // long eventkind, unsigned long keycode, char charCode
void beeb_handlemouse(long message, long wParam, long lParam);

// Exposed to BeebViewController (for Caps Lock synchronization)
void beeb_syncCapsLockState(int macCapsLockIsOn); // C bool: 0=false, non-zero=true
void beeb_resetModifierTracking(long currentModifiers);
int beeb_getKeyboardMappingMode(); // Returns: 0=User, 1=Default, 2=Logical

// Notify Swift when user presses Caps Lock (for tracking legitimate state)
void swift_userDidPressCapsLock();

void beeb_handlejoystick(long message, long wParam, long lParam);


Expand Down
106 changes: 98 additions & 8 deletions XCode/Project/Src/MacBridge/BeebEm-Bridging-Keyboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ enum {
#include "SysVia.h"
extern BeebWin* mainWin;

// Forward declaration for Swift bridge function
extern "C" void swift_userDidPressCapsLock();

// map from APPLE input: https://eastmanreference.com/complete-list-of-applescript-key-codes
// to WINDOWS input: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
Expand Down Expand Up @@ -444,6 +446,56 @@ int remapKeys(int k)
/*
*/

// File-scoped variable to track last seen modifier state
// Moved from inside beeb_handlekeys to allow reset function access
static long last_wParam = 0;

// Send a complete Caps Lock key DOWN/UP cycle to the BBC emulator
// This triggers the BBC MOS to toggle its Caps Lock state
static void pressCapsLock()
{
// Send DOWN - BBC MOS detects key press and toggles Caps Lock state
int row, col;
mainWin->TranslateKey(VK_CAPITAL, false, row, col);

// Schedule UP for 50ms later (40ms Windows default + 25% reliability margin)
// This delay ensures BBC MOS has time to scan keyboard and process the key press
// The UP releases the key so the next toggle will be detected as a new press
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC),
dispatch_get_main_queue(), ^{
int up_row, up_col;
mainWin->TranslateKey(VK_CAPITAL, true, up_row, up_col);
});
}

// Sync BBC Caps Lock state to match Mac Caps Lock state
// Called on startup and when window gains focus
extern "C" void beeb_syncCapsLockState(int macCapsLockIsOn)
{
// Read current BBC Caps Lock state from LEDs global
bool macCapsOn = (macCapsLockIsOn != 0);
bool bbcCapsLockIsOn = LEDs.CapsLock;

// If states differ, send toggle to BBC to bring into sync
if (macCapsOn != bbcCapsLockIsOn) {
pressCapsLock();
}
}

// Reset modifier tracking to current Mac state
// Called when window gains focus to prevent stale state issues
extern "C" void beeb_resetModifierTracking(long currentModifiers)
{
last_wParam = currentModifiers;
}

// Query current keyboard mapping mode for Caps Lock sync behavior
extern "C" int beeb_getKeyboardMappingMode()
{
// Return keyboard mapping mode as integer
// 0 = User, 1 = Default, 2 = Logical
return static_cast<int>(mainWin->m_KeyboardMapping);
}

// SWIFT calls this to
extern "C" void beeb_handlekeys(long message, long wParam, long lParam)
Expand Down Expand Up @@ -488,10 +540,8 @@ extern "C" void beeb_handlekeys(long message, long wParam, long lParam)
}
break;
case kEventRawKeyModifiersChanged:
{
// fprintf(stderr, "Key modifier : code = %016x\n", wParam);

static long last_wParam = 0;

long diff_wParam = wParam ^ last_wParam; // XOR - to find what changed

// bitpatterns
Expand Down Expand Up @@ -524,15 +574,56 @@ extern "C" void beeb_handlekeys(long message, long wParam, long lParam)
mainWin->TranslateKey(VK_CONTROL, (wParam & CTRLMASK)==0, row, col);
}

// APPLE ALT KEY
if ((diff_wParam & ALTMASK)!=0) // left and right caps key
// APPLE ALT/OPTION KEY
if ((diff_wParam & ALTMASK)!=0) // left and right alt/option key
{
// UP when mask is 0, DOWN if mask is 1
mainWin->TranslateKey(VK_CAPITAL, (wParam & ALTMASK)==0, row, col);
// Maps to Windows VK_MENU (the Alt key - confusing name,
// but VK_MENU is indeed the Windows Alt key)
mainWin->TranslateKey(VK_MENU, (wParam & ALTMASK)==0, row, col);
}

// APPLE CAPS LOCK KEY
//
// Implementation Notes (discovered through systematic research and testing):
//
// 1. macOS Caps Lock behavior:
// - Toggle key with persistent state (LED on physical key)
// - We receive modifier change events when state toggles (ON to OFF)
// - Events report the NEW state, not press/release
//
// 2. BBC Micro Caps Lock behavior:
// - Momentary key at keyboard matrix position row 4, column 0
// - BBC MOS scans keyboard matrix and toggles IC32 bit 6 on key PRESS
// - IC32 bit 6 controls Caps Lock LED (active-low: 0=ON, 1=OFF)
// - The MOS toggles Caps Lock state on key DOWN only, not on key UP
//
// 3. Discovered through testing:
// - Sending KEY DOWN triggers BBC MOS to toggle Caps Lock LED
// - Sending KEY UP has no effect (TranslateKey returns row=-1, col=-1)
//
// 4. State synchronization implications:
// - Mac and BBC Caps Lock states can get out of phase when Mac key is
// toggled outside of BeebEm, e.g. while BeebEm window is unfocused
// so we take special action on focus gain to resynchronise states
//
// 5. Known limitations:
// - INKEY(-65) cannot detect "key held". This is a limitation of how
// Caps Lock works in macOS.
// - Games needing held Caps Lock should use "Map A,S to Caps,Ctrl" option
// - Initial state may be out of sync (BBC boots with LED ON due to IC32State=0x00)
// so we take special action on startup to sync states
//
if ((diff_wParam & CAPSMASK)!=0)
{
pressCapsLock();
// Notify Swift that user pressed Caps Lock (for legitimate state tracking)
swift_userDidPressCapsLock();
}

last_wParam = wParam;
break;
}
break;
}
}

Expand Down Expand Up @@ -628,7 +719,6 @@ extern "C" void beeb_handlejoystick(long message, long wParam, long lParam)
mainWin->AppProc(MM_JOY1BUTTONDOWN, buttons1, 0);
}
break;
break;
}

}
Expand Down
11 changes: 11 additions & 0 deletions XCode/Project/Swift/BeebBridge/BeebEm-Beeb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,14 @@ public func swift_SetMachineType(_ type: MachineModel)

CBridge.machineType = mtf
}

@_cdecl("swift_userDidPressCapsLock")
public func swift_userDidPressCapsLock()
{
// Notify BeebViewController that user pressed Caps Lock
// This is called from the C++ bridge when user presses physical Caps Lock
NotificationCenter.default.post(
name: NSNotification.Name("UserDidPressCapsLock"),
object: nil
)
}
134 changes: 122 additions & 12 deletions XCode/Project/Swift/BeebControllers/BeebViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,17 @@ class BeebViewController: NSViewController {
var screenFilename : String?

var BeebReady : Bool = false


// Tracks if BBC has initialized LEDs (indicates boot complete) and there it
// safe to assume that Caps Lock sync can be performed
private var bbcLedsInitialized = false

// Caps Lock synchronization state (Logical mode only)
private var bbcCapsLockLegitimateState = false // Last known state from user keypress
private var bbcCapsLockPendingState = false // State pending debounce confirmation
private var bbcCapsLockStateChangeTime: TimeInterval = 0 // Time of last LED state change
private var bbcCapsLockLastSyncTime: TimeInterval = 0 // Time of last sync operation

// var timer: Timer = Timer()


Expand Down Expand Up @@ -169,11 +179,17 @@ class BeebViewController: NSViewController {
if let window = view.window {
// set the delegate to respond to window being closed
window.delegate = self

window.acceptsMouseMovedEvents = true
}



// Observe user Caps Lock presses for legitimate state tracking
NotificationCenter.default.addObserver(
self,
selector: #selector(userDidPressCapsLock),
name: NSNotification.Name("UserDidPressCapsLock"),
object: nil
)

}

Expand Down Expand Up @@ -236,22 +252,34 @@ class BeebViewController: NSViewController {


deinit {
// Remove notification observer
NotificationCenter.default.removeObserver(self)

if (!BeebReady)
{
return
}

// Stop the display link. A better place to stop the link is in
// the viewController or windowController within functions such as
// windowWillClose(_:)

CVDisplayLinkStop(displayLink!)

// timer.invalidate()

end_cpu()
}

// Called when user presses Caps Lock key (notified from C++ bridge)
@objc func userDidPressCapsLock() {
// Toggle legitimate state
bbcCapsLockLegitimateState.toggle()
// Reset pending state since this is a known good change
bbcCapsLockPendingState = bbcCapsLockLegitimateState
bbcCapsLockStateChangeTime = 0 // Clear any pending debounce
}


private var width : Int = 0
private var height : Int = 0
Expand Down Expand Up @@ -317,6 +345,21 @@ extension BeebViewController

func LEDs_update()
{
// Detect first LED update - indicates BBC MOS has initialized the
// keyboard LEDs and will be responsive to Caps Lock changes
// This is the right moment to sync Caps Lock states
if !bbcLedsInitialized && !CBridge.leds.isEmpty {
bbcLedsInitialized = true

// BBC has now initialized - safe to sync Caps Lock
let macCapsLockIsOn = NSEvent.modifierFlags.contains(.capsLock)
beeb_syncCapsLockState(macCapsLockIsOn ? 1 : 0)
bbcCapsLockLegitimateState = macCapsLockIsOn // Initialize legitimate state
}

// Check for debounced Caps Lock sync (Logical mode only)
checkCapsLockDebounce()

WIPlabel.stringValue = CBridge.windowTitle

if #available(OSX 10.14, *) {
Expand All @@ -340,15 +383,82 @@ extension BeebViewController
}
}

private func checkCapsLockDebounce() {
// Only sync in Logical keyboard mapping mode (mode 2)
let keyboardMode = beeb_getKeyboardMappingMode()
guard keyboardMode == 2 else { // 2 = Logical mode
return
}

// Get current BBC Caps Lock LED state
let bbcCapsLockIsOn = CBridge.leds.contains(.CapsLED)

// Check if LED state differs from legitimate state
if bbcCapsLockIsOn != bbcCapsLockLegitimateState {
let currentTime = Date().timeIntervalSince1970

// Check if this is a new state change or continuation
if bbcCapsLockIsOn != bbcCapsLockPendingState {
// New state - restart debounce timer
bbcCapsLockPendingState = bbcCapsLockIsOn
bbcCapsLockStateChangeTime = currentTime
return
}

// State unchanged - check if debounce period (100ms) has elapsed
if currentTime - bbcCapsLockStateChangeTime < 0.1 {
return // Still within debounce period
}

// Check rate limit (500ms since last sync)
if currentTime - bbcCapsLockLastSyncTime < 0.5 {
return // Too soon since last sync
}

// Debounce and rate limit passed - trigger re-sync
let macCapsLockIsOn = NSEvent.modifierFlags.contains(.capsLock)

// Only sync if Mac and BBC states differ (avoid unnecessary sync)
if macCapsLockIsOn != bbcCapsLockIsOn {
beeb_syncCapsLockState(macCapsLockIsOn ? 1 : 0)
bbcCapsLockLastSyncTime = currentTime
bbcCapsLockLegitimateState = macCapsLockIsOn // Update to new expected state
}

bbcCapsLockStateChangeTime = 0 // Clear pending state
} else {
// LED now matches legitimate state - cancel any pending debounce
bbcCapsLockPendingState = bbcCapsLockIsOn
bbcCapsLockStateChangeTime = 0
}
}

}



extension BeebViewController: NSWindowDelegate {

// This method is called just before the window is closed.
func windowWillClose(_ notification: Notification) {
// if the BeebViewController goes, then just stop the app.
NSApp.terminate(self)
}

// This method is called when the window gains focus (becomes key window)
func windowDidBecomeKey(_ notification: Notification) {
// Sync Caps Lock states when window gains focus
// This ensures Mac and BBC Caps Lock stay synchronized even if
// the user changed Mac Caps Lock while BeebEm was not focused
let currentModifiers = NSEvent.modifierFlags
let macCapsLockIsOn = currentModifiers.contains(.capsLock)

beeb_syncCapsLockState(macCapsLockIsOn ? 1 : 0)

// Update legitimate state to match after sync
bbcCapsLockLegitimateState = macCapsLockIsOn

// Reset modifier tracking to prevent stale state detection
beeb_resetModifierTracking(Int(currentModifiers.rawValue))
}

// This method is called just before the window is closed.
func windowWillClose(_ notification: Notification) {
// if the BeebViewController goes, then just stop the app.
NSApp.terminate(self)
}
}