1
/* This file is part of the KDE project
2
Copyright (C) 2002 Matthias Hölzer-Klüpfel <mhk@kde.org>
4
This library is free software; you can redistribute it and/or
5
modify it under the terms of the GNU Library General Public
6
License as published by the Free Software Foundation; either
7
version 2 of the License, or (at your option) any later version.
9
This library is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
Library General Public License for more details.
14
You should have received a copy of the GNU Library General Public License
15
along with this library; see the file COPYING.LIB. If not, write to
16
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17
Boston, MA 02110-1301, USA.
20
#include "kacceleratormanager.h"
22
#include <qapplication.h>
23
#include <qcheckbox.h>
24
#include <qcombobox.h>
25
#include <q3groupbox.h>
27
#include <qlineedit.h>
29
#include <qmenudata.h>
30
#include <qmetaobject.h>
33
#include <qpushbutton.h>
34
#include <qradiobutton.h>
37
#include <q3textview.h>
39
#include <QStackedWidget>
40
#include <QTextDocument>
42
#include <kstdaction.h>
43
#include <kstaticdeleter.h>
47
#include "kacceleratormanager_private.h"
48
#include "../kdeui/kstdaction_p.h"
51
/*********************************************************************
53
class Item - helper class containing widget information
55
This class stores information about the widgets the need accelerators,
56
as well as about their relationship.
58
*********************************************************************/
62
/*********************************************************************
64
class KAcceleratorManagerPrivate - internal helper class
66
This class does all the work to find accelerators for a hierarchy of
69
*********************************************************************/
72
class KAcceleratorManagerPrivate
76
static void manage(QWidget *widget);
77
static bool programmers_mode;
78
static bool standardName(const QString &str);
80
static bool checkChange(const KAccelString &as) {
81
QString t2 = as.accelerated();
82
QString t1 = as.originalText();
85
if (as.accel() == -1) {
86
removed_string += "<tr><td>" + Qt::escape(t1) + "</td></tr>";
87
} else if (as.originalAccel() == -1) {
88
added_string += "<tr><td>" + Qt::escape(t2) + "</td></tr>";
90
changed_string += "<tr><td>" + Qt::escape(t1) + "</td>";
91
changed_string += "<td>" + Qt::escape(t2) + "</td></tr>";
97
static QString changed_string;
98
static QString added_string;
99
static QString removed_string;
100
static QMap<QWidget *, int> ignored_widgets;
105
typedef QList<Item *> ItemList;
108
static void traverseChildren(QWidget *widget, Item *item);
110
static void manageWidget(QWidget *widget, Item *item);
111
static void manageMenuBar(QMenuBar *mbar, Item *item);
112
static void manageTabBar(QTabBar *bar, Item *item);
114
static void calculateAccelerators(Item *item, QString &used);
120
Item() : m_widget(0), m_children(0), m_index(-1) {};
123
void addChild(Item *item);
126
KAccelString m_content;
127
ItemList *m_children;
134
bool KAcceleratorManagerPrivate::programmers_mode = false;
135
QString KAcceleratorManagerPrivate::changed_string;
136
QString KAcceleratorManagerPrivate::added_string;
137
QString KAcceleratorManagerPrivate::removed_string;
138
static QStringList *kaccmp_sns = 0;
139
static KStaticDeleter<QStringList> kaccmp_sns_d;
140
QMap<QWidget*, int> KAcceleratorManagerPrivate::ignored_widgets;
142
bool KAcceleratorManagerPrivate::standardName(const QString &str)
145
kaccmp_sns_d.setObject(kaccmp_sns, new QStringList(KStdAction::internal_stdNames()));
146
return kaccmp_sns->contains(str);
149
KAcceleratorManagerPrivate::Item::~Item()
152
while (!m_children->isEmpty())
153
delete m_children->takeFirst();
159
void KAcceleratorManagerPrivate::Item::addChild(Item *item)
162
m_children = new ItemList;
165
m_children->append(item);
168
void KAcceleratorManagerPrivate::manage(QWidget *widget)
172
kDebug(131) << "null pointer given to manage" << endl;
176
if (qobject_cast<QMenu*>(widget))
178
// create a popup accel manager that can deal with dynamic menus
179
KPopupAccelManager::manage(static_cast<QMenu*>(widget));
183
Item *root = new Item;
185
manageWidget(widget, root);
188
calculateAccelerators(root, used);
193
void KAcceleratorManagerPrivate::calculateAccelerators(Item *item, QString &used)
195
if (!item->m_children)
198
// collect the contents
199
KAccelStringList contents;
200
foreach(Item *it, *item->m_children)
202
contents << it->m_content;
205
// find the right accelerators
206
KAccelManagerAlgorithm::findAccelerators(contents, used);
208
// write them back into the widgets
210
foreach(Item *it, *item->m_children)
214
QTabBar *tabBar = qobject_cast<QTabBar*>(it->m_widget);
217
if (checkChange(contents[cnt]))
218
tabBar->setTabText(it->m_index, contents[cnt].accelerated());
221
QMenuBar *menuBar = qobject_cast<QMenuBar*>(it->m_widget);
224
if (it->m_index >= 0)
226
QAction *maction = menuBar->actions()[it->m_index];
229
checkChange(contents[cnt]);
230
maction->setText(contents[cnt].accelerated());
235
// we possibly reserved an accel, but we won't set it as it looks silly
236
if ( qobject_cast<Q3GroupBox*>( it->m_widget ) )
239
kDebug(131) << "write " << cnt << " " << it->m_widget->metaObject()->className() << " " <<contents[cnt].accelerated() << endl;
241
int tprop = it->m_widget->metaObject()->indexOfProperty("text");
243
if (checkChange(contents[cnt]))
244
it->m_widget->setProperty("text", contents[cnt].accelerated());
246
tprop = it->m_widget->metaObject()->indexOfProperty("title");
247
if (tprop != -1 && checkChange(contents[cnt]))
248
it->m_widget->setProperty("title", contents[cnt].accelerated());
252
// calculate the accelerators for the children
253
foreach(Item *it, *item->m_children)
256
kDebug(131) << "children " << it->m_widget->metaObject()->className() << endl;
257
if (it->m_widget && it->m_widget->isVisibleTo( item->m_widget ) )
258
calculateAccelerators(it, used);
263
void KAcceleratorManagerPrivate::traverseChildren(QWidget *widget, Item *item)
265
QList<QWidget*> childList = widget->findChildren<QWidget*>();
266
foreach ( QWidget *w , childList ) {
267
// Ignore unless we have the direct parent
268
if(qobject_cast<QWidget *>(w->parent()) != widget) continue;
270
if ( !w->isVisibleTo( widget ) || w->isTopLevel() )
273
if ( KAcceleratorManagerPrivate::ignored_widgets.find( w ) != KAcceleratorManagerPrivate::ignored_widgets.end() )
276
manageWidget(w, item);
280
void KAcceleratorManagerPrivate::manageWidget(QWidget *w, Item *item)
282
// first treat the special cases
284
QTabBar *tabBar = qobject_cast<QTabBar*>(w);
287
manageTabBar(tabBar, item);
291
QStackedWidget *wds = qobject_cast<QStackedWidget*>( w );
294
QWidgetStackAccelManager::manage( wds );
298
QMenu *popupMenu = qobject_cast<QMenu*>(w);
301
// create a popup accel manager that can deal with dynamic menus
302
KPopupAccelManager::manage(popupMenu);
306
QStackedWidget *wdst = qobject_cast<QStackedWidget*>( w );
309
QWidgetStackAccelManager::manage( wdst );
313
QMenuBar *menuBar = qobject_cast<QMenuBar*>(w);
316
manageMenuBar(menuBar, item);
320
if (qobject_cast<QComboBox*>(w) || qobject_cast<QLineEdit*>(w) ||
321
qobject_cast<Q3TextEdit*>(w) || qobject_cast<Q3TextView*>(w) ||
322
qobject_cast<QSpinBox*>(w) || w->inherits( "KMultiTabBar" ) )
325
// now treat 'ordinary' widgets
326
QLabel *label = qobject_cast<QLabel*>(w);
328
if ( !label->buddy() )
331
kDebug() << "label-text " << label->textFormat() << endl;
332
if ( label->textFormat() == Qt::RichText ||
333
( label->textFormat() == Qt::AutoText &&
334
Qt::mightBeRichText( label->text() ) ) )
339
if (w->focusPolicy() != Qt::NoFocus || label || qobject_cast<Q3GroupBox*>(w) || qobject_cast<QRadioButton*>( w ))
343
int tprop = w->metaObject()->indexOfProperty("text");
345
QMetaProperty p = w->metaObject()->property( tprop );
347
variant = p.read (w);
353
tprop = w->metaObject()->indexOfProperty("title");
355
QMetaProperty p = w->metaObject()->property( tprop );
357
variant = p.read (w);
361
if (variant.isValid())
362
content = variant.toString();
364
if (!content.isEmpty())
369
// put some more weight on the usual action elements
370
int weight = KAccelManagerAlgorithm::DEFAULT_WEIGHT;
371
if (qobject_cast<QPushButton*>(w) || qobject_cast<QCheckBox*>(w) || qobject_cast<QRadioButton*>(w) || qobject_cast<QLabel*>(w))
372
weight = KAccelManagerAlgorithm::ACTION_ELEMENT_WEIGHT;
374
// don't put weight on group boxes, as usually the contents are more important
375
if (qobject_cast<Q3GroupBox*>(w))
376
weight = KAccelManagerAlgorithm::GROUP_BOX_WEIGHT;
378
// put a lot of extra weight on the KDialogBaseButton's
379
if (w->inherits("KDialogBaseButton"))
380
weight += KAccelManagerAlgorithm::DIALOG_BUTTON_EXTRA_WEIGHT;
382
i->m_content = KAccelString(content, weight);
386
traverseChildren(w, item);
389
void KAcceleratorManagerPrivate::manageTabBar(QTabBar *bar, Item *item)
391
for (int i=0; i<bar->count(); i++)
393
QString content = bar->tabText(i);
394
if (content.isEmpty())
401
it->m_content = KAccelString(content);
405
void KAcceleratorManagerPrivate::manageMenuBar(QMenuBar *mbar, Item *item)
410
for (int i=0; i<mbar->actions().count(); ++i)
412
maction = mbar->actions()[i];
416
// nothing to do for separators
417
if (maction->isSeparator())
427
// menu titles are important, so raise the weight
428
KAccelManagerAlgorithm::MENU_TITLE_WEIGHT);
434
// have a look at the popup as well, if present
436
KPopupAccelManager::manage(maction->menu());
441
/*********************************************************************
443
class KAcceleratorManager - main entry point
445
This class is just here to provide a clean public API...
447
*********************************************************************/
449
void KAcceleratorManager::manage(QWidget *widget, bool programmers_mode)
451
kDebug(131) << "KAcceleratorManager::manage\n";
452
KAcceleratorManagerPrivate::changed_string.clear();
453
KAcceleratorManagerPrivate::added_string.clear();
454
KAcceleratorManagerPrivate::removed_string.clear();
455
KAcceleratorManagerPrivate::programmers_mode = programmers_mode;
456
KAcceleratorManagerPrivate::manage(widget);
459
void KAcceleratorManager::last_manage(QString &added, QString &changed, QString &removed)
461
added = KAcceleratorManagerPrivate::added_string;
462
changed = KAcceleratorManagerPrivate::changed_string;
463
removed = KAcceleratorManagerPrivate::removed_string;
467
/*********************************************************************
469
class KAccelString - a string with weighted characters
471
*********************************************************************/
473
KAccelString::KAccelString(const QString &input, int initialWeight)
474
: m_pureText(input), m_weight()
476
m_orig_accel = m_pureText.indexOf("(!)&");
477
if (m_orig_accel != -1)
478
m_pureText.remove(m_orig_accel, 4);
480
m_orig_accel = m_pureText.indexOf("(&&)");
481
if (m_orig_accel != -1)
482
m_pureText.replace(m_orig_accel, 4, "&");
484
m_origText = m_pureText;
486
if (m_pureText.contains('\t'))
487
m_pureText = m_pureText.left(m_pureText.indexOf('\t'));
489
m_orig_accel = m_accel = stripAccelerator(m_pureText);
491
kDebug(131) << input << " " << m_orig_accel << " " << m_accel << " " << m_pureText << endl;
492
if (initialWeight == -1)
493
initialWeight = KAccelManagerAlgorithm::DEFAULT_WEIGHT;
495
calculateWeights(initialWeight);
501
QString KAccelString::accelerated() const
503
QString result = m_origText;
504
if (result.isEmpty())
507
if (KAcceleratorManagerPrivate::programmers_mode)
509
if (m_accel != m_orig_accel) {
510
int oa = m_orig_accel;
513
result.insert(m_accel, "(!)&");
514
if (m_accel < m_orig_accel)
517
if (m_orig_accel >= 0)
518
result.replace(oa, 1, "(&&)");
521
if (m_accel >= 0 && m_orig_accel != m_accel) {
522
if (m_orig_accel != -1)
523
result.remove(m_orig_accel, 1);
524
result.insert(m_accel, "&");
531
QChar KAccelString::accelerator() const
533
if ((m_accel < 0) || (m_accel > (int)m_pureText.length()))
536
return m_pureText[m_accel].toLower();
540
void KAccelString::calculateWeights(int initialWeight)
542
m_weight.resize(m_pureText.length());
545
bool start_character = true;
547
while (pos<m_pureText.length())
549
QChar c = m_pureText[pos];
551
int weight = initialWeight+1;
553
// add special weight to first character
555
weight += KAccelManagerAlgorithm::FIRST_CHARACTER_EXTRA_WEIGHT;
557
// add weight to word beginnings
560
weight += KAccelManagerAlgorithm::WORD_BEGINNING_EXTRA_WEIGHT;
561
start_character = false;
564
// add decreasing weight to left characters
568
// try to preserve the wanted accelarators
569
if ((int)pos == accel()) {
570
weight += KAccelManagerAlgorithm::WANTED_ACCEL_EXTRA_WEIGHT;
571
// kDebug(131) << "wanted " << m_pureText << " " << KAcceleratorManagerPrivate::standardName(m_origText) << endl;
572
if (KAcceleratorManagerPrivate::standardName(m_origText)) {
573
weight += KAccelManagerAlgorithm::STANDARD_ACCEL;
577
// skip non typeable characters
578
if (!c.isLetterOrNumber())
581
start_character = true;
584
m_weight[pos] = weight;
591
int KAccelString::stripAccelerator(QString &text)
593
// Note: this code is derived from QAccel::shortcutKey
598
p = text.indexOf('&', p)+1;
600
if (p <= 0 || p >= (int)text.length())
620
int KAccelString::maxWeight(int &index, const QString &used)
625
for (int pos=0; pos<m_pureText.length(); ++pos)
626
if (used.indexOf(m_pureText[pos], 0, Qt::CaseInsensitive) == -1 && m_pureText[pos].toLatin1() != 0)
627
if (m_weight[pos] > max)
637
void KAccelString::dump()
640
for (int i=0; i<m_weight.count(); ++i)
641
s += QString("%1(%2) ").arg(pure()[i]).arg(m_weight[i]);
642
kDebug() << "s " << s << endl;
646
/*********************************************************************
648
findAccelerators - the algorithm determining the new accelerators
650
The algorithm is very crude:
652
* each character in each widget text is assigned a weight
653
* the character with the highest weight over all is picked
654
* that widget is removed from the list
655
* the weights are recalculated
656
* the process is repeated until no more accelerators can be found
658
The algorithm has some advantages:
660
* it favors 'nice' accelerators (first characters in a word, etc.)
661
* it is quite fast, O(N²)
662
* it is easy to understand :-)
666
* it does not try to find as many accelerators as possible
670
* The result is always correct, but not neccesarily optimal. Perhaps
671
it would be a good idea to add another algorithm with higher complexity
672
that gets used when this one fails, i.e. leaves widgets without
675
* The weights probably need some tweaking so they make more sense.
677
*********************************************************************/
679
void KAccelManagerAlgorithm::findAccelerators(KAccelStringList &result, QString &used)
681
kDebug(131) << "findAccelerators\n";
682
KAccelStringList accel_strings = result;
684
// initally remove all accelerators
685
for (KAccelStringList::Iterator it = result.begin(); it != result.end(); ++it) {
686
kDebug(131) << "reset " << ( *it ).pure() << endl;
690
// pick the highest bids
691
for (int cnt=0; cnt<accel_strings.count(); ++cnt)
693
kDebug(131) << "cnt " << accel_strings[cnt].pure() << endl;
694
int max = 0, index = -1, accel = -1;
696
// find maximum weight
697
for (int i=0; i<accel_strings.count(); ++i)
700
int m = accel_strings[i].maxWeight(a, used);
709
// stop if no more accelerators can be found
713
// insert the accelerator
716
result[index].setAccel(accel);
717
used.append(result[index].accelerator());
720
// make sure we don't visit this one again
721
accel_strings[index] = KAccelString();
726
/*********************************************************************
728
class KPopupAccelManager - managing QPopupMenu widgets dynamically
730
*********************************************************************/
732
KPopupAccelManager::KPopupAccelManager(QMenu *popup)
733
: QObject(popup), m_popup(popup), m_count(-1)
735
aboutToShow(); // do one check and then connect to show
736
connect(popup, SIGNAL(aboutToShow()), SLOT(aboutToShow()));
740
void KPopupAccelManager::aboutToShow()
742
// Note: we try to be smart and avoid recalculating the accelerators
743
// whenever possible. Unfortunately, there is no way to know if an
744
// item has been added or removed, so we can not do much more than
745
// to compare the items each time the menu is shown :-(
747
if (m_count != (int)m_popup->actions().count())
749
findMenuEntries(m_entries);
750
calculateAccelerators();
751
m_count = m_popup->actions().count();
755
KAccelStringList entries;
756
findMenuEntries(entries);
757
if (entries != m_entries)
760
calculateAccelerators();
766
void KPopupAccelManager::calculateAccelerators()
768
// find the new accelerators
770
KAccelManagerAlgorithm::findAccelerators(m_entries, used);
772
// change the menu entries
773
setMenuEntries(m_entries);
777
void KPopupAccelManager::findMenuEntries(KAccelStringList &list)
784
// read out the menu entries
785
for (int i=0; i<m_popup->actions().count(); i++)
787
maction = m_popup->actions()[i];
788
if (maction->isSeparator())
793
// in full menus, look at entries with global accelerators last
795
if (s.contains('\t'))
798
list.append(KAccelString(s, weight));
800
// have a look at the popup as well, if present
802
KPopupAccelManager::manage(maction->menu());
807
void KPopupAccelManager::setMenuEntries(const KAccelStringList &list)
812
for (int i=0; i<m_popup->actions().count(); i++)
814
maction = m_popup->actions()[i];
815
if (maction->isSeparator())
818
if (KAcceleratorManagerPrivate::checkChange(list[cnt]))
819
maction->setText(list[cnt].accelerated());
825
void KPopupAccelManager::manage(QMenu *popup)
827
// don't add more than one manager to a popup
828
if (popup->findChild<KPopupAccelManager*>(QString()) == 0 )
829
new KPopupAccelManager(popup);
832
void QWidgetStackAccelManager::manage( QStackedWidget *stack )
834
if ( stack->findChild<QWidgetStackAccelManager*>(QString()) == 0 )
835
new QWidgetStackAccelManager( stack );
838
QWidgetStackAccelManager::QWidgetStackAccelManager(QStackedWidget *stack)
839
: QObject(stack), m_stack(stack)
841
currentChanged(stack->currentIndex()); // do one check and then connect to show
842
connect(stack, SIGNAL(currentChanged(int)), SLOT(currentChanged(int)));
845
bool QWidgetStackAccelManager::eventFilter ( QObject * watched, QEvent * e )
847
if ( e->type() == QEvent::Show && qApp->activeWindow() ) {
848
KAcceleratorManager::manage( qApp->activeWindow() );
849
watched->removeEventFilter( this );
854
void QWidgetStackAccelManager::currentChanged(int child)
856
if (child < 0 || child >= static_cast<QStackedWidget*>(parent())->count())
858
kDebug(131) << k_funcinfo << "invalid index provided" << endl;
862
static_cast<QStackedWidget*>(parent())->widget(child)->installEventFilter( this );
865
void KAcceleratorManager::setNoAccel( QWidget *widget )
867
KAcceleratorManagerPrivate::ignored_widgets[widget] = 1;
870
#include "kacceleratormanager_private.moc"