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