diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 526e8cbb0..0ecea33b5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -82,6 +82,7 @@ set(SOURCES core/globalshortcutbackend.cpp core/globalshortcuts.cpp core/gnomeglobalshortcutbackend.cpp + core/kglobalaccelglobalshortcutbackend.cpp core/mergedproxymodel.cpp core/metatypes.cpp core/multisortfilterproxy.cpp @@ -413,6 +414,7 @@ set(HEADERS core/globalshortcuts.h core/globalshortcutbackend.h core/gnomeglobalshortcutbackend.h + core/kglobalaccelglobalshortcutbackend.h core/mergedproxymodel.h core/mimedata.h core/network.h @@ -937,6 +939,15 @@ if(UNIX AND HAVE_DBUS) dbus/org.gnome.SettingsDaemon.MediaKeys.xml dbus/gnomesettingsdaemon) + # org.kde.KGlobalAccel interfaces + # these are taken from the KGlobalAccel sources (LGPL 2.1) + qt5_add_dbus_interface(SOURCES + dbus/org.kde.KGlobalAccel.xml + dbus/kglobalaccel) + qt5_add_dbus_interface(SOURCES + dbus/org.kde.kglobalaccel.Component.xml + dbus/kglobalaccelcomponent) + # org.freedesktop.Avahi.Server interface add_custom_command( OUTPUT diff --git a/src/core/globalshortcuts.cpp b/src/core/globalshortcuts.cpp index 025327884..b1c87c5f8 100644 --- a/src/core/globalshortcuts.cpp +++ b/src/core/globalshortcuts.cpp @@ -22,6 +22,7 @@ #include "config.h" #include "globalshortcuts.h" #include "gnomeglobalshortcutbackend.h" +#include "kglobalaccelglobalshortcutbackend.h" #include "macglobalshortcutbackend.h" #include "qxtglobalshortcutbackend.h" @@ -41,7 +42,9 @@ GlobalShortcuts::GlobalShortcuts(QWidget* parent) : QWidget(parent), gnome_backend_(nullptr), system_backend_(nullptr), - use_gnome_(false) { + use_gnome_(false), + have_kglobalaccel_( + KGlobalAccelShortcutBackend::IsKGlobalAccelAvailable()) { settings_.beginGroup(kSettingsGroup); // Create actions @@ -91,6 +94,7 @@ GlobalShortcuts::GlobalShortcuts(QWidget* parent) // Create backends - these do the actual shortcut registration gnome_backend_ = new GnomeGlobalShortcutBackend(this); + kglobalaccel_backend_ = new KGlobalAccelShortcutBackend(this); #ifndef Q_OS_DARWIN system_backend_ = new QxtGlobalShortcutBackend(this); @@ -158,10 +162,12 @@ void GlobalShortcuts::ReloadSettings() { void GlobalShortcuts::Unregister() { if (gnome_backend_->is_active()) gnome_backend_->Unregister(); + if (kglobalaccel_backend_->is_active()) kglobalaccel_backend_->Unregister(); if (system_backend_->is_active()) system_backend_->Unregister(); } void GlobalShortcuts::Register() { + if (have_kglobalaccel_ && kglobalaccel_backend_->Register()) return; if (use_gnome_ && gnome_backend_->Register()) return; system_backend_->Register(); } diff --git a/src/core/globalshortcuts.h b/src/core/globalshortcuts.h index ad97aadbd..c811f71e6 100644 --- a/src/core/globalshortcuts.h +++ b/src/core/globalshortcuts.h @@ -93,12 +93,14 @@ signals: private: GlobalShortcutBackend* gnome_backend_; + GlobalShortcutBackend* kglobalaccel_backend_; GlobalShortcutBackend* system_backend_; QMap shortcuts_; QSettings settings_; bool use_gnome_; + bool have_kglobalaccel_; }; #endif // CORE_GLOBALSHORTCUTS_H_ diff --git a/src/core/kglobalaccelglobalshortcutbackend.cpp b/src/core/kglobalaccelglobalshortcutbackend.cpp new file mode 100644 index 000000000..fcb40d665 --- /dev/null +++ b/src/core/kglobalaccelglobalshortcutbackend.cpp @@ -0,0 +1,268 @@ +#include "core/logging.h" +#include "kglobalaccelglobalshortcutbackend.h" + +#include +#include + +#ifdef HAVE_DBUS + +#include +#include + +#endif + +// Most of this file is based on the KGlobalAccel sources +// (https://phabricator.kde.org/source/kglobalaccel) + +namespace { +#ifdef HAVE_DBUS +QString ComponentDisplayName() { + return QGuiApplication::applicationDisplayName().isEmpty() + ? QCoreApplication::applicationName() + : QGuiApplication::applicationDisplayName(); +} + +QString ComponentUniqueName() { return QCoreApplication::applicationName(); } + +const QString& IdActionUniqueName(const QStringList& id) { return id.at(1); } + +bool IsCorrectMediaKeyShortcut(const GlobalShortcuts::Shortcut& shortcut) { + if (shortcut.id == QStringLiteral("play_pause")) { + return shortcut.action->shortcut() == QKeySequence(Qt::Key_MediaPlay); + } else if (shortcut.id == QStringLiteral("stop")) { + return shortcut.action->shortcut() == QKeySequence(Qt::Key_MediaStop); + } else if (shortcut.id == QStringLiteral("next_track")) { + return shortcut.action->shortcut() == QKeySequence(Qt::Key_MediaNext); + } else if (shortcut.id == QStringLiteral("prev_track")) { + return shortcut.action->shortcut() == QKeySequence(Qt::Key_MediaPrevious); + } else { + return false; + } +} +#endif // HAVE_DBUS +} // namespace + +#ifdef HAVE_DBUS + +KGlobalAccelShortcutBackend::KGlobalAccelShortcutBackend( + GlobalShortcuts* parent) + : GlobalShortcutBackend(parent), iface_(nullptr), component_(nullptr) {} + +#else // HAVE_DBUS +KGlobalAccelShortcutBackend::KGlobalAccelShortcutBackend( + GlobalShortcuts* parent) + : GlobalShortcutBackend(parent) {} +#endif // HAVE_DBUS + +bool KGlobalAccelShortcutBackend::IsKGlobalAccelAvailable() { +#ifdef HAVE_DBUS + return QDBusConnection::sessionBus().interface()->isServiceRegistered( + kService); +#else // HAVE_DBUS + return false; +#endif // HAVE_DBUS +} + +bool KGlobalAccelShortcutBackend::DoRegister() { +#ifdef HAVE_DBUS + qLog(Debug) << "Registering shortcuts"; + + if (!AcquireInterface()) return false; + + bool complete = true; + for (const GlobalShortcuts::Shortcut& shortcut : + manager_->shortcuts().values()) { + if (shortcut.action->shortcut().isEmpty()) continue; + + if (!RegisterShortcut(shortcut)) complete = false; + } + + if (!AcquireComponent()) return false; + + QObject::connect(component_, + &OrgKdeKglobalaccelComponentInterface::globalShortcutPressed, + this, &KGlobalAccelShortcutBackend::OnShortcutPressed, + Qt::UniqueConnection); + + return complete; +#else // HAVE_DBUS + qLog(Warning) << "dbus not available"; + return false; +#endif // HAVE_DBUS +} + +void KGlobalAccelShortcutBackend::DoUnregister() { +#ifdef HAVE_DBUS + if (!AcquireInterface()) return; + + if (!AcquireComponent()) return; + + for (const GlobalShortcuts::Shortcut& shortcut : manager_->shortcuts()) + UnregisterAction(shortcut.id, shortcut.action); + + QObject::disconnect( + component_, &OrgKdeKglobalaccelComponentInterface::globalShortcutPressed, + this, &KGlobalAccelShortcutBackend::OnShortcutPressed); +#endif // HAVE_DBUS +} + +#ifdef HAVE_DBUS + +const char* KGlobalAccelShortcutBackend::kService = "org.kde.kglobalaccel"; +const char* KGlobalAccelShortcutBackend::kPath = "/kglobalaccel"; + +bool KGlobalAccelShortcutBackend::AcquireComponent() { + Q_ASSERT(iface_ && iface_->isValid()); + + if (component_) return true; + + QDBusReply reply = + iface_->getComponent(ComponentUniqueName()); + if (!reply.isValid()) { + if (reply.error().name() == + QLatin1String("org.kde.kglobalaccel.NoSuchComponent")) + return false; + + qLog(Warning) << "Failed to get DBus path for KGlobalAccel component"; + return false; + } + + component_ = new org::kde::kglobalaccel::Component( + kService, reply.value().path(), QDBusConnection::sessionBus(), iface_); + + if (!component_->isValid()) { + qLog(Warning) << "Failed to get KGlobalAccel component:" + << QDBusConnection::sessionBus().lastError(); + component_->deleteLater(); + component_ = nullptr; + return false; + } + + return true; +} + +bool KGlobalAccelShortcutBackend::AcquireInterface() { + if (iface_ && iface_->isValid()) return true; + + if (IsKGlobalAccelAvailable()) { + iface_ = new OrgKdeKGlobalAccelInterface( + kService, kPath, QDBusConnection::sessionBus(), this); + } + + if (iface_ && iface_->isValid()) return true; + + if (!iface_) + qLog(Warning) << "KGlobalAccel daemon not registered"; + else if (!iface_->isValid()) + qLog(Warning) << "KGlobalAccel daemon is not valid"; + return false; +} + +QStringList KGlobalAccelShortcutBackend::GetId(const QString& name, + const QAction* action) { + Q_ASSERT(action); + + QStringList ret; + ret << ComponentUniqueName(); + ret << name; + ret << ComponentDisplayName(); + ret << action->text().replace(QLatin1Char('&'), QStringLiteral("")); + if (ret.back().isEmpty()) ret.back() = name; + return ret; +} + +QList KGlobalAccelShortcutBackend::ToIntList( + const QList& seq) { + QList ret; + for (const QKeySequence& sequence : seq) { + ret.append(sequence[0]); + } + while (!ret.isEmpty() && ret.last() == 0) { + ret.removeLast(); + } + return ret; +} + +bool KGlobalAccelShortcutBackend::RegisterAction(const QString& name, + QAction* action) { + Q_ASSERT(action); + + if (name.isEmpty() && + (action->objectName().isEmpty() || + action->objectName().startsWith(QLatin1String("unnamed-")))) { + qLog(Warning) << "Cannot register shortcut for unnamed action"; + return false; + } + + QStringList action_id = GetId(name, action); + name_to_action_.insertMulti(IdActionUniqueName(action_id), action); + iface_->doRegister(action_id); + + return true; +} + +bool KGlobalAccelShortcutBackend::RegisterShortcut( + const GlobalShortcuts::Shortcut& shortcut) { + if (!RegisterAction(shortcut.id, shortcut.action)) return false; + + QList active_shortcut; + active_shortcut << shortcut.action->shortcut(); + + QStringList action_id = GetId(shortcut.id, shortcut.action); + const QList result = iface_->setShortcut( + action_id, ToIntList(active_shortcut), SetShortcutFlag::SetPresent); + + const QList result_sequence = ToKeySequenceList(result); + if (result_sequence != active_shortcut) { + qLog(Warning) << "Tried setting global shortcut" << active_shortcut + << "but KGlobalAccel returned" << result_sequence; + + if (!result_sequence.isEmpty()) { + if (!IsCorrectMediaKeyShortcut(shortcut)) { + // there is some conflict with our preferred shortcut so we use + // the new shortcut that kglobalaccel suggests + shortcut.action->setShortcut(result_sequence[0]); + } else { + // media keys are properly handled by plasma through the + // media player plasmoid so we don't do anything in those cases + qLog(Debug) << "Leaving media key shortcuts unchanged"; + } + } + } + + return true; +} + +QList KGlobalAccelShortcutBackend::ToKeySequenceList( + const QList& seq) { + QList ret; + for (int i : seq) { + ret.append(i); + } + return ret; +} + +void KGlobalAccelShortcutBackend::UnregisterAction(const QString& name, + QAction* action) { + Q_ASSERT(action); + + QStringList action_id = GetId(name, action); + name_to_action_.remove(IdActionUniqueName(action_id), action); + iface_->unRegister(action_id); +} + +void KGlobalAccelShortcutBackend::OnShortcutPressed( + const QString& component_unique, const QString& action_unique, + qlonglong timestamp) const { + QAction* action = nullptr; + const QList candidates = name_to_action_.values(action_unique); + for (QAction* a : candidates) { + if (ComponentUniqueName() == component_unique) { + action = a; + } + } + + if (action && action->isEnabled()) action->trigger(); +} + +#endif // HAVE_DBUS diff --git a/src/core/kglobalaccelglobalshortcutbackend.h b/src/core/kglobalaccelglobalshortcutbackend.h new file mode 100644 index 000000000..8f687df84 --- /dev/null +++ b/src/core/kglobalaccelglobalshortcutbackend.h @@ -0,0 +1,66 @@ +#ifndef CORE_KGLOBALACCELGLOBALSHORTCUTBACKEND_H_ +#define CORE_KGLOBALACCELGLOBALSHORTCUTBACKEND_H_ + +#include "config.h" +#include "globalshortcutbackend.h" +#include "globalshortcuts.h" + +#include +#include + +class QAction; + +class OrgKdeKGlobalAccelInterface; + +class OrgKdeKglobalaccelComponentInterface; + +class KGlobalAccelShortcutBackend : public GlobalShortcutBackend { + Q_OBJECT + + public: + explicit KGlobalAccelShortcutBackend(GlobalShortcuts* parent); + + static bool IsKGlobalAccelAvailable(); + + protected: + bool DoRegister() override; + + void DoUnregister() override; + + private: +#ifdef HAVE_DBUS + enum SetShortcutFlag { SetPresent = 2, NoAutoloading = 4, IsDefault = 8 }; + + bool AcquireComponent(); + + bool AcquireInterface(); + + static QStringList GetId(const QString& name, const QAction* action); + + static QList ToIntList(const QList& seq); + + bool RegisterAction(const QString& name, QAction* action); + + bool RegisterShortcut(const GlobalShortcuts::Shortcut& shortcut); + + static QList ToKeySequenceList(const QList& seq); + + void UnregisterAction(const QString& name, QAction* action); + + private slots: + + void OnShortcutPressed(const QString& component_unique, + const QString& action_unique, + qlonglong timestamp) const; + + private: + static const char* kService; + static const char* kPath; + + OrgKdeKGlobalAccelInterface* iface_; + OrgKdeKglobalaccelComponentInterface* component_; + QMultiHash name_to_action_; +#endif // HAVE_DBUS +}; + +#endif // CORE_KGLOBALACCELGLOBALSHORTCUTBACKEND_H_ diff --git a/src/dbus/org.kde.KGlobalAccel.xml b/src/dbus/org.kde.KGlobalAccel.xml new file mode 100644 index 000000000..84cb0fbba --- /dev/null +++ b/src/dbus/org.kde.KGlobalAccel.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.kde.kglobalaccel.Component.xml b/src/dbus/org.kde.kglobalaccel.Component.xml new file mode 100644 index 000000000..aca7ef6ea --- /dev/null +++ b/src/dbus/org.kde.kglobalaccel.Component.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +