1/****************************************************************************
2**
3** Copyright (C) 2018 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the plugins of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qcocoamenuloader.h"
41
42#include "qcocoahelpers.h"
43#include "qcocoansmenu.h"
44#include "qcocoamenubar.h"
45#include "qcocoamenuitem.h"
46#include "qcocoaintegration.h"
47
48#include <QtCore/private/qcore_mac_p.h>
49#include <QtCore/private/qthread_p.h>
50#include <QtCore/qcoreapplication.h>
51#include <QtGui/private/qguiapplication_p.h>
52
53@implementation QCocoaMenuLoader {
54    NSMenu *theMenu;
55    NSMenu *appMenu;
56    NSMenuItem *quitItem;
57    NSMenuItem *preferencesItem;
58    NSMenuItem *aboutItem;
59    NSMenuItem *aboutQtItem;
60    NSMenuItem *hideItem;
61    NSMenuItem *servicesItem;
62    NSMenuItem *hideAllOthersItem;
63    NSMenuItem *showAllItem;
64}
65
66+ (instancetype)sharedMenuLoader
67{
68    static QCocoaMenuLoader *shared = nil;
69    static dispatch_once_t onceToken;
70    dispatch_once(&onceToken, ^{
71        shared = [[self alloc] init];
72        atexit_b(^{
73            [shared release];
74            shared = nil;
75        });
76    });
77    return shared;
78}
79
80- (instancetype)init
81{
82    if ((self = [super init])) {
83        NSString *appName = qt_mac_applicationName().toNSString();
84
85        // Menubar as menu. Title as set in the NIB file
86        theMenu = [[NSMenu alloc] initWithTitle:@"Main Menu"];
87
88        // Application menu. Since 10.6, the first menu
89        // is always identified as the application menu.
90        NSMenuItem *appItem = [[[NSMenuItem alloc] init] autorelease];
91        appItem.title = appName;
92        [theMenu addItem:appItem];
93        appMenu = [[NSMenu alloc] initWithTitle:appName];
94        appItem.submenu = appMenu;
95
96        // About Application
97        aboutItem = [[QCocoaNSMenuItem alloc] init];
98        aboutItem.title = [@"About " stringByAppendingString:appName];
99        // FIXME This seems useless since barely adding a QAction
100        // with AboutRole role will reset the target/action
101        aboutItem.target = self;
102        aboutItem.action = @selector(orderFrontStandardAboutPanel:);
103        // Disable until a QAction is associated
104        aboutItem.enabled = NO;
105        aboutItem.hidden = YES;
106        [appMenu addItem:aboutItem];
107
108        // About Qt (shameless self-promotion)
109        aboutQtItem = [[QCocoaNSMenuItem alloc] init];
110        aboutQtItem.title = @"About Qt";
111        // Disable until a QAction is associated
112        aboutQtItem.enabled = NO;
113        aboutQtItem.hidden = YES;
114        [appMenu addItem:aboutQtItem];
115
116        [appMenu addItem:[NSMenuItem separatorItem]];
117
118        // Preferences
119        // We'll be adding app specific items after this. The macOS HIG state that,
120        // "In general, a Preferences menu item should be the first app-specific menu item."
121        // https://developer.apple.com/macos/human-interface-guidelines/menus/menu-bar-menus/
122        preferencesItem = [[QCocoaNSMenuItem alloc] init];
123        preferencesItem.title = @"Preferences…";
124        preferencesItem.keyEquivalent = @",";
125        // Disable until a QAction is associated
126        preferencesItem.enabled = NO;
127        preferencesItem.hidden = YES;
128        [appMenu addItem:preferencesItem];
129
130        [appMenu addItem:[NSMenuItem separatorItem]];
131
132        // Services item and menu
133        servicesItem = [[NSMenuItem alloc] init];
134        servicesItem.title = @"Services";
135        NSMenu *servicesMenu = [[[NSMenu alloc] initWithTitle:@"Services"] autorelease];
136        servicesItem.submenu = servicesMenu;
137        [NSApplication sharedApplication].servicesMenu = servicesMenu;
138        [appMenu addItem:servicesItem];
139
140        [appMenu addItem:[NSMenuItem separatorItem]];
141
142        // Hide Application
143        hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName]
144                                              action:@selector(hide:)
145                                       keyEquivalent:@"h"];
146        hideItem.target = self;
147        [appMenu addItem:hideItem];
148
149        // Hide Others
150        hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others"
151                                                       action:@selector(hideOtherApplications:)
152                                                keyEquivalent:@"h"];
153        hideAllOthersItem.target = self;
154        hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption;
155        [appMenu addItem:hideAllOthersItem];
156
157        // Show All
158        showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All"
159                                                 action:@selector(unhideAllApplications:)
160                                          keyEquivalent:@""];
161        showAllItem.target = self;
162        [appMenu addItem:showAllItem];
163
164        [appMenu addItem:[NSMenuItem separatorItem]];
165
166        // Quit Application
167        quitItem = [[QCocoaNSMenuItem alloc] init];
168        quitItem.title = [@"Quit " stringByAppendingString:appName];
169        quitItem.keyEquivalent = @"q";
170        // This will remain true until synced with a QCocoaMenuItem.
171        // This way, we will always have a functional Quit menu item
172        // even if no QAction is added.
173        quitItem.action = @selector(terminate:);
174        [appMenu addItem:quitItem];
175    }
176
177    return self;
178}
179
180- (void)dealloc
181{
182    [theMenu release];
183    [appMenu release];
184    [aboutItem release];
185    [aboutQtItem release];
186    [preferencesItem release];
187    [servicesItem release];
188    [hideItem release];
189    [hideAllOthersItem release];
190    [showAllItem release];
191    [quitItem release];
192
193    [super dealloc];
194}
195
196- (void)ensureAppMenuInMenu:(NSMenu *)menu
197{
198    // The application menu is the menu in the menu bar that contains the
199    // 'Quit' item. When changing menu bar (e.g when switching between
200    // windows with different menu bars), we never recreate this menu, but
201    // instead pull it out the current menu bar and place into the new one:
202    NSMenu *mainMenu = [NSApp mainMenu];
203    if (mainMenu == menu)
204        return; // nothing to do (menu is the current menu bar)!
205
206#ifndef QT_NAMESPACE
207    Q_ASSERT(mainMenu);
208#endif
209    // Grab the app menu out of the current menu.
210    auto unparentAppMenu = ^bool (NSMenu *supermenu) {
211        auto index = [supermenu indexOfItemWithSubmenu:appMenu];
212        if (index != -1) {
213            [supermenu removeItemAtIndex:index];
214            return true;
215        }
216        return false;
217    };
218
219    if (!mainMenu || !unparentAppMenu(mainMenu))
220        if (appMenu.supermenu)
221            unparentAppMenu(appMenu.supermenu);
222
223    NSMenuItem *appMenuItem = [[NSMenuItem alloc] initWithTitle:@"Apple"
224                               action:nil keyEquivalent:@""];
225    appMenuItem.submenu = appMenu;
226    [menu insertItem:appMenuItem atIndex:0];
227}
228
229- (NSMenu *)menu
230{
231    return [[theMenu retain] autorelease];
232}
233
234- (NSMenu *)applicationMenu
235{
236    return [[appMenu retain] autorelease];
237}
238
239- (NSMenuItem *)quitMenuItem
240{
241    return [[quitItem retain] autorelease];
242}
243
244- (NSMenuItem *)preferencesMenuItem
245{
246    return [[preferencesItem retain] autorelease];
247}
248
249- (NSMenuItem *)aboutMenuItem
250{
251    return [[aboutItem retain] autorelease];
252}
253
254- (NSMenuItem *)aboutQtMenuItem
255{
256    return [[aboutQtItem retain] autorelease];
257}
258
259- (NSMenuItem *)hideMenuItem
260{
261    return [[hideItem retain] autorelease];
262}
263
264- (NSMenuItem *)appSpecificMenuItem:(QCocoaMenuItem *)platformItem
265{
266    // No reason to create the item if it already exists.
267    for (NSMenuItem *item in appMenu.itemArray)
268        if (qt_objc_cast<QCocoaNSMenuItem *>(item).platformMenuItem == platformItem)
269            return item;
270
271    // Create an App-Specific menu item, insert it into the menu and return
272    // it as an autorelease item.
273    QCocoaNSMenuItem *item;
274    if (platformItem->isSeparator())
275        item = [QCocoaNSMenuItem separatorItemWithPlatformMenuItem:platformItem];
276    else
277        item = [[[QCocoaNSMenuItem alloc] initWithPlatformMenuItem:platformItem] autorelease];
278
279    const auto location = [self indexOfLastAppSpecificMenuItem];
280    [appMenu insertItem:item atIndex:NSInteger(location) + 1];
281
282    return item;
283}
284
285- (void)orderFrontStandardAboutPanel:(id)sender
286{
287    [NSApp orderFrontStandardAboutPanel:sender];
288}
289
290- (void)hideOtherApplications:(id)sender
291{
292    [NSApp hideOtherApplications:sender];
293}
294
295- (void)unhideAllApplications:(id)sender
296{
297    [NSApp unhideAllApplications:sender];
298}
299
300- (void)hide:(id)sender
301{
302    [NSApp hide:sender];
303}
304
305- (void)qtTranslateApplicationMenu
306{
307#ifndef QT_NO_TRANSLATION
308    aboutItem.title = qt_mac_applicationmenu_string(AboutAppMenuItem).arg(qt_mac_applicationName()).toNSString();
309    preferencesItem.title = qt_mac_applicationmenu_string(PreferencesAppMenuItem).toNSString();
310    servicesItem.title = qt_mac_applicationmenu_string(ServicesAppMenuItem).toNSString();
311    hideItem.title = qt_mac_applicationmenu_string(HideAppMenuItem).arg(qt_mac_applicationName()).toNSString();
312    hideAllOthersItem.title = qt_mac_applicationmenu_string(HideOthersAppMenuItem).toNSString();
313    showAllItem.title = qt_mac_applicationmenu_string(ShowAllAppMenuItem).toNSString();
314    quitItem.title = qt_mac_applicationmenu_string(QuitAppMenuItem).arg(qt_mac_applicationName()).toNSString();
315#endif
316}
317
318- (BOOL)validateMenuItem:(NSMenuItem*)menuItem
319{
320    if (menuItem.action == @selector(hideOtherApplications:)
321        || menuItem.action == @selector(unhideAllApplications:))
322        return [NSApp validateMenuItem:menuItem];
323
324    if (menuItem.action == @selector(hide:)) {
325        auto *w = QCocoaIntegration::instance()->activePopupWindow();
326        if (w && (w->window()->type() != Qt::ToolTip))
327            return NO;
328        return [NSApp validateMenuItem:menuItem];
329    }
330
331    return menuItem.enabled;
332}
333
334- (NSArray<NSMenuItem *> *)mergeable
335{
336    // Don't include the quitItem here, since we want it always visible and enabled regardless
337    auto items = [NSArray arrayWithObjects:preferencesItem, aboutItem,  aboutQtItem,
338                  appMenu.itemArray[[self indexOfLastAppSpecificMenuItem]], nil];
339    return items;
340}
341
342- (NSUInteger)indexOfLastAppSpecificMenuItem
343{
344    // Either the 'Preferences', which is the first app specific menu item, or something
345    // else we appended later (thus the reverse order):
346    const auto location = [appMenu.itemArray indexOfObjectWithOptions:NSEnumerationReverse
347                           passingTest:^BOOL(NSMenuItem *item, NSUInteger, BOOL *) {
348                               if (auto qtItem = qt_objc_cast<QCocoaNSMenuItem*>(item))
349                                   return qtItem != quitItem;
350                              return NO;
351                           }];
352    Q_ASSERT(location != NSNotFound);
353    return location;
354}
355
356
357@end
358