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
qcocoamenu.mm
Go to the documentation of this file.
1// Copyright (C) 2018 The Qt Company Ltd.
2// Copyright (C) 2012 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author James Turner <james.turner@kdab.com>
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4// Qt-Security score:significant reason:default
5
6#include <AppKit/AppKit.h>
7
8#include "qcocoamenu.h"
9#include "qcocoansmenu.h"
10
11#include "qcocoahelpers.h"
12
13#include <QtCore/QtDebug>
17#include "qcocoamenubar.h"
18#include "qcocoawindow.h"
19#include "qcocoascreen.h"
21
22#include <QtCore/private/qcore_mac_p.h>
23#include <QtCore/qpointer.h>
24
25QT_BEGIN_NAMESPACE
26
27QCocoaMenu::QCocoaMenu() :
28 m_attachedItem(nil),
29 m_enabled(true),
30 m_parentEnabled(true),
31 m_visible(true),
32 m_isOpen(false)
33{
34 QMacAutoReleasePool pool;
35
36 m_nativeMenu = [[QCocoaNSMenu alloc] initWithPlatformMenu:this];
37}
38
40{
41 for (auto *item : std::as_const(m_menuItems)) {
42 if (item->menuParent() == this)
43 item->setMenuParent(nullptr);
44 }
45
46 if (isOpen())
48
49 if (NSMenu *superMenu = m_nativeMenu.supermenu) {
50 for (NSMenuItem *item in superMenu.itemArray) {
51 if (item.submenu == m_nativeMenu) {
52 [superMenu removeItem:item];
53 break;
54 }
55 }
56 }
57
58 [m_nativeMenu release];
59}
60
61void QCocoaMenu::setText(const QString &text)
62{
63 QMacAutoReleasePool pool;
64 QString stripped = qt_mac_removeAmpersandEscapes(text);
65 m_nativeMenu.title = stripped.toNSString();
66}
67
69{
70 m_nativeMenu.minimumWidth = width;
71}
72
73void QCocoaMenu::setFont(const QFont &font)
74{
75 if (font.resolveMask()) {
76 NSFont *customMenuFont = [NSFont fontWithName:font.families().constFirst().toNSString()
77 size:font.pointSize()];
78 m_nativeMenu.font = customMenuFont;
79 }
80}
81
83{
84 return static_cast<NSMenu *>(m_nativeMenu);
85}
86
88{
89 QMacAutoReleasePool pool;
90 QCocoaApplicationDelegate.sharedDelegate.dockMenu = m_nativeMenu;
91}
92
93void QCocoaMenu::insertMenuItem(QPlatformMenuItem *menuItem, QPlatformMenuItem *before)
94{
95 QMacAutoReleasePool pool;
96 QCocoaMenuItem *cocoaItem = static_cast<QCocoaMenuItem *>(menuItem);
97 QCocoaMenuItem *beforeItem = static_cast<QCocoaMenuItem *>(before);
98
99 cocoaItem->sync();
100 if (beforeItem) {
101 int index = m_menuItems.indexOf(beforeItem);
102 // if a before item is supplied, it should be in the menu
103 if (index < 0) {
104 qCWarning(lcQpaMenus) << beforeItem << "not in" << m_menuItems;
105 return;
106 }
107 m_menuItems.insert(index, cocoaItem);
108 } else {
109 m_menuItems.append(cocoaItem);
110 }
111
112 insertNative(cocoaItem, beforeItem);
113
114 // Empty menus on a menubar are hidden by default. If the menu gets
115 // added to the menubar before it contains any item, we need to sync.
116 if (isVisible() && attachedItem().hidden) {
117 if (auto *mb = qobject_cast<QCocoaMenuBar *>(menuParent()))
118 mb->syncMenu(this);
119 }
120}
121
122void QCocoaMenu::insertNative(QCocoaMenuItem *item, QCocoaMenuItem *beforeItem)
123{
124 item->resolveTargetAction();
125 NSMenuItem *nativeItem = item->nsItem();
126 // Someone's adding new items after aboutToShow() was emitted
127 if (isOpen() && nativeItem && item->menu())
128 item->menu()->setAttachedItem(nativeItem);
129
130 item->setParentEnabled(isEnabled());
131
132 if (item->isMerged())
133 return;
134
135 // if the item we're inserting before is merged, skip along until
136 // we find a non-merged real item to insert ahead of.
137 while (beforeItem && beforeItem->isMerged()) {
138 beforeItem = itemOrNull(m_menuItems.indexOf(beforeItem) + 1);
139 }
140
141 if (nativeItem.menu) {
142 qCWarning(lcQpaMenus) << "Menu item" << item->text() << "already in menu" << QString::fromNSString(nativeItem.menu.title);
143 return;
144 }
145
146 if (beforeItem) {
147 if (beforeItem->isMerged()) {
148 qCWarning(lcQpaMenus, "No non-merged before menu item found");
149 return;
150 }
151 const NSInteger nativeIndex = [m_nativeMenu indexOfItem:beforeItem->nsItem()];
152 [m_nativeMenu insertItem:nativeItem atIndex:nativeIndex];
153 } else {
154 [m_nativeMenu addItem:nativeItem];
155 }
156 item->setMenuParent(this);
157}
158
159bool QCocoaMenu::isOpen() const
160{
161 return m_isOpen;
162}
163
164void QCocoaMenu::setIsOpen(bool isOpen)
165{
166 m_isOpen = isOpen;
167}
168
170{
171 return m_isAboutToShow;
172}
173
174void QCocoaMenu::setIsAboutToShow(bool isAbout)
175{
176 m_isAboutToShow = isAbout;
177}
178
179void QCocoaMenu::removeMenuItem(QPlatformMenuItem *menuItem)
180{
181 QMacAutoReleasePool pool;
182 QCocoaMenuItem *cocoaItem = static_cast<QCocoaMenuItem *>(menuItem);
183 if (!m_menuItems.contains(cocoaItem)) {
184 qCWarning(lcQpaMenus) << m_menuItems << "does not contain" << cocoaItem;
185 return;
186 }
187
188 if (cocoaItem->menuParent() == this)
189 cocoaItem->setMenuParent(nullptr);
190
191 // Ignore any parent enabled state
192 cocoaItem->setParentEnabled(true);
193
194 m_menuItems.removeOne(cocoaItem);
195 if (!cocoaItem->isMerged()) {
196 if (m_nativeMenu != cocoaItem->nsItem().menu) {
197 qCWarning(lcQpaMenus) << cocoaItem << "does not belong to" << m_nativeMenu;
198 return;
199 }
200 [m_nativeMenu removeItem:cocoaItem->nsItem()];
201 }
202}
203
204QCocoaMenuItem *QCocoaMenu::itemOrNull(int index) const
205{
206 if ((index < 0) || (index >= m_menuItems.size()))
207 return nullptr;
208
209 return m_menuItems.at(index);
210}
211
212void QCocoaMenu::scheduleUpdate()
213{
214 using namespace std::chrono_literals;
215
216 if (!m_updateTimer.isActive())
217 m_updateTimer.start(0ms, this);
218}
219
220void QCocoaMenu::timerEvent(QTimerEvent *e)
221{
222 if (e->id() == m_updateTimer.id()) {
223 m_updateTimer.stop();
224 [m_nativeMenu update];
225 }
226}
227
228void QCocoaMenu::syncMenuItem(QPlatformMenuItem *menuItem)
229{
230 syncMenuItem_helper(menuItem, false /*menubarUpdate*/);
231}
232
233void QCocoaMenu::syncMenuItem_helper(QPlatformMenuItem *menuItem, bool menubarUpdate)
234{
235 QMacAutoReleasePool pool;
236 QCocoaMenuItem *cocoaItem = static_cast<QCocoaMenuItem *>(menuItem);
237 if (!m_menuItems.contains(cocoaItem)) {
238 qCWarning(lcQpaMenus) << cocoaItem << "does not belong to" << this;
239 return;
240 }
241
242 const bool wasMerged = cocoaItem->isMerged();
243 NSMenuItem *oldItem = cocoaItem->nsItem();
244 NSMenuItem *syncedItem = cocoaItem->sync();
245
246 if (syncedItem != oldItem) {
247 // native item was changed for some reason
248 if (oldItem) {
249 if (wasMerged) {
250 oldItem.enabled = NO;
251 oldItem.hidden = YES;
252 oldItem.keyEquivalent = @"";
253 oldItem.keyEquivalentModifierMask = NSEventModifierFlagCommand;
254
255 } else {
256 [m_nativeMenu removeItem:oldItem];
257 }
258 }
259
260 QCocoaMenuItem* beforeItem = itemOrNull(m_menuItems.indexOf(cocoaItem) + 1);
261 insertNative(cocoaItem, beforeItem);
262 } else {
263 // Schedule NSMenuValidation to kick in. This is needed e.g.
264 // when an item's enabled state changes after menuWillOpen:
265 scheduleUpdate();
266 }
267
268 // This may be a good moment to attach this item's eventual submenu to the
269 // synced item, but only on the condition we're all currently hooked to the
270 // menunbar. A good indicator of this being the right moment is knowing that
271 // we got called from QCocoaMenuBar::updateMenuBarImmediately().
272 if (menubarUpdate)
273 if (QCocoaMenu *submenu = cocoaItem->menu())
274 submenu->setAttachedItem(syncedItem);
275}
276
278{
279 QMacAutoReleasePool pool;
280 if (enable) {
281 bool previousIsSeparator = true; // setting to true kills all the separators placed at the top.
282 NSMenuItem *lastVisibleItem = nil;
283
284 for (NSMenuItem *item in m_nativeMenu.itemArray) {
285 if (item.separatorItem) {
286 // hide item if previous was a separator, or if it's explicitly hidden
287 bool hideItem = previousIsSeparator;
288 if (auto *cocoaItem = qt_objc_cast<QCocoaNSMenuItem *>(item).platformMenuItem)
289 hideItem = previousIsSeparator || !cocoaItem->isVisible();
290 item.hidden = hideItem;
291 }
292
293 if (!item.hidden) {
294 lastVisibleItem = item;
295 previousIsSeparator = lastVisibleItem.separatorItem;
296 }
297 }
298
299 // We now need to check the final item since we don't want any separators at the end of the list.
300 if (lastVisibleItem && lastVisibleItem.separatorItem)
301 lastVisibleItem.hidden = YES;
302 } else {
303 for (auto *item : std::as_const(m_menuItems)) {
304 if (!item->isSeparator())
305 continue;
306
307 // sync the visibility directly
308 item->sync();
309 }
310 }
311}
312
313void QCocoaMenu::setEnabled(bool enabled)
314{
315 if (m_enabled == enabled)
316 return;
317 m_enabled = enabled;
318 const bool wasParentEnabled = m_parentEnabled;
320 m_parentEnabled = wasParentEnabled; // Reset to the parent value
321}
322
324{
325 return m_enabled && m_parentEnabled;
326}
327
328void QCocoaMenu::setVisible(bool visible)
329{
330 m_visible = visible;
331}
332
333void QCocoaMenu::showPopup(const QWindow *parentWindow, const QRect &targetRect, const QPlatformMenuItem *item)
334{
335 QMacAutoReleasePool pool;
336
337 QPointer<QCocoaMenu> guard = this;
338
339 QPoint pos = QPoint(targetRect.left(), targetRect.top() + targetRect.height());
340 // If the app quits while the menu is open (e.g. through a timer that starts before the menu was opened),
341 // then the window will have been destroyed before this function finishes executing. Account for that with QPointer.
342 QPointer<QCocoaWindow> cocoaWindow = parentWindow ? static_cast<QCocoaWindow *>(parentWindow->handle()) : nullptr;
343 NSView *view = cocoaWindow ? cocoaWindow->view() : nil;
344 NSMenuItem *nsItem = item ? ((QCocoaMenuItem *)item)->nsItem() : nil;
345
346 // store the window that this popup belongs to so that we can evaluate whether we are modally blocked
347 bool resetMenuParent = false;
348 if (!menuParent()) {
349 setMenuParent(cocoaWindow);
350 resetMenuParent = true;
351 }
352 auto menuParentGuard = qScopeGuard([&]{
353 if (resetMenuParent)
354 setMenuParent(nullptr);
355 });
356
357 QScreen *screen = nullptr;
358 if (parentWindow)
359 screen = parentWindow->screen();
360 if (!screen && !QGuiApplication::screens().isEmpty())
361 screen = QGuiApplication::screens().at(0);
362 Q_ASSERT(screen);
363
364 // Ideally, we would call -popUpMenuPositioningItem:atLocation:inView:.
365 // However, this showed not to work with modal windows where the menu items
366 // would appear disabled. So, we resort to a more artisanal solution. Note
367 // that this implies several things.
368 if (nsItem) {
369 // If we want to position the menu popup so that a specific item lies under
370 // the mouse cursor, we resort to NSPopUpButtonCell to do that. This is the
371 // typical use-case for a choice list, or non-editable combobox. We can't
372 // re-use the popUpContextMenu:withEvent:forView: logic below since it won't
373 // respect the menu's minimum width.
374 NSPopUpButtonCell *popupCell = [[[NSPopUpButtonCell alloc] initTextCell:@"" pullsDown:NO]
375 autorelease];
376 popupCell.altersStateOfSelectedItem = NO;
377 popupCell.transparent = YES;
378 popupCell.menu = m_nativeMenu;
379 [popupCell selectItem:nsItem];
380
381 QCocoaScreen *cocoaScreen = static_cast<QCocoaScreen *>(screen->handle());
382 int availableHeight = cocoaScreen->availableGeometry().height();
383 const QPoint globalPos = cocoaWindow ? cocoaWindow->mapToGlobal(pos) : pos;
384 int menuHeight = m_nativeMenu.size.height;
385 if (globalPos.y() + menuHeight > availableHeight) {
386 // Maybe we need to fix the vertical popup position but we don't know the
387 // exact popup height at the moment (and Cocoa is just guessing) nor its
388 // position. So, instead of translating by the popup's full height, we need
389 // to estimate where the menu will show up and translate by the remaining height.
390 float idx = ([m_nativeMenu indexOfItem:nsItem] + 1.0f) / m_nativeMenu.numberOfItems;
391 float heightBelowPos = (1.0 - idx) * menuHeight;
392 if (globalPos.y() + heightBelowPos > availableHeight)
393 pos.setY(pos.y() - globalPos.y() + availableHeight - heightBelowPos);
394 }
395
396 NSRect cellFrame = NSMakeRect(pos.x(), pos.y(), m_nativeMenu.minimumWidth, 10);
397 [popupCell performClickWithFrame:cellFrame inView:view];
398 } else {
399 // Else, we need to transform 'pos' to window or screen coordinates.
400 NSPoint nsPos = NSMakePoint(pos.x() - 1, pos.y());
401 if (view) {
402 // convert coordinates from view to the view's window
403 nsPos = [view convertPoint:nsPos toView:nil];
404 } else {
405 nsPos.y = screen->availableVirtualSize().height() - nsPos.y;
406 }
407
408 if (view) {
409 // Finally, we need to synthesize an event.
410 NSEvent *menuEvent = [NSEvent mouseEventWithType:NSEventTypeRightMouseDown
411 location:nsPos
412 modifierFlags:0
413 timestamp:0
414 windowNumber:view ? view.window.windowNumber : 0
415 context:nil
416 eventNumber:0
417 clickCount:1
418 pressure:1.0];
419 [NSMenu popUpContextMenu:m_nativeMenu withEvent:menuEvent forView:view];
420 } else {
421 [m_nativeMenu popUpMenuPositioningItem:nsItem atLocation:nsPos inView:nil];
422 }
423 }
424
425 if (!guard) {
426 menuParentGuard.dismiss();
427 return;
428 }
429
430 // The calls above block, and also swallow any mouse release event,
431 // so we need to clear any mouse button that triggered the menu popup.
432 if (cocoaWindow && !cocoaWindow->isForeignWindow())
433 [qnsview_cast(view) resetMouseButtons];
434}
435
437{
438 [m_nativeMenu cancelTracking];
439}
440
442{
443 if (0 <= position && position < m_menuItems.count())
444 return m_menuItems.at(position);
445
446 return nullptr;
447}
448
450{
451 for (auto *item : std::as_const(m_menuItems)) {
452 if (item->tag() == tag)
453 return item;
454 }
455
456 return nullptr;
457}
458
460{
461 return m_menuItems;
462}
463
465{
466 QList<QCocoaMenuItem *> result;
467 for (auto *item : std::as_const(m_menuItems)) {
468 if (item->menu()) { // recurse into submenus
469 result.append(item->menu()->merged());
470 continue;
471 }
472
473 if (item->isMerged())
474 result.append(item);
475 }
476
477 return result;
478}
479
481{
482 QMacAutoReleasePool pool; // FIXME Is this still needed for Creator? See 6a0bb4206a2928b83648
483
484 m_parentEnabled = enabled;
485 if (!m_enabled && enabled) // Some ancestor was enabled, but this menu is not
486 return;
487
488 for (auto *item : std::as_const(m_menuItems)) {
489 if (QCocoaMenu *menu = item->menu())
490 menu->propagateEnabledState(enabled);
491 else
492 item->setParentEnabled(enabled);
493 }
494}
495
496void QCocoaMenu::setAttachedItem(NSMenuItem *item)
497{
498 if (item == m_attachedItem)
499 return;
500
501 if (m_attachedItem)
502 m_attachedItem.submenu = nil;
503
504 m_attachedItem = item;
505
506 if (m_attachedItem)
507 m_attachedItem.submenu = m_nativeMenu;
508
509 // NSMenuItems with a submenu and submenuAction: as the item's action
510 // will not take part in NSMenuValidation, so explicitly enable/disable
511 // the item here. See also QCocoaMenuItem::resolveTargetAction()
512 m_attachedItem.enabled = m_attachedItem.hasSubmenu;
513}
514
516{
517 return m_attachedItem;
518}
519
520QT_END_NAMESPACE
void dismiss() override
QPlatformMenuItem * menuItemForTag(quintptr tag) const override
QList< QCocoaMenuItem * > merged() const
void timerEvent(QTimerEvent *e) override
This event handler can be reimplemented in a subclass to receive timer events for the object.
void setEnabled(bool enabled) override
QList< QCocoaMenuItem * > items() const
NSMenuItem * attachedItem() const
void insertMenuItem(QPlatformMenuItem *menuItem, QPlatformMenuItem *before) override
Definition qcocoamenu.mm:93
void setIsOpen(bool isOpen)
void setAsDockMenu() const override
Definition qcocoamenu.mm:87
void setText(const QString &text) override
Definition qcocoamenu.mm:61
bool isEnabled() const override
void propagateEnabledState(bool enabled)
void syncSeparatorsCollapsible(bool enable) override
void setIsAboutToShow(bool isAbout)
void showPopup(const QWindow *parentWindow, const QRect &targetRect, const QPlatformMenuItem *item) override
bool isAboutToShow() const
void setFont(const QFont &font) override
Definition qcocoamenu.mm:73
bool isOpen() const
QPlatformMenuItem * menuItemAt(int position) const override
void syncMenuItem(QPlatformMenuItem *menuItem) override
void removeMenuItem(QPlatformMenuItem *menuItem) override
void syncMenuItem_helper(QPlatformMenuItem *menuItem, bool menubarUpdate)
NSMenu * nsMenu() const override
Definition qcocoamenu.mm:82
void setVisible(bool visible) override
void setMinimumWidth(int width) override
Definition qcocoamenu.mm:68