1/****************************************************************************
2**
3** Copyright (C) 2018 The Qt Company Ltd.
4** Copyright (C) 2012 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author James Turner <james.turner@kdab.com>
5** Contact: https://www.qt.io/licensing/
6**
7** This file is part of the plugins of the Qt Toolkit.
8**
9** $QT_BEGIN_LICENSE:LGPL$
10** Commercial License Usage
11** Licensees holding valid commercial Qt licenses may use this file in
12** accordance with the commercial license agreement provided with the
13** Software or, alternatively, in accordance with the terms contained in
14** a written agreement between you and The Qt Company. For licensing terms
15** and conditions see https://www.qt.io/terms-conditions. For further
16** information use the contact form at https://www.qt.io/contact-us.
17**
18** GNU Lesser General Public License Usage
19** Alternatively, this file may be used under the terms of the GNU Lesser
20** General Public License version 3 as published by the Free Software
21** Foundation and appearing in the file LICENSE.LGPL3 included in the
22** packaging of this file. Please review the following information to
23** ensure the GNU Lesser General Public License version 3 requirements
24** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
25**
26** GNU General Public License Usage
27** Alternatively, this file may be used under the terms of the GNU
28** General Public License version 2.0 or (at your option) the GNU General
29** Public license version 3 or any later version approved by the KDE Free
30** Qt Foundation. The licenses are as published by the Free Software
31** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
32** included in the packaging of this file. Please review the following
33** information to ensure the GNU General Public License requirements will
34** be met: https://www.gnu.org/licenses/gpl-2.0.html and
35** https://www.gnu.org/licenses/gpl-3.0.html.
36**
37** $QT_END_LICENSE$
38**
39****************************************************************************/
40
41#include <AppKit/AppKit.h>
42
43#include "qcocoamenubar.h"
44#include "qcocoawindow.h"
45#include "qcocoamenuloader.h"
46#include "qcocoaapplication.h" // for custom application category
47#include "qcocoaapplicationdelegate.h"
48
49#include <QtGui/QGuiApplication>
50#include <QtCore/QDebug>
51
52QT_BEGIN_NAMESPACE
53
54static QList<QCocoaMenuBar*> static_menubars;
55
56QCocoaMenuBar::QCocoaMenuBar()
57{
58    static_menubars.append(this);
59
60    m_nativeMenu = [[NSMenu alloc] init];
61#ifdef QT_COCOA_ENABLE_MENU_DEBUG
62    qDebug() << "Construct QCocoaMenuBar" << this << m_nativeMenu;
63#endif
64}
65
66QCocoaMenuBar::~QCocoaMenuBar()
67{
68#ifdef QT_COCOA_ENABLE_MENU_DEBUG
69    qDebug() << "~QCocoaMenuBar" << this;
70#endif
71    for (auto menu : qAsConst(m_menus)) {
72        if (!menu)
73            continue;
74        NSMenuItem *item = nativeItemForMenu(menu);
75        if (menu->attachedItem() == item)
76            menu->setAttachedItem(nil);
77    }
78
79    [m_nativeMenu release];
80    static_menubars.removeOne(this);
81
82    if (!m_window.isNull() && m_window->menubar() == this) {
83        m_window->setMenubar(nullptr);
84
85        // Delete the children first so they do not cause
86        // the native menu items to be hidden after
87        // the menu bar was updated
88        qDeleteAll(children());
89        updateMenuBarImmediately();
90    }
91}
92
93bool QCocoaMenuBar::needsImmediateUpdate()
94{
95    if (!m_window.isNull()) {
96        if (m_window->window()->isActive())
97            return true;
98    } else {
99        // Only update if the focus/active window has no
100        // menubar, which means it'll be using this menubar.
101        // This is to avoid a modification in a parentless
102        // menubar to affect a window-assigned menubar.
103        QWindow *fw = QGuiApplication::focusWindow();
104        if (!fw) {
105            // Same if there's no focus window, BTW.
106            return true;
107        } else {
108            QCocoaWindow *cw = static_cast<QCocoaWindow *>(fw->handle());
109            if (cw && !cw->menubar())
110                return true;
111        }
112    }
113
114    // Either the menubar is attached to a non-active window,
115    // or the application's focus window has its own menubar
116    // (which is different from this one)
117    return false;
118}
119
120void QCocoaMenuBar::insertMenu(QPlatformMenu *platformMenu, QPlatformMenu *before)
121{
122    QCocoaMenu *menu = static_cast<QCocoaMenu *>(platformMenu);
123    QCocoaMenu *beforeMenu = static_cast<QCocoaMenu *>(before);
124#ifdef QT_COCOA_ENABLE_MENU_DEBUG
125    qDebug() << "QCocoaMenuBar" << this << "insertMenu" << menu << "before" << before;
126#endif
127
128    if (m_menus.contains(QPointer<QCocoaMenu>(menu))) {
129        qWarning("This menu already belongs to the menubar, remove it first");
130        return;
131    }
132
133    if (beforeMenu && !m_menus.contains(QPointer<QCocoaMenu>(beforeMenu))) {
134        qWarning("The before menu does not belong to the menubar");
135        return;
136    }
137
138    int insertionIndex = beforeMenu ? m_menus.indexOf(beforeMenu) : m_menus.size();
139    m_menus.insert(insertionIndex, menu);
140
141    {
142        QMacAutoReleasePool pool;
143        NSMenuItem *item = [[[NSMenuItem alloc] init] autorelease];
144        item.tag = reinterpret_cast<NSInteger>(menu);
145
146        if (beforeMenu) {
147            // QMenuBar::toNSMenu() exposes the native menubar and
148            // the user could have inserted its own items in there.
149            // Same remark applies to removeMenu().
150            NSMenuItem *beforeItem = nativeItemForMenu(beforeMenu);
151            NSInteger nativeIndex = [m_nativeMenu indexOfItem:beforeItem];
152            [m_nativeMenu insertItem:item atIndex:nativeIndex];
153        } else {
154            [m_nativeMenu addItem:item];
155        }
156    }
157
158    syncMenu_helper(menu, false /*internaCall*/);
159
160    if (needsImmediateUpdate())
161        updateMenuBarImmediately();
162}
163
164void QCocoaMenuBar::removeMenu(QPlatformMenu *platformMenu)
165{
166    QCocoaMenu *menu = static_cast<QCocoaMenu *>(platformMenu);
167    if (!m_menus.contains(menu)) {
168        qWarning("Trying to remove a menu that does not belong to the menubar");
169        return;
170    }
171
172    NSMenuItem *item = nativeItemForMenu(menu);
173    if (menu->attachedItem() == item)
174        menu->setAttachedItem(nil);
175    m_menus.removeOne(menu);
176
177    QMacAutoReleasePool pool;
178
179    // See remark in insertMenu().
180    NSInteger nativeIndex = [m_nativeMenu indexOfItem:item];
181    [m_nativeMenu removeItemAtIndex:nativeIndex];
182}
183
184void QCocoaMenuBar::syncMenu(QPlatformMenu *menu)
185{
186    syncMenu_helper(menu, false /*internaCall*/);
187}
188
189void QCocoaMenuBar::syncMenu_helper(QPlatformMenu *menu, bool menubarUpdate)
190{
191    QMacAutoReleasePool pool;
192
193    QCocoaMenu *cocoaMenu = static_cast<QCocoaMenu *>(menu);
194    for (QCocoaMenuItem *item : cocoaMenu->items())
195        cocoaMenu->syncMenuItem_helper(item, menubarUpdate);
196
197    BOOL shouldHide = YES;
198    if (cocoaMenu->isVisible()) {
199        // If the NSMenu has no visble items, or only separators, we should hide it
200        // on the menubar. This can happen after syncing the menu items since they
201        // can be moved to other menus.
202        for (NSMenuItem *item in cocoaMenu->nsMenu().itemArray)
203            if (!item.separatorItem && !item.hidden) {
204                shouldHide = NO;
205                break;
206            }
207    }
208
209    if (NSMenuItem *attachedItem = cocoaMenu->attachedItem()) {
210        // Non-nil attached item means the item's submenu is set
211        attachedItem.title = cocoaMenu->nsMenu().title;
212        attachedItem.hidden = shouldHide;
213    }
214}
215
216NSMenuItem *QCocoaMenuBar::nativeItemForMenu(QCocoaMenu *menu) const
217{
218    if (!menu)
219        return nil;
220
221    return [m_nativeMenu itemWithTag:reinterpret_cast<NSInteger>(menu)];
222}
223
224void QCocoaMenuBar::handleReparent(QWindow *newParentWindow)
225{
226#ifdef QT_COCOA_ENABLE_MENU_DEBUG
227    qDebug() << "QCocoaMenuBar" << this << "handleReparent" << newParentWindow;
228#endif
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
241    updateMenuBarImmediately();
242}
243
244QWindow *QCocoaMenuBar::parentWindow() const
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 : qAsConst(static_menubars)) {
261        if (menubar->m_window.isNull())
262            return menubar;
263    }
264
265    return nullptr;
266}
267
268void QCocoaMenuBar::updateMenuBarImmediately()
269{
270    QMacAutoReleasePool pool;
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#ifdef QT_COCOA_ENABLE_MENU_DEBUG
297    qDebug() << "QCocoaMenuBar" << "updateMenuBarImmediately" << cw;
298#endif
299    bool disableForModal = mb->shouldDisable(cw);
300
301    for (auto menu : qAsConst(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()];
331    [loader qtTranslateApplicationMenu];
332}
333
334QList<QCocoaMenuItem*> QCocoaMenuBar::merged() const
335{
336    QList<QCocoaMenuItem*> r;
337    for (auto menu : qAsConst(m_menus))
338        r.append(menu->merged());
339
340    return r;
341}
342
343bool QCocoaMenuBar::shouldDisable(QCocoaWindow *active) const
344{
345    if (active && (active->window()->modality() == Qt::NonModal))
346        return false;
347
348    if (m_window == active) {
349        // modal window owns us, we should be enabled!
350        return false;
351    }
352
353    QWindowList topWindows(qApp->topLevelWindows());
354    // When there is an application modal window on screen, the entries of
355    // the menubar should be disabled. The exception in Qt is that if the
356    // modal window is the only window on screen, then we enable the menu bar.
357    for (auto *window : qAsConst(topWindows)) {
358        if (window->isVisible() && window->modality() == Qt::ApplicationModal) {
359            // check for other visible windows
360            for (auto *other : qAsConst(topWindows)) {
361                if ((window != other) && (other->isVisible())) {
362                    // INVARIANT: we found another visible window
363                    // on screen other than our modalWidget. We therefore
364                    // disable the menu bar to follow normal modality logic:
365                    return true;
366                }
367            }
368
369            // INVARIANT: We have only one window on screen that happends
370            // to be application modal. We choose to enable the menu bar
371            // in that case to e.g. enable the quit menu item.
372            return false;
373        }
374    }
375
376    return true;
377}
378
379QPlatformMenu *QCocoaMenuBar::menuForTag(quintptr tag) const
380{
381    for (auto menu : qAsConst(m_menus))
382        if (menu->tag() ==  tag)
383            return menu;
384
385    return nullptr;
386}
387
388NSMenuItem *QCocoaMenuBar::itemForRole(QPlatformMenuItem::MenuRole role)
389{
390    for (auto menu : qAsConst(m_menus))
391        for (auto *item : menu->items())
392            if (item->effectiveRole() == role)
393                return item->nsItem();
394
395    return nil;
396}
397
398QCocoaWindow *QCocoaMenuBar::cocoaWindow() const
399{
400    return m_window.data();
401}
402
403QT_END_NAMESPACE
404
405#include "moc_qcocoamenubar.cpp"
406