blob: d384078e91bc712a6e08f158feb310aa217c69db [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qcocoamenuloader.h"
#include "messages.h"
#include "qcocoahelpers.h"
#include "qcocoansmenu.h"
#include "qcocoamenubar.h"
#include "qcocoamenuitem.h"
#include "qcocoaintegration.h"
#include <QtCore/private/qcore_mac_p.h>
#include <QtCore/private/qthread_p.h>
#include <QtCore/qcoreapplication.h>
#include <QtGui/private/qguiapplication_p.h>
@implementation QCocoaMenuLoader {
NSMenu *theMenu;
NSMenu *appMenu;
NSMenuItem *quitItem;
NSMenuItem *preferencesItem;
NSMenuItem *aboutItem;
NSMenuItem *aboutQtItem;
NSMenuItem *hideItem;
NSMenuItem *servicesItem;
NSMenuItem *hideAllOthersItem;
NSMenuItem *showAllItem;
}
+ (instancetype)sharedMenuLoader
{
static QCocoaMenuLoader *shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared = [[self alloc] init];
atexit_b(^{
[shared release];
shared = nil;
});
});
return shared;
}
- (instancetype)init
{
if ((self = [super init])) {
NSString *appName = qt_mac_applicationName().toNSString();
// Menubar as menu. Title as set in the NIB file
theMenu = [[NSMenu alloc] initWithTitle:@"Main Menu"];
// Application menu. Since 10.6, the first menu
// is always identified as the application menu.
NSMenuItem *appItem = [[[NSMenuItem alloc] init] autorelease];
appItem.title = appName;
[theMenu addItem:appItem];
appMenu = [[NSMenu alloc] initWithTitle:appName];
appItem.submenu = appMenu;
// About Application
aboutItem = [[QCocoaNSMenuItem alloc] init];
aboutItem.title = [@"About " stringByAppendingString:appName];
// FIXME This seems useless since barely adding a QAction
// with AboutRole role will reset the target/action
aboutItem.target = self;
aboutItem.action = @selector(orderFrontStandardAboutPanel:);
// Disable until a QAction is associated
aboutItem.enabled = NO;
aboutItem.hidden = YES;
[appMenu addItem:aboutItem];
// About Qt (shameless self-promotion)
aboutQtItem = [[QCocoaNSMenuItem alloc] init];
aboutQtItem.title = @"About Qt";
// Disable until a QAction is associated
aboutQtItem.enabled = NO;
aboutQtItem.hidden = YES;
[appMenu addItem:aboutQtItem];
[appMenu addItem:[NSMenuItem separatorItem]];
// Preferences
// We'll be adding app specific items after this. The macOS HIG state that,
// "In general, a Preferences menu item should be the first app-specific menu item."
// https://developer.apple.com/macos/human-interface-guidelines/menus/menu-bar-menus/
preferencesItem = [[QCocoaNSMenuItem alloc] init];
preferencesItem.title = @"Preferences…";
preferencesItem.keyEquivalent = @",";
// Disable until a QAction is associated
preferencesItem.enabled = NO;
preferencesItem.hidden = YES;
[appMenu addItem:preferencesItem];
[appMenu addItem:[NSMenuItem separatorItem]];
// Services item and menu
servicesItem = [[NSMenuItem alloc] init];
servicesItem.title = @"Services";
NSMenu *servicesMenu = [[[NSMenu alloc] initWithTitle:@"Services"] autorelease];
servicesItem.submenu = servicesMenu;
[NSApplication sharedApplication].servicesMenu = servicesMenu;
[appMenu addItem:servicesItem];
[appMenu addItem:[NSMenuItem separatorItem]];
// Hide Application
hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName]
action:@selector(hide:)
keyEquivalent:@"h"];
hideItem.target = self;
[appMenu addItem:hideItem];
// Hide Others
hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others"
action:@selector(hideOtherApplications:)
keyEquivalent:@"h"];
hideAllOthersItem.target = self;
hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption;
[appMenu addItem:hideAllOthersItem];
// Show All
showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All"
action:@selector(unhideAllApplications:)
keyEquivalent:@""];
showAllItem.target = self;
[appMenu addItem:showAllItem];
[appMenu addItem:[NSMenuItem separatorItem]];
// Quit Application
quitItem = [[QCocoaNSMenuItem alloc] init];
quitItem.title = [@"Quit " stringByAppendingString:appName];
quitItem.keyEquivalent = @"q";
// This will remain true until synced with a QCocoaMenuItem.
// This way, we will always have a functional Quit menu item
// even if no QAction is added.
quitItem.action = @selector(terminate:);
[appMenu addItem:quitItem];
}
return self;
}
- (void)dealloc
{
[theMenu release];
[appMenu release];
[aboutItem release];
[aboutQtItem release];
[preferencesItem release];
[servicesItem release];
[hideItem release];
[hideAllOthersItem release];
[showAllItem release];
[quitItem release];
[super dealloc];
}
- (void)ensureAppMenuInMenu:(NSMenu *)menu
{
// The application menu is the menu in the menu bar that contains the
// 'Quit' item. When changing menu bar (e.g when switching between
// windows with different menu bars), we never recreate this menu, but
// instead pull it out the current menu bar and place into the new one:
NSMenu *mainMenu = [NSApp mainMenu];
if (mainMenu == menu)
return; // nothing to do (menu is the current menu bar)!
#ifndef QT_NAMESPACE
Q_ASSERT(mainMenu);
#endif
// Grab the app menu out of the current menu.
auto unparentAppMenu = ^bool (NSMenu *supermenu) {
auto index = [supermenu indexOfItemWithSubmenu:appMenu];
if (index != -1) {
[supermenu removeItemAtIndex:index];
return true;
}
return false;
};
if (!mainMenu || !unparentAppMenu(mainMenu))
if (appMenu.supermenu)
unparentAppMenu(appMenu.supermenu);
NSMenuItem *appMenuItem = [[NSMenuItem alloc] initWithTitle:@"Apple"
action:nil keyEquivalent:@""];
appMenuItem.submenu = appMenu;
[menu insertItem:appMenuItem atIndex:0];
}
- (NSMenu *)menu
{
return [[theMenu retain] autorelease];
}
- (NSMenu *)applicationMenu
{
return [[appMenu retain] autorelease];
}
- (NSMenuItem *)quitMenuItem
{
return [[quitItem retain] autorelease];
}
- (NSMenuItem *)preferencesMenuItem
{
return [[preferencesItem retain] autorelease];
}
- (NSMenuItem *)aboutMenuItem
{
return [[aboutItem retain] autorelease];
}
- (NSMenuItem *)aboutQtMenuItem
{
return [[aboutQtItem retain] autorelease];
}
- (NSMenuItem *)hideMenuItem
{
return [[hideItem retain] autorelease];
}
- (NSMenuItem *)appSpecificMenuItem:(QCocoaMenuItem *)platformItem
{
// No reason to create the item if it already exists.
for (NSMenuItem *item in appMenu.itemArray)
if (qt_objc_cast<QCocoaNSMenuItem *>(item).platformMenuItem == platformItem)
return item;
// Create an App-Specific menu item, insert it into the menu and return
// it as an autorelease item.
QCocoaNSMenuItem *item;
if (platformItem->isSeparator())
item = [QCocoaNSMenuItem separatorItemWithPlatformMenuItem:platformItem];
else
item = [[[QCocoaNSMenuItem alloc] initWithPlatformMenuItem:platformItem] autorelease];
const auto location = [self indexOfLastAppSpecificMenuItem];
[appMenu insertItem:item atIndex:NSInteger(location) + 1];
return item;
}
- (void)orderFrontStandardAboutPanel:(id)sender
{
[NSApp orderFrontStandardAboutPanel:sender];
}
- (void)hideOtherApplications:(id)sender
{
[NSApp hideOtherApplications:sender];
}
- (void)unhideAllApplications:(id)sender
{
[NSApp unhideAllApplications:sender];
}
- (void)hide:(id)sender
{
[NSApp hide:sender];
}
- (void)qtTranslateApplicationMenu
{
#ifndef QT_NO_TRANSLATION
aboutItem.title = qt_mac_applicationmenu_string(AboutAppMenuItem).arg(qt_mac_applicationName()).toNSString();
preferencesItem.title = qt_mac_applicationmenu_string(PreferencesAppMenuItem).toNSString();
servicesItem.title = qt_mac_applicationmenu_string(ServicesAppMenuItem).toNSString();
hideItem.title = qt_mac_applicationmenu_string(HideAppMenuItem).arg(qt_mac_applicationName()).toNSString();
hideAllOthersItem.title = qt_mac_applicationmenu_string(HideOthersAppMenuItem).toNSString();
showAllItem.title = qt_mac_applicationmenu_string(ShowAllAppMenuItem).toNSString();
quitItem.title = qt_mac_applicationmenu_string(QuitAppMenuItem).arg(qt_mac_applicationName()).toNSString();
#endif
}
- (BOOL)validateMenuItem:(NSMenuItem*)menuItem
{
if (menuItem.action == @selector(hideOtherApplications:)
|| menuItem.action == @selector(unhideAllApplications:))
return [NSApp validateMenuItem:menuItem];
if (menuItem.action == @selector(hide:)) {
if (QCocoaIntegration::instance()->activePopupWindow())
return NO;
return [NSApp validateMenuItem:menuItem];
}
return menuItem.enabled;
}
- (NSArray<NSMenuItem *> *)mergeable
{
// Don't include the quitItem here, since we want it always visible and enabled regardless
auto items = [NSArray arrayWithObjects:preferencesItem, aboutItem, aboutQtItem,
appMenu.itemArray[[self indexOfLastAppSpecificMenuItem]], nil];
return items;
}
- (NSUInteger)indexOfLastAppSpecificMenuItem
{
// Either the 'Preferences', which is the first app specific menu item, or something
// else we appended later (thus the reverse order):
const auto location = [appMenu.itemArray indexOfObjectWithOptions:NSEnumerationReverse
passingTest:^BOOL(NSMenuItem *item, NSUInteger, BOOL *) {
if (auto qtItem = qt_objc_cast<QCocoaNSMenuItem*>(item))
return qtItem != quitItem;
return NO;
}];
Q_ASSERT(location != NSNotFound);
return location;
}
@end