From c0ff12fc3a8b517c7951411d5b1290a7ffa85e78 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Tue, 17 Mar 2026 10:22:10 -0500 Subject: [PATCH] tdf#94251: enable Emoji & Symbols, Dictation, and AutoFill menu items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These are added automatically by macOS whenever the main menu is set with [NSApp setMainMenu:] and an Edit menu exists. Previously LO would set the main menu before adding all the actual menus, which prevented macOS from doing the automatic additions. Instead, re-set the main menu whenever LO changes the menu layout. However, some Edit menu translations in LO do not match macOS standard Edit menu names (eg Español, where LO uses Editar but macOS uses Edición) and in these cases macOS won't add the standard items. To handle this case cache the macOS standard menu name inside the AquaSalMenuItem, use the macOS standard name when calling [NSApp setMainMenu:] and flip back to the LO name immediately after. Change-Id: I38d33299f05f99bb084175f6004a4f50068acc74 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/201933 Reviewed-by: Dan Williams Reviewed-by: Patrick Luby Tested-by: Jenkins Reviewed-by: Adolfo Jayme Barrientos --- vcl/Package_osxres.mk | 12 ++- vcl/inc/osx/salinst.h | 3 + vcl/inc/osx/salmenu.h | 16 +++- vcl/osx/res/MenuTranslations.plist | 75 +++++++++++++++ vcl/osx/salinst.cxx | 5 + vcl/osx/salmenu.cxx | 143 ++++++++++++++++++++++------- 6 files changed, 214 insertions(+), 40 deletions(-) create mode 100644 vcl/osx/res/MenuTranslations.plist diff --git a/vcl/Package_osxres.mk b/vcl/Package_osxres.mk index 99252d500ebd..0b690c31f3d2 100644 --- a/vcl/Package_osxres.mk +++ b/vcl/Package_osxres.mk @@ -6,20 +6,24 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # -nib_WRKDIR := $(gb_CustomTarget_workdir)/vcl/osx/res +res_WRKDIR := $(gb_CustomTarget_workdir)/vcl/osx/res -$(nib_WRKDIR)/.dir: +$(res_WRKDIR)/.dir: mkdir -p $(@D) && touch $@ -$(nib_WRKDIR)/MainMenu.nib: $(SRCDIR)/vcl/osx/res/MainMenu.xib | $(nib_WRKDIR)/.dir +$(res_WRKDIR)/MainMenu.nib: $(SRCDIR)/vcl/osx/res/MainMenu.xib | $(res_WRKDIR)/.dir $(call gb_Output_announce,$(@F),$(true),NIB,2) ibtool --compile "$@" "$<" -$(eval $(call gb_Package_Package,vcl_osxres,$(nib_WRKDIR))) +$(res_WRKDIR)/MenuTranslations.plist: $(SRCDIR)/vcl/osx/res/MenuTranslations.plist | $(res_WRKDIR)/.dir + cp "$<" "$@" + +$(eval $(call gb_Package_Package,vcl_osxres,$(res_WRKDIR))) $(call gb_Package_get_clean_target,vcl_osxres): $(call gb_CustomTarget_get_clean_target,vcl/osx/res) $(eval $(call gb_Package_add_files_with_dir,vcl_osxres,Resources,\ MainMenu.nib \ + MenuTranslations.plist \ )) # vim:set noet sw=4 ts=4: diff --git a/vcl/inc/osx/salinst.h b/vcl/inc/osx/salinst.h index 5a6fd56a01ae..0d4c42adc9d3 100644 --- a/vcl/inc/osx/salinst.h +++ b/vcl/inc/osx/salinst.h @@ -165,6 +165,9 @@ public: static const short DispatchTimerEvent = 30; static NSMenu* GetDynamicDockMenu(); + +private: + NSDictionary *mpMenuTranslations; }; CGImageRef CreateCGImage( const Image& ); diff --git a/vcl/inc/osx/salmenu.h b/vcl/inc/osx/salmenu.h index 8d41715564cc..b8cebd31ca02 100644 --- a/vcl/inc/osx/salmenu.h +++ b/vcl/inc/osx/salmenu.h @@ -47,11 +47,15 @@ public: // for OOStatusView }; private: std::vector< MenuBarButtonEntry > maButtons; + NSString * mpAltTitle; + NSDictionary * mpMenuTranslations; MenuBarButtonEntry* findButtonItem( sal_uInt16 i_nItemId ); static void statusLayout(); + void useAltTitle(bool bAltTitle); + public: - AquaSalMenu( bool bMenuBar ); + AquaSalMenu( bool bMenuBar, NSDictionary *pTranslations ); virtual ~AquaSalMenu() override; virtual bool VisibleMenuBar() override; @@ -95,14 +99,22 @@ public: class AquaSalMenuItem : public SalMenuItem { public: - AquaSalMenuItem( const SalItemParams* ); + AquaSalMenuItem( const SalItemParams*, NSDictionary* ); virtual ~AquaSalMenuItem() override; + OUString getItemCommand(); + void setTitle(const OUString& i_rText); + void useAltTitle(bool bUseAlt); sal_uInt16 mnId; // Item ID VclPtr mpVCLMenu; // VCL Menu into which this MenuItem is inserted AquaSalMenu* mpParentMenu; // The menu in which this menu item is inserted AquaSalMenu* mpSubMenu; // Sub menu of this item (if defined) NSMenuItem* mpMenuItem; // The NSMenuItem + +private: + NSString * mpOrigTitle; + NSString * mpAltTitle; + NSDictionary * mpMenuTranslations; }; /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/osx/res/MenuTranslations.plist b/vcl/osx/res/MenuTranslations.plist new file mode 100644 index 000000000000..af00846579d0 --- /dev/null +++ b/vcl/osx/res/MenuTranslations.plist @@ -0,0 +1,75 @@ + + + + + .uno:EditMenu + + ar + تحرير + ca + Editar + cs + Úpravy + da + Rediger + de + Bearbeiten + el + Επεξεργασία + es-419 + Edición + es + Edición + fi + Muokkaa + fr + Édition + he + עריכה + hr + Uredi + hu + Szerkesztés + id + Edit + it + Modifica + ja + 編集 + ko + 편집 + ms + Edit + nb + Rediger + nl + Wijzig + pl + Edycja + pt-BR + Editar + pt-PT + Edição + ro + Editare + ru + Правка + sk + Upraviť + sv + Redigera + th + แก้ไข + tr + Düzen + uk + Редагування + vi + Sửa + zh-Hans + 编辑 + zh-Hant + 編輯 + + + diff --git a/vcl/osx/salinst.cxx b/vcl/osx/salinst.cxx index dbe517f432a8..4164853cc358 100644 --- a/vcl/osx/salinst.cxx +++ b/vcl/osx/salinst.cxx @@ -352,6 +352,9 @@ AquaSalInstance::AquaSalInstance() mpStepperCell = [[NSStepperCell alloc] init]; mpListNodeCell = [[NSButtonCell alloc] init]; + NSString *path = [[[NSBundle mainBundle] pathForResource:@"MenuTranslations" ofType:@"plist"] autorelease]; + mpMenuTranslations = [NSDictionary dictionaryWithContentsOfFile:path]; + #if HAVE_FEATURE_SKIA AquaSkiaSalGraphicsImpl::prepareSkia(); #endif @@ -375,6 +378,8 @@ AquaSalInstance::~AquaSalInstance() [mpCheckCell release]; [mpButtonCell release]; + [mpMenuTranslations release]; + #if HAVE_FEATURE_SKIA SkiaHelper::cleanup(); #endif diff --git a/vcl/osx/salmenu.cxx b/vcl/osx/salmenu.cxx index e7f3fdc5dcd3..960dd21f7238 100644 --- a/vcl/osx/salmenu.cxx +++ b/vcl/osx/salmenu.cxx @@ -23,7 +23,9 @@ #include +#include #include +#include #include #include #include @@ -207,7 +209,7 @@ std::unique_ptr AquaSalInstance::CreateMenu( bool bMenuBar, Menu* pVCLM { initAppMenu(); - AquaSalMenu *pAquaSalMenu = new AquaSalMenu( bMenuBar ); + AquaSalMenu *pAquaSalMenu = new AquaSalMenu( bMenuBar, mpMenuTranslations ); pAquaSalMenu->mpVCLMenu = pVCLMenu; return std::unique_ptr(pAquaSalMenu); @@ -215,7 +217,7 @@ std::unique_ptr AquaSalInstance::CreateMenu( bool bMenuBar, Menu* pVCLM std::unique_ptr AquaSalInstance::CreateMenuItem( const SalItemParams & rItemData ) { - AquaSalMenuItem *pSalMenuItem = new AquaSalMenuItem( &rItemData ); + AquaSalMenuItem *pSalMenuItem = new AquaSalMenuItem( &rItemData, mpMenuTranslations ); return std::unique_ptr(pSalMenuItem); } @@ -224,7 +226,8 @@ std::unique_ptr AquaSalInstance::CreateMenuItem( const SalItemParam * AquaSalMenu */ -AquaSalMenu::AquaSalMenu( bool bMenuBar ) : +AquaSalMenu::AquaSalMenu( bool bMenuBar, NSDictionary *pMenuTranslations ) : + mpMenuTranslations( [pMenuTranslations retain] ), mbMenuBar( bMenuBar ), mpMenu( nil ), mpFrame( nullptr ), @@ -291,6 +294,10 @@ AquaSalMenu::~AquaSalMenu() [mpMenu autorelease]; } } + + [mpMenuTranslations autorelease]; + if( mpAltTitle ) + [mpAltTitle autorelease]; } bool AquaSalMenu::ShowNativePopupMenu(FloatingWindow * pWin, const tools::Rectangle& rRect, FloatWinPopupFlags nFlags) @@ -395,12 +402,19 @@ void AquaSalMenu::unsetMainMenu() { pCurrentMenuBar = nullptr; - // remove items from main menu + // remove all menus except the app menu NSMenu* pMenu = [NSApp mainMenu]; + [NSApp setMainMenu:nil]; for( int nItems = [pMenu numberOfItems]; nItems > 1; nItems-- ) [pMenu removeItemAtIndex: 1]; } +void AquaSalMenu::useAltTitle(bool bAltTitle) +{ + for( std::vector::size_type i = 0; i < maItems.size(); i++ ) + maItems[i]->useAltTitle(bAltTitle); +} + void AquaSalMenu::setMainMenu() { SAL_WARN_IF( !mbMenuBar, "vcl", "setMainMenu on non menubar" ); @@ -418,7 +432,7 @@ void AquaSalMenu::setMainMenu() // tdf#165448 Allow macOS to add menu items in LibreOffice windows menu // macOS will automatically insert menu items in NSApp's // windows menu so set that menu to LibreOffice's windows menu. - if( maItems[i]->mpVCLMenu && maItems[i]->mpVCLMenu->GetItemCommand( maItems[i]->mnId ) == u".uno:WindowList"_ustr ) + if ( maItems[i]->getItemCommand() == u".uno:WindowList"_ustr ) { // Avoid macOS inserting duplicate menu items in the // windows menu @@ -429,6 +443,15 @@ void AquaSalMenu::setMainMenu() } pCurrentMenuBar = this; + // Flip menu/item names to alt ones so macOS will automatically add standard + // menu items like Dictation and Emojis & Symbols + useAltTitle(true); + + [NSApp setMainMenu:mpMenu]; + + // Flip alt menu/item names back to LO ones + useAltTitle(false); + // change status item statusLayout(); } @@ -453,6 +476,7 @@ void AquaSalMenu::setDefaultMenu() if( [pItem menu] == nil ) [pMenu insertItem: pItem atIndex: i+1]; } + [NSApp setMainMenu:pMenu]; // Related: tdf#128186 force key window to a native full screen window // AquaSalMenu::setDefaultMenu() is generally called when the key @@ -672,25 +696,9 @@ void AquaSalMenu::SetItemImage( unsigned /*nPos*/, SalMenuItem* pSMI, const Imag void AquaSalMenu::SetItemText( unsigned /*i_nPos*/, SalMenuItem* i_pSalMenuItem, const OUString& i_rText ) { - if (!i_pSalMenuItem) - return; - - AquaSalMenuItem *pAquaSalMenuItem = static_cast(i_pSalMenuItem); - - // Delete all mnemonics of mbMenuBar and CJK-style mnemonic - OUString aText = MnemonicGenerator::EraseAllMnemonicChars(i_rText); - - if (aText.endsWith("...", &aText)) - aText += u"\u2026"; - - NSString* pString = CreateNSString( aText ); - if (pString) - { - [pAquaSalMenuItem->mpMenuItem setTitle: pString]; - // if the menu item has a submenu, change its title as well - if (pAquaSalMenuItem->mpSubMenu) - [pAquaSalMenuItem->mpSubMenu->mpMenu setTitle: pString]; - [pString release]; + if (i_pSalMenuItem) { + AquaSalMenuItem *pAquaSalMenuItem = static_cast(i_pSalMenuItem); + pAquaSalMenuItem->setTitle( i_rText ); } } @@ -927,16 +935,46 @@ SAL_WNODEPRECATED_DECLARATIONS_POP ); } +static NSString *getMenuTranslation( NSDictionary *pDict, OUString rUnoCommand ) +{ + NSDictionary *pLangs = pDict[ [CreateNSString(rUnoCommand) autorelease] ]; + + if ( !pLangs ) + return NULL; + + rtl_Locale *pLocale = NULL; + osl_getProcessLocale(&pLocale); + + NSString *pLang = [CreateNSString(pLocale->Language) autorelease]; + NSString *pRegion = [CreateNSString(pLocale->Country) autorelease]; + NSString *pLangAndRegion = NULL; + + // HACK: Español (Latinoamérica) is reported as es-419 but osl_getProcessLocale() + // can't deal with 3-character regions; convert back for translation lookup + if ([pLang isEqualToString:@"es"] && [pRegion isEqualToString:@"41"]) + pLangAndRegion = @"es_419"; + + if (!pLangAndRegion) + pLangAndRegion = [[[NSString alloc] initWithFormat:@"%@_%@", pLang, pRegion] autorelease]; + + NSString *pAltTitle = pLangs[pLangAndRegion]; + if ( !pAltTitle ) + pAltTitle = pLangs[pLang]; + + return pAltTitle; +} + /* * SalMenuItem */ -AquaSalMenuItem::AquaSalMenuItem( const SalItemParams* pItemData ) : +AquaSalMenuItem::AquaSalMenuItem( const SalItemParams* pItemData, NSDictionary *pMenuTranslations ) : mnId( pItemData->nId ), mpVCLMenu( pItemData->pMenu ), mpParentMenu( nullptr ), mpSubMenu( nullptr ), - mpMenuItem( nil ) + mpMenuItem( nil ), + mpMenuTranslations( [pMenuTranslations retain] ) { if (pItemData->eType == MenuItemType::SEPARATOR) { @@ -950,14 +988,8 @@ AquaSalMenuItem::AquaSalMenuItem( const SalItemParams* pItemData ) : mpMenuItem = [[SalNSMenuItem alloc] initWithMenuItem: this]; [static_cast(mpMenuItem) setReallyEnabled: YES]; - // peel mnemonics because on mac there are no such things for menu items - // Delete CJK-style mnemonics for the dropdown menu of the 'New button' and lower menu of 'File > New' - NSString* pString = CreateNSString(MnemonicGenerator::EraseAllMnemonicChars(pItemData->aText)); - if (pString) - { - [mpMenuItem setTitle: pString]; - [pString release]; - } + setTitle(pItemData->aText); + // anything but a separator should set a menu to dispatch to SAL_WARN_IF( !mpVCLMenu, "vcl", "no menu" ); } @@ -973,6 +1005,49 @@ AquaSalMenuItem::~AquaSalMenuItem() */ if( mpMenuItem ) [mpMenuItem autorelease]; + if( mpAltTitle ) + [mpAltTitle autorelease]; + [mpOrigTitle autorelease]; + [mpMenuTranslations autorelease]; +} + +// getItemCommand() returns the UNO command ID of the menu item, +// eg like ".uno:WindowList" or ".uno:EditMenu" +OUString AquaSalMenuItem::getItemCommand() +{ + return mpVCLMenu ? mpVCLMenu->GetItemCommand( mnId ) : OUString(); +} + +void AquaSalMenuItem::useAltTitle(bool bUseAlt) +{ + NSString *pTitle = mpOrigTitle; + + if (bUseAlt && mpAltTitle) + pTitle = mpAltTitle; + + [mpMenuItem setTitle: pTitle]; + + // if the menu item has a submenu, change its title too + if (mpSubMenu) + [mpSubMenu->mpMenu setTitle: pTitle]; +} + +void AquaSalMenuItem::setTitle(const OUString& i_rText) +{ + // Delete all normal and CJK-style mnemonic + OUString aText = MnemonicGenerator::EraseAllMnemonicChars(i_rText); + + if (aText.endsWith("...", &aText)) + aText += u"\u2026"; + + mpOrigTitle = CreateNSString( aText ); + if ( !mpOrigTitle ) + return; + + [mpAltTitle autorelease]; + mpAltTitle = getMenuTranslation( mpMenuTranslations, getItemCommand() ); + + useAltTitle(false); } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */