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     p.setPen(palette().active().highlight());
00113     p.drawLine(0, 0, r.right(), 0);
00114 }
00115 
00116 QSize KPopupTitle::sizeHint() const
00117 {
00118     return minimumSize();
00119 }
00120 
00121 class KPopupMenu::KPopupMenuPrivate
00122 {
00123 public:
00124     KPopupMenuPrivate ()
00125         : noMatches(false)
00126         , shortcuts(false)
00127         , autoExec(false)
00128         , lastHitIndex(-1)
00129         , state(Qt::NoButton)
00130         , m_ctxMenu(0)
00131     {}
00132 
00133     ~KPopupMenuPrivate ()
00134     {
00135         delete m_ctxMenu;
00136     }
00137 
00138     QString m_lastTitle;
00139 
00140     // variables for keyboard navigation
00141     QTimer clearTimer;
00142 
00143     bool noMatches : 1;
00144     bool shortcuts : 1;
00145     bool autoExec : 1;
00146 
00147     QString keySeq;
00148     QString originalText;
00149 
00150     int lastHitIndex;
00151     Qt::ButtonState state;
00152 
00153     // support for RMB menus on menus
00154     QPopupMenu* m_ctxMenu;
00155     static bool s_continueCtxMenuShow;
00156     static int s_highlightedItem;
00157     static KPopupMenu* s_contextedMenu;
00158 };
00159 
00160 int KPopupMenu::KPopupMenuPrivate::s_highlightedItem(-1);
00161 KPopupMenu* KPopupMenu::KPopupMenuPrivate::s_contextedMenu(0);
00162 bool KPopupMenu::KPopupMenuPrivate::s_continueCtxMenuShow(true);
00163 
00164 KPopupMenu::KPopupMenu(QWidget *parent, const char *name)
00165     : QPopupMenu(parent, name)
00166 {
00167     d = new KPopupMenuPrivate;
00168     resetKeyboardVars();
00169     connect(&(d->clearTimer), SIGNAL(timeout()), SLOT(resetKeyboardVars()));
00170 }
00171 
00172 KPopupMenu::~KPopupMenu()
00173 {
00174     if (KPopupMenuPrivate::s_contextedMenu == this)
00175     {
00176         KPopupMenuPrivate::s_contextedMenu = 0;
00177         KPopupMenuPrivate::s_highlightedItem = -1;
00178     }
00179 
00180     delete d;
00181 }
00182 
00183 int KPopupMenu::insertTitle(const QString &text, int id, int index)
00184 {
00185     KPopupTitle *titleItem = new KPopupTitle();
00186     titleItem->setTitle(text);
00187     int ret = insertItem(titleItem, id, index);
00188     setItemEnabled(ret, false);
00189     return ret;
00190 }
00191 
00192 int KPopupMenu::insertTitle(const QPixmap &icon, const QString &text, int id,
00193                             int index)
00194 {
00195     KPopupTitle *titleItem = new KPopupTitle();
00196     titleItem->setTitle(text, &icon);
00197     int ret = insertItem(titleItem, id, index);
00198     setItemEnabled(ret, false);
00199     return ret;
00200 }
00201 
00202 void KPopupMenu::changeTitle(int id, const QString &text)
00203 {
00204     QMenuItem *item = findItem(id);
00205     if(item){
00206         if(item->widget())
00207             ((KPopupTitle *)item->widget())->setTitle(text);
00208 #ifndef NDEBUG
00209         else
00210             kdWarning() << "KPopupMenu: changeTitle() called with non-title id "<< id << endl;
00211 #endif
00212     }
00213 #ifndef NDEBUG
00214     else
00215         kdWarning() << "KPopupMenu: changeTitle() called with invalid id " << id << endl;
00216 #endif
00217 }
00218 
00219 void KPopupMenu::changeTitle(int id, const QPixmap &icon, const QString &text)
00220 {
00221     QMenuItem *item = findItem(id);
00222     if(item){
00223         if(item->widget())
00224             ((KPopupTitle *)item->widget())->setTitle(text, &icon);
00225 #ifndef NDEBUG
00226         else
00227             kdWarning() << "KPopupMenu: changeTitle() called with non-title id "<< id << endl;
00228 #endif
00229     }
00230 #ifndef NDEBUG
00231     else
00232         kdWarning() << "KPopupMenu: changeTitle() called with invalid id " << id << endl;
00233 #endif
00234 }
00235 
00236 QString KPopupMenu::title(int id) const
00237 {
00238     if(id == -1) // obsolete
00239         return d->m_lastTitle;
00240     QMenuItem *item = findItem(id);
00241     if(item){
00242         if(item->widget())
00243             return ((KPopupTitle *)item->widget())->title();
00244         else
00245             qWarning("KPopupMenu: title() called with non-title id %d.", id);
00246     }
00247     else
00248         qWarning("KPopupMenu: title() called with invalid id %d.", id);
00249     return QString::null;
00250 }
00251 
00252 QPixmap KPopupMenu::titlePixmap(int id) const
00253 {
00254     QMenuItem *item = findItem(id);
00255     if(item){
00256         if(item->widget())
00257             return ((KPopupTitle *)item->widget())->icon();
00258         else
00259             qWarning("KPopupMenu: titlePixmap() called with non-title id %d.", id);
00260     }
00261     else
00262         qWarning("KPopupMenu: titlePixmap() called with invalid id %d.", id);
00263     QPixmap tmp;
00264     return tmp;
00265 }
00266 
00270 void KPopupMenu::closeEvent(QCloseEvent*e)
00271 {
00272     if (d->shortcuts)
00273         resetKeyboardVars();
00274     QPopupMenu::closeEvent(e);
00275 }
00276 
00277 void KPopupMenu::activateItemAt(int index)
00278 {
00279     d->state = Qt::NoButton;
00280     QPopupMenu::activateItemAt(index);
00281 }
00282 
00283 Qt::ButtonState KPopupMenu::state() const
00284 {
00285     return d->state;
00286 }
00287 
00288 void KPopupMenu::keyPressEvent(QKeyEvent* e)
00289 {
00290     d->state = Qt::NoButton;
00291     if (!d->shortcuts) {
00292         // continue event processing by Qpopup
00293         //e->ignore();
00294         d->state = e->state();
00295         QPopupMenu::keyPressEvent(e);
00296         return;
00297     }
00298 
00299     int i = 0;
00300     bool firstpass = true;
00301     QString keyString = e->text();
00302 
00303     // check for common commands dealt with by QPopup
00304     int key = e->key();
00305     if (key == Key_Escape || key == Key_Return || key == Key_Enter
00306             || key == Key_Up || key == Key_Down || key == Key_Left
00307             || key == Key_Right || key == Key_F1) {
00308 
00309         resetKeyboardVars();
00310         // continue event processing by Qpopup
00311         //e->ignore();
00312         d->state = e->state();
00313         QPopupMenu::keyPressEvent(e);
00314         return;
00315     } else if ( key == Key_Shift || key == Key_Control || key == Key_Alt || key == Key_Meta )
00316     return QPopupMenu::keyPressEvent(e);
00317 
00318     // check to see if the user wants to remove a key from the sequence (backspace)
00319     // or clear the sequence (delete)
00320     if (!d->keySeq.isNull()) {
00321 
00322         if (key == Key_Backspace) {
00323 
00324             if (d->keySeq.length() == 1) {
00325                 resetKeyboardVars();
00326                 return;
00327             }
00328 
00329             // keep the last sequence in keyString
00330             keyString = d->keySeq.left(d->keySeq.length() - 1);
00331 
00332             // allow sequence matching to be tried again
00333             resetKeyboardVars();
00334 
00335         } else if (key == Key_Delete) {
00336             resetKeyboardVars();
00337 
00338             // clear active item
00339             setActiveItem(0);
00340             return;
00341 
00342         } else if (d->noMatches) {
00343             // clear if there are no matches
00344             resetKeyboardVars();
00345 
00346             // clear active item
00347             setActiveItem(0);
00348 
00349         } else {
00350             // the key sequence is not a null string
00351             // therefore the lastHitIndex is valid
00352             i = d->lastHitIndex;
00353         }
00354     } else if (key == Key_Backspace && parentMenu) {
00355         // backspace with no chars in the buffer... go back a menu.
00356         hide();
00357         resetKeyboardVars();
00358         return;
00359     }
00360 
00361     d->keySeq += keyString;
00362     int seqLen = d->keySeq.length();
00363 
00364     for (; i < (int)count(); i++) {
00365         // compare typed text with text of this entry
00366         int j = idAt(i);
00367 
00368         // don't search disabled entries
00369         if (!isItemEnabled(j))
00370             continue;
00371 
00372         QString thisText;
00373 
00374         // retrieve the right text
00375         // (the last selected item one may have additional ampersands)
00376         if (i == d->lastHitIndex)
00377             thisText = d->originalText;
00378         else
00379             thisText = text(j);
00380 
00381         // if there is an accelerator present, remove it
00382         if ((int)accel(j) != 0)
00383             thisText = thisText.replace("&", QString::null);
00384 
00385         // chop text to the search length
00386         thisText = thisText.left(seqLen);
00387 
00388         // do the search
00389         if (!thisText.find(d->keySeq, 0, false)) {
00390 
00391             if (firstpass) {
00392                 // match
00393                 setActiveItem(i);
00394 
00395                 // check to see if we're underlining a different item
00396                 if (d->lastHitIndex != i)
00397                     // yes; revert the underlining
00398                     changeItem(idAt(d->lastHitIndex), d->originalText);
00399 
00400                 // set the original text if it's a different item
00401                 if (d->lastHitIndex != i || d->lastHitIndex == -1)
00402                     d->originalText = text(j);
00403 
00404                 // underline the currently selected item
00405                 changeItem(j, underlineText(d->originalText, d->keySeq.length()));
00406 
00407                 // remember what's going on
00408                 d->lastHitIndex = i;
00409 
00410                 // start/restart the clear timer
00411                 d->clearTimer.start(5000, true);
00412 
00413                 // go around for another try, to see if we can execute
00414                 firstpass = false;
00415             } else {
00416                 // don't allow execution
00417                 return;
00418             }
00419         }
00420 
00421         // fall through to allow execution
00422     }
00423 
00424     if (!firstpass) {
00425         if (d->autoExec) {
00426             // activate anything
00427             activateItemAt(d->lastHitIndex);
00428             resetKeyboardVars();
00429 
00430         } else if (findItem(idAt(d->lastHitIndex)) &&
00431                  findItem(idAt(d->lastHitIndex))->popup()) {
00432             // only activate sub-menus
00433             activateItemAt(d->lastHitIndex);
00434             resetKeyboardVars();
00435         }
00436 
00437         return;
00438     }
00439 
00440     // no matches whatsoever, clean up
00441     resetKeyboardVars(true);
00442     //e->ignore();
00443     QPopupMenu::keyPressEvent(e);
00444 }
00445 
00446 bool KPopupMenu::focusNextPrevChild( bool next )
00447 {
00448     resetKeyboardVars();
00449     return QPopupMenu::focusNextPrevChild( next );
00450 }
00451 
00452 QString KPopupMenu::underlineText(const QString& text, uint length)
00453 {
00454     QString ret = text;
00455     for (uint i = 0; i < length; i++) {
00456         if (ret[2*i] != '&')
00457             ret.insert(2*i, "&");
00458     }
00459     return ret;
00460 }
00461 
00462 void KPopupMenu::resetKeyboardVars(bool noMatches /* = false */)
00463 {
00464     // Clean up keyboard variables
00465     if (d->lastHitIndex != -1) {
00466         changeItem(idAt(d->lastHitIndex), d->originalText);
00467         d->lastHitIndex = -1;
00468     }
00469 
00470     if (!noMatches) {
00471         d->keySeq = QString::null;
00472     }
00473 
00474     d->noMatches = noMatches;
00475 }
00476 
00477 void KPopupMenu::setKeyboardShortcutsEnabled(bool enable)
00478 {
00479     d->shortcuts = enable;
00480 }
00481 
00482 void KPopupMenu::setKeyboardShortcutsExecute(bool enable)
00483 {
00484     d->autoExec = enable;
00485 }
00494 void KPopupMenu::mousePressEvent(QMouseEvent* e)
00495 {
00496     if (d->m_ctxMenu && d->m_ctxMenu->isVisible())
00497     {
00498         // hide on a second context menu event
00499         d->m_ctxMenu->hide();
00500     }
00501 
00502     QPopupMenu::mousePressEvent(e);
00503 }
00504 
00505 void KPopupMenu::mouseReleaseEvent(QMouseEvent* e)
00506 {
00507     // Save the button, and the modifiers from state()
00508     d->state = Qt::ButtonState(e->button() | (e->state() & KeyButtonMask));
00509     
00510     if ( !d->m_ctxMenu || !d->m_ctxMenu->isVisible() )
00511     QPopupMenu::mouseReleaseEvent(e);
00512 }
00513 
00514 QPopupMenu* KPopupMenu::contextMenu()
00515 {
00516     if (!d->m_ctxMenu)
00517     {
00518         d->m_ctxMenu = new QPopupMenu(this);
00519         connect(d->m_ctxMenu, SIGNAL(aboutToHide()), this, SLOT(ctxMenuHiding()));
00520     }
00521 
00522     return d->m_ctxMenu;
00523 }
00524 
00525 const QPopupMenu* KPopupMenu::contextMenu() const
00526 {
00527     return const_cast< KPopupMenu* >( this )->contextMenu();
00528 }
00529 
00530 void KPopupMenu::hideContextMenu()
00531 {
00532     KPopupMenuPrivate::s_continueCtxMenuShow = false;
00533 }
00534 
00535 int KPopupMenu::contextMenuFocusItem()
00536 {
00537     return KPopupMenuPrivate::s_highlightedItem;
00538 }
00539 
00540 KPopupMenu* KPopupMenu::contextMenuFocus()
00541 {
00542     return KPopupMenuPrivate::s_contextedMenu;
00543 }
00544 
00545 void KPopupMenu::itemHighlighted(int /* whichItem */)
00546 {
00547     if (!d->m_ctxMenu || !d->m_ctxMenu->isVisible())
00548     {
00549         return;
00550     }
00551 
00552     d->m_ctxMenu->hide();
00553     showCtxMenu(mapFromGlobal(QCursor::pos()));
00554 }
00555 
00556 void KPopupMenu::showCtxMenu(QPoint pos)
00557 {
00558     QMenuItem* item = findItem(KPopupMenuPrivate::s_highlightedItem);
00559     if (item)
00560     {
00561         QPopupMenu* subMenu = item->popup();
00562         if (subMenu)
00563         {
00564             disconnect(subMenu, SIGNAL(aboutToShow()), this, SLOT(ctxMenuHideShowingMenu()));
00565         }
00566     }
00567 
00568     KPopupMenuPrivate::s_highlightedItem = idAt(pos);
00569 
00570     if (KPopupMenuPrivate::s_highlightedItem == -1)
00571     {
00572         KPopupMenuPrivate::s_contextedMenu = 0;
00573         return;
00574     }
00575 
00576     emit aboutToShowContextMenu(this, KPopupMenuPrivate::s_highlightedItem, d->m_ctxMenu);
00577 
00578     QPopupMenu* subMenu = findItem(KPopupMenuPrivate::s_highlightedItem)->popup();
00579     if (subMenu)
00580     {
00581         connect(subMenu, SIGNAL(aboutToShow()), SLOT(ctxMenuHideShowingMenu()));
00582         QTimer::singleShot(100, subMenu, SLOT(hide()));
00583     }
00584 
00585     if (!KPopupMenuPrivate::s_continueCtxMenuShow)
00586     {
00587         KPopupMenuPrivate::s_continueCtxMenuShow = true;
00588         return;
00589     }
00590 
00591     KPopupMenuPrivate::s_contextedMenu = this;
00592     d->m_ctxMenu->popup(this->mapToGlobal(pos));
00593     connect(this, SIGNAL(highlighted(int)), this, SLOT(itemHighlighted(int)));
00594 }
00595 
00596 /*
00597  * this method helps prevent submenus popping up while we have a context menu
00598  * showing
00599  */
00600 void KPopupMenu::ctxMenuHideShowingMenu()
00601 {
00602     QMenuItem* item = findItem(KPopupMenuPrivate::s_highlightedItem);
00603     if (item)
00604     {
00605         QPopupMenu* subMenu = item->popup();
00606         if (subMenu)
00607         {
00608             QTimer::singleShot(0, subMenu, SLOT(hide()));
00609         }
00610     }
00611 }
00612 
00613 void KPopupMenu::ctxMenuHiding()
00614 {
00615     if (KPopupMenuPrivate::s_highlightedItem)
00616     {
00617         QPopupMenu* subMenu = findItem(KPopupMenuPrivate::s_highlightedItem)->popup();
00618         if (subMenu)
00619         {
00620             disconnect(subMenu, SIGNAL(aboutToShow()), this, SLOT(ctxMenuHideShowingMenu()));
00621         }
00622     }
00623 
00624     disconnect(this, SIGNAL(highlighted(int)), this, SLOT(itemHighlighted(int)));
00625     KPopupMenuPrivate::s_continueCtxMenuShow = true;
00626 }
00627 
00628 void KPopupMenu::contextMenuEvent(QContextMenuEvent* e)
00629 {
00630     if (d->m_ctxMenu)
00631     {
00632         if (e->reason() == QContextMenuEvent::Mouse)
00633         {
00634             showCtxMenu(e->pos());
00635         }
00636         else if (actItem != -1)
00637         {
00638             showCtxMenu(itemGeometry(actItem).center());
00639         }
00640 
00641         e->accept();
00642         return;
00643     }
00644 
00645     QPopupMenu::contextMenuEvent(e);
00646 }
00647 
00648 void KPopupMenu::hideEvent(QHideEvent*)
00649 {
00650     if (d->m_ctxMenu && d->m_ctxMenu->isVisible())
00651     {
00652         // we need to block signals here when the ctxMenu is showing
00653         // to prevent the QPopupMenu::activated(int) signal from emitting
00654         // when hiding with a context menu, the user doesn't expect the
00655         // menu to actually do anything.
00656         // since hideEvent gets called very late in the process of hiding
00657         // (deep within QWidget::hide) the activated(int) signal is the
00658         // last signal to be emitted, even after things like aboutToHide()
00659         // AJS
00660         blockSignals(true);
00661         d->m_ctxMenu->hide();
00662         blockSignals(false);
00663     }
00664 }
00669 // Obsolete
00670 KPopupMenu::KPopupMenu(const QString& title, QWidget *parent, const char *name)
00671     : QPopupMenu(parent, name)
00672 {
00673     d = new KPopupMenuPrivate;
00674     insertTitle(title);
00675 }
00676 
00677 // Obsolete
00678 void KPopupMenu::setTitle(const QString &title)
00679 {
00680     KPopupTitle *titleItem = new KPopupTitle();
00681     titleItem->setTitle(title);
00682     insertItem(titleItem);
00683     d->m_lastTitle = title;
00684 }
00685 
00686 void KPopupTitle::virtual_hook( int, void* )
00687 { /*BASE::virtual_hook( id, data );*/ }
00688 
00689 void KPopupMenu::virtual_hook( int, void* )
00690 { /*BASE::virtual_hook( id, data );*/ }
00691 
00692 #include "kpopupmenu.moc"
KDE Home | KDE Accessibility Home | Description of Access Keys