File indexing completed on 2024-09-29 06:30:49
0001 /* 0002 This file is part of the KDE project 0003 SPDX-FileCopyrightText: 2021 Felix Ernst <fe.a.ernst@gmail.com> 0004 0005 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0006 */ 0007 0008 #include "khamburgermenu.h" 0009 #include "khamburgermenu_p.h" 0010 0011 #include "khamburgermenuhelpers_p.h" 0012 0013 #include <KLocalizedString> 0014 0015 #include <QMenu> 0016 #include <QMenuBar> 0017 #include <QStyle> 0018 #include <QToolBar> 0019 #include <QToolButton> 0020 0021 #include <algorithm> 0022 #include <forward_list> 0023 #include <unordered_set> 0024 0025 KHamburgerMenu::KHamburgerMenu(QObject *parent) 0026 : QWidgetAction{parent} 0027 , d_ptr{new KHamburgerMenuPrivate(this)} 0028 { 0029 } 0030 0031 KHamburgerMenuPrivate::KHamburgerMenuPrivate(KHamburgerMenu *qq) 0032 : q_ptr{qq} 0033 , m_listeners{new ListenerContainer(this)} 0034 { 0035 q_ptr->setPriority(QAction::LowPriority); 0036 connect(q_ptr, &QAction::changed, this, &KHamburgerMenuPrivate::slotActionChanged); 0037 connect(q_ptr, &QAction::triggered, this, &KHamburgerMenuPrivate::slotActionTriggered); 0038 } 0039 0040 KHamburgerMenu::~KHamburgerMenu() = default; 0041 0042 KHamburgerMenuPrivate::~KHamburgerMenuPrivate() = default; 0043 0044 void KHamburgerMenu::setMenuBar(QMenuBar *menuBar) 0045 { 0046 Q_D(KHamburgerMenu); 0047 d->setMenuBar(menuBar); 0048 } 0049 0050 void KHamburgerMenuPrivate::setMenuBar(QMenuBar *menuBar) 0051 { 0052 if (m_menuBar) { 0053 m_menuBar->removeEventFilter(m_listeners->get<VisibilityChangesListener>()); 0054 m_menuBar->removeEventFilter(m_listeners->get<AddOrRemoveActionListener>()); 0055 } 0056 m_menuBar = menuBar; 0057 updateVisibility(); 0058 if (m_menuBar) { 0059 m_menuBar->installEventFilter(m_listeners->get<VisibilityChangesListener>()); 0060 m_menuBar->installEventFilter(m_listeners->get<AddOrRemoveActionListener>()); 0061 } 0062 } 0063 0064 QMenuBar *KHamburgerMenu::menuBar() const 0065 { 0066 Q_D(const KHamburgerMenu); 0067 return d->menuBar(); 0068 } 0069 0070 QMenuBar *KHamburgerMenuPrivate::menuBar() const 0071 { 0072 return m_menuBar; 0073 } 0074 0075 void KHamburgerMenu::setMenuBarAdvertised(bool advertise) 0076 { 0077 Q_D(KHamburgerMenu); 0078 d->setMenuBarAdvertised(advertise); 0079 } 0080 0081 void KHamburgerMenuPrivate::setMenuBarAdvertised(bool advertise) 0082 { 0083 m_advertiseMenuBar = advertise; 0084 } 0085 0086 bool KHamburgerMenu::menuBarAdvertised() const 0087 { 0088 Q_D(const KHamburgerMenu); 0089 return d->menuBarAdvertised(); 0090 } 0091 0092 bool KHamburgerMenuPrivate::menuBarAdvertised() const 0093 { 0094 return m_advertiseMenuBar; 0095 } 0096 0097 void KHamburgerMenu::setShowMenuBarAction(QAction *showMenuBarAction) 0098 { 0099 Q_D(KHamburgerMenu); 0100 d->setShowMenuBarAction(showMenuBarAction); 0101 } 0102 0103 void KHamburgerMenuPrivate::setShowMenuBarAction(QAction *showMenuBarAction) 0104 { 0105 m_showMenuBarAction = showMenuBarAction; 0106 } 0107 0108 void KHamburgerMenu::addToMenu(QMenu *menu) 0109 { 0110 Q_D(KHamburgerMenu); 0111 d->insertIntoMenuBefore(menu, nullptr); 0112 } 0113 0114 void KHamburgerMenu::insertIntoMenuBefore(QMenu *menu, QAction *before) 0115 { 0116 Q_D(KHamburgerMenu); 0117 d->insertIntoMenuBefore(menu, before); 0118 } 0119 0120 void KHamburgerMenuPrivate::insertIntoMenuBefore(QMenu *menu, QAction *before) 0121 { 0122 Q_CHECK_PTR(menu); 0123 Q_Q(KHamburgerMenu); 0124 if (!m_menuAction) { 0125 m_menuAction = new QAction(this); 0126 m_menuAction->setText(i18nc("@action:inmenu General purpose menu", "&Menu")); 0127 m_menuAction->setIcon(q->icon()); 0128 m_menuAction->setMenu(m_actualMenu.get()); 0129 } 0130 updateVisibility(); // Sets the appropriate visibility of m_menuAction. 0131 0132 menu->insertAction(before, m_menuAction); 0133 connect(menu, &QMenu::aboutToShow, this, [this, menu, q]() { 0134 if (m_menuAction->isVisible()) { 0135 Q_EMIT q->aboutToShowMenu(); 0136 hideActionsOf(menu); 0137 resetMenu(); 0138 } 0139 }); 0140 } 0141 0142 void KHamburgerMenu::hideActionsOf(QWidget *widget) 0143 { 0144 Q_D(KHamburgerMenu); 0145 d->hideActionsOf(widget); 0146 } 0147 0148 void KHamburgerMenuPrivate::hideActionsOf(QWidget *widget) 0149 { 0150 Q_CHECK_PTR(widget); 0151 m_widgetsWithActionsToBeHidden.remove(nullptr); 0152 if (listContainsWidget(m_widgetsWithActionsToBeHidden, widget)) { 0153 return; 0154 } 0155 m_widgetsWithActionsToBeHidden.emplace_front(QPointer<const QWidget>(widget)); 0156 if (QMenu *menu = qobject_cast<QMenu *>(widget)) { 0157 // QMenus are normally hidden. This will avoid redundancy with their actions anyways. 0158 menu->installEventFilter(m_listeners->get<AddOrRemoveActionListener>()); 0159 notifyMenuResetNeeded(); 0160 } else { 0161 // Only avoid redundancy when the widget is visible. 0162 widget->installEventFilter(m_listeners->get<VisibleActionsChangeListener>()); 0163 if (widget->isVisible()) { 0164 notifyMenuResetNeeded(); 0165 } 0166 } 0167 } 0168 0169 void KHamburgerMenu::showActionsOf(QWidget *widget) 0170 { 0171 Q_D(KHamburgerMenu); 0172 d->showActionsOf(widget); 0173 } 0174 0175 void KHamburgerMenuPrivate::showActionsOf(QWidget *widget) 0176 { 0177 Q_CHECK_PTR(widget); 0178 m_widgetsWithActionsToBeHidden.remove(widget); 0179 widget->removeEventFilter(m_listeners->get<AddOrRemoveActionListener>()); 0180 widget->removeEventFilter(m_listeners->get<VisibleActionsChangeListener>()); 0181 if (isWidgetActuallyVisible(widget)) { 0182 notifyMenuResetNeeded(); 0183 } 0184 } 0185 0186 QWidget *KHamburgerMenu::createWidget(QWidget *parent) 0187 { 0188 Q_D(KHamburgerMenu); 0189 return d->createWidget(parent); 0190 } 0191 0192 QWidget *KHamburgerMenuPrivate::createWidget(QWidget *parent) 0193 { 0194 if (qobject_cast<QMenu *>(parent)) { 0195 qDebug( 0196 "Adding a KHamburgerMenu directly to a QMenu. " 0197 "This will look odd. Use addToMenu() instead."); 0198 } 0199 Q_Q(KHamburgerMenu); 0200 0201 auto toolButton = new QToolButton(parent); 0202 // Set appearance 0203 toolButton->setDefaultAction(q); 0204 toolButton->setMenu(m_actualMenu.get()); 0205 toolButton->setAttribute(Qt::WidgetAttribute::WA_CustomWhatsThis); 0206 toolButton->setPopupMode(QToolButton::InstantPopup); 0207 updateButtonStyle(toolButton); 0208 if (const QToolBar *toolbar = qobject_cast<QToolBar *>(parent)) { 0209 connect(toolbar, &QToolBar::toolButtonStyleChanged, toolButton, &QToolButton::setToolButtonStyle); 0210 } 0211 0212 setToolButtonVisible(toolButton, !isMenuBarVisible(m_menuBar)); 0213 0214 // Make sure the menu will be ready in time 0215 toolButton->installEventFilter(m_listeners->get<ButtonPressListener>()); 0216 0217 hideActionsOf(parent); 0218 return toolButton; 0219 } 0220 0221 QAction *KHamburgerMenuPrivate::actionWithExclusivesFrom(QAction *from, QWidget *parent, std::unordered_set<const QAction *> &nonExclusives) const 0222 { 0223 Q_CHECK_PTR(from); 0224 if (nonExclusives.count(from) > 0) { 0225 return nullptr; // The action is non-exclusive/already visible elsewhere. 0226 } 0227 if (!from->menu() || from->menu()->isEmpty()) { 0228 return from; // The action is exclusive and doesn't have a menu. 0229 } 0230 std::unique_ptr<QAction> menuActionWithExclusives(new QAction(from->icon(), from->text(), parent)); 0231 std::unique_ptr<QMenu> menuWithExclusives(new QMenu(parent)); 0232 const auto fromMenuActions = from->menu()->actions(); 0233 for (QAction *action : fromMenuActions) { 0234 QAction *actionWithExclusives = actionWithExclusivesFrom(action, menuWithExclusives.get(), nonExclusives); 0235 if (actionWithExclusives) { 0236 menuWithExclusives->addAction(actionWithExclusives); 0237 } 0238 } 0239 if (menuWithExclusives->isEmpty()) { 0240 return nullptr; // "from" has a menu that contains zero exclusive actions. 0241 // There is a chance that "from" is an exclusive action itself and should 0242 // therefore be returned instead but that is unlikely for an action that has a menu(). 0243 // This fringe case is the only one that can't be correctly covered because we can 0244 // not know or assume that activating the action does something or if it is nothing 0245 // but a container for a menu. 0246 } 0247 menuActionWithExclusives->setMenu(menuWithExclusives.release()); 0248 return menuActionWithExclusives.release(); 0249 } 0250 0251 std::unique_ptr<QMenu> KHamburgerMenuPrivate::newMenu() 0252 { 0253 std::unique_ptr<QMenu> menu(new QMenu()); 0254 Q_Q(const KHamburgerMenu); 0255 0256 // Make sure we notice if the q->menu() is changed or replaced in the future. 0257 if (q->menu() != m_lastUsedMenu) { 0258 q->menu()->installEventFilter(m_listeners->get<AddOrRemoveActionListener>()); 0259 0260 if (m_lastUsedMenu && !listContainsWidget(m_widgetsWithActionsToBeHidden, m_lastUsedMenu)) { 0261 m_lastUsedMenu->removeEventFilter(m_listeners->get<AddOrRemoveActionListener>()); 0262 } 0263 m_lastUsedMenu = q->menu(); 0264 } 0265 0266 if (!q->menu() && !m_menuBar) { 0267 return menu; // empty menu 0268 } 0269 0270 if (!q->menu()) { 0271 // We have nothing else to work with so let's just add the menuBar contents. 0272 const auto menuBarActions = m_menuBar->actions(); 0273 for (QAction *menuAction : menuBarActions) { 0274 menu->addAction(menuAction); 0275 } 0276 return menu; 0277 } 0278 0279 // Collect actions which shouldn't be added to the menu 0280 std::unordered_set<const QAction *> visibleActions; 0281 m_widgetsWithActionsToBeHidden.remove(nullptr); 0282 for (const QWidget *widget : m_widgetsWithActionsToBeHidden) { 0283 if (qobject_cast<const QMenu *>(widget) || isWidgetActuallyVisible(widget)) { 0284 // avoid redundancy with menus even when they are not actually visible. 0285 visibleActions.reserve(visibleActions.size() + widget->actions().size()); 0286 const auto widgetActions = widget->actions(); 0287 for (QAction *action : widgetActions) { 0288 visibleActions.insert(action); 0289 } 0290 } 0291 } 0292 // Populate the menu 0293 const auto menuActions = q->menu()->actions(); 0294 for (QAction *action : menuActions) { 0295 if (visibleActions.count(action) == 0) { 0296 menu->addAction(action); 0297 visibleActions.insert(action); 0298 } 0299 } 0300 // Add the last two menu actions 0301 if (m_menuBar) { 0302 connect(menu.get(), &QMenu::aboutToShow, this, [this]() { 0303 if (m_menuBar->actions().last()->icon().isNull()) { 0304 m_helpIconIsSet = false; 0305 m_menuBar->actions().last()->setIcon(QIcon::fromTheme(QStringLiteral("help-contents"))); // set "Help" menu icon 0306 } else { 0307 m_helpIconIsSet = true; // if the "Help" icon was set by the application, we want to leave it untouched 0308 } 0309 }); 0310 connect(menu.get(), &QMenu::aboutToHide, this, [this]() { 0311 if (m_menuBar->actions().last()->icon().name() == QStringLiteral("help-contents") && !m_helpIconIsSet) { 0312 m_menuBar->actions().last()->setIcon(QIcon()); 0313 } 0314 }); 0315 menu->addAction(m_menuBar->actions().last()); // add "Help" menu 0316 visibleActions.insert(m_menuBar->actions().last()); 0317 if (m_advertiseMenuBar) { 0318 menu->addSeparator(); 0319 m_menuBarAdvertisementMenu = newMenuBarAdvertisementMenu(visibleActions); 0320 menu->addAction(m_menuBarAdvertisementMenu->menuAction()); 0321 } 0322 } 0323 return menu; 0324 } 0325 0326 std::unique_ptr<QMenu> KHamburgerMenuPrivate::newMenuBarAdvertisementMenu(std::unordered_set<const QAction *> &visibleActions) 0327 { 0328 std::unique_ptr<QMenu> advertiseMenuBarMenu(new QMenu()); 0329 m_showMenuBarWithAllActionsText = i18nc("@action:inmenu A menu item that advertises and enables the menubar", "Show &Menubar with All Actions"); 0330 connect(advertiseMenuBarMenu.get(), &QMenu::aboutToShow, this, [this]() { 0331 if (m_showMenuBarAction) { 0332 m_showMenuBarText = m_showMenuBarAction->text(); 0333 m_showMenuBarAction->setText(m_showMenuBarWithAllActionsText); 0334 } 0335 }); 0336 connect(advertiseMenuBarMenu.get(), &QMenu::aboutToHide, this, [this]() { 0337 if (m_showMenuBarAction && m_showMenuBarAction->text() == m_showMenuBarWithAllActionsText) { 0338 m_showMenuBarAction->setText(m_showMenuBarText); 0339 } 0340 }); 0341 if (m_showMenuBarAction) { 0342 advertiseMenuBarMenu->addAction(m_showMenuBarAction); 0343 visibleActions.insert(m_showMenuBarAction); 0344 } 0345 QAction *section = advertiseMenuBarMenu->addSeparator(); 0346 0347 const auto menuBarActions = m_menuBar->actions(); 0348 for (QAction *menuAction : menuBarActions) { 0349 QAction *menuActionWithExclusives = actionWithExclusivesFrom(menuAction, advertiseMenuBarMenu.get(), visibleActions); 0350 if (menuActionWithExclusives) { 0351 advertiseMenuBarMenu->addAction(menuActionWithExclusives); 0352 } 0353 } 0354 advertiseMenuBarMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-more-symbolic"))); 0355 advertiseMenuBarMenu->setTitle(i18nc("@action:inmenu A menu text advertising its contents (more features).", "More")); 0356 section->setText(i18nc("@action:inmenu A section heading advertising the contents of the menu bar", "More Actions")); 0357 return advertiseMenuBarMenu; 0358 } 0359 0360 void KHamburgerMenuPrivate::resetMenu() 0361 { 0362 Q_Q(KHamburgerMenu); 0363 if (!m_menuResetNeeded && m_actualMenu && m_lastUsedMenu == q->menu()) { 0364 return; 0365 } 0366 m_menuResetNeeded = false; 0367 0368 m_actualMenu = newMenu(); 0369 0370 const auto createdWidgets = q->createdWidgets(); 0371 for (auto widget : createdWidgets) { 0372 static_cast<QToolButton *>(widget)->setMenu(m_actualMenu.get()); 0373 } 0374 if (m_menuAction) { 0375 m_menuAction->setMenu(m_actualMenu.get()); 0376 } 0377 } 0378 0379 void KHamburgerMenuPrivate::updateVisibility() 0380 { 0381 Q_Q(KHamburgerMenu); 0382 /** The visibility of KHamburgerMenu should be opposite to the visibility of m_menuBar. 0383 * Exception: We only consider a visible m_menuBar as actually visible if it is not a native 0384 * menu bar because native menu bars can come in many shapes and sizes which don't necessarily 0385 * have the same usability benefits as a traditional in-window menu bar. 0386 * KDE applications normally allow the user to remove any actions from their toolbar(s) anyway. */ 0387 const bool menuBarVisible = isMenuBarVisible(m_menuBar); 0388 0389 const auto createdWidgets = q->createdWidgets(); 0390 for (auto widget : createdWidgets) { 0391 setToolButtonVisible(widget, !menuBarVisible); 0392 } 0393 0394 if (!m_menuAction) { 0395 if (menuBarVisible && m_actualMenu) { 0396 m_actualMenu.release()->deleteLater(); // might as well free up some memory 0397 } 0398 return; 0399 } 0400 0401 // The m_menuAction acts as a fallback if both the m_menuBar and all createdWidgets() on the UI 0402 // are currently hidden. Only then should the m_menuAction ever be visible in a QMenu. 0403 if (menuBarVisible || (m_menuBar && m_menuBar->isNativeMenuBar()) // See [1] below. 0404 || std::any_of(createdWidgets.cbegin(), createdWidgets.cend(), isWidgetActuallyVisible)) { 0405 m_menuAction->setVisible(false); 0406 return; 0407 } 0408 m_menuAction->setVisible(true); 0409 0410 // [1] While the m_menuAction can be used as a normal menu by users that don't mind invoking a 0411 // QMenu to access any menu actions, its primary use really is that of a fallback. 0412 // Therefore the existence of a native menu bar (no matter what shape or size it might have) 0413 // is enough reason for us to hide m_menuAction. 0414 } 0415 0416 void KHamburgerMenuPrivate::slotActionChanged() 0417 { 0418 Q_Q(KHamburgerMenu); 0419 const auto createdWidgets = q->createdWidgets(); 0420 for (auto widget : createdWidgets) { 0421 auto toolButton = static_cast<QToolButton *>(widget); 0422 updateButtonStyle(toolButton); 0423 } 0424 } 0425 0426 void KHamburgerMenuPrivate::slotActionTriggered() 0427 { 0428 if (isMenuBarVisible(m_menuBar)) { 0429 const auto menuBarActions = m_menuBar->actions(); 0430 for (const auto action : menuBarActions) { 0431 if (action->isEnabled() && !action->isSeparator()) { 0432 m_menuBar->setActiveAction(m_menuBar->actions().constFirst()); 0433 return; 0434 } 0435 } 0436 } 0437 0438 Q_Q(KHamburgerMenu); 0439 const auto createdWidgets = q->createdWidgets(); 0440 for (auto widget : createdWidgets) { 0441 if (isWidgetActuallyVisible(widget) && widget->isActiveWindow()) { 0442 auto toolButton = static_cast<QToolButton *>(widget); 0443 m_listeners->get<ButtonPressListener>()->prepareHamburgerButtonForPress(toolButton); 0444 toolButton->pressed(); 0445 return; 0446 } 0447 } 0448 0449 Q_EMIT q->aboutToShowMenu(); 0450 resetMenu(); 0451 prepareParentlessMenuForShowing(m_actualMenu.get(), nullptr); 0452 m_actualMenu->popup(QCursor::pos()); 0453 } 0454 0455 void KHamburgerMenuPrivate::updateButtonStyle(QToolButton *hamburgerMenuButton) const 0456 { 0457 Q_Q(const KHamburgerMenu); 0458 Qt::ToolButtonStyle buttonStyle = Qt::ToolButtonFollowStyle; 0459 if (QToolBar *toolbar = qobject_cast<QToolBar *>(hamburgerMenuButton->parent())) { 0460 buttonStyle = toolbar->toolButtonStyle(); 0461 } 0462 if (buttonStyle == Qt::ToolButtonFollowStyle) { 0463 buttonStyle = static_cast<Qt::ToolButtonStyle>(hamburgerMenuButton->style()->styleHint(QStyle::SH_ToolButtonStyle)); 0464 } 0465 if (buttonStyle == Qt::ToolButtonTextBesideIcon && q->priority() < QAction::NormalPriority) { 0466 hamburgerMenuButton->setToolButtonStyle(Qt::ToolButtonIconOnly); 0467 } else { 0468 hamburgerMenuButton->setToolButtonStyle(buttonStyle); 0469 } 0470 } 0471 0472 #include "moc_khamburgermenu.cpp" 0473 #include "moc_khamburgermenu_p.cpp"