File indexing completed on 2024-04-21 14:55:55

0001 /* This file is part of the KDE libraries
0002    Copyright (C) 2000 Daniel M. Duley <mosfet@kde.org>
0003    Copyright (C) 2002,2006 Hamish Rodda <rodda@kde.org>
0004    Copyright (C) 2006 Olivier Goffart <ogoffart@kde.org>
0005 
0006    This library is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU Library General Public
0008    License version 2 as published by the Free Software Foundation.
0009 
0010    This library is distributed in the hope that it will be useful,
0011    but WITHOUT ANY WARRANTY; without even the implied warranty of
0012    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0013    Library General Public License for more details.
0014 
0015    You should have received a copy of the GNU Library General Public License
0016    along with this library; see the file COPYING.LIB.  If not, write to
0017    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0018    Boston, MA 02110-1301, USA.
0019 */
0020 
0021 #include "kmenu.h"
0022 
0023 #include <QMetaMethod>
0024 #include <QObject>
0025 #include <QPointer>
0026 #include <QTimer>
0027 #include <QApplication>
0028 #include <QCursor>
0029 #include <QFontMetrics>
0030 #include <QHBoxLayout>
0031 #include <QKeyEvent>
0032 #include <QLabel>
0033 #include <QPainter>
0034 #include <QStyle>
0035 #include <QToolButton>
0036 #include <QWidgetAction>
0037 
0038 #include <kdebug.h>
0039 #include <klocalizedstring.h>
0040 #include <kacceleratormanager.h>
0041 
0042 static const char KMENU_TITLE[] = "kmenu_title";
0043 
0044 class Q_DECL_HIDDEN KMenu::KMenuPrivate
0045     : public QObject
0046 {
0047 public:
0048     KMenuPrivate(KMenu *_parent);
0049     ~KMenuPrivate() override;
0050 
0051     void resetKeyboardVars(bool noMatches = false);
0052     void actionHovered(QAction *action);
0053     void showCtxMenu(const QPoint &pos);
0054     void skipTitles(QKeyEvent *event);
0055 
0056     /**
0057      * @internal
0058      *
0059      * This event filter which is installed
0060      * on the title of the menu, which is a QToolButton. This will
0061      * prevent clicks (what would change down and focus properties on
0062      * the title) on the title of the menu.
0063      *
0064      * @author Rafael Fernández López <ereslibre@kde.org>
0065      */
0066     bool eventFilter(QObject *object, QEvent *event) override
0067     {
0068         Q_UNUSED(object);
0069 
0070         if (event->type() == QEvent::Paint ||
0071                 event->type() == QEvent::KeyPress ||
0072                 event->type() == QEvent::KeyRelease) {
0073             return false;
0074         }
0075 
0076         event->accept();
0077         return true;
0078     }
0079 
0080     KMenu *parent;
0081 
0082     // variables for keyboard navigation
0083     QTimer clearTimer;
0084 
0085     bool noMatches : 1;
0086     bool shortcuts : 1;
0087     bool autoExec : 1;
0088 
0089     QString keySeq;
0090     QString originalText;
0091 
0092     QAction *lastHitAction;
0093     QAction *lastHoveredAction;
0094     Qt::MouseButtons mouseButtons;
0095     Qt::KeyboardModifiers keyboardModifiers;
0096 
0097     // support for RMB menus on menus
0098     QMenu *ctxMenu;
0099     QPointer<QAction> highlightedAction;
0100 
0101 };
0102 
0103 KMenu::KMenuPrivate::KMenuPrivate(KMenu *_parent)
0104     : parent(_parent)
0105     , noMatches(false)
0106     , shortcuts(false)
0107     , autoExec(false)
0108     , lastHitAction(nullptr)
0109     , lastHoveredAction(nullptr)
0110     , mouseButtons(Qt::NoButton)
0111     , keyboardModifiers(Qt::NoModifier)
0112     , ctxMenu(nullptr)
0113     , highlightedAction(nullptr)
0114 {
0115     resetKeyboardVars();
0116     KAcceleratorManager::manage(parent);
0117 }
0118 
0119 KMenu::KMenuPrivate::~KMenuPrivate()
0120 {
0121     delete ctxMenu;
0122 }
0123 
0124 /**
0125  * custom variant type for QAction::data of kmenu context menus
0126  * @author Joseph Wenninger <jowenn@kde.org>
0127  */
0128 class KMenuContext
0129 {
0130 public:
0131     KMenuContext();
0132     KMenuContext(const KMenuContext &o);
0133     KMenuContext(QPointer<KMenu> menu, QPointer<QAction> action);
0134 
0135     inline QPointer<KMenu> menu() const
0136     {
0137         return m_menu;
0138     }
0139     inline QPointer<QAction> action() const
0140     {
0141         return m_action;
0142     }
0143 
0144 private:
0145     QPointer<KMenu> m_menu;
0146     QPointer<QAction> m_action;
0147 };
0148 
0149 Q_DECLARE_METATYPE(KMenuContext)
0150 
0151 KMenu::KMenu(QWidget *parent)
0152     : QMenu(parent)
0153     , d(new KMenuPrivate(this))
0154 {
0155     connect(&(d->clearTimer), SIGNAL(timeout()), SLOT(resetKeyboardVars()));
0156 }
0157 
0158 KMenu::KMenu(const QString &title, QWidget *parent)
0159     : QMenu(title, parent)
0160     , d(new KMenuPrivate(this))
0161 {
0162     connect(&(d->clearTimer), SIGNAL(timeout()), SLOT(resetKeyboardVars()));
0163 }
0164 
0165 KMenu::~KMenu()
0166 {
0167     delete d;
0168 }
0169 
0170 QAction *KMenu::addTitle(const QString &text, QAction *before)
0171 {
0172     return addTitle(QIcon(), text, before);
0173 }
0174 
0175 QAction *KMenu::addTitle(const QIcon &icon, const QString &text, QAction *before)
0176 {
0177     QAction *buttonAction = new QAction(this);
0178     QFont font = buttonAction->font();
0179     font.setBold(true);
0180     buttonAction->setFont(font);
0181     buttonAction->setText(text);
0182     buttonAction->setIcon(icon);
0183 
0184     QWidgetAction *action = new QWidgetAction(this);
0185     action->setObjectName(KMENU_TITLE);
0186     QToolButton *titleButton = new QToolButton(this);
0187     titleButton->installEventFilter(d); // prevent clicks on the title of the menu
0188     titleButton->setDefaultAction(buttonAction);
0189     titleButton->setDown(true); // prevent hover style changes in some styles
0190     titleButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
0191     action->setDefaultWidget(titleButton);
0192 
0193     insertAction(before, action);
0194     return action;
0195 }
0196 
0197 /**
0198  * This is re-implemented for keyboard navigation.
0199  */
0200 void KMenu::closeEvent(QCloseEvent *e)
0201 {
0202     if (d->shortcuts) {
0203         d->resetKeyboardVars();
0204     }
0205     QMenu::closeEvent(e);
0206 }
0207 
0208 Qt::MouseButtons KMenu::mouseButtons() const
0209 {
0210     return d->mouseButtons;
0211 }
0212 
0213 Qt::KeyboardModifiers KMenu::keyboardModifiers() const
0214 {
0215     return d->keyboardModifiers;
0216 }
0217 
0218 void KMenu::keyPressEvent(QKeyEvent *e)
0219 {
0220     d->mouseButtons = Qt::NoButton;
0221     d->keyboardModifiers = Qt::NoModifier;
0222 
0223     if (!d->shortcuts) {
0224         d->keyboardModifiers = e->modifiers();
0225         QMenu::keyPressEvent(e);
0226 
0227         if (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down) {
0228             d->skipTitles(e);
0229         }
0230 
0231         return;
0232     }
0233 
0234     QAction *a = nullptr;
0235     bool firstpass = true;
0236     QString keyString = e->text();
0237 
0238     // check for common commands dealt with by QMenu
0239     int key = e->key();
0240     if (key == Qt::Key_Escape || key == Qt::Key_Return || key == Qt::Key_Enter
0241             || key == Qt::Key_Up || key == Qt::Key_Down || key == Qt::Key_Left
0242             || key == Qt::Key_Right || key == Qt::Key_F1 || key == Qt::Key_PageUp
0243             || key == Qt::Key_PageDown || key == Qt::Key_Back || key == Qt::Key_Select) {
0244 
0245         d->resetKeyboardVars();
0246         // continue event processing by QMenu
0247         //e->ignore();
0248         d->keyboardModifiers = e->modifiers();
0249         QMenu::keyPressEvent(e);
0250 
0251         if (key == Qt::Key_Up || key == Qt::Key_Down) {
0252             d->skipTitles(e);
0253         }
0254         return;
0255     } else if (key == Qt::Key_Shift || key == Qt::Key_Control || key == Qt::Key_Alt || key == Qt::Key_Meta) {
0256         return QMenu::keyPressEvent(e);
0257     }
0258 
0259     // check to see if the user wants to remove a key from the sequence (backspace)
0260     // or clear the sequence (delete)
0261     if (!d->keySeq.isNull()) {
0262         if (key == Qt::Key_Backspace) {
0263 
0264             if (d->keySeq.length() == 1) {
0265                 d->resetKeyboardVars();
0266                 return;
0267             }
0268 
0269             // keep the last sequence in keyString
0270             keyString = d->keySeq.left(d->keySeq.length() - 1);
0271 
0272             // allow sequence matching to be tried again
0273             d->resetKeyboardVars();
0274 
0275         } else if (key == Qt::Key_Delete) {
0276             d->resetKeyboardVars();
0277 
0278             // clear active item
0279             setActiveAction(nullptr);
0280             return;
0281 
0282         } else if (d->noMatches) {
0283             // clear if there are no matches
0284             d->resetKeyboardVars();
0285 
0286             // clear active item
0287             setActiveAction(nullptr);
0288 
0289         } else {
0290             // the key sequence is not a null string
0291             // therefore the lastHitAction is valid
0292             a = d->lastHitAction;
0293         }
0294 
0295     } else if (key == Qt::Key_Backspace && menuAction()) {
0296         // backspace with no chars in the buffer... go back a menu.
0297         hide();
0298         d->resetKeyboardVars();
0299         return;
0300     }
0301 
0302     d->keySeq += keyString;
0303     const int seqLen = d->keySeq.length();
0304 
0305     foreach (a, actions()) {
0306         // don't search disabled entries
0307         if (!a->isEnabled()) {
0308             continue;
0309         }
0310 
0311         QString thisText;
0312 
0313         // retrieve the right text
0314         // (the last selected item one may have additional ampersands)
0315         if (a == d->lastHitAction) {
0316             thisText = d->originalText;
0317         } else {
0318             thisText = a->text();
0319         }
0320 
0321         // if there is an accelerator present, remove it
0322         thisText = KLocalizedString::removeAcceleratorMarker(thisText);
0323 
0324         // chop text to the search length
0325         thisText = thisText.left(seqLen);
0326 
0327         // do the search
0328         if (!thisText.indexOf(d->keySeq, 0, Qt::CaseInsensitive)) {
0329 
0330             if (firstpass) {
0331                 // match
0332                 setActiveAction(a);
0333 
0334                 // check to see if we're underlining a different item
0335                 if (d->lastHitAction && d->lastHitAction != a)
0336                     // yes; revert the underlining
0337                 {
0338                     d->lastHitAction->setText(d->originalText);
0339                 }
0340 
0341                 // set the original text if it's a different item
0342                 if (d->lastHitAction != a || d->lastHitAction == nullptr) {
0343                     d->originalText = a->text();
0344                 }
0345 
0346                 // underline the currently selected item
0347                 a->setText(underlineText(d->originalText, d->keySeq.length()));
0348 
0349                 // remember what's going on
0350                 d->lastHitAction = a;
0351 
0352                 // start/restart the clear timer
0353                 d->clearTimer.setSingleShot(true);
0354                 d->clearTimer.start(5000);
0355 
0356                 // go around for another try, to see if we can execute
0357                 firstpass = false;
0358             } else {
0359                 // don't allow execution
0360                 return;
0361             }
0362         }
0363 
0364         // fall through to allow execution
0365     }
0366 
0367     if (!firstpass) {
0368         if (d->autoExec) {
0369             // activate anything
0370             d->lastHitAction->activate(QAction::Trigger);
0371             d->resetKeyboardVars();
0372 
0373         } else if (d->lastHitAction && d->lastHitAction->menu()) {
0374             // only activate sub-menus
0375             d->lastHitAction->activate(QAction::Trigger);
0376             d->resetKeyboardVars();
0377         }
0378 
0379         return;
0380     }
0381 
0382     // no matches whatsoever, clean up
0383     d->resetKeyboardVars(true);
0384     //e->ignore();
0385     QMenu::keyPressEvent(e);
0386 }
0387 
0388 bool KMenu::focusNextPrevChild(bool next)
0389 {
0390     d->resetKeyboardVars();
0391     return QMenu::focusNextPrevChild(next);
0392 }
0393 
0394 QString KMenu::underlineText(const QString &text, uint length)
0395 {
0396     QString ret = text;
0397     for (uint i = 0; i < length; i++) {
0398         if (ret[2 * i] != '&') {
0399             ret.insert(2 * i, '&');
0400         }
0401     }
0402     return ret;
0403 }
0404 
0405 void KMenu::KMenuPrivate::resetKeyboardVars(bool _noMatches)
0406 {
0407     // Clean up keyboard variables
0408     if (lastHitAction) {
0409         lastHitAction->setText(originalText);
0410         lastHitAction = nullptr;
0411     }
0412 
0413     if (!noMatches) {
0414         keySeq.clear();
0415     }
0416 
0417     noMatches = _noMatches;
0418 }
0419 
0420 void KMenu::setKeyboardShortcutsEnabled(bool enable)
0421 {
0422     d->shortcuts = enable;
0423 }
0424 
0425 void KMenu::setKeyboardShortcutsExecute(bool enable)
0426 {
0427     d->autoExec = enable;
0428 }
0429 /**
0430  * End keyboard navigation.
0431  */
0432 
0433 /**
0434  * RMB menus on menus
0435  */
0436 
0437 void KMenu::mousePressEvent(QMouseEvent *e)
0438 {
0439     if (d->ctxMenu && d->ctxMenu->isVisible()) {
0440         // hide on a second context menu event
0441         d->ctxMenu->hide();
0442     }
0443 
0444     if (e->button() == Qt::MidButton) {
0445         return;
0446     }
0447 
0448     QMenu::mousePressEvent(e);
0449 }
0450 
0451 void KMenu::mouseReleaseEvent(QMouseEvent *e)
0452 {
0453     // Save the button, and the modifiers
0454     d->keyboardModifiers = e->modifiers();
0455     d->mouseButtons = e->buttons();
0456 
0457     if (e->button() == Qt::MidButton) {
0458         if (activeAction()) {
0459             const QMetaObject *metaObject = activeAction()->metaObject();
0460             const int index = metaObject->indexOfMethod("triggered(Qt::MouseButtons,Qt::KeyboardModifiers)");
0461             if (index != -1) {
0462                 const QMetaMethod method = metaObject->method(index);
0463                 method.invoke(activeAction(), Qt::DirectConnection,
0464                               Q_ARG(Qt::MouseButtons, e->button()),
0465                               Q_ARG(Qt::KeyboardModifiers, QApplication::keyboardModifiers()));
0466             }
0467         }
0468         return;
0469     }
0470 
0471     if (!d->ctxMenu || !d->ctxMenu->isVisible()) {
0472         QMenu::mouseReleaseEvent(e);
0473     }
0474 }
0475 
0476 QMenu *KMenu::contextMenu()
0477 {
0478     if (!d->ctxMenu) {
0479         d->ctxMenu = new QMenu(this);
0480         connect(this, SIGNAL(hovered(QAction*)), SLOT(actionHovered(QAction*)));
0481     }
0482 
0483     return d->ctxMenu;
0484 }
0485 
0486 const QMenu *KMenu::contextMenu() const
0487 {
0488     return const_cast< KMenu * >(this)->contextMenu();
0489 }
0490 
0491 void KMenu::hideContextMenu()
0492 {
0493     if (!d->ctxMenu || !d->ctxMenu->isVisible()) {
0494         return;
0495     }
0496 
0497     d->ctxMenu->hide();
0498 }
0499 
0500 void KMenu::KMenuPrivate::actionHovered(QAction *action)
0501 {
0502     lastHoveredAction = action;
0503     parent->hideContextMenu();
0504 }
0505 
0506 static void KMenuSetActionData(QMenu *menu, KMenu *contextedMenu, QAction *contextedAction)
0507 {
0508     const QList<QAction *> actions = menu->actions();
0509     QVariant v;
0510     v.setValue(KMenuContext(contextedMenu, contextedAction));
0511     for (int i = 0; i < actions.count(); i++) {
0512         actions[i]->setData(v);
0513     }
0514 }
0515 
0516 void KMenu::KMenuPrivate::showCtxMenu(const QPoint &pos)
0517 {
0518     highlightedAction = parent->activeAction();
0519 
0520     if (!highlightedAction) {
0521         KMenuSetActionData(parent, nullptr, nullptr);
0522         return;
0523     }
0524 
0525     emit parent->aboutToShowContextMenu(parent, highlightedAction, ctxMenu);
0526     KMenuSetActionData(parent, parent, highlightedAction);
0527 
0528     if (QMenu *subMenu = highlightedAction->menu()) {
0529         QTimer::singleShot(100, subMenu, SLOT(hide()));
0530     }
0531 
0532     ctxMenu->popup(parent->mapToGlobal(pos));
0533 }
0534 
0535 void KMenu::KMenuPrivate::skipTitles(QKeyEvent *event)
0536 {
0537     QWidgetAction *action = qobject_cast<QWidgetAction *>(parent->activeAction());
0538     QWidgetAction *firstAction = action;
0539     while (action && action->objectName() == KMENU_TITLE) {
0540         parent->keyPressEvent(event);
0541         action = qobject_cast<QWidgetAction *>(parent->activeAction());
0542         if (firstAction == action) { // we looped and only found titles
0543             parent->setActiveAction(nullptr);
0544             break;
0545         }
0546     }
0547 }
0548 
0549 KMenu *KMenu::contextMenuFocus()
0550 {
0551     return qobject_cast<KMenu *>(QApplication::activePopupWidget());
0552 }
0553 
0554 QAction *KMenu::contextMenuFocusAction()
0555 {
0556     if (KMenu *menu = qobject_cast<KMenu *>(QApplication::activePopupWidget())) {
0557         if (!menu->d->lastHoveredAction) {
0558             return nullptr;
0559         }
0560         QVariant var = menu->d->lastHoveredAction->data();
0561         KMenuContext ctx = var.value<KMenuContext>();
0562         Q_ASSERT(ctx.menu() == menu);
0563         return ctx.action();
0564     }
0565 
0566     return nullptr;
0567 }
0568 
0569 void KMenu::contextMenuEvent(QContextMenuEvent *e)
0570 {
0571     if (d->ctxMenu) {
0572         if (e->reason() == QContextMenuEvent::Mouse) {
0573             d->showCtxMenu(e->pos());
0574         } else if (activeAction()) {
0575             d->showCtxMenu(actionGeometry(activeAction()).center());
0576         }
0577 
0578         e->accept();
0579         return;
0580     }
0581 
0582     QMenu::contextMenuEvent(e);
0583 }
0584 
0585 void KMenu::hideEvent(QHideEvent *e)
0586 {
0587     if (d->ctxMenu && d->ctxMenu->isVisible()) {
0588         // we need to block signals here when the ctxMenu is showing
0589         // to prevent the QPopupMenu::activated(int) signal from emitting
0590         // when hiding with a context menu, the user doesn't expect the
0591         // menu to actually do anything.
0592         // since hideEvent gets called very late in the process of hiding
0593         // (deep within QWidget::hide) the activated(int) signal is the
0594         // last signal to be emitted, even after things like aboutToHide()
0595         // AJS
0596         bool blocked = blockSignals(true);
0597         d->ctxMenu->hide();
0598         blockSignals(blocked);
0599     }
0600     QMenu::hideEvent(e);
0601 }
0602 /**
0603  * end of RMB menus on menus support
0604  */
0605 
0606 KMenuContext::KMenuContext()
0607     : m_menu(nullptr)
0608     , m_action(nullptr)
0609 {
0610 }
0611 
0612 KMenuContext::KMenuContext(const KMenuContext &o)
0613     : m_menu(o.m_menu)
0614     , m_action(o.m_action)
0615 {
0616 }
0617 
0618 KMenuContext::KMenuContext(QPointer<KMenu> menu, QPointer<QAction> action)
0619     : m_menu(menu)
0620     , m_action(action)
0621 {
0622 }
0623 
0624 #include "moc_kmenu.cpp"