From 9752be89adf374da34ad9d624bc534fa7a6419ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 24 Aug 2026 02:38:36 -0300 Subject: [PATCH 1/3] Add native macOS global menu bar integration Mirror UIMenuBar hierarchies into AppKit while keeping eepp menus as the source of truth. Add semantic item activation and menu lifecycle hooks so native actions, lazy menus, checkboxes, radio buttons, visibility, enabled state, and submenus preserve existing eepp behavior. Support shortcuts and standard About, Preferences, Quit, Window, Help, and Services menu integration. Keep the platform adapter private to the UI implementation and support safe installation, synchronization, and removal of the native menu. Migrate ecode away from its hand-written Cocoa menu, assign semantic menu roles, and populate the UI Language submenu synchronously so it is ready on its first opening. Add unit tests covering activation, menu lifecycle, roles, and submenu preparation. Refs: SpartanJ/ecode#823 --- include/eepp/ui/uimenu.hpp | 18 +- include/eepp/ui/uimenubar.hpp | 12 + include/eepp/ui/uimenucheckbox.hpp | 4 +- include/eepp/ui/uimenuitem.hpp | 15 +- include/eepp/ui/uimenuradiobutton.hpp | 4 +- include/eepp/ui/uimenusubmenu.hpp | 2 + premake4.lua | 16 +- premake5.lua | 18 +- projects/macos/ee.files | 3 + src/eepp/ui/platform/macos/macosmenubar.m | 539 ++++++++++++++++++++++ src/eepp/ui/platformmenubar.cpp | 25 + src/eepp/ui/platformmenubar.hpp | 28 ++ src/eepp/ui/uimenu.cpp | 32 +- src/eepp/ui/uimenubar.cpp | 33 ++ src/eepp/ui/uimenucheckbox.cpp | 14 +- src/eepp/ui/uimenuitem.cpp | 43 +- src/eepp/ui/uimenuradiobutton.cpp | 13 +- src/eepp/ui/uimenusubmenu.cpp | 8 +- src/eepp/ui/uipopupmenu.cpp | 4 +- src/tests/unit_tests/uimenu_tests.cpp | 87 ++++ src/tools/ecode/ecode.cpp | 3 +- src/tools/ecode/macos/macos.hpp | 2 - src/tools/ecode/macos/macos.m | 61 --- src/tools/ecode/settingsmenu.cpp | 69 ++- 24 files changed, 912 insertions(+), 141 deletions(-) create mode 100644 src/eepp/ui/platform/macos/macosmenubar.m create mode 100644 src/eepp/ui/platformmenubar.cpp create mode 100644 src/eepp/ui/platformmenubar.hpp create mode 100644 src/tests/unit_tests/uimenu_tests.cpp diff --git a/include/eepp/ui/uimenu.hpp b/include/eepp/ui/uimenu.hpp index 923fe7c49..efd6da627 100644 --- a/include/eepp/ui/uimenu.hpp +++ b/include/eepp/ui/uimenu.hpp @@ -11,6 +11,8 @@ namespace EE { namespace UI { +enum class MenuBarRole : Uint8 { Normal, Window, Help }; + class EE_API UIMenu : public UIWidget { public: static UIMenu* New(); @@ -26,8 +28,6 @@ class EE_API UIMenu : public UIWidget { UIMenuItem* add( const String& text, DrawablePtr icon = {}, const String& shortcutText = "" ); - UIWidget* add( UIWidget* widget ); - UIMenuSeparator* addSeparator(); UIMenuCheckBox* addCheckBox( const String& text, const bool& active = false, @@ -35,8 +35,7 @@ class EE_API UIMenu : public UIWidget { UIMenuRadioButton* addRadioButton( const String& text, const bool& active = false ); - UIMenuSubMenu* addSubMenu( const String& text, DrawablePtr icon = {}, - UIMenu* subMenu = NULL ); + UIMenuSubMenu* addSubMenu( const String& text, DrawablePtr icon = {}, UIMenu* subMenu = NULL ); UIWidget* getItem( const Uint32& index ); @@ -88,6 +87,14 @@ class EE_API UIMenu : public UIWidget { const Clock& getInactiveTime() const; + void notifyMenuWillShow(); + + void notifyMenuDidHide(); + + MenuBarRole getMenuBarRole() const; + + UIMenu* setMenuBarRole( MenuBarRole role ); + protected: friend class UIMenuItem; friend class UIMenuCheckBox; @@ -103,6 +110,7 @@ class EE_API UIMenu : public UIWidget { UIWidget* mItemSelected; Uint32 mItemSelectedIndex; bool mResizing; + MenuBarRole mMenuBarRole{ MenuBarRole::Normal }; UIWidget* mOwnerNode; Sizei mIconMinSize; UIMenu* mCurrentSubMenu{ nullptr }; @@ -122,6 +130,8 @@ class EE_API UIMenu : public UIWidget { void resizeMe(); + UIWidget* add( UIWidget* widget ); + UIMenuItem* createMenuItem( const String& text, DrawablePtr icon, const String& shortcutText = "" ); diff --git a/include/eepp/ui/uimenubar.hpp b/include/eepp/ui/uimenubar.hpp index ddcdb31f3..42c16b9aa 100644 --- a/include/eepp/ui/uimenubar.hpp +++ b/include/eepp/ui/uimenubar.hpp @@ -5,9 +5,12 @@ #include #include #include +#include namespace EE { namespace UI { +class PlatformMenuBar; + class EE_API UIMenuBar : public UIWidget { public: static UIMenuBar* New(); @@ -56,6 +59,12 @@ class EE_API UIMenuBar : public UIWidget { void showPrevMenu(); + bool isGlobalMenuBarSupported() const; + + UIMenuBar* setGlobalMenuBarEnabled( bool enabled ); + + bool isGlobalMenuBarEnabled() const; + protected: UIMenuBar(); @@ -65,6 +74,7 @@ class EE_API UIMenuBar : public UIWidget { UIPopUpMenu* mCurrentMenu; MenuBarList mButtons; UIPopUpMenu* mWaitingUp; + std::unique_ptr mPlatformMenuBar; Uint32 getMenuIndex( UIPopUpMenu* menu ); @@ -83,6 +93,8 @@ class EE_API UIMenuBar : public UIWidget { void destroyMenus(); void autoHeight(); + + void syncGlobalMenuBar(); }; }} // namespace EE::UI diff --git a/include/eepp/ui/uimenucheckbox.hpp b/include/eepp/ui/uimenucheckbox.hpp index 4edf3de94..b70f41c30 100644 --- a/include/eepp/ui/uimenucheckbox.hpp +++ b/include/eepp/ui/uimenucheckbox.hpp @@ -23,6 +23,8 @@ class EE_API UIMenuCheckBox : public UIMenuItem { void switchActive(); + virtual void activate(); + virtual bool applyProperty( const StyleSheetProperty& attribute ); virtual std::string getPropertyString( const PropertyDefinition* propertyDef, @@ -37,8 +39,6 @@ class EE_API UIMenuCheckBox : public UIMenuItem { UIMenuCheckBox(); - Uint32 onMessage( const NodeMessage* msg ); - virtual void onStateChange(); }; diff --git a/include/eepp/ui/uimenuitem.hpp b/include/eepp/ui/uimenuitem.hpp index f2766b36d..ef21f6015 100644 --- a/include/eepp/ui/uimenuitem.hpp +++ b/include/eepp/ui/uimenuitem.hpp @@ -1,10 +1,13 @@ #ifndef EE_UICUIMENUITEM_HPP #define EE_UICUIMENUITEM_HPP +#include #include namespace EE { namespace UI { +enum class MenuRole : Uint8 { NoRole, About, Preferences, Quit }; + class EE_API UIMenuItem : public UIPushButton { public: typedef std::function OnShouldCloseCb; @@ -19,8 +22,12 @@ class EE_API UIMenuItem : public UIPushButton { virtual void setTheme( UITheme* Theme ); + virtual void activate(); + virtual UIMenuItem* setShortcutText( const String& text ); + const KeyBindings::Shortcut& getShortcut() const; + UITextView* getShortcutView() const; virtual UIWidget* getExtraInnerWidget() const; @@ -29,9 +36,15 @@ class EE_API UIMenuItem : public UIPushButton { UIMenuItem* setOnShouldCloseCb( const OnShouldCloseCb& onShouldCloseCb ); + MenuRole getMenuRole() const; + + UIMenuItem* setMenuRole( MenuRole role ); + protected: UITextView* mShortcutView; OnShouldCloseCb mOnShouldCloseCb; + mutable KeyBindings::Shortcut mShortcut; + MenuRole mMenuRole{ MenuRole::NoRole }; UIMenuItem(); @@ -47,8 +60,6 @@ class EE_API UIMenuItem : public UIPushButton { virtual Uint32 onMouseLeave( const Vector2i& pos, const Uint32& flags ); - virtual Uint32 onMouseClick( const Vector2i& pos, const Uint32& flags ); - void createShortcutView(); void refreshShortcut(); diff --git a/include/eepp/ui/uimenuradiobutton.hpp b/include/eepp/ui/uimenuradiobutton.hpp index a5f07055b..38f8bf649 100644 --- a/include/eepp/ui/uimenuradiobutton.hpp +++ b/include/eepp/ui/uimenuradiobutton.hpp @@ -23,6 +23,8 @@ class EE_API UIMenuRadioButton : public UIMenuItem { void switchActive(); + virtual void activate(); + virtual bool applyProperty( const StyleSheetProperty& attribute ); virtual std::string getPropertyString( const PropertyDefinition* propertyDef, @@ -37,8 +39,6 @@ class EE_API UIMenuRadioButton : public UIMenuItem { UIMenuRadioButton(); - virtual Uint32 onMouseUp( const Vector2i& position, const Uint32& flags ); - virtual void onStateChange(); }; diff --git a/include/eepp/ui/uimenusubmenu.hpp b/include/eepp/ui/uimenusubmenu.hpp index 3955e93f5..1123ad733 100644 --- a/include/eepp/ui/uimenusubmenu.hpp +++ b/include/eepp/ui/uimenusubmenu.hpp @@ -27,6 +27,8 @@ class EE_API UIMenuSubMenu : public UIMenuItem { void showSubMenu(); + void notifySubMenuWillShow(); + const Time& getMouseOverTimeShowMenu() const; void setMouseOverTimeShowMenu( const Time& maxTime ); diff --git a/premake4.lua b/premake4.lua index d2606701c..9a2b117b8 100644 --- a/premake4.lua +++ b/premake4.lua @@ -691,7 +691,7 @@ function generate_os_links() elseif os.is_real("mingw64") then multiple_insert( os_links, { "opengl32", "glu32", "gdi32", "ws2_32", "winmm", "ole32", "uuid", "dwrite" } ) elseif os.is_real("macosx") then - multiple_insert( os_links, { "OpenGL.framework", "CoreFoundation.framework", "CoreText.framework" } ) + multiple_insert( os_links, { "eepp-macos-helper-static", "Cocoa.framework", "OpenGL.framework", "CoreFoundation.framework", "CoreText.framework" } ) elseif os.is_real("freebsd") then multiple_insert( os_links, { "rt", "pthread", "GL" } ) elseif os.is_real("haiku") then @@ -1610,6 +1610,20 @@ solution "eepp" end build_base_cpp_configuration( "languages-syntax-highlighting" ) + if os.is_real("macosx") then + project "eepp-macos-helper-static" + kind "StaticLib" + language "C++" + set_targetdir("libs/" .. os.get_real() .. "/") + includedirs { "include", "src" } + files { "src/eepp/ui/platform/macos/macosmenubar.m" } + buildoptions { "-x objective-c++" } + if not is_vs() then + buildoptions{ "-std=c++20" } + end + build_base_cpp_configuration( "eepp-macos-helper" ) + end + -- Library if not _OPTIONS["disable-static-build"] then project "eepp-static" diff --git a/premake5.lua b/premake5.lua index ab79b203f..328efe74d 100644 --- a/premake5.lua +++ b/premake5.lua @@ -656,7 +656,7 @@ function generate_os_links() elseif os.istarget("mingw32") then multiple_insert( os_links, { "opengl32", "glu32", "gdi32", "ws2_32", "winmm", "ole32", "uuid", "dwrite" } ) elseif os.istarget("macosx") then - multiple_insert( os_links, { "OpenGL.framework", "CoreFoundation.framework", "CoreText.framework" } ) + multiple_insert( os_links, { "eepp-macos-helper-static", "Cocoa.framework", "OpenGL.framework", "CoreFoundation.framework", "CoreText.framework" } ) elseif os.istarget("bsd") then multiple_insert( os_links, { "rt", "pthread", "GL" } ) elseif os.istarget("haiku") then @@ -1652,6 +1652,22 @@ workspace "eepp" filter { "action:export-compile-commands", "system:macosx" } buildoptions { "-std=c++20" } + if os.istarget("macosx") then + project "eepp-macos-helper-static" + kind "StaticLib" + language "C++" + cppdialect "C++20" + incdirs { "include", "src" } + files { "src/eepp/ui/platform/macos/macosmenubar.m" } + buildoptions { "-x objective-c++" } + build_base_cpp_configuration( "eepp-macos-helper" ) + target_dir_lib( "" ) + filter "action:not vs*" + buildoptions { "-Wall" } + filter { "action:export-compile-commands", "system:macosx" } + buildoptions { "-std=c++20" } + end + -- Library if not _OPTIONS["disable-static-build"] then project "eepp-static" diff --git a/projects/macos/ee.files b/projects/macos/ee.files index 94e09839b..70b88d858 100644 --- a/projects/macos/ee.files +++ b/projects/macos/ee.files @@ -1181,6 +1181,9 @@ ../../src/eepp/ui/uiloader.cpp ../../src/eepp/ui/uimanager.cpp ../../src/eepp/ui/uimenubar.cpp +../../src/eepp/ui/platformmenubar.hpp +../../src/eepp/ui/platformmenubar.cpp +../../src/eepp/ui/platform/macos/macosmenubar.m ../../src/eepp/ui/uimenucheckbox.cpp ../../src/eepp/ui/uimenu.cpp ../../src/eepp/ui/uimenuitem.cpp diff --git a/src/eepp/ui/platform/macos/macosmenubar.m b/src/eepp/ui/platform/macos/macosmenubar.m new file mode 100644 index 000000000..35bc7430c --- /dev/null +++ b/src/eepp/ui/platform/macos/macosmenubar.m @@ -0,0 +1,539 @@ +#define Rect AppleRect +#import +#undef Rect +#undef BSD + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace EE; +using namespace EE::UI; +using namespace EE::Window; + +#if __has_feature( objc_arc ) +#define EE_OBJC_RELEASE( object ) +#else +#define EE_OBJC_RELEASE( object ) [object release] +#endif + +namespace { + +NSString* toNSString( const String& string ) { + const std::string utf8( string.toUtf8() ); + return [NSString stringWithUTF8String:utf8.c_str()]; +} + +NSString* applicationName() { + NSBundle* bundle = [NSBundle mainBundle]; + NSString* name = [bundle objectForInfoDictionaryKey:@"CFBundleDisplayName"]; + if ( nil == name || 0 == name.length ) + name = [bundle objectForInfoDictionaryKey:@"CFBundleName"]; + if ( nil == name || 0 == name.length ) + name = [[NSProcessInfo processInfo] processName]; + return nil != name && name.length > 0 ? name : @"Application"; +} + +NSString* keyEquivalent( Keycode key ) { + if ( key >= KEY_F1 && key <= KEY_F12 ) { + unichar character = NSF1FunctionKey + ( key - KEY_F1 ); + return [NSString stringWithCharacters:&character length:1]; + } + if ( key >= KEY_F13 && key <= KEY_F24 ) { + unichar character = NSF13FunctionKey + ( key - KEY_F13 ); + return [NSString stringWithCharacters:&character length:1]; + } + + unichar character = 0; + switch ( key ) { + case KEY_RETURN: + case KEY_KP_ENTER: + character = NSCarriageReturnCharacter; + break; + case KEY_ESCAPE: + character = 0x1B; + break; + case KEY_TAB: + character = NSTabCharacter; + break; + case KEY_BACKSPACE: + character = NSBackspaceCharacter; + break; + case KEY_DELETE: + character = NSDeleteFunctionKey; + break; + case KEY_INSERT: + character = NSInsertFunctionKey; + break; + case KEY_HOME: + character = NSHomeFunctionKey; + break; + case KEY_END: + character = NSEndFunctionKey; + break; + case KEY_PAGEUP: + character = NSPageUpFunctionKey; + break; + case KEY_PAGEDOWN: + character = NSPageDownFunctionKey; + break; + case KEY_LEFT: + character = NSLeftArrowFunctionKey; + break; + case KEY_RIGHT: + character = NSRightArrowFunctionKey; + break; + case KEY_UP: + character = NSUpArrowFunctionKey; + break; + case KEY_DOWN: + character = NSDownArrowFunctionKey; + break; + default: + if ( key > 0 && key < 0x80 ) + character = static_cast( key ); + break; + } + + if ( 0 == character ) + return @""; + character = [[[NSString stringWithCharacters:&character + length:1] lowercaseString] characterAtIndex:0]; + return [NSString stringWithCharacters:&character length:1]; +} + +NSEventModifierFlags modifierMask( Uint32 modifiers ) { + NSEventModifierFlags mask = 0; + if ( modifiers & KEYMOD_META ) + mask |= NSEventModifierFlagCommand; + if ( modifiers & KEYMOD_ALT ) + mask |= NSEventModifierFlagOption; + if ( modifiers & KEYMOD_CTRL ) + mask |= NSEventModifierFlagControl; + if ( modifiers & KEYMOD_SHIFT ) + mask |= NSEventModifierFlagShift; + return mask; +} + +struct NativeMenuSource { + UIMenu* menu; + UIMenuSubMenu* owner; +}; + +} // namespace + +@interface EEPPMenuBarBridge : NSObject { + @private + UIMenuBar* _menuBar; + NSMenu* _installedMainMenu; + NSMenu* _previousMainMenu; + NSMenu* _servicesMenu; + NSMenu* _windowsMenu; + NSMenu* _helpMenu; + BOOL _capturedPreviousMainMenu; + std::unordered_map _menuMap; + std::unordered_map _itemMap; +} + +- (instancetype)initWithMenuBar:(UIMenuBar*)menuBar; +- (void)uninstall; +- (void)syncTopLevel; +- (void)menuItemActivated:(NSMenuItem*)sender; + +@end + +@implementation EEPPMenuBarBridge + +- (instancetype)initWithMenuBar:(UIMenuBar*)menuBar { + self = [super init]; + if ( nil != self ) + _menuBar = menuBar; + return self; +} + +- (void)dealloc { + [self uninstall]; +#if !__has_feature( objc_arc ) + [super dealloc]; +#endif +} + +- (UIMenuItem*)findItemWithRole:(MenuRole)role inMenu:(UIMenu*)menu { + if ( nullptr == menu ) + return nullptr; + for ( Uint32 i = 0; i < menu->getCount(); ++i ) { + UIWidget* widget = menu->getItem( i ); + if ( widget->isType( UI_TYPE_MENUITEM ) ) { + UIMenuItem* item = widget->asType(); + if ( item->getMenuRole() == role ) + return item; + } + if ( widget->isType( UI_TYPE_MENUSUBMENU ) ) { + UIMenuItem* found = + [self findItemWithRole:role inMenu:widget->asType()->getSubMenu()]; + if ( nullptr != found ) + return found; + } + } + return nullptr; +} + +- (UIMenuItem*)findItemWithRole:(MenuRole)role { + if ( nullptr == _menuBar ) + return nullptr; + for ( Uint32 i = 0; i < _menuBar->getButtonsCount(); ++i ) { + UIMenuItem* item = [self findItemWithRole:role inMenu:_menuBar->getPopUpMenu( i )]; + if ( nullptr != item ) + return item; + } + return nullptr; +} + +- (NSMenuItem*)mirroredItemWithTitle:(NSString*)title source:(UIMenuItem*)source { + const KeyBindings::Shortcut& shortcut = source->getShortcut(); + NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:title + action:@selector( menuItemActivated: ) + keyEquivalent:keyEquivalent( shortcut.key )]; + [item setTarget:self]; + [item setEnabled:source->isEnabled()]; + [item setKeyEquivalentModifierMask:modifierMask( shortcut.mod )]; + _itemMap[item] = source; + return item; +} + +- (void)addRoleItem:(UIMenuItem*)source + title:(NSString*)title + toMenu:(NSMenu*)menu + defaultAction:(SEL)defaultAction { + NSMenuItem* item; + if ( nullptr != source ) { + item = [self mirroredItemWithTitle:title source:source]; + if ( source->getMenuRole() == MenuRole::Quit ) { + [item setKeyEquivalent:@"q"]; + [item setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; + } + } else { + item = [[NSMenuItem alloc] initWithTitle:title action:defaultAction keyEquivalent:@""]; + [item setTarget:NSApp]; + } + [menu addItem:item]; + EE_OBJC_RELEASE( item ); +} + +- (void)buildApplicationMenuInMainMenu:(NSMenu*)mainMenu { + NSString* appName = applicationName(); + NSMenu* appMenu = [[NSMenu alloc] initWithTitle:appName]; + [appMenu setAutoenablesItems:NO]; + + NSString* title = [@"About " stringByAppendingString:appName]; + [self addRoleItem:[self findItemWithRole:MenuRole::About] + title:title + toMenu:appMenu + defaultAction:@selector( orderFrontStandardAboutPanel: )]; + + UIMenuItem* preferences = [self findItemWithRole:MenuRole::Preferences]; + if ( nullptr != preferences ) { + [appMenu addItem:[NSMenuItem separatorItem]]; + [self addRoleItem:preferences title:@"Settings…" toMenu:appMenu defaultAction:nil]; + } + + [appMenu addItem:[NSMenuItem separatorItem]]; + _servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"]; + NSMenuItem* servicesItem = [[NSMenuItem alloc] initWithTitle:@"Services" + action:nil + keyEquivalent:@""]; + [servicesItem setSubmenu:_servicesMenu]; + [appMenu addItem:servicesItem]; + [NSApp setServicesMenu:_servicesMenu]; + EE_OBJC_RELEASE( servicesItem ); + EE_OBJC_RELEASE( _servicesMenu ); + + [appMenu addItem:[NSMenuItem separatorItem]]; + title = [@"Hide " stringByAppendingString:appName]; + NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:title + action:@selector( hide: ) + keyEquivalent:@"h"]; + [item setTarget:NSApp]; + [appMenu addItem:item]; + EE_OBJC_RELEASE( item ); + + item = [[NSMenuItem alloc] initWithTitle:@"Hide Others" + action:@selector( hideOtherApplications: ) + keyEquivalent:@"h"]; + [item setTarget:NSApp]; + [item setKeyEquivalentModifierMask:NSEventModifierFlagCommand | NSEventModifierFlagOption]; + [appMenu addItem:item]; + EE_OBJC_RELEASE( item ); + + item = [[NSMenuItem alloc] initWithTitle:@"Show All" + action:@selector( unhideAllApplications: ) + keyEquivalent:@""]; + [item setTarget:NSApp]; + [appMenu addItem:item]; + EE_OBJC_RELEASE( item ); + + [appMenu addItem:[NSMenuItem separatorItem]]; + title = [@"Quit " stringByAppendingString:appName]; + UIMenuItem* quit = [self findItemWithRole:MenuRole::Quit]; + if ( nullptr != quit ) { + [self addRoleItem:quit title:title toMenu:appMenu defaultAction:nil]; + } else { + item = [[NSMenuItem alloc] initWithTitle:title + action:@selector( terminate: ) + keyEquivalent:@"q"]; + [item setTarget:NSApp]; + [appMenu addItem:item]; + EE_OBJC_RELEASE( item ); + } + + NSMenuItem* appMenuItem = [[NSMenuItem alloc] initWithTitle:appName + action:nil + keyEquivalent:@""]; + [appMenuItem setSubmenu:appMenu]; + [mainMenu addItem:appMenuItem]; + EE_OBJC_RELEASE( appMenuItem ); + EE_OBJC_RELEASE( appMenu ); +} + +- (void)removeMappingsForMenu:(NSMenu*)menu removeMenu:(BOOL)removeMenu { + for ( NSMenuItem* item in [menu itemArray] ) { + _itemMap.erase( item ); + if ( nil != item.submenu ) + [self removeMappingsForMenu:item.submenu removeMenu:YES]; + } + if ( removeMenu ) + _menuMap.erase( menu ); +} + +- (NSMenuItem*)createNativeItem:(UIWidget*)widget { + if ( widget->isType( UI_TYPE_MENU_SEPARATOR ) ) + return [NSMenuItem separatorItem]; + if ( !widget->isType( UI_TYPE_MENUITEM ) ) { + Log::warning( "Global menu bar: unsupported widget type %u skipped", widget->getType() ); + return nil; + } + + UIMenuItem* source = widget->asType(); + if ( source->getMenuRole() != MenuRole::NoRole ) + return nil; + + NSMenuItem* item; + if ( widget->isType( UI_TYPE_MENUSUBMENU ) ) { + item = [[NSMenuItem alloc] initWithTitle:toNSString( source->getText() ) + action:nil + keyEquivalent:@""]; + [item setEnabled:source->isEnabled()]; + UIMenu* eeSubMenu = widget->asType()->getSubMenu(); + if ( nullptr != eeSubMenu ) { + NSMenu* subMenu = [[NSMenu alloc] initWithTitle:toNSString( source->getText() )]; + [subMenu setAutoenablesItems:NO]; + [subMenu setDelegate:self]; + [item setSubmenu:subMenu]; + _menuMap[subMenu] = { eeSubMenu, widget->asType() }; + EE_OBJC_RELEASE( subMenu ); + } + } else { + item = [self mirroredItemWithTitle:toNSString( source->getText() ) source:source]; + if ( widget->isType( UI_TYPE_MENUCHECKBOX ) ) { + [item setState:widget->asType()->isActive() ? NSControlStateValueOn + : NSControlStateValueOff]; + } else if ( widget->isType( UI_TYPE_MENURADIOBUTTON ) ) { + [item setState:widget->asType()->isActive() + ? NSControlStateValueOn + : NSControlStateValueOff]; + } + } + return item; +} + +- (void)rebuildMenu:(NSMenu*)nativeMenu fromMenu:(UIMenu*)eeMenu { + [self removeMappingsForMenu:nativeMenu removeMenu:NO]; + [nativeMenu removeAllItems]; + bool lastWasSeparator = true; + for ( Uint32 i = 0; i < eeMenu->getCount(); ++i ) { + UIWidget* widget = eeMenu->getItem( i ); + if ( !widget->isVisible() ) + continue; + const bool isSeparator = widget->isType( UI_TYPE_MENU_SEPARATOR ); + if ( isSeparator && lastWasSeparator ) + continue; + NSMenuItem* item = [self createNativeItem:widget]; + if ( nil == item ) + continue; + [nativeMenu addItem:item]; + lastWasSeparator = isSeparator; + if ( !isSeparator ) + EE_OBJC_RELEASE( item ); + } + if ( lastWasSeparator && [nativeMenu numberOfItems] > 0 ) + [nativeMenu removeItemAtIndex:[nativeMenu numberOfItems] - 1]; +} + +- (void)syncTopLevel { + eeASSERT( [NSThread isMainThread] ); + if ( nullptr == _menuBar ) + return; + + if ( nil == NSApp ) + [NSApplication sharedApplication]; + if ( !_capturedPreviousMainMenu ) { + _previousMainMenu = [NSApp mainMenu]; +#if !__has_feature( objc_arc ) + [_previousMainMenu retain]; +#endif + _capturedPreviousMainMenu = YES; + } + + if ( nil != _installedMainMenu ) { + [self removeMappingsForMenu:_installedMainMenu removeMenu:YES]; + _itemMap.clear(); + _menuMap.clear(); + EE_OBJC_RELEASE( _installedMainMenu ); + } + _installedMainMenu = [[NSMenu alloc] initWithTitle:@""]; + [_installedMainMenu setAutoenablesItems:NO]; + [self buildApplicationMenuInMainMenu:_installedMainMenu]; + + _windowsMenu = nil; + _helpMenu = nil; + for ( Uint32 i = 0; i < _menuBar->getButtonsCount(); ++i ) { + UIPopUpMenu* eeMenu = _menuBar->getPopUpMenu( i ); + if ( nullptr == eeMenu ) + continue; + NSString* title = toNSString( _menuBar->getButton( i )->getText() ); + NSMenu* nativeMenu = [[NSMenu alloc] initWithTitle:title]; + [nativeMenu setAutoenablesItems:NO]; + [nativeMenu setDelegate:self]; + _menuMap[nativeMenu] = { eeMenu, nullptr }; + + NSMenuItem* topLevelItem = [[NSMenuItem alloc] initWithTitle:title + action:nil + keyEquivalent:@""]; + [topLevelItem setSubmenu:nativeMenu]; + [_installedMainMenu addItem:topLevelItem]; + + if ( eeMenu->getMenuBarRole() == MenuBarRole::Window ) + _windowsMenu = nativeMenu; + else if ( eeMenu->getMenuBarRole() == MenuBarRole::Help ) + _helpMenu = nativeMenu; + + EE_OBJC_RELEASE( topLevelItem ); + EE_OBJC_RELEASE( nativeMenu ); + } + + [NSApp setMainMenu:_installedMainMenu]; + [NSApp setWindowsMenu:_windowsMenu]; + [NSApp setHelpMenu:_helpMenu]; +} + +- (void)uninstall { + eeASSERT( [NSThread isMainThread] ); + if ( nullptr == _menuBar && nil == _installedMainMenu ) + return; + + _menuBar = nullptr; + if ( [NSApp servicesMenu] == _servicesMenu ) + [NSApp setServicesMenu:nil]; + if ( [NSApp windowsMenu] == _windowsMenu ) + [NSApp setWindowsMenu:nil]; + if ( [NSApp helpMenu] == _helpMenu ) + [NSApp setHelpMenu:nil]; + if ( [NSApp mainMenu] == _installedMainMenu ) + [NSApp setMainMenu:_previousMainMenu]; + + if ( nil != _installedMainMenu ) + [self removeMappingsForMenu:_installedMainMenu removeMenu:YES]; + _itemMap.clear(); + _menuMap.clear(); + EE_OBJC_RELEASE( _installedMainMenu ); + EE_OBJC_RELEASE( _previousMainMenu ); + _installedMainMenu = nil; + _previousMainMenu = nil; + _servicesMenu = nil; + _windowsMenu = nil; + _helpMenu = nil; + _capturedPreviousMainMenu = NO; +} + +- (void)menuNeedsUpdate:(NSMenu*)menu { + eeASSERT( [NSThread isMainThread] ); + auto found = _menuMap.find( menu ); + if ( found == _menuMap.end() || nullptr == found->second.menu ) + return; + NativeMenuSource source = found->second; + if ( nullptr != source.owner ) { + source.owner->notifySubMenuWillShow(); + found = _menuMap.find( menu ); + if ( found == _menuMap.end() || found->second.owner != source.owner ) + return; + source.menu = source.owner->getSubMenu(); + if ( nullptr == source.menu ) + return; + found->second.menu = source.menu; + } + UIMenu* eeMenu = source.menu; + eeMenu->notifyMenuWillShow(); + found = _menuMap.find( menu ); + if ( found != _menuMap.end() && found->second.menu == eeMenu && + found->second.owner == source.owner ) + [self rebuildMenu:menu fromMenu:eeMenu]; +} + +- (void)menuDidClose:(NSMenu*)menu { + auto found = _menuMap.find( menu ); + if ( found != _menuMap.end() && nullptr != found->second.menu ) + found->second.menu->notifyMenuDidHide(); +} + +- (void)menuItemActivated:(NSMenuItem*)sender { + eeASSERT( [NSThread isMainThread] ); + auto found = _itemMap.find( sender ); + if ( found != _itemMap.end() && nullptr != found->second ) + found->second->activate(); +} + +@end + +namespace EE { namespace UI { + +class MacOSMenuBar final : public PlatformMenuBar { + public: + ~MacOSMenuBar() { uninstall(); } + + void install( UIMenuBar* menuBar ) { + eeASSERT( [NSThread isMainThread] ); + uninstall(); + mBridge = [[EEPPMenuBarBridge alloc] initWithMenuBar:menuBar]; + [mBridge syncTopLevel]; + } + + void uninstall() { + if ( nil == mBridge ) + return; + [mBridge uninstall]; + EE_OBJC_RELEASE( mBridge ); + mBridge = nil; + } + + void syncTopLevel() { + if ( nil != mBridge ) + [mBridge syncTopLevel]; + } + + private: + EEPPMenuBarBridge* mBridge{ nil }; +}; + +std::unique_ptr createMacOSPlatformMenuBar() { + return std::make_unique(); +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/platformmenubar.cpp b/src/eepp/ui/platformmenubar.cpp new file mode 100644 index 000000000..254bd15c1 --- /dev/null +++ b/src/eepp/ui/platformmenubar.cpp @@ -0,0 +1,25 @@ +#include + +namespace EE { namespace UI { + +#if EE_PLATFORM == EE_PLATFORM_MACOS +std::unique_ptr createMacOSPlatformMenuBar(); +#endif + +bool PlatformMenuBar::isSupported() { +#if EE_PLATFORM == EE_PLATFORM_MACOS + return true; +#else + return false; +#endif +} + +std::unique_ptr PlatformMenuBar::create() { +#if EE_PLATFORM == EE_PLATFORM_MACOS + return createMacOSPlatformMenuBar(); +#else + return nullptr; +#endif +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/platformmenubar.hpp b/src/eepp/ui/platformmenubar.hpp new file mode 100644 index 000000000..fc98684c3 --- /dev/null +++ b/src/eepp/ui/platformmenubar.hpp @@ -0,0 +1,28 @@ +#ifndef EE_UI_PLATFORMMENUBAR_HPP +#define EE_UI_PLATFORMMENUBAR_HPP + +#include +#include + +namespace EE { namespace UI { + +class UIMenuBar; + +class PlatformMenuBar { + public: + virtual ~PlatformMenuBar() = default; + + virtual void install( UIMenuBar* menuBar ) = 0; + + virtual void uninstall() = 0; + + virtual void syncTopLevel() = 0; + + static bool isSupported(); + + static std::unique_ptr create(); +}; + +}} // namespace EE::UI + +#endif diff --git a/src/eepp/ui/uimenu.cpp b/src/eepp/ui/uimenu.cpp index 68e5c5831..9f14c8c81 100644 --- a/src/eepp/ui/uimenu.cpp +++ b/src/eepp/ui/uimenu.cpp @@ -305,8 +305,13 @@ Uint32 UIMenu::onMessage( const NodeMessage* msg ) { } case NodeMessage::MouseUp: { if ( msg->getSender()->getParent() == this && ( msg->getFlags() & EE_BUTTONS_LRM ) ) { - Event itemEvent( msg->getSender(), Event::OnItemClicked ); - sendEvent( &itemEvent ); + if ( msg->getSender()->isType( UI_TYPE_MENUITEM ) && + !msg->getSender()->isType( UI_TYPE_MENUSUBMENU ) ) { + msg->getSender()->asType()->activate(); + } else { + Event itemEvent( msg->getSender(), Event::OnItemClicked ); + sendEvent( &itemEvent ); + } return 1; } break; @@ -386,18 +391,41 @@ void UIMenu::resizeMe() { } bool UIMenu::show() { + const bool notify = !isVisible(); setEnabled( true ); setVisible( true ); + if ( notify ) + notifyMenuWillShow(); return true; } bool UIMenu::hide() { + const bool notify = isVisible(); setEnabled( false ); setVisible( false ); safeHide(); + if ( notify ) + notifyMenuDidHide(); return true; } +void UIMenu::notifyMenuWillShow() { + sendCommonEvent( Event::OnMenuShow ); +} + +void UIMenu::notifyMenuDidHide() { + sendCommonEvent( Event::OnMenuHide ); +} + +MenuBarRole UIMenu::getMenuBarRole() const { + return mMenuBarRole; +} + +UIMenu* UIMenu::setMenuBarRole( MenuBarRole role ) { + mMenuBarRole = role; + return this; +} + void UIMenu::safeHide() { if ( mOwnerNode && mOwnerNode->isType( UI_TYPE_MENUSUBMENU ) ) { UIMenu* menu = mOwnerNode->getParent()->asType(); diff --git a/src/eepp/ui/uimenubar.cpp b/src/eepp/ui/uimenubar.cpp index 544262bff..bbe16bf1d 100644 --- a/src/eepp/ui/uimenubar.cpp +++ b/src/eepp/ui/uimenubar.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -88,6 +89,7 @@ void UIMenuBar::showPrevMenu() { } UIMenuBar::~UIMenuBar() { + setGlobalMenuBarEnabled( false ); destroyMenus(); } @@ -153,6 +155,7 @@ void UIMenuBar::addMenuButton( const String& buttonText, UIPopUpMenu* menu ) { button->setThemeSkin( mTheme, "menubarbutton" ); refreshButtons(); + syncGlobalMenuBar(); } void UIMenuBar::setTheme( UITheme* theme ) { @@ -176,6 +179,7 @@ void UIMenuBar::removeMenuButton( const String& buttonText ) { it->second->close(); mButtons.erase( it ); refreshButtons(); + syncGlobalMenuBar(); break; } } @@ -208,9 +212,38 @@ UIPopUpMenu* UIMenuBar::getPopUpMenu( const Uint32& index ) const { UIMenuBar* UIMenuBar::setPopUpMenu( const Uint32& index, UIPopUpMenu* menu ) { eeASSERT( index < mButtons.size() ); mButtons[index].second = menu; + syncGlobalMenuBar(); return this; } +bool UIMenuBar::isGlobalMenuBarSupported() const { + return PlatformMenuBar::isSupported(); +} + +UIMenuBar* UIMenuBar::setGlobalMenuBarEnabled( bool enabled ) { + if ( enabled == isGlobalMenuBarEnabled() ) + return this; + + if ( enabled ) { + mPlatformMenuBar = PlatformMenuBar::create(); + if ( mPlatformMenuBar ) + mPlatformMenuBar->install( this ); + } else if ( mPlatformMenuBar ) { + mPlatformMenuBar->uninstall(); + mPlatformMenuBar.reset(); + } + return this; +} + +bool UIMenuBar::isGlobalMenuBarEnabled() const { + return nullptr != mPlatformMenuBar; +} + +void UIMenuBar::syncGlobalMenuBar() { + if ( mPlatformMenuBar ) + mPlatformMenuBar->syncTopLevel(); +} + size_t UIMenuBar::getButtonsCount() const { return mButtons.size(); } diff --git a/src/eepp/ui/uimenucheckbox.cpp b/src/eepp/ui/uimenucheckbox.cpp index 3f1a964c5..295e9f774 100644 --- a/src/eepp/ui/uimenucheckbox.cpp +++ b/src/eepp/ui/uimenucheckbox.cpp @@ -106,15 +106,11 @@ void UIMenuCheckBox::switchActive() { setActive( !mActive ); } -Uint32 UIMenuCheckBox::onMessage( const NodeMessage* msg ) { - switch ( msg->getMsg() ) { - case NodeMessage::MouseUp: { - if ( msg->getFlags() & EE_BUTTON_LMASK ) - switchActive(); - break; - } - } - return 0; +void UIMenuCheckBox::activate() { + if ( !isEnabled() ) + return; + switchActive(); + UIMenuItem::activate(); } void UIMenuCheckBox::onStateChange() { diff --git a/src/eepp/ui/uimenuitem.cpp b/src/eepp/ui/uimenuitem.cpp index b27eaa743..85e105f13 100644 --- a/src/eepp/ui/uimenuitem.cpp +++ b/src/eepp/ui/uimenuitem.cpp @@ -36,7 +36,25 @@ void UIMenuItem::setTheme( UITheme* Theme ) { onThemeLoaded(); } +void UIMenuItem::activate() { + if ( !isEnabled() || nullptr == getParent() || !getParent()->isType( UI_TYPE_MENU ) ) + return; + + UIMenu* menu = getParent()->asType(); + Input* input = nullptr != getUISceneNode() ? getInput() : nullptr; + if ( ( nullptr == input || !input->isShiftPressed() ) && + ( !mOnShouldCloseCb || mOnShouldCloseCb( this ) ) ) { + menu->backpropagateHide(); + } + + Event itemEvent( this, Event::OnItemClicked ); + menu->sendEvent( &itemEvent ); +} + UIMenuItem* UIMenuItem::setShortcutText( const String& text ) { + mShortcut = {}; + if ( !text.empty() && nullptr != getUISceneNode() ) + mShortcut = KeyBindings::toShortcut( getInput(), text.toUtf8() ); if ( !text.empty() ) createShortcutView(); if ( mShortcutView ) @@ -44,6 +62,14 @@ UIMenuItem* UIMenuItem::setShortcutText( const String& text ) { return this; } +const KeyBindings::Shortcut& UIMenuItem::getShortcut() const { + if ( mShortcut.empty() && nullptr != mShortcutView && !mShortcutView->getText().empty() && + nullptr != getUISceneNode() ) { + mShortcut = KeyBindings::toShortcut( getInput(), mShortcutView->getText().toUtf8() ); + } + return mShortcut; +} + UITextView* UIMenuItem::getShortcutView() const { return mShortcutView; } @@ -75,14 +101,6 @@ Uint32 UIMenuItem::onMouseLeave( const Vector2i& pos, const Uint32& flags ) { return 1; } -Uint32 UIMenuItem::onMouseClick( const Vector2i&, const Uint32& flags ) { - if ( !getInput()->isShiftPressed() && ( flags & EE_BUTTON_LMASK ) && - ( !mOnShouldCloseCb || mOnShouldCloseCb( this ) ) ) { - getParent()->asType()->backpropagateHide(); - } - return 1; -} - UIWidget* UIMenuItem::getExtraInnerWidget() const { return mShortcutView; } @@ -96,6 +114,15 @@ UIMenuItem* UIMenuItem::setOnShouldCloseCb( const OnShouldCloseCb& onShouldClose return this; } +MenuRole UIMenuItem::getMenuRole() const { + return mMenuRole; +} + +UIMenuItem* UIMenuItem::setMenuRole( MenuRole role ) { + mMenuRole = role; + return this; +} + void UIMenuItem::createShortcutView() { if ( mShortcutView ) return; diff --git a/src/eepp/ui/uimenuradiobutton.cpp b/src/eepp/ui/uimenuradiobutton.cpp index c7f3653a7..beff8d713 100644 --- a/src/eepp/ui/uimenuradiobutton.cpp +++ b/src/eepp/ui/uimenuradiobutton.cpp @@ -119,13 +119,12 @@ void UIMenuRadioButton::switchActive() { setActive( !mActive ); } -Uint32 UIMenuRadioButton::onMouseUp( const Vector2i& Pos, const Uint32& Flags ) { - UIMenuItem::onMouseUp( Pos, Flags ); - - if ( getParent()->isVisible() && ( Flags & EE_BUTTONS_LRM ) && !mActive ) - switchActive(); - - return 1; +void UIMenuRadioButton::activate() { + if ( !isEnabled() ) + return; + if ( !mActive ) + setActive( true ); + UIMenuItem::activate(); } void UIMenuRadioButton::onStateChange() { diff --git a/src/eepp/ui/uimenusubmenu.cpp b/src/eepp/ui/uimenusubmenu.cpp index 738a09ad0..8b9108e43 100644 --- a/src/eepp/ui/uimenusubmenu.cpp +++ b/src/eepp/ui/uimenusubmenu.cpp @@ -97,7 +97,9 @@ UIMenu* UIMenuSubMenu::getSubMenu() const { } void UIMenuSubMenu::showSubMenu() { - sendCommonEvent( Event::OnMenuShow ); + notifySubMenuWillShow(); + if ( nullptr == mSubMenu ) + return; UIMenu* menu = getParent()->asType(); mSubMenu->setParent( menu->getParent() ); Vector2f pos = getPixelsPosition(); @@ -114,6 +116,10 @@ void UIMenuSubMenu::showSubMenu() { } } +void UIMenuSubMenu::notifySubMenuWillShow() { + sendCommonEvent( Event::OnMenuShow ); +} + Uint32 UIMenuSubMenu::onMouseOver( const Vector2i& pos, const Uint32& flags ) { if ( nullptr == mCurWait ) { mCurWait = Actions::Runnable::New( diff --git a/src/eepp/ui/uipopupmenu.cpp b/src/eepp/ui/uipopupmenu.cpp index 7ed363c62..bbf6d1df7 100644 --- a/src/eepp/ui/uipopupmenu.cpp +++ b/src/eepp/ui/uipopupmenu.cpp @@ -52,7 +52,7 @@ bool UIPopUpMenu::show() { getUISceneNode()->getUIThemeManager()->getWidgetsFadeOutTime() ), Actions::Spawn::New( Actions::Enable::New(), Actions::Visible::New( true ) ) ) ); } - sendCommonEvent( Event::OnMenuShow ); + notifyMenuWillShow(); setFocus(); return true; } @@ -92,7 +92,7 @@ bool UIPopUpMenu::hide() { close(); } safeHide(); - sendCommonEvent( Event::OnMenuHide ); + notifyMenuDidHide(); return true; } return false; diff --git a/src/tests/unit_tests/uimenu_tests.cpp b/src/tests/unit_tests/uimenu_tests.cpp new file mode 100644 index 000000000..ca8e1e586 --- /dev/null +++ b/src/tests/unit_tests/uimenu_tests.cpp @@ -0,0 +1,87 @@ +#include "utest.h" + +#include +#include +#include +#include +#include +#include + +using namespace EE; +using namespace EE::System; +using namespace EE::UI; +using namespace EE::Window; + +UTEST( UIMenu, SemanticActivationLifecycleAndRoles ) { + UIApplication app( + WindowSettings( 320, 240, "eepp - UIMenu Test", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + UIMenu* menu = UIMenu::New(); + menu->setParent( app.getUI() ); + + int clicked = 0; + int shown = 0; + int hidden = 0; + menu->on( Event::OnItemClicked, [&clicked]( const Event* ) { ++clicked; } ); + menu->on( Event::OnMenuShow, [&shown]( const Event* ) { ++shown; } ); + menu->on( Event::OnMenuHide, [&hidden]( const Event* ) { ++hidden; } ); + + UIMenuItem* item = menu->add( "Item" ); + EXPECT_EQ( item->getMenuRole(), MenuRole::NoRole ); + EXPECT_EQ( item->setMenuRole( MenuRole::About ), item ); + EXPECT_EQ( item->getMenuRole(), MenuRole::About ); + item->activate(); + EXPECT_EQ( clicked, 1 ); + item->setEnabled( false ); + item->activate(); + EXPECT_EQ( clicked, 1 ); + item->setEnabled( true ); + + menu->show(); + item->setOnShouldCloseCb( []( UIMenuItem* ) { return false; } ); + item->activate(); + EXPECT_TRUE( menu->isVisible() ); + EXPECT_EQ( clicked, 2 ); + item->setOnShouldCloseCb( {} ); + menu->hide(); + + UIMenuCheckBox* checkBox = menu->addCheckBox( "Check" ); + EXPECT_FALSE( checkBox->isActive() ); + checkBox->activate(); + EXPECT_TRUE( checkBox->isActive() ); + checkBox->activate(); + EXPECT_FALSE( checkBox->isActive() ); + EXPECT_EQ( clicked, 4 ); + + UIMenuRadioButton* radioA = menu->addRadioButton( "A", true ); + UIMenuRadioButton* radioB = menu->addRadioButton( "B" ); + radioB->activate(); + EXPECT_FALSE( radioA->isActive() ); + EXPECT_TRUE( radioB->isActive() ); + radioB->activate(); + EXPECT_TRUE( radioB->isActive() ); + EXPECT_EQ( clicked, 6 ); + + menu->notifyMenuWillShow(); + menu->notifyMenuDidHide(); + EXPECT_EQ( shown, 2 ); + EXPECT_EQ( hidden, 3 ); + + EXPECT_EQ( menu->getMenuBarRole(), MenuBarRole::Normal ); + EXPECT_EQ( menu->setMenuBarRole( MenuBarRole::Help ), menu ); + EXPECT_EQ( menu->getMenuBarRole(), MenuBarRole::Help ); + + UIPopUpMenu* childMenu = UIPopUpMenu::New(); + childMenu->setParent( app.getUI() ); + UIMenuSubMenu* subMenu = menu->addSubMenu( "Submenu", {}, childMenu ); + int subMenuShowStep = 0; + subMenu->on( Event::OnMenuShow, [&subMenuShowStep]( const Event* ) { + subMenuShowStep = 0 == subMenuShowStep ? 1 : -1; + } ); + childMenu->on( Event::OnMenuShow, [&subMenuShowStep]( const Event* ) { + subMenuShowStep = 1 == subMenuShowStep ? 2 : -1; + } ); + subMenu->showSubMenu(); + EXPECT_EQ( subMenuShowStep, 2 ); +} diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 07cd4e5ca..defa5a249 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -4681,7 +4681,6 @@ void App::init( InitParameters& params ) { EE_PLATFORM == EE_PLATFORM_BSD #if EE_PLATFORM == EE_PLATFORM_MACOS - macOS_createApplicationMenus(); macOS_enableScrollMomentum(); macOS_removeTitleBarSeparator( mWindow->getWindowHandler() ); #endif @@ -5136,6 +5135,8 @@ void App::init( InitParameters& params ) { mSettings = std::make_unique(); mSettings->createSettingsMenu( this, mMenuBar ); + if ( mMenuBar->isGlobalMenuBarSupported() ) + mMenuBar->setGlobalMenuBarEnabled( true ); mSplitter->createEditorWithTabWidget( mBaseLayout ); diff --git a/src/tools/ecode/macos/macos.hpp b/src/tools/ecode/macos/macos.hpp index fede2f729..a34362ee6 100644 --- a/src/tools/ecode/macos/macos.hpp +++ b/src/tools/ecode/macos/macos.hpp @@ -5,8 +5,6 @@ extern "C" { #endif -void macOS_createApplicationMenus(); - void macOS_enableScrollMomentum(); void macOS_removeTitleBarSeparator( void* nsWindow ); diff --git a/src/tools/ecode/macos/macos.m b/src/tools/ecode/macos/macos.m index dfd8282fe..02381be97 100644 --- a/src/tools/ecode/macos/macos.m +++ b/src/tools/ecode/macos/macos.m @@ -3,67 +3,6 @@ #include "macos.hpp" -/* setAppleMenu disappeared from the headers in 10.4 */ -@interface NSApplication(NSAppleMenu) -- (void)setAppleMenu:(NSMenu *)menu; -@end - -// Recreates the menubar replacing the default SDL menubar -void macOS_createApplicationMenus() { - NSString *appName; - NSString *title; - NSMenu *appleMenu; - NSMenu *windowMenu; - NSMenuItem *menuItem; - NSMenu *mainMenu; - - if (NSApp == nil) { - return; - } - - mainMenu = [[NSApplication sharedApplication] mainMenu]; - [mainMenu removeAllItems]; - - appName = @"ecode"; - appleMenu = [[NSMenu alloc] initWithTitle:@""]; - - title = [@"About " stringByAppendingString:appName]; - [appleMenu addItemWithTitle:title action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@""]; - - [appleMenu addItem:[NSMenuItem separatorItem]]; - - [appleMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""]; - - [appleMenu addItem:[NSMenuItem separatorItem]]; - - title = [@"Quit " stringByAppendingString:appName]; - [appleMenu addItemWithTitle:title action:@selector(performClose:) keyEquivalent:@"q"]; - - menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; - [menuItem setSubmenu:appleMenu]; - - [[NSApp mainMenu] addItem:menuItem]; - - [NSApp setAppleMenu:appleMenu]; - - windowMenu = [[NSMenu alloc] initWithTitle:@"Window"]; - - menuItem = [[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(miniaturize:) keyEquivalent:@"m"]; - [menuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; - [windowMenu addItem:menuItem]; - - menuItem = [[NSMenuItem alloc] initWithTitle:@"Toggle Full Screen" action:@selector(toggleFullScreen:) keyEquivalent:@"f"]; - [menuItem setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand]; - [windowMenu addItem:menuItem]; - - menuItem = [[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""]; - [menuItem setSubmenu:windowMenu]; - - [[NSApp mainMenu] addItem:menuItem]; - - [NSApp setWindowsMenu:windowMenu]; -} - void macOS_enableScrollMomentum() { [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"AppleMomentumScrollSupported"]; } diff --git a/src/tools/ecode/settingsmenu.cpp b/src/tools/ecode/settingsmenu.cpp index 8102b6a5a..f754da942 100644 --- a/src/tools/ecode/settingsmenu.cpp +++ b/src/tools/ecode/settingsmenu.cpp @@ -168,6 +168,7 @@ void SettingsMenu::createSettingsMenu( App* app, UIMenuBar* menuBar ) { toolsMenuButton->on( Event::OnMenuShow, lazyBuildToolsMenu ); mWindowMenu = UIPopUpMenu::New(); + mWindowMenu->setMenuBarRole( MenuBarRole::Window ); const auto lazyBuildWindowMenu = [this]( const Event* ) { if ( mWindowMenu->getCount() == 0 ) { createWindowMenu(); @@ -183,6 +184,7 @@ void SettingsMenu::createSettingsMenu( App* app, UIMenuBar* menuBar ) { windowMenuButton->on( Event::OnMenuShow, lazyBuildWindowMenu ); mHelpMenu = UIPopUpMenu::New(); + mHelpMenu->setMenuBarRole( MenuBarRole::Help ); const auto lazyBuildHelpMenu = [this]( const Event* ) { if ( mHelpMenu->getCount() == 0 ) { createHelpMenu(); @@ -206,6 +208,7 @@ void SettingsMenu::createSettingsMenu( App* app, UIMenuBar* menuBar ) { ->setId( "close-folder" ); mSettingsMenu->addSeparator(); mSettingsMenu->add( i18n( "quit", "Quit" ), findIcon( "quit" ), getKeybind( "close-app" ) ) + ->setMenuRole( MenuRole::Quit ) ->setId( "close-app" ); mSettingsButton = mUISceneNode->find( "settings" ); mSettingsButton->on( Event::MouseClick, [this]( const Event* ) { toggleSettingsMenu(); } ); @@ -280,6 +283,7 @@ void SettingsMenu::createSettingsMenu( App* app, UIMenuBar* menuBar ) { onMenuShowEvent( mToolsMenu, toolsMenuButton, 6 ); onMenuShowEvent( mWindowMenu, windowMenuButton, 7 ); onMenuShowEvent( mHelpMenu, helpMenuButton, 8 ); + createHelpMenu(); updateMenu(); @@ -2464,6 +2468,7 @@ UIMenu* SettingsMenu::createHelpMenu() { mHelpMenu->add( i18n( "check_for_updates", "Check for Updates" ), findIcon( "refresh" ) ) ->setId( "check-for-updates" ); mHelpMenu->add( i18n( "about_ecode", "About ecode" ), findIcon( "ecode" ) ) + ->setMenuRole( MenuRole::About ) ->setId( "about-ecode" ); mHelpMenu->on( Event::OnItemClicked, [this]( const Event* event ) { runCommand( event->getNode()->getId() ); } ); @@ -2524,46 +2529,38 @@ UIMenu* SettingsMenu::createLanguagesMenu() { if ( curLang.empty() ) curLang = "en"; - mApp->getThreadPool()->run( [this, menu, curLang] { - auto files = - FileSystem::filesInfoGetInPath( mApp->geti18nPath(), false, true, false, true ); + auto files = FileSystem::filesInfoGetInPath( mApp->geti18nPath(), false, true, false, true ); - std::map languages; - for ( const auto& file : files ) { - if ( file.getExtension() != "xml" ) - continue; - auto name( FileSystem::fileRemoveExtension( file.getFileName() ) ); - std::string data; - FileSystem::fileGet( file.getFilepath(), data ); - std::string lptrn( "title=\"(.-)\"" ); - LuaPattern pattern( lptrn ); - PatternMatcher::Range matches[2]; - if ( pattern.matches( data, matches ) ) { - std::string title( - data.substr( matches[1].start, matches[1].end - matches[1].start ) ); - languages[title] = name; - } else { - languages[name] = name; - } + std::map languages; + for ( const auto& file : files ) { + if ( file.getExtension() != "xml" ) + continue; + auto name( FileSystem::fileRemoveExtension( file.getFileName() ) ); + std::string data; + FileSystem::fileGet( file.getFilepath(), data ); + std::string lptrn( "title=\"(.-)\"" ); + LuaPattern pattern( lptrn ); + PatternMatcher::Range matches[2]; + if ( pattern.matches( data, matches ) ) { + std::string title( data.substr( matches[1].start, matches[1].end - matches[1].start ) ); + languages[title] = name; + } else { + languages[name] = name; } - if ( languages.empty() ) - return; + } - menu->runOnMainThread( [this, menu, curLang, languages] { - for ( const auto& lang : languages ) - menu->addRadioButton( lang.first, curLang == lang.second )->setId( lang.second ); + for ( const auto& lang : languages ) + menu->addRadioButton( lang.first, curLang == lang.second )->setId( lang.second ); - menu->on( Event::OnItemClicked, [this]( const Event* event ) { - auto id = event->getNode()->getId(); - mApp->getConfig().ui.language = id; - UIMessageBox* msg = UIMessageBox::New( - UIMessageBox::OK, - i18n( "new_ui_language", "New language assigned.\nPlease restart the " - "application to see the complete changes." ) ); - msg->showWhenReady(); - mApp->setFocusEditorOnClose( msg ); - } ); - } ); + menu->on( Event::OnItemClicked, [this]( const Event* event ) { + auto id = event->getNode()->getId(); + mApp->getConfig().ui.language = id; + UIMessageBox* msg = UIMessageBox::New( UIMessageBox::OK, + i18n( "new_ui_language", + "New language assigned.\nPlease restart the " + "application to see the complete changes." ) ); + msg->showWhenReady(); + mApp->setFocusEditorOnClose( msg ); } ); return menu; From 6ff677ff2b37525ebfdf9d7e8c0141d190ae087f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 24 Aug 2026 10:15:28 -0300 Subject: [PATCH 2/3] Rename macosmenubar.m to .mm to avoid getting compiled as Objective-C and not Objective-C++. --- premake4.lua | 2 +- premake5.lua | 2 +- projects/macos/ee.files | 2 +- src/eepp/ui/platform/macos/{macosmenubar.m => macosmenubar.mm} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename src/eepp/ui/platform/macos/{macosmenubar.m => macosmenubar.mm} (100%) diff --git a/premake4.lua b/premake4.lua index 9a2b117b8..0101e8c2f 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1616,7 +1616,7 @@ solution "eepp" language "C++" set_targetdir("libs/" .. os.get_real() .. "/") includedirs { "include", "src" } - files { "src/eepp/ui/platform/macos/macosmenubar.m" } + files { "src/eepp/ui/platform/macos/macosmenubar.mm" } buildoptions { "-x objective-c++" } if not is_vs() then buildoptions{ "-std=c++20" } diff --git a/premake5.lua b/premake5.lua index 328efe74d..c8c7dae81 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1658,7 +1658,7 @@ workspace "eepp" language "C++" cppdialect "C++20" incdirs { "include", "src" } - files { "src/eepp/ui/platform/macos/macosmenubar.m" } + files { "src/eepp/ui/platform/macos/macosmenubar.mm" } buildoptions { "-x objective-c++" } build_base_cpp_configuration( "eepp-macos-helper" ) target_dir_lib( "" ) diff --git a/projects/macos/ee.files b/projects/macos/ee.files index 70b88d858..fc65f96d2 100644 --- a/projects/macos/ee.files +++ b/projects/macos/ee.files @@ -1183,7 +1183,7 @@ ../../src/eepp/ui/uimenubar.cpp ../../src/eepp/ui/platformmenubar.hpp ../../src/eepp/ui/platformmenubar.cpp -../../src/eepp/ui/platform/macos/macosmenubar.m +../../src/eepp/ui/platform/macos/macosmenubar.mm ../../src/eepp/ui/uimenucheckbox.cpp ../../src/eepp/ui/uimenu.cpp ../../src/eepp/ui/uimenuitem.cpp diff --git a/src/eepp/ui/platform/macos/macosmenubar.m b/src/eepp/ui/platform/macos/macosmenubar.mm similarity index 100% rename from src/eepp/ui/platform/macos/macosmenubar.m rename to src/eepp/ui/platform/macos/macosmenubar.mm From 55ad998de1dbf526602c66dbdcddcc58bdc0d773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 24 Aug 2026 19:10:54 -0300 Subject: [PATCH 3/3] Use Nearest filter in eeiv images by default. --- src/tools/eeiv/eeiv.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/eeiv/eeiv.hpp b/src/tools/eeiv/eeiv.hpp index f3b0abab5..c53e48dcb 100644 --- a/src/tools/eeiv/eeiv.hpp +++ b/src/tools/eeiv/eeiv.hpp @@ -93,6 +93,6 @@ class App { UIConsole* mConsole{ nullptr }; UIWelcomeScreen* mWelcomeScreen{ nullptr }; bool mCursorVisible{ true }; - Texture::Filter mTextureFilter{ Texture::Filter::Linear }; + Texture::Filter mTextureFilter{ Texture::Filter::Nearest }; std::unique_ptr mConsoleCommands; };