Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
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
5#include <AppKit/AppKit.h>
6
7#include "qcocoamenu.h"
8#include "qcocoansmenu.h"
9
10#include "qcocoahelpers.h"
11
12#include <QtCore/QtDebug>
13#include "qcocoaapplication.h"
14#include "qcocoaintegration.h"
15#include "qcocoamenuloader.h"
16#include "qcocoamenubar.h"
17#include "qcocoawindow.h"
18#include "qcocoascreen.h"
20
21#include <QtCore/private/qcore_mac_p.h>
22#include <QtCore/qpointer.h>
23
25
27 m_attachedItem(nil),
28 m_updateTimer(0),
29 m_enabled(true),
30 m_parentEnabled(true),
31 m_visible(true),
32 m_isOpen(false)
33{
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())
47 dismiss();
48 [m_nativeMenu release];
49}
50
52{
55 m_nativeMenu.title = stripped.toNSString();
56}
57
59{
60 m_nativeMenu.minimumWidth = width;
61}
62
64{
65 if (font.resolveMask()) {
66 NSFont *customMenuFont = [NSFont fontWithName:font.families().constFirst().toNSString()
68 m_nativeMenu.font = customMenuFont;
69 }
70}
71
72NSMenu *QCocoaMenu::nsMenu() const
73{
74 return static_cast<NSMenu *>(m_nativeMenu);
75}
76
78{
80 QCocoaApplicationDelegate.sharedDelegate.dockMenu = m_nativeMenu;
81}
82
84{
86 QCocoaMenuItem *cocoaItem = static_cast<QCocoaMenuItem *>(menuItem);
87 QCocoaMenuItem *beforeItem = static_cast<QCocoaMenuItem *>(before);
88
89 cocoaItem->sync();
90 if (beforeItem) {
91 int index = m_menuItems.indexOf(beforeItem);
92 // if a before item is supplied, it should be in the menu
93 if (index < 0) {
94 qCWarning(lcQpaMenus) << beforeItem << "not in" << m_menuItems;
95 return;
96 }
97 m_menuItems.insert(index, cocoaItem);
98 } else {
99 m_menuItems.append(cocoaItem);
100 }
101
102 insertNative(cocoaItem, beforeItem);
103
104 // Empty menus on a menubar are hidden by default. If the menu gets
105 // added to the menubar before it contains any item, we need to sync.
106 if (isVisible() && attachedItem().hidden) {
107 if (auto *mb = qobject_cast<QCocoaMenuBar *>(menuParent()))
108 mb->syncMenu(this);
109 }
110}
111
112void QCocoaMenu::insertNative(QCocoaMenuItem *item, QCocoaMenuItem *beforeItem)
113{
114 item->resolveTargetAction();
115 NSMenuItem *nativeItem = item->nsItem();
116 // Someone's adding new items after aboutToShow() was emitted
117 if (isOpen() && nativeItem && item->menu())
118 item->menu()->setAttachedItem(nativeItem);
119
120 item->setParentEnabled(isEnabled());
121
122 if (item->isMerged())
123 return;
124
125 // if the item we're inserting before is merged, skip along until
126 // we find a non-merged real item to insert ahead of.
127 while (beforeItem && beforeItem->isMerged()) {
128 beforeItem = itemOrNull(m_menuItems.indexOf(beforeItem) + 1);
129 }
130
131 if (nativeItem.menu) {
132 qCWarning(lcQpaMenus) << "Menu item" << item->text() << "already in menu" << QString::fromNSString(nativeItem.menu.title);
133 return;
134 }
135
136 if (beforeItem) {
137 if (beforeItem->isMerged()) {
138 qCWarning(lcQpaMenus, "No non-merged before menu item found");
139 return;
140 }
141 const NSInteger nativeIndex = [m_nativeMenu indexOfItem:beforeItem->nsItem()];
142 [m_nativeMenu insertItem:nativeItem atIndex:nativeIndex];
143 } else {
144 [m_nativeMenu addItem:nativeItem];
145 }
146 item->setMenuParent(this);
147}
148
150{
151 return m_isOpen;
152}
153
154void QCocoaMenu::setIsOpen(bool isOpen)
155{
156 m_isOpen = isOpen;
157}
158
160{
161 return m_isAboutToShow;
162}
163
165{
166 m_isAboutToShow = isAbout;
167}
168
170{
172 QCocoaMenuItem *cocoaItem = static_cast<QCocoaMenuItem *>(menuItem);
173 if (!m_menuItems.contains(cocoaItem)) {
174 qCWarning(lcQpaMenus) << m_menuItems << "does not contain" << cocoaItem;
175 return;
176 }
177
178 if (cocoaItem->menuParent() == this)
179 cocoaItem->setMenuParent(nullptr);
180
181 // Ignore any parent enabled state
182 cocoaItem->setParentEnabled(true);
183
184 m_menuItems.removeOne(cocoaItem);
185 if (!cocoaItem->isMerged()) {
186 if (m_nativeMenu != cocoaItem->nsItem().menu) {
187 qCWarning(lcQpaMenus) << cocoaItem << "does not belong to" << m_nativeMenu;
188 return;
189 }
190 [m_nativeMenu removeItem:cocoaItem->nsItem()];
191 }
192}
193
194QCocoaMenuItem *QCocoaMenu::itemOrNull(int index) const
195{
196 if ((index < 0) || (index >= m_menuItems.size()))
197 return nullptr;
198
199 return m_menuItems.at(index);
200}
201
202void QCocoaMenu::scheduleUpdate()
203{
204 if (!m_updateTimer)
205 m_updateTimer = startTimer(0);
206}
207
209{
210 if (e->timerId() == m_updateTimer) {
211 killTimer(m_updateTimer);
212 m_updateTimer = 0;
213 [m_nativeMenu update];
214 }
215}
216
218{
219 syncMenuItem_helper(menuItem, false /*menubarUpdate*/);
220}
221
222void QCocoaMenu::syncMenuItem_helper(QPlatformMenuItem *menuItem, bool menubarUpdate)
223{
225 QCocoaMenuItem *cocoaItem = static_cast<QCocoaMenuItem *>(menuItem);
226 if (!m_menuItems.contains(cocoaItem)) {
227 qCWarning(lcQpaMenus) << cocoaItem << "does not belong to" << this;
228 return;
229 }
230
231 const bool wasMerged = cocoaItem->isMerged();
232 NSMenuItem *oldItem = cocoaItem->nsItem();
233 NSMenuItem *syncedItem = cocoaItem->sync();
234
235 if (syncedItem != oldItem) {
236 // native item was changed for some reason
237 if (oldItem) {
238 if (wasMerged) {
239 oldItem.enabled = NO;
240 oldItem.hidden = YES;
241 oldItem.keyEquivalent = @"";
242 oldItem.keyEquivalentModifierMask = NSEventModifierFlagCommand;
243
244 } else {
245 [m_nativeMenu removeItem:oldItem];
246 }
247 }
248
249 QCocoaMenuItem* beforeItem = itemOrNull(m_menuItems.indexOf(cocoaItem) + 1);
250 insertNative(cocoaItem, beforeItem);
251 } else {
252 // Schedule NSMenuValidation to kick in. This is needed e.g.
253 // when an item's enabled state changes after menuWillOpen:
254 scheduleUpdate();
255 }
256
257 // This may be a good moment to attach this item's eventual submenu to the
258 // synced item, but only on the condition we're all currently hooked to the
259 // menunbar. A good indicator of this being the right moment is knowing that
260 // we got called from QCocoaMenuBar::updateMenuBarImmediately().
261 if (menubarUpdate)
262 if (QCocoaMenu *submenu = cocoaItem->menu())
263 submenu->setAttachedItem(syncedItem);
264}
265
267{
269 if (enable) {
270 bool previousIsSeparator = true; // setting to true kills all the separators placed at the top.
271 NSMenuItem *lastVisibleItem = nil;
272
273 for (NSMenuItem *item in m_nativeMenu.itemArray) {
274 if (item.separatorItem) {
275 // hide item if previous was a separator, or if it's explicitly hidden
276 bool hideItem = previousIsSeparator;
277 if (auto *cocoaItem = qt_objc_cast<QCocoaNSMenuItem *>(item).platformMenuItem)
278 hideItem = previousIsSeparator || !cocoaItem->isVisible();
279 item.hidden = hideItem;
280 }
281
282 if (!item.hidden) {
283 lastVisibleItem = item;
284 previousIsSeparator = lastVisibleItem.separatorItem;
285 }
286 }
287
288 // We now need to check the final item since we don't want any separators at the end of the list.
289 if (lastVisibleItem && lastVisibleItem.separatorItem)
290 lastVisibleItem.hidden = YES;
291 } else {
292 for (auto *item : std::as_const(m_menuItems)) {
293 if (!item->isSeparator())
294 continue;
295
296 // sync the visibility directly
297 item->sync();
298 }
299 }
300}
301
303{
304 if (m_enabled == enabled)
305 return;
306 m_enabled = enabled;
307 const bool wasParentEnabled = m_parentEnabled;
308 propagateEnabledState(m_enabled);
309 m_parentEnabled = wasParentEnabled; // Reset to the parent value
310}
311
313{
314 return m_enabled && m_parentEnabled;
315}
316
317void QCocoaMenu::setVisible(bool visible)
318{
319 m_visible = visible;
320}
321
322void QCocoaMenu::showPopup(const QWindow *parentWindow, const QRect &targetRect, const QPlatformMenuItem *item)
323{
325
326 QPointer<QCocoaMenu> guard = this;
327
328 QPoint pos = QPoint(targetRect.left(), targetRect.top() + targetRect.height());
329 // If the app quits while the menu is open (e.g. through a timer that starts before the menu was opened),
330 // then the window will have been destroyed before this function finishes executing. Account for that with QPointer.
331 QPointer<QCocoaWindow> cocoaWindow = parentWindow ? static_cast<QCocoaWindow *>(parentWindow->handle()) : nullptr;
332 NSView *view = cocoaWindow ? cocoaWindow->view() : nil;
333 NSMenuItem *nsItem = item ? ((QCocoaMenuItem *)item)->nsItem() : nil;
334
335 // store the window that this popup belongs to so that we can evaluate whether we are modally blocked
336 bool resetMenuParent = false;
337 if (!menuParent()) {
338 setMenuParent(cocoaWindow);
339 resetMenuParent = true;
340 }
341 auto menuParentGuard = qScopeGuard([&]{
342 if (resetMenuParent)
343 setMenuParent(nullptr);
344 });
345
346 QScreen *screen = nullptr;
347 if (parentWindow)
348 screen = parentWindow->screen();
349 if (!screen && !QGuiApplication::screens().isEmpty())
352
353 // Ideally, we would call -popUpMenuPositioningItem:atLocation:inView:.
354 // However, this showed not to work with modal windows where the menu items
355 // would appear disabled. So, we resort to a more artisanal solution. Note
356 // that this implies several things.
357 if (nsItem) {
358 // If we want to position the menu popup so that a specific item lies under
359 // the mouse cursor, we resort to NSPopUpButtonCell to do that. This is the
360 // typical use-case for a choice list, or non-editable combobox. We can't
361 // re-use the popUpContextMenu:withEvent:forView: logic below since it won't
362 // respect the menu's minimum width.
363 NSPopUpButtonCell *popupCell = [[[NSPopUpButtonCell alloc] initTextCell:@"" pullsDown:NO]
364 autorelease];
365 popupCell.altersStateOfSelectedItem = NO;
366 popupCell.transparent = YES;
367 popupCell.menu = m_nativeMenu;
368 [popupCell selectItem:nsItem];
369
370 QCocoaScreen *cocoaScreen = static_cast<QCocoaScreen *>(screen->handle());
371 int availableHeight = cocoaScreen->availableGeometry().height();
372 const QPoint globalPos = cocoaWindow ? cocoaWindow->mapToGlobal(pos) : pos;
373 int menuHeight = m_nativeMenu.size.height;
374 if (globalPos.y() + menuHeight > availableHeight) {
375 // Maybe we need to fix the vertical popup position but we don't know the
376 // exact popup height at the moment (and Cocoa is just guessing) nor its
377 // position. So, instead of translating by the popup's full height, we need
378 // to estimate where the menu will show up and translate by the remaining height.
379 float idx = ([m_nativeMenu indexOfItem:nsItem] + 1.0f) / m_nativeMenu.numberOfItems;
380 float heightBelowPos = (1.0 - idx) * menuHeight;
381 if (globalPos.y() + heightBelowPos > availableHeight)
382 pos.setY(pos.y() - globalPos.y() + availableHeight - heightBelowPos);
383 }
384
385 NSRect cellFrame = NSMakeRect(pos.x(), pos.y(), m_nativeMenu.minimumWidth, 10);
386 [popupCell performClickWithFrame:cellFrame inView:view];
387 } else {
388 // Else, we need to transform 'pos' to window or screen coordinates.
389 NSPoint nsPos = NSMakePoint(pos.x() - 1, pos.y());
390 if (view) {
391 // convert coordinates from view to the view's window
392 nsPos = [view convertPoint:nsPos toView:nil];
393 } else {
394 nsPos.y = screen->availableVirtualSize().height() - nsPos.y;
395 }
396
397 if (view) {
398 // Finally, we need to synthesize an event.
399 NSEvent *menuEvent = [NSEvent mouseEventWithType:NSEventTypeRightMouseDown
400 location:nsPos
401 modifierFlags:0
402 timestamp:0
403 windowNumber:view ? view.window.windowNumber : 0
404 context:nil
405 eventNumber:0
406 clickCount:1
407 pressure:1.0];
408 [NSMenu popUpContextMenu:m_nativeMenu withEvent:menuEvent forView:view];
409 } else {
410 [m_nativeMenu popUpMenuPositioningItem:nsItem atLocation:nsPos inView:nil];
411 }
412 }
413
414 if (!guard) {
415 menuParentGuard.dismiss();
416 return;
417 }
418
419 // The calls above block, and also swallow any mouse release event,
420 // so we need to clear any mouse button that triggered the menu popup.
421 if (cocoaWindow && !cocoaWindow->isForeignWindow())
422 [qnsview_cast(view) resetMouseButtons];
423}
424
426{
427 [m_nativeMenu cancelTracking];
428}
429
431{
432 if (0 <= position && position < m_menuItems.count())
433 return m_menuItems.at(position);
434
435 return nullptr;
436}
437
439{
440 for (auto *item : std::as_const(m_menuItems)) {
441 if (item->tag() == tag)
442 return item;
443 }
444
445 return nullptr;
446}
447
448QList<QCocoaMenuItem *> QCocoaMenu::items() const
449{
450 return m_menuItems;
451}
452
453QList<QCocoaMenuItem *> QCocoaMenu::merged() const
454{
455 QList<QCocoaMenuItem *> result;
456 for (auto *item : std::as_const(m_menuItems)) {
457 if (item->menu()) { // recurse into submenus
458 result.append(item->menu()->merged());
459 continue;
460 }
461
462 if (item->isMerged())
463 result.append(item);
464 }
465
466 return result;
467}
468
470{
471 QMacAutoReleasePool pool; // FIXME Is this still needed for Creator? See 6a0bb4206a2928b83648
472
473 m_parentEnabled = enabled;
474 if (!m_enabled && enabled) // Some ancestor was enabled, but this menu is not
475 return;
476
477 for (auto *item : std::as_const(m_menuItems)) {
478 if (QCocoaMenu *menu = item->menu())
479 menu->propagateEnabledState(enabled);
480 else
481 item->setParentEnabled(enabled);
482 }
483}
484
486{
487 if (item == m_attachedItem)
488 return;
489
490 if (m_attachedItem)
491 m_attachedItem.submenu = nil;
492
493 m_attachedItem = item;
494
495 if (m_attachedItem)
496 m_attachedItem.submenu = m_nativeMenu;
497
498 // NSMenuItems with a submenu and submenuAction: as the item's action
499 // will not take part in NSMenuValidation, so explicitly enable/disable
500 // the item here. See also QCocoaMenuItem::resolveTargetAction()
501 m_attachedItem.enabled = m_attachedItem.hasSubmenu;
502}
503
504NSMenuItem *QCocoaMenu::attachedItem() const
505{
506 return m_attachedItem;
507}
508
NSMenuItem * sync()
void setMenuParent(QObject *o)
QObject * menuParent() const
void dismiss() override
bool isVisible() const
Definition qcocoamenu.h:48
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:83
void setIsOpen(bool isOpen)
void setAsDockMenu() const override
Definition qcocoamenu.mm:77
void setText(const QString &text) override
Definition qcocoamenu.mm:51
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:63
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)
void setAttachedItem(NSMenuItem *item)
NSMenu * nsMenu() const override
Definition qcocoamenu.mm:72
void setVisible(bool visible) override
void setMinimumWidth(int width) override
Definition qcocoamenu.mm:58
QRect availableGeometry() const override
Reimplement in subclass to return the pixel geometry of the available space This normally is the desk...
\reentrant
Definition qfont.h:22
QStringList families() const
Definition qfont.cpp:2699
int pointSize() const
Returns the point size of the font.
Definition qfont.cpp:884
uint resolveMask() const
Definition qfont.h:312
static QList< QScreen * > screens()
Returns a list of all the screens associated with the windowing system the application is connected t...
qsizetype size() const noexcept
Definition qlist.h:397
iterator insert(qsizetype i, parameter_type t)
Definition qlist.h:488
bool removeOne(const AT &t)
Definition qlist.h:598
const_reference at(qsizetype i) const noexcept
Definition qlist.h:446
qsizetype count() const noexcept
Definition qlist.h:398
void append(parameter_type t)
Definition qlist.h:458
int startTimer(int interval, Qt::TimerType timerType=Qt::CoarseTimer)
This is an overloaded function that will start a timer of type timerType and a timeout of interval mi...
Definition qobject.cpp:1817
void killTimer(int id)
Kills the timer with timer identifier, id.
Definition qobject.cpp:1912
virtual quintptr tag() const
\inmodule QtCore\reentrant
Definition qpoint.h:25
constexpr int y() const noexcept
Returns the y coordinate of this point.
Definition qpoint.h:135
\inmodule QtCore\reentrant
Definition qrect.h:30
constexpr int height() const noexcept
Returns the height of the rectangle.
Definition qrect.h:239
The QScreen class is used to query screen properties. \inmodule QtGui.
Definition qscreen.h:32
QSize availableVirtualSize
the available size of the virtual desktop to which this screen belongs
Definition qscreen.h:44
QPlatformScreen * handle() const
Get the platform screen handle.
Definition qscreen.cpp:83
constexpr int height() const noexcept
Returns the height.
Definition qsize.h:133
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
\inmodule QtCore
Definition qcoreevent.h:366
int timerId() const
Returns the unique timer identifier, which is the same identifier as returned from QObject::startTime...
Definition qcoreevent.h:370
\inmodule QtGui
Definition qwindow.h:63
QString text
Combined button and popup list for selecting options.
static void * context
QNSView * qnsview_cast(NSView *view)
Returns the view cast to a QNSview if possible.
QString qt_mac_removeAmpersandEscapes(QString s)
NSMenuItem * hideItem
long NSInteger
instancetype initWithPlatformMenu
AudioChannelLayoutTag tag
static glyph_t stripped(glyph_t glyph)
#define qCWarning(category,...)
GLint location
GLenum GLuint GLintptr GLsizeiptr size
[1]
GLuint index
[2]
GLenum GLenum GLsizei const GLuint GLboolean enabled
GLint GLsizei width
GLboolean enable
GLuint in
GLuint64EXT * result
[6]
static qreal position(const QQuickItem *item, QQuickAnchors::Anchor anchorLine)
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
QScopeGuard< typename std::decay< F >::type > qScopeGuard(F &&f)
[qScopeGuard]
Definition qscopeguard.h:60
QScreen * screen
[1]
Definition main.cpp:29
size_t quintptr
Definition qtypes.h:167
#define enabled
sem release()
scene addItem(form)
QGraphicsItem * item
QMenu menu
[5]
QQuickView * view
[0]
qsizetype indexOf(const AT &t, qsizetype from=0) const noexcept
Definition qlist.h:962
bool contains(const AT &t) const noexcept
Definition qlist.h:45