File indexing completed on 2024-05-05 17:43:52

0001 /*
0002     SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-or-later
0005 */
0006 
0007 #include "actions.h"
0008 
0009 #include "debug.h"
0010 
0011 #include <QDBusConnection>
0012 #include <QDBusMessage>
0013 #include <QDBusPendingCallWatcher>
0014 #include <QDBusPendingReply>
0015 #include <QDebug>
0016 #include <QVariantList>
0017 
0018 static const QString s_orgGtkActions = QStringLiteral("org.gtk.Actions");
0019 
0020 Actions::Actions(const QString &serviceName, const QString &objectPath, QObject *parent)
0021     : QObject(parent)
0022     , m_serviceName(serviceName)
0023     , m_objectPath(objectPath)
0024 {
0025     Q_ASSERT(!serviceName.isEmpty());
0026     Q_ASSERT(!m_objectPath.isEmpty());
0027 
0028     if (!QDBusConnection::sessionBus().connect(serviceName,
0029                                                objectPath,
0030                                                s_orgGtkActions,
0031                                                QStringLiteral("Changed"),
0032                                                this,
0033                                                SLOT(onActionsChanged(QStringList, StringBoolMap, QVariantMap, GMenuActionMap)))) {
0034         qCWarning(DBUSMENUPROXY) << "Failed to subscribe to action changes for" << parent << "on" << serviceName << "at" << objectPath;
0035     }
0036 }
0037 
0038 Actions::~Actions() = default;
0039 
0040 void Actions::load()
0041 {
0042     QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, m_objectPath, s_orgGtkActions, QStringLiteral("DescribeAll"));
0043 
0044     QDBusPendingReply<GMenuActionMap> reply = QDBusConnection::sessionBus().asyncCall(msg);
0045     QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
0046     connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
0047         QDBusPendingReply<GMenuActionMap> reply = *watcher;
0048         if (reply.isError()) {
0049             qCWarning(DBUSMENUPROXY) << "Failed to get actions from" << m_serviceName << "at" << m_objectPath << reply.error();
0050             Q_EMIT failedToLoad();
0051         } else {
0052             m_actions = reply.value();
0053             Q_EMIT loaded();
0054         }
0055         watcher->deleteLater();
0056     });
0057 }
0058 
0059 bool Actions::get(const QString &name, GMenuAction &action) const
0060 {
0061     auto it = m_actions.find(name);
0062     if (it == m_actions.constEnd()) {
0063         return false;
0064     }
0065 
0066     action = *it;
0067     return true;
0068 }
0069 
0070 GMenuActionMap Actions::getAll() const
0071 {
0072     return m_actions;
0073 }
0074 
0075 void Actions::trigger(const QString &name, const QVariant &target, uint timestamp)
0076 {
0077     if (!m_actions.contains(name)) {
0078         qCWarning(DBUSMENUPROXY) << "Cannot invoke action" << name << "which doesn't exist";
0079         return;
0080     }
0081 
0082     QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, m_objectPath, s_orgGtkActions, QStringLiteral("Activate"));
0083     msg << name;
0084 
0085     QVariantList args;
0086     if (target.isValid()) {
0087         args << target;
0088     }
0089     msg << QVariant::fromValue(args);
0090 
0091     QVariantMap platformData;
0092 
0093     if (timestamp) {
0094         // From documentation:
0095         // If the startup notification id is not available, this can be just "_TIMEtime", where
0096         // time is the time stamp from the event triggering the call.
0097         // see also gtkwindow.c extract_time_from_startup_id and startup_id_is_fake
0098         platformData.insert(QStringLiteral("desktop-startup-id"), QStringLiteral("_TIME") + QString::number(timestamp));
0099     }
0100 
0101     msg << platformData;
0102 
0103     QDBusPendingReply<void> reply = QDBusConnection::sessionBus().asyncCall(msg);
0104     QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
0105     connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, name](QDBusPendingCallWatcher *watcher) {
0106         QDBusPendingReply<void> reply = *watcher;
0107         if (reply.isError()) {
0108             qCWarning(DBUSMENUPROXY) << "Failed to invoke action" << name << "on" << m_serviceName << "at" << m_objectPath << reply.error();
0109         }
0110         watcher->deleteLater();
0111     });
0112 }
0113 
0114 bool Actions::isValid() const
0115 {
0116     return !m_actions.isEmpty();
0117 }
0118 
0119 void Actions::onActionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added)
0120 {
0121     // Collect the actions that we removed, altered, or added, so we can eventually signal changes for all menus that contain one of those actions
0122     QStringList dirtyActions;
0123 
0124     // TODO I bet for most of the loops below we could use a nice short std algorithm
0125 
0126     for (const QString &removedAction : removed) {
0127         if (m_actions.remove(removedAction)) {
0128             dirtyActions.append(removedAction);
0129         }
0130     }
0131 
0132     for (auto it = enabledChanges.constBegin(), end = enabledChanges.constEnd(); it != end; ++it) {
0133         const QString &actionName = it.key();
0134         const bool enabled = it.value();
0135 
0136         auto actionIt = m_actions.find(actionName);
0137         if (actionIt == m_actions.end()) {
0138             qCInfo(DBUSMENUPROXY) << "Got enabled changed for action" << actionName << "which we don't know";
0139             continue;
0140         }
0141 
0142         GMenuAction &action = *actionIt;
0143         if (action.enabled != enabled) {
0144             action.enabled = enabled;
0145             dirtyActions.append(actionName);
0146         } else {
0147             qCInfo(DBUSMENUPROXY) << "Got enabled change for action" << actionName << "which didn't change it";
0148         }
0149     }
0150 
0151     for (auto it = stateChanges.constBegin(), end = stateChanges.constEnd(); it != end; ++it) {
0152         const QString &actionName = it.key();
0153         const QVariant &state = it.value();
0154 
0155         auto actionIt = m_actions.find(actionName);
0156         if (actionIt == m_actions.end()) {
0157             qCInfo(DBUSMENUPROXY) << "Got state changed for action" << actionName << "which we don't know";
0158             continue;
0159         }
0160 
0161         GMenuAction &action = *actionIt;
0162 
0163         if (action.state.isEmpty()) {
0164             qCDebug(DBUSMENUPROXY) << "Got new state for action" << actionName << "that didn't have any state before";
0165             action.state.append(state);
0166             dirtyActions.append(actionName);
0167         } else {
0168             // Action state is a list but the state change only sends us a single variant, so just overwrite the first one
0169             QVariant &firstState = action.state.first();
0170             if (firstState != state) {
0171                 firstState = state;
0172                 dirtyActions.append(actionName);
0173             } else {
0174                 qCInfo(DBUSMENUPROXY) << "Got state change for action" << actionName << "which didn't change it";
0175             }
0176         }
0177     }
0178 
0179     // unite() will result in keys being present multiple times, do it manually and overwrite existing ones
0180     for (auto it = added.constBegin(), end = added.constEnd(); it != end; ++it) {
0181         const QString &actionName = it.key();
0182 
0183         if (DBUSMENUPROXY().isInfoEnabled()) {
0184             if (m_actions.contains(actionName)) {
0185                 qCInfo(DBUSMENUPROXY) << "Got new action" << actionName << "that we already have, overwriting existing one";
0186             }
0187         }
0188 
0189         m_actions.insert(actionName, it.value());
0190 
0191         dirtyActions.append(actionName);
0192     }
0193 
0194     if (!dirtyActions.isEmpty()) {
0195         Q_EMIT actionsChanged(dirtyActions);
0196     }
0197 }