yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
app_delegate.mm
Go to the documentation of this file.
1// AppDelegate.mm
2
3// Must define before any ImGui includes (needed by imgui_test_engine via editor_manager.h)
4#ifndef IMGUI_DEFINE_MATH_OPERATORS
5#define IMGUI_DEFINE_MATH_OPERATORS
6#endif
7
9#import "app/controller.h"
10#import "app/application.h"
16#import "core/project.h"
17#import "util/file_util.h"
18#import "app/editor/editor.h"
19#import "rom/rom.h"
20#include <chrono>
21#include <cstdio>
22#include <cstdlib>
23#include <string>
24#include <vector>
25
26#if defined(__APPLE__) && defined(__MACH__)
27/* Apple OSX and iOS (Darwin). */
28#include <TargetConditionals.h>
29
30#import <CoreText/CoreText.h>
31
32#if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1
33/* iOS in Xcode simulator */
34
35#elif TARGET_OS_MAC == 1
36/* macOS */
37#import <Cocoa/Cocoa.h>
38
39#ifndef YAZE_USE_SDL3
40#include <SDL.h>
41#endif
42
43namespace nm = yaze::platform::native_menu;
44
45namespace {
46
47// This file builds without ARC; hand freshly allocated menu objects to the
48// current autorelease pool so their owning NSMenu holds the only reference.
49template <typename T>
50T* Autoreleased(T* object) {
51#if __has_feature(objc_arc)
52 return object;
53#else
54 return [object autorelease];
55#endif
56}
57
58yaze::editor::EditorManager* CurrentEditorManager() {
59 auto& app = yaze::Application::Instance();
60 if (!app.IsReady() || app.GetController() == nullptr) {
61 return nullptr;
62 }
63 return app.GetController()->editor_manager();
64}
65
66// NativeMenuHost over the live EditorManager. Every side effect is queued on
67// EditorManager's deferred-action queue so it runs inside the next ImGui
68// frame, exactly where ShortcutManager callbacks run for keyboard chords.
69class EditorManagerMenuHost final : public nm::NativeMenuHost {
70 public:
71 yaze::editor::ShortcutManager* shortcut_manager() override {
72 auto* manager = CurrentEditorManager();
73 return manager ? manager->shortcut_manager() : nullptr;
74 }
75
76 bool HasLoadedRom() const override {
77 auto* manager = CurrentEditorManager();
78 if (manager == nullptr) {
79 return false;
80 }
81 auto* rom = manager->GetCurrentRom();
82 return rom != nullptr && rom->is_loaded();
83 }
84
85 bool HasCurrentEditor() const override {
86 auto* manager = CurrentEditorManager();
87 return manager != nullptr && manager->GetCurrentEditor() != nullptr;
88 }
89
90 // Not every editor keeps its history in the shared UndoManager (palette and
91 // assembly editors use their own stacks, others hold a pending action until
92 // Undo() finalizes it), so an empty UndoManager is not proof that Undo would
93 // do nothing. Enable whenever an editor is active; the title carries the
94 // UndoManager description when there is one.
95 bool CanUndo() const override { return HasCurrentEditor(); }
96 bool CanRedo() const override { return HasCurrentEditor(); }
97
98 bool WantsTextInput() const override {
99 return ImGui::GetCurrentContext() != nullptr &&
100 ImGui::GetIO().WantTextInput;
101 }
102
103 void Defer(std::function<void()> action) override {
104 if (auto* manager = CurrentEditorManager()) {
105 manager->QueueDeferredAction(std::move(action));
106 }
107 }
108
109 void InjectCommandChord(ImGuiKey key) override {
110 if (ImGui::GetCurrentContext() == nullptr) {
111 return;
112 }
113 ImGuiIO& io = ImGui::GetIO();
114 // The SDL backend reports Cmd as Super; ImGui swaps it to Ctrl when
115 // ConfigMacOSXBehaviors is on. Feed the same thing a real Cmd+key sends.
116 const ImGuiKey mod = io.ConfigMacOSXBehaviors ? ImGuiMod_Super : ImGuiMod_Ctrl;
117 io.AddKeyEvent(mod, true);
118 io.AddKeyEvent(key, true);
119 io.AddKeyEvent(key, false);
120 io.AddKeyEvent(mod, false);
121 }
122
123 void RunHostAction(nm::MenuAction action) override {
124 auto* manager = CurrentEditorManager();
125 if (manager == nullptr) {
126 return;
127 }
128 manager->QueueDeferredAction([action]() {
129 auto* m = CurrentEditorManager();
130 if (m == nullptr) {
131 return;
132 }
133 switch (action) {
134 case nm::MenuAction::kAbout:
135 if (auto* popups = m->popup_manager()) {
136 popups->Show(yaze::editor::PopupID::kAbout);
137 }
138 break;
139 case nm::MenuAction::kSettings:
140 m->SwitchToEditor(yaze::editor::EditorType::kSettings);
141 break;
142 case nm::MenuAction::kQuit:
143 m->Quit();
144 break;
145 case nm::MenuAction::kOpenRom:
146 (void)m->LoadRom();
147 break;
148 case nm::MenuAction::kOpenProject:
149 (void)m->OpenProject();
150 break;
151 case nm::MenuAction::kSave:
152 (void)m->SaveRom();
153 break;
154 case nm::MenuAction::kSaveAs:
155 if (auto* popups = m->popup_manager()) {
156 popups->Show(yaze::editor::PopupID::kSaveAs);
157 }
158 break;
159 case nm::MenuAction::kCloseRom:
160 m->CloseCurrentSession();
161 break;
162 case nm::MenuAction::kUndo:
163 if (auto* editor = m->GetCurrentEditor()) {
164 (void)editor->Undo();
165 }
166 break;
167 case nm::MenuAction::kRedo:
168 if (auto* editor = m->GetCurrentEditor()) {
169 (void)editor->Redo();
170 }
171 break;
172 case nm::MenuAction::kToggleSidebar:
173 m->window_manager().ToggleSidebarVisibility();
174 break;
175 case nm::MenuAction::kKeyboardShortcuts:
176 if (auto* ui = m->ui_coordinator()) {
177 ui->ShowShortcutsBrowser();
178 }
179 break;
180 default:
181 break;
182 }
183 });
184 }
185
186 std::vector<std::string> RecentFiles() const override {
188 }
189
190 void OpenRecentFile(const std::string& path) override {
191 Defer([path]() {
192 if (auto* m = CurrentEditorManager()) {
193 (void)m->OpenRomOrProject(path);
194 }
195 });
196 }
197
198 void ClearRecentFiles() override {
200 recent.Clear();
201 recent.Save();
202 }
203};
204
205EditorManagerMenuHost& MenuHost() {
206 static EditorManagerMenuHost host;
207 return host;
208}
209
210NSEventModifierFlags ModifierFlagsForChord(const nm::KeyChord& chord) {
211 NSEventModifierFlags flags = 0;
212 if (chord.command) flags |= NSEventModifierFlagCommand;
213 if (chord.shift) flags |= NSEventModifierFlagShift;
214 if (chord.option) flags |= NSEventModifierFlagOption;
215 if (chord.control) flags |= NSEventModifierFlagControl;
216 return flags;
217}
218
219nm::KeyChord ChordFromNSEvent(NSEvent* event) {
220 nm::KeyChord chord;
221 if (event.type != NSEventTypeKeyDown) {
222 return chord;
223 }
224 NSString* chars = nil;
225 if (@available(macOS 10.15, *)) {
226 chars = [event charactersByApplyingModifiers:0];
227 }
228 if (chars.length == 0) {
229 chars = event.charactersIgnoringModifiers;
230 }
231 if (chars.length != 1) {
232 return chord;
233 }
234 const unichar c = [chars.lowercaseString characterAtIndex:0];
235 if (c < 0x20 || c > 0x7e) {
236 return chord;
237 }
238 const NSEventModifierFlags flags = event.modifierFlags;
239 chord.key = static_cast<char>(c);
240 chord.command = (flags & NSEventModifierFlagCommand) != 0;
241 chord.shift = (flags & NSEventModifierFlagShift) != 0;
242 chord.option = (flags & NSEventModifierFlagOption) != 0;
243 chord.control = (flags & NSEventModifierFlagControl) != 0;
244 return chord;
245}
246
247void ApplyChordToItem(NSMenuItem* item, const nm::ResolvedItem& resolved) {
248 if (!resolved.chord.valid()) {
249 item.keyEquivalent = @"";
250 item.keyEquivalentModifierMask = 0;
251 return;
252 }
253 item.keyEquivalent = [NSString stringWithFormat:@"%c", resolved.chord.key];
254 item.keyEquivalentModifierMask = ModifierFlagsForChord(resolved.chord);
255}
256
257#ifndef YAZE_USE_SDL3
258SDL_EventFilter g_prev_sdl_filter = nullptr;
259void* g_prev_sdl_filter_userdata = nullptr;
260
261nm::KeyChord ChordFromSdlKey(const SDL_KeyboardEvent& key) {
262 nm::KeyChord chord;
263 const SDL_Keycode sym = key.keysym.sym;
264 if (sym < 0x20 || sym > 0x7e) {
265 return chord;
266 }
267 const Uint16 mod = key.keysym.mod;
268 chord.key = static_cast<char>(sym >= 'A' && sym <= 'Z' ? sym - 'A' + 'a' : sym);
269 chord.command = (mod & KMOD_GUI) != 0;
270 chord.shift = (mod & KMOD_SHIFT) != 0;
271 chord.option = (mod & KMOD_ALT) != 0;
272 chord.control = (mod & KMOD_CTRL) != 0;
273 return chord;
274}
275
276// SDL's Cocoa pump turns every NSEvent into an SDL key event *before*
277// [NSApp sendEvent:] lets NSMenu perform key equivalents. For chords the
278// native menu owns, drop the SDL copy so ImGui/ShortcutManager never sees it
279// (ShortcutManager matches modifiers loosely: Ctrl+Cmd+F would also fire Find).
280int YazeNativeMenuSdlFilter(void* userdata, SDL_Event* event) {
281 (void)userdata;
282 if (event != nullptr &&
283 (event->type == SDL_KEYDOWN || event->type == SDL_KEYUP)) {
284 const nm::KeyChord chord = ChordFromSdlKey(event->key);
285 if (chord.valid() &&
286 nm::IsNativeOwnedChord(MenuHost().shortcut_manager(), chord)) {
287 return 0;
288 }
289 }
290 if (g_prev_sdl_filter != nullptr) {
291 return g_prev_sdl_filter(g_prev_sdl_filter_userdata, event);
292 }
293 return 1;
294}
295#endif // YAZE_USE_SDL3
296
297} // namespace
298
299// Root menu: never performs a key equivalent ImGui owns. ImGui-owned chords (⌘S, ⌘Z, ⌘W, ...) are displayed on their items for
300// discoverability but fall through so SDL/ImGui runs them exactly once.
301@interface YazeMainMenu : NSMenu
302@end
303
304@implementation YazeMainMenu
305- (BOOL)performKeyEquivalent:(NSEvent*)event {
306 const nm::KeyChord chord = ChordFromNSEvent(event);
307 if (!nm::ShouldMenuPerformKeyEquivalent(MenuHost().shortcut_manager(),
308 chord)) {
309 return NO;
310 }
311 return [super performKeyEquivalent:event];
312}
313@end
314
315@interface YazeMenuController : NSObject <NSMenuDelegate, NSMenuItemValidation>
316@property(nonatomic, assign) NSMenu* openRecentMenu;
317- (void)performMenuAction:(NSMenuItem*)sender;
318- (void)openRecentFile:(NSMenuItem*)sender;
319- (void)clearRecentFiles:(NSMenuItem*)sender;
320@end
321
322@implementation YazeMenuController
323
324- (void)performMenuAction:(NSMenuItem*)sender {
325 nm::PerformAction(MenuHost(), static_cast<nm::MenuAction>(sender.tag));
326}
327
328- (void)openRecentFile:(NSMenuItem*)sender {
329 NSString* path = sender.representedObject;
330 if (path.length > 0) {
331 MenuHost().OpenRecentFile(path.UTF8String);
332 }
333}
334
335- (void)clearRecentFiles:(NSMenuItem*)sender {
336 (void)sender;
337 MenuHost().ClearRecentFiles();
338}
339
340- (BOOL)validateMenuItem:(NSMenuItem*)item {
341 if (item.action == @selector(performMenuAction:)) {
342 const auto action = static_cast<nm::MenuAction>(item.tag);
343 if (action == nm::MenuAction::kUndo || action == nm::MenuAction::kRedo) {
344 std::string title = action == nm::MenuAction::kUndo ? "Undo" : "Redo";
345 if (auto* manager = CurrentEditorManager()) {
346 if (auto* editor = manager->GetCurrentEditor()) {
347 const std::string desc = action == nm::MenuAction::kUndo
348 ? editor->GetUndoDescription()
349 : editor->GetRedoDescription();
350 if (!desc.empty()) {
351 title += " " + desc;
352 }
353 }
354 }
355 item.title = [NSString stringWithUTF8String:title.c_str()];
356 }
357 return nm::IsActionEnabled(MenuHost(), action) ? YES : NO;
358 }
359 if (item.action == @selector(openRecentFile:)) {
360 return CurrentEditorManager() != nullptr;
361 }
362 if (item.action == @selector(clearRecentFiles:)) {
363 return !MenuHost().RecentFiles().empty();
364 }
365 return YES;
366}
367
368- (void)rebuildOpenRecent:(NSMenu*)menu {
369 [menu removeAllItems];
370 const std::vector<std::string> files = MenuHost().RecentFiles();
371 for (const std::string& path : files) {
372 NSString* nsPath = [NSString stringWithUTF8String:path.c_str()];
373 NSString* label = [NSString
374 stringWithUTF8String:nm::RecentFileLabel(path).c_str()];
375 if (nsPath == nil || label == nil) {
376 continue;
377 }
378 NSMenuItem* item = Autoreleased([[NSMenuItem alloc] initWithTitle:label
379 action:@selector(openRecentFile:)
380 keyEquivalent:@""]);
381 item.target = self;
382 item.representedObject = nsPath;
383 item.toolTip = nsPath;
384 [menu addItem:item];
385 }
386 if (!files.empty()) {
387 [menu addItem:[NSMenuItem separatorItem]];
388 }
389 NSMenuItem* clear = Autoreleased([[NSMenuItem alloc] initWithTitle:@"Clear Menu"
390 action:@selector(clearRecentFiles:)
391 keyEquivalent:@""]);
392 clear.target = self;
393 [menu addItem:clear];
394}
395
396// Refresh chords every time a menu opens so rebinding in ShortcutManager (or a
397// compiled-out shortcut) is reflected, and repopulate Open Recent.
398- (void)menuNeedsUpdate:(NSMenu*)menu {
399 if (menu == self.openRecentMenu) {
400 [self rebuildOpenRecent:menu];
401 return;
402 }
403 auto* shortcuts = MenuHost().shortcut_manager();
404 for (NSMenuItem* item in menu.itemArray) {
405 if (item.isSeparatorItem || item.hasSubmenu || item.tag < 0 ||
406 item.tag >= static_cast<NSInteger>(nm::MenuAction::kCount)) {
407 continue;
408 }
409 ApplyChordToItem(item, nm::ResolveItem(
410 shortcuts, static_cast<nm::MenuAction>(item.tag)));
411 }
412}
413
414@end
415
416namespace {
417
418YazeMenuController* g_menu_controller = nil;
419
420SEL CocoaSelectorFor(nm::MenuAction action) {
421 switch (action) {
422 case nm::MenuAction::kHide:
423 return @selector(hide:);
424 case nm::MenuAction::kHideOthers:
425 return @selector(hideOtherApplications:);
426 case nm::MenuAction::kShowAll:
427 return @selector(unhideAllApplications:);
428 case nm::MenuAction::kToggleFullScreen:
429 return @selector(toggleFullScreen:);
430 case nm::MenuAction::kMinimize:
431 return @selector(performMiniaturize:);
432 case nm::MenuAction::kZoom:
433 return @selector(performZoom:);
434 case nm::MenuAction::kBringAllToFront:
435 return @selector(arrangeInFront:);
436 default:
437 return nullptr;
438 }
439}
440
441NSMenuItem* AddActionItem(NSMenu* menu, nm::MenuAction action) {
442 const nm::ActionSpec* spec = nm::FindSpec(action);
443 NSString* title = [NSString stringWithUTF8String:spec ? spec->title : "?"];
444 const nm::ResolvedItem resolved =
445 nm::ResolveItem(MenuHost().shortcut_manager(), action);
446 NSMenuItem* item = nil;
447 if (spec != nullptr && spec->dispatch == nm::Dispatch::kCocoa) {
448 // nil target: the responder chain routes these to NSApp or the SDL
449 // NSWindow, which also validates them (e.g. Minimize with no window).
450 item = Autoreleased([[NSMenuItem alloc] initWithTitle:title
451 action:CocoaSelectorFor(action)
452 keyEquivalent:@""]);
453 } else {
454 item = Autoreleased([[NSMenuItem alloc] initWithTitle:title
455 action:@selector(performMenuAction:)
456 keyEquivalent:@""]);
457 item.target = g_menu_controller;
458 }
459 item.tag = static_cast<NSInteger>(action);
460 ApplyChordToItem(item, resolved);
461 [menu addItem:item];
462 return item;
463}
464
465NSMenu* AddTopLevelMenu(NSMenu* mainMenu, NSString* title) {
466 NSMenu* menu = Autoreleased([[NSMenu alloc] initWithTitle:title]);
467 menu.delegate = g_menu_controller;
468 NSMenuItem* holder = Autoreleased([[NSMenuItem alloc] initWithTitle:title
469 action:nil
470 keyEquivalent:@""]);
471 holder.submenu = menu;
472 [mainMenu addItem:holder];
473 return menu;
474}
475
476// Build and install the main menu. Called after Application::Initialize, so
477// the SDL window exists and EditorManager has configured ShortcutManager.
478// SDL does not create its own menu here (NSApp already existed when SDL
479// registered), so this replaces whatever mainMenu there is.
480void InstallNativeMainMenu() {
481 if (g_menu_controller == nil) {
482 g_menu_controller = [[YazeMenuController alloc] init];
483 }
484 YazeMainMenu* mainMenu = Autoreleased([[YazeMainMenu alloc] initWithTitle:@"MainMenu"]);
485
486 // App menu (title is replaced by the process name in the menu bar).
487 NSMenu* appMenu = AddTopLevelMenu(mainMenu, @"yaze");
488 AddActionItem(appMenu, nm::MenuAction::kAbout);
489 [appMenu addItem:[NSMenuItem separatorItem]];
490 AddActionItem(appMenu, nm::MenuAction::kSettings);
491 [appMenu addItem:[NSMenuItem separatorItem]];
492 NSMenu* servicesMenu = Autoreleased([[NSMenu alloc] initWithTitle:@"Services"]);
493 NSMenuItem* servicesItem = Autoreleased([[NSMenuItem alloc] initWithTitle:@"Services"
494 action:nil
495 keyEquivalent:@""]);
496 servicesItem.submenu = servicesMenu;
497 servicesItem.tag = -1;
498 [appMenu addItem:servicesItem];
499 [NSApp setServicesMenu:servicesMenu];
500 [appMenu addItem:[NSMenuItem separatorItem]];
501 AddActionItem(appMenu, nm::MenuAction::kHide);
502 AddActionItem(appMenu, nm::MenuAction::kHideOthers);
503 AddActionItem(appMenu, nm::MenuAction::kShowAll);
504 [appMenu addItem:[NSMenuItem separatorItem]];
505 AddActionItem(appMenu, nm::MenuAction::kQuit);
506
507 NSMenu* fileMenu = AddTopLevelMenu(mainMenu, @"File");
508 AddActionItem(fileMenu, nm::MenuAction::kOpenRom);
509 AddActionItem(fileMenu, nm::MenuAction::kOpenProject);
510 NSMenu* recentMenu = Autoreleased([[NSMenu alloc] initWithTitle:@"Open Recent"]);
511 recentMenu.delegate = g_menu_controller;
512 g_menu_controller.openRecentMenu = recentMenu;
513 NSMenuItem* recentItem = Autoreleased([[NSMenuItem alloc] initWithTitle:@"Open Recent"
514 action:nil
515 keyEquivalent:@""]);
516 recentItem.submenu = recentMenu;
517 recentItem.tag = -1;
518 [fileMenu addItem:recentItem];
519 [g_menu_controller rebuildOpenRecent:recentMenu];
520 [fileMenu addItem:[NSMenuItem separatorItem]];
521 AddActionItem(fileMenu, nm::MenuAction::kSave);
522 AddActionItem(fileMenu, nm::MenuAction::kSaveAs);
523 [fileMenu addItem:[NSMenuItem separatorItem]];
524 AddActionItem(fileMenu, nm::MenuAction::kCloseRom);
525
526 NSMenu* editMenu = AddTopLevelMenu(mainMenu, @"Edit");
527 AddActionItem(editMenu, nm::MenuAction::kUndo);
528 AddActionItem(editMenu, nm::MenuAction::kRedo);
529 [editMenu addItem:[NSMenuItem separatorItem]];
530 AddActionItem(editMenu, nm::MenuAction::kCut);
531 AddActionItem(editMenu, nm::MenuAction::kCopy);
532 AddActionItem(editMenu, nm::MenuAction::kPaste);
533 AddActionItem(editMenu, nm::MenuAction::kSelectAll);
534
535 NSMenu* viewMenu = AddTopLevelMenu(mainMenu, @"View");
536 AddActionItem(viewMenu, nm::MenuAction::kToggleSidebar);
537 [viewMenu addItem:[NSMenuItem separatorItem]];
538 AddActionItem(viewMenu, nm::MenuAction::kToggleFullScreen);
539
540 NSMenu* windowMenu = AddTopLevelMenu(mainMenu, @"Window");
541 AddActionItem(windowMenu, nm::MenuAction::kMinimize);
542 AddActionItem(windowMenu, nm::MenuAction::kZoom);
543 [windowMenu addItem:[NSMenuItem separatorItem]];
544 AddActionItem(windowMenu, nm::MenuAction::kBringAllToFront);
545
546 NSMenu* helpMenu = AddTopLevelMenu(mainMenu, @"Help");
547 AddActionItem(helpMenu, nm::MenuAction::kKeyboardShortcuts);
548
549 [NSApp setMainMenu:mainMenu];
550 // setAppleMenu: is not public API on recent SDKs; the first item of the
551 // main menu is the application menu by convention.
552 [NSApp setWindowsMenu:windowMenu];
553 [NSApp setHelpMenu:helpMenu];
554
555#ifndef YAZE_USE_SDL3
556 SDL_EventFilter existing = nullptr;
557 void* existing_userdata = nullptr;
558 if (SDL_GetEventFilter(&existing, &existing_userdata) &&
559 existing != YazeNativeMenuSdlFilter) {
560 g_prev_sdl_filter = existing;
561 g_prev_sdl_filter_userdata = existing_userdata;
562 }
563 SDL_SetEventFilter(YazeNativeMenuSdlFilter, nullptr);
564#endif
565}
566
567std::string DescribeChord(const nm::KeyChord& chord) {
568 if (!chord.valid()) {
569 return "-";
570 }
571 std::string out;
572 if (chord.control) out += "Ctrl+";
573 if (chord.option) out += "Opt+";
574 if (chord.shift) out += "Shift+";
575 if (chord.command) out += "Cmd+";
576 out += chord.key;
577 return out;
578}
579
580const char* OwnerName(nm::ChordOwner owner) {
581 switch (owner) {
582 case nm::ChordOwner::kImGui:
583 return "imgui";
584 case nm::ChordOwner::kNative:
585 return "native";
586 default:
587 return "none";
588 }
589}
590
591// Diagnostics for hidden runs: YAZE_NATIVE_MENU_DUMP=<path> appends the live
592// menu tree, chord ownership, validation state, and dispatch self-checks.
593// Read-only: the self-checks never perform an ImGui-owned chord, and they
594// only ask whether a native chord is claimed (never perform ⌘M etc).
595void DumpNativeMenu(const char* phase) {
596 const char* path = std::getenv("YAZE_NATIVE_MENU_DUMP");
597 if (path == nullptr || path[0] == '\0') {
598 return;
599 }
600 FILE* out = std::fopen(path, "a");
601 if (out == nullptr) {
602 return;
603 }
604 auto* shortcuts = MenuHost().shortcut_manager();
605 std::fprintf(out, "== phase=%s shortcut_manager=%s rom_loaded=%d\n", phase,
606 shortcuts ? "yes" : "no", MenuHost().HasLoadedRom() ? 1 : 0);
607 NSMenu* mainMenu = [NSApp mainMenu];
608 std::fprintf(out, "mainMenu class=%s items=%ld\n",
609 mainMenu ? NSStringFromClass([mainMenu class]).UTF8String : "nil",
610 static_cast<long>(mainMenu.numberOfItems));
611 for (NSMenuItem* top in mainMenu.itemArray) {
612 NSMenu* sub = top.submenu;
613 // Mirror what AppKit does before display so chords/titles are current.
614 if (sub.delegate != nil &&
615 [sub.delegate respondsToSelector:@selector(menuNeedsUpdate:)]) {
616 [sub.delegate menuNeedsUpdate:sub];
617 }
618 std::fprintf(out, "[%s]\n", top.title.UTF8String);
619 for (NSMenuItem* item in sub.itemArray) {
620 if (item.isSeparatorItem) {
621 std::fprintf(out, " ---\n");
622 continue;
623 }
624 std::string owner = "n/a";
625 std::string resolved_chord = "-";
626 if (item.tag >= 0 &&
627 item.tag < static_cast<NSInteger>(nm::MenuAction::kCount)) {
628 const auto r = nm::ResolveItem(
629 shortcuts, static_cast<nm::MenuAction>(item.tag));
630 owner = OwnerName(r.owner);
631 resolved_chord = DescribeChord(r.chord);
632 }
633 BOOL enabled = YES;
634 id target = item.target;
635 if (target != nil &&
636 [target respondsToSelector:@selector(validateMenuItem:)]) {
637 enabled = [target validateMenuItem:item];
638 }
639 std::fprintf(out,
640 " %-24s key='%s' mods=0x%lx chord=%s owner=%s "
641 "action=%s enabled=%d%s%s\n",
642 item.title.UTF8String, item.keyEquivalent.UTF8String,
643 static_cast<unsigned long>(item.keyEquivalentModifierMask),
644 resolved_chord.c_str(), owner.c_str(),
645 item.action ? NSStringFromSelector(item.action).UTF8String
646 : "(none)",
647 enabled ? 1 : 0, item.hasSubmenu ? " [submenu]" : "",
648 item.isHidden ? " [hidden]" : "");
649 if (item.hasSubmenu) {
650 for (NSMenuItem* child in item.submenu.itemArray) {
651 std::fprintf(out, " - %s\n",
652 child.isSeparatorItem ? "---" : child.title.UTF8String);
653 }
654 }
655 }
656 }
657
658 // Self-check 1: the real NSMenu must decline ImGui-owned chords, so ⌘S
659 // reaches only SDL/ImGui. Safe to call: a NO result performs nothing.
660 struct Probe {
661 const char* label;
662 NSString* chars;
663 unsigned short key_code;
664 NSEventModifierFlags flags;
665 };
666 const Probe probes[] = {
667 {"Cmd+S", @"s", 1, NSEventModifierFlagCommand},
668 {"Cmd+Z", @"z", 6, NSEventModifierFlagCommand},
669 {"Cmd+W", @"w", 13, NSEventModifierFlagCommand},
670 {"Cmd+Q", @"q", 12, NSEventModifierFlagCommand},
671 };
672 for (const Probe& probe : probes) {
673 NSEvent* ev = [NSEvent keyEventWithType:NSEventTypeKeyDown
674 location:NSZeroPoint
675 modifierFlags:probe.flags
676 timestamp:0
677 windowNumber:0
678 context:nil
679 characters:probe.chars
680 charactersIgnoringModifiers:probe.chars
681 isARepeat:NO
682 keyCode:probe.key_code];
683 const nm::KeyChord chord = ChordFromNSEvent(ev);
684 // Only call into AppKit for chords the gate declines; a declined chord
685 // performs nothing, so the probe has no side effects.
686 if (nm::ShouldMenuPerformKeyEquivalent(shortcuts, chord)) {
687 std::fprintf(out, "selfcheck nsmenu %s gate=native (not probed)\n",
688 probe.label);
689 continue;
690 }
691 const BOOL performed = [mainMenu performKeyEquivalent:ev];
692 std::fprintf(out, "selfcheck nsmenu %s gate=imgui performed=%d\n",
693 probe.label, performed ? 1 : 0);
694 }
695
696#ifndef YAZE_USE_SDL3
697 // Self-check 2: the SDL filter keeps ImGui-owned chords and drops the SDL
698 // copy of native-owned ones (evaluated directly, no events are posted).
699 struct SdlProbe {
700 const char* label;
701 SDL_Keycode sym;
702 Uint16 mod;
703 };
704 const SdlProbe sdl_probes[] = {
705 {"Cmd+S", SDLK_s, KMOD_LGUI},
706 {"Cmd+Q", SDLK_q, KMOD_LGUI},
707 {"Cmd+W", SDLK_w, KMOD_LGUI},
708 {"Cmd+,", SDLK_COMMA, KMOD_LGUI},
709 {"Cmd+M", SDLK_m, KMOD_LGUI},
710 {"Ctrl+Cmd+F", SDLK_f, static_cast<Uint16>(KMOD_LGUI | KMOD_LCTRL)},
711 {"Opt+Cmd+H", SDLK_h, static_cast<Uint16>(KMOD_LGUI | KMOD_LALT)},
712 {"Cmd+H", SDLK_h, KMOD_LGUI},
713 };
714 for (const SdlProbe& probe : sdl_probes) {
715 SDL_Event ev{};
716 ev.type = SDL_KEYDOWN;
717 ev.key.keysym.sym = probe.sym;
718 ev.key.keysym.mod = probe.mod;
719 const int keep = YazeNativeMenuSdlFilter(nullptr, &ev);
720 std::fprintf(out, "selfcheck sdlfilter %s keep=%d\n", probe.label, keep);
721 }
722#endif
723 std::fclose(out);
724}
725
726} // namespace
727
728@interface AppDelegate : NSObject <NSApplicationDelegate>
729@end
730
731@implementation AppDelegate
732
733- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
734 // The main menu is installed after SDL initializes (InstallNativeMainMenu);
735 // [NSApp mainMenu] is still nil here because there is no nib.
736
737 // Disable automatic UI state persistence to prevent crashes
738 // macOS NSPersistentUIManager can crash if state gets corrupted
739 [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSQuitAlwaysKeepsWindows"];
740
741 // The native menu adds its own View > Enter Full Screen (⌃⌘F); stop AppKit
742 // from inserting a second copy.
743 [[NSUserDefaults standardUserDefaults] setBool:NO
744 forKey:@"NSFullScreenMenuItemEverywhere"];
745}
746
747- (NSApplicationTerminateReply)applicationShouldTerminate:
748 (NSApplication *)sender {
749 (void)sender;
750 auto& app = yaze::Application::Instance();
751 if (!app.IsReady() || app.GetController() == nullptr ||
752 app.GetController()->editor_manager() == nullptr) {
753 return NSTerminateNow;
754 }
755
756 // Cocoa's default terminate path calls exit() immediately. Because yaze
757 // drives its own SDL/ImGui loop instead of NSApplication::run, that bypasses
758 // Application::Shutdown and leaves the ImGui test engine bound to a live
759 // ImGui context. Route native Quit through EditorManager so unsaved-work
760 // guarding runs and the main loop can unwind resources in ownership order.
761 app.GetController()->editor_manager()->Quit();
762 return NSTerminateCancel;
763}
764
765@end
766
767extern "C" void yaze_initialize_cocoa() {
768 @autoreleasepool {
769 AppDelegate *delegate = [[AppDelegate alloc] init];
770 [NSApplication sharedApplication];
771 [NSApp setDelegate:delegate];
772 [NSApp finishLaunching];
773 }
774}
775
776extern "C" int yaze_run_cocoa_app_delegate(const yaze::AppConfig& config) {
777 yaze_initialize_cocoa();
778
779 // Initialize the Application singleton with the provided config
780 // This will create the Controller and the SDL Window
782
783 // Main loop
784 // We continue to run our own loop rather than [NSApp run]
785 // because we're driving SDL/ImGui manually.
786 // SDL's event polling works fine with Cocoa in this setup.
787
788 auto& app = yaze::Application::Instance();
789
790 if (app.IsReady()) {
791 @autoreleasepool {
792 InstallNativeMainMenu();
793 DumpNativeMenu("install");
794 }
795 }
796
797 const auto started = std::chrono::steady_clock::now();
798 bool late_dump_done = false;
799 while (app.IsReady() && app.GetController()->IsActive()) {
800 @autoreleasepool {
801 app.Tick();
802 if (!late_dump_done &&
803 std::chrono::steady_clock::now() - started > std::chrono::seconds(10)) {
804 late_dump_done = true;
805 DumpNativeMenu("late");
806 }
807 }
808 }
809
810 app.Shutdown();
811 return EXIT_SUCCESS;
812}
813
814#endif
815
816#endif
static Application & Instance()
void Initialize(const AppConfig &config)
bool is_loaded() const
Definition rom.h:187
The EditorManager controls the main editor window and manages the various editor classes.
static RecentFilesManager & GetInstance()
Definition project.h:598
const std::vector< std::string > & GetRecentFiles() const
Definition project.h:635
constexpr const char * kAbout
constexpr const char * kSaveAs
Configuration options for the application startup.
Definition application.h:26