@@ -320,11 +320,56 @@ - (id)init
320320
321321 recentSnapFileNames = [NSMutableArray arrayWithCapacity: NUM_RECENT_ITEMS];
322322 [recentSnapFileNames retain ];
323+
324+ /* Resync the emulator's modifier ivars whenever the main window
325+ regains key status. AppKit delivers NSFlagsChanged to the key
326+ window's first responder, so a modifier release while a utility
327+ window was key never reaches DisplayOpenGLView and
328+ Emulator.commandDown stays stuck — dropping every subsequent
329+ keystroke in Emulator.keyDown:. The user-visible form is the
330+ Cmd+, → Preferences flow: open Settings, close, then the
331+ Spectrum keyboard ignores everything until the next modifier
332+ press triggers a real NSFlagsChanged on the main window. The
333+ filter inside the handler skips notifications for other
334+ windows. */
335+ [[NSNotificationCenter defaultCenter ]
336+ addObserver: self
337+ selector: @selector (mainWindowDidBecomeKey: )
338+ name: NSWindowDidBecomeKeyNotification
339+ object: nil ];
323340 }
324341
325342 return singleton;
326343}
327344
345+ - (void )mainWindowDidBecomeKey : (NSNotification *)note
346+ {
347+ /* The observer is registered with object:nil because the main window
348+ does not yet exist when the FuseController singleton is constructed
349+ during MainMenu.xib load. Filter to the main emulator window inside
350+ the handler — and bail out early if the main window does not yet
351+ exist, otherwise a non-nil note object would falsely pass the !=
352+ comparison against nil during startup. */
353+ NSWindow *mainWindow = [[DisplayOpenGLView instance ] window ];
354+ if ( !mainWindow ) return ;
355+ if ( [note object ] != mainWindow ) return ;
356+
357+ Emulator *emu = [Emulator instance ];
358+ if ( !emu ) return ;
359+
360+ NSEvent *sync = [NSEvent keyEventWithType: NSFlagsChanged
361+ location: NSZeroPoint
362+ modifierFlags: [NSEvent modifierFlags ]
363+ timestamp: 0
364+ windowNumber: 0
365+ context: nil
366+ characters: @" "
367+ charactersIgnoringModifiers: @" "
368+ isARepeat: NO
369+ keyCode: 0 ];
370+ [emu flagsChanged: sync];
371+ }
372+
328373- (IBAction )disk_eject_a : (id )sender
329374{
330375 cocoaui_disk_eject ( 0 );
@@ -652,7 +697,6 @@ - (IBAction)open:(id)sender
652697
653698 [[DisplayOpenGLView instance ] unpause ];
654699 }
655- [self releaseCmdKeys: @" o" withCode: QZ_o];
656700}
657701
658702- (IBAction )reset : (id )sender
@@ -697,13 +741,11 @@ - (IBAction)rzx_play:(id)sender
697741- (IBAction )rzx_insert_snap : (id )sender
698742{
699743 [[DisplayOpenGLView instance ] rzxInsertSnap ];
700- [self releaseCmdKeys: @" b" withCode: QZ_b];
701744}
702745
703746- (IBAction )rzx_rollback : (id )sender
704747{
705748 [[DisplayOpenGLView instance ] rzxRollback ];
706- [self releaseCmdKeys: @" z" withCode: QZ_z];
707749}
708750
709751- (IBAction )rzx_start : (id )sender
@@ -863,7 +905,6 @@ - (IBAction)save_as:(id)sender
863905save_as_exit:
864906 [[DisplayOpenGLView instance ] unpause ];
865907 }
866- [self releaseCmdKeys: @" s" withCode: QZ_s];
867908}
868909
869910- (IBAction )open_screen : (id )sender
@@ -991,13 +1032,11 @@ - (IBAction)hard_reset:(id)sender
9911032- (IBAction )joystick_keyboard : (id )sender
9921033{
9931034 [[DisplayOpenGLView instance ] joystickToggleKeyboard ];
994- [self releaseCmdKeys: @" j" withCode: QZ_j];
9951035}
9961036
9971037- (IBAction )keyboard_recreated : (id )sender
9981038{
9991039 [[DisplayOpenGLView instance ] keyboardToggleRecreatedZXSpectrum ];
1000- [self releaseCmdKeys: @" r" withCode: QZ_r];
10011040}
10021041
10031042- (IBAction )keyboard_arrows_shifted : (id )sender
@@ -1032,7 +1071,6 @@ - (IBAction)tape_open:(id)sender
10321071- (IBAction )tape_play : (id )sender
10331072{
10341073 [[DisplayOpenGLView instance ] tapeTogglePlay ];
1035- [self releaseCmdKeys: @" p" withCode: QZ_p];
10361074}
10371075
10381076- (IBAction )tape_rewind : (id )sender
@@ -1124,35 +1162,49 @@ - (IBAction)quit:(id)sender
11241162 if ( !settings_current.full_screen ) {
11251163 [[NSApp keyWindow ] performClose: self ];
11261164 }
1127- [self releaseCmdKeys: @" q" withCode: QZ_q];
11281165}
11291166
11301167- (IBAction )hide : (id )sender
11311168{
11321169 [NSApp hide: self ];
1133- [self releaseCmdKeys: @" h" withCode: QZ_h];
11341170}
11351171
11361172- (IBAction )help : (id )sender
11371173{
11381174 if ( !settings_current.full_screen ) {
11391175 [NSApp showHelp: self ];
11401176 }
1141- [self releaseCmdKeys: @" ?" withCode: QZ_SLASH];
11421177}
11431178
11441179- (IBAction )cocoa_break : (id )sender
11451180{
11461181 if ( gdbserver_debugging_enabled ) {
11471182 return ;
11481183 }
1184+
1185+ /* Re-entry: if the Debugger is already showing, bring it forward and
1186+ leave state alone. Calling debugger_activate: a second time would
1187+ double-pause and unbalance the counter. */
1188+ DebuggerController *dbg = [DebuggerController loadedSingleton ];
1189+ if ( dbg && [[dbg window ] isVisible ] ) {
1190+ [[dbg window ] makeKeyAndOrderFront: nil ];
1191+ return ;
1192+ }
1193+
11491194 if ( paused ) {
1150- debugger_mode = DEBUGGER_MODE_HALTED;
1195+ /* User-pause: release the user-pause contribution so the timer can
1196+ run, then set HALTED. The emulator detects HALTED on the next
1197+ instruction and ui_debugger_activate parks the thread on the
1198+ semaphore via the proper path. The user-pause is not restored on
1199+ dismissal — the user explicitly opened the Debugger from a paused
1200+ state, and Continue means "resume." */
11511201 paused = 0 ;
1152- [[DebuggerController singleton ] debugger_activate: nil ];
1153- } else {
1154- [[DisplayOpenGLView instance ] cocoaBreak ];
1202+ [[DisplayOpenGLView instance ] unpause ];
11551203 }
1204+ /* The utility-window case (fuse_emulation_paused > 0 && !paused) is
1205+ unreachable: validateMenuItem: disables the Debugger menu item in
1206+ that state. */
1207+ [[DisplayOpenGLView instance ] cocoaBreak ];
11561208 [self setPauseState ];
11571209}
11581210
@@ -1186,7 +1238,6 @@ - (IBAction)showKeyboardPane:(id)sender
11861238 [keyboardController showCloseWindow: self ];
11871239 }
11881240 }
1189- [self releaseCmdKeys: @" k" withCode: QZ_k];
11901241}
11911242
11921243- (IBAction )showLoadBinaryPane : (id )sender
@@ -1248,7 +1299,6 @@ - (IBAction)showPreferencesPane:(id)sender;
12481299 }
12491300 [preferencesController showWindow: self ];
12501301 }
1251- [self releaseCmdKeys: @" ." withCode: QZ_PERIOD];
12521302}
12531303
12541304- (IBAction )saveFileTypeClicked : (id )sender ;
@@ -1280,6 +1330,8 @@ - (IBAction)resetUserDefaults:(id)sender
12801330
12811331- (void )dealloc
12821332{
1333+ [[NSNotificationCenter defaultCenter ] removeObserver: self ];
1334+
12831335 [tapeBrowserController release ];
12841336 [keyboardController release ];
12851337 [saveBinaryController release ];
@@ -1291,55 +1343,6 @@ - (void)dealloc
12911343 [super dealloc ];
12921344}
12931345
1294- /* ------------------------------------------------------------------------------
1295- * releaseCmdKeys - This method fixes an issue when modal windows are used with
1296- * the Mac OSX version of the SDL library.
1297- * As the SDL normally captures all keystrokes, but we need to type in some
1298- * Mac windows, all of the control menu windows run in modal mode. However,
1299- * when this happens, the release of the command key and the shortcut key
1300- * are not sent to SDL. We have to manually cause these events to happen
1301- * to keep the SDL library in a sane state, otherwise only every other
1302- * shortcut keypress will work.
1303- *-----------------------------------------------------------------------------*/
1304- - (void ) releaseCmdKeys : (NSString *)character withCode : (int )keyCode
1305- {
1306- NSEvent *event1, *event2;
1307- NSPoint point = { 0 , 0 };
1308-
1309- event1 = [NSEvent keyEventWithType: NSKeyUp location: point modifierFlags: 0
1310- timestamp: 0 windowNumber: 0 context: nil characters: character
1311- charactersIgnoringModifiers: character isARepeat: NO
1312- keyCode: keyCode];
1313- [NSApp postEvent: event1 atStart: NO ];
1314-
1315- event2 = [NSEvent keyEventWithType: NSFlagsChanged location: point
1316- modifierFlags: 0 timestamp: 0 windowNumber: 0 context: nil
1317- characters: nil charactersIgnoringModifiers: nil isARepeat: NO
1318- keyCode: 0 ];
1319- [NSApp postEvent: event2 atStart: NO ];
1320- }
1321-
1322- /* ------------------------------------------------------------------------------
1323- * releaseKey - This method fixes an issue when modal windows are used with
1324- * the Mac OSX version of the SDL library.
1325- * As the SDL normally captures all keystrokes, but we need to type in some
1326- * Mac windows, all of the control menu windows run in modal mode. However,
1327- * when this happens, the release of function key which started the process
1328- * is not sent to SDL. We have to manually cause these events to happen
1329- * to keep the SDL library in a sane state, otherwise only everyother shortcut
1330- * keypress will work.
1331- *-----------------------------------------------------------------------------*/
1332- - (void ) releaseKey : (int )keyCode
1333- {
1334- NSEvent *event1;
1335- NSPoint point = { 0 , 0 };
1336-
1337- event1 = [NSEvent keyEventWithType: NSKeyUp location: point modifierFlags: 0
1338- timestamp: 0 windowNumber: 0 context: nil characters: @" "
1339- charactersIgnoringModifiers: @" " isARepeat: NO keyCode: keyCode];
1340- [NSApp postEvent: event1 atStart: NO ];
1341- }
1342-
13431346- (void )ui_menu_activate_media_cartridge : (NSNumber *)active
13441347{
13451348 [cartridge setEnabled: [active boolValue ]];
@@ -2112,7 +2115,22 @@ - (BOOL)validateMenuItem:(NSMenuItem*)menuItem
21122115 return multiface == 0 ? NO : YES ;
21132116 break ;
21142117 case 176 :
2115- return debuggerEnabled == 0 ? NO : YES ;
2118+ if ( !debuggerEnabled ) return NO ;
2119+ /* Disable Machine→Debugger when a utility window is holding the
2120+ emulator paused and the Debugger itself is not already up. The
2121+ menu cannot do anything useful in that state: cocoa_break: sets
2122+ HALTED, but z80 is not executing because the utility window's
2123+ pause stopped the timer, so no trap fires and the Debugger
2124+ window never appears until the user closes the utility. The
2125+ comparison must subtract the user-pause contribution so the
2126+ guard fires regardless of whether Machine→Pause is also set —
2127+ the utility-window holds are the part that breaks the trap
2128+ path. */
2129+ if ( fuse_emulation_paused > (paused ? 1 : 0 ) ) {
2130+ DebuggerController *dbg = [DebuggerController loadedSingleton ];
2131+ if ( !dbg || ![[dbg window ] isVisible ] ) return NO ;
2132+ }
2133+ return YES ;
21162134 break ;
21172135 default :
21182136 return YES ;
0 commit comments