kdecore Library API Documentation

kaccelmanager.cpp

00001 /*  This file is part of the KDE project
00002     Copyright (C) 2002 Matthias Hölzer-Klüpfel <mhk@kde.org>
00003 
00004     This library is free software; you can redistribute it and/or
00005     modify it under the terms of the GNU Library General Public
00006     License as published by the Free Software Foundation; either
00007     version 2 of the License, or (at your option) any later version.
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., 59 Temple Place - Suite 330,
00017     Boston, MA 02111-1307, USA.
00018 */
00019 
00020 
00021 #include <qwidget.h>
00022 #include <qobjectlist.h>
00023 #include <qapplication.h>
00024 #include <qpopupmenu.h>
00025 #include <qmenubar.h>
00026 #include <qmemarray.h>
00027 #include <qmainwindow.h>
00028 #include <qtabbar.h>
00029 #include <qwidgetstack.h>
00030 #include <qlabel.h>
00031 #include <qptrlist.h>
00032 
00033 #include <kstdaction.h>
00034 #include <kstaticdeleter.h>
00035 #include <kdebug.h>
00036 
00037 
00038 #include "kaccelmanager_private.h"
00039 
00040 
00041 #include "kaccelmanager.h"
00042 
00043 // Default control weight
00044 const int KAccelManagerAlgorithm::DEFAULT_WEIGHT = 50;
00045 // Additional weight for first character in string
00046 const int KAccelManagerAlgorithm::FIRST_CHARACTER_EXTRA_WEIGHT = 50;
00047 // Additional weight for the beginning of a word
00048 const int KAccelManagerAlgorithm::WORD_BEGINNING_EXTRA_WEIGHT = 50;
00049 // Additional weight for the dialog buttons (large, we basically never want these reassigned)
00050 const int KAccelManagerAlgorithm::DIALOG_BUTTON_EXTRA_WEIGHT = 300;
00051 // Additional weight for a 'wanted' accelerator
00052 const int KAccelManagerAlgorithm::WANTED_ACCEL_EXTRA_WEIGHT = 150;
00053 // Default weight for an 'action' widget (ie, pushbuttons)
00054 const int KAccelManagerAlgorithm::ACTION_ELEMENT_WEIGHT = 50;
00055 // Default weight for group boxes (low priority)
00056 const int KAccelManagerAlgorithm::GROUP_BOX_WEIGHT = 0;
00057 // Default weight for menu titles
00058 const int KAccelManagerAlgorithm::MENU_TITLE_WEIGHT = 250;
00059 
00060 
00061 /*********************************************************************
00062 
00063  class Item - helper class containing widget information
00064 
00065  This class stores information about the widgets the need accelerators,
00066  as well as about their relationship.
00067 
00068  *********************************************************************/
00069 
00070 
00071 
00072 /*********************************************************************
00073 
00074  class KAcceleratorManagerPrivate - internal helper class
00075 
00076  This class does all the work to find accelerators for a hierarchy of
00077  widgets.
00078 
00079  *********************************************************************/
00080 
00081 
00082 class KAcceleratorManagerPrivate
00083 {
00084 public:
00085 
00086     static void manage(QWidget *widget);
00087     static bool programmers_mode;
00088     static bool standardName(const QString &str);
00089 
00090 private:
00091   class Item;
00092   typedef QPtrList<Item> ItemList;
00093 
00094   static void traverseChildren(QWidget *widget, Item *item);
00095 
00096   static void manageMenuBar(QMenuBar *mbar, Item *item);
00097   static void manageTabBar(QTabBar *bar, Item *item);
00098 
00099   static void calculateAccelerators(Item *item, QString &used);
00100 
00101   class Item
00102   {
00103   public:
00104 
00105     Item() : m_widget(0), m_children(0), m_index(-1) {};
00106     ~Item();
00107 
00108     void addChild(Item *item);
00109 
00110     QWidget       *m_widget;
00111     KAccelString  m_content;
00112     ItemList      *m_children;
00113     int           m_index;
00114 
00115   };
00116 };
00117 
00118 
00119 bool KAcceleratorManagerPrivate::programmers_mode = false;
00120 static QStringList *kaccmp_sns = 0;
00121 static KStaticDeleter<QStringList> kaccmp_sns_d;
00122 
00123 bool KAcceleratorManagerPrivate::standardName(const QString &str)
00124 {
00125     if (!kaccmp_sns)
00126         kaccmp_sns =  kaccmp_sns_d.setObject(new QStringList(KStdAction::stdNames()));
00127     return kaccmp_sns->contains(str);
00128 }
00129 
00130 KAcceleratorManagerPrivate::Item::~Item()
00131 {
00132   delete m_children;
00133 }
00134 
00135 
00136 void KAcceleratorManagerPrivate::Item::addChild(Item *item)
00137 {
00138     if (!m_children)
00139       m_children = new ItemList;
00140 
00141     m_children->append(item);
00142 }
00143 
00144 void KAcceleratorManagerPrivate::manage(QWidget *widget)
00145 {
00146 /*    widget = qApp->activeWindow();
00147     if ( !widget )
00148         return;
00149 */
00150   if (widget->inherits("QPopupMenu"))
00151   {
00152     // create a popup accel manager that can deal with dynamic menues
00153     KPopupAccelManager::manage(static_cast<QPopupMenu*>(widget));
00154     return;
00155   }
00156 
00157   Item *root = new Item;
00158 
00159   traverseChildren(widget, root);
00160 
00161   QString used;
00162   calculateAccelerators(root, used);
00163   delete root;
00164 }
00165 
00166 
00167 void KAcceleratorManagerPrivate::calculateAccelerators(Item *item, QString &used)
00168 {
00169   if (!item->m_children)
00170     return;
00171 
00172   // collect the contents
00173   KAccelStringList contents;
00174   for (Item *it = item->m_children->first(); it != 0; it = item->m_children->next()) {
00175       contents << it->m_content;
00176   }
00177 
00178   // find the right accelerators
00179   KAccelManagerAlgorithm::findAccelerators(contents, used);
00180 
00181   // write them back into the widgets
00182   int cnt = -1;
00183   for (Item *it = item->m_children->first(); it != 0; it = item->m_children->next())
00184   {
00185     cnt++;
00186 
00187     if (it->m_widget->inherits("QTabBar"))
00188     {
00189       QTabBar *bar = static_cast<QTabBar*>(it->m_widget);
00190       bar->tabAt(it->m_index)->setText(contents[cnt].accelerated());
00191 
00192       continue;
00193     }
00194     if (it->m_widget->inherits("QMenuBar"))
00195     {
00196       QMenuBar *bar = static_cast<QMenuBar*>(it->m_widget);
00197       if (it->m_index >= 0)
00198       {
00199     QMenuItem *mitem = bar->findItem(bar->idAt(it->m_index));
00200     if (mitem)
00201       mitem->setText(contents[cnt].accelerated());
00202       }
00203       continue;
00204     }
00205     if (!it->m_widget->setProperty("text", contents[cnt].accelerated()))
00206       it->m_widget->setProperty("title", contents[cnt].accelerated());
00207   }
00208 
00209   // calculate the accelerators for the children
00210   for (Item *it = item->m_children->first(); it != 0; it = item->m_children->next()) {
00211       if (it->m_widget && it->m_widget->isVisibleTo( item->m_widget ))
00212           calculateAccelerators(it, used);
00213   }
00214 }
00215 
00216 
00217 void KAcceleratorManagerPrivate::traverseChildren(QWidget *widget, Item *item)
00218 {
00219   QObjectList *childList = widget->queryList("QWidget", 0, false, false);
00220   for ( QObject *it = childList->first(); it; it = childList->next() )
00221   {
00222     QWidget *w = static_cast<QWidget*>(it);
00223 
00224     if ( !w->isVisibleTo( widget ) )
00225         continue;
00226 
00227     // first treat the special cases
00228 
00229     if (w->inherits("QTabBar"))
00230     {
00231       manageTabBar(static_cast<QTabBar*>(w), item);
00232       continue;
00233     }
00234 
00235     if (w->inherits("QPopupMenu"))
00236     {
00237       // create a popup accel manager that can deal with dynamic menues
00238       KPopupAccelManager::manage(static_cast<QPopupMenu*>(w));
00239       continue;
00240     }
00241 
00242     if (w->inherits("QMenuBar"))
00243     {
00244       manageMenuBar(static_cast<QMenuBar*>(w), item);
00245       continue;
00246     }
00247 
00248     if (w->inherits("QComboBox") || w->inherits("QLineEdit") || w->inherits("QTextEdit") || w->inherits("QTextView"))
00249         continue;
00250 
00251     // now treat 'ordinary' widgets
00252     if (w->isFocusEnabled() || (w->inherits("QLabel") && static_cast<QLabel*>(w)->buddy()) || w->inherits("QGroupBox"))
00253     {
00254       QString content;
00255       QVariant variant = w->property("text");
00256       if (variant.isValid())
00257           content = variant.toString();
00258 
00259       if (content.isEmpty())
00260       {
00261           variant = w->property("title");
00262           if (variant.isValid())
00263               content = variant.toString();
00264       }
00265 
00266       if (!content.isEmpty())
00267       {
00268           Item *i = new Item;
00269           i->m_widget = w;
00270 
00271           // put some more weight on the usual action elements
00272           int weight = KAccelManagerAlgorithm::DEFAULT_WEIGHT;
00273           if (w->inherits("QPushButton") || w->inherits("QCheckBox") || w->inherits("QRadioButton") || w->inherits("QLabel"))
00274               weight = KAccelManagerAlgorithm::ACTION_ELEMENT_WEIGHT;
00275 
00276           // don't put weight on group boxes, as usally the contents are more important
00277           if (w->inherits("QGroupBox"))
00278               weight = KAccelManagerAlgorithm::GROUP_BOX_WEIGHT;
00279 
00280           // put a lot of extra weight on the KDialogBaseButton's
00281           if (w->inherits("KDialogBaseButton"))
00282               weight += KAccelManagerAlgorithm::DIALOG_BUTTON_EXTRA_WEIGHT;
00283 
00284           i->m_content = KAccelString(content, weight);
00285           item->addChild(i);
00286       }
00287     }
00288 
00289     traverseChildren(w, item);
00290   }
00291   delete childList;
00292 }
00293 
00294 
00295 void KAcceleratorManagerPrivate::manageTabBar(QTabBar *bar, Item *item)
00296 {
00297   for (int i=0; i<bar->count(); i++)
00298   {
00299     QString content = bar->tabAt(i)->text();
00300     if (content.isEmpty())
00301       continue;
00302 
00303     Item *it = new Item;
00304     item->addChild(it);
00305     it->m_widget = bar;
00306     it->m_index = i;
00307     it->m_content = KAccelString(content);
00308   }
00309 }
00310 
00311 
00312 void KAcceleratorManagerPrivate::manageMenuBar(QMenuBar *mbar, Item *item)
00313 {
00314   QMenuItem *mitem;
00315   QString s;
00316 
00317   for (uint i=0; i<mbar->count(); ++i)
00318   {
00319     mitem = mbar->findItem(mbar->idAt(i));
00320     if (!mitem)
00321       continue;
00322 
00323     // nothing to do for separators
00324     if (mitem->isSeparator())
00325       continue;
00326 
00327     s = mitem->text();
00328     if (!s.isEmpty())
00329     {
00330       Item *it = new Item;
00331       item->addChild(it);
00332       it->m_content = KAccelString(s, KAccelManagerAlgorithm::MENU_TITLE_WEIGHT); // menu titles are important, so raise the weight
00333       it->m_widget = mbar;
00334       it->m_index = i;
00335     }
00336 
00337     // have a look at the popup as well, if present
00338     if (mitem->popup())
00339       KPopupAccelManager::manage(mitem->popup());
00340   }
00341 }
00342 
00343 
00344 /*********************************************************************
00345 
00346  class KAcceleratorManager - main entry point
00347 
00348  This class is just here to provide a clean public API...
00349 
00350  *********************************************************************/
00351 
00352 void KAcceleratorManager::manage(QWidget *widget)
00353 {
00354     KAcceleratorManager::manage(widget, false);
00355 }
00356 
00357 void KAcceleratorManager::manage(QWidget *widget, bool programmers_mode)
00358 {
00359     KAcceleratorManagerPrivate::programmers_mode = programmers_mode;
00360     KAcceleratorManagerPrivate::manage(widget);
00361 }
00362 
00363 /*********************************************************************
00364 
00365  class KAccelString - a string with weighted characters
00366 
00367  *********************************************************************/
00368 
00369 KAccelString::KAccelString(const QString &input, int initialWeight)
00370   : m_pureText(input), m_weight()
00371 {
00372     orig_accel = m_pureText.find("(!)&");
00373     m_pureText.replace(orig_accel, 4, "");
00374     orig_accel = m_pureText.find("(&&)");
00375     if (orig_accel != -1)
00376         m_pureText.replace(orig_accel, 4, "&");
00377     orig_accel = m_accel = stripAccelerator(m_pureText);
00378 
00379     kdDebug(125) << input << " " << orig_accel << " " << m_accel << " " << m_pureText << endl;
00380     if (initialWeight == -1)
00381         initialWeight = KAccelManagerAlgorithm::DEFAULT_WEIGHT;
00382 
00383     calculateWeights(initialWeight);
00384 
00385     dump();
00386 }
00387 
00388 
00389 QString KAccelString::accelerated() const
00390 {
00391   QString result = m_pureText;
00392   if (result.isEmpty())
00393       return result;
00394 
00395   if (KAcceleratorManagerPrivate::programmers_mode)
00396   {
00397       int oa = orig_accel;
00398 
00399       if (m_accel >= 0) {
00400           if (m_accel != orig_accel) {
00401               result.insert(m_accel, "(!)&");
00402               if (m_accel < orig_accel)
00403                   oa += 4;
00404           } else {
00405               result.insert(m_accel, "&");
00406               if (m_accel < orig_accel)
00407                   oa++;
00408           }
00409       }
00410 
00411       if (m_accel != orig_accel && orig_accel >= 0)
00412           result.insert(oa, "(&&)");
00413   } else {
00414       if (m_accel >= 0)
00415           result.insert(m_accel, "&");
00416   }
00417 
00418   return result;
00419 }
00420 
00421 
00422 QChar KAccelString::accelerator() const
00423 {
00424   if ((m_accel < 0) || (m_accel > (int)m_pureText.length()))
00425     return QChar();
00426 
00427   return m_pureText[m_accel].lower();
00428 }
00429 
00430 
00431 void KAccelString::calculateWeights(int initialWeight)
00432 {
00433   m_weight.resize(m_pureText.length());
00434 
00435   uint pos = 0;
00436   bool start_character = true;
00437 
00438   while (pos<m_pureText.length())
00439   {
00440     QChar c = m_pureText[pos];
00441 
00442     int weight = initialWeight+1;
00443 
00444     // add special weight to first character
00445     if (pos == 0)
00446       weight += KAccelManagerAlgorithm::FIRST_CHARACTER_EXTRA_WEIGHT;
00447 
00448     // add weight to word beginnings
00449     if (start_character)
00450     {
00451       weight += KAccelManagerAlgorithm::WORD_BEGINNING_EXTRA_WEIGHT;
00452       start_character = false;
00453     }
00454 
00455     // add decreasing weight to left characters
00456     if (pos < 50)
00457       weight += (50-pos);
00458 
00459     // try to preserve the wanted accelarators
00460     if ((int)pos == accel())
00461       weight += KAccelManagerAlgorithm::WANTED_ACCEL_EXTRA_WEIGHT;
00462 
00463     // skip non typeable characters
00464     if (!c.isLetterOrNumber())
00465     {
00466       weight = 0;
00467       start_character = true;
00468     }
00469 
00470     m_weight[pos] = weight;
00471 
00472     ++pos;
00473   }
00474 }
00475 
00476 
00477 int KAccelString::stripAccelerator(QString &text)
00478 {
00479   // Note: this code is derived from QAccel::shortcutKey
00480   int p = 0;
00481 
00482   while (p >= 0)
00483   {
00484     p = text.find('&', p)+1;
00485 
00486     if (p <= 0 || p >= (int)text.length())
00487       return -1;
00488 
00489     if (text[p] != '&')
00490     {
00491       QChar c = text[p];
00492       if (c.isPrint())
00493       {
00494         text.remove(p-1,1);
00495     return p-1;
00496       }
00497     }
00498 
00499     p++;
00500   }
00501 
00502   return -1;
00503 }
00504 
00505 
00506 int KAccelString::maxWeight(int &index, const QString &used)
00507 {
00508   int max = 0;
00509   index = -1;
00510 
00511   for (uint pos=0; pos<m_pureText.length(); ++pos)
00512     if (!used.contains(m_pureText[pos].lower()))
00513       if (m_weight[pos] > max)
00514       {
00515         max = m_weight[pos];
00516     index = pos;
00517       }
00518 
00519   return max;
00520 }
00521 
00522 
00523 void KAccelString::dump()
00524 {
00525   QString s;
00526   for (uint i=0; i<m_weight.count(); ++i)
00527     s += QString("%1(%2)").arg(pure()[i]).arg(m_weight[i]);
00528 }
00529 
00530 
00531 /*********************************************************************
00532 
00533  findAccelerators - the algorithm determining the new accelerators
00534 
00535  The algorithm is very crude:
00536 
00537    * each character in each widget text is assigned a weight
00538    * the character with the highest weight over all is picked
00539    * that widget is removed from the list
00540    * the weights are recalculated
00541    * the process is repeated until no more accelerators can be found
00542 
00543  The algorithm has some advantages:
00544 
00545    * it favours 'nice' accelerators (first characters in a word, etc.)
00546    * it is quite fast, O(N²)
00547    * it is easy to understand :-)
00548 
00549  The disadvantages:
00550 
00551    * it does not try to find as many accelerators as possible
00552 
00553  TODO:
00554 
00555  * The result is always correct, but not neccesarily optimal. Perhaps
00556    it would be a good idea to add another algorithm with higher complexity
00557    that gets used when this one fails, i.e. leaves widgets without
00558    accelerators.
00559 
00560  * The weights probably need some tweaking so they make more sense.
00561 
00562  *********************************************************************/
00563 
00564 void KAccelManagerAlgorithm::findAccelerators(KAccelStringList &result, QString &used)
00565 {
00566   KAccelStringList accel_strings = result;
00567 
00568   // initally remove all accelerators
00569   for (KAccelStringList::Iterator it = result.begin(); it != result.end(); ++it)
00570     (*it).setAccel(-1);
00571 
00572   // pick the highest bids
00573   for (uint cnt=0; cnt<accel_strings.count(); ++cnt)
00574   {
00575     int max = 0, index = -1, accel = -1;
00576 
00577     // find maximum weight
00578     for (uint i=0; i<accel_strings.count(); ++i)
00579     {
00580       int a;
00581       int m = accel_strings[i].maxWeight(a, used);
00582       if (m>max)
00583       {
00584         max = m;
00585         index = i;
00586         accel = a;
00587       }
00588     }
00589 
00590     // stop if no more accelerators can be found
00591     if (index < 0)
00592       return;
00593 
00594     // insert the accelerator
00595     if (accel >= 0)
00596     {
00597       result[index].setAccel(accel);
00598       used.append(result[index].accelerator());
00599     }
00600 
00601     // make sure we don't visit this one again
00602     accel_strings[index] = KAccelString();
00603   }
00604 }
00605 
00606 
00607 /*********************************************************************
00608 
00609  class KPopupAccelManager - managing QPopupMenu widgets dynamically
00610 
00611  *********************************************************************/
00612 
00613 KPopupAccelManager::KPopupAccelManager(QPopupMenu *popup)
00614   : QObject(popup), m_popup(popup), m_count(-1)
00615 {
00616   connect(popup, SIGNAL(aboutToShow()), SLOT(aboutToShow()));
00617 }
00618 
00619 
00620 void KPopupAccelManager::aboutToShow()
00621 {
00622   // Note: we try to be smart and avoid recalculating the accelerators
00623   // whenever possible. Unfortunately, there is no way to know if an
00624   // item has been added or removed, so we can not do much more than
00625   // to compare the items each time the menu is shown :-(
00626 
00627   if (m_count != (int)m_popup->count())
00628   {
00629     findMenuEntries(m_entries);
00630     calculateAccelerators();
00631     m_count = m_popup->count();
00632   }
00633   else
00634   {
00635     KAccelStringList entries;
00636     findMenuEntries(entries);
00637     if (entries != m_entries)
00638     {
00639       m_entries = entries;
00640       calculateAccelerators();
00641     }
00642   }
00643 }
00644 
00645 
00646 void KPopupAccelManager::calculateAccelerators()
00647 {
00648   // find the new accelerators
00649   QString used;
00650   KAccelManagerAlgorithm::findAccelerators(m_entries, used);
00651 
00652   // change the menu entries
00653   setMenuEntries(m_entries);
00654 }
00655 
00656 
00657 void KPopupAccelManager::findMenuEntries(KAccelStringList &list)
00658 {
00659   QMenuItem *mitem;
00660   QString s;
00661 
00662   list.clear();
00663 
00664   // read out the menu entries
00665   for (uint i=0; i<m_popup->count(); i++)
00666   {
00667     mitem = m_popup->findItem(m_popup->idAt(i));
00668     if (mitem->isSeparator())
00669       continue;
00670 
00671     s = mitem->text();
00672 
00673     // in full menues, look at entries with global accelerators last
00674     int weight = 50;
00675     if (s.contains('\t'))
00676       weight = 0;
00677 
00678     if (KAcceleratorManagerPrivate::standardName(s))
00679         weight += 300;
00680 
00681     list.append(KAccelString(s, weight));
00682   }
00683 }
00684 
00685 
00686 void KPopupAccelManager::setMenuEntries(const KAccelStringList &list)
00687 {
00688   QMenuItem *mitem;
00689 
00690   uint cnt = 0;
00691   for (uint i=0; i<m_popup->count(); i++)
00692   {
00693     mitem = m_popup->findItem(m_popup->idAt(i));
00694     if (mitem->isSeparator())
00695       continue;
00696 
00697     mitem->setText(list[cnt++].accelerated());
00698   }
00699 }
00700 
00701 
00702 void KPopupAccelManager::manage(QPopupMenu *popup)
00703 {
00704   // don't add more than one manager to a popup
00705   if (popup->child(0, "KPopupAccelManager", false) == 0 )
00706     new KPopupAccelManager(popup);
00707 }
00708 
00709 
00710 #include "kaccelmanager_private.moc"
KDE Logo
This file is part of the documentation for kdelibs Version 3.1.4.
Documentation copyright © 1996-2002 the KDE developers.
Generated on Sun Feb 27 22:14:45 2005 by doxygen 1.3.4 written by Dimitri van Heesch, © 1997-2001