1 | /* |
2 | This file is part of the KDE project |
3 | SPDX-FileCopyrightText: 2002 Matthias Hölzer-Klüpfel <mhk@kde.org> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.0-or-later |
6 | */ |
7 | |
8 | #include "kacceleratormanager.h" |
9 | #include "kacceleratormanager_p.h" |
10 | |
11 | #include <QApplication> |
12 | #include <QCheckBox> |
13 | #include <QComboBox> |
14 | #include <QDockWidget> |
15 | #include <QGroupBox> |
16 | #include <QLabel> |
17 | #include <QLineEdit> |
18 | #include <QList> |
19 | #include <QMainWindow> |
20 | #include <QMenuBar> |
21 | #include <QMetaProperty> |
22 | #include <QObject> |
23 | #include <QPushButton> |
24 | #include <QRadioButton> |
25 | #include <QStackedWidget> |
26 | #include <QTabBar> |
27 | #include <QTextEdit> |
28 | #include <QWidget> |
29 | |
30 | #include "common_helpers_p.h" |
31 | #include "loggingcategory.h" |
32 | |
33 | /********************************************************************* |
34 | |
35 | class Item - helper class containing widget information |
36 | |
37 | This class stores information about the widgets the need accelerators, |
38 | as well as about their relationship. |
39 | |
40 | *********************************************************************/ |
41 | |
42 | bool KAcceleratorManagerPrivate::programmers_mode = false; |
43 | QString KAcceleratorManagerPrivate::changed_string; |
44 | QString KAcceleratorManagerPrivate::added_string; |
45 | QString KAcceleratorManagerPrivate::removed_string; |
46 | QMap<QWidget *, int> KAcceleratorManagerPrivate::ignored_widgets; |
47 | QStringList KAcceleratorManagerPrivate::standardNames; |
48 | |
49 | void KAcceleratorManagerPrivate::addStandardActionNames(const QStringList &list) |
50 | { |
51 | standardNames.append(l: list); |
52 | } |
53 | |
54 | bool KAcceleratorManagerPrivate::standardName(const QString &str) |
55 | { |
56 | return standardNames.contains(str); |
57 | } |
58 | |
59 | KAcceleratorManagerPrivate::Item::~Item() |
60 | { |
61 | if (m_children) { |
62 | while (!m_children->isEmpty()) { |
63 | delete m_children->takeFirst(); |
64 | } |
65 | delete m_children; |
66 | } |
67 | } |
68 | |
69 | void KAcceleratorManagerPrivate::Item::addChild(Item *item) |
70 | { |
71 | if (!m_children) { |
72 | m_children = new ItemList; |
73 | } |
74 | |
75 | m_children->append(t: item); |
76 | } |
77 | |
78 | void KAcceleratorManagerPrivate::manage(QWidget *widget) |
79 | { |
80 | if (!widget) { |
81 | qCDebug(KWidgetsAddonsLog) << "null pointer given to manage" ; |
82 | return; |
83 | } |
84 | |
85 | if (KAcceleratorManagerPrivate::ignored_widgets.contains(key: widget)) { |
86 | return; |
87 | } |
88 | |
89 | if (qobject_cast<QMenu *>(object: widget)) { |
90 | // create a popup accel manager that can deal with dynamic menus |
91 | KPopupAccelManager::manage(popup: static_cast<QMenu *>(widget)); |
92 | return; |
93 | } |
94 | |
95 | Item *root = new Item; |
96 | |
97 | QString used; |
98 | manageWidget(widget, item: root, used); |
99 | calculateAccelerators(item: root, used); |
100 | delete root; |
101 | } |
102 | |
103 | void KAcceleratorManagerPrivate::calculateAccelerators(Item *item, QString &used) |
104 | { |
105 | if (!item->m_children) { |
106 | return; |
107 | } |
108 | |
109 | // collect the contents |
110 | KAccelStringList contents; |
111 | contents.reserve(asize: item->m_children->size()); |
112 | for (Item *it : std::as_const(t&: *item->m_children)) { |
113 | contents << it->m_content; |
114 | } |
115 | |
116 | // find the right accelerators |
117 | KAccelManagerAlgorithm::findAccelerators(result&: contents, used); |
118 | |
119 | // write them back into the widgets |
120 | int cnt = -1; |
121 | for (Item *it : std::as_const(t&: *item->m_children)) { |
122 | cnt++; |
123 | |
124 | QDockWidget *dock = qobject_cast<QDockWidget *>(object: it->m_widget); |
125 | if (dock) { |
126 | if (checkChange(as: contents[cnt])) { |
127 | dock->setWindowTitle(contents[cnt].accelerated()); |
128 | } |
129 | continue; |
130 | } |
131 | QTabBar *tabBar = qobject_cast<QTabBar *>(object: it->m_widget); |
132 | if (tabBar) { |
133 | if (checkChange(as: contents[cnt])) { |
134 | tabBar->setTabText(index: it->m_index, text: contents[cnt].accelerated()); |
135 | } |
136 | continue; |
137 | } |
138 | QMenuBar * = qobject_cast<QMenuBar *>(object: it->m_widget); |
139 | if (menuBar) { |
140 | if (it->m_index >= 0) { |
141 | QAction *maction = menuBar->actions()[it->m_index]; |
142 | if (maction) { |
143 | checkChange(as: contents[cnt]); |
144 | maction->setText(contents[cnt].accelerated()); |
145 | } |
146 | continue; |
147 | } |
148 | } |
149 | |
150 | // we possibly reserved an accel, but we won't set it as it looks silly |
151 | QGroupBox *groupBox = qobject_cast<QGroupBox *>(object: it->m_widget); |
152 | if (groupBox && !groupBox->isCheckable()) { |
153 | continue; |
154 | } |
155 | |
156 | int tprop = it->m_widget->metaObject()->indexOfProperty(name: "text" ); |
157 | if (tprop != -1) { |
158 | if (checkChange(as: contents[cnt])) { |
159 | it->m_widget->setProperty(name: "text" , value: contents[cnt].accelerated()); |
160 | } |
161 | } else { |
162 | tprop = it->m_widget->metaObject()->indexOfProperty(name: "title" ); |
163 | if (tprop != -1 && checkChange(as: contents[cnt])) { |
164 | it->m_widget->setProperty(name: "title" , value: contents[cnt].accelerated()); |
165 | } |
166 | } |
167 | } |
168 | |
169 | // calculate the accelerators for the children |
170 | for (Item *it : std::as_const(t&: *item->m_children)) { |
171 | if (it->m_widget && it->m_widget->isVisibleTo(item->m_widget)) { |
172 | calculateAccelerators(item: it, used); |
173 | } |
174 | } |
175 | } |
176 | |
177 | void KAcceleratorManagerPrivate::traverseChildren(QWidget *widget, Item *item, QString &used) |
178 | { |
179 | // we are only interested in direct child widgets |
180 | const QList<QWidget *> childList = widget->findChildren<QWidget *>(aName: QString(), options: Qt::FindDirectChildrenOnly); |
181 | for (QWidget *w : childList) { |
182 | if (!w->isVisibleTo(widget) || (w->isWindow() && qobject_cast<QMenu *>(object: w) == nullptr)) { |
183 | continue; |
184 | } |
185 | |
186 | if (KAcceleratorManagerPrivate::ignored_widgets.contains(key: w)) { |
187 | continue; |
188 | } |
189 | |
190 | manageWidget(widget: w, item, used); |
191 | } |
192 | } |
193 | |
194 | void KAcceleratorManagerPrivate::manageWidget(QWidget *w, Item *item, QString &used) |
195 | { |
196 | // If the widget has any action whose shortcuts contain keystrokes in the |
197 | // form of Alt+X we need to mark X as used, otherwise we may assign it as accelerator |
198 | // and there will be a conflict when trying to use it |
199 | const QList<QAction *> widgetActions = w->actions(); |
200 | for (QAction *action : widgetActions) { |
201 | const QList<QKeySequence> actionShortcuts = action->shortcuts(); |
202 | for (const QKeySequence &sequence : actionShortcuts) { |
203 | const QString sequenceAsText = sequence.toString(format: QKeySequence::PortableText); |
204 | const QStringList splitSequence = sequenceAsText.split(QStringLiteral(", " )); |
205 | for (const QString &shortcut : splitSequence) { |
206 | if (shortcut.length() == 5 && shortcut.startsWith(QStringLiteral("Alt+" ))) { |
207 | used.append(s: shortcut.right(n: 1)); |
208 | } |
209 | } |
210 | } |
211 | } |
212 | |
213 | // first treat the special cases |
214 | |
215 | QTabBar *tabBar = qobject_cast<QTabBar *>(object: w); |
216 | if (tabBar) { |
217 | manageTabBar(bar: tabBar, item); |
218 | return; |
219 | } |
220 | |
221 | QStackedWidget *wds = qobject_cast<QStackedWidget *>(object: w); |
222 | if (wds) { |
223 | QWidgetStackAccelManager::manage(popup: wds); |
224 | // return; |
225 | } |
226 | |
227 | QDockWidget *dock = qobject_cast<QDockWidget *>(object: w); |
228 | if (dock) { |
229 | // QWidgetStackAccelManager::manage( wds ); |
230 | manageDockWidget(dock, item); |
231 | } |
232 | |
233 | QMenu * = qobject_cast<QMenu *>(object: w); |
234 | if (popupMenu) { |
235 | // create a popup accel manager that can deal with dynamic menus |
236 | KPopupAccelManager::manage(popup: popupMenu); |
237 | return; |
238 | } |
239 | |
240 | QStackedWidget *wdst = qobject_cast<QStackedWidget *>(object: w); |
241 | if (wdst) { |
242 | QWidgetStackAccelManager::manage(popup: wdst); |
243 | // return; |
244 | } |
245 | |
246 | QMenuBar * = qobject_cast<QMenuBar *>(object: w); |
247 | if (menuBar) { |
248 | manageMenuBar(mbar: menuBar, item); |
249 | return; |
250 | } |
251 | |
252 | if (qobject_cast<QComboBox *>(object: w) || qobject_cast<QLineEdit *>(object: w) // |
253 | || w->inherits(classname: "Q3TextEdit" ) // |
254 | || qobject_cast<QTextEdit *>(object: w) // |
255 | || qobject_cast<QAbstractSpinBox *>(object: w) // |
256 | || w->inherits(classname: "KMultiTabBar" ) // |
257 | || w->inherits(classname: "qdesigner_internal::TextPropertyEditor" )) { |
258 | return; |
259 | } |
260 | |
261 | if (w->inherits(classname: "KUrlRequester" )) { |
262 | traverseChildren(widget: w, item, used); |
263 | return; |
264 | } |
265 | |
266 | // now treat 'ordinary' widgets |
267 | QLabel *label = qobject_cast<QLabel *>(object: w); |
268 | if (label) { |
269 | if (!label->buddy()) { |
270 | return; |
271 | } else { |
272 | if (label->textFormat() == Qt::RichText // |
273 | || (label->textFormat() == Qt::AutoText && Qt::mightBeRichText(label->text()))) { |
274 | return; |
275 | } |
276 | } |
277 | } |
278 | |
279 | if (w->focusPolicy() != Qt::NoFocus || label || qobject_cast<QGroupBox *>(object: w) || qobject_cast<QRadioButton *>(object: w)) { |
280 | QString content; |
281 | QVariant variant; |
282 | int tprop = w->metaObject()->indexOfProperty(name: "text" ); |
283 | if (tprop != -1) { |
284 | QMetaProperty p = w->metaObject()->property(index: tprop); |
285 | if (p.isValid() && p.isWritable()) { |
286 | variant = p.read(obj: w); |
287 | } else { |
288 | tprop = -1; |
289 | } |
290 | } |
291 | |
292 | if (tprop == -1) { |
293 | tprop = w->metaObject()->indexOfProperty(name: "title" ); |
294 | if (tprop != -1) { |
295 | QMetaProperty p = w->metaObject()->property(index: tprop); |
296 | if (p.isValid() && p.isWritable()) { |
297 | variant = p.read(obj: w); |
298 | } |
299 | } |
300 | } |
301 | |
302 | if (variant.isValid()) { |
303 | content = variant.toString(); |
304 | } |
305 | |
306 | if (!content.isEmpty()) { |
307 | Item *i = new Item; |
308 | i->m_widget = w; |
309 | |
310 | // put some more weight on the usual action elements |
311 | int weight = KAccelManagerAlgorithm::DEFAULT_WEIGHT; |
312 | if (qobject_cast<QPushButton *>(object: w) || qobject_cast<QCheckBox *>(object: w) || qobject_cast<QRadioButton *>(object: w) || qobject_cast<QLabel *>(object: w)) { |
313 | weight = KAccelManagerAlgorithm::ACTION_ELEMENT_WEIGHT; |
314 | } |
315 | |
316 | // don't put weight on non-checkable group boxes, |
317 | // as usually the contents are more important |
318 | QGroupBox *groupBox = qobject_cast<QGroupBox *>(object: w); |
319 | if (groupBox) { |
320 | if (groupBox->isCheckable()) { |
321 | weight = KAccelManagerAlgorithm::CHECKABLE_GROUP_BOX_WEIGHT; |
322 | } else { |
323 | weight = KAccelManagerAlgorithm::GROUP_BOX_WEIGHT; |
324 | } |
325 | } |
326 | |
327 | i->m_content = KAccelString(content, weight); |
328 | item->addChild(item: i); |
329 | } |
330 | } |
331 | traverseChildren(widget: w, item, used); |
332 | } |
333 | |
334 | void KAcceleratorManagerPrivate::manageTabBar(QTabBar *bar, Item *item) |
335 | { |
336 | // ignore QTabBar for QDockWidgets, because QDockWidget on its title change |
337 | // also updates its tabbar entry, so on the next run of KCheckAccelerators |
338 | // this looks like a conflict and triggers a new reset of the shortcuts -> endless loop |
339 | QWidget *parentWidget = bar->parentWidget(); |
340 | if (parentWidget) { |
341 | QMainWindow *mainWindow = qobject_cast<QMainWindow *>(object: parentWidget); |
342 | // TODO: find better hints that this is a QTabBar for QDockWidgets |
343 | if (mainWindow) { // && (mainWindow->layout()->indexOf(bar) != -1)) QMainWindowLayout lacks proper support |
344 | return; |
345 | } |
346 | } |
347 | |
348 | for (int i = 0; i < bar->count(); i++) { |
349 | QString content = bar->tabText(index: i); |
350 | if (content.isEmpty()) { |
351 | continue; |
352 | } |
353 | |
354 | Item *it = new Item; |
355 | item->addChild(item: it); |
356 | it->m_widget = bar; |
357 | it->m_index = i; |
358 | it->m_content = KAccelString(content); |
359 | } |
360 | } |
361 | |
362 | void KAcceleratorManagerPrivate::manageDockWidget(QDockWidget *dock, Item *item) |
363 | { |
364 | // As of Qt 4.4.3 setting a shortcut to a QDockWidget has no effect, |
365 | // because a QDockWidget does not grab it, even while displaying an underscore |
366 | // in the title for the given shortcut letter. |
367 | // Still it is useful to set the shortcut, because if QDockWidgets are tabbed, |
368 | // the tab automatically gets the same text as the QDockWidget title, including the shortcut. |
369 | // And for the QTabBar the shortcut does work, it gets grabbed as usual. |
370 | // Having the QDockWidget without a shortcut and resetting the tab text with a title including |
371 | // the shortcut does not work, the tab text is instantly reverted to the QDockWidget title |
372 | // (see also manageTabBar()). |
373 | // All in all QDockWidgets and shortcuts are a little broken for now. |
374 | QString content = dock->windowTitle(); |
375 | if (content.isEmpty()) { |
376 | return; |
377 | } |
378 | |
379 | Item *it = new Item; |
380 | item->addChild(item: it); |
381 | it->m_widget = dock; |
382 | it->m_content = KAccelString(content, KAccelManagerAlgorithm::STANDARD_ACCEL); |
383 | } |
384 | |
385 | void KAcceleratorManagerPrivate::(QMenuBar *mbar, Item *item) |
386 | { |
387 | QAction *maction; |
388 | QString s; |
389 | |
390 | for (int i = 0; i < mbar->actions().count(); ++i) { |
391 | maction = mbar->actions()[i]; |
392 | if (!maction) { |
393 | continue; |
394 | } |
395 | |
396 | // nothing to do for separators |
397 | if (maction->isSeparator()) { |
398 | continue; |
399 | } |
400 | |
401 | s = maction->text(); |
402 | if (!s.isEmpty()) { |
403 | Item *it = new Item; |
404 | item->addChild(item: it); |
405 | it->m_content = KAccelString(s, |
406 | KAccelManagerAlgorithm::MENU_TITLE_WEIGHT); // menu titles are important, so raise the weight |
407 | |
408 | it->m_widget = mbar; |
409 | it->m_index = i; |
410 | } |
411 | |
412 | // have a look at the popup as well, if present |
413 | if (maction->menu()) { |
414 | KPopupAccelManager::manage(popup: maction->menu()); |
415 | } |
416 | } |
417 | } |
418 | |
419 | /********************************************************************* |
420 | |
421 | class KAcceleratorManager - main entry point |
422 | |
423 | This class is just here to provide a clean public API... |
424 | |
425 | *********************************************************************/ |
426 | |
427 | void KAcceleratorManager::manage(QWidget *widget, bool programmers_mode) |
428 | { |
429 | KAcceleratorManagerPrivate::changed_string.clear(); |
430 | KAcceleratorManagerPrivate::added_string.clear(); |
431 | KAcceleratorManagerPrivate::removed_string.clear(); |
432 | KAcceleratorManagerPrivate::programmers_mode = programmers_mode; |
433 | KAcceleratorManagerPrivate::manage(widget); |
434 | } |
435 | |
436 | void KAcceleratorManager::last_manage(QString &added, QString &changed, QString &removed) |
437 | { |
438 | added = KAcceleratorManagerPrivate::added_string; |
439 | changed = KAcceleratorManagerPrivate::changed_string; |
440 | removed = KAcceleratorManagerPrivate::removed_string; |
441 | } |
442 | |
443 | /********************************************************************* |
444 | |
445 | class KAccelString - a string with weighted characters |
446 | |
447 | *********************************************************************/ |
448 | |
449 | KAccelString::KAccelString(const QString &input, int initialWeight) |
450 | : m_pureText(input) |
451 | , m_weight() |
452 | { |
453 | m_orig_accel = m_pureText.indexOf(s: QLatin1String("(!)&" )); |
454 | if (m_orig_accel != -1) { |
455 | m_pureText.remove(i: m_orig_accel, len: 4); |
456 | } |
457 | |
458 | m_orig_accel = m_pureText.indexOf(s: QLatin1String("(&&)" )); |
459 | if (m_orig_accel != -1) { |
460 | m_pureText.replace(i: m_orig_accel, len: 4, QStringLiteral("&" )); |
461 | } |
462 | |
463 | m_origText = m_pureText; |
464 | |
465 | const int tabPos = m_pureText.indexOf(c: QLatin1Char('\t')); |
466 | if (tabPos != -1) { |
467 | m_pureText.truncate(pos: tabPos); |
468 | } |
469 | |
470 | m_orig_accel = m_accel = stripAccelerator(input&: m_pureText); |
471 | |
472 | if (initialWeight == -1) { |
473 | initialWeight = KAccelManagerAlgorithm::DEFAULT_WEIGHT; |
474 | } |
475 | |
476 | calculateWeights(initialWeight); |
477 | |
478 | // dump(); |
479 | } |
480 | |
481 | QString KAccelString::accelerated() const |
482 | { |
483 | QString result = m_origText; |
484 | if (result.isEmpty()) { |
485 | return result; |
486 | } |
487 | |
488 | if (KAcceleratorManagerPrivate::programmers_mode) { |
489 | if (m_accel != m_orig_accel) { |
490 | int oa = m_orig_accel; |
491 | |
492 | if (m_accel >= 0) { |
493 | result.insert(i: m_accel, s: QLatin1String("(!)&" )); |
494 | if (m_accel < m_orig_accel) { |
495 | oa += 4; |
496 | } |
497 | } |
498 | if (m_orig_accel >= 0) { |
499 | result.replace(i: oa, len: 1, QStringLiteral("(&&)" )); |
500 | } |
501 | } |
502 | } else { |
503 | if (m_accel >= 0 && m_orig_accel != m_accel) { |
504 | if (m_orig_accel != -1) { |
505 | result.remove(i: m_orig_accel, len: 1); |
506 | } |
507 | result.insert(i: m_accel, c: QLatin1Char('&')); |
508 | } |
509 | } |
510 | return result; |
511 | } |
512 | |
513 | QChar KAccelString::accelerator() const |
514 | { |
515 | if ((m_accel < 0) || (m_accel > m_pureText.length())) { |
516 | return QChar(); |
517 | } |
518 | |
519 | return m_pureText[m_accel].toLower(); |
520 | } |
521 | |
522 | void KAccelString::calculateWeights(int initialWeight) |
523 | { |
524 | m_weight.resize(size: m_pureText.length()); |
525 | |
526 | int pos = 0; |
527 | bool start_character = true; |
528 | |
529 | while (pos < m_pureText.length()) { |
530 | QChar c = m_pureText[pos]; |
531 | |
532 | int weight = initialWeight + 1; |
533 | |
534 | // add special weight to first character |
535 | if (pos == 0) { |
536 | weight += KAccelManagerAlgorithm::FIRST_CHARACTER_EXTRA_WEIGHT; |
537 | } |
538 | |
539 | // add weight to word beginnings |
540 | if (start_character) { |
541 | weight += KAccelManagerAlgorithm::WORD_BEGINNING_EXTRA_WEIGHT; |
542 | start_character = false; |
543 | } |
544 | |
545 | // add decreasing weight to left characters |
546 | if (pos < 50) { |
547 | weight += (50 - pos); |
548 | } |
549 | |
550 | // try to preserve the wanted accelerators |
551 | if (pos == accel()) { |
552 | weight += KAccelManagerAlgorithm::WANTED_ACCEL_EXTRA_WEIGHT; |
553 | // qCDebug(KWidgetsAddonsLog) << "wanted " << m_pureText << " " << KAcceleratorManagerPrivate::standardName(m_origText); |
554 | if (KAcceleratorManagerPrivate::standardName(str: m_origText)) { |
555 | weight += KAccelManagerAlgorithm::STANDARD_ACCEL; |
556 | } |
557 | } |
558 | |
559 | // skip non typeable characters |
560 | if (!c.isLetterOrNumber()) { |
561 | weight = 0; |
562 | start_character = true; |
563 | } |
564 | |
565 | m_weight[pos] = weight; |
566 | |
567 | ++pos; |
568 | } |
569 | } |
570 | |
571 | int KAccelString::stripAccelerator(QString &text) |
572 | { |
573 | // Note: this code is derived from QAccel::shortcutKey |
574 | int p = 0; |
575 | |
576 | while (p >= 0) { |
577 | p = text.indexOf(c: QLatin1Char('&'), from: p) + 1; |
578 | |
579 | if (p <= 0 || p >= text.length()) { |
580 | break; |
581 | } |
582 | |
583 | if (text[p] != QLatin1Char('&')) { |
584 | QChar c = text[p]; |
585 | if (c.isPrint()) { |
586 | text.remove(i: p - 1, len: 1); |
587 | return p - 1; |
588 | } |
589 | } |
590 | |
591 | p++; |
592 | } |
593 | |
594 | return -1; |
595 | } |
596 | |
597 | int KAccelString::maxWeight(int &index, const QString &used) const |
598 | { |
599 | int max = 0; |
600 | index = -1; |
601 | |
602 | for (int pos = 0; pos < m_pureText.length(); ++pos) { |
603 | if (used.indexOf(c: m_pureText[pos], from: 0, cs: Qt::CaseInsensitive) == -1 && m_pureText[pos].toLatin1() != 0) { |
604 | if (m_weight[pos] > max) { |
605 | max = m_weight[pos]; |
606 | index = pos; |
607 | } |
608 | } |
609 | } |
610 | |
611 | return max; |
612 | } |
613 | |
614 | void KAccelString::dump() |
615 | { |
616 | QString s; |
617 | for (int i = 0; i < m_weight.count(); ++i) { |
618 | s += QStringLiteral("%1(%2) " ).arg(a: pure()[i]).arg(a: m_weight[i]); |
619 | } |
620 | qCDebug(KWidgetsAddonsLog) << "s " << s; |
621 | } |
622 | |
623 | /********************************************************************* |
624 | |
625 | findAccelerators - the algorithm determining the new accelerators |
626 | |
627 | The algorithm is very crude: |
628 | |
629 | * each character in each widget text is assigned a weight |
630 | * the character with the highest weight over all is picked |
631 | * that widget is removed from the list |
632 | * the weights are recalculated |
633 | * the process is repeated until no more accelerators can be found |
634 | |
635 | The algorithm has some advantages: |
636 | |
637 | * it favors 'nice' accelerators (first characters in a word, etc.) |
638 | * it is quite fast, O(N²) |
639 | * it is easy to understand :-) |
640 | |
641 | The disadvantages: |
642 | |
643 | * it does not try to find as many accelerators as possible |
644 | |
645 | TODO: |
646 | |
647 | * The result is always correct, but not necessarily optimal. Perhaps |
648 | it would be a good idea to add another algorithm with higher complexity |
649 | that gets used when this one fails, i.e. leaves widgets without |
650 | accelerators. |
651 | |
652 | * The weights probably need some tweaking so they make more sense. |
653 | |
654 | *********************************************************************/ |
655 | |
656 | void KAccelManagerAlgorithm::findAccelerators(KAccelStringList &result, QString &used) |
657 | { |
658 | KAccelStringList accel_strings = result; |
659 | |
660 | // initially remove all accelerators |
661 | for (KAccelStringList::Iterator it = result.begin(), total = result.end(); it != total; ++it) { |
662 | (*it).setAccel(-1); |
663 | } |
664 | |
665 | // pick the highest bids |
666 | for (int cnt = 0; cnt < accel_strings.count(); ++cnt) { |
667 | int max = 0; |
668 | int index = -1; |
669 | int accel = -1; |
670 | |
671 | // find maximum weight |
672 | for (int i = 0; i < accel_strings.count(); ++i) { |
673 | int a; |
674 | int m = accel_strings[i].maxWeight(index&: a, used); |
675 | if (m > max) { |
676 | max = m; |
677 | index = i; |
678 | accel = a; |
679 | } |
680 | } |
681 | |
682 | // stop if no more accelerators can be found |
683 | if (index < 0) { |
684 | return; |
685 | } |
686 | |
687 | // insert the accelerator |
688 | if (accel >= 0) { |
689 | result[index].setAccel(accel); |
690 | used.append(c: result[index].accelerator()); |
691 | } |
692 | |
693 | // make sure we don't visit this one again |
694 | accel_strings[index] = KAccelString(); |
695 | } |
696 | } |
697 | |
698 | /********************************************************************* |
699 | |
700 | class KPopupAccelManager - managing QMenu widgets dynamically |
701 | |
702 | *********************************************************************/ |
703 | |
704 | KPopupAccelManager::(QMenu *) |
705 | : QObject(popup) |
706 | , m_popup(popup) |
707 | , m_count(-1) |
708 | { |
709 | aboutToShow(); // do one check and then connect to show |
710 | connect(sender: popup, signal: &QMenu::aboutToShow, context: this, slot: &KPopupAccelManager::aboutToShow); |
711 | } |
712 | |
713 | void KPopupAccelManager::() |
714 | { |
715 | // Note: we try to be smart and avoid recalculating the accelerators |
716 | // whenever possible. Unfortunately, there is no way to know if an |
717 | // item has been added or removed, so we can not do much more than |
718 | // to compare the items each time the menu is shown :-( |
719 | |
720 | if (m_count != m_popup->actions().count()) { |
721 | findMenuEntries(list&: m_entries); |
722 | calculateAccelerators(); |
723 | m_count = m_popup->actions().count(); |
724 | } else { |
725 | KAccelStringList entries; |
726 | findMenuEntries(list&: entries); |
727 | if (entries != m_entries) { |
728 | m_entries = entries; |
729 | calculateAccelerators(); |
730 | } |
731 | } |
732 | } |
733 | |
734 | void KPopupAccelManager::() |
735 | { |
736 | // find the new accelerators |
737 | QString used; |
738 | KAccelManagerAlgorithm::findAccelerators(result&: m_entries, used); |
739 | |
740 | // change the menu entries |
741 | setMenuEntries(m_entries); |
742 | } |
743 | |
744 | void KPopupAccelManager::(KAccelStringList &list) |
745 | { |
746 | QString s; |
747 | |
748 | list.clear(); |
749 | |
750 | // read out the menu entries |
751 | const auto = m_popup->actions(); |
752 | for (QAction *maction : menuActions) { |
753 | if (maction->isSeparator()) { |
754 | continue; |
755 | } |
756 | |
757 | s = maction->text(); |
758 | |
759 | // in full menus, look at entries with global accelerators last |
760 | int weight = 50; |
761 | if (s.contains(c: QLatin1Char('\t'))) { |
762 | weight = 0; |
763 | } |
764 | |
765 | list.append(t: KAccelString(s, weight)); |
766 | |
767 | // have a look at the popup as well, if present |
768 | if (maction->menu()) { |
769 | KPopupAccelManager::manage(popup: maction->menu()); |
770 | } |
771 | } |
772 | } |
773 | |
774 | // Duplicated from qaction.cpp |
775 | static QString copy_of_qt_strippedText(QString s) |
776 | { |
777 | s.remove(s: QLatin1String("..." )); |
778 | for (int i = 0; i < s.size(); ++i) { |
779 | if (s.at(i) == QLatin1Char('&')) { |
780 | s.remove(i, len: 1); |
781 | } |
782 | } |
783 | return s.trimmed(); |
784 | } |
785 | |
786 | void KPopupAccelManager::(const KAccelStringList &list) |
787 | { |
788 | uint cnt = 0; |
789 | const auto = m_popup->actions(); |
790 | for (QAction *maction : menuActions) { |
791 | if (maction->isSeparator()) { |
792 | continue; |
793 | } |
794 | |
795 | QString iconText = maction->iconText(); |
796 | const QString oldText = maction->text(); |
797 | |
798 | // Check if iconText was generated by Qt. In that case ignore it (no support for CJK accelerators) and set it from the text. |
799 | if (iconText == copy_of_qt_strippedText(s: oldText)) { |
800 | iconText = removeAcceleratorMarker(label: oldText); |
801 | |
802 | // ensure we don't re-add the ... that Qt removes per default |
803 | iconText.remove(s: QLatin1String("..." )); |
804 | |
805 | if (iconText != maction->iconText()) { |
806 | maction->setIconText(iconText); |
807 | } |
808 | } |
809 | |
810 | if (KAcceleratorManagerPrivate::checkChange(as: list[cnt])) { |
811 | maction->setText(list[cnt].accelerated()); |
812 | } |
813 | cnt++; |
814 | } |
815 | } |
816 | |
817 | void KPopupAccelManager::(QMenu *) |
818 | { |
819 | // don't add more than one manager to a popup |
820 | if (popup->findChild<KPopupAccelManager *>(aName: QString()) == nullptr) { |
821 | new KPopupAccelManager(popup); |
822 | } |
823 | } |
824 | |
825 | void QWidgetStackAccelManager::manage(QStackedWidget *stack) |
826 | { |
827 | if (stack->findChild<QWidgetStackAccelManager *>(aName: QString()) == nullptr) { |
828 | new QWidgetStackAccelManager(stack); |
829 | } |
830 | } |
831 | |
832 | QWidgetStackAccelManager::QWidgetStackAccelManager(QStackedWidget *stack) |
833 | : QObject(stack) |
834 | , m_stack(stack) |
835 | { |
836 | currentChanged(child: stack->currentIndex()); // do one check and then connect to show |
837 | connect(sender: stack, signal: &QStackedWidget::currentChanged, context: this, slot: &QWidgetStackAccelManager::currentChanged); |
838 | } |
839 | |
840 | bool QWidgetStackAccelManager::eventFilter(QObject *watched, QEvent *e) |
841 | { |
842 | if (e->type() == QEvent::Show && qApp->activeWindow()) { |
843 | KAcceleratorManager::manage(qApp->activeWindow()); |
844 | watched->removeEventFilter(obj: this); |
845 | } |
846 | return false; |
847 | } |
848 | |
849 | void QWidgetStackAccelManager::currentChanged(int child) |
850 | { |
851 | if (child < 0 || child >= static_cast<QStackedWidget *>(parent())->count()) { |
852 | // NOTE: QStackedWidget emits currentChanged(-1) when it is emptied |
853 | return; |
854 | } |
855 | |
856 | static_cast<QStackedWidget *>(parent())->widget(child)->installEventFilter(filterObj: this); |
857 | } |
858 | |
859 | void KAcceleratorManager::setNoAccel(QWidget *widget) |
860 | { |
861 | KAcceleratorManagerPrivate::ignored_widgets[widget] = 1; |
862 | } |
863 | |
864 | void KAcceleratorManager::addStandardActionNames(const QStringList &names) |
865 | { |
866 | KAcceleratorManagerPrivate::addStandardActionNames(list: names); |
867 | } |
868 | |
869 | #include "moc_kacceleratormanager_p.cpp" |
870 | |