diff options
22 files changed, 1423 insertions, 256 deletions
diff --git a/src/plugins/platforms/cocoa/cocoa.pro b/src/plugins/platforms/cocoa/cocoa.pro index 313b666793..467ce96e63 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 2cecb296f4..90f09aa620 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 ec086fe62c..3855563bab 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 5113aca3f7..f8032603e5 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 8f8d7b84a6..c0b6f3abb6 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 bf8bba1ddf..479d4b53c2 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 <james.turner@kdab.com> ** 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. -// - -#include "qmacdefines_mac.h" -#import <Cocoa/Cocoa.h> - -QT_FORWARD_DECLARE_CLASS(QMenu) -QT_FORWARD_DECLARE_CLASS(QAction) - -#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_5 - -@protocol NSMenuDelegate <NSObject> -- (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 +#ifndef QCOCOAMENU_H +#define QCOCOAMENU_H -#endif +#include <QtCore/QList> +#include <qpa/qplatformmenu.h> +#include "qcocoamenuitem.h" + +@class NSMenuItem; +@class NSMenu; +@class NSObject; + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE -@interface QT_MANGLE_NAMESPACE(QNativeCocoaMenu) : NSMenu <NSMenuDelegate> +class QCocoaMenu : public QPlatformMenu { - 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 +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<QCocoaMenuItem *> merged() const; +private: + QCocoaMenuItem *itemOrNull(int index) const; + void insertNative(QCocoaMenuItem *item, QCocoaMenuItem *beforeItem); + + QList<QCocoaMenuItem *> m_menuItems; + NSMenu *m_nativeMenu; + NSMenuItem *m_nativeItem; + NSObject *m_delegate; + bool m_enabled; + quintptr m_tag; +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif diff --git a/src/plugins/platforms/cocoa/qcocoamenu.mm b/src/plugins/platforms/cocoa/qcocoamenu.mm index 00bbf49cca..9e466deb9a 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 <james.turner@kdab.com> ** 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 <private/qapplication_p.h> -#include <private/qaction_p.h> +#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 <NSMenuDelegate> { + 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); + Q_UNUSED(m); + emit m_menu->aboutToShow(); +} - if (!item) { - if (previousAction) { - qt_mac_clear_status_text(previousAction); - previousAction = 0; - } - return; - } +- (void) menuDidClose:(NSMenu*)m +{ + Q_UNUSED(m); + // wrong, but it's the best we can do + emit m_menu->aboutToHide(); +} - if (QAction *action = reinterpret_cast<QAction *>([item tag])) { - QMenu *qtmenu = static_cast<QT_MANGLE_NAMESPACE(QNativeCocoaMenu) *>(menu)->qmenu; - previousAction = action; - action->activate(QAction::Hover); - qt_mac_menu_emit_hovered(qtmenu, action); - action->showStatusText(0); // 0 widget -> action's parent - } +- (void) itemFired:(NSMenuItem*) item +{ + QCocoaMenuItem *cocoaItem = reinterpret_cast<QCocoaMenuItem *>([item tag]); + cocoaItem->activated(); } -- (void)menuWillOpen:(NSMenu*)menu +- (BOOL)validateMenuItem:(NSMenuItem*)menuItem { - while (QWidget *popup - = QApplication::activePopupWidget()) - popup->close(); - QMenu *qtmenu = static_cast<QT_MANGLE_NAMESPACE(QNativeCocoaMenu) *>(menu)->qmenu; - qt_mac_emit_menuSignals(qtmenu, true); - qt_mac_menu_collapseSeparators(menu, qtmenu->separatorsCollapsible()); + if (![menuItem tag]) + return YES; + + + QCocoaMenuItem* cocoaItem = reinterpret_cast<QCocoaMenuItem *>([menuItem tag]); + return cocoaItem->isEnabled(); } -- (void)menuDidClose:(NSMenu*)menu +@end + +QCocoaMenu::QCocoaMenu() : + m_enabled(true), + m_tag(0) { - qt_mac_emit_menuSignals(((QT_MANGLE_NAMESPACE(QNativeCocoaMenu) *)menu)->qmenu, false); - if (previousAction) { - qt_mac_clear_status_text(previousAction); - previousAction = 0; + 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<QCocoaMenuItem *>(menuItem); + QCocoaMenuItem *beforeItem = static_cast<QCocoaMenuItem *>(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 { + m_menuItems.append(cocoaItem); } + + insertNative(cocoaItem, beforeItem); } -- (BOOL)hasShortcut:(NSMenu *)menu forKey:(NSString *)key forModifiers:(NSUInteger)modifier - whichItem:(NSMenuItem**)outItem +void QCocoaMenu::insertNative(QCocoaMenuItem *item, QCocoaMenuItem *beforeItem) { - 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; - } + [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()]; } - if (outItem) - *outItem = 0; - return NO; } -NSString *qt_mac_removePrivateUnicode(NSString* string) +void QCocoaMenu::removeMenuItem(QPlatformMenuItem *menuItem) { - int len = [string length]; - if (len) { - QVarLengthArray <unichar, 10> characters(len); - bool changed = false; - for (int i = 0; i<len; i++) { - characters[i] = [string characterAtIndex:i]; - // check if they belong to key codes in private unicode range - // currently we need to handle only the NSDeleteFunctionKey - if (characters[i] == NSDeleteFunctionKey) { - characters[i] = NSDeleteCharacter; - changed = true; - } - } - if (changed) - return [NSString stringWithCharacters:characters.data() length:len]; + QCocoaMenuItem *cocoaItem = static_cast<QCocoaMenuItem *>(menuItem); + Q_ASSERT(m_menuItems.contains(cocoaItem)); + m_menuItems.removeOne(cocoaItem); + if (!cocoaItem->isMerged()) { + [m_nativeMenu removeItem: cocoaItem->nsItem()]; } - return string; } -- (BOOL)menuHasKeyEquivalent:(NSMenu *)menu forEvent:(NSEvent *)event target:(id *)target action:(SEL *)action +QCocoaMenuItem *QCocoaMenu::itemOrNull(int index) const +{ + if ((index < 0) || (index >= m_menuItems.size())) + return 0; + + return m_menuItems.at(index); +} + +void QCocoaMenu::syncMenuItem(QPlatformMenuItem *menuItem) { - // Check if the menu actually has a keysequence defined for this key event. - // If it does, then we will first send the key sequence to the QWidget that has focus - // since (in Qt's eyes) it needs to a chance at the key event first. If the widget - // accepts the key event, we then return YES, but set the target and action to be nil, - // which means that the action should not be triggered, and instead dispatch the event ourselves. - // In every other case we return NO, which means that Cocoa can do as it pleases - // (i.e., fire the menu action). - NSMenuItem *whichItem; - // Change the private unicode keys to the ones used in setting the "Key Equivalents" - NSString *characters = qt_mac_removePrivateUnicode([event characters]); - if ([self hasShortcut:menu - forKey:characters - // Interested only in Shift, Cmd, Ctrl & Alt Keys, so ignoring masks like, Caps lock, Num Lock ... - forModifiers:([event modifierFlags] & (NSShiftKeyMask | NSControlKeyMask | NSCommandKeyMask | NSAlternateKeyMask)) - whichItem:&whichItem]) { - QWidget *widget = 0; - QAction *qaction = 0; - if (whichItem && [whichItem tag]) { - qaction = reinterpret_cast<QAction *>([whichItem tag]); + QCocoaMenuItem *cocoaItem = static_cast<QCocoaMenuItem *>(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]; } - 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; + + 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<NSMenuItem *>([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(); + } } - return NO; } -- (NSInteger)indexOfItemWithTarget:(id)anObject andAction:(SEL)actionSelector +void QCocoaMenu::setParentItem(QCocoaMenuItem *item) { - 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; + Q_UNUSED(item); } -@end +void QCocoaMenu::setEnabled(bool enabled) +{ + m_enabled = enabled; +} -QT_BEGIN_NAMESPACE -extern int qt_mac_menus_open_count; // qmenu_mac.mm +QPlatformMenuItem *QCocoaMenu::menuItemAt(int position) const +{ + return m_menuItems.at(position); +} -void qt_mac_emit_menuSignals(QMenu *menu, bool show) +QPlatformMenuItem *QCocoaMenu::menuItemForTag(quintptr tag) const { - if (!menu) - return; - int delta; - if (show) { - emit menu->aboutToShow(); - delta = 1; - } else { - emit menu->aboutToHide(); - delta = -1; + foreach (QCocoaMenuItem *item, m_menuItems) { + if (item->tag() == tag) + return item; } - qt_mac_menus_open_count += delta; + + return 0; } -void qt_mac_clear_status_text(QAction *action) +QList<QCocoaMenuItem *> QCocoaMenu::merged() const { - action->d_func()->showStatusText(0, QString()); + QList<QCocoaMenuItem *> 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 qt_mac_menu_emit_hovered(QMenu *menu, QAction *action) +void QCocoaMenu::syncModalState(bool modal) { - emit menu->hovered(action); -} + if (!m_enabled) + modal = true; + [m_nativeItem setEnabled:!modal]; -QT_END_NAMESPACE + 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 0000000000..06e8eb6c9d --- /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 <james.turner@kdab.com> +** 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 <QtCore/QList> +#include <qpa/qplatformmenu.h> +#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<QCocoaMenuItem*> merged() const; +private: + static QCocoaWindow *findWindowForMenubar(); + static QCocoaMenuBar *findGlobalMenubar(); + + bool shouldDisable(QCocoaWindow *active) const; + + QList<QCocoaMenu*> 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 0000000000..fc403ba504 --- /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 <james.turner@kdab.com> +** 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 <Cocoa/Cocoa.h> + +#include "qcocoamenubar.h" +#include "qcocoawindow.h" +#include "qcocoamenuloader.h" +#include "qcocoaapplication.h" // for custom application category +#include "qcocoaautoreleasepool.h" + +#include <QtGui/QGuiApplication> +#include <QtCore/QDebug> + +static QList<QCocoaMenuBar*> 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<QCocoaMenu *>(platformMenu); + QCocoaMenu *beforeMenu = static_cast<QCocoaMenu *>(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<QCocoaMenu *>(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<QCocoaWindow*>(newParentWindow->handle()); + m_window->setMenubar(this); + } + + updateMenuBarImmediately(); +} + +QCocoaWindow *QCocoaMenuBar::findWindowForMenubar() +{ + if (qApp->focusWindow()) + return static_cast<QCocoaWindow*>(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<QCocoaMenuItem*> QCocoaMenuBar::merged() const +{ + QList<QCocoaMenuItem*> 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 0000000000..4a063d5965 --- /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 <james.turner@kdab.com> +** 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 <qpa/qplatformmenu.h> +#include <QtGui/QImage> + +//#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 0000000000..150e94ab07 --- /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 <james.turner@kdab.com> +** 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 <QtCore/QDebug> + +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<QCocoaMenu *>(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<NSInteger>(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<NSInteger>(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<NSInteger>(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<NSInteger>(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<NSInteger>(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 b0d61fa541..fe9d0e0af5 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 7854a83253..45ac878ea4 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 <QtCore/private/qcore_mac_p.h> #include <QtCore/qcoreapplication.h> @@ -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<NSMenuItem *>(sender); - if (QAction *action = reinterpret_cast<QAction *>([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<QCocoaMenuItem *>([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<QCocoaMenuItem *>([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 0346eafec4..fd0b10358f 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 df9ae23606..06d1f9ee0f 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 <qbytearray.h> #include <qwindow.h> @@ -50,12 +51,17 @@ #include "qsurfaceformat.h" #include <qpa/qplatformopenglcontext.h> #include "qopenglcontext.h" +#include "qguiapplication.h" #include <qdebug.h> #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 d126050f98..3c071d44c3 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 cfb146ad45..2f630c7763 100644 --- a/src/plugins/platforms/cocoa/qcocoatheme.mm +++ b/src/plugins/platforms/cocoa/qcocoatheme.mm @@ -41,12 +41,19 @@ #include "qcocoatheme.h" -#include <QVariant> +#include <QtCore/QVariant> #include "qcocoacolordialoghelper.h" #include "qcocoafiledialoghelper.h" #include "qcocoafontdialoghelper.h" #include "qcocoasystemsettings.h" +#include "qcocoamenuitem.h" +#include "qcocoamenu.h" +#include "qcocoamenubar.h" + +#include <QtGui/private/qguiapplication_p.h> +#include <QtGui/QPlatformIntegration> +#include <QtGui/QPlatformNativeInterface> 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 f2d6ac67bb..debc8c42a2 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 5480b32083..880292b24a 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 0031e0e4e5..78941153c2 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 @@ <string>id</string> <key>orderFrontStandardAboutPanel</key> <string>id</string> - <key>qtDispatcherToQAction</key> + <key>qtDispatcherToQPAMenuItem</key> <string>id</string> <key>terminate</key> <string>id</string> diff --git a/tests/manual/cocoa/menus/main.cpp b/tests/manual/cocoa/menus/main.cpp new file mode 100644 index 0000000000..d695c6c138 --- /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 <QtGui> +#include <QtWidgets> + + +class Responder : public QObject +{ + Q_OBJECT + +public: + Responder(QObject *pr) : + QObject(pr) + { + } + +public slots: + + + void toggleChecked(bool b) + { + QAction *a = qobject_cast<QAction *>(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 0000000000..0ccb25feb9 --- /dev/null +++ b/tests/manual/cocoa/menus/menus.pro @@ -0,0 +1,5 @@ +TEMPLATE = app + +SOURCES += main.cpp +QT += gui widgets +CONFIG -=app_bundle |