1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 1999 Reginald Stadlbauer <reggie@kde.org> |
4 | SPDX-FileCopyrightText: 1999 Simon Hausmann <hausmann@kde.org> |
5 | SPDX-FileCopyrightText: 2000 Nicolas Hadacek <haadcek@kde.org> |
6 | SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org> |
7 | SPDX-FileCopyrightText: 2000 Michael Koch <koch@kde.org> |
8 | SPDX-FileCopyrightText: 2001 Holger Freyther <freyther@kde.org> |
9 | SPDX-FileCopyrightText: 2002 Ellis Whitehead <ellis@kde.org> |
10 | SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org> |
11 | SPDX-FileCopyrightText: 2005-2007 Hamish Rodda <rodda@kde.org> |
12 | |
13 | SPDX-License-Identifier: LGPL-2.0-only |
14 | */ |
15 | |
16 | #include "config-xmlgui.h" |
17 | |
18 | #include "kactioncollection.h" |
19 | |
20 | #include "debug.h" |
21 | #include "kactioncategory.h" |
22 | #include "kxmlguiclient.h" |
23 | #include "kxmlguifactory.h" |
24 | |
25 | #include <KAuthorized> |
26 | #include <KConfigGroup> |
27 | #if HAVE_GLOBALACCEL |
28 | #include <KGlobalAccel> |
29 | #endif |
30 | #include <KSharedConfig> |
31 | |
32 | #include <QDomDocument> |
33 | #include <QGuiApplication> |
34 | #include <QList> |
35 | #include <QMetaMethod> |
36 | #include <QSet> |
37 | |
38 | static bool actionHasGlobalShortcut(const QAction *action) |
39 | { |
40 | #if HAVE_GLOBALACCEL |
41 | return KGlobalAccel::self()->hasShortcut(action); |
42 | #else |
43 | Q_UNUSED(action) |
44 | return false; |
45 | #endif |
46 | } |
47 | |
48 | struct ActionStorage { |
49 | void addAction(const QString &name, QAction *action) |
50 | { |
51 | Q_ASSERT(std::find(m_actions.begin(), m_actions.end(), action) == m_actions.end()); |
52 | m_actions.push_back(t: action); |
53 | m_names.push_back(t: name); |
54 | Q_ASSERT(m_names.size() == m_actions.size()); |
55 | } |
56 | |
57 | bool removeAction(QAction *action) |
58 | { |
59 | auto it = std::find(first: m_actions.begin(), last: m_actions.end(), val: action); |
60 | if (it != m_actions.end()) { |
61 | // We can't have the same action twice. |
62 | Q_ASSERT(std::find(it + 1, m_actions.end(), action) == m_actions.end()); |
63 | auto idx = std::distance(first: m_actions.begin(), last: it); |
64 | m_names.erase(pos: m_names.begin() + idx); |
65 | m_actions.erase(pos: it); |
66 | return true; |
67 | } |
68 | Q_ASSERT(m_names.size() == m_actions.size()); |
69 | return false; |
70 | } |
71 | |
72 | QAction *findAction(const QString &name) const |
73 | { |
74 | auto it = std::find(first: m_names.begin(), last: m_names.end(), val: name); |
75 | if (it != m_names.end() && *it == name) { |
76 | return m_actions[std::distance(first: m_names.begin(), last: it)]; |
77 | } |
78 | return nullptr; |
79 | } |
80 | |
81 | void clear() |
82 | { |
83 | m_actions = {}; |
84 | m_names = {}; |
85 | } |
86 | |
87 | int size() const |
88 | { |
89 | return m_actions.size(); |
90 | } |
91 | |
92 | template<typename F> |
93 | void foreachAction(F f) |
94 | { |
95 | Q_ASSERT(m_names.size() == m_actions.size()); |
96 | for (int i = 0; i < m_actions.size(); ++i) { |
97 | f(m_names.at(i), m_actions.at(i)); |
98 | } |
99 | } |
100 | |
101 | const QList<QAction *> &actions() const |
102 | { |
103 | return m_actions; |
104 | } |
105 | |
106 | private: |
107 | // 1:1 list of names and actions |
108 | QList<QString> m_names; |
109 | QList<QAction *> m_actions; |
110 | }; |
111 | |
112 | class KActionCollectionPrivate |
113 | { |
114 | public: |
115 | KActionCollectionPrivate(KActionCollection *qq) |
116 | : q(qq) |
117 | , configIsGlobal(false) |
118 | , connectTriggered(false) |
119 | , connectHovered(false) |
120 | |
121 | { |
122 | } |
123 | |
124 | void setComponentForAction(QAction *action) |
125 | { |
126 | const bool hasGlobalShortcut = actionHasGlobalShortcut(action); |
127 | if (!hasGlobalShortcut) { |
128 | action->setProperty(name: "componentName" , value: m_componentName); |
129 | action->setProperty(name: "componentDisplayName" , value: m_componentDisplayName); |
130 | } |
131 | } |
132 | |
133 | static QList<KActionCollection *> s_allCollections; |
134 | |
135 | void _k_associatedWidgetDestroyed(QObject *obj); |
136 | void _k_actionDestroyed(QObject *obj); |
137 | |
138 | bool writeKXMLGUIConfigFile(); |
139 | |
140 | QString m_componentName; |
141 | QString m_componentDisplayName; |
142 | |
143 | //! Remove a action from our internal bookkeeping. Returns a nullptr if the |
144 | //! action doesn't belong to us. |
145 | QAction *unlistAction(QAction *); |
146 | |
147 | ActionStorage actionStore; |
148 | |
149 | KActionCollection *q = nullptr; |
150 | |
151 | const KXMLGUIClient *m_parentGUIClient = nullptr; |
152 | |
153 | QString configGroup{QStringLiteral("Shortcuts" )}; |
154 | bool configIsGlobal : 1; |
155 | |
156 | bool connectTriggered : 1; |
157 | bool connectHovered : 1; |
158 | |
159 | QList<QWidget *> associatedWidgets; |
160 | }; |
161 | |
162 | QList<KActionCollection *> KActionCollectionPrivate::s_allCollections; |
163 | |
164 | KActionCollection::KActionCollection(QObject *parent, const QString &cName) |
165 | : QObject(parent) |
166 | , d(new KActionCollectionPrivate(this)) |
167 | { |
168 | KActionCollectionPrivate::s_allCollections.append(t: this); |
169 | |
170 | setComponentName(cName); |
171 | } |
172 | |
173 | KActionCollection::KActionCollection(const KXMLGUIClient *parent) |
174 | : QObject(nullptr) |
175 | , d(new KActionCollectionPrivate(this)) |
176 | { |
177 | KActionCollectionPrivate::s_allCollections.append(t: this); |
178 | |
179 | d->m_parentGUIClient = parent; |
180 | d->m_componentName = parent->componentName(); |
181 | } |
182 | |
183 | KActionCollection::~KActionCollection() |
184 | { |
185 | KActionCollectionPrivate::s_allCollections.removeAll(t: this); |
186 | } |
187 | |
188 | void KActionCollection::clear() |
189 | { |
190 | const QList<QAction *> copy = d->actionStore.actions(); |
191 | qDeleteAll(c: copy); |
192 | d->actionStore.clear(); |
193 | } |
194 | |
195 | QAction *KActionCollection::action(const QString &name) const |
196 | { |
197 | if (!name.isEmpty()) { |
198 | return d->actionStore.findAction(name); |
199 | } |
200 | return nullptr; |
201 | } |
202 | |
203 | QAction *KActionCollection::action(int index) const |
204 | { |
205 | // ### investigate if any apps use this at all |
206 | return actions().value(i: index); |
207 | } |
208 | |
209 | int KActionCollection::count() const |
210 | { |
211 | return d->actionStore.size(); |
212 | } |
213 | |
214 | bool KActionCollection::isEmpty() const |
215 | { |
216 | return count() == 0; |
217 | } |
218 | |
219 | void KActionCollection::setComponentName(const QString &cName) |
220 | { |
221 | for (auto *a : d->actionStore.actions()) { |
222 | if (actionHasGlobalShortcut(action: a)) { |
223 | // Its component name is part of an action's signature in the context of |
224 | // global shortcuts and the semantics of changing an existing action's |
225 | // signature are, as it seems, impossible to get right. |
226 | qCWarning(DEBUG_KXMLGUI) << "KActionCollection::setComponentName does not work on a KActionCollection containing actions with global shortcuts!" |
227 | << cName; |
228 | break; |
229 | } |
230 | } |
231 | |
232 | if (!cName.isEmpty()) { |
233 | d->m_componentName = cName; |
234 | } else { |
235 | d->m_componentName = QCoreApplication::applicationName(); |
236 | } |
237 | } |
238 | |
239 | QString KActionCollection::componentName() const |
240 | { |
241 | return d->m_componentName; |
242 | } |
243 | |
244 | void KActionCollection::setComponentDisplayName(const QString &displayName) |
245 | { |
246 | d->m_componentDisplayName = displayName; |
247 | } |
248 | |
249 | QString KActionCollection::componentDisplayName() const |
250 | { |
251 | if (!d->m_componentDisplayName.isEmpty()) { |
252 | return d->m_componentDisplayName; |
253 | } |
254 | if (!QGuiApplication::applicationDisplayName().isEmpty()) { |
255 | return QGuiApplication::applicationDisplayName(); |
256 | } |
257 | return QCoreApplication::applicationName(); |
258 | } |
259 | |
260 | const KXMLGUIClient *KActionCollection::parentGUIClient() const |
261 | { |
262 | return d->m_parentGUIClient; |
263 | } |
264 | |
265 | QList<QAction *> KActionCollection::actions() const |
266 | { |
267 | return d->actionStore.actions(); |
268 | } |
269 | |
270 | const QList<QAction *> KActionCollection::actionsWithoutGroup() const |
271 | { |
272 | QList<QAction *> ret; |
273 | for (auto *action : std::as_const(t: d->actionStore.actions())) { |
274 | if (!action->actionGroup()) { |
275 | ret.append(t: action); |
276 | } |
277 | } |
278 | return ret; |
279 | } |
280 | |
281 | const QList<QActionGroup *> KActionCollection::actionGroups() const |
282 | { |
283 | QSet<QActionGroup *> set; |
284 | for (auto *action : std::as_const(t: d->actionStore.actions())) { |
285 | if (action->actionGroup()) { |
286 | set.insert(value: action->actionGroup()); |
287 | } |
288 | } |
289 | return set.values(); |
290 | } |
291 | |
292 | QAction *KActionCollection::addAction(const QString &name, QAction *action) |
293 | { |
294 | if (!action) { |
295 | return action; |
296 | } |
297 | |
298 | const QString objectName = action->objectName(); |
299 | QString indexName = name; |
300 | |
301 | if (indexName.isEmpty()) { |
302 | // No name provided. Use the objectName. |
303 | indexName = objectName; |
304 | |
305 | } else { |
306 | // A name was provided. Check against objectName. |
307 | if ((!objectName.isEmpty()) && (objectName != indexName)) { |
308 | // The user specified a new name and the action already has a |
309 | // different one. The objectName is used for saving shortcut |
310 | // settings to disk. Both for local and global shortcuts. |
311 | qCDebug(DEBUG_KXMLGUI) << "Registering action " << objectName << " under new name " << indexName; |
312 | // If there is a global shortcuts it's a very bad idea. |
313 | #if HAVE_GLOBALACCEL |
314 | if (KGlobalAccel::self()->hasShortcut(action)) { |
315 | // In debug mode assert |
316 | Q_ASSERT(!KGlobalAccel::self()->hasShortcut(action)); |
317 | // In release mode keep the old name |
318 | qCCritical(DEBUG_KXMLGUI) << "Changing action name from " << objectName << " to " << indexName |
319 | << "\nignored because of active global shortcut." ; |
320 | indexName = objectName; |
321 | } |
322 | #endif |
323 | } |
324 | |
325 | // Set the new name |
326 | action->setObjectName(indexName); |
327 | } |
328 | |
329 | // No name provided and the action had no name. Make one up. This will not |
330 | // work when trying to save shortcuts. Both local and global shortcuts. |
331 | if (indexName.isEmpty()) { |
332 | indexName = QString::asprintf(format: "unnamed-%p" , (void *)action); |
333 | action->setObjectName(indexName); |
334 | } |
335 | |
336 | // From now on the objectName has to have a value. Else we cannot safely |
337 | // remove actions. |
338 | Q_ASSERT(!action->objectName().isEmpty()); |
339 | |
340 | // look if we already have THIS action under THIS name ;) |
341 | auto oldAction = d->actionStore.findAction(name: indexName); |
342 | if (oldAction == action) { |
343 | return action; |
344 | } |
345 | |
346 | if (!KAuthorized::authorizeAction(action: indexName)) { |
347 | // Disable this action |
348 | action->setEnabled(false); |
349 | action->setVisible(false); |
350 | action->blockSignals(b: true); |
351 | } |
352 | |
353 | // Check if we have another action under this name |
354 | if (oldAction) { |
355 | takeAction(action: oldAction); |
356 | } |
357 | |
358 | // Remove if we have this action under a different name. |
359 | // Not using takeAction because we don't want to remove it from categories, |
360 | // and because it has the new name already. |
361 | d->actionStore.removeAction(action); |
362 | |
363 | // Add action to our lists. |
364 | d->actionStore.addAction(name: indexName, action); |
365 | |
366 | for (QWidget *widget : std::as_const(t&: d->associatedWidgets)) { |
367 | widget->addAction(action); |
368 | } |
369 | |
370 | connect(sender: action, signal: &QObject::destroyed, context: this, slot: [this](QObject *obj) { |
371 | d->_k_actionDestroyed(obj); |
372 | }); |
373 | |
374 | d->setComponentForAction(action); |
375 | |
376 | if (d->connectHovered) { |
377 | connect(sender: action, signal: &QAction::hovered, context: this, slot: &KActionCollection::slotActionHovered); |
378 | } |
379 | |
380 | if (d->connectTriggered) { |
381 | connect(sender: action, signal: &QAction::triggered, context: this, slot: &KActionCollection::slotActionTriggered); |
382 | } |
383 | |
384 | Q_EMIT inserted(action); |
385 | Q_EMIT changed(); |
386 | return action; |
387 | } |
388 | |
389 | void KActionCollection::addActions(const QList<QAction *> &actions) |
390 | { |
391 | for (QAction *action : actions) { |
392 | addAction(name: action->objectName(), action); |
393 | } |
394 | } |
395 | |
396 | void KActionCollection::removeAction(QAction *action) |
397 | { |
398 | delete takeAction(action); |
399 | } |
400 | |
401 | QAction *KActionCollection::takeAction(QAction *action) |
402 | { |
403 | if (!d->unlistAction(action)) { |
404 | return nullptr; |
405 | } |
406 | |
407 | // Remove the action from all widgets |
408 | for (QWidget *widget : std::as_const(t&: d->associatedWidgets)) { |
409 | widget->removeAction(action); |
410 | } |
411 | |
412 | action->disconnect(receiver: this); |
413 | |
414 | Q_EMIT changed(); |
415 | return action; |
416 | } |
417 | |
418 | QAction *KActionCollection::addAction(KStandardAction::StandardAction actionType, const QObject *receiver, const char *member) |
419 | { |
420 | QAction *action = KStandardAction::create(id: actionType, recvr: receiver, slot: member, parent: this); |
421 | return action; |
422 | } |
423 | |
424 | QAction *KActionCollection::addAction(KStandardAction::StandardAction actionType, const QString &name, const QObject *receiver, const char *member) |
425 | { |
426 | // pass 0 as parent, because if the parent is a KActionCollection KStandardAction::create automatically |
427 | // adds the action to it under the default name. We would trigger the |
428 | // warning about renaming the action then. |
429 | QAction *action = KStandardAction::create(id: actionType, recvr: receiver, slot: member, parent: nullptr); |
430 | // Give it a parent for gc. |
431 | action->setParent(this); |
432 | // Remove the name to get rid of the "rename action" warning above |
433 | action->setObjectName(name); |
434 | // And now add it with the desired name. |
435 | return addAction(name, action); |
436 | } |
437 | |
438 | QAction *KActionCollection::addAction(KStandardActions::StandardAction actionType) |
439 | { |
440 | // Use implementation from KConfigWidgets instead of KConfigGui |
441 | // as it provides tighter integration with QtWidgets applications. |
442 | // KStandardAction automatically adds it to the collection. |
443 | QAction *action = KStandardAction::create(id: static_cast<KStandardAction::StandardAction>(actionType), recvr: nullptr, slot: {}, parent: this); |
444 | return action; |
445 | } |
446 | |
447 | QAction *KActionCollection::addAction(KStandardActions::StandardAction actionType, const QString &name) |
448 | { |
449 | // Use implementation from KConfigWidgets instead of KConfigGui |
450 | // as it provides tighter integration with QtWidgets applications. |
451 | QAction *action = KStandardAction::create(id: static_cast<KStandardAction::StandardAction>(actionType), recvr: nullptr, slot: {}, parent: nullptr); |
452 | action->setParent(this); |
453 | action->setObjectName(name); |
454 | return addAction(name, action); |
455 | } |
456 | |
457 | QAction *KActionCollection::addAction(const QString &name, const QObject *receiver, const char *member) |
458 | { |
459 | QAction *a = new QAction(this); |
460 | if (receiver && member) { |
461 | connect(sender: a, SIGNAL(triggered(bool)), receiver, member); |
462 | } |
463 | return addAction(name, action: a); |
464 | } |
465 | |
466 | QKeySequence KActionCollection::defaultShortcut(QAction *action) |
467 | { |
468 | const QList<QKeySequence> shortcuts = defaultShortcuts(action); |
469 | return shortcuts.isEmpty() ? QKeySequence() : shortcuts.first(); |
470 | } |
471 | |
472 | QList<QKeySequence> KActionCollection::defaultShortcuts(QAction *action) |
473 | { |
474 | return action->property(name: "defaultShortcuts" ).value<QList<QKeySequence>>(); |
475 | } |
476 | |
477 | void KActionCollection::setDefaultShortcut(QAction *action, const QKeySequence &shortcut) |
478 | { |
479 | setDefaultShortcuts(action, shortcuts: QList<QKeySequence>() << shortcut); |
480 | } |
481 | |
482 | void KActionCollection::setDefaultShortcuts(QAction *action, const QList<QKeySequence> &shortcuts) |
483 | { |
484 | action->setShortcuts(shortcuts); |
485 | action->setProperty(name: "defaultShortcuts" , value: QVariant::fromValue(value: shortcuts)); |
486 | } |
487 | |
488 | bool KActionCollection::isShortcutsConfigurable(QAction *action) |
489 | { |
490 | // Considered as true by default |
491 | const QVariant value = action->property(name: "isShortcutConfigurable" ); |
492 | return value.isValid() ? value.toBool() : true; |
493 | } |
494 | |
495 | void KActionCollection::setShortcutsConfigurable(QAction *action, bool configurable) |
496 | { |
497 | action->setProperty(name: "isShortcutConfigurable" , value: configurable); |
498 | } |
499 | |
500 | QString KActionCollection::configGroup() const |
501 | { |
502 | return d->configGroup; |
503 | } |
504 | |
505 | void KActionCollection::setConfigGroup(const QString &group) |
506 | { |
507 | d->configGroup = group; |
508 | } |
509 | |
510 | bool KActionCollection::configIsGlobal() const |
511 | { |
512 | return d->configIsGlobal; |
513 | } |
514 | |
515 | void KActionCollection::setConfigGlobal(bool global) |
516 | { |
517 | d->configIsGlobal = global; |
518 | } |
519 | |
520 | void KActionCollection::importGlobalShortcuts(KConfigGroup *config) |
521 | { |
522 | #if HAVE_GLOBALACCEL |
523 | Q_ASSERT(config); |
524 | if (!config || !config->exists()) { |
525 | return; |
526 | } |
527 | |
528 | d->actionStore.foreachAction(f: [config](const QString &actionName, QAction *action) { |
529 | if (!action) { |
530 | return; |
531 | } |
532 | |
533 | if (isShortcutsConfigurable(action)) { |
534 | QString entry = config->readEntry(key: actionName, aDefault: QString()); |
535 | if (!entry.isEmpty()) { |
536 | KGlobalAccel::self()->setShortcut(action, shortcut: QKeySequence::listFromString(str: entry), loadFlag: KGlobalAccel::NoAutoloading); |
537 | } else { |
538 | QList<QKeySequence> defaultShortcut = KGlobalAccel::self()->defaultShortcut(action); |
539 | KGlobalAccel::self()->setShortcut(action, shortcut: defaultShortcut, loadFlag: KGlobalAccel::NoAutoloading); |
540 | } |
541 | } |
542 | }); |
543 | #else |
544 | Q_UNUSED(config); |
545 | #endif |
546 | } |
547 | |
548 | void KActionCollection::readSettings(KConfigGroup *config) |
549 | { |
550 | KConfigGroup cg(KSharedConfig::openConfig(), configGroup()); |
551 | if (!config) { |
552 | config = &cg; |
553 | } |
554 | |
555 | if (!config->exists()) { |
556 | return; |
557 | } |
558 | |
559 | d->actionStore.foreachAction(f: [config](const QString &actionName, QAction *action) { |
560 | if (!action) { |
561 | return; |
562 | } |
563 | |
564 | if (isShortcutsConfigurable(action)) { |
565 | QString entry = config->readEntry(key: actionName, aDefault: QString()); |
566 | if (!entry.isEmpty()) { |
567 | action->setShortcuts(QKeySequence::listFromString(str: entry)); |
568 | } else { |
569 | action->setShortcuts(defaultShortcuts(action)); |
570 | } |
571 | } |
572 | }); |
573 | |
574 | // qCDebug(DEBUG_KXMLGUI) << " done"; |
575 | } |
576 | |
577 | void KActionCollection::exportGlobalShortcuts(KConfigGroup *config, bool writeAll) const |
578 | { |
579 | #if HAVE_GLOBALACCEL |
580 | Q_ASSERT(config); |
581 | if (!config) { |
582 | return; |
583 | } |
584 | |
585 | d->actionStore.foreachAction(f: [config, this, writeAll](const QString &actionName, QAction *action) { |
586 | if (!action) { |
587 | return; |
588 | } |
589 | // If the action name starts with unnamed- spit out a warning. That name |
590 | // will change at will and will break loading writing |
591 | if (actionName.startsWith(s: QLatin1String("unnamed-" ))) { |
592 | qCCritical(DEBUG_KXMLGUI) << "Skipped exporting Shortcut for action without name " << action->text() << "!" ; |
593 | return; |
594 | } |
595 | |
596 | if (isShortcutsConfigurable(action) && KGlobalAccel::self()->hasShortcut(action)) { |
597 | bool bConfigHasAction = !config->readEntry(key: actionName, aDefault: QString()).isEmpty(); |
598 | bool bSameAsDefault = (KGlobalAccel::self()->shortcut(action) == KGlobalAccel::self()->defaultShortcut(action)); |
599 | // If we're using a global config or this setting |
600 | // differs from the default, then we want to write. |
601 | KConfigGroup::WriteConfigFlags flags = KConfigGroup::Persistent; |
602 | if (configIsGlobal()) { |
603 | flags |= KConfigGroup::Global; |
604 | } |
605 | if (writeAll || !bSameAsDefault) { |
606 | QString s = QKeySequence::listToString(list: KGlobalAccel::self()->shortcut(action)); |
607 | if (s.isEmpty()) { |
608 | s = QStringLiteral("none" ); |
609 | } |
610 | qCDebug(DEBUG_KXMLGUI) << "\twriting " << actionName << " = " << s; |
611 | config->writeEntry(key: actionName, value: s, pFlags: flags); |
612 | } |
613 | // Otherwise, this key is the same as default |
614 | // but exists in config file. Remove it. |
615 | else if (bConfigHasAction) { |
616 | qCDebug(DEBUG_KXMLGUI) << "\tremoving " << actionName << " because == default" ; |
617 | config->deleteEntry(pKey: actionName, pFlags: flags); |
618 | } |
619 | } |
620 | }); |
621 | |
622 | config->sync(); |
623 | #else |
624 | Q_UNUSED(config); |
625 | Q_UNUSED(writeAll); |
626 | #endif |
627 | } |
628 | |
629 | bool KActionCollectionPrivate::writeKXMLGUIConfigFile() |
630 | { |
631 | const KXMLGUIClient *kxmlguiClient = q->parentGUIClient(); |
632 | // return false if there is no KXMLGUIClient |
633 | if (!kxmlguiClient || kxmlguiClient->xmlFile().isEmpty()) { |
634 | return false; |
635 | } |
636 | |
637 | qCDebug(DEBUG_KXMLGUI) << "xmlFile=" << kxmlguiClient->xmlFile(); |
638 | |
639 | QString attrShortcut = QStringLiteral("shortcut" ); |
640 | |
641 | // Read XML file |
642 | QString sXml(KXMLGUIFactory::readConfigFile(filename: kxmlguiClient->xmlFile(), componentName: q->componentName())); |
643 | QDomDocument doc; |
644 | doc.setContent(data: sXml); |
645 | |
646 | // Process XML data |
647 | |
648 | // Get hold of ActionProperties tag |
649 | QDomElement elem = KXMLGUIFactory::actionPropertiesElement(doc); |
650 | |
651 | // now, iterate through our actions |
652 | actionStore.foreachAction(f: [&elem, &attrShortcut, this](const QString &actionName, QAction *action) { |
653 | if (!action) { |
654 | return; |
655 | } |
656 | |
657 | // If the action name starts with unnamed- spit out a warning and ignore |
658 | // it. That name will change at will and will break loading writing |
659 | if (actionName.startsWith(s: QLatin1String("unnamed-" ))) { |
660 | qCCritical(DEBUG_KXMLGUI) << "Skipped writing shortcut for action " << actionName << "(" << action->text() << ")!" ; |
661 | return; |
662 | } |
663 | |
664 | bool bSameAsDefault = (action->shortcuts() == q->defaultShortcuts(action)); |
665 | qCDebug(DEBUG_KXMLGUI) << "name = " << actionName << " shortcut = " << QKeySequence::listToString(list: action->shortcuts()) |
666 | #if HAVE_GLOBALACCEL |
667 | << " globalshortcut = " << QKeySequence::listToString(list: KGlobalAccel::self()->shortcut(action)) |
668 | #endif |
669 | << " def = " << QKeySequence::listToString(list: q->defaultShortcuts(action)); |
670 | |
671 | // now see if this element already exists |
672 | // and create it if necessary (unless bSameAsDefault) |
673 | QDomElement act_elem = KXMLGUIFactory::findActionByName(elem, sName: actionName, create: !bSameAsDefault); |
674 | if (act_elem.isNull()) { |
675 | return; |
676 | } |
677 | |
678 | if (bSameAsDefault) { |
679 | act_elem.removeAttribute(name: attrShortcut); |
680 | // qCDebug(DEBUG_KXMLGUI) << "act_elem.attributes().count() = " << act_elem.attributes().count(); |
681 | if (act_elem.attributes().count() == 1) { |
682 | elem.removeChild(oldChild: act_elem); |
683 | } |
684 | } else { |
685 | act_elem.setAttribute(name: attrShortcut, value: QKeySequence::listToString(list: action->shortcuts())); |
686 | } |
687 | }); |
688 | |
689 | // Write back to XML file |
690 | KXMLGUIFactory::saveConfigFile(doc, filename: kxmlguiClient->localXMLFile(), componentName: q->componentName()); |
691 | // and since we just changed the xml file clear the dom we have in memory |
692 | // it'll be rebuilt if needed |
693 | const_cast<KXMLGUIClient *>(kxmlguiClient)->setXMLGUIBuildDocument({}); |
694 | return true; |
695 | } |
696 | |
697 | void KActionCollection::writeSettings(KConfigGroup *config, bool writeAll, QAction *oneAction) const |
698 | { |
699 | // If the caller didn't provide a config group we try to save the KXMLGUI |
700 | // Configuration file. If that succeeds we are finished. |
701 | if (config == nullptr && d->writeKXMLGUIConfigFile()) { |
702 | return; |
703 | } |
704 | |
705 | KConfigGroup cg(KSharedConfig::openConfig(), configGroup()); |
706 | if (!config) { |
707 | config = &cg; |
708 | } |
709 | |
710 | QList<QAction *> writeActions; |
711 | if (oneAction) { |
712 | writeActions.append(t: oneAction); |
713 | } else { |
714 | writeActions = actions(); |
715 | } |
716 | |
717 | d->actionStore.foreachAction(f: [config, this, writeAll](const QString &actionName, QAction *action) { |
718 | if (!action) { |
719 | return; |
720 | } |
721 | |
722 | // If the action name starts with unnamed- spit out a warning and ignore |
723 | // it. That name will change at will and will break loading writing |
724 | if (actionName.startsWith(s: QLatin1String("unnamed-" ))) { |
725 | qCCritical(DEBUG_KXMLGUI) << "Skipped saving Shortcut for action without name " << action->text() << "!" ; |
726 | return; |
727 | } |
728 | |
729 | // Write the shortcut |
730 | if (isShortcutsConfigurable(action)) { |
731 | bool bConfigHasAction = !config->readEntry(key: actionName, aDefault: QString()).isEmpty(); |
732 | bool bSameAsDefault = (action->shortcuts() == defaultShortcuts(action)); |
733 | // If we're using a global config or this setting |
734 | // differs from the default, then we want to write. |
735 | KConfigGroup::WriteConfigFlags flags = KConfigGroup::Persistent; |
736 | |
737 | // Honor the configIsGlobal() setting |
738 | if (configIsGlobal()) { |
739 | flags |= KConfigGroup::Global; |
740 | } |
741 | |
742 | if (writeAll || !bSameAsDefault) { |
743 | // We are instructed to write all shortcuts or the shortcut is |
744 | // not set to its default value. Write it |
745 | QString s = QKeySequence::listToString(list: action->shortcuts()); |
746 | if (s.isEmpty()) { |
747 | s = QStringLiteral("none" ); |
748 | } |
749 | qCDebug(DEBUG_KXMLGUI) << "\twriting " << actionName << " = " << s; |
750 | config->writeEntry(key: actionName, value: s, pFlags: flags); |
751 | |
752 | } else if (bConfigHasAction) { |
753 | // Otherwise, this key is the same as default but exists in |
754 | // config file. Remove it. |
755 | qCDebug(DEBUG_KXMLGUI) << "\tremoving " << actionName << " because == default" ; |
756 | config->deleteEntry(pKey: actionName, pFlags: flags); |
757 | } |
758 | } |
759 | }); |
760 | |
761 | config->sync(); |
762 | } |
763 | |
764 | void KActionCollection::slotActionTriggered() |
765 | { |
766 | QAction *action = qobject_cast<QAction *>(object: sender()); |
767 | if (action) { |
768 | Q_EMIT actionTriggered(action); |
769 | } |
770 | } |
771 | |
772 | void KActionCollection::slotActionHovered() |
773 | { |
774 | QAction *action = qobject_cast<QAction *>(object: sender()); |
775 | if (action) { |
776 | Q_EMIT actionHovered(action); |
777 | } |
778 | } |
779 | |
780 | // The downcast from a QObject to a QAction triggers UBSan |
781 | // but we're only comparing pointers, so UBSan shouldn't check vptrs |
782 | // Similar to https://github.com/itsBelinda/plog/pull/1/files |
783 | #if defined(__clang__) || __GNUC__ >= 8 |
784 | __attribute__((no_sanitize("vptr" ))) |
785 | #endif |
786 | void KActionCollectionPrivate::_k_actionDestroyed(QObject *obj) |
787 | { |
788 | // obj isn't really a QAction anymore. So make sure we don't do fancy stuff |
789 | // with it. |
790 | QAction *action = static_cast<QAction *>(obj); |
791 | |
792 | if (!unlistAction(action)) { |
793 | return; |
794 | } |
795 | |
796 | Q_EMIT q->changed(); |
797 | } |
798 | |
799 | void KActionCollection::connectNotify(const QMetaMethod &signal) |
800 | { |
801 | if (d->connectHovered && d->connectTriggered) { |
802 | return; |
803 | } |
804 | |
805 | if (signal.methodSignature() == "actionHovered(QAction*)" ) { |
806 | if (!d->connectHovered) { |
807 | d->connectHovered = true; |
808 | |
809 | for (auto *action : d->actionStore.actions()) { |
810 | connect(sender: action, signal: &QAction::hovered, context: this, slot: &KActionCollection::slotActionHovered); |
811 | } |
812 | } |
813 | |
814 | } else if (signal.methodSignature() == "actionTriggered(QAction*)" ) { |
815 | if (!d->connectTriggered) { |
816 | d->connectTriggered = true; |
817 | for (auto *action : d->actionStore.actions()) { |
818 | connect(sender: action, signal: &QAction::triggered, context: this, slot: &KActionCollection::slotActionTriggered); |
819 | } |
820 | } |
821 | } |
822 | |
823 | QObject::connectNotify(signal); |
824 | } |
825 | |
826 | const QList<KActionCollection *> &KActionCollection::allCollections() |
827 | { |
828 | return KActionCollectionPrivate::s_allCollections; |
829 | } |
830 | |
831 | void KActionCollection::associateWidget(QWidget *widget) const |
832 | { |
833 | for (auto *action : d->actionStore.actions()) { |
834 | if (!widget->actions().contains(t: action)) { |
835 | widget->addAction(action); |
836 | } |
837 | } |
838 | } |
839 | |
840 | void KActionCollection::addAssociatedWidget(QWidget *widget) |
841 | { |
842 | if (!d->associatedWidgets.contains(t: widget)) { |
843 | widget->addActions(actions: actions()); |
844 | |
845 | d->associatedWidgets.append(t: widget); |
846 | connect(sender: widget, signal: &QObject::destroyed, context: this, slot: [this](QObject *obj) { |
847 | d->_k_associatedWidgetDestroyed(obj); |
848 | }); |
849 | } |
850 | } |
851 | |
852 | void KActionCollection::removeAssociatedWidget(QWidget *widget) |
853 | { |
854 | for (auto *action : d->actionStore.actions()) { |
855 | widget->removeAction(action); |
856 | } |
857 | |
858 | d->associatedWidgets.removeAll(t: widget); |
859 | disconnect(sender: widget, signal: &QObject::destroyed, receiver: this, zero: nullptr); |
860 | } |
861 | |
862 | QAction *KActionCollectionPrivate::unlistAction(QAction *action) |
863 | { |
864 | // ATTENTION: |
865 | // This method is called with an QObject formerly known as a QAction |
866 | // during _k_actionDestroyed(). So don't do fancy stuff here that needs a |
867 | // real QAction! |
868 | |
869 | // Remove the action |
870 | if (!actionStore.removeAction(action)) { |
871 | return nullptr; |
872 | } |
873 | |
874 | // Remove the action from the categories. Should be only one |
875 | const QList<KActionCategory *> categories = q->findChildren<KActionCategory *>(); |
876 | for (KActionCategory *category : categories) { |
877 | category->unlistAction(action); |
878 | } |
879 | |
880 | return action; |
881 | } |
882 | |
883 | QList<QWidget *> KActionCollection::associatedWidgets() const |
884 | { |
885 | return d->associatedWidgets; |
886 | } |
887 | |
888 | void KActionCollection::clearAssociatedWidgets() |
889 | { |
890 | for (QWidget *widget : std::as_const(t&: d->associatedWidgets)) { |
891 | for (auto *action : d->actionStore.actions()) { |
892 | widget->removeAction(action); |
893 | } |
894 | } |
895 | |
896 | d->associatedWidgets.clear(); |
897 | } |
898 | |
899 | void KActionCollectionPrivate::_k_associatedWidgetDestroyed(QObject *obj) |
900 | { |
901 | associatedWidgets.removeAll(t: static_cast<QWidget *>(obj)); |
902 | } |
903 | |
904 | #include "moc_kactioncollection.cpp" |
905 | |