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
qcocoamenuitem.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 <qpa/qplatformtheme.h>
8
10
11#include "qcocoansmenu.h"
12#include "qcocoamenu.h"
13#include "qcocoamenubar.h"
14#include "qcocoahelpers.h"
15#include "qcocoaapplication.h" // for custom application category
17#include <QtGui/private/qcoregraphics_p.h>
18#include <QtCore/qregularexpression.h>
19#include <QtCore/private/qcore_mac_p.h>
20#include <QtGui/private/qapplekeymapper_p.h>
21
22#include <QtCore/QDebug>
23
24QT_BEGIN_NAMESPACE
25
26using namespace Qt::StringLiterals;
27
28static const char *application_menu_strings[] =
29{
30 QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU","About %1"),
31 QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU","Preferences..."),
32 QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU","Services"),
33 QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU","Hide %1"),
34 QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU","Hide Others"),
35 QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU","Show All"),
36 QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU","Quit %1")
37};
38
40{
41 QString menuString = QString::fromLatin1(application_menu_strings[type]);
42 const QString translated = QCoreApplication::translate("QMenuBar", application_menu_strings[type]);
43 if (translated != menuString) {
44 return translated;
45 } else {
46 return QCoreApplication::translate("MAC_APPLICATION_MENU", application_menu_strings[type]);
47 }
48}
49
53 m_menu(nullptr),
55 m_iconSize(16),
56 m_textSynced(false),
57 m_isVisible(true),
58 m_enabled(true),
59 m_parentEnabled(true),
60 m_isSeparator(false),
61 m_checked(false),
62 m_merged(false)
63{
64}
65
67{
68 QMacAutoReleasePool pool;
69
70 if (m_menu && m_menu->menuParent() == this)
71 m_menu->setMenuParent(nullptr);
72 if (m_merged) {
73 m_native.hidden = YES;
74 } else {
75 if (m_menu && m_menu->attachedItem() == m_native)
76 m_menu->setAttachedItem(nil);
77 [m_native release];
78 }
79
80 [m_itemView release];
81}
82
83void QCocoaMenuItem::setText(const QString &text)
84{
85 m_text = text;
86}
87
88void QCocoaMenuItem::setIcon(const QIcon &icon)
89{
90 m_icon = icon;
91}
92
93void QCocoaMenuItem::setMenu(QPlatformMenu *menu)
94{
95 if (menu == m_menu)
96 return;
97
98 bool setAttached = false;
99 if ([m_native.menu isKindOfClass:[QCocoaNSMenu class]]) {
100 auto parentMenu = static_cast<QCocoaNSMenu *>(m_native.menu);
101 setAttached = parentMenu.platformMenu && parentMenu.platformMenu->isAboutToShow();
102 }
103
104 if (m_menu && m_menu->menuParent() == this) {
105 m_menu->setMenuParent(nullptr);
106 // Free the menu from its parent's influence
107 m_menu->propagateEnabledState(true);
108 if (m_native && m_menu->attachedItem() == m_native)
109 m_menu->setAttachedItem(nil);
110 }
111
113 m_menu = static_cast<QCocoaMenu *>(menu);
114 if (m_menu) {
115 m_menu->setMenuParent(this);
116 m_menu->propagateEnabledState(isEnabled());
117 if (setAttached)
118 m_menu->setAttachedItem(m_native);
119 } else {
120 // we previously had a menu, but no longer
121 // clear out our item so the nexy sync() call builds a new one
122 [m_native release];
123 m_native = nil;
124 }
125}
126
127void QCocoaMenuItem::setVisible(bool isVisible)
128{
129 m_isVisible = isVisible;
130}
131
132void QCocoaMenuItem::setIsSeparator(bool isSeparator)
133{
134 m_isSeparator = isSeparator;
135}
136
137void QCocoaMenuItem::setFont(const QFont &font)
138{
139 Q_UNUSED(font);
140}
141
142void QCocoaMenuItem::setRole(MenuRole role)
143{
144 if (role != m_role)
145 m_textSynced = false; // Changing role deserves a second chance.
146 m_role = role;
147}
148
149#ifndef QT_NO_SHORTCUT
150void QCocoaMenuItem::setShortcut(const QKeySequence& shortcut)
151{
152 m_shortcut = shortcut;
153}
154#endif
155
156void QCocoaMenuItem::setChecked(bool isChecked)
157{
158 m_checked = isChecked;
159}
160
161void QCocoaMenuItem::setEnabled(bool enabled)
162{
163 if (m_enabled != enabled) {
164 m_enabled = enabled;
165 if (m_menu)
166 m_menu->propagateEnabledState(isEnabled());
167 }
168}
169
171{
172 NSView *itemView = (NSView *)item;
173 if (m_itemView == itemView)
174 return;
175 [m_itemView release];
176 m_itemView = [itemView retain];
177 m_itemView.autoresizesSubviews = YES;
178 m_itemView.autoresizingMask = NSViewWidthSizable;
179 m_itemView.hidden = NO;
180 m_itemView.needsDisplay = YES;
181}
182
183static QPlatformMenuItem::MenuRole detectMenuRole(const QString &captionWithPossibleMnemonic)
184{
185 QString itemCaption(captionWithPossibleMnemonic);
186 itemCaption.remove(u'&');
187
188 static const std::tuple<QPlatformMenuItem::MenuRole, std::vector<std::tuple<Qt::MatchFlags, const char *>>> roleMap[] = {
189 { QPlatformMenuItem::AboutRole, {
190 { Qt::MatchStartsWith | Qt::MatchEndsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "About") }
191 }},
192 { QPlatformMenuItem::PreferencesRole, {
193 { Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Config") },
194 { Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Preference") },
195 { Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Options") },
196 { Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Setting") },
197 { Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Setup") },
198 }},
199 { QPlatformMenuItem::QuitRole, {
200 { Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Quit") },
201 { Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Exit") },
202 }},
203 { QPlatformMenuItem::CutRole, {
204 { Qt::MatchExactly, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Cut") }
205 }},
206 { QPlatformMenuItem::CopyRole, {
207 { Qt::MatchExactly, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Copy") }
208 }},
209 { QPlatformMenuItem::PasteRole, {
210 { Qt::MatchExactly, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Paste") }
211 }},
212 { QPlatformMenuItem::SelectAllRole, {
213 { Qt::MatchExactly, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Select All") }
214 }},
215 };
216
217 auto match = [](const QString &caption, const QString &itemCaption, Qt::MatchFlags matchFlags) {
218 if (matchFlags.testFlag(Qt::MatchExactly))
219 return !itemCaption.compare(caption, Qt::CaseInsensitive);
220 if (matchFlags.testFlag(Qt::MatchStartsWith) && itemCaption.startsWith(caption, Qt::CaseInsensitive))
221 return true;
222 if (matchFlags.testFlag(Qt::MatchEndsWith) && itemCaption.endsWith(caption, Qt::CaseInsensitive))
223 return true;
224 return false;
225 };
226
227 QPlatformMenuItem::MenuRole detectedRole = [&]{
228 for (const auto &[role, captions] : roleMap) {
229 for (const auto &[matchFlags, caption] : captions) {
230 // Check for untranslated match
231 if (match(caption, itemCaption, matchFlags))
232 return role;
233 // Then translated with the current Qt translation
234 if (match(QCoreApplication::translate("QCocoaMenuItem", caption), itemCaption, matchFlags))
235 return role;
236 }
237 }
238 return QPlatformMenuItem::NoRole;
239 }();
240
241 if (detectedRole == QPlatformMenuItem::AboutRole) {
242 static const QRegularExpression qtRegExp("qt$"_L1, QRegularExpression::CaseInsensitiveOption);
243 if (itemCaption.contains(qtRegExp))
244 detectedRole = QPlatformMenuItem::AboutQtRole;
245 }
246
247 return detectedRole;
248}
249
251{
252 if (m_isSeparator != m_native.separatorItem) {
253 [m_native release];
254 if (m_isSeparator)
255 m_native = [[QCocoaNSMenuItem separatorItemWithPlatformMenuItem:this] retain];
256 else
257 m_native = nil;
258 }
259
260 if ((m_role != NoRole && !m_textSynced) || m_merged) {
261 QCocoaMenuBar *menubar = nullptr;
262 if (m_role == TextHeuristicRole) {
263 // Recognized menu roles are only found in the first menus below the menubar
264 QObject *p = menuParent();
265 int depth = 1;
266 while (depth < 3 && p && !(menubar = qobject_cast<QCocoaMenuBar *>(p))) {
267 ++depth;
268 QCocoaMenuObject *menuObject = dynamic_cast<QCocoaMenuObject *>(p);
269 p = menuObject ? menuObject->menuParent() : nullptr;
270 }
271
272 if (menubar && depth < 3)
273 m_detectedRole = detectMenuRole(m_text);
274 else
275 m_detectedRole = NoRole;
276 }
277
278 QCocoaMenuLoader *loader = [QCocoaMenuLoader sharedMenuLoader];
279 NSMenuItem *mergeItem = nil;
280 const auto role = effectiveRole();
281 switch (role) {
282 case AboutRole:
283 mergeItem = [loader aboutMenuItem];
284 break;
285 case AboutQtRole:
286 mergeItem = [loader aboutQtMenuItem];
287 break;
288 case PreferencesRole:
289 mergeItem = [loader preferencesMenuItem];
290 break;
291 case ApplicationSpecificRole:
292 mergeItem = [loader appSpecificMenuItem:this];
293 break;
294 case QuitRole:
295 mergeItem = [loader quitMenuItem];
296 break;
297 case CutRole:
298 case CopyRole:
299 case PasteRole:
300 case SelectAllRole:
301 if (menubar)
302 mergeItem = menubar->itemForRole(role);
303 break;
304 case NoRole:
305 // The heuristic couldn't resolve the menu role
306 m_textSynced = false;
307 break;
308 default:
309 if (!m_text.isEmpty())
310 m_textSynced = true;
311 break;
312 }
313
314 if (mergeItem) {
315 m_textSynced = true;
316 m_merged = true;
317 [mergeItem retain];
318 [m_native release];
319 m_native = mergeItem;
320 if (auto *nativeItem = qt_objc_cast<QCocoaNSMenuItem *>(m_native))
321 nativeItem.platformMenuItem = this;
322 } else if (m_merged) {
323 // was previously merged, but no longer
324 [m_native release];
325 m_native = nil; // create item below
326 m_merged = false;
327 }
328 } else if (!m_text.isEmpty()) {
329 m_textSynced = true; // NoRole, and that was set explicitly. So, nothing to do anymore.
330 }
331
332 if (!m_native) {
333 m_native = [[QCocoaNSMenuItem alloc] initWithPlatformMenuItem:this];
334 m_native.title = m_text.toNSString();
335 }
336
338
339 m_native.hidden = !m_isVisible;
340 m_native.view = m_itemView;
341
342 QString text = mergeText();
343#ifndef QT_NO_SHORTCUT
344 QKeySequence accel = mergeAccel();
345
346 // Show multiple key sequences as part of the menu text.
347 if (accel.count() > 1)
348 text += " ("_L1 + accel.toString(QKeySequence::NativeText) + ")"_L1;
349#endif
350
351 m_native.title = QPlatformTheme::removeMnemonics(text).toNSString();
352
353#ifndef QT_NO_SHORTCUT
354 if (accel.count() == 1) {
355 auto key = accel[0].key();
356 auto modifiers = accel[0].keyboardModifiers();
357
358 QChar cocoaKey = QAppleKeyMapper::toCocoaKey(key);
359 if (cocoaKey.isNull())
360 cocoaKey = char16_t(QChar::toLower(key));
361 // Similar to qt_mac_removePrivateUnicode change the delete key,
362 // so the symbol is correctly seen in native menu bar.
363 if (cocoaKey.unicode() == NSDeleteFunctionKey)
364 cocoaKey = char16_t(NSDeleteCharacter);
365
366 m_native.keyEquivalent = QStringView(&cocoaKey, 1).toNSString();
367 m_native.keyEquivalentModifierMask = QAppleKeyMapper::toCocoaModifiers(modifiers);
368 } else
369#endif
370 {
371 m_native.keyEquivalent = @"";
372 m_native.keyEquivalentModifierMask = NSEventModifierFlagCommand;
373 }
374
375 m_native.image = [NSImage imageFromQIcon:m_icon withSize:m_iconSize];
376
377 m_native.state = m_checked ? NSControlStateValueOn : NSControlStateValueOff;
378 return m_native;
379}
380
381QString QCocoaMenuItem::mergeText()
382{
383 QCocoaMenuLoader *loader = [QCocoaMenuLoader sharedMenuLoader];
384 if (m_native == [loader aboutMenuItem]) {
385 return qt_mac_applicationmenu_string(AboutAppMenuItem).arg(qt_mac_applicationName());
386 } else if (m_native== [loader aboutQtMenuItem]) {
387 if (m_text == QString("About Qt"))
388 return QCoreApplication::translate("QCocoaMenuItem", "About Qt");
389 else
390 return m_text;
391 } else if (m_native == [loader preferencesMenuItem]) {
392 return qt_mac_applicationmenu_string(PreferencesAppMenuItem);
393 } else if (m_native == [loader quitMenuItem]) {
394 return qt_mac_applicationmenu_string(QuitAppMenuItem).arg(qt_mac_applicationName());
395 } else if (m_text.contains('\t')) {
396 return m_text.left(m_text.indexOf('\t'));
397 }
398 return m_text;
399}
400
401#ifndef QT_NO_SHORTCUT
402QKeySequence QCocoaMenuItem::mergeAccel()
403{
404 QCocoaMenuLoader *loader = [QCocoaMenuLoader sharedMenuLoader];
405 if (m_native == [loader preferencesMenuItem])
406 return QKeySequence(QKeySequence::Preferences);
407 else if (m_native == [loader quitMenuItem])
408 return QKeySequence(QKeySequence::Quit);
409 else if (m_text.contains('\t'))
410 return QKeySequence(m_text.mid(m_text.indexOf('\t') + 1), QKeySequence::NativeText);
411
412 return m_shortcut;
413}
414#endif
415
417{
418 if (!m_merged) {
419 qCWarning(lcQpaMenus) << "Trying to sync non-merged" << this;
420 return;
421 }
422
423 m_native.hidden = !m_isVisible;
424}
425
427{
428 if (m_parentEnabled != enabled) {
429 m_parentEnabled = enabled;
430 if (m_menu)
431 m_menu->propagateEnabledState(isEnabled());
432 }
433}
434
436{
437 if (m_role > TextHeuristicRole)
438 return m_role;
439 else
440 return m_detectedRole;
441}
442
444{
445 m_iconSize = size;
446}
447
449{
450 if (m_native.separatorItem)
451 return;
452
453 // Some items created by QCocoaMenuLoader are not
454 // instances of QCocoaNSMenuItem and have their
455 // target/action set as Interface Builder would.
456 if (![m_native isMemberOfClass:[QCocoaNSMenuItem class]])
457 return;
458
459 // Use the responder chain and ensure native modal dialogs
460 // continue receiving cut/copy/paste/etc. key equivalents.
461 SEL roleAction;
462 switch (effectiveRole()) {
463 case CutRole:
464 roleAction = @selector(cut:);
465 break;
466 case CopyRole:
467 roleAction = @selector(copy:);
468 break;
469 case PasteRole:
470 roleAction = @selector(paste:);
471 break;
472 case SelectAllRole:
473 roleAction = @selector(selectAll:);
474 break;
475 default:
476 if (m_menu) {
477 // Menu items that represent sub menus should have submenuAction: as their
478 // action, so that clicking the menu item opens the sub menu without closing
479 // the entire menu hierarchy. A menu item with this action and a valid submenu
480 // will disable NSMenuValidation for the item, which is normally not an issue
481 // as NSMenuItems are enabled by default. But in our case, we haven't attached
482 // the submenu yet, which results in AppKit concluding that there's no validator
483 // for the item (the target is nil, and nothing responds to submenuAction:), and
484 // will in response disable the menu item. To work around this we explicitly
485 // enable the menu item in QCocoaMenu::setAttachedItem() once we have a submenu.
486 roleAction = @selector(submenuAction:);
487 } else {
488 roleAction = @selector(qt_itemFired:);
489 }
490 }
491
492 m_native.action = roleAction;
493 m_native.target = nil;
494}
495
496QT_END_NAMESPACE
void setParentEnabled(bool enabled)
void setRole(MenuRole role) override
NSMenuItem * sync()
void setNativeContents(WId item) override
void setIsSeparator(bool isSeparator) override
void setText(const QString &text) override
void setIcon(const QIcon &icon) override
void setShortcut(const QKeySequence &shortcut) override
void setFont(const QFont &font) override
void setEnabled(bool isEnabled) override
void setVisible(bool isVisible) override
void setChecked(bool isChecked) override
void setIconSize(int size) override
MenuRole effectiveRole() const
QString qt_mac_applicationmenu_string(int type)
static const char * application_menu_strings[]
static QPlatformMenuItem::MenuRole detectMenuRole(const QString &captionWithPossibleMnemonic)
QThreadPool pool
[8]