4#ifndef IMGUI_DEFINE_MATH_OPERATORS
5#define IMGUI_DEFINE_MATH_OPERATORS
26#if defined(__APPLE__) && defined(__MACH__)
28#include <TargetConditionals.h>
30#import <CoreText/CoreText.h>
32#if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1
35#elif TARGET_OS_MAC == 1
37#import <Cocoa/Cocoa.h>
50T* Autoreleased(T*
object) {
51#if __has_feature(objc_arc)
54 return [
object autorelease];
60 if (!app.IsReady() || app.GetController() ==
nullptr) {
63 return app.GetController()->editor_manager();
69class EditorManagerMenuHost final :
public nm::NativeMenuHost {
72 auto* manager = CurrentEditorManager();
73 return manager ? manager->shortcut_manager() :
nullptr;
76 bool HasLoadedRom()
const override {
77 auto* manager = CurrentEditorManager();
78 if (manager ==
nullptr) {
81 auto* rom = manager->GetCurrentRom();
82 return rom !=
nullptr && rom->
is_loaded();
85 bool HasCurrentEditor()
const override {
86 auto* manager = CurrentEditorManager();
87 return manager !=
nullptr && manager->GetCurrentEditor() !=
nullptr;
95 bool CanUndo()
const override {
return HasCurrentEditor(); }
96 bool CanRedo()
const override {
return HasCurrentEditor(); }
98 bool WantsTextInput()
const override {
99 return ImGui::GetCurrentContext() !=
nullptr &&
100 ImGui::GetIO().WantTextInput;
103 void Defer(std::function<
void()> action)
override {
104 if (
auto* manager = CurrentEditorManager()) {
105 manager->QueueDeferredAction(std::move(action));
109 void InjectCommandChord(ImGuiKey key)
override {
110 if (ImGui::GetCurrentContext() ==
nullptr) {
113 ImGuiIO& io = ImGui::GetIO();
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);
123 void RunHostAction(nm::MenuAction action)
override {
124 auto* manager = CurrentEditorManager();
125 if (manager ==
nullptr) {
128 manager->QueueDeferredAction([action]() {
129 auto* m = CurrentEditorManager();
134 case nm::MenuAction::kAbout:
135 if (
auto* popups = m->popup_manager()) {
139 case nm::MenuAction::kSettings:
142 case nm::MenuAction::kQuit:
145 case nm::MenuAction::kOpenRom:
148 case nm::MenuAction::kOpenProject:
149 (void)m->OpenProject();
151 case nm::MenuAction::kSave:
154 case nm::MenuAction::kSaveAs:
155 if (
auto* popups = m->popup_manager()) {
159 case nm::MenuAction::kCloseRom:
160 m->CloseCurrentSession();
162 case nm::MenuAction::kUndo:
163 if (
auto* editor = m->GetCurrentEditor()) {
164 (void)editor->Undo();
167 case nm::MenuAction::kRedo:
168 if (
auto* editor = m->GetCurrentEditor()) {
169 (void)editor->Redo();
172 case nm::MenuAction::kToggleSidebar:
173 m->window_manager().ToggleSidebarVisibility();
175 case nm::MenuAction::kKeyboardShortcuts:
176 if (
auto* ui = m->ui_coordinator()) {
177 ui->ShowShortcutsBrowser();
186 std::vector<std::string> RecentFiles()
const override {
190 void OpenRecentFile(
const std::string& path)
override {
192 if (
auto* m = CurrentEditorManager()) {
193 (void)m->OpenRomOrProject(path);
198 void ClearRecentFiles()
override {
205EditorManagerMenuHost& MenuHost() {
206 static EditorManagerMenuHost host;
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;
219nm::KeyChord ChordFromNSEvent(NSEvent* event) {
221 if (event.type != NSEventTypeKeyDown) {
224 NSString* chars = nil;
225 if (@available(macOS 10.15, *)) {
226 chars = [
event charactersByApplyingModifiers:0];
228 if (chars.length == 0) {
229 chars =
event.charactersIgnoringModifiers;
231 if (chars.length != 1) {
234 const unichar c = [chars.lowercaseString characterAtIndex:0];
235 if (c < 0x20 || c > 0x7e) {
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;
247void ApplyChordToItem(NSMenuItem* item,
const nm::ResolvedItem& resolved) {
248 if (!resolved.chord.valid()) {
249 item.keyEquivalent =
@"";
250 item.keyEquivalentModifierMask = 0;
253 item.keyEquivalent = [NSString stringWithFormat:
@"%c", resolved.chord.key];
254 item.keyEquivalentModifierMask = ModifierFlagsForChord(resolved.chord);
258SDL_EventFilter g_prev_sdl_filter =
nullptr;
259void* g_prev_sdl_filter_userdata =
nullptr;
261nm::KeyChord ChordFromSdlKey(
const SDL_KeyboardEvent& key) {
263 const SDL_Keycode sym = key.keysym.sym;
264 if (sym < 0x20 || sym > 0x7e) {
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;
280int YazeNativeMenuSdlFilter(
void* userdata, SDL_Event* event) {
282 if (event !=
nullptr &&
283 (event->type == SDL_KEYDOWN || event->type == SDL_KEYUP)) {
284 const nm::KeyChord chord = ChordFromSdlKey(event->key);
286 nm::IsNativeOwnedChord(MenuHost().shortcut_manager(), chord)) {
290 if (g_prev_sdl_filter !=
nullptr) {
291 return g_prev_sdl_filter(g_prev_sdl_filter_userdata, event);
301@interface YazeMainMenu : NSMenu
304@implementation YazeMainMenu
305- (BOOL)performKeyEquivalent:(NSEvent*)event {
306 const nm::KeyChord chord = ChordFromNSEvent(event);
307 if (!nm::ShouldMenuPerformKeyEquivalent(MenuHost().shortcut_manager(),
311 return [
super performKeyEquivalent:event];
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;
322@implementation YazeMenuController
324- (void)performMenuAction:(NSMenuItem*)sender {
325 nm::PerformAction(MenuHost(),
static_cast<nm::MenuAction
>(sender.tag));
328- (void)openRecentFile:(NSMenuItem*)sender {
329 NSString* path = sender.representedObject;
330 if (path.length > 0) {
331 MenuHost().OpenRecentFile(path.UTF8String);
335- (void)clearRecentFiles:(NSMenuItem*)sender {
337 MenuHost().ClearRecentFiles();
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();
355 item.title = [NSString stringWithUTF8String:title.c_str()];
357 return nm::IsActionEnabled(MenuHost(), action) ? YES : NO;
359 if (item.action ==
@selector(openRecentFile:)) {
360 return CurrentEditorManager() !=
nullptr;
362 if (item.action ==
@selector(clearRecentFiles:)) {
363 return !MenuHost().RecentFiles().empty();
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) {
378 NSMenuItem* item = Autoreleased([[NSMenuItem alloc] initWithTitle:label
379 action:@selector(openRecentFile:)
382 item.representedObject = nsPath;
383 item.toolTip = nsPath;
386 if (!files.empty()) {
387 [menu addItem:[NSMenuItem separatorItem]];
389 NSMenuItem* clear = Autoreleased([[NSMenuItem alloc] initWithTitle:
@"Clear Menu"
390 action:@selector(clearRecentFiles:)
393 [menu addItem:clear];
398- (void)menuNeedsUpdate:(NSMenu*)menu {
399 if (menu == self.openRecentMenu) {
400 [
self rebuildOpenRecent:menu];
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)) {
409 ApplyChordToItem(item, nm::ResolveItem(
410 shortcuts,
static_cast<nm::MenuAction
>(item.tag)));
418YazeMenuController* g_menu_controller = nil;
420SEL CocoaSelectorFor(nm::MenuAction 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:);
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) {
450 item = Autoreleased([[NSMenuItem alloc] initWithTitle:title
451 action:CocoaSelectorFor(action)
454 item = Autoreleased([[NSMenuItem alloc] initWithTitle:title
455 action:@selector(performMenuAction:)
457 item.target = g_menu_controller;
459 item.tag =
static_cast<NSInteger
>(action);
460 ApplyChordToItem(item, resolved);
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
471 holder.submenu = menu;
472 [mainMenu addItem:holder];
480void InstallNativeMainMenu() {
481 if (g_menu_controller == nil) {
482 g_menu_controller = [[YazeMenuController alloc] init];
484 YazeMainMenu* mainMenu = Autoreleased([[YazeMainMenu alloc] initWithTitle:
@"MainMenu"]);
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"
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);
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"
516 recentItem.submenu = recentMenu;
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);
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);
535 NSMenu* viewMenu = AddTopLevelMenu(mainMenu,
@"View");
536 AddActionItem(viewMenu, nm::MenuAction::kToggleSidebar);
537 [viewMenu addItem:[NSMenuItem separatorItem]];
538 AddActionItem(viewMenu, nm::MenuAction::kToggleFullScreen);
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);
546 NSMenu* helpMenu = AddTopLevelMenu(mainMenu,
@"Help");
547 AddActionItem(helpMenu, nm::MenuAction::kKeyboardShortcuts);
549 [NSApp setMainMenu:mainMenu];
552 [NSApp setWindowsMenu:windowMenu];
553 [NSApp setHelpMenu:helpMenu];
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;
563 SDL_SetEventFilter(YazeNativeMenuSdlFilter,
nullptr);
567std::string DescribeChord(
const nm::KeyChord& chord) {
568 if (!chord.valid()) {
572 if (chord.control) out += "Ctrl+";
573 if (chord.option) out += "Opt+";
574 if (chord.shift) out += "Shift+";
575 if (chord.command) out += "
Cmd+";
580const char* OwnerName(nm::ChordOwner owner) {
582 case nm::ChordOwner::kImGui:
584 case nm::ChordOwner::kNative:
595void DumpNativeMenu(
const char* phase) {
596 const char* path = std::getenv(
"YAZE_NATIVE_MENU_DUMP");
597 if (path ==
nullptr || path[0] ==
'\0') {
600 FILE* out = std::fopen(path,
"a");
601 if (out ==
nullptr) {
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;
614 if (sub.delegate != nil &&
615 [sub.delegate respondsToSelector:@selector(menuNeedsUpdate:)]) {
616 [sub.delegate menuNeedsUpdate:sub];
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");
624 std::string owner =
"n/a";
625 std::string resolved_chord =
"-";
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);
634 id target = item.target;
636 [target respondsToSelector:@selector(validateMenuItem:)]) {
637 enabled = [target validateMenuItem:item];
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
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);
663 unsigned short key_code;
664 NSEventModifierFlags flags;
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},
672 for (
const Probe& probe : probes) {
673 NSEvent* ev = [NSEvent keyEventWithType:NSEventTypeKeyDown
675 modifierFlags:probe.flags
679 characters:probe.chars
680 charactersIgnoringModifiers:probe.chars
682 keyCode:probe.key_code];
683 const nm::KeyChord chord = ChordFromNSEvent(ev);
686 if (nm::ShouldMenuPerformKeyEquivalent(shortcuts, chord)) {
687 std::fprintf(out,
"selfcheck nsmenu %s gate=native (not probed)\n",
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);
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},
714 for (
const SdlProbe& probe : sdl_probes) {
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);
728@interface AppDelegate : NSObject <NSApplicationDelegate>
733- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
739 [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSQuitAlwaysKeepsWindows"];
743 [[NSUserDefaults standardUserDefaults] setBool:NO
744 forKey:@"NSFullScreenMenuItemEverywhere"];
747- (NSApplicationTerminateReply)applicationShouldTerminate:
748 (NSApplication *)sender {
751 if (!app.IsReady() || app.GetController() ==
nullptr ||
752 app.GetController()->editor_manager() ==
nullptr) {
753 return NSTerminateNow;
761 app.GetController()->editor_manager()->Quit();
762 return NSTerminateCancel;
767extern "C" void yaze_initialize_cocoa() {
770 [NSApplication sharedApplication];
771 [NSApp setDelegate:delegate];
772 [NSApp finishLaunching];
776extern "C" int yaze_run_cocoa_app_delegate(
const yaze::AppConfig& config) {
777 yaze_initialize_cocoa();
792 InstallNativeMainMenu();
793 DumpNativeMenu(
"install");
797 const auto started = std::chrono::steady_clock::now();
798 bool late_dump_done =
false;
799 while (app.IsReady() && app.GetController()->IsActive()) {
802 if (!late_dump_done &&
803 std::chrono::steady_clock::now() - started > std::chrono::seconds(10)) {
804 late_dump_done =
true;
805 DumpNativeMenu(
"late");
static Application & Instance()
void Initialize(const AppConfig &config)
The EditorManager controls the main editor window and manages the various editor classes.
static RecentFilesManager & GetInstance()
const std::vector< std::string > & GetRecentFiles() const
Configuration options for the application startup.