From f769029bddc29d4853965d1a5b98ffb1bbcda2e6 Mon Sep 17 00:00:00 2001 From: Stefan Csomor Date: Mon, 6 Oct 2025 10:12:56 +0200 Subject: [PATCH] OSX ios first version of application menubar support (#25863) * first version of app menubar support on ios, activate with wxUSE_MENUBAR = 1 --- include/wx/osx/app.h | 4 +- include/wx/osx/menu.h | 4 + src/osx/carbon/app.cpp | 10 ++ src/osx/iphone/menu.mm | 268 ++++++++++++------------------------- src/osx/iphone/menuitem.mm | 21 +-- src/osx/iphone/utils.mm | 20 ++- src/osx/menu_osx.cpp | 8 +- 7 files changed, 136 insertions(+), 199 deletions(-) diff --git a/include/wx/osx/app.h b/include/wx/osx/app.h index 98611c9337..f295a36fcf 100644 --- a/include/wx/osx/app.h +++ b/include/wx/osx/app.h @@ -152,7 +152,9 @@ public: virtual bool OSXOnShouldTerminate(); // before application terminates virtual void OSXOnWillTerminate(); - +#if wxOSX_USE_IPHONE && wxUSE_MENUBAR + virtual void OSXOnBuildMenu(WX_NSObject menuBuilder); +#endif private: bool m_onInitResult; bool m_inited; diff --git a/include/wx/osx/menu.h b/include/wx/osx/menu.h index ed3d83c8ec..ed3f7d52b6 100644 --- a/include/wx/osx/menu.h +++ b/include/wx/osx/menu.h @@ -151,6 +151,10 @@ public: wxMenu *OSXGetAppleMenu() const { return m_appleMenu; } +#if wxOSX_USE_IPHONE + void OSXOnBuildMenu( WX_NSObject builder ) ; +#endif + static void SetAutoWindowMenu( bool enable ) { s_macAutoWindowMenu = enable ; } static bool GetAutoWindowMenu() { return s_macAutoWindowMenu ; } diff --git a/src/osx/carbon/app.cpp b/src/osx/carbon/app.cpp index 140054d7b0..34d71b9c5f 100644 --- a/src/osx/carbon/app.cpp +++ b/src/osx/carbon/app.cpp @@ -220,6 +220,16 @@ bool wxApp::OSXOnShouldTerminate() ProcessEvent(event); return !event.GetVeto(); } + +#endif + +#if wxOSX_USE_IPHONE && wxUSE_MENUBAR + +void wxApp::OSXOnBuildMenu(WX_NSObject builder) +{ + wxMenuBar::MacGetInstalledMenuBar()->OSXOnBuildMenu(builder); +} + #endif #if wxDEBUG_LEVEL && wxOSX_USE_COCOA_OR_CARBON diff --git a/src/osx/iphone/menu.mm b/src/osx/iphone/menu.mm index decf0d5b0b..46c6c40afa 100644 --- a/src/osx/iphone/menu.mm +++ b/src/osx/iphone/menu.mm @@ -34,174 +34,24 @@ // other standard headers // ---------------------- #include -/* -@implementation wxUIMenu -- (id) initWithTitle:(NSString*) title -{ - if ( self = [super initWithTitle:title] ) - { - impl = nullptr; - } - return self; -} - -- (void)setImplementation: (wxMenuImpl *) theImplementation -{ - impl = theImplementation; -} - -- (wxMenuImpl*) implementation -{ - return impl; -} - -@end -*/ -// this is more compatible, as it is also called for command-key shortcuts -// and under 10.4, we are not getting a 'close' event however... -#define wxOSX_USE_NEEDSUPDATE_HOOK 1 - -@interface wxUIMenuController : NSObject // TODO wxIOS -{ -} - -#if wxOSX_USE_NEEDSUPDATE_HOOK -- (void)menuNeedsUpdate:(UIMenu*)smenu; -#else -- (void)menuWillOpen:(UIMenu *)menu; -#endif -- (void)menuDidClose:(UIMenu *)menu; -- (void)menu:(UIMenu *)menu willHighlightItem:(UIMenuItem *)item; - -@end - -@implementation wxUIMenuController - -- (id) init -{ - self = [super init]; - return self; -} - -/* -#if wxOSX_USE_NEEDSUPDATE_HOOK -- (void)menuNeedsUpdate:(UIMenu*)smenu -{ - wxUIMenu* menu = (wxUIMenu*) smenu; - wxMenuImpl* menuimpl = [menu implementation]; - if ( menuimpl ) - { - wxMenu* wxpeer = (wxMenu*) menuimpl->GetWXPeer(); - if ( wxpeer ) - wxpeer->HandleMenuOpened(); - } -} -#else -- (void)menuWillOpen:(UIMenu *)smenu -{ - wxUIMenu* menu = (wxUIMenu*) smenu; - wxMenuImpl* menuimpl = [menu implementation]; - if ( menuimpl ) - { - wxMenu* wxpeer = (wxMenu*) menuimpl->GetWXPeer(); - if ( wxpeer ) - wxpeer->HandleMenuOpened(); - } -} -#endif - -- (void)menuDidClose:(UIMenu *)smenu -{ - wxUIMenu* menu = (wxUIMenu*) smenu; - wxMenuImpl* menuimpl = [menu implementation]; - if ( menuimpl ) - { - wxMenu* wxpeer = (wxMenu*) menuimpl->GetWXPeer(); - if ( wxpeer ) - wxpeer->HandleMenuClosed(); - } -} - -- (void)menu:(UIMenu *)smenu willHighlightItem:(UIMenuItem *)item -{ - wxUIMenu* menu = (wxUIMenu*) smenu; - wxMenuImpl* menuimpl = [menu implementation]; - if ( menuimpl ) - { - wxMenuItem* menuitem = nullptr; - wxMenu* wxpeer = (wxMenu*) menuimpl->GetWXPeer(); - - if ( [ item isKindOfClass:[wxUIMenuItem class] ] ) - { - wxMenuItemImpl* menuitemimpl = (wxMenuItemImpl*) [ (wxUIMenuItem*) item implementation ]; - if ( menuitemimpl ) - { - menuitem = menuitemimpl->GetWXPeer(); - } - } - - if ( wxpeer ) - { - wxpeer->HandleMenuItemHighlighted( menuitem ); - } - } -} -*/ - -@end - -class wxMenuCocoaImpl : public wxMenuImpl +class wxMenuIPhoneImpl : public wxMenuImpl { public : - wxMenuCocoaImpl( wxMenu* peer , UIMenu* menu) : wxMenuImpl(peer), m_osxMenu(wxCFRetain(menu)) + wxMenuIPhoneImpl( wxMenu* peer , UIMenu* menu) : wxMenuImpl(peer), m_osxMenu(wxCFRetain(menu)) { - /* - static wxUIMenuController* controller = nullptr; - if ( controller == nullptr ) - { - controller = [[wxUIMenuController alloc] init]; - } - [menu setDelegate:controller]; - [m_osxMenu setImplementation:this]; - // gc aware - if ( m_osxMenu ) - CFRetain(m_osxMenu); - [m_osxMenu release]; - */ } - virtual ~wxMenuCocoaImpl(); + virtual ~wxMenuIPhoneImpl(); virtual void InsertOrAppend(wxMenuItem *pItem, size_t pos) override { UIMenuElement* uimenuitem = (UIMenuElement*) pItem->GetPeer()->GetHMenuItem(); m_children.push_back(uimenuitem); - /* - // make sure a call of SetSubMenu is also reflected (occurring after Create) - // update the native menu item accordingly - - if ( pItem->IsSubMenu() ) - { - wxMenu* wxsubmenu = pItem->GetSubMenu(); - WXHMENU nssubmenu = wxsubmenu->GetHMenu(); - if ( [nsmenuitem submenu] != nssubmenu ) - { - wxsubmenu->GetPeer()->SetTitle( pItem->GetItemLabelText() ); - [nsmenuitem setSubmenu:nssubmenu]; - } - } - - if ( pos == (size_t) -1 ) - [m_osxMenu addItem:nsmenuitem ]; - else - [m_osxMenu insertItem:nsmenuitem atIndex:pos]; - */ } virtual void Remove( wxMenuItem *pItem ) override { - // [m_osxMenu removeItem:(UIMenuItem*) pItem->GetPeer()->GetHMenuItem()]; } virtual void MakeRoot() override @@ -214,8 +64,6 @@ public : virtual void SetTitle( const wxString& text ) override { - // wxCFStringRef cfText(text); - // [m_osxMenu setTitle:cfText.AsNSString()]; } virtual void PopUp( wxWindow *win, int x, int y ) override @@ -271,29 +119,38 @@ public : void DisableAutoEnable() { - /* - [m_osxMenu setAutoenablesItems:NO]; - - wxMenu* menu = GetWXPeer(); - for ( wxMenuItemList::compatibility_iterator node = menu->GetMenuItems().GetFirst(); - node; - node = node->GetNext() ) - { - const wxMenuItem* const item = node->GetData(); - if ( item->IsSubMenu() ) - { - wxMenuCocoaImpl* subimpl = dynamic_cast(item->GetSubMenu()->GetPeer()); - if ( subimpl ) - subimpl->DisableAutoEnable(); - } - } -*/ } WXHMENU GetHMenu() override { if ( m_osxMenu == nil ) { - m_osxMenu.reset( [UIMenu menuWithTitle:wxCFStringRef(m_peer->GetTitle()).AsNSString() children:m_children] ); + NSString* title = wxNSStringWithWxString(wxStripMenuCodes(m_peer->GetTitle(), wxStrip_Menu)); + + wxCFMutableArrayRef groups; + wxCFMutableArrayRef currentMenuElements; + + for ( int i = 0 ; i < m_children.size(); i++ ) + { + UIMenuElement* element = m_children[i]; + if ( element.title.length > 0 ) + { + currentMenuElements.push_back(element); + } + else // separator, start a new menu group + { + UIMenu* menu = [UIMenu menuWithTitle:@"" image:nil identifier:nil options:UIMenuOptionsDisplayInline children:currentMenuElements]; + currentMenuElements.clear(); + groups.push_back(menu); + } + } + if ( !currentMenuElements.empty() ) + { + UIMenu* menu = [UIMenu menuWithTitle:@"" image:nil identifier:nil options:UIMenuOptionsDisplayInline children:currentMenuElements]; + currentMenuElements.clear(); + groups.push_back(menu); + } + + m_osxMenu.reset( [UIMenu menuWithTitle:title children:groups] ); } return m_osxMenu; } @@ -303,25 +160,70 @@ public : protected : wxCFRef m_osxMenu; wxCFMutableArrayRef m_children; - -/* - UIMenu* menu = [[UIMenu menuWithTitle:cfText.AsNSString() children:]; -*/ - } ; -wxMenuCocoaImpl::~wxMenuCocoaImpl() +wxMenuIPhoneImpl::~wxMenuIPhoneImpl() { - // [m_osxMenu setDelegate:nil]; - // [m_osxMenu setImplementation:nil]; - // gc aware } wxMenuImpl* wxMenuImpl::Create( wxMenu* peer, const wxString& title ) { wxCFStringRef cfText( title ); - wxMenuImpl* c = new wxMenuCocoaImpl( peer, nil ); + wxMenuImpl* c = new wxMenuIPhoneImpl( peer, nil ); return c; } +#if wxUSE_MENUBAR +void wxMenuBar::OSXOnBuildMenu(WX_NSObject b) +{ + id builder = (id) b; + + UIMenuIdentifier lastmenuid = UIMenuApplication; + + [builder removeMenuForIdentifier:UIMenuFile]; + [builder removeMenuForIdentifier:UIMenuEdit]; + [builder removeMenuForIdentifier:UIMenuFormat]; + [builder removeMenuForIdentifier:UIMenuView]; + + NSString* nsHelpMenuTitle = wxNSStringWithWxString(wxStripMenuCodes(wxApp::s_macHelpMenuTitleName, wxStrip_Menu)); + NSString* nsTranslatedHelpTitle = wxNSStringWithWxString(wxGETTEXT_IN_CONTEXT("macOS menu name", "Help")); + + NSString* nsWindowMenuTitle = wxNSStringWithWxString(wxStripMenuCodes(wxApp::s_macWindowMenuTitleName, wxStrip_Menu)); + NSString* nsTranslatedWindowMenuTitle = wxNSStringWithWxString(wxGETTEXT_IN_CONTEXT("macOS menu name", "Window")); + + + for ( wxMenuItemList::compatibility_iterator node = m_rootMenu->GetMenuItems().GetFirst(); + node; + node = node->GetNext() ) + { + const wxMenuItem* const item = node->GetData(); + if ( item->IsSubMenu() ) + { + UIMenu* menu = (UIMenu *)item->GetSubMenu()->GetHMenu(); + + if ( item->GetSubMenu() == m_appleMenu ) + { + // the default is quite resonable + // TODO merge non desktop commands into the application menu, discard the rest + } + else if ([[menu title] isEqualToString:nsWindowMenuTitle] || + [[menu title] isEqualToString:nsTranslatedWindowMenuTitle]) + { + [builder replaceMenuForIdentifier:UIMenuWindow withMenu:menu]; + } + else if ([[menu title] isEqualToString:nsHelpMenuTitle] || + [[menu title] isEqualToString:nsTranslatedHelpTitle]) + { + [builder replaceMenuForIdentifier:UIMenuHelp withMenu:menu]; + } + else + { + [builder insertSiblingMenu:menu afterMenuForIdentifier:(UIMenuIdentifier) lastmenuid]; + } + lastmenuid = menu.identifier; + } + } +} +#endif + #endif diff --git a/src/osx/iphone/menuitem.mm b/src/osx/iphone/menuitem.mm index 15a3e51737..650a24b8b7 100644 --- a/src/osx/iphone/menuitem.mm +++ b/src/osx/iphone/menuitem.mm @@ -182,14 +182,14 @@ void wxMacCocoaMenuItemSetAccelerator( UIMenuItem* menuItem, wxAcceleratorEntry* */ -class wxMenuItemCocoaImpl : public wxMenuItemImpl +class wxMenuItemIPhoneImpl : public wxMenuItemImpl { public : - wxMenuItemCocoaImpl( wxMenuItem* peer, UIMenuElement* item ) : wxMenuItemImpl(peer), m_osxMenuItem(wxCFRetain(item)) + wxMenuItemIPhoneImpl( wxMenuItem* peer, UIMenuElement* item ) : wxMenuItemImpl(peer), m_osxMenuItem(wxCFRetain(item)) { } - ~wxMenuItemCocoaImpl(); + ~wxMenuItemIPhoneImpl(); void SetBitmap( const wxBitmapBundle& bitmap ) override { @@ -244,11 +244,11 @@ protected : wxCFRef m_osxMenuItem ; } ; -wxMenuItemCocoaImpl::~wxMenuItemCocoaImpl() +wxMenuItemIPhoneImpl::~wxMenuItemIPhoneImpl() { } -bool wxMenuItemCocoaImpl::DoDefault() +bool wxMenuItemIPhoneImpl::DoDefault() { bool handled=false; int menuid = m_peer->GetId(); @@ -291,7 +291,8 @@ wxMenuItemImpl* wxMenuItemImpl::Create( wxMenuItem* peer, wxMenu *pParentMenu, if ( kind == wxITEM_SEPARATOR ) { - // TODO new version of item = [[UIMenuItem separatorItem] retain]; + UIAction* menuItem = [UIAction actionWithTitle:@"" image:nil identifier:nil handler: ^( UIAction* action) {} ]; + item = menuItem; } else { @@ -305,12 +306,14 @@ wxMenuItemImpl* wxMenuItemImpl::Create( wxMenuItem* peer, wxMenu *pParentMenu, } else { + wxString identifier = wxString::Format( wxT("%p"), peer ); + wxCFStringRef cfidentifier(identifier); // currently in the iOS 14 Beta UICommands trigger a layout violation so stick with UIActions for the time being if ( entry ) { // TODO UIKeyCommand* command = [UIKeyCommand commandWithTitle ...] - UIAction* menuitem = [UIAction actionWithTitle:cfText.AsNSString() image:nil identifier:nil + UIAction* menuitem = [UIAction actionWithTitle:cfText.AsNSString() image:nil identifier:cfidentifier.AsNSString() handler: ^( UIAction* action) { peer->GetMenu()->HandleCommandProcess(peer);} ]; item = menuitem; } @@ -320,13 +323,13 @@ wxMenuItemImpl* wxMenuItemImpl::Create( wxMenuItem* peer, wxMenu *pParentMenu, UICommand* command = [UICommand commandWithTitle:cfText.AsNSString() image:nil action:@selector(menuItemAction:) propertyList:nil]; */ - UIAction* menuitem = [UIAction actionWithTitle:cfText.AsNSString() image:nil identifier:nil + UIAction* menuitem = [UIAction actionWithTitle:cfText.AsNSString() image:nil identifier:cfidentifier.AsNSString() handler: ^( UIAction* action) { peer->GetMenu()->HandleCommandProcess(peer);} ]; item = menuitem; } } } - c = new wxMenuItemCocoaImpl( peer, item ); + c = new wxMenuItemIPhoneImpl( peer, item ); return c; } diff --git a/src/osx/iphone/utils.mm b/src/osx/iphone/utils.mm index 25bda977e2..50b78346c0 100644 --- a/src/osx/iphone/utils.mm +++ b/src/osx/iphone/utils.mm @@ -42,7 +42,7 @@ // Common Event Support // ---------------------------------------------------------------------------- -@interface wxAppDelegate : NSObject { +@interface wxAppDelegate : UIResponder { } @end @@ -57,19 +57,31 @@ return YES; } -- (void)applicationDidFinishLaunching:(UIApplication *)application { +- (void)applicationDidFinishLaunching:(UIApplication *)application +{ wxTheApp->OSXOnDidFinishLaunching(); } -- (void)applicationWillTerminate:(UIApplication *)application { +- (void)applicationWillTerminate:(UIApplication *)application +{ wxUnusedVar(application); wxTheApp->OSXOnWillTerminate(); } -- (void)dealloc { +- (void)dealloc +{ [super dealloc]; } +#if wxUSE_MENUBAR +- (void) buildMenuWithBuilder:(id) builder +{ + if ( builder.system == UIMenuSystem.mainSystem ) + { + wxTheApp->OSXOnBuildMenu((WX_NSObject) builder); + } +} +#endif @end diff --git a/src/osx/menu_osx.cpp b/src/osx/menu_osx.cpp index 21d6316d60..5ed683cf78 100644 --- a/src/osx/menu_osx.cpp +++ b/src/osx/menu_osx.cpp @@ -665,11 +665,13 @@ wxMenu *wxMenuBar::Replace(size_t pos, wxMenu *menu, const wxString& title) bool wxMenuBar::Insert(size_t pos, wxMenu *menu, const wxString& title) { + // must be set early because base class method call might call back and create a native menu which would be missing a title + menu->SetTitle(title); + if ( !wxMenuBarBase::Insert(pos, menu, title) ) return false; m_rootMenu->Insert( pos+firstMenuPos, wxMenuItem::New( m_rootMenu, wxID_ANY, title, "", wxITEM_NORMAL, menu ) ); - menu->SetTitle(title); return true; } @@ -688,6 +690,9 @@ wxMenu *wxMenuBar::Remove(size_t pos) bool wxMenuBar::Append(wxMenu *menu, const wxString& title) { + // must be set early because base class method call might call back and create a native menu which would be missing a title + menu->SetTitle(title); + WXHMENU submenu = menu ? menu->GetHMenu() : nullptr; wxCHECK_MSG( submenu, false, wxT("can't append invalid menu to menubar") ); @@ -695,7 +700,6 @@ bool wxMenuBar::Append(wxMenu *menu, const wxString& title) return false; m_rootMenu->AppendSubMenu(menu, title); - menu->SetTitle(title); return true; }