Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qcocoansmenu.mm
Go to the documentation of this file.
1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5#include <AppKit/AppKit.h>
6
7#include "qcocoansmenu.h"
8#include "qcocoamenu.h"
10#include "qcocoamenubar.h"
11#include "qcocoawindow.h"
12#include "qnsview.h"
13#include "qcocoahelpers.h"
14
15#include <QtCore/qcoreapplication.h>
16#include <QtCore/qcoreevent.h>
17#include <QtCore/qvarlengtharray.h>
18#include <QtGui/private/qapplekeymapper_p.h>
19
20#include <QtCore/qpointer.h>
21
22static NSString *qt_mac_removePrivateUnicode(NSString *string)
23{
24 if (const int len = string.length) {
25 QVarLengthArray<unichar, 10> characters(len);
26 bool changed = false;
27 for (int i = 0; i < len; i++) {
28 characters[i] = [string characterAtIndex:i];
29 // check if they belong to key codes in private unicode range
30 // currently we need to handle only the NSDeleteFunctionKey
31 if (characters[i] == NSDeleteFunctionKey) {
32 characters[i] = NSDeleteCharacter;
33 changed = true;
34 }
35 }
36 if (changed)
37 return [NSString stringWithCharacters:characters.data() length:len];
38 }
39 return string;
40}
41
42@implementation QCocoaNSMenu
43{
44 QPointer<QCocoaMenu> _platformMenu;
45}
46
47- (instancetype)initWithPlatformMenu:(QCocoaMenu *)menu
48{
49 if ((self = [super initWithTitle:@"Untitled"])) {
50 _platformMenu = menu;
51 self.autoenablesItems = YES;
52 self.delegate = [QCocoaNSMenuDelegate sharedMenuDelegate];
53 }
54
55 return self;
56}
57
58- (instancetype)initWithoutPlatformMenu:(NSString *)title
59{
60 if (self = [super initWithTitle:title])
61 self.delegate = [QCocoaNSMenuDelegate sharedMenuDelegate];
62 return self;
63}
64
65- (QCocoaMenu *)platformMenu
66{
67 return _platformMenu.data();
68}
69
70@end
71
72@implementation QCocoaNSMenuItem
73{
74 QPointer<QCocoaMenuItem> _platformMenuItem;
75}
76
77+ (instancetype)separatorItemWithPlatformMenuItem:(QCocoaMenuItem *)menuItem
78{
79 // Safe because +[NSMenuItem separatorItem] invokes [[self alloc] init]
80 auto *item = qt_objc_cast<QCocoaNSMenuItem *>([self separatorItem]);
81 Q_ASSERT_X(item, qPrintable(__FUNCTION__),
82 "Did +[NSMenuItem separatorItem] not invoke [[self alloc] init]?");
83 if (item)
84 item.platformMenuItem = menuItem;
85
86 return item;
87}
88
89- (instancetype)initWithPlatformMenuItem:(QCocoaMenuItem *)menuItem
90{
91 if ((self = [super initWithTitle:@"" action:nil keyEquivalent:@""])) {
92 _platformMenuItem = menuItem;
93 }
94
95 return self;
96}
97
98- (instancetype)init
99{
100 return [self initWithPlatformMenuItem:nullptr];
101}
102
103- (QCocoaMenuItem *)platformMenuItem
104{
105 return _platformMenuItem.data();
106}
107
108- (void)setPlatformMenuItem:(QCocoaMenuItem *)menuItem
109{
110 _platformMenuItem = menuItem;
111}
112
113@end
114
115#define CHECK_MENU_CLASS(menu) Q_ASSERT_X([menu isMemberOfClass:[QCocoaNSMenu class]],
116 __FUNCTION__, "Menu is not a QCocoaNSMenu")
117
118@implementation QCocoaNSMenuDelegate
119
120+ (instancetype)sharedMenuDelegate
121{
122 static QCocoaNSMenuDelegate *shared = nil;
123 static dispatch_once_t onceToken;
124 dispatch_once(&onceToken, ^{
125 shared = [[self alloc] init];
126 atexit_b(^{
127 [shared release];
128 shared = nil;
129 });
130 });
131 return shared;
132}
133
134- (NSInteger)numberOfItemsInMenu:(NSMenu *)menu
135{
136 CHECK_MENU_CLASS(menu);
137 return menu.numberOfItems;
138}
139
140- (BOOL)menu:(NSMenu *)menu updateItem:(NSMenuItem *)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel
141{
142 Q_UNUSED(index);
143 CHECK_MENU_CLASS(menu);
144
145 if (shouldCancel)
146 return NO;
147
148 const auto &platformMenu = static_cast<QCocoaNSMenu *>(menu).platformMenu;
149 if (!platformMenu)
150 return YES;
151
152 if (auto *platformItem = qt_objc_cast<QCocoaNSMenuItem *>(item).platformMenuItem) {
153 if (platformMenu->items().contains(platformItem)) {
154 if (auto *itemSubmenu = platformItem->menu())
155 itemSubmenu->setAttachedItem(item);
156 }
157 }
158
159 return YES;
160}
161
162- (void)menu:(NSMenu *)menu willHighlightItem:(NSMenuItem *)item
163{
164 CHECK_MENU_CLASS(menu);
165 if (auto *platformItem = qt_objc_cast<QCocoaNSMenuItem *>(item).platformMenuItem)
166 emit platformItem->hovered();
167}
168
169- (void)menuWillOpen:(NSMenu *)menu
170{
171 CHECK_MENU_CLASS(menu);
172 auto *platformMenu = static_cast<QCocoaNSMenu *>(menu).platformMenu;
173 if (!platformMenu)
174 return;
175
176 platformMenu->setIsOpen(true);
177 platformMenu->setIsAboutToShow(true);
178 emit platformMenu->aboutToShow();
179 platformMenu->setIsAboutToShow(false);
180}
181
182- (void)menuDidClose:(NSMenu *)menu
183{
184 CHECK_MENU_CLASS(menu);
185 auto *platformMenu = static_cast<QCocoaNSMenu *>(menu).platformMenu;
186 if (!platformMenu)
187 return;
188
189 platformMenu->setIsOpen(false);
190 // wrong, but it's the best we can do
191 emit platformMenu->aboutToHide();
192}
193
194- (BOOL)menuHasKeyEquivalent:(NSMenu *)menu forEvent:(NSEvent *)event target:(id *)target action:(SEL *)action
195{
196 /*
197 Check if the menu actually has a keysequence defined for this key event.
198 If it does, then we will first send the key sequence to the QWidget that has focus
199 since (in Qt's eyes) it needs to a chance at the key event first (QEvent::ShortcutOverride).
200 If the widget accepts the key event, we then return YES, but set the target and action to be nil,
201 which means that the action should not be triggered, and instead dispatch the event ourselves.
202 In every other case we return NO, which means that Cocoa can do as it pleases
203 (i.e., fire the menu action).
204 */
205
206 CHECK_MENU_CLASS(menu);
207
208 // Interested only in Shift, Cmd, Ctrl & Alt Keys, so ignoring masks like, Caps lock, Num Lock ...
209 static const NSUInteger mask = NSEventModifierFlagShift | NSEventModifierFlagControl
210 | NSEventModifierFlagCommand | NSEventModifierFlagOption;
211
212 // Change the private unicode keys to the ones used in setting the "Key Equivalents"
213 NSString *characters = qt_mac_removePrivateUnicode(event.charactersIgnoringModifiers);
214 const auto modifiers = event.modifierFlags & mask;
215 NSMenuItem *keyEquivalentItem = [self findItemInMenu:menu
216 forKey:characters
217 modifiers:modifiers];
218 if (!keyEquivalentItem) {
219 // Maybe the modified character is what we're looking for after all
220 characters = qt_mac_removePrivateUnicode(event.characters);
221 keyEquivalentItem = [self findItemInMenu:menu
222 forKey:characters
223 modifiers:modifiers];
224 }
225
226 if (keyEquivalentItem) {
227 QObject *object = qApp->focusObject();
228 if (object) {
229 QChar ch;
230 int keyCode;
231 ulong nativeModifiers = event.modifierFlags;
232 Qt::KeyboardModifiers modifiers = QAppleKeyMapper::fromCocoaModifiers(nativeModifiers);
233 NSString *charactersIgnoringModifiers = event.charactersIgnoringModifiers;
234 NSString *characters = event.characters;
235
236 if (charactersIgnoringModifiers.length > 0) { // convert the first character into a key code
237 if ((modifiers & Qt::ControlModifier) && characters.length > 0) {
238 ch = QChar([characters characterAtIndex:0]);
239 } else {
240 ch = QChar([charactersIgnoringModifiers characterAtIndex:0]);
241 }
242 keyCode = QAppleKeyMapper::fromCocoaKey(ch);
243 } else {
244 // might be a dead key
245 ch = QChar::ReplacementCharacter;
246 keyCode = Qt::Key_unknown;
247 }
248
249 QKeyEvent accel_ev(QEvent::ShortcutOverride, (keyCode & (~Qt::KeyboardModifierMask)),
250 Qt::KeyboardModifiers(modifiers & Qt::KeyboardModifierMask));
251 accel_ev.ignore();
252 QCoreApplication::sendEvent(object, &accel_ev);
253 if (accel_ev.isAccepted()) {
254 [[NSApp keyWindow] sendEvent:event];
255 *target = nil;
256 *action = nil;
257 return YES;
258 }
259 }
260 }
261
262 return NO;
263}
264
265- (NSMenuItem *)findItemInMenu:(NSMenu *)menu
266 forKey:(NSString *)key
267 modifiers:(NSUInteger)modifiers
268{
269 // Find an item in 'menu' that has the same key equivalent as specified by
270 // 'key' and 'modifiers'. We ignore disabled, hidden and separator items.
271 // In a similar fashion, we don't need to recurse into submenus because their
272 // delegate will have [menuHasKeyEquivalent:...] invoked at some point.
273
274 for (NSMenuItem *item in menu.itemArray) {
275 if (!item.enabled || item.hidden || item.separatorItem)
276 continue;
277
278 if (item.hasSubmenu)
279 continue;
280
281 NSString *menuKey = item.keyEquivalent;
282 if (menuKey && NSOrderedSame == [menuKey compare:key]
283 && modifiers == item.keyEquivalentModifierMask)
284 return item;
285 }
286
287 return nil;
288}
289
290@end
291
292#undef CHECK_MENU_CLASS
QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QCocoaMenu)
Q_FORWARD_DECLARE_OBJC_CLASS(NSMenuItem)
QT_FORWARD_DECLARE_CLASS(QCocoaMenuItem)
static NSString * qt_mac_removePrivateUnicode(NSString *string)
#define CHECK_MENU_CLASS(menu)
long NSInteger