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"