blob: 363defdd2802a08a24c4854769bdd841b6f6b57e [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Copyright (C) 2012 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author James Turner <james.turner@kdab.com>
** 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 <AppKit/AppKit.h>
#include "qcocoamenubar.h"
#include "qcocoawindow.h"
#include "qcocoamenuloader.h"
#include "qcocoaapplication.h" // for custom application category
#include "qcocoaapplicationdelegate.h"
#include <QtGui/QGuiApplication>
#include <QtCore/QDebug>
QT_BEGIN_NAMESPACE
static QList<QCocoaMenuBar*> static_menubars;
QCocoaMenuBar::QCocoaMenuBar()
{
static_menubars.append(this);
m_nativeMenu = [[NSMenu alloc] init];
#ifdef QT_COCOA_ENABLE_MENU_DEBUG
qDebug() << "Construct QCocoaMenuBar" << this << m_nativeMenu;
#endif
}
QCocoaMenuBar::~QCocoaMenuBar()
{
#ifdef QT_COCOA_ENABLE_MENU_DEBUG
qDebug() << "~QCocoaMenuBar" << this;
#endif
for (auto menu : qAsConst(m_menus)) {
if (!menu)
continue;
NSMenuItem *item = nativeItemForMenu(menu);
if (menu->attachedItem() == item)
menu->setAttachedItem(nil);
}
[m_nativeMenu release];
static_menubars.removeOne(this);
if (!m_window.isNull() && m_window->menubar() == this) {
m_window->setMenubar(nullptr);
// Delete the children first so they do not cause
// the native menu items to be hidden after
// the menu bar was updated
qDeleteAll(children());
updateMenuBarImmediately();
}
}
bool QCocoaMenuBar::needsImmediateUpdate()
{
if (!m_window.isNull()) {
if (m_window->window()->isActive())
return true;
} else {
// Only update if the focus/active window has no
// menubar, which means it'll be using this menubar.
// This is to avoid a modification in a parentless
// menubar to affect a window-assigned menubar.
QWindow *fw = QGuiApplication::focusWindow();
if (!fw) {
// Same if there's no focus window, BTW.
return true;
} else {
QCocoaWindow *cw = static_cast<QCocoaWindow *>(fw->handle());
if (cw && !cw->menubar())
return true;
}
}
// Either the menubar is attached to a non-active window,
// or the application's focus window has its own menubar
// (which is different from this one)
return false;
}
void QCocoaMenuBar::insertMenu(QPlatformMenu *platformMenu, QPlatformMenu *before)
{
QCocoaMenu *menu = static_cast<QCocoaMenu *>(platformMenu);
QCocoaMenu *beforeMenu = static_cast<QCocoaMenu *>(before);
#ifdef QT_COCOA_ENABLE_MENU_DEBUG
qDebug() << "QCocoaMenuBar" << this << "insertMenu" << menu << "before" << before;
#endif
if (m_menus.contains(QPointer<QCocoaMenu>(menu))) {
qWarning("This menu already belongs to the menubar, remove it first");
return;
}
if (beforeMenu && !m_menus.contains(QPointer<QCocoaMenu>(beforeMenu))) {
qWarning("The before menu does not belong to the menubar");
return;
}
int insertionIndex = beforeMenu ? m_menus.indexOf(beforeMenu) : m_menus.size();
m_menus.insert(insertionIndex, menu);
{
QMacAutoReleasePool pool;
NSMenuItem *item = [[[NSMenuItem alloc] init] autorelease];
item.tag = reinterpret_cast<NSInteger>(menu);
if (beforeMenu) {
// QMenuBar::toNSMenu() exposes the native menubar and
// the user could have inserted its own items in there.
// Same remark applies to removeMenu().
NSMenuItem *beforeItem = nativeItemForMenu(beforeMenu);
NSInteger nativeIndex = [m_nativeMenu indexOfItem:beforeItem];
[m_nativeMenu insertItem:item atIndex:nativeIndex];
} else {
[m_nativeMenu addItem:item];
}
}
syncMenu_helper(menu, false /*internaCall*/);
if (needsImmediateUpdate())
updateMenuBarImmediately();
}
void QCocoaMenuBar::removeMenu(QPlatformMenu *platformMenu)
{
QCocoaMenu *menu = static_cast<QCocoaMenu *>(platformMenu);
if (!m_menus.contains(menu)) {
qWarning("Trying to remove a menu that does not belong to the menubar");
return;
}
NSMenuItem *item = nativeItemForMenu(menu);
if (menu->attachedItem() == item)
menu->setAttachedItem(nil);
m_menus.removeOne(menu);
QMacAutoReleasePool pool;
// See remark in insertMenu().
NSInteger nativeIndex = [m_nativeMenu indexOfItem:item];
[m_nativeMenu removeItemAtIndex:nativeIndex];
}
void QCocoaMenuBar::syncMenu(QPlatformMenu *menu)
{
syncMenu_helper(menu, false /*internaCall*/);
}
void QCocoaMenuBar::syncMenu_helper(QPlatformMenu *menu, bool menubarUpdate)
{
QMacAutoReleasePool pool;
QCocoaMenu *cocoaMenu = static_cast<QCocoaMenu *>(menu);
Q_FOREACH (QCocoaMenuItem *item, cocoaMenu->items())
cocoaMenu->syncMenuItem_helper(item, menubarUpdate);
BOOL shouldHide = YES;
if (cocoaMenu->isVisible()) {
// If the NSMenu has no visble items, or only separators, we should hide it
// on the menubar. This can happen after syncing the menu items since they
// can be moved to other menus.
for (NSMenuItem *item in cocoaMenu->nsMenu().itemArray)
if (!item.separatorItem && !item.hidden) {
shouldHide = NO;
break;
}
}
if (NSMenuItem *attachedItem = cocoaMenu->attachedItem()) {
// Non-nil attached item means the item's submenu is set
attachedItem.title = cocoaMenu->nsMenu().title;
attachedItem.hidden = shouldHide;
}
}
NSMenuItem *QCocoaMenuBar::nativeItemForMenu(QCocoaMenu *menu) const
{
if (!menu)
return nil;
return [m_nativeMenu itemWithTag:reinterpret_cast<NSInteger>(menu)];
}
void QCocoaMenuBar::handleReparent(QWindow *newParentWindow)
{
#ifdef QT_COCOA_ENABLE_MENU_DEBUG
qDebug() << "QCocoaMenuBar" << this << "handleReparent" << newParentWindow;
#endif
if (!m_window.isNull())
m_window->setMenubar(nullptr);
if (!newParentWindow) {
m_window.clear();
} else {
newParentWindow->create();
m_window = static_cast<QCocoaWindow*>(newParentWindow->handle());
m_window->setMenubar(this);
}
updateMenuBarImmediately();
}
QWindow *QCocoaMenuBar::parentWindow() const
{
return m_window ? m_window->window() : nullptr;
}
QCocoaWindow *QCocoaMenuBar::findWindowForMenubar()
{
if (qApp->focusWindow())
return static_cast<QCocoaWindow*>(qApp->focusWindow()->handle());
return nullptr;
}
QCocoaMenuBar *QCocoaMenuBar::findGlobalMenubar()
{
for (auto *menubar : qAsConst(static_menubars)) {
if (menubar->m_window.isNull())
return menubar;
}
return nullptr;
}
void QCocoaMenuBar::updateMenuBarImmediately()
{
QMacAutoReleasePool pool;
QCocoaMenuBar *mb = findGlobalMenubar();
QCocoaWindow *cw = findWindowForMenubar();
QWindow *win = cw ? cw->window() : nullptr;
if (win && (win->flags() & Qt::Popup) == Qt::Popup) {
// context menus, comboboxes, etc. don't need to update the menubar,
// but if an application has only Qt::Tool window(s) on start,
// we still have to update the menubar.
if ((win->flags() & Qt::WindowType_Mask) != Qt::Tool)
return;
NSApplication *app = [NSApplication sharedApplication];
if (![app.delegate isKindOfClass:[QCocoaApplicationDelegate class]])
return;
// We apply this logic _only_ during the startup.
QCocoaApplicationDelegate *appDelegate = app.delegate;
if (!appDelegate.inLaunch)
return;
}
if (cw && cw->menubar())
mb = cw->menubar();
if (!mb)
return;
#ifdef QT_COCOA_ENABLE_MENU_DEBUG
qDebug() << "QCocoaMenuBar" << "updateMenuBarImmediately" << cw;
#endif
bool disableForModal = mb->shouldDisable(cw);
for (auto menu : qAsConst(mb->m_menus)) {
if (!menu)
continue;
NSMenuItem *item = mb->nativeItemForMenu(menu);
menu->setAttachedItem(item);
menu->setMenuParent(mb);
// force a sync?
mb->syncMenu_helper(menu, true /*menubarUpdate*/);
menu->propagateEnabledState(!disableForModal);
}
QCocoaMenuLoader *loader = [QCocoaMenuLoader sharedMenuLoader];
[loader ensureAppMenuInMenu:mb->nsMenu()];
NSMutableSet *mergedItems = [[NSMutableSet setWithCapacity:mb->merged().count()] retain];
for (auto mergedItem : mb->merged()) {
[mergedItems addObject:mergedItem->nsItem()];
mergedItem->syncMerged();
}
// hide+disable all mergeable items we're not currently using
for (NSMenuItem *mergeable in [loader mergeable]) {
if (![mergedItems containsObject:mergeable]) {
mergeable.hidden = YES;
mergeable.enabled = NO;
}
}
[mergedItems release];
[NSApp setMainMenu:mb->nsMenu()];
[loader qtTranslateApplicationMenu];
}
QList<QCocoaMenuItem*> QCocoaMenuBar::merged() const
{
QList<QCocoaMenuItem*> r;
for (auto menu : qAsConst(m_menus))
r.append(menu->merged());
return r;
}
bool QCocoaMenuBar::shouldDisable(QCocoaWindow *active) const
{
if (active && (active->window()->modality() == Qt::NonModal))
return false;
if (m_window == active) {
// modal window owns us, we should be enabled!
return false;
}
QWindowList topWindows(qApp->topLevelWindows());
// When there is an application modal window on screen, the entries of
// the menubar should be disabled. The exception in Qt is that if the
// modal window is the only window on screen, then we enable the menu bar.
for (auto *window : qAsConst(topWindows)) {
if (window->isVisible() && window->modality() == Qt::ApplicationModal) {
// check for other visible windows
for (auto *other : qAsConst(topWindows)) {
if ((window != other) && (other->isVisible())) {
// INVARIANT: we found another visible window
// on screen other than our modalWidget. We therefore
// disable the menu bar to follow normal modality logic:
return true;
}
}
// INVARIANT: We have only one window on screen that happends
// to be application modal. We choose to enable the menu bar
// in that case to e.g. enable the quit menu item.
return false;
}
}
return true;
}
QPlatformMenu *QCocoaMenuBar::menuForTag(quintptr tag) const
{
for (auto menu : qAsConst(m_menus))
if (menu->tag() == tag)
return menu;
return nullptr;
}
NSMenuItem *QCocoaMenuBar::itemForRole(QPlatformMenuItem::MenuRole role)
{
for (auto menu : qAsConst(m_menus))
for (auto *item : menu->items())
if (item->effectiveRole() == role)
return item->nsItem();
return nil;
}
QCocoaWindow *QCocoaMenuBar::cocoaWindow() const
{
return m_window.data();
}
QT_END_NAMESPACE
#include "moc_qcocoamenubar.cpp"