summaryrefslogtreecommitdiffstats
path: root/src/plugins/platforms/cocoa/qcocoansmenu.mm
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/platforms/cocoa/qcocoansmenu.mm')
-rw-r--r--src/plugins/platforms/cocoa/qcocoansmenu.mm296
1 files changed, 296 insertions, 0 deletions
diff --git a/src/plugins/platforms/cocoa/qcocoansmenu.mm b/src/plugins/platforms/cocoa/qcocoansmenu.mm
new file mode 100644
index 0000000000..996a4ff194
--- /dev/null
+++ b/src/plugins/platforms/cocoa/qcocoansmenu.mm
@@ -0,0 +1,296 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the plugins of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#import "qcocoansmenu.h"
+#include "qcocoamenu.h"
+#include "qcocoamenuitem.h"
+#import "qnsview.h"
+
+#include <QtCore/qmetaobject.h>
+#include <QtCore/private/qthread_p.h>
+#include <QtGui/private/qguiapplication_p.h>
+
+static NSString *qt_mac_removePrivateUnicode(NSString* string)
+{
+ 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];
+ }
+ return string;
+}
+
+@implementation QCocoaNSMenu
+
+- (instancetype)initWithQPAMenu:(QCocoaMenu *)menu
+{
+ if ((self = [super initWithTitle:@"Untitled"])) {
+ _qpaMenu = menu;
+ self.autoenablesItems = YES;
+ self.delegate = [QCocoaNSMenuDelegate sharedMenuDelegate];
+ }
+
+ return self;
+}
+
+@end
+
+#define CHECK_MENU_CLASS(menu) Q_ASSERT([menu isMemberOfClass:[QCocoaNSMenu class]])
+
+@implementation QCocoaNSMenuDelegate
+
++ (instancetype)sharedMenuDelegate
+{
+ static QCocoaNSMenuDelegate *shared = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ shared = [[self alloc] init];
+ atexit_b(^{
+ [shared release];
+ shared = nil;
+ });
+ });
+ return shared;
+}
+
+- (NSInteger)numberOfItemsInMenu:(NSMenu *)menu
+{
+ CHECK_MENU_CLASS(menu);
+ return menu.numberOfItems;
+}
+
+- (BOOL)menu:(NSMenu *)menu updateItem:(NSMenuItem *)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel
+{
+ Q_UNUSED(index);
+ CHECK_MENU_CLASS(menu);
+
+ if (shouldCancel)
+ return NO;
+
+ const auto &qpaMenu = static_cast<QCocoaNSMenu *>(menu).qpaMenu;
+ if (qpaMenu.isNull())
+ return YES;
+
+ auto *menuItem = reinterpret_cast<QCocoaMenuItem *>(item.tag);
+ if (qpaMenu->items().contains(menuItem)) {
+ if (QCocoaMenu *itemSubmenu = menuItem->menu())
+ itemSubmenu->setAttachedItem(item);
+ }
+
+ return YES;
+}
+
+- (void)menu:(NSMenu *)menu willHighlightItem:(NSMenuItem *)item
+{
+ CHECK_MENU_CLASS(menu);
+ auto *qpaItem = reinterpret_cast<QCocoaMenuItem *>(item.tag);
+ if (qpaItem)
+ qpaItem->hovered();
+}
+
+- (void)menuWillOpen:(NSMenu *)menu
+{
+ CHECK_MENU_CLASS(menu);
+ const auto &qpaMenu = static_cast<QCocoaNSMenu *>(menu).qpaMenu;
+ if (qpaMenu.isNull())
+ return;
+
+ qpaMenu->setIsOpen(true);
+ emit qpaMenu->aboutToShow();
+}
+
+- (void)menuDidClose:(NSMenu *)menu
+{
+ CHECK_MENU_CLASS(menu);
+ const auto &qpaMenu = static_cast<QCocoaNSMenu *>(menu).qpaMenu;
+ if (qpaMenu.isNull())
+ return;
+
+ qpaMenu->setIsOpen(false);
+ // wrong, but it's the best we can do
+ emit qpaMenu->aboutToHide();
+}
+
+- (void)itemFired:(NSMenuItem *)item
+{
+ auto *qpaItem = reinterpret_cast<QCocoaMenuItem *>(item.tag);
+ // Menu-holding items also get a target to play nicely
+ // with NSMenuValidation but should not trigger.
+ if (!qpaItem || qpaItem->menu())
+ return;
+
+ QScopedScopeLevelCounter scopeLevelCounter(QGuiApplicationPrivate::instance()->threadData);
+ QGuiApplicationPrivate::modifier_buttons = [QNSView convertKeyModifiers:[NSEvent modifierFlags]];
+
+ static QMetaMethod activatedSignal = QMetaMethod::fromSignal(&QCocoaMenuItem::activated);
+ activatedSignal.invoke(qpaItem, Qt::QueuedConnection);
+}
+
+- (BOOL)validateMenuItem:(NSMenuItem*)item
+{
+ auto *qpaItem = reinterpret_cast<QCocoaMenuItem *>(item.tag);
+ // Menu-holding items are always enabled, as it's conventional in Cocoa
+ if (!qpaItem || qpaItem->menu())
+ return YES;
+
+ return qpaItem->isEnabled();
+}
+
+- (BOOL)menuHasKeyEquivalent:(NSMenu *)menu forEvent:(NSEvent *)event target:(id *)target action:(SEL *)action
+{
+ /*
+ 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 (QEvent::ShortcutOverride).
+ 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).
+ */
+
+ CHECK_MENU_CLASS(menu);
+
+ // Interested only in Shift, Cmd, Ctrl & Alt Keys, so ignoring masks like, Caps lock, Num Lock ...
+ static const NSUInteger mask = NSShiftKeyMask | NSControlKeyMask | NSCommandKeyMask | NSAlternateKeyMask;
+
+ // Change the private unicode keys to the ones used in setting the "Key Equivalents"
+ NSString *characters = qt_mac_removePrivateUnicode(event.charactersIgnoringModifiers);
+ const auto modifiers = event.modifierFlags & mask;
+ NSMenuItem *keyEquivalentItem = [self findItemInMenu:menu
+ forKey:characters
+ modifiers:modifiers];
+ if (!keyEquivalentItem) {
+ // Maybe the modified character is what we're looking for after all
+ characters = qt_mac_removePrivateUnicode(event.characters);
+ keyEquivalentItem = [self findItemInMenu:menu
+ forKey:characters
+ modifiers:modifiers];
+ }
+
+ if (keyEquivalentItem) {
+ if (!keyEquivalentItem.target) {
+ // This item was modified by QCocoaMenuBar::redirectKnownMenuItemsToFirstResponder
+ // and it looks like we're running a modal session for NSOpenPanel/NSSavePanel.
+ // QCocoaFileDialogHelper is actually the only place we use this and we run NSOpenPanel modal
+ // (modal sheet, window modal, application modal).
+ // Whatever the current first responder is, let's give it a chance
+ // and do not touch the Qt's focusObject (which is different from some native view
+ // having a focus inside NSSave/OpenPanel.
+ *target = nil;
+ *action = keyEquivalentItem.action;
+ return YES;
+ }
+
+ QObject *object = qApp->focusObject();
+ if (object) {
+ QChar ch;
+ int keyCode;
+ ulong nativeModifiers = [event modifierFlags];
+ Qt::KeyboardModifiers modifiers = [QNSView convertKeyModifiers: nativeModifiers];
+ NSString *charactersIgnoringModifiers = [event charactersIgnoringModifiers];
+ NSString *characters = [event characters];
+
+ if ([charactersIgnoringModifiers length] > 0) { // convert the first character into a key code
+ if ((modifiers & Qt::ControlModifier) && ([characters length] != 0)) {
+ ch = QChar([characters characterAtIndex:0]);
+ } else {
+ ch = QChar([charactersIgnoringModifiers characterAtIndex:0]);
+ }
+ keyCode = qt_mac_cocoaKey2QtKey(ch);
+ } else {
+ // might be a dead key
+ ch = QChar::ReplacementCharacter;
+ keyCode = Qt::Key_unknown;
+ }
+
+ QKeyEvent accel_ev(QEvent::ShortcutOverride, (keyCode & (~Qt::KeyboardModifierMask)),
+ Qt::KeyboardModifiers(modifiers & Qt::KeyboardModifierMask));
+ accel_ev.ignore();
+ QCoreApplication::sendEvent(object, &accel_ev);
+ if (accel_ev.isAccepted()) {
+ [[NSApp keyWindow] sendEvent: event];
+ *target = nil;
+ *action = nil;
+ return YES;
+ }
+ }
+ }
+
+ return NO;
+}
+
+- (NSMenuItem *)findItemInMenu:(NSMenu *)menu
+ forKey:(NSString *)key
+ modifiers:(NSUInteger)modifiers
+{
+ // Find an item in 'menu' that has the same key equivalent as specified by
+ // 'key' and 'modifiers'. We ignore disabled, hidden and separator items.
+ // In a similar fashion, we don't need to recurse into submenus because their
+ // delegate will have [menuHasKeyEquivalent:...] invoked at some point.
+
+ for (NSMenuItem *item in menu.itemArray) {
+ if (!item.enabled || item.hidden || item.separatorItem)
+ continue;
+
+ if (item.hasSubmenu)
+ continue;
+
+ NSString *menuKey = item.keyEquivalent;
+ if (menuKey && NSOrderedSame == [menuKey compare:key]
+ && modifiers == item.keyEquivalentModifierMask)
+ return item;
+ }
+
+ return nil;
+}
+
+@end
+
+#undef CHECK_MENU_CLASS