From b8246f08e49eb672974fd3d3d972a5ff13c1524d Mon Sep 17 00:00:00 2001 From: James Turner Date: Fri, 4 May 2012 14:16:05 +0100 Subject: [PATCH] Cocoa implementation of QPA menu interface. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the QPA platform menu interface for Cocoa, including native menubar support and merging with the predefined menus created from the bundled .nib. Cleanup code previously used to maintain the menus, and add a manual test of the menus code. Change-Id: Ia99267ddb6485e18e05c540eb32c5aee6cbb85db Reviewed-by: Morten Johan Sørvig --- src/plugins/platforms/cocoa/cocoa.pro | 10 +- .../platforms/cocoa/qcocoaapplication.mm | 11 - .../cocoa/qcocoaapplicationdelegate.mm | 2 +- src/plugins/platforms/cocoa/qcocoahelpers.h | 5 + src/plugins/platforms/cocoa/qcocoahelpers.mm | 14 + src/plugins/platforms/cocoa/qcocoamenu.h | 93 +++-- src/plugins/platforms/cocoa/qcocoamenu.mm | 393 +++++++++--------- src/plugins/platforms/cocoa/qcocoamenubar.h | 82 ++++ src/plugins/platforms/cocoa/qcocoamenubar.mm | 254 +++++++++++ src/plugins/platforms/cocoa/qcocoamenuitem.h | 117 ++++++ src/plugins/platforms/cocoa/qcocoamenuitem.mm | 358 ++++++++++++++++ .../platforms/cocoa/qcocoamenuloader.h | 5 +- .../platforms/cocoa/qcocoamenuloader.mm | 80 ++-- .../platforms/cocoa/qcocoanativeinterface.h | 5 + .../platforms/cocoa/qcocoanativeinterface.mm | 11 + src/plugins/platforms/cocoa/qcocoatheme.h | 4 + src/plugins/platforms/cocoa/qcocoatheme.mm | 32 +- src/plugins/platforms/cocoa/qcocoawindow.h | 5 + src/plugins/platforms/cocoa/qcocoawindow.mm | 14 + .../platforms/cocoa/qt_menu.nib/classes.nib | 2 +- tests/manual/cocoa/menus/main.cpp | 201 +++++++++ tests/manual/cocoa/menus/menus.pro | 5 + 22 files changed, 1435 insertions(+), 268 deletions(-) create mode 100644 src/plugins/platforms/cocoa/qcocoamenubar.h create mode 100644 src/plugins/platforms/cocoa/qcocoamenubar.mm create mode 100644 src/plugins/platforms/cocoa/qcocoamenuitem.h create mode 100644 src/plugins/platforms/cocoa/qcocoamenuitem.mm create mode 100644 tests/manual/cocoa/menus/main.cpp create mode 100644 tests/manual/cocoa/menus/menus.pro diff --git a/src/plugins/platforms/cocoa/cocoa.pro b/src/plugins/platforms/cocoa/cocoa.pro index 313b666793b..467ce96e638 100644 --- a/src/plugins/platforms/cocoa/cocoa.pro +++ b/src/plugins/platforms/cocoa/cocoa.pro @@ -14,9 +14,12 @@ OBJECTIVE_SOURCES += main.mm \ qcocoaglcontext.mm \ qcocoanativeinterface.mm \ qcocoaeventdispatcher.mm \ - qcocoamenuloader.mm \ qcocoaapplicationdelegate.mm \ qcocoaapplication.mm \ + qcocoamenu.mm \ + qcocoamenuitem.mm \ + qcocoamenubar.mm \ + qcocoamenuloader.mm \ qcocoahelpers.mm \ qmultitouch_mac.mm \ qcocoaaccessibilityelement.mm \ @@ -46,9 +49,12 @@ HEADERS += qcocoaintegration.h \ qcocoaglcontext.h \ qcocoanativeinterface.h \ qcocoaeventdispatcher.h \ - qcocoamenuloader.h \ qcocoaapplicationdelegate.h \ qcocoaapplication.h \ + qcocoamenu.h \ + qcocoamenuitem.h \ + qcocoamenubar.h \ + qcocoamenuloader.h \ qcocoahelpers.h \ qmultitouch_mac_p.h \ qcocoaaccessibilityelement.h \ diff --git a/src/plugins/platforms/cocoa/qcocoaapplication.mm b/src/plugins/platforms/cocoa/qcocoaapplication.mm index 2cecb296f48..90f09aa620c 100644 --- a/src/plugins/platforms/cocoa/qcocoaapplication.mm +++ b/src/plugins/platforms/cocoa/qcocoaapplication.mm @@ -189,17 +189,6 @@ QT_USE_NAMESPACE [super sendEvent:event]; } -- (void)qtDispatcherToQAction:(id)sender -{ - // Forward actions sent from the menu bar (e.g. quit) to the menu loader. - // Having this method here means that we are the last stop in the responder - // chain, and that we are able to handle menu actions even when no window is - // visible on screen. Note: If Qt is used as a plugin, Qt will not use a - // native menu bar. Hence, we will also not need to do any redirection etc. as - // we do with sendEvent. - [[NSApp QT_MANGLE_NAMESPACE(qt_qcocoamenuLoader)] qtDispatcherToQAction:sender]; -} - @end QT_BEGIN_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm b/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm index ec086fe62cb..3855563bab2 100644 --- a/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm +++ b/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm @@ -355,7 +355,7 @@ static void cleanupCocoaApplicationDelegate() - (void)qtDispatcherToQAction:(id)sender { Q_UNUSED(sender); - [qtMenuLoader qtDispatcherToQAction:sender]; + [qtMenuLoader qtDispatcherToQPAMenuItem:sender]; } @end diff --git a/src/plugins/platforms/cocoa/qcocoahelpers.h b/src/plugins/platforms/cocoa/qcocoahelpers.h index 5113aca3f71..f8032603e51 100644 --- a/src/plugins/platforms/cocoa/qcocoahelpers.h +++ b/src/plugins/platforms/cocoa/qcocoahelpers.h @@ -115,6 +115,11 @@ inline NSPoint qt_mac_flipPoint(const QPointF &p) NSRect qt_mac_flipRect(const QRect &rect, QWindow *window); +// strip out '&' characters, and convert "&&" to a single '&', in menu +// text - since menu text is sometimes decorated with these for Windows +// accelerators. +QString qt_mac_removeAmpersandEscapes(QString s); + QT_END_NAMESPACE #endif //QCOCOAHELPERS_H diff --git a/src/plugins/platforms/cocoa/qcocoahelpers.mm b/src/plugins/platforms/cocoa/qcocoahelpers.mm index 8f8d7b84a67..c0b6f3abb68 100644 --- a/src/plugins/platforms/cocoa/qcocoahelpers.mm +++ b/src/plugins/platforms/cocoa/qcocoahelpers.mm @@ -581,4 +581,18 @@ CGFloat qt_mac_get_scalefactor() return [[NSScreen mainScreen] userSpaceScaleFactor]; } +QString qt_mac_removeAmpersandEscapes(QString s) +{ + int i = 0; + while (i < s.size()) { + ++i; + if (s.at(i-1) != QLatin1Char('&')) + continue; + if (i < s.size() && s.at(i) == QLatin1Char('&')) + ++i; + s.remove(i-1,1); + } + return s.trimmed(); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoamenu.h b/src/plugins/platforms/cocoa/qcocoamenu.h index bf8bba1ddf2..479d4b53c22 100644 --- a/src/plugins/platforms/cocoa/qcocoamenu.h +++ b/src/plugins/platforms/cocoa/qcocoamenu.h @@ -1,9 +1,10 @@ /**************************************************************************** ** ** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Copyright (C) 2012 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author James Turner ** Contact: http://www.qt-project.org/ ** -** This file is part of the QtGui module of the Qt Toolkit. +** This file is part of the plugins of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** GNU Lesser General Public License Usage @@ -39,42 +40,66 @@ ** ****************************************************************************/ -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists purely as an -// implementation detail. This header file may change from version to -// version without notice, or even be removed. -// -// We mean it. -// +#ifndef QCOCOAMENU_H +#define QCOCOAMENU_H -#include "qmacdefines_mac.h" -#import +#include +#include +#include "qcocoamenuitem.h" -QT_FORWARD_DECLARE_CLASS(QMenu) -QT_FORWARD_DECLARE_CLASS(QAction) +@class NSMenuItem; +@class NSMenu; +@class NSObject; -#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_5 +QT_BEGIN_HEADER -@protocol NSMenuDelegate -- (void)menu:(NSMenu*)menu willHighlightItem:(NSMenuItem*)item; -- (void)menuWillOpen:(NSMenu*)menu; -- (void)menuDidClose:(NSMenu*)menu; -- (BOOL)hasShortcut:(NSMenu *)menu forKey:(NSString *)key forModifiers:(NSUInteger)modifier - whichItem:(NSMenuItem**)outItem; -@end +QT_BEGIN_NAMESPACE + +class QCocoaMenu : public QPlatformMenu +{ +public: + QCocoaMenu(); + + inline virtual void setTag(quintptr tag) + { m_tag = tag; } + inline virtual quintptr tag() const + { return m_tag; } + + void insertMenuItem(QPlatformMenuItem *menuItem, QPlatformMenuItem *before); + void removeMenuItem(QPlatformMenuItem *menuItem); + void syncMenuItem(QPlatformMenuItem *menuItem); + void setEnabled(bool enabled); + void syncSeparatorsCollapsible(bool enable); + + void syncModalState(bool modal); + + virtual void setText(const QString &text); + + void setParentItem(QCocoaMenuItem* item); + + inline NSMenu *nsMenu() const + { return m_nativeMenu; } + inline NSMenuItem *nsMenuItem() const + { return m_nativeItem; } + + virtual QPlatformMenuItem *menuItemAt(int position) const; + virtual QPlatformMenuItem *menuItemForTag(quintptr tag) const; + + QList merged() const; +private: + QCocoaMenuItem *itemOrNull(int index) const; + void insertNative(QCocoaMenuItem *item, QCocoaMenuItem *beforeItem); + + QList m_menuItems; + NSMenu *m_nativeMenu; + NSMenuItem *m_nativeItem; + NSObject *m_delegate; + bool m_enabled; + quintptr m_tag; +}; + +QT_END_NAMESPACE + +QT_END_HEADER #endif - -@interface QT_MANGLE_NAMESPACE(QNativeCocoaMenu) : NSMenu -{ - QMenu *qmenu; - QAction *previousAction; -} -- (id)initWithQMenu:(QMenu*)menu; -- (BOOL)menuHasKeyEquivalent:(NSMenu *)menu forEvent:(NSEvent *)event target:(id *)target action:(SEL *)action; -- (NSInteger)indexOfItemWithTarget:(id)anObject andAction:(SEL)actionSelector; -@end - diff --git a/src/plugins/platforms/cocoa/qcocoamenu.mm b/src/plugins/platforms/cocoa/qcocoamenu.mm index 00bbf49ccaf..9e466deb9ac 100644 --- a/src/plugins/platforms/cocoa/qcocoamenu.mm +++ b/src/plugins/platforms/cocoa/qcocoamenu.mm @@ -1,9 +1,9 @@ /**************************************************************************** ** -** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Copyright (C) 2012 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author James Turner ** Contact: http://www.qt-project.org/ ** -** This file is part of the QtGui module of the Qt Toolkit. +** This file is part of the plugins of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** GNU Lesser General Public License Usage @@ -39,229 +39,244 @@ ** ****************************************************************************/ -#include "qapplication.h" -#include "qvarlengtharray.h" -#import "qcocoamenu.h" -#import "qcocoamenuloader.h" -#import "qcocoaapplication.h" +#include "qcocoamenu.h" + #include "qcocoahelpers.h" -#include -#include +#include "qcocoaautoreleasepool.h" -QT_FORWARD_DECLARE_CLASS(QAction) -QT_FORWARD_DECLARE_CLASS(QWidget) -QT_FORWARD_DECLARE_CLASS(QApplication) -QT_FORWARD_DECLARE_CLASS(QCoreApplication) -QT_FORWARD_DECLARE_CLASS(QApplicationPrivate) -QT_FORWARD_DECLARE_CLASS(QKeyEvent) -QT_FORWARD_DECLARE_CLASS(QEvent) +@interface QT_MANGLE_NAMESPACE(QCocoaMenuDelegate) : NSObject { + QCocoaMenu *m_menu; +} -QT_BEGIN_NAMESPACE -extern void qt_mac_menu_collapseSeparators(NSMenu *menu, bool collapse); -void qt_mac_clear_status_text(QAction *action); -extern void qt_mac_emit_menuSignals(QMenu *menu, bool show); -extern void qt_mac_menu_emit_hovered(QMenu *menu, QAction *action); -QT_END_NAMESPACE +- (id) initWithMenu:(QCocoaMenu*) m; -QT_USE_NAMESPACE +@end -@implementation QT_MANGLE_NAMESPACE(QNativeCocoaMenu) +@implementation QT_MANGLE_NAMESPACE(QCocoaMenuDelegate) -- (id)initWithQMenu:(QMenu*)menu +- (id) initWithMenu:(QCocoaMenu*) m { - self = [super init]; - if (self) { - qmenu = menu; - previousAction = 0; - [self setAutoenablesItems:NO]; - [self setDelegate:self]; - } + if ((self = [super init])) + m_menu = m; + return self; } -- (void)menu:(NSMenu*)menu willHighlightItem:(NSMenuItem*)item +- (void) menuWillOpen:(NSMenu*)m { - Q_UNUSED(menu); - - if (!item) { - if (previousAction) { - qt_mac_clear_status_text(previousAction); - previousAction = 0; - } - return; - } - - if (QAction *action = reinterpret_cast([item tag])) { - QMenu *qtmenu = static_cast(menu)->qmenu; - previousAction = action; - action->activate(QAction::Hover); - qt_mac_menu_emit_hovered(qtmenu, action); - action->showStatusText(0); // 0 widget -> action's parent - } + Q_UNUSED(m); + emit m_menu->aboutToShow(); } -- (void)menuWillOpen:(NSMenu*)menu +- (void) menuDidClose:(NSMenu*)m { - while (QWidget *popup - = QApplication::activePopupWidget()) - popup->close(); - QMenu *qtmenu = static_cast(menu)->qmenu; - qt_mac_emit_menuSignals(qtmenu, true); - qt_mac_menu_collapseSeparators(menu, qtmenu->separatorsCollapsible()); + Q_UNUSED(m); + // wrong, but it's the best we can do + emit m_menu->aboutToHide(); } -- (void)menuDidClose:(NSMenu*)menu +- (void) itemFired:(NSMenuItem*) item { - qt_mac_emit_menuSignals(((QT_MANGLE_NAMESPACE(QNativeCocoaMenu) *)menu)->qmenu, false); - if (previousAction) { - qt_mac_clear_status_text(previousAction); - previousAction = 0; - } + QCocoaMenuItem *cocoaItem = reinterpret_cast([item tag]); + cocoaItem->activated(); } -- (BOOL)hasShortcut:(NSMenu *)menu forKey:(NSString *)key forModifiers:(NSUInteger)modifier - whichItem:(NSMenuItem**)outItem +- (BOOL)validateMenuItem:(NSMenuItem*)menuItem { - for (NSMenuItem *item in [menu itemArray]) { - if (![item isEnabled] || [item isHidden] || [item isSeparatorItem]) - continue; - if ([item hasSubmenu]) { - if ([self hasShortcut:[item submenu] - forKey:key - forModifiers:modifier whichItem:outItem]) { - if (outItem) - *outItem = item; - return YES; - } - } - NSString *menuKey = [item keyEquivalent]; - if (menuKey && NSOrderedSame == [menuKey compare:key] - && (modifier == [item keyEquivalentModifierMask])) { - if (outItem) - *outItem = item; - return YES; - } - } - if (outItem) - *outItem = 0; - return NO; -} + if (![menuItem tag]) + return YES; -NSString *qt_mac_removePrivateUnicode(NSString* string) -{ - int len = [string length]; - if (len) { - QVarLengthArray characters(len); - bool changed = false; - for (int i = 0; i([whichItem tag]); - } - if (qApp->activePopupWidget()) - widget = (qApp->activePopupWidget()->focusWidget() ? - qApp->activePopupWidget()->focusWidget() : qApp->activePopupWidget()); - else if (QApplicationPrivate::focus_widget) - widget = QApplicationPrivate::focus_widget; - // If we could not find any receivers, pass it to the active window - if (!widget) - widget = qApp->activeWindow(); - if (qaction && widget) { - int key = qaction->shortcut(); - QKeyEvent accel_ev(QEvent::ShortcutOverride, (key & (~Qt::KeyboardModifierMask)), - Qt::KeyboardModifiers(key & Qt::KeyboardModifierMask)); - accel_ev.ignore(); - -// ### qt_sendSpontaneousEvent(widget, &accel_ev); - - if (accel_ev.isAccepted()) { - qWarning("Unimplemented: qt_dispatchKeyEvent"); -#if 0 - qt_dispatchKeyEvent(event, widget); -#endif - *target = nil; - *action = nil; - return YES; - } - } - } - return NO; -} - -- (NSInteger)indexOfItemWithTarget:(id)anObject andAction:(SEL)actionSelector -{ - NSInteger index = [super indexOfItemWithTarget:anObject andAction:actionSelector]; - static SEL selForOFCP = NSSelectorFromString(@"orderFrontCharacterPalette:"); - if (index == -1 && selForOFCP == actionSelector) { - // Check if the 'orderFrontCharacterPalette' SEL exists for QNativeCocoaMenuLoader object - QT_MANGLE_NAMESPACE(QCocoaMenuLoader) *loader = [NSApp QT_MANGLE_NAMESPACE(qt_qcocoamenuLoader)]; - return [super indexOfItemWithTarget:loader andAction:actionSelector]; - } - return index; + QCocoaMenuItem* cocoaItem = reinterpret_cast([menuItem tag]); + return cocoaItem->isEnabled(); } @end -QT_BEGIN_NAMESPACE -extern int qt_mac_menus_open_count; // qmenu_mac.mm - -void qt_mac_emit_menuSignals(QMenu *menu, bool show) +QCocoaMenu::QCocoaMenu() : + m_enabled(true), + m_tag(0) { - if (!menu) - return; - int delta; - if (show) { - emit menu->aboutToShow(); - delta = 1; + m_delegate = [[QCocoaMenuDelegate alloc] initWithMenu:this]; + m_nativeItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; + m_nativeMenu = [[NSMenu alloc] initWithTitle:@"Untitled"]; + [m_nativeMenu setAutoenablesItems:YES]; + m_nativeMenu.delegate = (QCocoaMenuDelegate *) m_delegate; + [m_nativeItem setSubmenu:m_nativeMenu]; +} + +void QCocoaMenu::setText(const QString &text) +{ + QString stripped = qt_mac_removeAmpersandEscapes(text); + [m_nativeMenu setTitle:QCFString::toNSString(stripped)]; + [m_nativeItem setTitle:QCFString::toNSString(stripped)]; +} + +void QCocoaMenu::insertMenuItem(QPlatformMenuItem *menuItem, QPlatformMenuItem *before) +{ + QCocoaMenuItem *cocoaItem = static_cast(menuItem); + QCocoaMenuItem *beforeItem = static_cast(before); + + cocoaItem->sync(); + if (beforeItem) { + int index = m_menuItems.indexOf(beforeItem); + // if a before item is supplied, it should be in the menu + Q_ASSERT(index >= 0); + m_menuItems.insert(index, cocoaItem); } else { - emit menu->aboutToHide(); - delta = -1; + m_menuItems.append(cocoaItem); } - qt_mac_menus_open_count += delta; + + insertNative(cocoaItem, beforeItem); } -void qt_mac_clear_status_text(QAction *action) +void QCocoaMenu::insertNative(QCocoaMenuItem *item, QCocoaMenuItem *beforeItem) { - action->d_func()->showStatusText(0, QString()); + [item->nsItem() setTarget:m_delegate]; + [item->nsItem() setAction:@selector(itemFired:)]; + + if (item->isMerged()) + return; + + // if the item we're inserting before is merged, skip along until + // we find a non-merged real item to insert ahead of. + while (beforeItem && beforeItem->isMerged()) { + beforeItem = itemOrNull(m_menuItems.indexOf(beforeItem) + 1); + } + + if (beforeItem) { + Q_ASSERT(!beforeItem->isMerged()); + NSUInteger nativeIndex = [m_nativeMenu indexOfItem:beforeItem->nsItem()]; + [m_nativeMenu insertItem: item->nsItem() atIndex: nativeIndex]; + } else { + [m_nativeMenu addItem: item->nsItem()]; + } } -void qt_mac_menu_emit_hovered(QMenu *menu, QAction *action) +void QCocoaMenu::removeMenuItem(QPlatformMenuItem *menuItem) { - emit menu->hovered(action); + QCocoaMenuItem *cocoaItem = static_cast(menuItem); + Q_ASSERT(m_menuItems.contains(cocoaItem)); + m_menuItems.removeOne(cocoaItem); + if (!cocoaItem->isMerged()) { + [m_nativeMenu removeItem: cocoaItem->nsItem()]; + } } +QCocoaMenuItem *QCocoaMenu::itemOrNull(int index) const +{ + if ((index < 0) || (index >= m_menuItems.size())) + return 0; -QT_END_NAMESPACE + return m_menuItems.at(index); +} +void QCocoaMenu::syncMenuItem(QPlatformMenuItem *menuItem) +{ + QCocoaMenuItem *cocoaItem = static_cast(menuItem); + Q_ASSERT(m_menuItems.contains(cocoaItem)); + + bool wasMerged = cocoaItem->isMerged(); + NSMenuItem *oldItem = [m_nativeMenu itemWithTag:(NSInteger) cocoaItem]; + + if (cocoaItem->sync() != oldItem) { + // native item was changed for some reason + if (!wasMerged) { + [m_nativeMenu removeItem:oldItem]; + } + + QCocoaMenuItem* beforeItem = itemOrNull(m_menuItems.indexOf(cocoaItem) + 1); + insertNative(cocoaItem, beforeItem); + } +} + +void QCocoaMenu::syncSeparatorsCollapsible(bool enable) +{ + QCocoaAutoReleasePool pool; + if (enable) { + bool previousIsSeparator = true; // setting to true kills all the separators placed at the top. + NSMenuItem *previousItem = nil; + + NSArray *itemArray = [m_nativeMenu itemArray]; + for (unsigned int i = 0; i < [itemArray count]; ++i) { + NSMenuItem *item = reinterpret_cast([itemArray objectAtIndex:i]); + if ([item isSeparatorItem]) + [item setHidden:previousIsSeparator]; + + if (![item isHidden]) { + previousItem = item; + previousIsSeparator = ([previousItem isSeparatorItem]); + } + } + + // We now need to check the final item since we don't want any separators at the end of the list. + if (previousItem && previousIsSeparator) + [previousItem setHidden:YES]; + } else { + foreach (QCocoaMenuItem *item, m_menuItems) { + if (!item->isSeparator()) + continue; + + // sync the visiblity directly + item->sync(); + } + } +} + +void QCocoaMenu::setParentItem(QCocoaMenuItem *item) +{ + Q_UNUSED(item); +} + +void QCocoaMenu::setEnabled(bool enabled) +{ + m_enabled = enabled; +} + +QPlatformMenuItem *QCocoaMenu::menuItemAt(int position) const +{ + return m_menuItems.at(position); +} + +QPlatformMenuItem *QCocoaMenu::menuItemForTag(quintptr tag) const +{ + foreach (QCocoaMenuItem *item, m_menuItems) { + if (item->tag() == tag) + return item; + } + + return 0; +} + +QList QCocoaMenu::merged() const +{ + QList result; + foreach (QCocoaMenuItem *item, m_menuItems) { + if (item->menu()) { // recurse into submenus + result.append(item->menu()->merged()); + continue; + } + + if (item->isMerged()) + result.append(item); + } + + return result; +} + +void QCocoaMenu::syncModalState(bool modal) +{ + if (!m_enabled) + modal = true; + + [m_nativeItem setEnabled:!modal]; + + foreach (QCocoaMenuItem *item, m_menuItems) { + if (item->menu()) { // recurse into submenus + item->menu()->syncModalState(modal); + continue; + } + + item->syncModalState(modal); + } +} diff --git a/src/plugins/platforms/cocoa/qcocoamenubar.h b/src/plugins/platforms/cocoa/qcocoamenubar.h new file mode 100644 index 00000000000..06e8eb6c9d9 --- /dev/null +++ b/src/plugins/platforms/cocoa/qcocoamenubar.h @@ -0,0 +1,82 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Copyright (C) 2012 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author James Turner +** Contact: http://www.qt-project.org/ +** +** This file is part of the plugins module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** GNU Lesser General Public License Usage +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this +** file. Please review the following information to ensure the GNU Lesser +** General Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU General +** Public License version 3.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of this +** file. Please review the following information to ensure the GNU General +** Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QCOCOAMENUBAR_H +#define QCOCOAMENUBAR_H + +#include +#include +#include "qcocoamenu.h" + +@class NSMenu; +class QCocoaWindow; + +class QCocoaMenuBar : public QPlatformMenuBar +{ +public: + QCocoaMenuBar(); + virtual ~QCocoaMenuBar(); + + virtual void insertMenu(QPlatformMenu *menu, QPlatformMenu* before); + virtual void removeMenu(QPlatformMenu *menu); + virtual void syncMenu(QPlatformMenuItem *menuItem); + virtual void handleReparent(QWindow *newParentWindow); + virtual QPlatformMenu *menuForTag(quintptr tag) const; + + inline NSMenu *nsMenu() const + { return m_nativeMenu; } + + static void updateMenuBarImmediately(); + + QList merged() const; +private: + static QCocoaWindow *findWindowForMenubar(); + static QCocoaMenuBar *findGlobalMenubar(); + + bool shouldDisable(QCocoaWindow *active) const; + + QList m_menus; + NSMenu *m_nativeMenu; + QCocoaWindow *m_window; +}; + +#endif diff --git a/src/plugins/platforms/cocoa/qcocoamenubar.mm b/src/plugins/platforms/cocoa/qcocoamenubar.mm new file mode 100644 index 00000000000..fc403ba504f --- /dev/null +++ b/src/plugins/platforms/cocoa/qcocoamenubar.mm @@ -0,0 +1,254 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author James Turner +** Contact: http://www.qt-project.org/ +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** GNU Lesser General Public License Usage +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this +** file. Please review the following information to ensure the GNU Lesser +** General Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU General +** Public License version 3.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of this +** file. Please review the following information to ensure the GNU General +** Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include + +#include "qcocoamenubar.h" +#include "qcocoawindow.h" +#include "qcocoamenuloader.h" +#include "qcocoaapplication.h" // for custom application category +#include "qcocoaautoreleasepool.h" + +#include +#include + +static QList static_menubars; + +static inline QT_MANGLE_NAMESPACE(QCocoaMenuLoader) *getMenuLoader() +{ + return [NSApp QT_MANGLE_NAMESPACE(qt_qcocoamenuLoader)]; +} + + +QCocoaMenuBar::QCocoaMenuBar() : + m_window(0) +{ + 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 + [m_nativeMenu release]; + static_menubars.removeOne(this); + + if (m_window) + m_window->setMenubar(0); +} + +void QCocoaMenuBar::insertMenu(QPlatformMenu *platformMenu, QPlatformMenu *before) +{ + QCocoaMenu *menu = static_cast(platformMenu); + QCocoaMenu *beforeMenu = static_cast(before); +#ifdef QT_COCOA_ENABLE_MENU_DEBUG + qDebug() << "QCocoaMenuBar" << this << "insertMenu" << menu << "before" << before; +#endif + + Q_ASSERT(!m_menus.contains(menu)); + if (beforeMenu) { + Q_ASSERT(m_menus.contains(beforeMenu)); + m_menus.insert(m_menus.indexOf(beforeMenu), menu); + NSUInteger nativeIndex = [m_nativeMenu indexOfItem:beforeMenu->nsMenuItem()]; + [m_nativeMenu insertItem: menu->nsMenuItem() atIndex: nativeIndex]; + } else { + m_menus.append(menu); + [m_nativeMenu addItem: menu->nsMenuItem()]; + } + + [m_nativeMenu setSubmenu: menu->nsMenu() forItem: menu->nsMenuItem()]; +} + +void QCocoaMenuBar::removeMenu(QPlatformMenu *platformMenu) +{ + QCocoaMenu *menu = static_cast(platformMenu); + Q_ASSERT(m_menus.contains(menu)); + m_menus.removeOne(menu); + + NSUInteger realIndex = [m_nativeMenu indexOfItem:menu->nsMenuItem()]; + [m_nativeMenu removeItemAtIndex: realIndex]; +} + +void QCocoaMenuBar::syncMenu(QPlatformMenuItem *menuItem) +{ + Q_UNUSED(menuItem); +} + +void QCocoaMenuBar::handleReparent(QWindow *newParentWindow) +{ +#ifdef QT_COCOA_ENABLE_MENU_DEBUG + qDebug() << "QCocoaMenuBar" << this << "handleReparent" << newParentWindow; +#endif + + if (m_window) + m_window->setMenubar(NULL); + + if (newParentWindow == NULL) { + m_window = NULL; + } else { + m_window = static_cast(newParentWindow->handle()); + m_window->setMenubar(this); + } + + updateMenuBarImmediately(); +} + +QCocoaWindow *QCocoaMenuBar::findWindowForMenubar() +{ + if (qApp->focusWindow()) + return static_cast(qApp->focusWindow()->handle()); + + return NULL; +} + +QCocoaMenuBar *QCocoaMenuBar::findGlobalMenubar() +{ + foreach (QCocoaMenuBar *mb, static_menubars) { + if (mb->m_window == NULL) + return mb; + } + + return NULL; +} + +void QCocoaMenuBar::updateMenuBarImmediately() +{ + QCocoaAutoReleasePool pool; + QCocoaMenuBar *mb = findGlobalMenubar(); + QCocoaWindow *cw = findWindowForMenubar(); + 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); + // force a sync? + foreach (QCocoaMenu *m, mb->m_menus) { + mb->syncMenu(m); + m->syncModalState(disableForModal); + } + + QT_MANGLE_NAMESPACE(QCocoaMenuLoader) *loader = getMenuLoader(); + [loader ensureAppMenuInMenu:mb->nsMenu()]; + + NSMutableSet *mergedItems = [[NSMutableSet setWithCapacity:0] retain]; + foreach (QCocoaMenuItem *m, mb->merged()) { + [mergedItems addObject:m->nsItem()]; + m->syncMerged(); + } + + // hide+disable all mergeable items we're not currently using + for (NSMenuItem *mergeable in [loader mergeable]) { + if (![mergedItems containsObject:mergeable]) { + [mergeable setHidden:YES]; + [mergeable setEnabled:NO]; + } + } + + [mergedItems release]; + [NSApp setMainMenu:mb->nsMenu()]; + [loader qtTranslateApplicationMenu]; +} + +QList QCocoaMenuBar::merged() const +{ + QList r; + foreach (QCocoaMenu* menu, m_menus) + r.append(menu->merged()); + + return r; +} + +bool QCocoaMenuBar::shouldDisable(QCocoaWindow *active) const +{ + if (active && (active->window()->windowModality() == 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. + foreach (QWindow *w, topWindows) { + if (w->isVisible() && w->windowModality() == Qt::ApplicationModal) { + // check for other visible windows + foreach (QWindow *other, topWindows) { + if ((w != 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 +{ + foreach (QCocoaMenu *menu, m_menus) { + if (menu->tag() == tag) + return menu; + } + + return 0; +} diff --git a/src/plugins/platforms/cocoa/qcocoamenuitem.h b/src/plugins/platforms/cocoa/qcocoamenuitem.h new file mode 100644 index 00000000000..4a063d59658 --- /dev/null +++ b/src/plugins/platforms/cocoa/qcocoamenuitem.h @@ -0,0 +1,117 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). +** Copyright (C) 2012 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author James Turner +** Contact: http://www.qt-project.org/ +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** GNU Lesser General Public License Usage +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this +** file. Please review the following information to ensure the GNU Lesser +** General Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU General +** Public License version 3.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of this +** file. Please review the following information to ensure the GNU General +** Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QCOCOAMENUITEM_H +#define QCOCOAMENUITEM_H + +#include +#include + +//#define QT_COCOA_ENABLE_MENU_DEBUG + +@class NSMenuItem; +@class NSMenu; + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +class QCocoaMenu; + +class QCocoaMenuItem : public QPlatformMenuItem +{ +public: + QCocoaMenuItem(); + virtual ~QCocoaMenuItem(); + + inline virtual void setTag(quintptr tag) + { m_tag = tag; } + inline virtual quintptr tag() const + { return m_tag; } + + void setText(const QString &text); + void setIcon(const QImage &icon); + void setMenu(QPlatformMenu *menu); + void setVisible(bool isVisible); + void setIsSeparator(bool isSeparator); + void setFont(const QFont &font); + void setRole(MenuRole role); + void setShortcut(const QKeySequence& shortcut); + void setChecked(bool isChecked); + void setEnabled(bool isEnabled); + + inline QString text() const { return m_text; } + inline NSMenuItem * nsItem() { return m_native; } + NSMenuItem *sync(); + + void syncMerged(); + void syncModalState(bool modal); + + inline bool isMerged() const { return m_merged; } + inline bool isEnabled() const { return m_enabled; } + inline bool isSeparator() const { return m_isSeparator; } + + QCocoaMenu *menu() const { return m_menu; } +private: + QString mergeText(); + QKeySequence mergeAccel(); + + NSMenuItem *m_native; + QString m_text; + QImage m_icon; + QCocoaMenu *m_menu; + bool m_isVisible; + bool m_enabled; + bool m_isSeparator; + QFont m_font; + MenuRole m_role; + QKeySequence m_shortcut; + bool m_checked; + bool m_merged; + quintptr m_tag; +}; + +QT_END_HEADER + +QT_END_NAMESPACE + +#endif diff --git a/src/plugins/platforms/cocoa/qcocoamenuitem.mm b/src/plugins/platforms/cocoa/qcocoamenuitem.mm new file mode 100644 index 00000000000..150e94ab07b --- /dev/null +++ b/src/plugins/platforms/cocoa/qcocoamenuitem.mm @@ -0,0 +1,358 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author James Turner +** Contact: http://www.qt-project.org/ +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** GNU Lesser General Public License Usage +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this +** file. Please review the following information to ensure the GNU Lesser +** General Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU General +** Public License version 3.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of this +** file. Please review the following information to ensure the GNU General +** Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qcocoamenuitem.h" + +#include "qcocoamenu.h" +#include "qcocoahelpers.h" +#include "qcocoaautoreleasepool.h" +#include "qt_mac_p.h" +#include "qcocoaapplication.h" // for custom application category +#include "qcocoamenuloader.h" + +#include + +static inline QT_MANGLE_NAMESPACE(QCocoaMenuLoader) *getMenuLoader() +{ + return [NSApp QT_MANGLE_NAMESPACE(qt_qcocoamenuLoader)]; +} + + +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 ? NSControlKeyMask : NSCommandKeyMask); + if ((accel_key & Qt::META) == Qt::META) + ret |= (dontSwap ? NSCommandKeyMask : NSControlKeyMask); + if ((accel_key & Qt::ALT) == Qt::ALT) + ret |= NSAlternateKeyMask; + if ((accel_key & Qt::SHIFT) == Qt::SHIFT) + ret |= NSShiftKeyMask; + return ret; +} + +// 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(); + 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]); +} + +QCocoaMenuItem::QCocoaMenuItem() : + m_native(NULL), + m_menu(NULL), + m_isVisible(true), + m_enabled(true), + m_isSeparator(false), + m_role(NoRole), + m_checked(false), + m_merged(false), + m_tag(0) +{ +} + +QCocoaMenuItem::~QCocoaMenuItem() +{ + if (m_merged) { + [m_native setHidden:YES]; + } + + [m_native release]; +} + +void QCocoaMenuItem::setText(const QString &text) +{ + m_text = qt_mac_removeAmpersandEscapes(text); +} + +void QCocoaMenuItem::setIcon(const QImage &icon) +{ + m_icon = icon; +} + +void QCocoaMenuItem::setMenu(QPlatformMenu *menu) +{ + if (menu == m_menu) + return; + + QCocoaAutoReleasePool pool; + m_menu = static_cast(menu); + if (m_menu) { + m_menu->setParentItem(this); + } 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) +{ + m_font = font; +} + +void QCocoaMenuItem::setRole(MenuRole role) +{ + m_role = role; +} + +void QCocoaMenuItem::setShortcut(const QKeySequence& shortcut) +{ + m_shortcut = shortcut; +} + +void QCocoaMenuItem::setChecked(bool isChecked) +{ + m_checked = isChecked; +} + +void QCocoaMenuItem::setEnabled(bool enabled) +{ + m_enabled = enabled; +} + +NSMenuItem *QCocoaMenuItem::sync() +{ + if (m_isSeparator != [m_native isSeparatorItem]) { + [m_native release]; + if (m_isSeparator) { + m_native = [[NSMenuItem separatorItem] retain]; + [m_native setTag:reinterpret_cast(this)]; + } else + m_native = nil; + } + + if (m_menu) { + if (m_native != m_menu->nsMenuItem()) { + [m_native release]; + m_native = [m_menu->nsMenuItem() retain]; + [m_native setTag:reinterpret_cast(this)]; + } + } + + if ((m_role != NoRole) || m_merged) { + NSMenuItem *mergeItem = nil; + QT_MANGLE_NAMESPACE(QCocoaMenuLoader) *loader = getMenuLoader(); + switch (m_role) { + case ApplicationSpecificRole: + mergeItem = [loader appSpecificMenuItem]; + break; + case AboutRole: + mergeItem = [loader aboutMenuItem]; + break; + case AboutQtRole: + mergeItem = [loader aboutQtMenuItem]; + break; + case QuitRole: + mergeItem = [loader quitMenuItem]; + break; + case PreferencesRole: + mergeItem = [loader preferencesMenuItem]; + break; + case TextHeuristicRole: { + QString aboutString = tr("About").toLower(); + + if (m_text.startsWith(aboutString) || m_text.endsWith(aboutString)) { + if (m_text.indexOf(QRegExp(QString::fromLatin1("qt$"), Qt::CaseInsensitive)) == -1) + mergeItem = [loader aboutMenuItem]; + else + mergeItem = [loader aboutQtMenuItem]; + + m_merged = true; + } else if (m_text.startsWith(tr("Config").toLower()) + || m_text.startsWith(tr("Preference").toLower()) + || m_text.startsWith(tr("Options").toLower()) + || m_text.startsWith(tr("Setting").toLower()) + || m_text.startsWith(tr("Setup").toLower())) { + mergeItem = [loader preferencesMenuItem]; + } else if (m_text.startsWith(tr("Quit").toLower()) + || m_text.startsWith(tr("Exit").toLower())) { + mergeItem = [loader quitMenuItem]; + } + break; + } + + default: + qWarning() << Q_FUNC_INFO << "unsupported role" << (int) m_role; + } + + if (mergeItem) { + m_merged = true; + [m_native release]; + m_native = mergeItem; + [m_native retain]; // balance out release! + [m_native setTag:reinterpret_cast(this)]; + } else if (m_merged) { + // was previously merged, but no longer + [m_native release]; + m_native = nil; // create item below + m_merged = false; + } + } + + if (!m_native) { + m_native = [[NSMenuItem alloc] initWithTitle:QCFString::toNSString(m_text) + action:nil + keyEquivalent:@""]; + [m_native retain]; + [m_native setTag:reinterpret_cast(this)]; + } + +// [m_native setHidden:YES]; +// [m_native setHidden:NO]; + [m_native setHidden: !m_isVisible]; + + QString text = m_text; + QKeySequence accel = m_shortcut; + + { + int st = text.lastIndexOf(QLatin1Char('\t')); + if (st != -1) { + accel = QKeySequence(text.right(text.length()-(st+1))); + text.remove(st, text.length()-st); + } + } + + text = mergeText(); + accel = mergeAccel(); + + // Show multiple key sequences as part of the menu text. + if (accel.count() > 1) + text += QLatin1String(" (") + accel.toString(QKeySequence::NativeText) + QLatin1String(")"); + + QString finalString = qt_mac_removeMnemonics(text); + // Cocoa Font and title + if (m_font.resolve()) { + NSFont *customMenuFont = [NSFont fontWithName:QCFString::toNSString(m_font.family()) + size:m_font.pointSize()]; + NSArray *keys = [NSArray arrayWithObjects:NSFontAttributeName, nil]; + NSArray *objects = [NSArray arrayWithObjects:customMenuFont, nil]; + NSDictionary *attributes = [NSDictionary dictionaryWithObjects:objects forKeys:keys]; + NSAttributedString *str = [[[NSAttributedString alloc] initWithString:QCFString::toNSString(finalString) + attributes:attributes] autorelease]; + [m_native setAttributedTitle: str]; + } else { + [m_native setTitle: QCFString::toNSString(finalString)]; + } + + if (accel.count() == 1) { + [m_native setKeyEquivalent:keySequenceToKeyEqivalent(accel)]; + [m_native setKeyEquivalentModifierMask:keySequenceModifierMask(accel)]; + } else { + [m_native setKeyEquivalent:@""]; + [m_native setKeyEquivalentModifierMask:NSCommandKeyMask]; + } + + if (!m_icon.isNull()) { + NSImage *img = qt_mac_cgimage_to_nsimage(qt_mac_image_to_cgimage(m_icon)); + [m_native setImage: img]; + } + + [m_native setState:m_checked ? NSOnState : NSOffState]; + return m_native; +} + +QString QCocoaMenuItem::mergeText() +{ + extern QString qt_mac_applicationmenu_string(int type); + QT_MANGLE_NAMESPACE(QCocoaMenuLoader) *loader = getMenuLoader(); + if (m_native == [loader aboutMenuItem]) { + return qt_mac_applicationmenu_string(6).arg(qt_mac_applicationName()); + } else if (m_native== [loader aboutQtMenuItem]) { + if (m_text == QString("About Qt")) + return tr("About Qt"); + else + return m_text; + } else if (m_native == [loader preferencesMenuItem]) { + return qt_mac_applicationmenu_string(4); + } else if (m_native == [loader quitMenuItem]) { + return qt_mac_applicationmenu_string(5).arg(qt_mac_applicationName()); + } + return m_text; +} + +QKeySequence QCocoaMenuItem::mergeAccel() +{ + QT_MANGLE_NAMESPACE(QCocoaMenuLoader) *loader = getMenuLoader(); + if (m_native == [loader preferencesMenuItem]) + return QKeySequence(QKeySequence::Preferences); + else if (m_native == [loader quitMenuItem]) + return QKeySequence(QKeySequence::Quit); + + return m_shortcut; +} + +void QCocoaMenuItem::syncMerged() +{ + Q_ASSERT(m_merged); + [m_native setTag:reinterpret_cast(this)]; + [m_native setHidden: !m_isVisible]; +} + +void QCocoaMenuItem::syncModalState(bool modal) +{ + if (modal) + [m_native setEnabled:NO]; + else + [m_native setEnabled:m_enabled]; +} diff --git a/src/plugins/platforms/cocoa/qcocoamenuloader.h b/src/plugins/platforms/cocoa/qcocoamenuloader.h index b0d61fa5415..fe9d0e0af58 100644 --- a/src/plugins/platforms/cocoa/qcocoamenuloader.h +++ b/src/plugins/platforms/cocoa/qcocoamenuloader.h @@ -85,10 +85,11 @@ - (IBAction)hideOtherApplications:(id)sender; - (IBAction)unhideAllApplications:(id)sender; - (IBAction)hide:(id)sender; -- (IBAction)qtDispatcherToQAction:(id)sender; -- (void)qtUpdateMenubar; +- (IBAction)qtDispatcherToQPAMenuItem:(id)sender; - (void)orderFrontCharacterPalette:(id)sender; - (BOOL)validateMenuItem:(NSMenuItem*)menuItem; +- (void)qtTranslateApplicationMenu; +- (NSArray *)mergeable; @end void qt_mac_loadMenuNib(QT_MANGLE_NAMESPACE(QCocoaMenuLoader) *qtMenuLoader); diff --git a/src/plugins/platforms/cocoa/qcocoamenuloader.mm b/src/plugins/platforms/cocoa/qcocoamenuloader.mm index 7854a832530..45ac878ea4c 100644 --- a/src/plugins/platforms/cocoa/qcocoamenuloader.mm +++ b/src/plugins/platforms/cocoa/qcocoamenuloader.mm @@ -42,6 +42,8 @@ #include "qcocoamenuloader.h" #include "qcocoahelpers.h" +#include "qcocoamenubar.h" +#include "qcocoamenuitem.h" #include #include @@ -52,14 +54,33 @@ QT_FORWARD_DECLARE_CLASS(QCFString) QT_FORWARD_DECLARE_CLASS(QString) -#ifndef QT_NO_TRANSLATION - QT_BEGIN_NAMESPACE - extern QString qt_mac_applicationmenu_string(int type); - QT_END_NAMESPACE -#endif QT_USE_NAMESPACE +#ifndef QT_NO_TRANSLATION +static const char *application_menu_strings[] = { + QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU","Services"), + QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU","Hide %1"), + QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU","Hide Others"), + QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU","Show All"), + QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU","Preferences..."), + QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU","Quit %1"), + QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU","About %1") + }; + +QString qt_mac_applicationmenu_string(int type) +{ + QString menuString = QString::fromLatin1(application_menu_strings[type]); + QString translated = qApp->translate("QMenuBar", application_menu_strings[type]); + if (translated != menuString) { + return translated; + } else { + return qApp->translate("MAC_APPLICATION_MENU", + application_menu_strings[type]); + } +} +#endif + /* Loads and instantiates the main app menu from the menu nib file(s). @@ -127,6 +148,11 @@ void qt_mac_loadMenuNib(QT_MANGLE_NAMESPACE(QCocoaMenuLoader) *qtMenuLoader) // They should get synced back in. [preferencesItem setEnabled:NO]; [preferencesItem setHidden:YES]; + + // should set this in the NIB + [preferencesItem setTarget: self]; + [preferencesItem setAction: @selector(qtDispatcherToQPAMenuItem:)]; + [aboutItem setEnabled:NO]; [aboutItem setHidden:YES]; } @@ -269,19 +295,10 @@ void qt_mac_loadMenuNib(QT_MANGLE_NAMESPACE(QCocoaMenuLoader) *qtMenuLoader) [NSApp hide:sender]; } -- (void)qtUpdateMenubar -{ -#if 0 - QCocoaMenuBar::macUpdateMenuBarImmediatly(); -#endif -} - - (void)qtTranslateApplicationMenu { - qDebug() << "qtTranslateApplicationMenu"; -#if 0 - //#ifndef QT_NO_TRANSLATION +#ifndef QT_NO_TRANSLATION [servicesItem setTitle: QCFString::toNSString(qt_mac_applicationmenu_string(0))]; [hideItem setTitle: QCFString::toNSString(qt_mac_applicationmenu_string(1).arg(qt_mac_applicationName()))]; [hideAllOthersItem setTitle: QCFString::toNSString(qt_mac_applicationmenu_string(2))]; @@ -292,27 +309,27 @@ void qt_mac_loadMenuNib(QT_MANGLE_NAMESPACE(QCocoaMenuLoader) *qtMenuLoader) #endif } -- (IBAction)qtDispatcherToQAction:(id)sender +- (IBAction)qtDispatcherToQPAMenuItem:(id)sender { -#if 0 - // - //QScopedLoopLevelCounter loopLevelCounter(QApplicationPrivate::instance()->threadData); NSMenuItem *item = static_cast(sender); - if (QAction *action = reinterpret_cast([item tag])) { - action->trigger(); - } else if (item == quitItem) { + if (item == quitItem) { // We got here because someone was once the quitItem, but it has been // abandoned (e.g., the menubar was deleted). In the meantime, just do // normal QApplication::quit(). qApp->quit(); + return; + } + + if ([item tag]) { + QCocoaMenuItem *cocoaItem = reinterpret_cast([item tag]); + cocoaItem->activated(); } -#endif } - - (void)orderFrontCharacterPalette:(id)sender - { - [NSApp orderFrontCharacterPalette:sender]; - } +- (void)orderFrontCharacterPalette:(id)sender +{ + [NSApp orderFrontCharacterPalette:sender]; +} - (BOOL)validateMenuItem:(NSMenuItem*)menuItem { @@ -320,9 +337,18 @@ void qt_mac_loadMenuNib(QT_MANGLE_NAMESPACE(QCocoaMenuLoader) *qtMenuLoader) || [menuItem action] == @selector(hideOtherApplications:) || [menuItem action] == @selector(unhideAllApplications:)) { return [NSApp validateMenuItem:menuItem]; + } else if ([menuItem tag]) { + QCocoaMenuItem *cocoaItem = reinterpret_cast([menuItem tag]); + return cocoaItem->isEnabled(); } else { return [menuItem isEnabled]; } } +- (NSArray*) mergeable +{ + // don't include the quitItem here, since we want it always visible and enabled regardless + return [NSArray arrayWithObjects:preferencesItem, aboutItem, aboutQtItem, lastAppSpecificItem, nil]; +} + @end diff --git a/src/plugins/platforms/cocoa/qcocoanativeinterface.h b/src/plugins/platforms/cocoa/qcocoanativeinterface.h index 0346eafec44..fd0b10358f2 100644 --- a/src/plugins/platforms/cocoa/qcocoanativeinterface.h +++ b/src/plugins/platforms/cocoa/qcocoanativeinterface.h @@ -53,8 +53,13 @@ class QCocoaNativeInterface : public QPlatformNativeInterface { Q_OBJECT public: + QCocoaNativeInterface(); + void *nativeResourceForWindow(const QByteArray &resourceString, QWindow *window); +public Q_SLOTS: + void onAppFocusWindowChanged(QWindow *window); + private: /* "Virtual" function to create the platform printer support diff --git a/src/plugins/platforms/cocoa/qcocoanativeinterface.mm b/src/plugins/platforms/cocoa/qcocoanativeinterface.mm index df9ae236068..06d1f9ee0f3 100644 --- a/src/plugins/platforms/cocoa/qcocoanativeinterface.mm +++ b/src/plugins/platforms/cocoa/qcocoanativeinterface.mm @@ -43,6 +43,7 @@ #include "qcocoaglcontext.h" #include "qcocoawindow.h" #include "qcocoaprintersupport.h" +#include "qcocoamenubar.h" #include #include @@ -50,12 +51,17 @@ #include "qsurfaceformat.h" #include #include "qopenglcontext.h" +#include "qguiapplication.h" #include #include "qprintengine_mac_p.h" QT_BEGIN_NAMESPACE +QCocoaNativeInterface::QCocoaNativeInterface() +{ +} + void *QCocoaNativeInterface::nativeResourceForWindow(const QByteArray &resourceString, QWindow *window) { if (!window->handle()) { @@ -84,4 +90,9 @@ void *QCocoaNativeInterface::NSPrintInfoForPrintEngine(QPrintEngine *printEngine return macPrintEngine->d_func()->printInfo; } +void QCocoaNativeInterface::onAppFocusWindowChanged(QWindow *window) +{ + QCocoaMenuBar::updateMenuBarImmediately(); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoatheme.h b/src/plugins/platforms/cocoa/qcocoatheme.h index d126050f988..3c071d44c35 100644 --- a/src/plugins/platforms/cocoa/qcocoatheme.h +++ b/src/plugins/platforms/cocoa/qcocoatheme.h @@ -54,6 +54,10 @@ public: QCocoaTheme(); ~QCocoaTheme(); + virtual QPlatformMenuItem* createPlatformMenuItem() const; + virtual QPlatformMenu* createPlatformMenu() const; + virtual QPlatformMenuBar* createPlatformMenuBar() const; + bool usePlatformNativeDialog(DialogType dialogType) const; QPlatformDialogHelper *createPlatformDialogHelper(DialogType dialogType) const; diff --git a/src/plugins/platforms/cocoa/qcocoatheme.mm b/src/plugins/platforms/cocoa/qcocoatheme.mm index cfb146ad45c..2f630c77633 100644 --- a/src/plugins/platforms/cocoa/qcocoatheme.mm +++ b/src/plugins/platforms/cocoa/qcocoatheme.mm @@ -41,12 +41,19 @@ #include "qcocoatheme.h" -#include +#include #include "qcocoacolordialoghelper.h" #include "qcocoafiledialoghelper.h" #include "qcocoafontdialoghelper.h" #include "qcocoasystemsettings.h" +#include "qcocoamenuitem.h" +#include "qcocoamenu.h" +#include "qcocoamenubar.h" + +#include +#include +#include QT_BEGIN_NAMESPACE @@ -133,4 +140,27 @@ QVariant QCocoaTheme::themeHint(ThemeHint hint) const return QPlatformTheme::themeHint(hint); } +QPlatformMenuItem *QCocoaTheme::createPlatformMenuItem() const +{ + return new QCocoaMenuItem(); +} + +QPlatformMenu *QCocoaTheme::createPlatformMenu() const +{ + return new QCocoaMenu(); +} + +QPlatformMenuBar *QCocoaTheme::createPlatformMenuBar() const +{ + static bool haveMenubar = false; + if (!haveMenubar) { + haveMenubar = true; + QObject::connect(qGuiApp, SIGNAL(focusWindowChanged(QWindow*)), + QGuiApplicationPrivate::platformIntegration()->nativeInterface(), + SLOT(onAppFocusWindowChanged(QWindow*))); + } + + return new QCocoaMenuBar(); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoawindow.h b/src/plugins/platforms/cocoa/qcocoawindow.h index f2d6ac67bbf..debc8c42a27 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.h +++ b/src/plugins/platforms/cocoa/qcocoawindow.h @@ -85,6 +85,8 @@ QT_BEGIN_NAMESPACE // See the qt_on_cocoa manual tests for a working example, located // in tests/manual/cocoa at the time of writing. +class QCocoaMenuBar; + class QCocoaWindow : public QPlatformWindow { public: @@ -120,6 +122,8 @@ public: bool setWindowModified(bool modified) Q_DECL_OVERRIDE; + void setMenubar(QCocoaMenuBar *mb); + QCocoaMenuBar *menubar() const; protected: // NSWindow handling. The QCocoaWindow/QNSView can either be displayed // in an existing NSWindow or in one created by Qt. @@ -145,6 +149,7 @@ public: // for QNSView bool m_inConstructor; QCocoaGLContext *m_glContext; + QCocoaMenuBar *m_menubar; bool m_hasModalSession; }; diff --git a/src/plugins/platforms/cocoa/qcocoawindow.mm b/src/plugins/platforms/cocoa/qcocoawindow.mm index 5480b32083a..880292b24af 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.mm +++ b/src/plugins/platforms/cocoa/qcocoawindow.mm @@ -100,8 +100,12 @@ QCocoaWindow::QCocoaWindow(QWindow *tlw) , m_synchedWindowState(Qt::WindowActive) , m_inConstructor(true) , m_glContext(0) + , m_menubar(0) , m_hasModalSession(false) { +#ifdef QT_COCOA_ENABLE_WINDOW_DEBUG + qDebug() << "QCocoaWindow::QCocoaWindow" << this; +#endif QCocoaAutoReleasePool pool; m_contentView = [[QNSView alloc] initWithQWindow:tlw platformWindow:this]; @@ -577,3 +581,13 @@ bool QCocoaWindow::setWindowModified(bool modified) [m_nsWindow setDocumentEdited:(modified?YES:NO)]; return true; } + +void QCocoaWindow::setMenubar(QCocoaMenuBar *mb) +{ + m_menubar = mb; +} + +QCocoaMenuBar *QCocoaWindow::menubar() const +{ + return m_menubar; +} diff --git a/src/plugins/platforms/cocoa/qt_menu.nib/classes.nib b/src/plugins/platforms/cocoa/qt_menu.nib/classes.nib index 0031e0e4e50..78941153c2f 100644 --- a/src/plugins/platforms/cocoa/qt_menu.nib/classes.nib +++ b/src/plugins/platforms/cocoa/qt_menu.nib/classes.nib @@ -13,7 +13,7 @@ id orderFrontStandardAboutPanel id - qtDispatcherToQAction + qtDispatcherToQPAMenuItem id terminate id diff --git a/tests/manual/cocoa/menus/main.cpp b/tests/manual/cocoa/menus/main.cpp new file mode 100644 index 00000000000..d695c6c1386 --- /dev/null +++ b/tests/manual/cocoa/menus/main.cpp @@ -0,0 +1,201 @@ +/**************************************************************************** +** +** Copyright (C) 2012 KDAB +** Contact: http://www.qt-project.org/ +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** GNU Lesser General Public License Usage +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this +** file. Please review the following information to ensure the GNU Lesser +** General Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU General +** Public License version 3.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of this +** file. Please review the following information to ensure the GNU General +** Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include + + +class Responder : public QObject +{ + Q_OBJECT + +public: + Responder(QObject *pr) : + QObject(pr) + { + } + +public slots: + + + void toggleChecked(bool b) + { + QAction *a = qobject_cast(sender()); + } + + void showModalDialog() + { + QMessageBox::information(NULL, "Something", "Something happened. Modally."); + } + + void doPreferences() + { + qDebug() << "show preferences"; + } + + void aboutToShowSubmenu() + { + QMenu* m = (QMenu*) sender(); + qDebug() << "will show" << m; + + m->clear(); + + for (int i=0; i<10; ++i) { + m->addAction(QString("Recent File %1").arg(i + 1)); + } + } +}; + +void createWindow1() +{ + + QMainWindow *window = new QMainWindow; + QMenu *menu = new QMenu("TestMenu", window); + + window->menuBar()->addMenu(menu); + + Responder *r = new Responder(window); + + QAction *a = menu->addAction("TestMenuItem1"); + a->setShortcut( Qt::Key_A | Qt::SHIFT | Qt::CTRL ); + QObject::connect(a, SIGNAL(triggered()), + r, SLOT(showModalDialog())); + + + menu->addAction("T&estMenuItem2"); + a = menu->addAction("Preferences"); + a->setMenuRole(QAction::PreferencesRole); + QObject::connect(a, SIGNAL(triggered()), + r, SLOT(doPreferences())); + + a = menu->addAction("TestMenuItem4"); + a->setShortcut( Qt::Key_W | Qt::CTRL); + + QMenu *menu2 = new QMenu("SecondMenu", window); + window->menuBar()->addMenu(menu2); + + menu2->addAction("Yellow"); + a = menu2->addAction("Mau&ve"); + + QFont f; + f.setPointSize(9); + a->setFont(f); + + menu2->addAction("Taupe"); + + QMenu *submenu1 = new QMenu("Submenu", window); + submenu1->addAction("Sub Item 1"); + submenu1->addAction("Sub Item 2"); + submenu1->addAction("Sub Item 3"); + menu2->addMenu(submenu1); + + QMenu *submenu2 = new QMenu("Deeper", window); + submenu2->addAction("Sub Sub Item 1"); + submenu2->addAction("Sub Sub Item 2"); + submenu2->addAction("Sub Sub Item 3"); + submenu1->addMenu(submenu2); + + QMenu *menu3 = new QMenu("A Third Menu", window); + + menu3->addAction("Eins"); + + QMenu *submenu3 = new QMenu("Dynamic", window); + QObject::connect(submenu3, SIGNAL(aboutToShow()), r, SLOT(aboutToShowSubmenu())); + menu3->addMenu(submenu3); + + a = menu3->addAction("Zwei"); + a->setShortcut( Qt::Key_3 | Qt::ALT); + a = menu3->addAction("About Drei..."); + a->setMenuRole(QAction::AboutRole); + + window->menuBar()->addMenu(menu3); + + QAction *checkableAction = new QAction("Thing Enabled", window); + checkableAction->setCheckable(true); + checkableAction->setChecked(true); + QObject::connect(checkableAction, SIGNAL(triggered(bool)), + r, SLOT(toggleChecked(bool))); + + menu2->addAction(checkableAction); + + window->show(); + +} + +void createWindow2() +{ + QMainWindow *window = new QMainWindow; + QMenu *menu = new QMenu("Nuts", window); + + window->menuBar()->addMenu(menu); + + menu->addAction("Peanuts"); + menu->addAction("Walnuts"); + + QMenu *menu2 = new QMenu("Colours", window); + window->menuBar()->addMenu(menu2); + + menu2->addAction("Pink"); + menu2->addAction("Yellow"); + menu2->addAction("Grape"); + + QMenu *menu3 = new QMenu("Edit", window); + menu3->addAction("Cut"); + menu3->addAction("Copy boring way"); + menu3->addAction("Copy awesomely"); + menu3->addAction("Paste"); + + window->menuBar()->addMenu(menu3); + window->show(); +} + +int main(int argc, char **argv) { + QApplication app(argc, argv); + + app.setApplicationName("Banana"); + + createWindow1(); + createWindow2(); + + return app.exec(); +} + +#include "main.moc" diff --git a/tests/manual/cocoa/menus/menus.pro b/tests/manual/cocoa/menus/menus.pro new file mode 100644 index 00000000000..0ccb25feb98 --- /dev/null +++ b/tests/manual/cocoa/menus/menus.pro @@ -0,0 +1,5 @@ +TEMPLATE = app + +SOURCES += main.cpp +QT += gui widgets +CONFIG -=app_bundle