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