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