kpopupmenu.cpp
00001 /* This file is part of the KDE libraries 00002 Copyright (C) 2000 Daniel M. Duley <mosfet@kde.org> 00003 Copyright (C) 2002 Hamish Rodda <rodda@kde.org> 00004 00005 This library is free software; you can redistribute it and/or 00006 modify it under the terms of the GNU Library General Public 00007 License version 2 as published by the Free Software Foundation. 00008 00009 This library is distributed in the hope that it will be useful, 00010 but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 00012 Library General Public License for more details. 00013 00014 You should have received a copy of the GNU Library General Public License 00015 along with this library; see the file COPYING.LIB. If not, write to 00016 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 00017 Boston, MA 02110-1301, USA. 00018 */ 00019 #include <qcursor.h> 00020 #include <qpainter.h> 00021 #include <qtimer.h> 00022 #include <qfontmetrics.h> 00023 #include <qstyle.h> 00024 00025 #include "kpopupmenu.h" 00026 00027 #include <kdebug.h> 00028 #include <kapplication.h> 00029 00030 KPopupTitle::KPopupTitle(QWidget *parent, const char *name) 00031 : QWidget(parent, name) 00032 { 00033 setMinimumSize(16, fontMetrics().height()+8); 00034 } 00035 00036 KPopupTitle::KPopupTitle(KPixmapEffect::GradientType /* gradient */, 00037 const QColor &/* color */, const QColor &/* textColor */, 00038 QWidget *parent, const char *name) 00039 : QWidget(parent, name) 00040 { 00041 calcSize(); 00042 } 00043 00044 KPopupTitle::KPopupTitle(const KPixmap & /* background */, const QColor &/* color */, 00045 const QColor &/* textColor */, QWidget *parent, 00046 const char *name) 00047 : QWidget(parent, name) 00048 { 00049 calcSize(); 00050 } 00051 00052 void KPopupTitle::setTitle(const QString &text, const QPixmap *icon) 00053 { 00054 titleStr = text; 00055 if (icon) 00056 miniicon = *icon; 00057 else 00058 miniicon.resize(0, 0); 00059 00060 calcSize(); 00061 } 00062 00063 void KPopupTitle::setText( const QString &text ) 00064 { 00065 titleStr = text; 00066 calcSize(); 00067 } 00068 00069 void KPopupTitle::setIcon( const QPixmap &pix ) 00070 { 00071 miniicon = pix; 00072 calcSize(); 00073 } 00074 00075 void KPopupTitle::calcSize() 00076 { 00077 QFont f = font(); 00078 f.setBold(true); 00079 int w = miniicon.width()+QFontMetrics(f).width(titleStr); 00080 int h = QMAX( fontMetrics().height(), miniicon.height() ); 00081 setMinimumSize( w+16, h+8 ); 00082 } 00083 00084 void KPopupTitle::paintEvent(QPaintEvent *) 00085 { 00086 QRect r(rect()); 00087 QPainter p(this); 00088 kapp->style().drawPrimitive(QStyle::PE_HeaderSection, &p, r, palette().active()); 00089 00090 if (!miniicon.isNull()) 00091 p.drawPixmap(4, (r.height()-miniicon.height())/2, miniicon); 00092 00093 if (!titleStr.isNull()) 00094 { 00095 p.setPen(palette().active().text()); 00096 QFont f = p.font(); 00097 f.setBold(true); 00098 p.setFont(f); 00099 if(!miniicon.isNull()) 00100 { 00101 p.drawText(miniicon.width()+8, 0, width()-(miniicon.width()+8), 00102 height(), AlignLeft | AlignVCenter | SingleLine, 00103 titleStr); 00104 } 00105 else 00106 { 00107 p.drawText(0, 0, width(), height(), 00108 AlignCenter | SingleLine, titleStr); 00109 } 00110 } 00111 } 00112 00113 QSize KPopupTitle::sizeHint() const 00114 { 00115 return minimumSize(); 00116 } 00117 00118 class KPopupMenu::KPopupMenuPrivate 00119 { 00120 public: 00121 KPopupMenuPrivate () 00122 : noMatches(false) 00123 , shortcuts(false) 00124 , autoExec(false) 00125 , lastHitIndex(-1) 00126 , state(Qt::NoButton) 00127 , m_ctxMenu(0) 00128 {} 00129 00130 ~KPopupMenuPrivate () 00131 { 00132 delete m_ctxMenu; 00133 } 00134 00135 QString m_lastTitle; 00136 00137 // variables for keyboard navigation 00138 QTimer clearTimer; 00139 00140 bool noMatches : 1; 00141 bool shortcuts : 1; 00142 bool autoExec : 1; 00143 00144 QString keySeq; 00145 QString originalText; 00146 00147 int lastHitIndex; 00148 Qt::ButtonState state; 00149 00150 // support for RMB menus on menus 00151 QPopupMenu* m_ctxMenu; 00152 static bool s_continueCtxMenuShow; 00153 static int s_highlightedItem; 00154 static KPopupMenu* s_contextedMenu; 00155 }; 00156 00157 int KPopupMenu::KPopupMenuPrivate::s_highlightedItem(-1); 00158 KPopupMenu* KPopupMenu::KPopupMenuPrivate::s_contextedMenu(0); 00159 bool KPopupMenu::KPopupMenuPrivate::s_continueCtxMenuShow(true); 00160 00161 KPopupMenu::KPopupMenu(QWidget *parent, const char *name) 00162 : QPopupMenu(parent, name) 00163 { 00164 d = new KPopupMenuPrivate; 00165 resetKeyboardVars(); 00166 connect(&(d->clearTimer), SIGNAL(timeout()), SLOT(resetKeyboardVars())); 00167 } 00168 00169 KPopupMenu::~KPopupMenu() 00170 { 00171 if (KPopupMenuPrivate::s_contextedMenu == this) 00172 { 00173 KPopupMenuPrivate::s_contextedMenu = 0; 00174 KPopupMenuPrivate::s_highlightedItem = -1; 00175 } 00176 00177 delete d; 00178 } 00179 00180 int KPopupMenu::insertTitle(const QString &text, int id, int index) 00181 { 00182 KPopupTitle *titleItem = new KPopupTitle(); 00183 titleItem->setTitle(text); 00184 int ret = insertItem(titleItem, id, index); 00185 setItemEnabled(ret, false); 00186 return ret; 00187 } 00188 00189 int KPopupMenu::insertTitle(const QPixmap &icon, const QString &text, int id, 00190 int index) 00191 { 00192 KPopupTitle *titleItem = new KPopupTitle(); 00193 titleItem->setTitle(text, &icon); 00194 int ret = insertItem(titleItem, id, index); 00195 setItemEnabled(ret, false); 00196 return ret; 00197 } 00198 00199 void KPopupMenu::changeTitle(int id, const QString &text) 00200 { 00201 QMenuItem *item = findItem(id); 00202 if(item){ 00203 if(item->widget()) 00204 ((KPopupTitle *)item->widget())->setTitle(text); 00205 #ifndef NDEBUG 00206 else 00207 kdWarning() << "KPopupMenu: changeTitle() called with non-title id "<< id << endl; 00208 #endif 00209 } 00210 #ifndef NDEBUG 00211 else 00212 kdWarning() << "KPopupMenu: changeTitle() called with invalid id " << id << endl; 00213 #endif 00214 } 00215 00216 void KPopupMenu::changeTitle(int id, const QPixmap &icon, const QString &text) 00217 { 00218 QMenuItem *item = findItem(id); 00219 if(item){ 00220 if(item->widget()) 00221 ((KPopupTitle *)item->widget())->setTitle(text, &icon); 00222 #ifndef NDEBUG 00223 else 00224 kdWarning() << "KPopupMenu: changeTitle() called with non-title id "<< id << endl; 00225 #endif 00226 } 00227 #ifndef NDEBUG 00228 else 00229 kdWarning() << "KPopupMenu: changeTitle() called with invalid id " << id << endl; 00230 #endif 00231 } 00232 00233 QString KPopupMenu::title(int id) const 00234 { 00235 if(id == -1) // obsolete 00236 return d->m_lastTitle; 00237 QMenuItem *item = findItem(id); 00238 if(item){ 00239 if(item->widget()) 00240 return ((KPopupTitle *)item->widget())->title(); 00241 else 00242 qWarning("KPopupMenu: title() called with non-title id %d.", id); 00243 } 00244 else 00245 qWarning("KPopupMenu: title() called with invalid id %d.", id); 00246 return QString::null; 00247 } 00248 00249 QPixmap KPopupMenu::titlePixmap(int id) const 00250 { 00251 QMenuItem *item = findItem(id); 00252 if(item){ 00253 if(item->widget()) 00254 return ((KPopupTitle *)item->widget())->icon(); 00255 else 00256 qWarning("KPopupMenu: titlePixmap() called with non-title id %d.", id); 00257 } 00258 else 00259 qWarning("KPopupMenu: titlePixmap() called with invalid id %d.", id); 00260 QPixmap tmp; 00261 return tmp; 00262 } 00263 00267 void KPopupMenu::closeEvent(QCloseEvent*e) 00268 { 00269 if (d->shortcuts) 00270 resetKeyboardVars(); 00271 QPopupMenu::closeEvent(e); 00272 } 00273 00274 void KPopupMenu::activateItemAt(int index) 00275 { 00276 d->state = Qt::NoButton; 00277 QPopupMenu::activateItemAt(index); 00278 } 00279 00280 Qt::ButtonState KPopupMenu::state() const 00281 { 00282 return d->state; 00283 } 00284 00285 void KPopupMenu::keyPressEvent(QKeyEvent* e) 00286 { 00287 d->state = Qt::NoButton; 00288 if (!d->shortcuts) { 00289 // continue event processing by Qpopup 00290 //e->ignore(); 00291 d->state = e->state(); 00292 QPopupMenu::keyPressEvent(e); 00293 return; 00294 } 00295 00296 int i = 0; 00297 bool firstpass = true; 00298 QString keyString = e->text(); 00299 00300 // check for common commands dealt with by QPopup 00301 int key = e->key(); 00302 if (key == Key_Escape || key == Key_Return || key == Key_Enter 00303 || key == Key_Up || key == Key_Down || key == Key_Left 00304 || key == Key_Right || key == Key_F1) { 00305 00306 resetKeyboardVars(); 00307 // continue event processing by Qpopup 00308 //e->ignore(); 00309 d->state = e->state(); 00310 QPopupMenu::keyPressEvent(e); 00311 return; 00312 } else if ( key == Key_Shift || key == Key_Control || key == Key_Alt || key == Key_Meta ) 00313 return QPopupMenu::keyPressEvent(e); 00314 00315 // check to see if the user wants to remove a key from the sequence (backspace) 00316 // or clear the sequence (delete) 00317 if (!d->keySeq.isNull()) { 00318 00319 if (key == Key_Backspace) { 00320 00321 if (d->keySeq.length() == 1) { 00322 resetKeyboardVars(); 00323 return; 00324 } 00325 00326 // keep the last sequence in keyString 00327 keyString = d->keySeq.left(d->keySeq.length() - 1); 00328 00329 // allow sequence matching to be tried again 00330 resetKeyboardVars(); 00331 00332 } else if (key == Key_Delete) { 00333 resetKeyboardVars(); 00334 00335 // clear active item 00336 setActiveItem(0); 00337 return; 00338 00339 } else if (d->noMatches) { 00340 // clear if there are no matches 00341 resetKeyboardVars(); 00342 00343 // clear active item 00344 setActiveItem(0); 00345 00346 } else { 00347 // the key sequence is not a null string 00348 // therefore the lastHitIndex is valid 00349 i = d->lastHitIndex; 00350 } 00351 } else if (key == Key_Backspace && parentMenu) { 00352 // backspace with no chars in the buffer... go back a menu. 00353 hide(); 00354 resetKeyboardVars(); 00355 return; 00356 } 00357 00358 d->keySeq += keyString; 00359 int seqLen = d->keySeq.length(); 00360 00361 for (; i < (int)count(); i++) { 00362 // compare typed text with text of this entry 00363 int j = idAt(i); 00364 00365 // don't search disabled entries 00366 if (!isItemEnabled(j)) 00367 continue; 00368 00369 QString thisText; 00370 00371 // retrieve the right text 00372 // (the last selected item one may have additional ampersands) 00373 if (i == d->lastHitIndex) 00374 thisText = d->originalText; 00375 else 00376 thisText = text(j); 00377 00378 // if there is an accelerator present, remove it 00379 if ((int)accel(j) != 0) 00380 thisText = thisText.replace("&", QString::null); 00381 00382 // chop text to the search length 00383 thisText = thisText.left(seqLen); 00384 00385 // do the search 00386 if (!thisText.find(d->keySeq, 0, false)) { 00387 00388 if (firstpass) { 00389 // match 00390 setActiveItem(i); 00391 00392 // check to see if we're underlining a different item 00393 if (d->lastHitIndex != i) 00394 // yes; revert the underlining 00395 changeItem(idAt(d->lastHitIndex), d->originalText); 00396 00397 // set the original text if it's a different item 00398 if (d->lastHitIndex != i || d->lastHitIndex == -1) 00399 d->originalText = text(j); 00400 00401 // underline the currently selected item 00402 changeItem(j, underlineText(d->originalText, d->keySeq.length())); 00403 00404 // remember what's going on 00405 d->lastHitIndex = i; 00406 00407 // start/restart the clear timer 00408 d->clearTimer.start(5000, true); 00409 00410 // go around for another try, to see if we can execute 00411 firstpass = false; 00412 } else { 00413 // don't allow execution 00414 return; 00415 } 00416 } 00417 00418 // fall through to allow execution 00419 } 00420 00421 if (!firstpass) { 00422 if (d->autoExec) { 00423 // activate anything 00424 activateItemAt(d->lastHitIndex); 00425 resetKeyboardVars(); 00426 00427 } else if (findItem(idAt(d->lastHitIndex)) && 00428 findItem(idAt(d->lastHitIndex))->popup()) { 00429 // only activate sub-menus 00430 activateItemAt(d->lastHitIndex); 00431 resetKeyboardVars(); 00432 } 00433 00434 return; 00435 } 00436 00437 // no matches whatsoever, clean up 00438 resetKeyboardVars(true); 00439 //e->ignore(); 00440 QPopupMenu::keyPressEvent(e); 00441 } 00442 00443 bool KPopupMenu::focusNextPrevChild( bool next ) 00444 { 00445 resetKeyboardVars(); 00446 return QPopupMenu::focusNextPrevChild( next ); 00447 } 00448 00449 QString KPopupMenu::underlineText(const QString& text, uint length) 00450 { 00451 QString ret = text; 00452 for (uint i = 0; i < length; i++) { 00453 if (ret[2*i] != '&') 00454 ret.insert(2*i, "&"); 00455 } 00456 return ret; 00457 } 00458 00459 void KPopupMenu::resetKeyboardVars(bool noMatches /* = false */) 00460 { 00461 // Clean up keyboard variables 00462 if (d->lastHitIndex != -1) { 00463 changeItem(idAt(d->lastHitIndex), d->originalText); 00464 d->lastHitIndex = -1; 00465 } 00466 00467 if (!noMatches) { 00468 d->keySeq = QString::null; 00469 } 00470 00471 d->noMatches = noMatches; 00472 } 00473 00474 void KPopupMenu::setKeyboardShortcutsEnabled(bool enable) 00475 { 00476 d->shortcuts = enable; 00477 } 00478 00479 void KPopupMenu::setKeyboardShortcutsExecute(bool enable) 00480 { 00481 d->autoExec = enable; 00482 } 00491 void KPopupMenu::mousePressEvent(QMouseEvent* e) 00492 { 00493 if (d->m_ctxMenu && d->m_ctxMenu->isVisible()) 00494 { 00495 // hide on a second context menu event 00496 d->m_ctxMenu->hide(); 00497 } 00498 00499 QPopupMenu::mousePressEvent(e); 00500 } 00501 00502 void KPopupMenu::mouseReleaseEvent(QMouseEvent* e) 00503 { 00504 // Save the button, and the modifiers from state() 00505 d->state = Qt::ButtonState(e->button() | (e->state() & KeyButtonMask)); 00506 00507 if ( !d->m_ctxMenu || !d->m_ctxMenu->isVisible() ) 00508 QPopupMenu::mouseReleaseEvent(e); 00509 } 00510 00511 QPopupMenu* KPopupMenu::contextMenu() 00512 { 00513 if (!d->m_ctxMenu) 00514 { 00515 d->m_ctxMenu = new QPopupMenu(this); 00516 connect(d->m_ctxMenu, SIGNAL(aboutToHide()), this, SLOT(ctxMenuHiding())); 00517 } 00518 00519 return d->m_ctxMenu; 00520 } 00521 00522 const QPopupMenu* KPopupMenu::contextMenu() const 00523 { 00524 return const_cast< KPopupMenu* >( this )->contextMenu(); 00525 } 00526 00527 void KPopupMenu::hideContextMenu() 00528 { 00529 KPopupMenuPrivate::s_continueCtxMenuShow = false; 00530 } 00531 00532 int KPopupMenu::contextMenuFocusItem() 00533 { 00534 return KPopupMenuPrivate::s_highlightedItem; 00535 } 00536 00537 KPopupMenu* KPopupMenu::contextMenuFocus() 00538 { 00539 return KPopupMenuPrivate::s_contextedMenu; 00540 } 00541 00542 void KPopupMenu::itemHighlighted(int /* whichItem */) 00543 { 00544 if (!d->m_ctxMenu || !d->m_ctxMenu->isVisible()) 00545 { 00546 return; 00547 } 00548 00549 d->m_ctxMenu->hide(); 00550 showCtxMenu(mapFromGlobal(QCursor::pos())); 00551 } 00552 00553 void KPopupMenu::showCtxMenu(QPoint pos) 00554 { 00555 QMenuItem* item = findItem(KPopupMenuPrivate::s_highlightedItem); 00556 if (item) 00557 { 00558 QPopupMenu* subMenu = item->popup(); 00559 if (subMenu) 00560 { 00561 disconnect(subMenu, SIGNAL(aboutToShow()), this, SLOT(ctxMenuHideShowingMenu())); 00562 } 00563 } 00564 00565 KPopupMenuPrivate::s_highlightedItem = idAt(pos); 00566 00567 if (KPopupMenuPrivate::s_highlightedItem == -1) 00568 { 00569 KPopupMenuPrivate::s_contextedMenu = 0; 00570 return; 00571 } 00572 00573 emit aboutToShowContextMenu(this, KPopupMenuPrivate::s_highlightedItem, d->m_ctxMenu); 00574 00575 QPopupMenu* subMenu = findItem(KPopupMenuPrivate::s_highlightedItem)->popup(); 00576 if (subMenu) 00577 { 00578 connect(subMenu, SIGNAL(aboutToShow()), SLOT(ctxMenuHideShowingMenu())); 00579 QTimer::singleShot(100, subMenu, SLOT(hide())); 00580 } 00581 00582 if (!KPopupMenuPrivate::s_continueCtxMenuShow) 00583 { 00584 KPopupMenuPrivate::s_continueCtxMenuShow = true; 00585 return; 00586 } 00587 00588 KPopupMenuPrivate::s_contextedMenu = this; 00589 d->m_ctxMenu->popup(this->mapToGlobal(pos)); 00590 connect(this, SIGNAL(highlighted(int)), this, SLOT(itemHighlighted(int))); 00591 } 00592 00593 /* 00594 * this method helps prevent submenus popping up while we have a context menu 00595 * showing 00596 */ 00597 void KPopupMenu::ctxMenuHideShowingMenu() 00598 { 00599 QMenuItem* item = findItem(KPopupMenuPrivate::s_highlightedItem); 00600 if (item) 00601 { 00602 QPopupMenu* subMenu = item->popup(); 00603 if (subMenu) 00604 { 00605 QTimer::singleShot(0, subMenu, SLOT(hide())); 00606 } 00607 } 00608 } 00609 00610 void KPopupMenu::ctxMenuHiding() 00611 { 00612 if (KPopupMenuPrivate::s_highlightedItem) 00613 { 00614 QPopupMenu* subMenu = findItem(KPopupMenuPrivate::s_highlightedItem)->popup(); 00615 if (subMenu) 00616 { 00617 disconnect(subMenu, SIGNAL(aboutToShow()), this, SLOT(ctxMenuHideShowingMenu())); 00618 } 00619 } 00620 00621 disconnect(this, SIGNAL(highlighted(int)), this, SLOT(itemHighlighted(int))); 00622 KPopupMenuPrivate::s_continueCtxMenuShow = true; 00623 } 00624 00625 void KPopupMenu::contextMenuEvent(QContextMenuEvent* e) 00626 { 00627 if (d->m_ctxMenu) 00628 { 00629 if (e->reason() == QContextMenuEvent::Mouse) 00630 { 00631 showCtxMenu(e->pos()); 00632 } 00633 else if (actItem != -1) 00634 { 00635 showCtxMenu(itemGeometry(actItem).center()); 00636 } 00637 00638 e->accept(); 00639 return; 00640 } 00641 00642 QPopupMenu::contextMenuEvent(e); 00643 } 00644 00645 void KPopupMenu::hideEvent(QHideEvent*) 00646 { 00647 if (d->m_ctxMenu && d->m_ctxMenu->isVisible()) 00648 { 00649 // we need to block signals here when the ctxMenu is showing 00650 // to prevent the QPopupMenu::activated(int) signal from emitting 00651 // when hiding with a context menu, the user doesn't expect the 00652 // menu to actually do anything. 00653 // since hideEvent gets called very late in the process of hiding 00654 // (deep within QWidget::hide) the activated(int) signal is the 00655 // last signal to be emitted, even after things like aboutToHide() 00656 // AJS 00657 blockSignals(true); 00658 d->m_ctxMenu->hide(); 00659 blockSignals(false); 00660 } 00661 } 00666 // Obsolete 00667 KPopupMenu::KPopupMenu(const QString& title, QWidget *parent, const char *name) 00668 : QPopupMenu(parent, name) 00669 { 00670 d = new KPopupMenuPrivate; 00671 insertTitle(title); 00672 } 00673 00674 // Obsolete 00675 void KPopupMenu::setTitle(const QString &title) 00676 { 00677 KPopupTitle *titleItem = new KPopupTitle(); 00678 titleItem->setTitle(title); 00679 insertItem(titleItem); 00680 d->m_lastTitle = title; 00681 } 00682 00683 void KPopupTitle::virtual_hook( int, void* ) 00684 { /*BASE::virtual_hook( id, data );*/ } 00685 00686 void KPopupMenu::virtual_hook( int, void* ) 00687 { /*BASE::virtual_hook( id, data );*/ } 00688 00689 #include "kpopupmenu.moc"