blob: c35cf6d799259df974ead31943d3a27593457bdb [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 <qpa/qplatformtheme.h>
#include "qcocoamenuitem.h"
#include "qcocoansmenu.h"
#include "qcocoamenu.h"
#include "qcocoamenubar.h"
#include "messages.h"
#include "qcocoahelpers.h"
#include "qt_mac_p.h"
#include "qcocoaapplication.h" // for custom application category
#include "qcocoamenuloader.h"
#include <QtGui/private/qcoregraphics_p.h>
#include <QtCore/QDebug>
#include <QtCore/QRegExp>
QT_BEGIN_NAMESPACE
static quint32 constructModifierMask(quint32 accel_key)
{
quint32 ret = 0;
const bool dontSwap = qApp->testAttribute(Qt::AA_MacDontSwapCtrlAndMeta);
if ((accel_key & Qt::CTRL) == Qt::CTRL)
ret |= (dontSwap ? NSEventModifierFlagControl : NSEventModifierFlagCommand);
if ((accel_key & Qt::META) == Qt::META)
ret |= (dontSwap ? NSEventModifierFlagCommand : NSEventModifierFlagControl);
if ((accel_key & Qt::ALT) == Qt::ALT)
ret |= NSEventModifierFlagOption;
if ((accel_key & Qt::SHIFT) == Qt::SHIFT)
ret |= NSEventModifierFlagShift;
return ret;
}
#ifndef QT_NO_SHORTCUT
// return an autoreleased string given a QKeySequence (currently only looks at the first one).
NSString *keySequenceToKeyEqivalent(const QKeySequence &accel)
{
quint32 accel_key = (accel[0] & ~(Qt::MODIFIER_MASK | Qt::UNICODE_ACCEL));
QChar cocoa_key = qt_mac_qtKey2CocoaKey(Qt::Key(accel_key));
if (cocoa_key.isNull())
cocoa_key = QChar(accel_key).toLower().unicode();
// Similar to qt_mac_removePrivateUnicode change the delete key so the symbol is correctly seen in native menubar
if (cocoa_key.unicode() == NSDeleteFunctionKey)
cocoa_key = NSDeleteCharacter;
return [NSString stringWithCharacters:&cocoa_key.unicode() length:1];
}
// return the cocoa modifier mask for the QKeySequence (currently only looks at the first one).
NSUInteger keySequenceModifierMask(const QKeySequence &accel)
{
return constructModifierMask(accel[0]);
}
#endif
QCocoaMenuItem::QCocoaMenuItem() :
m_native(nil),
m_itemView(nil),
m_menu(nullptr),
m_role(NoRole),
m_iconSize(16),
m_textSynced(false),
m_isVisible(true),
m_enabled(true),
m_parentEnabled(true),
m_isSeparator(false),
m_checked(false),
m_merged(false)
{
}
QCocoaMenuItem::~QCocoaMenuItem()
{
QMacAutoReleasePool pool;
if (m_menu && m_menu->menuParent() == this)
m_menu->setMenuParent(nullptr);
if (m_merged) {
m_native.hidden = YES;
} else {
if (m_menu && m_menu->attachedItem() == m_native)
m_menu->setAttachedItem(nil);
[m_native release];
}
[m_itemView release];
}
void QCocoaMenuItem::setText(const QString &text)
{
m_text = text;
}
void QCocoaMenuItem::setIcon(const QIcon &icon)
{
m_icon = icon;
}
void QCocoaMenuItem::setMenu(QPlatformMenu *menu)
{
if (menu == m_menu)
return;
bool setAttached = false;
if ([m_native.menu isKindOfClass:[QCocoaNSMenu class]]) {
auto parentMenu = static_cast<QCocoaNSMenu *>(m_native.menu);
setAttached = parentMenu.platformMenu && parentMenu.platformMenu->isAboutToShow();
}
if (m_menu && m_menu->menuParent() == this) {
m_menu->setMenuParent(nullptr);
// Free the menu from its parent's influence
m_menu->propagateEnabledState(true);
if (m_native && m_menu->attachedItem() == m_native)
m_menu->setAttachedItem(nil);
}
QMacAutoReleasePool pool;
m_menu = static_cast<QCocoaMenu *>(menu);
if (m_menu) {
m_menu->setMenuParent(this);
m_menu->propagateEnabledState(isEnabled());
if (setAttached)
m_menu->setAttachedItem(m_native);
} else {
// we previously had a menu, but no longer
// clear out our item so the nexy sync() call builds a new one
[m_native release];
m_native = nil;
}
}
void QCocoaMenuItem::setVisible(bool isVisible)
{
m_isVisible = isVisible;
}
void QCocoaMenuItem::setIsSeparator(bool isSeparator)
{
m_isSeparator = isSeparator;
}
void QCocoaMenuItem::setFont(const QFont &font)
{
Q_UNUSED(font)
}
void QCocoaMenuItem::setRole(MenuRole role)
{
if (role != m_role)
m_textSynced = false; // Changing role deserves a second chance.
m_role = role;
}
#ifndef QT_NO_SHORTCUT
void QCocoaMenuItem::setShortcut(const QKeySequence& shortcut)
{
m_shortcut = shortcut;
}
#endif
void QCocoaMenuItem::setChecked(bool isChecked)
{
m_checked = isChecked;
}
void QCocoaMenuItem::setEnabled(bool enabled)
{
if (m_enabled != enabled) {
m_enabled = enabled;
if (m_menu)
m_menu->propagateEnabledState(isEnabled());
}
}
void QCocoaMenuItem::setNativeContents(WId item)
{
NSView *itemView = (NSView *)item;
if (m_itemView == itemView)
return;
[m_itemView release];
m_itemView = [itemView retain];
m_itemView.autoresizesSubviews = YES;
m_itemView.autoresizingMask = NSViewWidthSizable;
m_itemView.hidden = NO;
m_itemView.needsDisplay = YES;
}
NSMenuItem *QCocoaMenuItem::sync()
{
if (m_isSeparator != m_native.separatorItem) {
[m_native release];
if (m_isSeparator)
m_native = [[QCocoaNSMenuItem separatorItemWithPlatformMenuItem:this] retain];
else
m_native = nil;
}
if ((m_role != NoRole && !m_textSynced) || m_merged) {
QCocoaMenuBar *menubar = nullptr;
if (m_role == TextHeuristicRole) {
// Recognized menu roles are only found in the first menus below the menubar
QObject *p = menuParent();
int depth = 1;
while (depth < 3 && p && !(menubar = qobject_cast<QCocoaMenuBar *>(p))) {
++depth;
QCocoaMenuObject *menuObject = dynamic_cast<QCocoaMenuObject *>(p);
Q_ASSERT(menuObject);
p = menuObject->menuParent();
}
if (menubar && depth < 3)
m_detectedRole = detectMenuRole(m_text);
else
m_detectedRole = NoRole;
}
QCocoaMenuLoader *loader = [QCocoaMenuLoader sharedMenuLoader];
NSMenuItem *mergeItem = nil;
const auto role = effectiveRole();
switch (role) {
case AboutRole:
mergeItem = [loader aboutMenuItem];
break;
case AboutQtRole:
mergeItem = [loader aboutQtMenuItem];
break;
case PreferencesRole:
mergeItem = [loader preferencesMenuItem];
break;
case ApplicationSpecificRole:
mergeItem = [loader appSpecificMenuItem:this];
break;
case QuitRole:
mergeItem = [loader quitMenuItem];
break;
case CutRole:
case CopyRole:
case PasteRole:
case SelectAllRole:
if (menubar)
mergeItem = menubar->itemForRole(role);
break;
case NoRole:
// The heuristic couldn't resolve the menu role
m_textSynced = false;
break;
default:
if (!m_text.isEmpty())
m_textSynced = true;
break;
}
if (mergeItem) {
m_textSynced = true;
m_merged = true;
[mergeItem retain];
[m_native release];
m_native = mergeItem;
if (auto *nativeItem = qt_objc_cast<QCocoaNSMenuItem *>(m_native))
nativeItem.platformMenuItem = this;
} else if (m_merged) {
// was previously merged, but no longer
[m_native release];
m_native = nil; // create item below
m_merged = false;
}
} else if (!m_text.isEmpty()) {
m_textSynced = true; // NoRole, and that was set explicitly. So, nothing to do anymore.
}
if (!m_native) {
m_native = [[QCocoaNSMenuItem alloc] initWithPlatformMenuItem:this];
m_native.title = m_text.toNSString();
}
resolveTargetAction();
m_native.hidden = !m_isVisible;
m_native.view = m_itemView;
QString text = mergeText();
#ifndef QT_NO_SHORTCUT
QKeySequence accel = mergeAccel();
// Show multiple key sequences as part of the menu text.
if (accel.count() > 1)
text += QLatin1String(" (") + accel.toString(QKeySequence::NativeText) + QLatin1String(")");
#endif
m_native.title = QPlatformTheme::removeMnemonics(text).toNSString();
#ifndef QT_NO_SHORTCUT
if (accel.count() == 1) {
m_native.keyEquivalent = keySequenceToKeyEqivalent(accel);
m_native.keyEquivalentModifierMask = keySequenceModifierMask(accel);
} else
#endif
{
m_native.keyEquivalent = @"";
m_native.keyEquivalentModifierMask = NSEventModifierFlagCommand;
}
NSImage *img = nil;
if (!m_icon.isNull()) {
img = qt_mac_create_nsimage(m_icon, m_iconSize);
img.size = CGSizeMake(m_iconSize, m_iconSize);
}
m_native.image = img;
[img release];
m_native.state = m_checked ? NSOnState : NSOffState;
return m_native;
}
QString QCocoaMenuItem::mergeText()
{
QCocoaMenuLoader *loader = [QCocoaMenuLoader sharedMenuLoader];
if (m_native == [loader aboutMenuItem]) {
return qt_mac_applicationmenu_string(AboutAppMenuItem).arg(qt_mac_applicationName());
} else if (m_native== [loader aboutQtMenuItem]) {
if (m_text == QString("About Qt"))
return msgAboutQt();
else
return m_text;
} else if (m_native == [loader preferencesMenuItem]) {
return qt_mac_applicationmenu_string(PreferencesAppMenuItem);
} else if (m_native == [loader quitMenuItem]) {
return qt_mac_applicationmenu_string(QuitAppMenuItem).arg(qt_mac_applicationName());
} else if (m_text.contains('\t')) {
return m_text.left(m_text.indexOf('\t'));
}
return m_text;
}
#ifndef QT_NO_SHORTCUT
QKeySequence QCocoaMenuItem::mergeAccel()
{
QCocoaMenuLoader *loader = [QCocoaMenuLoader sharedMenuLoader];
if (m_native == [loader preferencesMenuItem])
return QKeySequence(QKeySequence::Preferences);
else if (m_native == [loader quitMenuItem])
return QKeySequence(QKeySequence::Quit);
else if (m_text.contains('\t'))
return QKeySequence(m_text.mid(m_text.indexOf('\t') + 1), QKeySequence::NativeText);
return m_shortcut;
}
#endif
void QCocoaMenuItem::syncMerged()
{
if (!m_merged) {
qWarning("Trying to sync a non-merged item");
return;
}
m_native.hidden = !m_isVisible;
}
void QCocoaMenuItem::setParentEnabled(bool enabled)
{
if (m_parentEnabled != enabled) {
m_parentEnabled = enabled;
if (m_menu)
m_menu->propagateEnabledState(isEnabled());
}
}
QPlatformMenuItem::MenuRole QCocoaMenuItem::effectiveRole() const
{
if (m_role > TextHeuristicRole)
return m_role;
else
return m_detectedRole;
}
void QCocoaMenuItem::setIconSize(int size)
{
m_iconSize = size;
}
void QCocoaMenuItem::resolveTargetAction()
{
if (m_native.separatorItem)
return;
// Some items created by QCocoaMenuLoader are not
// instances of QCocoaNSMenuItem and have their
// target/action set as Interface Builder would.
if (![m_native isMemberOfClass:[QCocoaNSMenuItem class]])
return;
// Use the responder chain and ensure native modal dialogs
// continue receiving cut/copy/paste/etc. key equivalents.
SEL roleAction;
switch (effectiveRole()) {
case CutRole:
roleAction = @selector(cut:);
break;
case CopyRole:
roleAction = @selector(copy:);
break;
case PasteRole:
roleAction = @selector(paste:);
break;
case SelectAllRole:
roleAction = @selector(selectAll:);
break;
default:
roleAction = @selector(qt_itemFired:);
}
m_native.action = roleAction;
m_native.target = nil;
}
QT_END_NAMESPACE