tdf#41775 copy the Start Center menubar to the default menubar on macOS

The ShutdownIcon class on macOS handles the menubar for when there are
no windows displayed. So copy the entire menubar's tree to a native
menu and use that native menu as the default menubar.

Note: not all menu items may crash without a C++ window so currently
the Tools > Configure menu must force the Start Center window to
reappear before displaying its dialog.

Change-Id: I3190da0dffdd6c7181939aec71954e067689d081
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/179980
Tested-by: Jenkins
Reviewed-by: Patrick Luby <guibomacdev@gmail.com>
This commit is contained in:
Patrick Luby
2025-01-13 02:38:35 +01:00
committed by Patrick Luby
parent e146f49704
commit 6efdd14448
9 changed files with 409 additions and 23 deletions
+1 -1
View File
@@ -49,7 +49,7 @@
#include <sfx2/objsh.hxx>
#include <comphelper/lok.hxx>
#include <objshimp.hxx>
#include "shutdownicon.hxx"
#include <shutdownicon.hxx>
using namespace ::com::sun::star::uno;
+7 -2
View File
@@ -20,7 +20,7 @@
#include <sal/config.h>
#include <sal/log.hxx>
#include "shutdownicon.hxx"
#include <shutdownicon.hxx>
#include <sfx2/strings.hrc>
#include <sfx2/app.hxx>
#include <svtools/imagemgr.hxx>
@@ -203,6 +203,11 @@ void ShutdownIcon::FileOpen()
void ShutdownIcon::FromTemplate()
{
ShutdownIcon::FromCommand( ".uno:NewDoc" );
}
void ShutdownIcon::FromCommand( const OUString& rCommand )
{
if ( !getInstance() || !getInstance()->m_xDesktop.is() )
return;
@@ -213,7 +218,7 @@ void ShutdownIcon::FromTemplate()
xFrame = xDesktop;
URL aTargetURL;
aTargetURL.Complete = ".uno:NewDoc";
aTargetURL.Complete = rCommand;
css::uno::Reference< util::XURLTransformer > xTrans( util::URLTransformer::create( ::comphelper::getProcessComponentContext() ) );
xTrans->parseStrict( aTargetURL );
+370 -18
View File
@@ -21,6 +21,7 @@
#include <unotools/moduleoptions.hxx>
#include <unotools/dynamicmenuoptions.hxx>
#include <unotools/historyoptions.hxx>
#include <officecfg/Office/Common.hxx>
#include <rtl/ustring.hxx>
#include <tools/urlobj.hxx>
#include <osl/file.h>
@@ -31,7 +32,10 @@
#include <sfx2/sfxresid.hxx>
#include <sfx2/strings.hrc>
#include <vcl/svapp.hxx>
#include "shutdownicon.hxx"
#include <vcl/mnemonic.hxx>
#include <vcl/image.hxx>
#include <svtools/imagemgr.hxx>
#include <shutdownicon.hxx>
#include <com/sun/star/util/XStringWidth.hpp>
@@ -55,6 +59,8 @@
#define MI_TEMPLATE 8
#define MI_STARTMODULE 9
#define UNO_TOGGLECURRENTMODULE_COMMAND ".uno:ToggleCurrentModule"
@interface QSMenuExecute : NSObject
{
}
@@ -65,6 +71,8 @@
@implementation QSMenuExecute
-(void)executeMenuItem: (NSMenuItem*)pItem
{
SolarMutexGuard aGuard;
switch( [pItem tag] )
{
case MI_OPEN:
@@ -102,6 +110,9 @@
-(void)dockIconClicked: (NSObject*)pSender
{
(void)pSender;
SolarMutexGuard aGuard;
// start module
ShutdownIcon::OpenURL( STARTMODULE_URL, "_default" );
}
@@ -113,6 +124,7 @@ bool ShutdownIcon::IsQuickstarterInstalled()
return true;
}
static NSArray<NSMenuItem*>* pPreferredMenus = nil;
static NSMenuItem* pDefMenu = nil, *pDockSubMenu = nil;
static QSMenuExecute* pExecute = nil;
@@ -147,6 +159,63 @@ class RecentFilesStringLength : public ::cppu::WeakImplHelper< css::util::XStrin
}
@interface QSCommandMenuItem : NSMenuItem
{
OUString m_aCommand;
}
-(void)menuItemTriggered: (id)aSender;
-(void)setCommand: (OUString)aCommand;
@end
@implementation QSCommandMenuItem
-(void)menuItemTriggered: (id)aSender
{
if ( m_aCommand.isEmpty() )
return;
SolarMutexGuard aGuard;
if ( m_aCommand == "vnd.org.libreoffice.recentdocs:ClearRecentFileList" )
{
// Clearing the recent file list requires an extra step
SvtHistoryOptions::Clear( EHistoryType::PickList, false );
}
else if ( m_aCommand == ".uno:Open" )
{
ShutdownIcon::FileOpen();
return;
}
else if ( m_aCommand == ".uno:ConfigureDialog" )
{
// Selecting some menu items will cause a crash if there are
// no visibile windows
ShutdownIcon::OpenURL( STARTMODULE_URL, "_default" );
}
else if ( m_aCommand == UNO_TOGGLECURRENTMODULE_COMMAND )
{
bool bIsExclusive = officecfg::Office::Common::History::ShowCurrentModuleOnly::get();
std::shared_ptr<comphelper::ConfigurationChanges> batch(comphelper::ConfigurationChanges::create());
officecfg::Office::Common::History::ShowCurrentModuleOnly::set(!bIsExclusive, batch);
batch->commit();
[self setState: bIsExclusive ? NSControlStateValueOff : NSControlStateValueOn];
return;
}
// "private:" commands are used for menu items in the File > New menu
if ( m_aCommand.startsWith( "private:" ) || m_aCommand == STARTMODULE_URL )
ShutdownIcon::OpenURL( m_aCommand, "_default" );
else
ShutdownIcon::FromCommand( m_aCommand );
}
-(void)setCommand: (OUString)aCommand
{
m_aCommand = aCommand;
}
@end
@interface RecentMenuDelegate : NSObject <NSMenuDelegate>
{
std::vector< RecentMenuEntry >* m_pRecentFilesItems;
@@ -175,6 +244,8 @@ class RecentFilesStringLength : public ::cppu::WeakImplHelper< css::util::XStrin
-(void)menuNeedsUpdate:(NSMenu *)menu
{
SolarMutexGuard aGuard;
// clear menu
int nItems = [menu numberOfItems];
while( nItems -- )
@@ -202,38 +273,114 @@ class RecentFilesStringLength : public ::cppu::WeakImplHelper< css::util::XStrin
// insert new recent items
for ( std::vector<RecentMenuEntry>::size_type i = 0; i < m_pRecentFilesItems->size(); i++ )
{
OUString aMenuTitle;
INetURLObject aURL( (*m_pRecentFilesItems)[i].aURL );
if ( aURL.GetProtocol() == INetProtocol::File )
OUStringBuffer aMenuShortCut;
if ( i <= 9 )
{
// Do handle file URL differently => convert it to a system
// path and abbreviate it with a special function:
OUString aSystemPath( aURL.getFSysPath( FSysStyle::Detect ) );
OUString aCompactedSystemPath;
oslFileError nError = osl_abbreviateSystemPath( aSystemPath.pData, &aCompactedSystemPath.pData, 46, nullptr );
if ( !nError )
aMenuTitle = aCompactedSystemPath;
if ( i == 9 )
aMenuShortCut.append( "1~0. " );
else
aMenuTitle = aSystemPath;
{
aMenuShortCut.append( "~N. " );
aMenuShortCut[ 1 ] = sal_Unicode( i + '1' );
}
}
else
{
// Use INetURLObject to abbreviate all other URLs
css::uno::Reference< css::util::XStringWidth > xStringLength( new RecentFilesStringLength() );
aMenuTitle = aURL.getAbbreviated( xStringLength, 46, INetURLObject::DecodeMechanism::Unambiguous );
aMenuShortCut.append( OUString::number(sal_Int32( i + 1 ) ) + ". " );
}
OUString aMenuTitle;
INetURLObject aURL( (*m_pRecentFilesItems)[i].aURL );
NSImage *pImage = nil;
if ( aURL.GetProtocol() == INetProtocol::File )
{
// Do handle file URL differently: don't show the protocol,
// just the file name
aMenuTitle = aURL.GetLastName(INetURLObject::DecodeMechanism::WithCharset);
if ( [NSApp respondsToSelector: @selector(createNSImage:)] )
{
BitmapEx aThumbnail(SvFileInformationManager::GetFileImageId(aURL));
Size aBmpSize = aThumbnail.GetSizePixel();
if ( aBmpSize.Width() > 0 && aBmpSize.Height() > 0 )
{
Image aImage( aThumbnail );
NSValue *pImageValue = [NSValue valueWithPointer: &aImage];
pImage = [NSApp performSelector: @selector(createNSImage:) withObject: pImageValue];
}
}
}
else
{
// In all other URLs show the protocol name before the file name
aMenuTitle = INetURLObject::GetSchemeName(aURL.GetProtocol()) + ": " + aURL.getName();
}
aMenuShortCut.append( aMenuTitle );
aMenuTitle = MnemonicGenerator::EraseAllMnemonicChars( aMenuShortCut.makeStringAndClear() );
if ( aMenuTitle.isEmpty() )
continue;
if ( aMenuTitle.endsWith( "...", &aMenuTitle ) )
aMenuTitle += u"\u2026";
NSMenuItem* pNewItem = [[NSMenuItem alloc] initWithTitle: getAutoreleasedString( aMenuTitle )
action: @selector(executeRecentEntry:)
keyEquivalent: @""];
[pNewItem setTag: i];
[pNewItem setTarget: self];
[pNewItem setEnabled: YES];
if ( pImage )
{
[pNewItem setImage: pImage];
[pImage release];
}
[menu addItem: pNewItem];
[pNewItem autorelease];
}
if ( [menu numberOfItems] )
{
TranslateId aId( "STR_CLEAR_RECENT_FILES", "Clear List" );
OUString aClearList = Translate::get( aId, Translate::Create("fwk") );
if ( !aClearList.isEmpty() )
{
[menu addItem: [NSMenuItem separatorItem]];
QSCommandMenuItem* pNewItem = [[QSCommandMenuItem alloc] initWithTitle: getAutoreleasedString( aClearList ) action: @selector(menuItemTriggered:) keyEquivalent: @""];
[pNewItem setCommand: "vnd.org.libreoffice.recentdocs:ClearRecentFileList"];
[pNewItem setTarget: pNewItem];
[pNewItem setEnabled: YES];
[menu addItem: pNewItem];
[pNewItem autorelease];
aId = TranslateId( "STR_TOGGLECURRENTMODULE", "Current Module Only" );
OUString aToggleCurrentMode = Translate::get( aId, Translate::Create("fwk") );
if ( !aToggleCurrentMode.isEmpty() )
{
pNewItem = [[QSCommandMenuItem alloc] initWithTitle: getAutoreleasedString( aToggleCurrentMode ) action: @selector(menuItemTriggered:) keyEquivalent: @""];
[pNewItem setCommand: UNO_TOGGLECURRENTMODULE_COMMAND];
[pNewItem setTarget: pNewItem];
[pNewItem setState: officecfg::Office::Common::History::ShowCurrentModuleOnly::get() ? NSControlStateValueOn : NSControlStateValueOff];
[pNewItem setEnabled: YES];
[menu addItem: pNewItem];
[pNewItem autorelease];
}
}
}
else
{
TranslateId aId( "STR_NODOCUMENT", "No Documents" );
OUString aNoDocuments = Translate::get( aId, Translate::Create("fwk") );
if ( !aNoDocuments.isEmpty() )
{
NSMenuItem* pNewItem = [[NSMenuItem alloc] initWithTitle: getAutoreleasedString( aNoDocuments ) action: nil keyEquivalent: @""];
[pNewItem setEnabled: YES];
[menu addItem: pNewItem];
[pNewItem autorelease];
}
}
}
-(void)executeRecentEntry: (NSMenuItem*)item
@@ -311,6 +458,7 @@ static void appendMenuItem( NSMenu* i_pMenu, NSMenu* i_pDockMenu, const OUString
[pItem setTarget: pExecute];
[pItem setEnabled: YES];
[i_pMenu addItem: pItem];
[pItem autorelease];
if( i_pDockMenu )
{
@@ -323,6 +471,7 @@ static void appendMenuItem( NSMenu* i_pMenu, NSMenu* i_pDockMenu, const OUString
[pItem setTarget: pExecute];
[pItem setEnabled: YES];
[i_pDockMenu addItem: pItem];
[pItem autorelease];
}
}
@@ -344,6 +493,206 @@ static void appendRecentMenu( NSMenu* i_pMenu, const OUString& i_rTitle )
[pItem setSubmenu: pRecentMenu];
}
void setKeyEquivalent( const vcl::KeyCode &rKeyCode, NSMenuItem *pNSMenuItem )
{
if ( !pNSMenuItem )
return;
sal_uInt16 nKeyCode = rKeyCode.GetCode();
if ( !nKeyCode )
return;
sal_Unicode nCommandKey = 0;
if ((nKeyCode>=KEY_A) && (nKeyCode<=KEY_Z)) // letter A..Z
nCommandKey = nKeyCode - KEY_A + 'a';
else if ((nKeyCode>=KEY_0) && (nKeyCode<=KEY_9)) // numbers 0..9
nCommandKey = nKeyCode - KEY_0 + '0';
else if ((nKeyCode>=KEY_F1) && (nKeyCode<=KEY_F26)) // function keys F1..F26
nCommandKey = nKeyCode - KEY_F1 + NSF1FunctionKey;
if ( !nCommandKey )
return;
sal_uInt16 nModifier = rKeyCode.GetModifier();
int nItemModifier = 0;
if ( nModifier & KEY_SHIFT )
{
nItemModifier |= NSEventModifierFlagShift; // actually useful only for function keys
if ( nKeyCode >= KEY_A && nKeyCode <= KEY_Z )
nCommandKey = nKeyCode - KEY_A + 'A';
}
if ( nModifier & KEY_MOD1 )
nItemModifier |= NSEventModifierFlagCommand;
if ( nModifier & KEY_MOD2 )
nItemModifier |= NSEventModifierFlagOption;
if ( nModifier & KEY_MOD3 )
nItemModifier |= NSEventModifierFlagControl;
OUString aCommandKey( &nCommandKey, 1 );
NSString *pCommandKey = [NSString stringWithCharacters: reinterpret_cast< unichar const* >(aCommandKey.getStr()) length: aCommandKey.getLength()];
[pNSMenuItem setKeyEquivalent: pCommandKey];
[pNSMenuItem setKeyEquivalentModifierMask: nItemModifier];
}
static NSMenu *getNSMenuForVCLMenu( Menu *pMenu )
{
NSMenu *pRet = nil;
if ( !pMenu )
return pRet;
pMenu->Activate();
sal_uInt16 nItemCount = pMenu->GetItemCount();
if ( nItemCount )
{
pRet = [[[NSMenu alloc] initWithTitle: @""] autorelease];
[pRet setAutoenablesItems: NO];
for ( sal_uInt16 i = 0; i < nItemCount; i++ )
{
sal_uInt16 nId = pMenu->GetItemId( i );
if ( nId && pMenu->IsItemEnabled( nId ) )
{
OUString aText = MnemonicGenerator::EraseAllMnemonicChars( pMenu->GetItemText( nId ) );
if ( aText.isEmpty() )
continue;
if ( aText.endsWith( "...", &aText ) )
aText += u"\u2026";
// Use a custom menu in place of the Start Center's recent
// documents menu so that the list can be dynamically updated
OUString aCommand = pMenu->GetItemCommand( nId );
if ( aCommand == ".uno:RecentFileList" )
{
appendRecentMenu( pRet, aText );
continue;
}
NSString *pText = getAutoreleasedString( aText );
// TODO: use the QSMenuExecute class to connect the command
// string to one of the existing handler functions
QSCommandMenuItem *pNSMenuItem = [[QSCommandMenuItem alloc] initWithTitle: pText action: @selector(menuItemTriggered:) keyEquivalent: @""];
NSMenu *pNSSubmenu = getNSMenuForVCLMenu( pMenu->GetPopupMenu( nId ) );
if ( pNSSubmenu && [pNSSubmenu numberOfItems] )
{
[pNSSubmenu setTitle: pText];
[pNSMenuItem setSubmenu: pNSSubmenu];
if ( aCommand == ".uno:AddDirect" )
{
SvtModuleOptions aModuleOptions;
if ( aModuleOptions.IsModuleInstalled( SvtModuleOptions::EModule::STARTMODULE ) )
{
QSCommandMenuItem *pStartModuleMenuItem = [[QSCommandMenuItem alloc] initWithTitle: getAutoreleasedString( SfxResId( STR_QUICKSTART_STARTCENTER ) ) action: @selector(menuItemTriggered:) keyEquivalent: @"n"];
[pStartModuleMenuItem setTarget: pStartModuleMenuItem];
[pStartModuleMenuItem setCommand: STARTMODULE_URL];
[pNSSubmenu insertItem: pStartModuleMenuItem atIndex: 0];
[pStartModuleMenuItem autorelease];
}
}
}
else if ( !aCommand.isEmpty() )
{
[pNSMenuItem setTarget: pNSMenuItem];
[pNSMenuItem setCommand: aCommand];
// Use the default menu's special "open new file" shortcuts
if ( aCommand == WRITER_URL )
[pNSMenuItem setKeyEquivalent: @"t"];
else if ( aCommand == CALC_URL )
[pNSMenuItem setKeyEquivalent: @"s"];
else if ( aCommand == IMPRESS_WIZARD_URL )
[pNSMenuItem setKeyEquivalent: @"p"];
else if ( aCommand == DRAW_URL )
[pNSMenuItem setKeyEquivalent: @"d"];
else if ( aCommand == MATH_URL )
[pNSMenuItem setKeyEquivalent: @"f"];
else if ( aCommand == BASE_URL )
[pNSMenuItem setKeyEquivalent: @"a"];
else
setKeyEquivalent( pMenu->GetAccelKey( nId ), pNSMenuItem );
}
[pRet addItem: pNSMenuItem];
[pNSMenuItem autorelease];
}
else if ( pMenu->GetItemType( i ) == MenuItemType::SEPARATOR )
{
[pRet addItem: [NSMenuItem separatorItem]];
}
}
}
pMenu->Deactivate();
return pRet;
}
static void clearDefaultMenuBar()
{
if( ![NSApp respondsToSelector: @selector(removeFallbackMenuItem:)] )
return;
// Remove previous default menu
if ( pDefMenu )
[NSApp performSelector:@selector(removeFallbackMenuItem:) withObject: pDefMenu];
// Remove previous preferred menu
if ( pPreferredMenus && [pPreferredMenus count] )
{
for ( NSMenuItem *pNSMenuItem in pPreferredMenus )
[NSApp performSelector:@selector(removeFallbackMenuItem:) withObject: pNSMenuItem];
}
}
static void resetMenuBar()
{
if( ![NSApp respondsToSelector: @selector(addFallbackMenuItem:)] )
return;
clearDefaultMenuBar();
if ( pPreferredMenus && [pPreferredMenus count] )
{
for ( NSMenuItem *pNSMenuItem in pPreferredMenus )
[NSApp performSelector:@selector(addFallbackMenuItem:) withObject: pNSMenuItem];
}
else if ( pDefMenu )
{
[NSApp performSelector:@selector(addFallbackMenuItem:) withObject: pDefMenu];
}
}
void ShutdownIcon::SetDefaultMenuBar( MenuBar *pMenuBar )
{
if ( !pMenuBar )
return;
SolarMutexGuard aGuard;
clearDefaultMenuBar();
if ( pPreferredMenus )
{
[pPreferredMenus release];
pPreferredMenus = nil;
}
NSMenu *pNSMenu = getNSMenuForVCLMenu( pMenuBar );
if ( pNSMenu && [pNSMenu numberOfItems] )
{
pPreferredMenus = [NSMutableArray arrayWithArray: [pNSMenu itemArray]];
[pNSMenu removeAllItems];
[pPreferredMenus retain];
}
resetMenuBar();
}
extern "C"
{
@@ -444,7 +793,7 @@ void aqua_init_systray()
appendMenuItem( pMenu, pDockMenu, aTitle, MI_OPEN, aKeyEquiv );
[pDefMenu setSubmenu: pMenu];
[NSApp performSelector:@selector(addFallbackMenuItem:) withObject: pDefMenu];
resetMenuBar();
if( [NSApp respondsToSelector: @selector(addDockMenuItem:)] )
{
@@ -454,6 +803,9 @@ void aqua_init_systray()
}
else
OSL_FAIL( "addDockMenuItem selector failed on NSApp" );
[pMenu autorelease];
[pDockMenu autorelease];
}
else
OSL_FAIL( "addFallbackMenuItem selector failed on NSApp" );
+1 -1
View File
@@ -25,7 +25,7 @@
#undef WB_LEFT
#undef WB_RIGHT
#include "shutdownicon.hxx"
#include <shutdownicon.hxx>
#include <sfx2/sfxresid.hxx>
#include <sfx2/strings.hrc>
#include <shlobj.h>
+10
View File
@@ -45,6 +45,10 @@
#include <sfx2/notebookbar/SfxNotebookBar.hxx>
#ifdef MACOSX
#include <shutdownicon.hxx>
#endif
namespace {
/**
@@ -396,6 +400,12 @@ void SAL_CALL BackingComp::attachFrame( /*IN*/ const css::uno::Reference< css::f
pBack->get_width_request(),
pBack->get_height_request() + nMenuHeight));
#ifdef MACOSX
SystemWindow *pSysWin = static_cast<SystemWindow*>(pParent.get());
if (pSysWin)
ShutdownIcon::SetDefaultMenuBar(pSysWin->GetMenuBar());
#endif
/* } SAFE */
}
@@ -32,6 +32,10 @@
#include <tools/link.hxx>
#include <memory>
#ifdef MACOSX
#include <vcl/menu.hxx>
#endif
extern "C" {
void SAL_DLLPUBLIC_EXPORT plugin_init_sys_tray();
@@ -101,6 +105,7 @@ class ShutdownIcon : public ShutdownIconServiceBase
static void OpenURL( const OUString& aURL, const OUString& rTarget, const css::uno::Sequence< css::beans::PropertyValue >& =
css::uno::Sequence< css::beans::PropertyValue >( 0 ) );
static void FromTemplate();
static void FromCommand( const OUString& rCommand );
static void SetAutostart( bool bActivate );
static bool GetAutostart();
@@ -139,6 +144,8 @@ class ShutdownIcon : public ShutdownIconServiceBase
#ifdef _WIN32
static void EnableAutostartW32( const OUString &aShortcutName );
static OUString GetAutostartFolderNameW32();
#elif defined MACOSX
static void SetDefaultMenuBar( MenuBar *pMenuBar );
#endif
};
+1 -1
View File
@@ -10086,7 +10086,6 @@ sfx2/source/appl/preventduplicateinteraction.cxx
sfx2/source/appl/sfxhelp.cxx
sfx2/source/appl/sfxpicklist.cxx
sfx2/source/appl/shutdownicon.cxx
sfx2/source/appl/shutdownicon.hxx
sfx2/source/appl/shutdowniconw32.cxx
sfx2/source/appl/workwin.cxx
sfx2/source/appl/xpackcreator.cxx
@@ -10208,6 +10207,7 @@ sfx2/source/inc/objshimp.hxx
sfx2/source/inc/partwnd.hxx
sfx2/source/inc/recfloat.hxx
sfx2/source/inc/sfxurlrelocator.hxx
sfx2/source/inc/shutdownicon.hxx
sfx2/source/inc/slotserv.hxx
sfx2/source/inc/splitwin.hxx
sfx2/source/inc/statcach.hxx
+1
View File
@@ -64,6 +64,7 @@ class AquaSalFrame;
-(BOOL)applicationShouldHandleReopen: (NSApplication*)pApp hasVisibleWindows: (BOOL)bWinVisible;
-(BOOL)applicationSupportsSecureRestorableState: (NSApplication *)pApp;
-(void)setDockIconClickHandler: (NSObject*)pHandler;
-(NSImage*)createNSImage: (NSValue*)pImageValue;
@end
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
+11
View File
@@ -478,6 +478,17 @@
GetSalData()->mpDockIconClickHandler = pHandler;
}
-(NSImage*)createNSImage: (NSValue*)pImageValue
{
if (pImageValue)
{
Image *pImage = static_cast<Image*>([pImageValue pointerValue]);
if (pImage)
return CreateNSImage(*pImage);
}
return nil;
}
@end