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