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 }