Cocoa: application menu items sometimes get duplicated
[profile/ivi/qtbase.git] / src / plugins / platforms / cocoa / qcocoamenu.mm
1 /****************************************************************************
2 **
3 ** Copyright (C) 2012 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author James Turner <james.turner@kdab.com>
4 ** Contact: http://www.qt-project.org/legal
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 Digia.  For licensing terms and
14 ** conditions see http://qt.digia.com/licensing.  For further information
15 ** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL included in the
21 ** packaging of this file.  Please review the following information to
22 ** ensure the GNU Lesser General Public License version 2.1 requirements
23 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
24 **
25 ** In addition, as a special exception, Digia gives you certain additional
26 ** rights.  These rights are described in the Digia Qt LGPL Exception
27 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
28 **
29 ** GNU General Public License Usage
30 ** Alternatively, this file may be used under the terms of the GNU
31 ** General Public License version 3.0 as published by the Free Software
32 ** Foundation and appearing in the file LICENSE.GPL included in the
33 ** packaging of this file.  Please review the following information to
34 ** ensure the GNU General Public License version 3.0 requirements will be
35 ** met: http://www.gnu.org/copyleft/gpl.html.
36 **
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41
42 #include "qcocoamenu.h"
43
44 #include "qcocoahelpers.h"
45 #include "qcocoaautoreleasepool.h"
46
47 #include <QtCore/QtDebug>
48 #include "qcocoaapplication.h"
49 #include "qcocoamenuloader.h"
50
51 static inline QT_MANGLE_NAMESPACE(QCocoaMenuLoader) *getMenuLoader()
52 {
53     return [NSApp QT_MANGLE_NAMESPACE(qt_qcocoamenuLoader)];
54 }
55
56 @interface QT_MANGLE_NAMESPACE(QCocoaMenuDelegate) : NSObject <NSMenuDelegate> {
57     QCocoaMenu *m_menu;
58 }
59
60 - (id) initWithMenu:(QCocoaMenu*) m;
61
62 @end
63
64 @implementation QT_MANGLE_NAMESPACE(QCocoaMenuDelegate)
65
66 - (id) initWithMenu:(QCocoaMenu*) m
67 {
68     if ((self = [super init]))
69         m_menu = m;
70
71     return self;
72 }
73
74 - (void) menuWillOpen:(NSMenu*)m
75 {
76     Q_UNUSED(m);
77     emit m_menu->aboutToShow();
78 }
79
80 - (void) menuDidClose:(NSMenu*)m
81 {
82     Q_UNUSED(m);
83     // wrong, but it's the best we can do
84     emit m_menu->aboutToHide();
85 }
86
87 - (void) itemFired:(NSMenuItem*) item
88 {
89     QCocoaMenuItem *cocoaItem = reinterpret_cast<QCocoaMenuItem *>([item tag]);
90     cocoaItem->activated();
91 }
92
93 - (BOOL)validateMenuItem:(NSMenuItem*)menuItem
94 {
95     if (![menuItem tag])
96         return YES;
97
98
99     QCocoaMenuItem* cocoaItem = reinterpret_cast<QCocoaMenuItem *>([menuItem tag]);
100     return cocoaItem->isEnabled();
101 }
102
103 @end
104
105 QT_BEGIN_NAMESPACE
106
107 QCocoaMenu::QCocoaMenu() :
108     m_enabled(true),
109     m_tag(0)
110 {
111     m_delegate = [[QT_MANGLE_NAMESPACE(QCocoaMenuDelegate) alloc] initWithMenu:this];
112     m_nativeItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
113     m_nativeMenu = [[NSMenu alloc] initWithTitle:@"Untitled"];
114     [m_nativeMenu setAutoenablesItems:YES];
115     m_nativeMenu.delegate = (QT_MANGLE_NAMESPACE(QCocoaMenuDelegate) *) m_delegate;
116     [m_nativeItem setSubmenu:m_nativeMenu];
117 }
118
119 QCocoaMenu::~QCocoaMenu()
120 {
121     QCocoaAutoReleasePool pool;
122     [m_nativeItem setSubmenu:nil];
123     [m_nativeMenu release];
124     [m_delegate release];
125     [m_nativeItem release];
126 }
127
128 void QCocoaMenu::setText(const QString &text)
129 {
130     QCocoaAutoReleasePool pool;
131     QString stripped = qt_mac_removeAmpersandEscapes(text);
132     [m_nativeMenu setTitle:QCFString::toNSString(stripped)];
133     [m_nativeItem setTitle:QCFString::toNSString(stripped)];
134 }
135
136 void QCocoaMenu::insertMenuItem(QPlatformMenuItem *menuItem, QPlatformMenuItem *before)
137 {
138     QCocoaAutoReleasePool pool;
139     QCocoaMenuItem *cocoaItem = static_cast<QCocoaMenuItem *>(menuItem);
140     QCocoaMenuItem *beforeItem = static_cast<QCocoaMenuItem *>(before);
141
142     cocoaItem->sync();
143     if (beforeItem) {
144         int index = m_menuItems.indexOf(beforeItem);
145         // if a before item is supplied, it should be in the menu
146         if (index < 0) {
147             qWarning() << Q_FUNC_INFO << "Before menu item not found";
148             return;
149         }
150         m_menuItems.insert(index, cocoaItem);
151     } else {
152         m_menuItems.append(cocoaItem);
153     }
154
155     insertNative(cocoaItem, beforeItem);
156 }
157
158 void QCocoaMenu::insertNative(QCocoaMenuItem *item, QCocoaMenuItem *beforeItem)
159 {
160     [item->nsItem() setTarget:m_delegate];
161     if (!item->menu())
162         [item->nsItem() setAction:@selector(itemFired:)];
163
164     if (item->isMerged())
165         return;
166
167     if ([item->nsItem() menu]) {
168         qWarning() << Q_FUNC_INFO << "Menu item is already in a menu, remove it from the other menu first before inserting";
169         return;
170     }
171     // if the item we're inserting before is merged, skip along until
172     // we find a non-merged real item to insert ahead of.
173     while (beforeItem && beforeItem->isMerged()) {
174         beforeItem = itemOrNull(m_menuItems.indexOf(beforeItem) + 1);
175     }
176
177     if (beforeItem) {
178         if (beforeItem->isMerged()) {
179             qWarning() << Q_FUNC_INFO << "No non-merged before menu item found";
180             return;
181         }
182         NSUInteger nativeIndex = [m_nativeMenu indexOfItem:beforeItem->nsItem()];
183         [m_nativeMenu insertItem: item->nsItem() atIndex: nativeIndex];
184     } else {
185         [m_nativeMenu addItem: item->nsItem()];
186     }
187 }
188
189 void QCocoaMenu::removeMenuItem(QPlatformMenuItem *menuItem)
190 {
191     QCocoaAutoReleasePool pool;
192     QCocoaMenuItem *cocoaItem = static_cast<QCocoaMenuItem *>(menuItem);
193     if (!m_menuItems.contains(cocoaItem)) {
194         qWarning() << Q_FUNC_INFO << "Menu does not contain the item to be removed";
195         return;
196     }
197     m_menuItems.removeOne(cocoaItem);
198     if (!cocoaItem->isMerged()) {
199         if (m_nativeMenu != [cocoaItem->nsItem() menu]) {
200             qWarning() << Q_FUNC_INFO << "Item to remove does not belong to this menu";
201             return;
202         }
203         [m_nativeMenu removeItem: cocoaItem->nsItem()];
204     }
205 }
206
207 QCocoaMenuItem *QCocoaMenu::itemOrNull(int index) const
208 {
209     if ((index < 0) || (index >= m_menuItems.size()))
210         return 0;
211
212     return m_menuItems.at(index);
213 }
214
215 void QCocoaMenu::syncMenuItem(QPlatformMenuItem *menuItem)
216 {
217     QCocoaAutoReleasePool pool;
218     QCocoaMenuItem *cocoaItem = static_cast<QCocoaMenuItem *>(menuItem);
219     if (!m_menuItems.contains(cocoaItem)) {
220         qWarning() << Q_FUNC_INFO << "Item does not belong to this menu";
221         return;
222     }
223
224     bool wasMerged = cocoaItem->isMerged();
225     NSMenu *oldMenu = wasMerged ? [getMenuLoader() applicationMenu] : m_nativeMenu;
226     NSMenuItem *oldItem = [oldMenu itemWithTag:(NSInteger) cocoaItem];
227
228     if (cocoaItem->sync() != oldItem) {
229         // native item was changed for some reason
230         if (oldItem) {
231             if (wasMerged) {
232                 [oldItem setEnabled:NO];
233                 [oldItem setHidden:YES];
234             } else {
235                 [m_nativeMenu removeItem:oldItem];
236             }
237         }
238
239         QCocoaMenuItem* beforeItem = itemOrNull(m_menuItems.indexOf(cocoaItem) + 1);
240         insertNative(cocoaItem, beforeItem);
241     }
242 }
243
244 void QCocoaMenu::syncSeparatorsCollapsible(bool enable)
245 {
246     QCocoaAutoReleasePool pool;
247     if (enable) {
248         bool previousIsSeparator = true; // setting to true kills all the separators placed at the top.
249         NSMenuItem *previousItem = nil;
250
251         NSArray *itemArray = [m_nativeMenu itemArray];
252         for (unsigned int i = 0; i < [itemArray count]; ++i) {
253             NSMenuItem *item = reinterpret_cast<NSMenuItem *>([itemArray objectAtIndex:i]);
254             if ([item isSeparatorItem])
255                 [item setHidden:previousIsSeparator];
256
257             if (![item isHidden]) {
258                 previousItem = item;
259                 previousIsSeparator = ([previousItem isSeparatorItem]);
260             }
261         }
262
263         // We now need to check the final item since we don't want any separators at the end of the list.
264         if (previousItem && previousIsSeparator)
265             [previousItem setHidden:YES];
266     } else {
267         foreach (QCocoaMenuItem *item, m_menuItems) {
268             if (!item->isSeparator())
269                 continue;
270
271             // sync the visiblity directly
272             item->sync();
273         }
274     }
275 }
276
277 void QCocoaMenu::setParentItem(QCocoaMenuItem *item)
278 {
279     Q_UNUSED(item);
280 }
281
282 void QCocoaMenu::setEnabled(bool enabled)
283 {
284     m_enabled = enabled;
285     syncModalState(!m_enabled);
286 }
287
288 void QCocoaMenu::setVisible(bool visible)
289 {
290     [m_nativeItem setSubmenu:(visible ? m_nativeMenu : nil)];
291 }
292
293 QPlatformMenuItem *QCocoaMenu::menuItemAt(int position) const
294 {
295     return m_menuItems.at(position);
296 }
297
298 QPlatformMenuItem *QCocoaMenu::menuItemForTag(quintptr tag) const
299 {
300     foreach (QCocoaMenuItem *item, m_menuItems) {
301         if (item->tag() ==  tag)
302             return item;
303     }
304
305     return 0;
306 }
307
308 QList<QCocoaMenuItem *> QCocoaMenu::merged() const
309 {
310     QList<QCocoaMenuItem *> result;
311     foreach (QCocoaMenuItem *item, m_menuItems) {
312         if (item->menu()) { // recurse into submenus
313             result.append(item->menu()->merged());
314             continue;
315         }
316
317         if (item->isMerged())
318             result.append(item);
319     }
320
321     return result;
322 }
323
324 void QCocoaMenu::syncModalState(bool modal)
325 {
326     if (!m_enabled)
327         modal = true;
328
329     [m_nativeItem setEnabled:!modal];
330
331     foreach (QCocoaMenuItem *item, m_menuItems) {
332         if (item->menu()) { // recurse into submenus
333             item->menu()->syncModalState(modal);
334             continue;
335         }
336
337         item->syncModalState(modal);
338     }
339 }
340
341 QT_END_NAMESPACE