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 <AppKit/AppKit.h> 42 43#include "qcocoamenubar.h" 44#include "qcocoawindow.h" 45#include "qcocoamenuloader.h" 46#include "qcocoaapplication.h" // for custom application category 47#include "qcocoaapplicationdelegate.h" 48 49#include <QtGui/QGuiApplication> 50#include <QtCore/QDebug> 51 52QT_BEGIN_NAMESPACE 53 54static QList<QCocoaMenuBar*> static_menubars; 55 56QCocoaMenuBar::QCocoaMenuBar() 57{ 58 static_menubars.append(this); 59 60 m_nativeMenu = [[NSMenu alloc] init]; 61#ifdef QT_COCOA_ENABLE_MENU_DEBUG 62 qDebug() << "Construct QCocoaMenuBar" << this << m_nativeMenu; 63#endif 64} 65 66QCocoaMenuBar::~QCocoaMenuBar() 67{ 68#ifdef QT_COCOA_ENABLE_MENU_DEBUG 69 qDebug() << "~QCocoaMenuBar" << this; 70#endif 71 for (auto menu : qAsConst(m_menus)) { 72 if (!menu) 73 continue; 74 NSMenuItem *item = nativeItemForMenu(menu); 75 if (menu->attachedItem() == item) 76 menu->setAttachedItem(nil); 77 } 78 79 [m_nativeMenu release]; 80 static_menubars.removeOne(this); 81 82 if (!m_window.isNull() && m_window->menubar() == this) { 83 m_window->setMenubar(nullptr); 84 85 // Delete the children first so they do not cause 86 // the native menu items to be hidden after 87 // the menu bar was updated 88 qDeleteAll(children()); 89 updateMenuBarImmediately(); 90 } 91} 92 93bool QCocoaMenuBar::needsImmediateUpdate() 94{ 95 if (!m_window.isNull()) { 96 if (m_window->window()->isActive()) 97 return true; 98 } else { 99 // Only update if the focus/active window has no 100 // menubar, which means it'll be using this menubar. 101 // This is to avoid a modification in a parentless 102 // menubar to affect a window-assigned menubar. 103 QWindow *fw = QGuiApplication::focusWindow(); 104 if (!fw) { 105 // Same if there's no focus window, BTW. 106 return true; 107 } else { 108 QCocoaWindow *cw = static_cast<QCocoaWindow *>(fw->handle()); 109 if (cw && !cw->menubar()) 110 return true; 111 } 112 } 113 114 // Either the menubar is attached to a non-active window, 115 // or the application's focus window has its own menubar 116 // (which is different from this one) 117 return false; 118} 119 120void QCocoaMenuBar::insertMenu(QPlatformMenu *platformMenu, QPlatformMenu *before) 121{ 122 QCocoaMenu *menu = static_cast<QCocoaMenu *>(platformMenu); 123 QCocoaMenu *beforeMenu = static_cast<QCocoaMenu *>(before); 124#ifdef QT_COCOA_ENABLE_MENU_DEBUG 125 qDebug() << "QCocoaMenuBar" << this << "insertMenu" << menu << "before" << before; 126#endif 127 128 if (m_menus.contains(QPointer<QCocoaMenu>(menu))) { 129 qWarning("This menu already belongs to the menubar, remove it first"); 130 return; 131 } 132 133 if (beforeMenu && !m_menus.contains(QPointer<QCocoaMenu>(beforeMenu))) { 134 qWarning("The before menu does not belong to the menubar"); 135 return; 136 } 137 138 int insertionIndex = beforeMenu ? m_menus.indexOf(beforeMenu) : m_menus.size(); 139 m_menus.insert(insertionIndex, menu); 140 141 { 142 QMacAutoReleasePool pool; 143 NSMenuItem *item = [[[NSMenuItem alloc] init] autorelease]; 144 item.tag = reinterpret_cast<NSInteger>(menu); 145 146 if (beforeMenu) { 147 // QMenuBar::toNSMenu() exposes the native menubar and 148 // the user could have inserted its own items in there. 149 // Same remark applies to removeMenu(). 150 NSMenuItem *beforeItem = nativeItemForMenu(beforeMenu); 151 NSInteger nativeIndex = [m_nativeMenu indexOfItem:beforeItem]; 152 [m_nativeMenu insertItem:item atIndex:nativeIndex]; 153 } else { 154 [m_nativeMenu addItem:item]; 155 } 156 } 157 158 syncMenu_helper(menu, false /*internaCall*/); 159 160 if (needsImmediateUpdate()) 161 updateMenuBarImmediately(); 162} 163 164void QCocoaMenuBar::removeMenu(QPlatformMenu *platformMenu) 165{ 166 QCocoaMenu *menu = static_cast<QCocoaMenu *>(platformMenu); 167 if (!m_menus.contains(menu)) { 168 qWarning("Trying to remove a menu that does not belong to the menubar"); 169 return; 170 } 171 172 NSMenuItem *item = nativeItemForMenu(menu); 173 if (menu->attachedItem() == item) 174 menu->setAttachedItem(nil); 175 m_menus.removeOne(menu); 176 177 QMacAutoReleasePool pool; 178 179 // See remark in insertMenu(). 180 NSInteger nativeIndex = [m_nativeMenu indexOfItem:item]; 181 [m_nativeMenu removeItemAtIndex:nativeIndex]; 182} 183 184void QCocoaMenuBar::syncMenu(QPlatformMenu *menu) 185{ 186 syncMenu_helper(menu, false /*internaCall*/); 187} 188 189void QCocoaMenuBar::syncMenu_helper(QPlatformMenu *menu, bool menubarUpdate) 190{ 191 QMacAutoReleasePool pool; 192 193 QCocoaMenu *cocoaMenu = static_cast<QCocoaMenu *>(menu); 194 for (QCocoaMenuItem *item : cocoaMenu->items()) 195 cocoaMenu->syncMenuItem_helper(item, menubarUpdate); 196 197 BOOL shouldHide = YES; 198 if (cocoaMenu->isVisible()) { 199 // If the NSMenu has no visble items, or only separators, we should hide it 200 // on the menubar. This can happen after syncing the menu items since they 201 // can be moved to other menus. 202 for (NSMenuItem *item in cocoaMenu->nsMenu().itemArray) 203 if (!item.separatorItem && !item.hidden) { 204 shouldHide = NO; 205 break; 206 } 207 } 208 209 if (NSMenuItem *attachedItem = cocoaMenu->attachedItem()) { 210 // Non-nil attached item means the item's submenu is set 211 attachedItem.title = cocoaMenu->nsMenu().title; 212 attachedItem.hidden = shouldHide; 213 } 214} 215 216NSMenuItem *QCocoaMenuBar::nativeItemForMenu(QCocoaMenu *menu) const 217{ 218 if (!menu) 219 return nil; 220 221 return [m_nativeMenu itemWithTag:reinterpret_cast<NSInteger>(menu)]; 222} 223 224void QCocoaMenuBar::handleReparent(QWindow *newParentWindow) 225{ 226#ifdef QT_COCOA_ENABLE_MENU_DEBUG 227 qDebug() << "QCocoaMenuBar" << this << "handleReparent" << newParentWindow; 228#endif 229 230 if (!m_window.isNull()) 231 m_window->setMenubar(nullptr); 232 233 if (!newParentWindow) { 234 m_window.clear(); 235 } else { 236 newParentWindow->create(); 237 m_window = static_cast<QCocoaWindow*>(newParentWindow->handle()); 238 m_window->setMenubar(this); 239 } 240 241 updateMenuBarImmediately(); 242} 243 244QWindow *QCocoaMenuBar::parentWindow() const 245{ 246 return m_window ? m_window->window() : nullptr; 247} 248 249 250QCocoaWindow *QCocoaMenuBar::findWindowForMenubar() 251{ 252 if (qApp->focusWindow()) 253 return static_cast<QCocoaWindow*>(qApp->focusWindow()->handle()); 254 255 return nullptr; 256} 257 258QCocoaMenuBar *QCocoaMenuBar::findGlobalMenubar() 259{ 260 for (auto *menubar : qAsConst(static_menubars)) { 261 if (menubar->m_window.isNull()) 262 return menubar; 263 } 264 265 return nullptr; 266} 267 268void QCocoaMenuBar::updateMenuBarImmediately() 269{ 270 QMacAutoReleasePool pool; 271 QCocoaMenuBar *mb = findGlobalMenubar(); 272 QCocoaWindow *cw = findWindowForMenubar(); 273 274 QWindow *win = cw ? cw->window() : nullptr; 275 if (win && (win->flags() & Qt::Popup) == Qt::Popup) { 276 // context menus, comboboxes, etc. don't need to update the menubar, 277 // but if an application has only Qt::Tool window(s) on start, 278 // we still have to update the menubar. 279 if ((win->flags() & Qt::WindowType_Mask) != Qt::Tool) 280 return; 281 NSApplication *app = [NSApplication sharedApplication]; 282 if (![app.delegate isKindOfClass:[QCocoaApplicationDelegate class]]) 283 return; 284 // We apply this logic _only_ during the startup. 285 QCocoaApplicationDelegate *appDelegate = app.delegate; 286 if (!appDelegate.inLaunch) 287 return; 288 } 289 290 if (cw && cw->menubar()) 291 mb = cw->menubar(); 292 293 if (!mb) 294 return; 295 296#ifdef QT_COCOA_ENABLE_MENU_DEBUG 297 qDebug() << "QCocoaMenuBar" << "updateMenuBarImmediately" << cw; 298#endif 299 bool disableForModal = mb->shouldDisable(cw); 300 301 for (auto menu : qAsConst(mb->m_menus)) { 302 if (!menu) 303 continue; 304 NSMenuItem *item = mb->nativeItemForMenu(menu); 305 menu->setAttachedItem(item); 306 menu->setMenuParent(mb); 307 // force a sync? 308 mb->syncMenu_helper(menu, true /*menubarUpdate*/); 309 menu->propagateEnabledState(!disableForModal); 310 } 311 312 QCocoaMenuLoader *loader = [QCocoaMenuLoader sharedMenuLoader]; 313 [loader ensureAppMenuInMenu:mb->nsMenu()]; 314 315 NSMutableSet *mergedItems = [[NSMutableSet setWithCapacity:mb->merged().count()] retain]; 316 for (auto mergedItem : mb->merged()) { 317 [mergedItems addObject:mergedItem->nsItem()]; 318 mergedItem->syncMerged(); 319 } 320 321 // hide+disable all mergeable items we're not currently using 322 for (NSMenuItem *mergeable in [loader mergeable]) { 323 if (![mergedItems containsObject:mergeable]) { 324 mergeable.hidden = YES; 325 mergeable.enabled = NO; 326 } 327 } 328 329 [mergedItems release]; 330 [NSApp setMainMenu:mb->nsMenu()]; 331 [loader qtTranslateApplicationMenu]; 332} 333 334QList<QCocoaMenuItem*> QCocoaMenuBar::merged() const 335{ 336 QList<QCocoaMenuItem*> r; 337 for (auto menu : qAsConst(m_menus)) 338 r.append(menu->merged()); 339 340 return r; 341} 342 343bool QCocoaMenuBar::shouldDisable(QCocoaWindow *active) const 344{ 345 if (active && (active->window()->modality() == Qt::NonModal)) 346 return false; 347 348 if (m_window == active) { 349 // modal window owns us, we should be enabled! 350 return false; 351 } 352 353 QWindowList topWindows(qApp->topLevelWindows()); 354 // When there is an application modal window on screen, the entries of 355 // the menubar should be disabled. The exception in Qt is that if the 356 // modal window is the only window on screen, then we enable the menu bar. 357 for (auto *window : qAsConst(topWindows)) { 358 if (window->isVisible() && window->modality() == Qt::ApplicationModal) { 359 // check for other visible windows 360 for (auto *other : qAsConst(topWindows)) { 361 if ((window != other) && (other->isVisible())) { 362 // INVARIANT: we found another visible window 363 // on screen other than our modalWidget. We therefore 364 // disable the menu bar to follow normal modality logic: 365 return true; 366 } 367 } 368 369 // INVARIANT: We have only one window on screen that happends 370 // to be application modal. We choose to enable the menu bar 371 // in that case to e.g. enable the quit menu item. 372 return false; 373 } 374 } 375 376 return true; 377} 378 379QPlatformMenu *QCocoaMenuBar::menuForTag(quintptr tag) const 380{ 381 for (auto menu : qAsConst(m_menus)) 382 if (menu->tag() == tag) 383 return menu; 384 385 return nullptr; 386} 387 388NSMenuItem *QCocoaMenuBar::itemForRole(QPlatformMenuItem::MenuRole role) 389{ 390 for (auto menu : qAsConst(m_menus)) 391 for (auto *item : menu->items()) 392 if (item->effectiveRole() == role) 393 return item->nsItem(); 394 395 return nil; 396} 397 398QCocoaWindow *QCocoaMenuBar::cocoaWindow() const 399{ 400 return m_window.data(); 401} 402 403QT_END_NAMESPACE 404 405#include "moc_qcocoamenubar.cpp" 406