[plasma/kglobalacceld] /: Implement setInverseShortcutActions D-Bus method and config storage

Jakob Petsovits <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 017dcc7c0c9f23f30c1113e87663d9a808b86a95 by Jakob Petsovits.
Committed on 07/08/2026 at 20:11.
Pushed by jpetso into branch 'master'.

Implement setInverseShortcutActions D-Bus method and config storage

Depends on new API introduced in KGlobalAccel 6.27.

In terms of storage, this persists the action pairs in a sub-group
of the component called "InverseActions", e.g. [kwin][$InverseAction]
for the "kwin" component. We do not currently store which of the
actions is "forward" and which is "backward", if we need this we
can add it at a later time.

M  +1    -1    CMakeLists.txt
M  +45   -0    src/component.cpp
M  +10   -0    src/component.h
M  +10   -0    src/globalshortcut.cpp
M  +9    -1    src/globalshortcut.h
M  +4    -0    src/globalshortcutsregistry.cpp
M  +34   -0    src/kglobalacceld.cpp
M  +4    -0    src/kglobalacceld.h
M  +8    -0    src/kserviceactioncomponent.cpp

https://invent.kde.org/plasma/kglobalacceld/-/commit/017dcc7c0c9f23f30c1113e87663d9a808b86a95

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 22f1d48..c686650 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -5,7 +5,7 @@ project(kglobalacceld)
 set(PROJECT_VERSION "6.7.80")
 
 set(QT_MIN_VERSION "6.10.0")
-set(KF6_MIN_VERSION "6.26.0")
+set(KF6_MIN_VERSION "6.27.0")
 
 set(CMAKE_CXX_STANDARD 20)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
diff --git a/src/component.cpp b/src/component.cpp
index 090295f..f10b83b 100644
--- a/src/component.cpp
+++ b/src/component.cpp
@@ -14,6 +14,8 @@
 #include <QKeySequence>
 #include <QStringList>
 
+using namespace Qt::StringLiterals;
+
 QSet<QKeySequence> Component::keysFromString(const QString &str)
 {
     QSet<QKeySequence> ret;
@@ -272,6 +274,30 @@ GlobalShortcut *Component::registerShortcut(const QString &uniqueName,
     return shortcut;
 }
 
+void Component::loadInverseAction(const QString &aUniqueName, const QStringList &configEntry)
+{
+    if (configEntry.size() != 1) { // possibility of later expansion with optional list entries
+        qCWarning(KGLOBALACCELD) << "Inverse action invalid format, ignoring:" << aUniqueName << configEntry;
+        return;
+    }
+    const QString &bUniqueName = configEntry.last();
+    GlobalShortcut *a = currentContext()->_actionsMap.value(aUniqueName);
+    GlobalShortcut *b = currentContext()->_actionsMap.value(bUniqueName);
+    if (!a || !b) {
+        qCWarning(KGLOBALACCELD) << "Inverse action configured but one or both shortcuts not found, ignoring:" << //
+            aUniqueName << static_cast<bool>(a) << "|" << bUniqueName << static_cast<bool>(b);
+        return;
+    }
+    if ((!a->inverseActionUniqueName().isEmpty() && a->inverseActionUniqueName() != bUniqueName)
+        || (!b->inverseActionUniqueName().isEmpty() && b->inverseActionUniqueName() != aUniqueName)) {
+        qCWarning(KGLOBALACCELD) << "Inverse action configured for actions with a pre-existing assignment:" << //
+            aUniqueName << a->inverseActionUniqueName() << "|" << bUniqueName << b->inverseActionUniqueName();
+        return;
+    }
+    a->setInverseActionUniqueName(bUniqueName);
+    b->setInverseActionUniqueName(aUniqueName);
+}
+
 void Component::loadSettings(const KConfigGroup &configGroup, const KConfigGroup &stateGroup)
 {
     // GlobalShortcutsRegistry::loadSettings handles contexts.
@@ -286,6 +312,13 @@ void Component::loadSettings(const KConfigGroup &configGroup, const KConfigGroup
 
         registerShortcut(confKey, entry[2], entry[0], entry[1], serial);
     }
+
+    const KConfigGroup inverseActionGroup(&configGroup, "$InverseAction"_L1);
+
+    const auto inverseActionKeys = inverseActionGroup.keyList();
+    for (const QString &confKey : inverseActionKeys) {
+        loadInverseAction(confKey, inverseActionGroup.readEntry(confKey, QStringList()));
+    }
 }
 
 void Component::setFriendlyName(const QString &name)
@@ -345,6 +378,8 @@ void Component::writeSettings(KConfigGroup &configGroup, KConfigGroup &stateGrou
             contextGroup.writeEntry("_k_friendly_name", context->friendlyName());
         }
 
+        KConfigGroup inverseActionGroup(&contextGroup, "$InverseAction"_L1);
+
         // qCDebug(KGLOBALACCELD) << "writing group " << _uniqueName << ":" << context->uniqueName();
 
         for (const GlobalShortcut *shortcut : std::as_const(context->_actionsMap)) {
@@ -363,8 +398,18 @@ void Component::writeSettings(KConfigGroup &configGroup, KConfigGroup &stateGrou
 
             contextGroup.writeEntry(shortcut->uniqueName(), entry);
             stateGroup.writeEntry(shortcut->uniqueName(), shortcut->serial());
+
+            if (!shortcut->inverseActionUniqueName().isEmpty() && !inverseActionGroup.hasKey(shortcut->inverseActionUniqueName())) {
+                inverseActionGroup.writeEntry(shortcut->uniqueName(), QStringList{shortcut->inverseActionUniqueName()});
+            }
         }
     }
 }
 
+// static
+bool Component::isReservedConfigGroupName(const QString &name)
+{
+    return name.startsWith("$"_L1); // "$InverseAction", "$Trigger", etc.
+}
+
 #include "moc_component.cpp"
diff --git a/src/component.h b/src/component.h
index bcb67b4..498d946 100644
--- a/src/component.h
+++ b/src/component.h
@@ -99,6 +99,9 @@ public:
 
     virtual void writeSettings(KConfigGroup &config, KConfigGroup &state) const;
 
+    //! Returns whether the given sub-group name is reserved for component info as opposed to specific contexts
+    static bool isReservedConfigGroupName(const QString &name);
+
 protected:
     friend class ::GlobalShortcutsRegistry;
     friend class ::ShortcutsTest;
@@ -122,6 +125,13 @@ protected:
                                      const QString &defaultShortcutString,
                                      uint64_t serial = 0);
 
+    /**
+     * Assign two already registered actions to each other as inverse actions.
+     * @param aUniqueName internal unique name of a given action
+     * @param configEntry internal unique name of its inverse action, plus optional extra flags
+     */
+    void loadInverseAction(const QString &aUniqueName, const QStringList &configEntry);
+
     static QString stringFromKeys(const QSet<QKeySequence> &keys);
     static QSet<QKeySequence> keysFromString(const QString &str);
 
diff --git a/src/globalshortcut.cpp b/src/globalshortcut.cpp
index 0c3fed3..8a5f035 100644
--- a/src/globalshortcut.cpp
+++ b/src/globalshortcut.cpp
@@ -165,6 +165,16 @@ void GlobalShortcut::setDefaultKeys(const QSet<QKeySequence> &newKeys)
     _defaultKeys = newKeys;
 }
 
+QString GlobalShortcut::inverseActionUniqueName() const
+{
+    return _inverseActionUniqueName;
+}
+
+void GlobalShortcut::setInverseActionUniqueName(const QString &uniqueName)
+{
+    _inverseActionUniqueName = uniqueName;
+}
+
 void GlobalShortcut::setActive()
 {
     if (!_isPresent || _isRegistered) {
diff --git a/src/globalshortcut.h b/src/globalshortcut.h
index aa8de10..334d5e6 100644
--- a/src/globalshortcut.h
+++ b/src/globalshortcut.h
@@ -40,7 +40,10 @@ public:
     //! Return the friendly display name for this shortcut.
     QString friendlyName() const;
 
-    //! Check if the shortcut is active. It's keys are grabbed
+    //! Returns the unique name of the associated inverse action, or empty if not paired.
+    QString inverseActionUniqueName() const;
+
+    //! Check if the shortcut is active. Its keys are grabbed
     bool isActive() const;
 
     //! Check if the shortcut is fresh/new. Is an internal state
@@ -64,6 +67,9 @@ public:
     //! Sets the friendly name for the shortcut. For display.
     void setFriendlyName(const QString &);
 
+    //! Sets the inverse action for this shortcut.
+    void setInverseActionUniqueName(const QString &uniqueName);
+
     //! Sets the shortcut inactive. No longer grabs the keys.
     void setInactive();
 
@@ -105,6 +111,8 @@ private:
     QSet<QKeySequence> _keys;
     QSet<QKeySequence> _defaultKeys;
     uint64_t _serial;
+
+    QString _inverseActionUniqueName;
 };
 
 #endif /* #ifndef GLOBALSHORTCUT_H */
diff --git a/src/globalshortcutsregistry.cpp b/src/globalshortcutsregistry.cpp
index d507e20..90e186b 100644
--- a/src/globalshortcutsregistry.cpp
+++ b/src/globalshortcutsregistry.cpp
@@ -676,6 +676,10 @@ void GlobalShortcutsRegistry::loadSettings()
             if (context == QLatin1String("Friendly Name")) {
                 continue;
             }
+            // Skip any other sub-groups that are used to store extended action info
+            if (Component::isReservedConfigGroupName(context)) {
+                continue;
+            }
 
             const KConfigGroup contextConfigGroup(&configGroup, context);
             const KConfigGroup contextStateGroup(&stateGroup, context);
diff --git a/src/kglobalacceld.cpp b/src/kglobalacceld.cpp
index f20c0c3..6257940 100644
--- a/src/kglobalacceld.cpp
+++ b/src/kglobalacceld.cpp
@@ -568,6 +568,40 @@ void KGlobalAccelD::setForeignShortcutKeys(const QStringList &actionId, const QS
     Q_EMIT yourShortcutsChanged(actionId, newKeys);
 }
 
+bool KGlobalAccelD::setInverseShortcutActions(const QString &componentUnique,
+                                              const QString &forwardActionUnique,
+                                              const QString &backwardActionUnique,
+                                              uint inverseSetterFlags)
+{
+    if (inverseSetterFlags != 0) { // reserved, may change pair assignment behaviors
+        return false;
+    }
+
+    GlobalShortcut *forwardShortcut = d->findAction(componentUnique, forwardActionUnique);
+    GlobalShortcut *backwardShortcut = d->findAction(componentUnique, backwardActionUnique);
+    if (!forwardShortcut || !backwardShortcut) {
+        qCWarning(KGLOBALACCELD) << "Inverse actions" << componentUnique << forwardActionUnique << backwardActionUnique
+                                 << "not assigned: one or both shortcuts were not registered when calling setInverseShortcutActions()";
+        return false;
+    }
+    const QString existingInverseOfForward = forwardShortcut->inverseActionUniqueName();
+    const QString existingInverseOfBackward = backwardShortcut->inverseActionUniqueName();
+
+    if (existingInverseOfForward == backwardActionUnique && existingInverseOfBackward == forwardActionUnique) {
+        // no changes, everything is as it was already loaded beforehand
+        return true;
+    } else if (!existingInverseOfForward.isEmpty() || !existingInverseOfBackward.isEmpty()) {
+        qCWarning(KGLOBALACCELD) << "Inverse actions" << componentUnique << forwardActionUnique << backwardActionUnique
+                                 << "not assigned: existing inverse actions exist and differ from requested action pair";
+        return false;
+    }
+
+    forwardShortcut->setInverseActionUniqueName(backwardActionUnique);
+    backwardShortcut->setInverseActionUniqueName(forwardActionUnique);
+    scheduleWriteSettings();
+    return true;
+}
+
 void KGlobalAccelD::scheduleWriteSettings() const
 {
     if (!d->writeoutTimer.isActive()) {
diff --git a/src/kglobalacceld.h b/src/kglobalacceld.h
index 6d3560c..9ca45b2 100644
--- a/src/kglobalacceld.h
+++ b/src/kglobalacceld.h
@@ -115,6 +115,10 @@ public Q_SLOTS:
 #endif
     Q_SCRIPTABLE void setForeignShortcutKeys(const QStringList &actionId, const QSet<QKeySequence> &keys);
 
+    //! \since 6.8
+    Q_SCRIPTABLE bool
+    setInverseShortcutActions(const QString &componentUnique, const QString &forwardActionUnique, const QString &backwardActionUnique, uint couplingFlags);
+
     // to be called when a KAction is destroyed. The shortcut stays in the data structures for
     // conflict resolution but won't trigger.
     Q_SCRIPTABLE void setInactive(const QStringList &actionId);
diff --git a/src/kserviceactioncomponent.cpp b/src/kserviceactioncomponent.cpp
index 1238602..05d3a05 100644
--- a/src/kserviceactioncomponent.cpp
+++ b/src/kserviceactioncomponent.cpp
@@ -140,6 +140,14 @@ void KServiceActionComponent::loadSettings(const KConfigGroup &configGroup, cons
         shortcut->setIsPresent(true);
     }
 
+    // Inverse actions - load only after all actions are registered, so they can be properly paired
+    for (const KServiceAction &action : actions) {
+        const QStringList entry = action.property<QStringList>(QStringLiteral("X-KDE-InverseAction"));
+        if (!entry.isEmpty()) {
+            loadInverseAction(action.name(), entry);
+        }
+    }
+
     const QString type = m_service->property<QString>(QStringLiteral("X-KDE-GlobalShortcutType"));
 
     // Type can be Application or Service
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.