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
qcocoamenubar.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 "qcocoamenubar.h"
8#include "qcocoawindow.h"
9#include "qcocoamenuloader.h"
10#include "qcocoaapplication.h" // for custom application category
12#include "qcocoahelpers.h"
13
14#include <QtGui/QGuiApplication>
15#include <QtCore/QDebug>
16
17#include <QtCore/private/qcore_mac_p.h>
18#include <QtGui/private/qguiapplication_p.h>
19
21
22static QList<QCocoaMenuBar*> static_menubars;
23
25{
26 static_menubars.append(this);
27
28 // clicks into the menu bar should close all popup windows
29 static QMacNotificationObserver menuBarClickObserver(nil, NSMenuDidBeginTrackingNotification, ^{
30 QGuiApplicationPrivate::instance()->closeAllPopups();
31 });
32
33 m_nativeMenu = [[NSMenu alloc] init];
34 qCDebug(lcQpaMenus) << "Constructed" << this << "with" << m_nativeMenu;
35}
36
38{
39 qCDebug(lcQpaMenus) << "Destructing" << this << "with" << m_nativeMenu;
40 for (auto menu : std::as_const(m_menus)) {
41 if (!menu)
42 continue;
43 NSMenuItem *item = nativeItemForMenu(menu);
44 if (menu->attachedItem() == item)
45 menu->setAttachedItem(nil);
46 }
47
48 [m_nativeMenu release];
49 static_menubars.removeOne(this);
50
51 if (!m_window.isNull() && m_window->menubar() == this) {
52 m_window->setMenubar(nullptr);
53
54 // Delete the children first so they do not cause
55 // the native menu items to be hidden after
56 // the menu bar was updated
59 }
60}
61
62bool QCocoaMenuBar::needsImmediateUpdate()
63{
64 if (!m_window.isNull()) {
65 if (m_window->window()->isActive())
66 return true;
67 } else {
68 // Only update if the focus/active window has no
69 // menubar, which means it'll be using this menubar.
70 // This is to avoid a modification in a parentless
71 // menubar to affect a window-assigned menubar.
73 if (!fw) {
74 // Same if there's no focus window, BTW.
75 return true;
76 } else {
77 QCocoaWindow *cw = static_cast<QCocoaWindow *>(fw->handle());
78 if (cw && !cw->menubar())
79 return true;
80 }
81 }
82
83 // Either the menubar is attached to a non-active window,
84 // or the application's focus window has its own menubar
85 // (which is different from this one)
86 return false;
87}
88
90{
91 QCocoaMenu *menu = static_cast<QCocoaMenu *>(platformMenu);
92 QCocoaMenu *beforeMenu = static_cast<QCocoaMenu *>(before);
93
94 qCDebug(lcQpaMenus) << "Inserting" << menu << "before" << before << "into" << this;
95
96 if (m_menus.contains(QPointer<QCocoaMenu>(menu))) {
97 qCWarning(lcQpaMenus, "This menu already belongs to the menubar, remove it first");
98 return;
99 }
100
101 if (beforeMenu && !m_menus.contains(QPointer<QCocoaMenu>(beforeMenu))) {
102 qCWarning(lcQpaMenus, "The before menu does not belong to the menubar");
103 return;
104 }
105
106 int insertionIndex = beforeMenu ? m_menus.indexOf(beforeMenu) : m_menus.size();
107 m_menus.insert(insertionIndex, menu);
108
109 {
111 NSMenuItem *item = [[[NSMenuItem alloc] init] autorelease];
112 item.tag = reinterpret_cast<NSInteger>(menu);
113
114 if (beforeMenu) {
115 // QMenuBar::toNSMenu() exposes the native menubar and
116 // the user could have inserted its own items in there.
117 // Same remark applies to removeMenu().
118 NSMenuItem *beforeItem = nativeItemForMenu(beforeMenu);
119 NSInteger nativeIndex = [m_nativeMenu indexOfItem:beforeItem];
120 [m_nativeMenu insertItem:item atIndex:nativeIndex];
121 } else {
122 [m_nativeMenu addItem:item];
123 }
124 }
125
126 syncMenu_helper(menu, false /*internaCall*/);
127
128 if (needsImmediateUpdate())
130}
131
133{
134 QCocoaMenu *menu = static_cast<QCocoaMenu *>(platformMenu);
135 if (!m_menus.contains(menu)) {
136 qCWarning(lcQpaMenus) << "Trying to remove" << menu << "that does not belong to" << this;
137 return;
138 }
139
140 NSMenuItem *item = nativeItemForMenu(menu);
141 if (menu->attachedItem() == item)
142 menu->setAttachedItem(nil);
143 m_menus.removeOne(menu);
144
146
147 // See remark in insertMenu().
148 NSInteger nativeIndex = [m_nativeMenu indexOfItem:item];
149 [m_nativeMenu removeItemAtIndex:nativeIndex];
150}
151
153{
154 syncMenu_helper(menu, false /*internaCall*/);
155}
156
158{
160
161 QCocoaMenu *cocoaMenu = static_cast<QCocoaMenu *>(menu);
162 for (QCocoaMenuItem *item : cocoaMenu->items())
163 cocoaMenu->syncMenuItem_helper(item, menubarUpdate);
164
165 BOOL shouldHide = YES;
166 if (cocoaMenu->isVisible()) {
167 // If the NSMenu has no visible items, or only separators, we should hide it
168 // on the menubar. This can happen after syncing the menu items since they
169 // can be moved to other menus.
170 for (NSMenuItem *item in cocoaMenu->nsMenu().itemArray)
171 if (!item.separatorItem && !item.hidden) {
172 shouldHide = NO;
173 break;
174 }
175 }
176
177 if (NSMenuItem *menuItem = cocoaMenu->attachedItem()) {
178 // Non-nil menu item means the item's sub menu is set
179
180 NSString *menuTitle = cocoaMenu->nsMenu().title;
181
182 // The NSMenu's title is what's visible to the user, and AppKit uses this
183 // for some of its heuristics of when to add special items to the menus,
184 // such as 'Enter Full Screen' in the View menu, the search bare in the
185 // Help menu, and the "Send App feedback to Apple" in the Help menu.
186 // This relies on the title matching AppKit's localized value from the
187 // MenuCommands table, which in turn depends on the preferredLocalizations
188 // of the AppKit bundle. We don't do any automatic translation of menu
189 // titles visible to the user, so this relies on the application developer
190 // having chosen translated titles that match AppKit's, and that the Qt
191 // preferred UI languages match AppKit's preferredLocalizations.
192
193 // In the case of the Edit menu, AppKit uses the NSMenuItem's title
194 // for its heuristics of when to add the dictation and emoji entries,
195 // and this title is not visible to the user. But like above, the
196 // heuristics are based on the localized title of the menu, so we need
197 // to ensure the title matches AppKit's localization.
198
199 // Unfortunately, the title we have at this point may have gone through
200 // Qt's i18n machinery already, via e.g. tr("Edit") in the application,
201 // in which case we don't know the context of the translation, and can't
202 // do a reverse lookup to go back to the untranslated title to pass to
203 // AppKit. As a workaround we translate the title via a our context,
204 // and document that the user needs to ensure their application matches
205 // this translation.
206 if ([menuTitle isEqual:@"Edit"] || [menuTitle isEqual:tr("Edit").toNSString()]) {
207 menuItem.title = qt_mac_AppKitString(@"InputManager", @"Edit");
208 } else {
209 // The Edit menu is the only case we know of so far, but to be on
210 // the safe side we always sync the menu title.
211 menuItem.title = menuTitle;
212 }
213
214 menuItem.hidden = shouldHide;
215 }
216}
217
218NSMenuItem *QCocoaMenuBar::nativeItemForMenu(QCocoaMenu *menu) const
219{
220 if (!menu)
221 return nil;
222
223 return [m_nativeMenu itemWithTag:reinterpret_cast<NSInteger>(menu)];
224}
225
227{
228 qCDebug(lcQpaMenus) << "Reparenting" << this << "to" << newParentWindow;
229
230 if (!m_window.isNull())
231 m_window->setMenubar(nullptr);
232
233 if (!newParentWindow) {
234 m_window.clear();
235 } else {
236 newParentWindow->create();
237 m_window = static_cast<QCocoaWindow*>(newParentWindow->handle());
238 m_window->setMenubar(this);
239 }
240
242}
243
245{
246 return m_window ? m_window->window() : nullptr;
247}
248
249
250QCocoaWindow *QCocoaMenuBar::findWindowForMenubar()
251{
252 if (qApp->focusWindow())
253 return static_cast<QCocoaWindow*>(qApp->focusWindow()->handle());
254
255 return nullptr;
256}
257
258QCocoaMenuBar *QCocoaMenuBar::findGlobalMenubar()
259{
260 for (auto *menubar : std::as_const(static_menubars)) {
261 if (menubar->m_window.isNull())
262 return menubar;
263 }
264
265 return nullptr;
266}
267
269{
271 QCocoaMenuBar *mb = findGlobalMenubar();
272 QCocoaWindow *cw = findWindowForMenubar();
273
274 QWindow *win = cw ? cw->window() : nullptr;
275 if (win && (win->flags() & Qt::Popup) == Qt::Popup) {
276 // context menus, comboboxes, etc. don't need to update the menubar,
277 // but if an application has only Qt::Tool window(s) on start,
278 // we still have to update the menubar.
279 if ((win->flags() & Qt::WindowType_Mask) != Qt::Tool)
280 return;
281 NSApplication *app = [NSApplication sharedApplication];
282 if (![app.delegate isKindOfClass:[QCocoaApplicationDelegate class]])
283 return;
284 // We apply this logic _only_ during the startup.
285 QCocoaApplicationDelegate *appDelegate = app.delegate;
286 if (!appDelegate.inLaunch)
287 return;
288 }
289
290 if (cw && cw->menubar())
291 mb = cw->menubar();
292
293 if (!mb)
294 return;
295
296 qCDebug(lcQpaMenus) << "Updating" << mb << "immediately for" << cw;
297
298 bool disableForModal = mb->shouldDisable(cw);
299
300 for (auto menu : std::as_const(mb->m_menus)) {
301 if (!menu)
302 continue;
303 NSMenuItem *item = mb->nativeItemForMenu(menu);
304 menu->setAttachedItem(item);
305 menu->setMenuParent(mb);
306 // force a sync?
307 mb->syncMenu_helper(menu, true /*menubarUpdate*/);
308 menu->propagateEnabledState(!disableForModal);
309 }
310
311 QCocoaMenuLoader *loader = [QCocoaMenuLoader sharedMenuLoader];
312 [loader ensureAppMenuInMenu:mb->nsMenu()];
313
314 NSMutableSet *mergedItems = [[NSMutableSet setWithCapacity:mb->merged().count()] retain];
315 for (auto mergedItem : mb->merged()) {
316 [mergedItems addObject:mergedItem->nsItem()];
317 mergedItem->syncMerged();
318 }
319
320 // hide+disable all mergeable items we're not currently using
321 for (NSMenuItem *mergeable in [loader mergeable]) {
322 if (![mergedItems containsObject:mergeable]) {
323 mergeable.hidden = YES;
324 mergeable.enabled = NO;
325 }
326 }
327
328 [mergedItems release];
329
330 NSMenu *newMainMenu = mb->nsMenu();
331 if (NSApp.mainMenu == newMainMenu) {
332 // NSApplication triggers _customizeMainMenu when the menu
333 // changes, which takes care of adding text input items to
334 // the edit menu e.g., but this doesn't happen if the menu
335 // is the same. In our case we might be re-using an existing
336 // menu, but the menu might have new sub menus that need to
337 // be customized. To ensure NSApplication does the right
338 // thing we reset the main menu first.
339 qCDebug(lcQpaMenus) << "Clearing main menu temporarily";
340 NSApp.mainMenu = nil;
341 }
342 NSApp.mainMenu = newMainMenu;
343
345 [loader qtTranslateApplicationMenu];
346}
347
349{
350 // For such an item/menu we get for 'free' an additional feature -
351 // a list of windows the application has created in the Dock's menu.
352
353 NSApplication *app = NSApplication.sharedApplication;
354 if (app.windowsMenu)
355 return;
356
357 NSMenu *mainMenu = app.mainMenu;
358 NSMenuItem *winMenuItem = [[[NSMenuItem alloc] initWithTitle:@"QtWindowMenu"
359 action:nil keyEquivalent:@""] autorelease];
360 // We don't want to show this menu, nobody asked us to do so:
361 winMenuItem.hidden = YES;
362
363 winMenuItem.submenu = [[[NSMenu alloc] initWithTitle:@"QtWindowMenu"] autorelease];
364
365 // AppKit has a bug in [NSApplication setWindowsMenu:] where it will resolve
366 // the last item of the window menu's itemArray, but not account for the array
367 // being empty, resulting in a lookup of itemAtIndex:-1. To work around this,
368 // we insert a hidden dummy item into the menu. See FB13369198.
369 auto *dummyItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
370 dummyItem.hidden = YES;
371 [winMenuItem.submenu addItem:[dummyItem autorelease]];
372
373 [mainMenu insertItem:winMenuItem atIndex:mainMenu.itemArray.count];
374 app.windowsMenu = winMenuItem.submenu;
375
376 // Windows that have already been ordered in at this point have already been
377 // evaluated by AppKit via _addToWindowsMenuIfNecessary and added to the menu,
378 // but since the menu didn't exist at that point the addition was a noop.
379 // Instead of trying to duplicate the logic AppKit uses for deciding if
380 // a window should be part of the Window menu we toggle one of the settings
381 // that definitely will affect this, which results in AppKit reevaluating the
382 // situation and adding the window to the menu if necessary.
383 for (NSWindow *win in app.windows) {
384 win.excludedFromWindowsMenu = !win.excludedFromWindowsMenu;
385 win.excludedFromWindowsMenu = !win.excludedFromWindowsMenu;
386 }
387}
388
389QList<QCocoaMenuItem*> QCocoaMenuBar::merged() const
390{
391 QList<QCocoaMenuItem*> r;
392 for (auto menu : std::as_const(m_menus)) {
393 if (!menu)
394 continue;
395 r.append(menu->merged());
396 }
397
398 return r;
399}
400
401bool QCocoaMenuBar::shouldDisable(QCocoaWindow *active) const
402{
403 if (active && (active->window()->modality() == Qt::NonModal))
404 return false;
405
406 if (m_window == active) {
407 // modal window owns us, we should be enabled!
408 return false;
409 }
410
411 QWindowList topWindows(qApp->topLevelWindows());
412 // When there is an application modal window on screen, the entries of
413 // the menubar should be disabled. The exception in Qt is that if the
414 // modal window is the only window on screen, then we enable the menu bar.
415 for (auto *window : std::as_const(topWindows)) {
416 if (window->isVisible() && window->modality() == Qt::ApplicationModal) {
417 // check for other visible windows
418 for (auto *other : std::as_const(topWindows)) {
419 if ((window != other) && (other->isVisible())) {
420 // INVARIANT: we found another visible window
421 // on screen other than our modalWidget. We therefore
422 // disable the menu bar to follow normal modality logic:
423 return true;
424 }
425 }
426
427 // INVARIANT: We have only one window on screen that happends
428 // to be application modal. We choose to enable the menu bar
429 // in that case to e.g. enable the quit menu item.
430 return false;
431 }
432 }
433
434 return true;
435}
436
438{
439 for (auto menu : std::as_const(m_menus))
440 if (menu && menu->tag() == tag)
441 return menu;
442
443 return nullptr;
444}
445
447{
448 for (auto menu : std::as_const(m_menus)) {
449 if (menu) {
450 for (auto *item : menu->items())
451 if (item->effectiveRole() == role)
452 return item->nsItem();
453 }
454 }
455
456 return nil;
457}
458
460{
461 return m_window.data();
462}
463
465
466#include "moc_qcocoamenubar.cpp"
static bool isEqual(const aiUVTransform &a, const aiUVTransform &b)
QList< QCocoaMenuItem * > merged() const
NSMenuItem * itemForRole(QPlatformMenuItem::MenuRole role)
void removeMenu(QPlatformMenu *menu) override
static void updateMenuBarImmediately()
void syncMenu_helper(QPlatformMenu *menu, bool menubarUpdate)
void syncMenu(QPlatformMenu *menuItem) override
void handleReparent(QWindow *newParentWindow) override
void insertMenu(QPlatformMenu *menu, QPlatformMenu *before) override
QCocoaWindow * cocoaWindow() const
QWindow * parentWindow() const override
static void insertWindowMenu()
QPlatformMenu * menuForTag(quintptr tag) const override
void syncMenuItem_helper(QPlatformMenuItem *menuItem, bool menubarUpdate)
void setMenubar(QCocoaMenuBar *mb)
QCocoaMenuBar * menubar() const
static QGuiApplicationPrivate * instance()
static QWindow * focusWindow()
Returns the QWindow that receives events tied to focus, such as key events.
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
void append(parameter_type t)
Definition qlist.h:458
const QObjectList & children() const
Returns a list of child objects.
Definition qobject.h:201
QWindow * window() const
Returns the window which belongs to the QPlatformWindow.
void clear() noexcept
Definition qpointer.h:87
T * data() const noexcept
Definition qpointer.h:73
bool isNull() const noexcept
Definition qpointer.h:84
\inmodule QtGui
Definition qwindow.h:63
Qt::WindowModality modality
the modality of the window
Definition qwindow.h:78
qDeleteAll(list.begin(), list.end())
Combined button and popup list for selecting options.
@ NonModal
@ ApplicationModal
@ Popup
Definition qnamespace.h:211
@ WindowType_Mask
Definition qnamespace.h:220
@ Tool
Definition qnamespace.h:212
NSString * qt_mac_AppKitString(NSString *table, NSString *key)
static QT_BEGIN_NAMESPACE QList< QCocoaMenuBar * > static_menubars
long NSInteger
NSMenu QCocoaMenu * platformMenu
#define qApp
AudioChannelLayoutTag tag
#define qCWarning(category,...)
#define qCDebug(category,...)
GLboolean r
[2]
GLuint in
static const struct TessellationWindingOrderTab cw[]
#define tr(X)
static QT_BEGIN_NAMESPACE void init(QTextBoundaryFinder::BoundaryType type, QStringView str, QCharAttributes *attributes)
size_t quintptr
Definition qtypes.h:167
QWidget * win
Definition settings.cpp:6
sem release()
QSharedPointer< T > other(t)
[5]
scene addItem(form)
QGraphicsItem * item
QApplication app(argc, argv)
[0]
aWidget window() -> setWindowTitle("New Window Title")
[2]
QMenu menu
[5]
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