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