/* This file is part of the KDE libraries SPDX-FileCopyrightText: 1999 Reginald Stadlbauer SPDX-FileCopyrightText: 1999 Simon Hausmann SPDX-FileCopyrightText: 2000 Nicolas Hadacek SPDX-FileCopyrightText: 2000 Kurt Granroth SPDX-FileCopyrightText: 2000 Michael Koch SPDX-FileCopyrightText: 2001 Holger Freyther SPDX-FileCopyrightText: 2002 Ellis Whitehead SPDX-FileCopyrightText: 2002 Joseph Wenninger SPDX-FileCopyrightText: 2005-2007 Hamish Rodda SPDX-License-Identifier: LGPL-2.0-only */ #include "kirigamiactioncollection.h" #include #include #include #include "debug.h" #include #include #include #include #include #include class KirigamiActionCollectionPrivate { public: KirigamiActionCollectionPrivate(KirigamiActionCollection *qq) : q(qq) , configIsGlobal(false) , connectTriggered(false) , connectHovered(false) { } static QList s_allCollections; void _k_associatedWidgetDestroyed(QObject *obj); void _k_actionDestroyed(QObject *obj); QString m_componentName; QString m_componentDisplayName; //! Remove a action from our internal bookkeeping. Returns a nullptr if the //! action doesn't belong to us. QAction *unlistAction(QAction *); QMap actionByName; QList actions; KirigamiActionCollection *q = nullptr; QString configGroup{QStringLiteral("Shortcuts")}; bool configIsGlobal : 1; bool connectTriggered : 1; bool connectHovered : 1; }; QList KirigamiActionCollectionPrivate::s_allCollections; KirigamiActionCollection::KirigamiActionCollection(QObject *parent, const QString &cName) : QObject(parent) , d(new KirigamiActionCollectionPrivate(this)) { setObjectName(cName); KirigamiActionCollectionPrivate::s_allCollections.append(this); } KirigamiActionCollection::~KirigamiActionCollection() { KirigamiActionCollectionPrivate::s_allCollections.removeAll(this); } void KirigamiActionCollection::clear() { d->actionByName.clear(); qDeleteAll(d->actions); d->actions.clear(); } QAction *KirigamiActionCollection::action(const QString &name) const { QAction *action = nullptr; if (!name.isEmpty()) { action = d->actionByName.value(name); } return action; } QAction *KirigamiActionCollection::action(int index) const { // ### investigate if any apps use this at all return actions().value(index); } int KirigamiActionCollection::count() const { return d->actions.count(); } bool KirigamiActionCollection::isEmpty() const { return count() == 0; } void KirigamiActionCollection::setComponentName(const QString &cName) { if (!cName.isEmpty()) { d->m_componentName = cName; } else { d->m_componentName = QCoreApplication::applicationName(); } } QString KirigamiActionCollection::componentName() const { return d->m_componentName; } void KirigamiActionCollection::setComponentDisplayName(const QString &displayName) { d->m_componentDisplayName = displayName; } QString KirigamiActionCollection::componentDisplayName() const { if (!d->m_componentDisplayName.isEmpty()) { return d->m_componentDisplayName; } if (!QGuiApplication::applicationDisplayName().isEmpty()) { return QGuiApplication::applicationDisplayName(); } return QCoreApplication::applicationName(); } QList KirigamiActionCollection::actions() const { return d->actions; } const QList KirigamiActionCollection::actionsWithoutGroup() const { QList ret; for (QAction *action : std::as_const(d->actions)) { if (!action->actionGroup()) { ret.append(action); } } return ret; } const QList KirigamiActionCollection::actionGroups() const { QSet set; for (QAction *action : std::as_const(d->actions)) { if (action->actionGroup()) { set.insert(action->actionGroup()); } } return set.values(); } QAction *KirigamiActionCollection::addAction(const QString &name, QAction *action) { if (!action) { return action; } const QString objectName = action->objectName(); QString indexName = name; if (indexName.isEmpty()) { // No name provided. Use the objectName. indexName = objectName; } else { // A name was provided. Check against objectName. if ((!objectName.isEmpty()) && (objectName != indexName)) { // The user specified a new name and the action already has a // different one. The objectName is used for saving shortcut // settings to disk. Both for local and global shortcuts. qCDebug(BASEAPP_LOG) << "Registering action " << objectName << " under new name " << indexName; // If there is a global shortcuts it's a very bad idea. } // Set the new name action->setObjectName(indexName); } // No name provided and the action had no name. Make one up. This will not // work when trying to save shortcuts. Both local and global shortcuts. if (indexName.isEmpty()) { indexName = QString::asprintf("unnamed-%p", (void *)action); action->setObjectName(indexName); } // From now on the objectName has to have a value. Else we cannot safely // remove actions. Q_ASSERT(!action->objectName().isEmpty()); // look if we already have THIS action under THIS name ;) if (d->actionByName.value(indexName, nullptr) == action) { // This is not a multi map! Q_ASSERT(d->actionByName.count(indexName) == 1); return action; } if (!KAuthorized::authorizeAction(indexName)) { // Disable this action action->setEnabled(false); action->setVisible(false); action->blockSignals(true); } // Check if we have another action under this name if (QAction *oldAction = d->actionByName.value(indexName)) { takeAction(oldAction); } // Check if we have this action under a different name. // Not using takeAction because we don't want to remove it from categories, // and because it has the new name already. const int oldIndex = d->actions.indexOf(action); if (oldIndex != -1) { d->actionByName.remove(d->actionByName.key(action)); d->actions.removeAt(oldIndex); } // Add action to our lists. d->actionByName.insert(indexName, action); d->actions.append(action); connect(action, &QObject::destroyed, this, [this](QObject *obj) { d->_k_actionDestroyed(obj); }); if (d->connectHovered) { connect(action, &QAction::hovered, this, &KirigamiActionCollection::slotActionHovered); } if (d->connectTriggered) { connect(action, &QAction::triggered, this, &KirigamiActionCollection::slotActionTriggered); } Q_EMIT inserted(action); Q_EMIT changed(); return action; } void KirigamiActionCollection::addActions(const QList &actions) { for (QAction *action : actions) { addAction(action->objectName(), action); } } void KirigamiActionCollection::removeAction(QAction *action) { delete takeAction(action); } QAction *KirigamiActionCollection::takeAction(QAction *action) { if (!d->unlistAction(action)) { return nullptr; } action->disconnect(this); Q_EMIT changed(); return action; } QKeySequence KirigamiActionCollection::defaultShortcut(QAction *action) { const QList shortcuts = defaultShortcuts(action); return shortcuts.isEmpty() ? QKeySequence() : shortcuts.first(); } QList KirigamiActionCollection::defaultShortcuts(QAction *action) { return action->property("defaultShortcuts").value>(); } void KirigamiActionCollection::setDefaultShortcut(QAction *action, const QKeySequence &shortcut) { setDefaultShortcuts(action, QList() << shortcut); } void KirigamiActionCollection::setDefaultShortcuts(QAction *action, const QList &shortcuts) { action->setShortcuts(shortcuts); action->setProperty("defaultShortcuts", QVariant::fromValue(shortcuts)); } bool KirigamiActionCollection::isShortcutsConfigurable(QAction *action) { // Considered as true by default const QVariant value = action->property("isShortcutConfigurable"); return value.isValid() ? value.toBool() : true; } void KirigamiActionCollection::setShortcutsConfigurable(QAction *action, bool configurable) { action->setProperty("isShortcutConfigurable", configurable); } QString KirigamiActionCollection::configGroup() const { return d->configGroup; } void KirigamiActionCollection::setConfigGroup(const QString &group) { d->configGroup = group; } bool KirigamiActionCollection::configIsGlobal() const { return d->configIsGlobal; } void KirigamiActionCollection::setConfigGlobal(bool global) { d->configIsGlobal = global; } void KirigamiActionCollection::readSettings(KConfigGroup *config) { KConfigGroup cg(KSharedConfig::openConfig(), configGroup()); if (!config) { config = &cg; } if (!config->exists()) { return; } for (auto it = d->actionByName.constBegin(); it != d->actionByName.constEnd(); ++it) { QAction *action = it.value(); if (!action) { continue; } if (isShortcutsConfigurable(action)) { const QString &actionName = it.key(); QString entry = config->readEntry(actionName, QString()); if (!entry.isEmpty()) { action->setShortcuts(QKeySequence::listFromString(entry)); } else { action->setShortcuts(defaultShortcuts(action)); } } } // qCDebug(BASEAPP_LOG) << " done"; } void KirigamiActionCollection::writeSettings(KConfigGroup *config, bool writeAll, QAction *oneAction) const { KConfigGroup cg(KSharedConfig::openConfig(), configGroup()); if (!config) { config = &cg; } QList writeActions; if (oneAction) { writeActions.append(oneAction); } else { writeActions = actions(); } for (QMap::ConstIterator it = d->actionByName.constBegin(); it != d->actionByName.constEnd(); ++it) { QAction *action = it.value(); if (!action) { continue; } const QString &actionName = it.key(); // If the action name starts with unnamed- spit out a warning and ignore // it. That name will change at will and will break loading writing if (actionName.startsWith(QLatin1String("unnamed-"))) { qCCritical(BASEAPP_LOG) << "Skipped saving Shortcut for action without name " << action->text() << "!"; continue; } // Write the shortcut if (isShortcutsConfigurable(action)) { bool bConfigHasAction = !config->readEntry(actionName, QString()).isEmpty(); bool bSameAsDefault = (action->shortcuts() == defaultShortcuts(action)); // If we're using a global config or this setting // differs from the default, then we want to write. KConfigGroup::WriteConfigFlags flags = KConfigGroup::Persistent; // Honor the configIsGlobal() setting if (configIsGlobal()) { flags |= KConfigGroup::Global; } if (writeAll || !bSameAsDefault) { // We are instructed to write all shortcuts or the shortcut is // not set to its default value. Write it QString s = QKeySequence::listToString(action->shortcuts()); if (s.isEmpty()) { s = QStringLiteral("none"); } qCDebug(BASEAPP_LOG) << "\twriting " << actionName << " = " << s; config->writeEntry(actionName, s, flags); } else if (bConfigHasAction) { // Otherwise, this key is the same as default but exists in // config file. Remove it. qCDebug(BASEAPP_LOG) << "\tremoving " << actionName << " because == default"; config->deleteEntry(actionName, flags); } } } config->sync(); } void KirigamiActionCollection::slotActionTriggered() { QAction *action = qobject_cast(sender()); if (action) { Q_EMIT actionTriggered(action); } } void KirigamiActionCollection::slotActionHovered() { QAction *action = qobject_cast(sender()); if (action) { Q_EMIT actionHovered(action); } } // The downcast from a QObject to a QAction triggers UBSan // but we're only comparing pointers, so UBSan shouldn't check vptrs // Similar to https://github.com/itsBelinda/plog/pull/1/files #if defined(__clang__) || __GNUC__ >= 8 __attribute__((no_sanitize("vptr"))) #endif void KirigamiActionCollectionPrivate::_k_actionDestroyed(QObject *obj) { // obj isn't really a QAction anymore. So make sure we don't do fancy stuff // with it. QAction *action = static_cast(obj); if (!unlistAction(action)) { return; } Q_EMIT q->changed(); } void KirigamiActionCollection::connectNotify(const QMetaMethod &signal) { if (d->connectHovered && d->connectTriggered) { return; } if (signal.methodSignature() == "actionHovered(QAction*)") { if (!d->connectHovered) { d->connectHovered = true; for (QAction *action : std::as_const(d->actions)) { connect(action, &QAction::hovered, this, &KirigamiActionCollection::slotActionHovered); } } } else if (signal.methodSignature() == "actionTriggered(QAction*)") { if (!d->connectTriggered) { d->connectTriggered = true; for (QAction *action : std::as_const(d->actions)) { connect(action, &QAction::triggered, this, &KirigamiActionCollection::slotActionTriggered); } } } QObject::connectNotify(signal); } const QList &KirigamiActionCollection::allCollections() { return KirigamiActionCollectionPrivate::s_allCollections; } QAction *KirigamiActionCollectionPrivate::unlistAction(QAction *action) { // ATTENTION: // This method is called with an QObject formerly known as a QAction // during _k_actionDestroyed(). So don't do fancy stuff here that needs a // real QAction! // Get the index for the action int index = actions.indexOf(action); // Action not found. if (index == -1) { return nullptr; } // An action collection can't have the same action twice. Q_ASSERT(actions.indexOf(action, index + 1) == -1); // Get the actions name const QString name = action->objectName(); // Remove the action actionByName.remove(name); actions.removeAt(index); return action; } #include "moc_kirigamiactioncollection.cpp"