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
This commit is contained in:
Martín Lucas Golini
2026-08-24 02:38:36 -03:00
parent 6b27ea2dc8
commit 9752be89ad
24 changed files with 912 additions and 141 deletions
+14 -4
View File
@@ -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 = "" );
+12
View File
@@ -5,9 +5,12 @@
#include <eepp/ui/uipopupmenu.hpp>
#include <eepp/ui/uiselectbutton.hpp>
#include <eepp/ui/uiwidget.hpp>
#include <memory>
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<PlatformMenuBar> mPlatformMenuBar;
Uint32 getMenuIndex( UIPopUpMenu* menu );
@@ -83,6 +93,8 @@ class EE_API UIMenuBar : public UIWidget {
void destroyMenus();
void autoHeight();
void syncGlobalMenuBar();
};
}} // namespace EE::UI
+2 -2
View File
@@ -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();
};
+13 -2
View File
@@ -1,10 +1,13 @@
#ifndef EE_UICUIMENUITEM_HPP
#define EE_UICUIMENUITEM_HPP
#include <eepp/ui/keyboardshortcut.hpp>
#include <eepp/ui/uipushbutton.hpp>
namespace EE { namespace UI {
enum class MenuRole : Uint8 { NoRole, About, Preferences, Quit };
class EE_API UIMenuItem : public UIPushButton {
public:
typedef std::function<bool( UIMenuItem* item )> 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();
+2 -2
View File
@@ -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();
};
+2
View File
@@ -27,6 +27,8 @@ class EE_API UIMenuSubMenu : public UIMenuItem {
void showSubMenu();
void notifySubMenuWillShow();
const Time& getMouseOverTimeShowMenu() const;
void setMouseOverTimeShowMenu( const Time& maxTime );
+15 -1
View File
@@ -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"
+17 -1
View File
@@ -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"
+3
View File
@@ -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
+539
View File
@@ -0,0 +1,539 @@
#define Rect AppleRect
#import <AppKit/AppKit.h>
#undef Rect
#undef BSD
#include <eepp/system/log.hpp>
#include <eepp/ui/platformmenubar.hpp>
#include <eepp/ui/uimenu.hpp>
#include <eepp/ui/uimenubar.hpp>
#include <eepp/ui/uimenucheckbox.hpp>
#include <eepp/ui/uimenuitem.hpp>
#include <eepp/ui/uimenuradiobutton.hpp>
#include <eepp/ui/uimenuseparator.hpp>
#include <eepp/ui/uimenusubmenu.hpp>
#include <eepp/window/keycodes.hpp>
#include <unordered_map>
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<unichar>( 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 <NSMenuDelegate> {
@private
UIMenuBar* _menuBar;
NSMenu* _installedMainMenu;
NSMenu* _previousMainMenu;
NSMenu* _servicesMenu;
NSMenu* _windowsMenu;
NSMenu* _helpMenu;
BOOL _capturedPreviousMainMenu;
std::unordered_map<NSMenu*, NativeMenuSource> _menuMap;
std::unordered_map<NSMenuItem*, UIMenuItem*> _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<UIMenuItem>();
if ( item->getMenuRole() == role )
return item;
}
if ( widget->isType( UI_TYPE_MENUSUBMENU ) ) {
UIMenuItem* found =
[self findItemWithRole:role inMenu:widget->asType<UIMenuSubMenu>()->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<UIMenuItem>();
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<UIMenuSubMenu>()->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<UIMenuSubMenu>() };
EE_OBJC_RELEASE( subMenu );
}
} else {
item = [self mirroredItemWithTitle:toNSString( source->getText() ) source:source];
if ( widget->isType( UI_TYPE_MENUCHECKBOX ) ) {
[item setState:widget->asType<UIMenuCheckBox>()->isActive() ? NSControlStateValueOn
: NSControlStateValueOff];
} else if ( widget->isType( UI_TYPE_MENURADIOBUTTON ) ) {
[item setState:widget->asType<UIMenuRadioButton>()->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<PlatformMenuBar> createMacOSPlatformMenuBar() {
return std::make_unique<MacOSMenuBar>();
}
}} // namespace EE::UI
+25
View File
@@ -0,0 +1,25 @@
#include <eepp/ui/platformmenubar.hpp>
namespace EE { namespace UI {
#if EE_PLATFORM == EE_PLATFORM_MACOS
std::unique_ptr<PlatformMenuBar> createMacOSPlatformMenuBar();
#endif
bool PlatformMenuBar::isSupported() {
#if EE_PLATFORM == EE_PLATFORM_MACOS
return true;
#else
return false;
#endif
}
std::unique_ptr<PlatformMenuBar> PlatformMenuBar::create() {
#if EE_PLATFORM == EE_PLATFORM_MACOS
return createMacOSPlatformMenuBar();
#else
return nullptr;
#endif
}
}} // namespace EE::UI
+28
View File
@@ -0,0 +1,28 @@
#ifndef EE_UI_PLATFORMMENUBAR_HPP
#define EE_UI_PLATFORMMENUBAR_HPP
#include <eepp/config.hpp>
#include <memory>
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<PlatformMenuBar> create();
};
}} // namespace EE::UI
#endif
+30 -2
View File
@@ -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<UIMenuItem>()->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<UIMenu>();
+33
View File
@@ -1,6 +1,7 @@
#include <eepp/graphics/textureregion.hpp>
#include <eepp/scene/scenemanager.hpp>
#include <eepp/ui/css/propertydefinition.hpp>
#include <eepp/ui/platformmenubar.hpp>
#include <eepp/ui/uimenubar.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uithememanager.hpp>
@@ -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();
}
+5 -9
View File
@@ -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() {
+35 -8
View File
@@ -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<UIMenu>();
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<UIMenu>()->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;
+6 -7
View File
@@ -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() {
+7 -1
View File
@@ -97,7 +97,9 @@ UIMenu* UIMenuSubMenu::getSubMenu() const {
}
void UIMenuSubMenu::showSubMenu() {
sendCommonEvent( Event::OnMenuShow );
notifySubMenuWillShow();
if ( nullptr == mSubMenu )
return;
UIMenu* menu = getParent()->asType<UIMenu>();
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(
+2 -2
View File
@@ -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;
+87
View File
@@ -0,0 +1,87 @@
#include "utest.h"
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uimenu.hpp>
#include <eepp/ui/uipopupmenu.hpp>
#include <eepp/ui/uiscenenode.hpp>
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 );
}
+2 -1
View File
@@ -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<SettingsMenu>();
mSettings->createSettingsMenu( this, mMenuBar );
if ( mMenuBar->isGlobalMenuBarSupported() )
mMenuBar->setGlobalMenuBarEnabled( true );
mSplitter->createEditorWithTabWidget( mBaseLayout );
-2
View File
@@ -5,8 +5,6 @@
extern "C" {
#endif
void macOS_createApplicationMenus();
void macOS_enableScrollMomentum();
void macOS_removeTitleBarSeparator( void* nsWindow );
-61
View File
@@ -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"];
}
+33 -36
View File
@@ -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<UIWidget>( "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<std::string, std::string> 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<std::string, std::string> 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;