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