// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2026 A-La-Karte Contributors #include "inputserviceclient.h" #include #include #include #include #include #include #include #include static const QString kInputService = QStringLiteral("org.kde.ALaKarte.Input1"); static const QString kInputPath = QStringLiteral("/org/kde/ALaKarte/Input1"); static const QString kInputInterface = QStringLiteral("org.kde.ALaKarte.Input1"); namespace { static QVariant unwrapDbusVariant(QVariant v) { if (v.canConvert()) { v = v.value().variant(); } return v; } static QVariantMap unwrapVariantMap(QVariant v) { v = unwrapDbusVariant(v); if (v.canConvert()) { return v.toMap(); } return {}; } static QVariantMap normalizeMap(QVariantMap m) { for (auto it = m.begin(); it != m.end(); ++it) { it.value() = unwrapDbusVariant(it.value()); } return m; } } InputServiceClient::InputServiceClient(QObject *parent) : QObject(parent) { QDBusConnection bus = QDBusConnection::sessionBus(); if (bus.isConnected()) { m_watcher = new QDBusServiceWatcher(kInputService, bus, QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, this); connect(m_watcher, &QDBusServiceWatcher::serviceRegistered, this, [this]() { setAvailable(true); connectSignals(); refresh(); }); connect(m_watcher, &QDBusServiceWatcher::serviceUnregistered, this, [this]() { setAvailable(false); m_controllers.clear(); m_profiles.clear(); Q_EMIT controllersChanged(); Q_EMIT profilesChanged(); }); } if (QCoreApplication::instance()) { connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() { m_connectedSignals = false; }); } ensureService(); refresh(); } InputServiceClient::~InputServiceClient() = default; bool InputServiceClient::available() const { return m_available; } QString InputServiceClient::lastError() const { return m_lastError; } QVariantList InputServiceClient::controllers() const { return m_controllers; } QVariantList InputServiceClient::profiles() const { return m_profiles; } void InputServiceClient::setAvailable(bool available) { if (m_available == available) { return; } m_available = available; Q_EMIT availableChanged(); } void InputServiceClient::setLastError(const QString &error) { if (m_lastError == error) { return; } m_lastError = error; Q_EMIT lastErrorChanged(); } void InputServiceClient::ensureService() { QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected() || !bus.interface()) { setAvailable(false); setLastError(QStringLiteral("session bus not available")); return; } if (bus.interface()->isServiceRegistered(kInputService)) { setAvailable(true); connectSignals(); return; } bool activatable = false; { const QDBusReply names = bus.interface()->activatableServiceNames(); if (names.isValid()) { activatable = names.value().contains(kInputService); } } const QDBusReply reply = bus.interface()->startService(kInputService); if (reply.isValid() || bus.interface()->isServiceRegistered(kInputService)) { setAvailable(true); connectSignals(); return; } setAvailable(false); if (activatable) { setLastError(reply.error().message()); } else { setLastError(QStringLiteral("Input service is not available (missing DBus activation for org.kde.ALaKarte.Input1)")); } } void InputServiceClient::connectSignals() { if (m_connectedSignals) { return; } QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected()) { return; } const bool ok1 = bus.connect(kInputService, kInputPath, kInputInterface, QStringLiteral("ControllerAdded"), this, SLOT(onControllerAdded(QVariantMap))); const bool ok2 = bus.connect(kInputService, kInputPath, kInputInterface, QStringLiteral("ControllerChanged"), this, SLOT(onControllerChanged(QVariantMap))); const bool ok3 = bus.connect(kInputService, kInputPath, kInputInterface, QStringLiteral("ControllerRemoved"), this, SLOT(onControllerRemoved(QString))); const bool ok4 = bus.connect(kInputService, kInputPath, kInputInterface, QStringLiteral("ProfilesChanged"), this, SLOT(onProfilesChanged())); m_connectedSignals = ok1 && ok2 && ok3 && ok4; } void InputServiceClient::onControllerAdded(QVariantMap controller) { upsertController(normalizeMap(controller)); } void InputServiceClient::onControllerChanged(QVariantMap controller) { upsertController(normalizeMap(controller)); } void InputServiceClient::onControllerRemoved(const QString &controllerId) { removeControllerById(controllerId); } void InputServiceClient::onProfilesChanged() { refreshProfiles(); } void InputServiceClient::upsertController(const QVariantMap &controller) { const QString id = controller.value(QStringLiteral("controllerId")).toString(); if (id.isEmpty()) { return; } for (int i = 0; i < m_controllers.size(); ++i) { const QVariantMap existing = m_controllers.at(i).toMap(); if (existing.value(QStringLiteral("controllerId")).toString() == id) { m_controllers[i] = controller; Q_EMIT controllersChanged(); return; } } m_controllers.push_back(controller); Q_EMIT controllersChanged(); } void InputServiceClient::removeControllerById(const QString &controllerId) { if (controllerId.isEmpty()) { return; } for (int i = 0; i < m_controllers.size(); ++i) { const QVariantMap existing = m_controllers.at(i).toMap(); if (existing.value(QStringLiteral("controllerId")).toString() == controllerId) { m_controllers.removeAt(i); Q_EMIT controllersChanged(); return; } } } void InputServiceClient::refresh() { refreshControllers(); refreshProfiles(); } void InputServiceClient::refreshControllers() { ensureService(); if (!m_available) { return; } QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus()); i.setTimeout(2000); const QDBusReply rescanReply = i.call(QStringLiteral("Rescan")); if (!rescanReply.isValid()) { setLastError(rescanReply.error().message()); } const QDBusReply reply = i.call(QStringLiteral("ListControllers")); if (!reply.isValid()) { setLastError(reply.error().message()); return; } m_controllers.clear(); const QVariantList list = reply.value(); m_controllers.reserve(list.size()); for (const QVariant &v : list) { m_controllers.push_back(unwrapVariantMap(v)); } Q_EMIT controllersChanged(); } void InputServiceClient::refreshProfiles() { ensureService(); if (!m_available) { return; } QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus()); i.setTimeout(2000); const QDBusReply reply = i.call(QStringLiteral("ListProfiles")); if (!reply.isValid()) { setLastError(reply.error().message()); return; } m_profiles.clear(); const QVariantList list = reply.value(); m_profiles.reserve(list.size()); for (const QVariant &v : list) { m_profiles.push_back(unwrapVariantMap(v)); } Q_EMIT profilesChanged(); } bool InputServiceClient::setActiveProfile(const QString &controllerId, const QString &profileId) { ensureService(); if (!m_available) { return false; } QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus()); i.setTimeout(2000); const QDBusReply reply = i.call(QStringLiteral("SetActiveProfile"), controllerId, profileId); if (!reply.isValid()) { setLastError(reply.error().message()); return false; } return reply.value(); } QString InputServiceClient::createProfile(const QString &name) { ensureService(); if (!m_available) { return {}; } QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus()); i.setTimeout(2000); const QDBusReply reply = i.call(QStringLiteral("CreateProfile"), name); if (!reply.isValid()) { setLastError(reply.error().message()); return {}; } refreshProfiles(); return reply.value(); } bool InputServiceClient::deleteProfile(const QString &profileId) { ensureService(); if (!m_available) { return false; } QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus()); i.setTimeout(2000); const QDBusReply reply = i.call(QStringLiteral("DeleteProfile"), profileId); if (!reply.isValid()) { setLastError(reply.error().message()); return false; } refreshProfiles(); refreshControllers(); return reply.value(); }