Qt 6.x
The Qt SDK
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
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, ^{
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 static const NSBundle *appKit = [NSBundle bundleForClass:NSApplication.class];
208 menuItem.title = [appKit localizedStringForKey:@"Edit" value:menuTitle table:@"InputManager"];
209 } else {
210 // The Edit menu is the only case we know of so far, but to be on
211 // the safe side we always sync the menu title.
212 menuItem.title = menuTitle;
213 }
214
215 menuItem.hidden = shouldHide;
216 }
217}
218
219NSMenuItem *QCocoaMenuBar::nativeItemForMenu(QCocoaMenu *menu) const
220{
221 if (!menu)
222 return nil;
223
224 return [m_nativeMenu itemWithTag:reinterpret_cast<NSInteger>(menu)];
225}
226
228{
229 qCDebug(lcQpaMenus) << "Reparenting" << this << "to" << newParentWindow;
230
231 if (!m_window.isNull())
232 m_window->setMenubar(nullptr);
233
234 if (!newParentWindow) {
235 m_window.clear();
236 } else {
237 newParentWindow->create();
238 m_window = static_cast<QCocoaWindow*>(newParentWindow->handle());
239 m_window->setMenubar(this);
240 }
241
243}
244
246{
247 return m_window ? m_window->window() : nullptr;
248}
249
250
251QCocoaWindow *QCocoaMenuBar::findWindowForMenubar()
252{
253 if (qApp->focusWindow())
254 return static_cast<QCocoaWindow*>(qApp->focusWindow()->handle());
255
256 return nullptr;
257}
258
259QCocoaMenuBar *QCocoaMenuBar::findGlobalMenubar()
260{
261 for (auto *menubar : std::as_const(static_menubars)) {
262 if (menubar->m_window.isNull())
263 return menubar;
264 }
265
266 return nullptr;
267}
268
270{
272 QCocoaMenuBar *mb = findGlobalMenubar();
273 QCocoaWindow *cw = findWindowForMenubar();
274
275 QWindow *win = cw ? cw->window() : nullptr;
276 if (win && (win->flags() & Qt::Popup) == Qt::Popup) {
277 // context menus, comboboxes, etc. don't need to update the menubar,
278 // but if an application has only Qt::Tool window(s) on start,
279 // we still have to update the menubar.
280 if ((win->flags() & Qt::WindowType_Mask) != Qt::Tool)
281 return;
282 NSApplication *app = [NSApplication sharedApplication];
283 if (![app.delegate isKindOfClass:[QCocoaApplicationDelegate class]])
284 return;
285 // We apply this logic _only_ during the startup.
286 QCocoaApplicationDelegate *appDelegate = app.delegate;
287 if (!appDelegate.inLaunch)
288 return;
289 }
290
291 if (cw && cw->menubar())
292 mb = cw->menubar();
293
294 if (!mb)
295 return;
296
297 qCDebug(lcQpaMenus) << "Updating" << mb << "immediately for" << cw;
298
299 bool disableForModal = mb->shouldDisable(cw);
300
301 for (auto menu : std::as_const(mb->m_menus)) {
302 if (!menu)
303 continue;
304 NSMenuItem *item = mb->nativeItemForMenu(menu);
305 menu->setAttachedItem(item);
306 menu->setMenuParent(mb);
307 // force a sync?
308 mb->syncMenu_helper(menu, true /*menubarUpdate*/);
309 menu->propagateEnabledState(!disableForModal);
310 }
311
312 QCocoaMenuLoader *loader = [QCocoaMenuLoader sharedMenuLoader];
313 [loader ensureAppMenuInMenu:mb->nsMenu()];
314
315 NSMutableSet *mergedItems = [[NSMutableSet setWithCapacity:mb->merged().count()] retain];
316 for (auto mergedItem : mb->merged()) {
317 [mergedItems addObject:mergedItem->nsItem()];
318 mergedItem->syncMerged();
319 }
320
321 // hide+disable all mergeable items we're not currently using
322 for (NSMenuItem *mergeable in [loader mergeable]) {
323 if (![mergedItems containsObject:mergeable]) {
324 mergeable.hidden = YES;
325 mergeable.enabled = NO;
326 }
327 }
328
329 [mergedItems release];
330 [NSApp setMainMenu:mb->nsMenu()];
332 [loader qtTranslateApplicationMenu];
333}
334
336{
337 // For such an item/menu we get for 'free' an additional feature -
338 // a list of windows the application has created in the Dock's menu.
339
340 NSApplication *app = NSApplication.sharedApplication;
341 if (app.windowsMenu)
342 return;
343
344 NSMenu *mainMenu = app.mainMenu;
345 NSMenuItem *winMenuItem = [[[NSMenuItem alloc] initWithTitle:@"QtWindowMenu"
346 action:nil keyEquivalent:@""] autorelease];
347 // We don't want to show this menu, nobody asked us to do so:
348 winMenuItem.hidden = YES;
349
350 winMenuItem.submenu = [[[NSMenu alloc] initWithTitle:@"QtWindowMenu"] autorelease];
351 [mainMenu insertItem:winMenuItem atIndex:mainMenu.itemArray.count];
352 app.windowsMenu = winMenuItem.submenu;
353
354 // Windows that have already been ordered in at this point have already been
355 // evaluated by AppKit via _addToWindowsMenuIfNecessary and added to the menu,
356 // but since the menu didn't exist at that point the addition was a noop.
357 // Instead of trying to duplicate the logic AppKit uses for deciding if
358 // a window should be part of the Window menu we toggle one of the settings
359 // that definitely will affect this, which results in AppKit reevaluating the
360 // situation and adding the window to the menu if necessary.
361 for (NSWindow *win in app.windows) {
362 win.excludedFromWindowsMenu = !win.excludedFromWindowsMenu;
363 win.excludedFromWindowsMenu = !win.excludedFromWindowsMenu;
364 }
365}
366
368{
370 for (auto menu : std::as_const(m_menus)) {
371 if (!menu)
372 continue;
373 r.append(menu->merged());
374 }
375
376 return r;
377}
378
379bool QCocoaMenuBar::shouldDisable(QCocoaWindow *active) const
380{
381 if (active && (active->window()->modality() == Qt::NonModal))
382 return false;
383
384 if (m_window == active) {
385 // modal window owns us, we should be enabled!
386 return false;
387 }
388
389 QWindowList topWindows(qApp->topLevelWindows());
390 // When there is an application modal window on screen, the entries of
391 // the menubar should be disabled. The exception in Qt is that if the
392 // modal window is the only window on screen, then we enable the menu bar.
393 for (auto *window : std::as_const(topWindows)) {
394 if (window->isVisible() && window->modality() == Qt::ApplicationModal) {
395 // check for other visible windows
396 for (auto *other : std::as_const(topWindows)) {
397 if ((window != other) && (other->isVisible())) {
398 // INVARIANT: we found another visible window
399 // on screen other than our modalWidget. We therefore
400 // disable the menu bar to follow normal modality logic:
401 return true;
402 }
403 }
404
405 // INVARIANT: We have only one window on screen that happends
406 // to be application modal. We choose to enable the menu bar
407 // in that case to e.g. enable the quit menu item.
408 return false;
409 }
410 }
411
412 return true;
413}
414
416{
417 for (auto menu : std::as_const(m_menus))
418 if (menu && menu->tag() == tag)
419 return menu;
420
421 return nullptr;
422}
423
425{
426 for (auto menu : std::as_const(m_menus)) {
427 if (menu) {
428 for (auto *item : menu->items())
429 if (item->effectiveRole() == role)
430 return item->nsItem();
431 }
432 }
433
434 return nil;
435}
436
438{
439 return m_window.data();
440}
441
443
444#include "moc_qcocoamenubar.cpp"
static bool isEqual(const aiUVTransform &a, const aiUVTransform &b)
NSMenu * nsMenu() const override
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
bool isVisible() const
Definition qcocoamenu.h:48
QList< QCocoaMenuItem * > items() const
NSMenuItem * attachedItem() const
void syncMenuItem_helper(QPlatformMenuItem *menuItem, bool menubarUpdate)
NSMenu * nsMenu() const override
Definition qcocoamenu.mm:69
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.
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
qsizetype count() const noexcept
Definition qlist.h:387
const QObjectList & children() const
Returns a list of child objects.
Definition qobject.h:171
QWindow * window() const
Returns the window which belongs to the QPlatformWindow.
\inmodule QtCore
Definition qpointer.h:18
void clear()
Definition qpointer.h:70
T * data() const
Definition qpointer.h:56
bool isNull() const
Returns true if the referenced object has been destroyed or if there is no referenced object; otherwi...
Definition qpointer.h:67
\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:210
@ WindowType_Mask
Definition qnamespace.h:219
@ Tool
Definition qnamespace.h:211
static QT_BEGIN_NAMESPACE QList< QCocoaMenuBar * > static_menubars
long NSInteger
NSMenu QCocoaMenu * platformMenu
#define qApp
AudioChannelLayoutTag tag
EGLOutputLayerEXT EGLint EGLAttrib value
[5]
#define qCWarning(category,...)
#define qCDebug(category,...)
GLboolean r
[2]
GLuint in
GLenum GLenum GLsizei void * table
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:72
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:955
bool contains(const AT &t) const noexcept
Definition qlist.h:44