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"