mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-03-26 17:03:08 +00:00
input: add input service daemon + client
This commit is contained in:
parent
7724205354
commit
1e5a86fda8
13 changed files with 1272 additions and 1 deletions
|
|
@ -9,6 +9,7 @@ add_executable(alakarte
|
||||||
game.cpp
|
game.cpp
|
||||||
gamepadmanager.cpp
|
gamepadmanager.cpp
|
||||||
inputmanager.cpp
|
inputmanager.cpp
|
||||||
|
inputserviceclient.cpp
|
||||||
mediamanager.cpp
|
mediamanager.cpp
|
||||||
runnermanagerclient.cpp
|
runnermanagerclient.cpp
|
||||||
screenshotmodel.cpp
|
screenshotmodel.cpp
|
||||||
|
|
@ -122,6 +123,7 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
|
||||||
game.h
|
game.h
|
||||||
gamepadmanager.h
|
gamepadmanager.h
|
||||||
inputmanager.h
|
inputmanager.h
|
||||||
|
inputserviceclient.h
|
||||||
platformimporter.h
|
platformimporter.h
|
||||||
steamimporter.h
|
steamimporter.h
|
||||||
lutrisimporter.h
|
lutrisimporter.h
|
||||||
|
|
@ -142,3 +144,5 @@ install(TARGETS alakarte ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||||
add_subdirectory(gamecenter)
|
add_subdirectory(gamecenter)
|
||||||
|
|
||||||
add_subdirectory(runner)
|
add_subdirectory(runner)
|
||||||
|
|
||||||
|
add_subdirectory(input)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ App::App(QObject *parent)
|
||||||
, m_gameModel(new GameModel(this))
|
, m_gameModel(new GameModel(this))
|
||||||
, m_launcher(new GameLauncher(this))
|
, m_launcher(new GameLauncher(this))
|
||||||
, m_runnerManager(new RunnerManagerClient(this))
|
, m_runnerManager(new RunnerManagerClient(this))
|
||||||
|
, m_inputService(new InputServiceClient(this))
|
||||||
, m_steamGridDB(new SteamGridDB(this))
|
, m_steamGridDB(new SteamGridDB(this))
|
||||||
, m_mediaManager(new MediaManager(this))
|
, m_mediaManager(new MediaManager(this))
|
||||||
, m_config(new Config(this))
|
, m_config(new Config(this))
|
||||||
|
|
@ -175,6 +176,11 @@ RunnerManagerClient *App::runnerManager() const
|
||||||
return m_runnerManager;
|
return m_runnerManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InputServiceClient *App::inputService() const
|
||||||
|
{
|
||||||
|
return m_inputService;
|
||||||
|
}
|
||||||
|
|
||||||
SteamGridDB *App::steamGridDB() const
|
SteamGridDB *App::steamGridDB() const
|
||||||
{
|
{
|
||||||
return m_steamGridDB;
|
return m_steamGridDB;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "gamelauncher.h"
|
#include "gamelauncher.h"
|
||||||
#include "gamemodel.h"
|
#include "gamemodel.h"
|
||||||
|
#include "inputserviceclient.h"
|
||||||
#include "mediamanager.h"
|
#include "mediamanager.h"
|
||||||
#include "runnermanagerclient.h"
|
#include "runnermanagerclient.h"
|
||||||
#include "steamgriddb.h"
|
#include "steamgriddb.h"
|
||||||
|
|
@ -24,6 +25,7 @@ class App : public QObject
|
||||||
Q_PROPERTY(GameModel *gameModel READ gameModel CONSTANT)
|
Q_PROPERTY(GameModel *gameModel READ gameModel CONSTANT)
|
||||||
Q_PROPERTY(GameLauncher *launcher READ launcher CONSTANT)
|
Q_PROPERTY(GameLauncher *launcher READ launcher CONSTANT)
|
||||||
Q_PROPERTY(RunnerManagerClient *runnerManager READ runnerManager CONSTANT)
|
Q_PROPERTY(RunnerManagerClient *runnerManager READ runnerManager CONSTANT)
|
||||||
|
Q_PROPERTY(InputServiceClient *inputService READ inputService CONSTANT)
|
||||||
Q_PROPERTY(SteamGridDB *steamGridDB READ steamGridDB CONSTANT)
|
Q_PROPERTY(SteamGridDB *steamGridDB READ steamGridDB CONSTANT)
|
||||||
Q_PROPERTY(MediaManager *mediaManager READ mediaManager CONSTANT)
|
Q_PROPERTY(MediaManager *mediaManager READ mediaManager CONSTANT)
|
||||||
Q_PROPERTY(Config *config READ config CONSTANT)
|
Q_PROPERTY(Config *config READ config CONSTANT)
|
||||||
|
|
@ -37,6 +39,7 @@ public:
|
||||||
GameModel *gameModel() const;
|
GameModel *gameModel() const;
|
||||||
GameLauncher *launcher() const;
|
GameLauncher *launcher() const;
|
||||||
RunnerManagerClient *runnerManager() const;
|
RunnerManagerClient *runnerManager() const;
|
||||||
|
InputServiceClient *inputService() const;
|
||||||
SteamGridDB *steamGridDB() const;
|
SteamGridDB *steamGridDB() const;
|
||||||
MediaManager *mediaManager() const;
|
MediaManager *mediaManager() const;
|
||||||
Config *config() const;
|
Config *config() const;
|
||||||
|
|
@ -78,6 +81,7 @@ private:
|
||||||
GameModel *m_gameModel;
|
GameModel *m_gameModel;
|
||||||
GameLauncher *m_launcher;
|
GameLauncher *m_launcher;
|
||||||
RunnerManagerClient *m_runnerManager;
|
RunnerManagerClient *m_runnerManager;
|
||||||
|
InputServiceClient *m_inputService;
|
||||||
SteamGridDB *m_steamGridDB;
|
SteamGridDB *m_steamGridDB;
|
||||||
MediaManager *m_mediaManager;
|
MediaManager *m_mediaManager;
|
||||||
Config *m_config;
|
Config *m_config;
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,21 @@ bool GamepadManager::active() const
|
||||||
return m_active;
|
return m_active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString GamepadManager::name() const
|
||||||
|
{
|
||||||
|
return m_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GamepadManager::vendorId() const
|
||||||
|
{
|
||||||
|
return m_vendorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GamepadManager::productId() const
|
||||||
|
{
|
||||||
|
return m_productId;
|
||||||
|
}
|
||||||
|
|
||||||
GamepadManager::ControllerStyle GamepadManager::controllerStyle() const
|
GamepadManager::ControllerStyle GamepadManager::controllerStyle() const
|
||||||
{
|
{
|
||||||
return m_controllerStyle;
|
return m_controllerStyle;
|
||||||
|
|
@ -229,14 +244,24 @@ void GamepadManager::closeGamepad()
|
||||||
void GamepadManager::updateControllerInfo()
|
void GamepadManager::updateControllerInfo()
|
||||||
{
|
{
|
||||||
ControllerStyle style = UnknownController;
|
ControllerStyle style = UnknownController;
|
||||||
|
QString name;
|
||||||
|
int vendorId = 0;
|
||||||
|
int productId = 0;
|
||||||
QString confirmLabel;
|
QString confirmLabel;
|
||||||
QString backLabel;
|
QString backLabel;
|
||||||
QString detailsLabel;
|
QString detailsLabel;
|
||||||
QString searchLabel;
|
QString searchLabel;
|
||||||
|
|
||||||
if (m_gamepad) {
|
if (m_gamepad) {
|
||||||
|
const char *padName = SDL_GetGamepadName(m_gamepad);
|
||||||
|
if (padName) {
|
||||||
|
name = QString::fromUtf8(padName);
|
||||||
|
}
|
||||||
const SDL_GamepadType type = SDL_GetGamepadType(m_gamepad);
|
const SDL_GamepadType type = SDL_GetGamepadType(m_gamepad);
|
||||||
const Uint16 vendor = SDL_GetGamepadVendor(m_gamepad);
|
const Uint16 vendor = SDL_GetGamepadVendor(m_gamepad);
|
||||||
|
const Uint16 product = SDL_GetGamepadProduct(m_gamepad);
|
||||||
|
vendorId = static_cast<int>(vendor);
|
||||||
|
productId = static_cast<int>(product);
|
||||||
style = controllerStyleFromTypeVendor(type, vendor);
|
style = controllerStyleFromTypeVendor(type, vendor);
|
||||||
|
|
||||||
#if SDL_VERSION_ATLEAST(3, 2, 0)
|
#if SDL_VERSION_ATLEAST(3, 2, 0)
|
||||||
|
|
@ -291,9 +316,13 @@ void GamepadManager::updateControllerInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool changed = (style != m_controllerStyle) || (confirmLabel != m_confirmButtonLabel) || (backLabel != m_backButtonLabel)
|
const bool changed = (style != m_controllerStyle) || (confirmLabel != m_confirmButtonLabel) || (backLabel != m_backButtonLabel)
|
||||||
|| (detailsLabel != m_detailsButtonLabel) || (searchLabel != m_searchButtonLabel);
|
|| (detailsLabel != m_detailsButtonLabel) || (searchLabel != m_searchButtonLabel) || (name != m_name) || (vendorId != m_vendorId)
|
||||||
|
|| (productId != m_productId);
|
||||||
|
|
||||||
m_controllerStyle = style;
|
m_controllerStyle = style;
|
||||||
|
m_name = name;
|
||||||
|
m_vendorId = vendorId;
|
||||||
|
m_productId = productId;
|
||||||
m_confirmButtonLabel = confirmLabel;
|
m_confirmButtonLabel = confirmLabel;
|
||||||
m_backButtonLabel = backLabel;
|
m_backButtonLabel = backLabel;
|
||||||
m_detailsButtonLabel = detailsLabel;
|
m_detailsButtonLabel = detailsLabel;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ class GamepadManager : public QObject
|
||||||
|
|
||||||
Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
|
Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
|
||||||
Q_PROPERTY(bool active READ active NOTIFY activeChanged)
|
Q_PROPERTY(bool active READ active NOTIFY activeChanged)
|
||||||
|
Q_PROPERTY(QString name READ name NOTIFY controllerInfoChanged)
|
||||||
|
Q_PROPERTY(int vendorId READ vendorId NOTIFY controllerInfoChanged)
|
||||||
|
Q_PROPERTY(int productId READ productId NOTIFY controllerInfoChanged)
|
||||||
Q_PROPERTY(ControllerStyle controllerStyle READ controllerStyle NOTIFY controllerInfoChanged)
|
Q_PROPERTY(ControllerStyle controllerStyle READ controllerStyle NOTIFY controllerInfoChanged)
|
||||||
Q_PROPERTY(QString confirmButtonLabel READ confirmButtonLabel NOTIFY controllerInfoChanged)
|
Q_PROPERTY(QString confirmButtonLabel READ confirmButtonLabel NOTIFY controllerInfoChanged)
|
||||||
Q_PROPERTY(QString backButtonLabel READ backButtonLabel NOTIFY controllerInfoChanged)
|
Q_PROPERTY(QString backButtonLabel READ backButtonLabel NOTIFY controllerInfoChanged)
|
||||||
|
|
@ -40,6 +43,10 @@ public:
|
||||||
bool connected() const;
|
bool connected() const;
|
||||||
bool active() const;
|
bool active() const;
|
||||||
|
|
||||||
|
QString name() const;
|
||||||
|
int vendorId() const;
|
||||||
|
int productId() const;
|
||||||
|
|
||||||
ControllerStyle controllerStyle() const;
|
ControllerStyle controllerStyle() const;
|
||||||
QString confirmButtonLabel() const;
|
QString confirmButtonLabel() const;
|
||||||
QString backButtonLabel() const;
|
QString backButtonLabel() const;
|
||||||
|
|
@ -86,6 +93,10 @@ private:
|
||||||
qint8 m_leftDirX = 0;
|
qint8 m_leftDirX = 0;
|
||||||
qint8 m_leftDirY = 0;
|
qint8 m_leftDirY = 0;
|
||||||
|
|
||||||
|
QString m_name;
|
||||||
|
int m_vendorId = 0;
|
||||||
|
int m_productId = 0;
|
||||||
|
|
||||||
void openFirstAvailableGamepad();
|
void openFirstAvailableGamepad();
|
||||||
void openGamepad(int deviceId);
|
void openGamepad(int deviceId);
|
||||||
void closeGamepad();
|
void closeGamepad();
|
||||||
|
|
|
||||||
41
src/input/CMakeLists.txt
Normal file
41
src/input/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
# SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
add_executable(alakarte_inputd
|
||||||
|
main.cpp
|
||||||
|
inputdaemon.cpp
|
||||||
|
inputdaemon.h
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(alakarte_inputd PRIVATE
|
||||||
|
Qt6::Core
|
||||||
|
Qt6::DBus
|
||||||
|
SDL3::SDL3
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(alakarte_inputd PROPERTIES
|
||||||
|
OUTPUT_NAME "alakarte-inputd"
|
||||||
|
)
|
||||||
|
|
||||||
|
install(TARGETS alakarte_inputd ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||||
|
|
||||||
|
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus")
|
||||||
|
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd")
|
||||||
|
|
||||||
|
configure_file(dbus/org.kde.ALaKarte.Input1.service.in
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.ALaKarte.Input1.service"
|
||||||
|
@ONLY
|
||||||
|
)
|
||||||
|
|
||||||
|
configure_file(systemd/org.kde.ALaKarte.Input1.service.in
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.ALaKarte.Input1.service"
|
||||||
|
@ONLY
|
||||||
|
)
|
||||||
|
|
||||||
|
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.ALaKarte.Input1.service"
|
||||||
|
DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.ALaKarte.Input1.service"
|
||||||
|
DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}
|
||||||
|
)
|
||||||
4
src/input/dbus/org.kde.ALaKarte.Input1.service.in
Normal file
4
src/input/dbus/org.kde.ALaKarte.Input1.service.in
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[D-BUS Service]
|
||||||
|
Name=org.kde.ALaKarte.Input1
|
||||||
|
Exec=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-inputd
|
||||||
|
SystemdService=org.kde.ALaKarte.Input1.service
|
||||||
618
src/input/inputdaemon.cpp
Normal file
618
src/input/inputdaemon.cpp
Normal file
|
|
@ -0,0 +1,618 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include "inputdaemon.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusConnectionInterface>
|
||||||
|
#include <QDBusError>
|
||||||
|
#include <QDBusReply>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#define SDL_MAIN_HANDLED 1
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3/SDL_main.h>
|
||||||
|
|
||||||
|
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 QString guidToString(SDL_GUID guid)
|
||||||
|
{
|
||||||
|
char buf[64] = {0};
|
||||||
|
SDL_GUIDToString(guid, buf, sizeof(buf));
|
||||||
|
return QString::fromLatin1(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString powerStateToString(SDL_PowerState st)
|
||||||
|
{
|
||||||
|
switch (st) {
|
||||||
|
case SDL_POWERSTATE_ON_BATTERY:
|
||||||
|
return QStringLiteral("on_battery");
|
||||||
|
case SDL_POWERSTATE_CHARGING:
|
||||||
|
return QStringLiteral("charging");
|
||||||
|
case SDL_POWERSTATE_CHARGED:
|
||||||
|
return QStringLiteral("charged");
|
||||||
|
case SDL_POWERSTATE_NO_BATTERY:
|
||||||
|
return QStringLiteral("no_battery");
|
||||||
|
case SDL_POWERSTATE_UNKNOWN:
|
||||||
|
return QStringLiteral("unknown");
|
||||||
|
default:
|
||||||
|
return QStringLiteral("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString connectionStateToString(SDL_JoystickConnectionState st)
|
||||||
|
{
|
||||||
|
switch (st) {
|
||||||
|
case SDL_JOYSTICK_CONNECTION_WIRED:
|
||||||
|
return QStringLiteral("wired");
|
||||||
|
case SDL_JOYSTICK_CONNECTION_WIRELESS:
|
||||||
|
return QStringLiteral("wireless");
|
||||||
|
default:
|
||||||
|
return QStringLiteral("unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString gamepadTypeToString(SDL_GamepadType t)
|
||||||
|
{
|
||||||
|
switch (t) {
|
||||||
|
case SDL_GAMEPAD_TYPE_XBOX360:
|
||||||
|
case SDL_GAMEPAD_TYPE_XBOXONE:
|
||||||
|
return QStringLiteral("xbox");
|
||||||
|
case SDL_GAMEPAD_TYPE_PS3:
|
||||||
|
case SDL_GAMEPAD_TYPE_PS4:
|
||||||
|
case SDL_GAMEPAD_TYPE_PS5:
|
||||||
|
return QStringLiteral("playstation");
|
||||||
|
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO:
|
||||||
|
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT:
|
||||||
|
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT:
|
||||||
|
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_PAIR:
|
||||||
|
return QStringLiteral("nintendo");
|
||||||
|
case SDL_GAMEPAD_TYPE_STANDARD:
|
||||||
|
return QStringLiteral("standard");
|
||||||
|
default:
|
||||||
|
return QStringLiteral("unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static QVariantMap unwrapVariantMap(QVariant v)
|
||||||
|
{
|
||||||
|
if (v.canConvert<QVariantMap>()) {
|
||||||
|
return v.toMap();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap InputDaemon::Profile::toVariantMap() const
|
||||||
|
{
|
||||||
|
QVariantMap out;
|
||||||
|
out.insert(QStringLiteral("id"), id);
|
||||||
|
out.insert(QStringLiteral("name"), name);
|
||||||
|
if (!mapping.isEmpty()) {
|
||||||
|
out.insert(QStringLiteral("mapping"), mapping);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputDaemon::Profile InputDaemon::Profile::fromVariantMap(const QVariantMap &map)
|
||||||
|
{
|
||||||
|
Profile p;
|
||||||
|
p.id = map.value(QStringLiteral("id")).toString();
|
||||||
|
p.name = map.value(QStringLiteral("name")).toString();
|
||||||
|
p.mapping = map.value(QStringLiteral("mapping")).toMap();
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputDaemon::Profile::isValid() const
|
||||||
|
{
|
||||||
|
return !id.isEmpty() && !name.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
InputDaemon::InputDaemon(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
SDL_SetMainReady();
|
||||||
|
|
||||||
|
m_pollTimer.setInterval(16);
|
||||||
|
connect(&m_pollTimer, &QTimer::timeout, this, &InputDaemon::pollEvents);
|
||||||
|
|
||||||
|
if (QCoreApplication::instance()) {
|
||||||
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
|
||||||
|
shutdownSdl();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputDaemon::init()
|
||||||
|
{
|
||||||
|
loadProfiles();
|
||||||
|
|
||||||
|
if (!initSdl()) {
|
||||||
|
qWarning() << "InputDaemon: SDL init failed" << SDL_GetError();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (!bus.isConnected()) {
|
||||||
|
qWarning() << "InputDaemon: session bus not connected";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bus.registerService(kInputService)) {
|
||||||
|
qWarning() << "InputDaemon: failed to register service" << kInputService << bus.lastError().message();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bus.registerObject(kInputPath, this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals)) {
|
||||||
|
qWarning() << "InputDaemon: failed to register object" << kInputPath << bus.lastError().message();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
openExistingControllers();
|
||||||
|
m_pollTimer.start();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputDaemon::initSdl()
|
||||||
|
{
|
||||||
|
if (m_sdlInitialized) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_sdlInitialized = SDL_Init(SDL_INIT_GAMEPAD | SDL_INIT_JOYSTICK);
|
||||||
|
if (!m_sdlInitialized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SetGamepadEventsEnabled(true);
|
||||||
|
SDL_SetJoystickEventsEnabled(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::shutdownSdl()
|
||||||
|
{
|
||||||
|
if (!m_sdlInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_pollTimer.stop();
|
||||||
|
|
||||||
|
const QList<int> keys = m_controllers.keys();
|
||||||
|
for (int instanceId : keys) {
|
||||||
|
closeController(instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Quit();
|
||||||
|
m_sdlInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InputDaemon::Ping() const
|
||||||
|
{
|
||||||
|
return QStringLiteral("ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::Version(uint &major, uint &minor) const
|
||||||
|
{
|
||||||
|
major = 1;
|
||||||
|
minor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap InputDaemon::GetCapabilities() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{QStringLiteral("supportsBattery"), true},
|
||||||
|
{QStringLiteral("supportsHotplug"), true},
|
||||||
|
{QStringLiteral("supportsProfiles"), true},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InputDaemon::profilesPath() const
|
||||||
|
{
|
||||||
|
const QString base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||||
|
return base + QStringLiteral("/input-profiles.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::loadProfiles()
|
||||||
|
{
|
||||||
|
m_profiles.clear();
|
||||||
|
m_controllerKeyToProfileId.clear();
|
||||||
|
|
||||||
|
QFile f(profilesPath());
|
||||||
|
if (f.open(QIODevice::ReadOnly)) {
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
|
||||||
|
if (doc.isObject()) {
|
||||||
|
const QJsonObject root = doc.object();
|
||||||
|
|
||||||
|
const QJsonArray profiles = root.value(QStringLiteral("profiles")).toArray();
|
||||||
|
for (const QJsonValue &v : profiles) {
|
||||||
|
if (!v.isObject()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QVariantMap m = v.toObject().toVariantMap();
|
||||||
|
const Profile p = Profile::fromVariantMap(m);
|
||||||
|
if (p.isValid()) {
|
||||||
|
m_profiles.insert(p.id, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject assignments = root.value(QStringLiteral("assignments")).toObject();
|
||||||
|
for (auto it = assignments.begin(); it != assignments.end(); ++it) {
|
||||||
|
const QString controllerKey = it.key();
|
||||||
|
const QString profileId = it.value().toString();
|
||||||
|
if (!controllerKey.isEmpty() && !profileId.isEmpty()) {
|
||||||
|
m_controllerKeyToProfileId.insert(controllerKey, profileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_profiles.isEmpty()) {
|
||||||
|
Profile def;
|
||||||
|
def.id = QStringLiteral("default");
|
||||||
|
def.name = QStringLiteral("Default");
|
||||||
|
m_profiles.insert(def.id, def);
|
||||||
|
saveProfiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputDaemon::saveProfiles() const
|
||||||
|
{
|
||||||
|
const QString path = profilesPath();
|
||||||
|
QDir().mkpath(QFileInfo(path).absolutePath());
|
||||||
|
|
||||||
|
QJsonArray profiles;
|
||||||
|
for (const Profile &p : m_profiles) {
|
||||||
|
profiles.push_back(QJsonObject::fromVariantMap(p.toVariantMap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject assignments;
|
||||||
|
for (auto it = m_controllerKeyToProfileId.constBegin(); it != m_controllerKeyToProfileId.constEnd(); ++it) {
|
||||||
|
assignments.insert(it.key(), it.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject root;
|
||||||
|
root.insert(QStringLiteral("profiles"), profiles);
|
||||||
|
root.insert(QStringLiteral("assignments"), assignments);
|
||||||
|
|
||||||
|
QFile f(path);
|
||||||
|
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.write(QJsonDocument(root).toJson());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputDaemon::Profile InputDaemon::profileById(const QString &profileId) const
|
||||||
|
{
|
||||||
|
auto it = m_profiles.constFind(profileId);
|
||||||
|
if (it == m_profiles.constEnd()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return it.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList InputDaemon::ListProfiles() const
|
||||||
|
{
|
||||||
|
QVariantList out;
|
||||||
|
out.reserve(m_profiles.size());
|
||||||
|
|
||||||
|
for (const Profile &p : m_profiles) {
|
||||||
|
out.push_back(p.toVariantMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap InputDaemon::GetProfile(const QString &profileId) const
|
||||||
|
{
|
||||||
|
const Profile p = profileById(profileId);
|
||||||
|
return p.toVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InputDaemon::CreateProfile(const QString &name)
|
||||||
|
{
|
||||||
|
const QString trimmed = name.trimmed();
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString id = QString::number(QDateTime::currentMSecsSinceEpoch(), 16);
|
||||||
|
|
||||||
|
Profile p;
|
||||||
|
p.id = QStringLiteral("p_") + id;
|
||||||
|
p.name = trimmed;
|
||||||
|
|
||||||
|
if (!p.isValid()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
m_profiles.insert(p.id, p);
|
||||||
|
saveProfiles();
|
||||||
|
Q_EMIT ProfilesChanged();
|
||||||
|
|
||||||
|
return p.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputDaemon::DeleteProfile(const QString &profileId)
|
||||||
|
{
|
||||||
|
if (profileId == QLatin1String("default")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_profiles.contains(profileId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_profiles.remove(profileId);
|
||||||
|
|
||||||
|
for (auto it = m_controllerKeyToProfileId.begin(); it != m_controllerKeyToProfileId.end();) {
|
||||||
|
if (it.value() == profileId) {
|
||||||
|
it = m_controllerKeyToProfileId.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveProfiles();
|
||||||
|
Q_EMIT ProfilesChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InputDaemon::controllerKeyForProfile(const Controller &c) const
|
||||||
|
{
|
||||||
|
if (!c.guid.isEmpty() && c.vendorId > 0 && c.productId > 0) {
|
||||||
|
return QStringLiteral("guid:%1:%2:%3").arg(c.guid).arg(c.vendorId).arg(c.productId);
|
||||||
|
}
|
||||||
|
if (!c.guid.isEmpty()) {
|
||||||
|
return QStringLiteral("guid:%1").arg(c.guid);
|
||||||
|
}
|
||||||
|
if (!c.path.isEmpty()) {
|
||||||
|
return QStringLiteral("path:%1").arg(c.path);
|
||||||
|
}
|
||||||
|
return c.controllerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InputDaemon::GetActiveProfile(const QString &controllerId) const
|
||||||
|
{
|
||||||
|
for (const Controller &c : m_controllers) {
|
||||||
|
if (c.controllerId == controllerId) {
|
||||||
|
const QString key = controllerKeyForProfile(c);
|
||||||
|
return m_controllerKeyToProfileId.value(key, QStringLiteral("default"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QStringLiteral("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputDaemon::SetActiveProfile(const QString &controllerId, const QString &profileId)
|
||||||
|
{
|
||||||
|
if (!m_profiles.contains(profileId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Controller &c : m_controllers) {
|
||||||
|
if (c.controllerId == controllerId) {
|
||||||
|
const QString key = controllerKeyForProfile(c);
|
||||||
|
m_controllerKeyToProfileId.insert(key, profileId);
|
||||||
|
saveProfiles();
|
||||||
|
Q_EMIT ControllerProfileChanged(controllerId, profileId);
|
||||||
|
Q_EMIT ControllerChanged(controllerToVariantMap(c));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap InputDaemon::controllerToVariantMap(const Controller &c) const
|
||||||
|
{
|
||||||
|
QVariantMap out;
|
||||||
|
out.insert(QStringLiteral("controllerId"), c.controllerId);
|
||||||
|
out.insert(QStringLiteral("instanceId"), c.instanceId);
|
||||||
|
out.insert(QStringLiteral("guid"), c.guid);
|
||||||
|
out.insert(QStringLiteral("name"), c.name);
|
||||||
|
out.insert(QStringLiteral("path"), c.path);
|
||||||
|
out.insert(QStringLiteral("vendorId"), c.vendorId);
|
||||||
|
out.insert(QStringLiteral("productId"), c.productId);
|
||||||
|
out.insert(QStringLiteral("type"), c.type);
|
||||||
|
out.insert(QStringLiteral("connectionState"), c.connectionState);
|
||||||
|
|
||||||
|
out.insert(QStringLiteral("batteryState"), c.batteryState);
|
||||||
|
out.insert(QStringLiteral("batteryStateName"), powerStateToString(static_cast<SDL_PowerState>(c.batteryState)));
|
||||||
|
out.insert(QStringLiteral("batteryPercent"), c.batteryPercent);
|
||||||
|
|
||||||
|
const QString activeId = m_controllerKeyToProfileId.value(controllerKeyForProfile(c), QStringLiteral("default"));
|
||||||
|
out.insert(QStringLiteral("activeProfileId"), activeId);
|
||||||
|
out.insert(QStringLiteral("activeProfileName"), profileById(activeId).name);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList InputDaemon::ListControllers() const
|
||||||
|
{
|
||||||
|
QVariantList out;
|
||||||
|
out.reserve(m_controllers.size());
|
||||||
|
|
||||||
|
for (const Controller &c : m_controllers) {
|
||||||
|
out.push_back(controllerToVariantMap(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap InputDaemon::GetController(const QString &controllerId) const
|
||||||
|
{
|
||||||
|
for (const Controller &c : m_controllers) {
|
||||||
|
if (c.controllerId == controllerId) {
|
||||||
|
return controllerToVariantMap(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputDaemon::Rescan()
|
||||||
|
{
|
||||||
|
openExistingControllers();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::openExistingControllers()
|
||||||
|
{
|
||||||
|
if (!m_sdlInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
std::unique_ptr<SDL_JoystickID[], decltype(&SDL_free)> ids(SDL_GetGamepads(&count), SDL_free);
|
||||||
|
if (!ids || count <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
const int instanceId = static_cast<int>(ids[i]);
|
||||||
|
if (!m_controllers.contains(instanceId)) {
|
||||||
|
openController(instanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::refreshControllerPower(Controller &c)
|
||||||
|
{
|
||||||
|
if (!c.pad) {
|
||||||
|
c.batteryState = static_cast<int>(SDL_POWERSTATE_UNKNOWN);
|
||||||
|
c.batteryPercent = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int percent = -1;
|
||||||
|
const SDL_PowerState st = SDL_GetGamepadPowerInfo(c.pad, &percent);
|
||||||
|
c.batteryState = static_cast<int>(st);
|
||||||
|
c.batteryPercent = percent;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::openController(int instanceId)
|
||||||
|
{
|
||||||
|
if (!m_sdlInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_controllers.contains(instanceId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Gamepad *pad = SDL_OpenGamepad(static_cast<SDL_JoystickID>(instanceId));
|
||||||
|
if (!pad) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Controller c;
|
||||||
|
c.pad = pad;
|
||||||
|
c.instanceId = instanceId;
|
||||||
|
c.controllerId = QStringLiteral("sdl:%1").arg(instanceId);
|
||||||
|
|
||||||
|
c.guid = guidToString(SDL_GetGamepadGUIDForID(static_cast<SDL_JoystickID>(instanceId)));
|
||||||
|
|
||||||
|
const char *name = SDL_GetGamepadName(pad);
|
||||||
|
if (name) {
|
||||||
|
c.name = QString::fromUtf8(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *path = SDL_GetGamepadPath(pad);
|
||||||
|
if (path) {
|
||||||
|
c.path = QString::fromUtf8(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.vendorId = SDL_GetGamepadVendor(pad);
|
||||||
|
c.productId = SDL_GetGamepadProduct(pad);
|
||||||
|
c.type = gamepadTypeToString(SDL_GetGamepadType(pad));
|
||||||
|
c.connectionState = connectionStateToString(SDL_GetGamepadConnectionState(pad));
|
||||||
|
|
||||||
|
refreshControllerPower(c);
|
||||||
|
|
||||||
|
m_controllers.insert(instanceId, c);
|
||||||
|
Q_EMIT ControllerAdded(controllerToVariantMap(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::closeController(int instanceId)
|
||||||
|
{
|
||||||
|
auto it = m_controllers.find(instanceId);
|
||||||
|
if (it == m_controllers.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString controllerId = it.value().controllerId;
|
||||||
|
|
||||||
|
if (it.value().pad) {
|
||||||
|
SDL_CloseGamepad(it.value().pad);
|
||||||
|
it.value().pad = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_controllers.erase(it);
|
||||||
|
Q_EMIT ControllerRemoved(controllerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::pollEvents()
|
||||||
|
{
|
||||||
|
if (!m_sdlInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Event e;
|
||||||
|
bool anyChanged = false;
|
||||||
|
|
||||||
|
while (SDL_PollEvent(&e)) {
|
||||||
|
switch (e.type) {
|
||||||
|
case SDL_EVENT_GAMEPAD_ADDED:
|
||||||
|
openController(static_cast<int>(e.gdevice.which));
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||||
|
closeController(static_cast<int>(e.gdevice.which));
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_JOYSTICK_BATTERY_UPDATED: {
|
||||||
|
const int instanceId = static_cast<int>(e.jbattery.which);
|
||||||
|
auto it = m_controllers.find(instanceId);
|
||||||
|
if (it == m_controllers.end()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
it.value().batteryState = static_cast<int>(e.jbattery.state);
|
||||||
|
it.value().batteryPercent = e.jbattery.percent;
|
||||||
|
Q_EMIT ControllerChanged(controllerToVariantMap(it.value()));
|
||||||
|
anyChanged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const qint64 nowMs = QDateTime::currentMSecsSinceEpoch();
|
||||||
|
if (m_lastPowerRefreshMs <= 0 || (nowMs - m_lastPowerRefreshMs) > 5000) {
|
||||||
|
m_lastPowerRefreshMs = nowMs;
|
||||||
|
for (auto it = m_controllers.begin(); it != m_controllers.end(); ++it) {
|
||||||
|
const int oldPercent = it.value().batteryPercent;
|
||||||
|
const int oldState = it.value().batteryState;
|
||||||
|
refreshControllerPower(it.value());
|
||||||
|
if (it.value().batteryPercent != oldPercent || it.value().batteryState != oldState) {
|
||||||
|
Q_EMIT ControllerChanged(controllerToVariantMap(it.value()));
|
||||||
|
anyChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_UNUSED(anyChanged)
|
||||||
|
}
|
||||||
104
src/input/inputdaemon.h
Normal file
104
src/input/inputdaemon.h
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QVariantList>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
struct SDL_Gamepad;
|
||||||
|
|
||||||
|
class InputDaemon : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_CLASSINFO("D-Bus Interface", "org.kde.ALaKarte.Input1")
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit InputDaemon(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
bool init();
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
QString Ping() const;
|
||||||
|
void Version(uint &major, uint &minor) const;
|
||||||
|
QVariantMap GetCapabilities() const;
|
||||||
|
|
||||||
|
QVariantList ListControllers() const;
|
||||||
|
QVariantMap GetController(const QString &controllerId) const;
|
||||||
|
bool Rescan();
|
||||||
|
|
||||||
|
QVariantList ListProfiles() const;
|
||||||
|
QVariantMap GetProfile(const QString &profileId) const;
|
||||||
|
QString CreateProfile(const QString &name);
|
||||||
|
bool DeleteProfile(const QString &profileId);
|
||||||
|
|
||||||
|
bool SetActiveProfile(const QString &controllerId, const QString &profileId);
|
||||||
|
QString GetActiveProfile(const QString &controllerId) const;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void ControllerAdded(const QVariantMap &controller);
|
||||||
|
void ControllerRemoved(const QString &controllerId);
|
||||||
|
void ControllerChanged(const QVariantMap &controller);
|
||||||
|
|
||||||
|
void ProfilesChanged();
|
||||||
|
void ControllerProfileChanged(const QString &controllerId, const QString &profileId);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Controller {
|
||||||
|
SDL_Gamepad *pad = nullptr;
|
||||||
|
int instanceId = 0;
|
||||||
|
QString controllerId;
|
||||||
|
QString guid;
|
||||||
|
QString name;
|
||||||
|
QString path;
|
||||||
|
uint vendorId = 0;
|
||||||
|
uint productId = 0;
|
||||||
|
QString type;
|
||||||
|
QString connectionState;
|
||||||
|
int batteryState = 0;
|
||||||
|
int batteryPercent = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Profile {
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
QVariantMap mapping;
|
||||||
|
|
||||||
|
QVariantMap toVariantMap() const;
|
||||||
|
static Profile fromVariantMap(const QVariantMap &map);
|
||||||
|
bool isValid() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool initSdl();
|
||||||
|
void shutdownSdl();
|
||||||
|
|
||||||
|
void pollEvents();
|
||||||
|
|
||||||
|
void openExistingControllers();
|
||||||
|
void openController(int instanceId);
|
||||||
|
void closeController(int instanceId);
|
||||||
|
|
||||||
|
void refreshControllerPower(Controller &c);
|
||||||
|
|
||||||
|
QString controllerKeyForProfile(const Controller &c) const;
|
||||||
|
QVariantMap controllerToVariantMap(const Controller &c) const;
|
||||||
|
|
||||||
|
QString profilesPath() const;
|
||||||
|
void loadProfiles();
|
||||||
|
bool saveProfiles() const;
|
||||||
|
|
||||||
|
Profile profileById(const QString &profileId) const;
|
||||||
|
|
||||||
|
QHash<int, Controller> m_controllers;
|
||||||
|
|
||||||
|
QHash<QString, Profile> m_profiles;
|
||||||
|
QHash<QString, QString> m_controllerKeyToProfileId;
|
||||||
|
|
||||||
|
bool m_sdlInitialized = false;
|
||||||
|
QTimer m_pollTimer;
|
||||||
|
qint64 m_lastPowerRefreshMs = 0;
|
||||||
|
};
|
||||||
19
src/input/main.cpp
Normal file
19
src/input/main.cpp
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
|
#include "inputdaemon.h"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
QCoreApplication app(argc, argv);
|
||||||
|
QCoreApplication::setApplicationName(QStringLiteral("alakarte"));
|
||||||
|
|
||||||
|
InputDaemon daemon;
|
||||||
|
if (!daemon.init()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
9
src/input/systemd/org.kde.ALaKarte.Input1.service.in
Normal file
9
src/input/systemd/org.kde.ALaKarte.Input1.service.in
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Unit]
|
||||||
|
Description=A-La-Karte Input Service
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-inputd
|
||||||
|
Type=dbus
|
||||||
|
BusName=org.kde.ALaKarte.Input1
|
||||||
|
Restart=no
|
||||||
354
src/inputserviceclient.cpp
Normal file
354
src/inputserviceclient.cpp
Normal file
|
|
@ -0,0 +1,354 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include "inputserviceclient.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusConnectionInterface>
|
||||||
|
#include <QDBusError>
|
||||||
|
#include <QDBusInterface>
|
||||||
|
#include <QDBusReply>
|
||||||
|
#include <QDBusServiceWatcher>
|
||||||
|
#include <QDBusVariant>
|
||||||
|
|
||||||
|
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<QDBusVariant>()) {
|
||||||
|
v = v.value<QDBusVariant>().variant();
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QVariantMap unwrapVariantMap(QVariant v)
|
||||||
|
{
|
||||||
|
v = unwrapDbusVariant(v);
|
||||||
|
if (v.canConvert<QVariantMap>()) {
|
||||||
|
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<QStringList> names = bus.interface()->activatableServiceNames();
|
||||||
|
if (names.isValid()) {
|
||||||
|
activatable = names.value().contains(kInputService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDBusReply<void> 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<bool> rescanReply = i.call(QStringLiteral("Rescan"));
|
||||||
|
if (!rescanReply.isValid()) {
|
||||||
|
setLastError(rescanReply.error().message());
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDBusReply<QVariantList> 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<QVariantList> 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<bool> 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<QString> 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<bool> reply = i.call(QStringLiteral("DeleteProfile"), profileId);
|
||||||
|
|
||||||
|
if (!reply.isValid()) {
|
||||||
|
setLastError(reply.error().message());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshProfiles();
|
||||||
|
refreshControllers();
|
||||||
|
return reply.value();
|
||||||
|
}
|
||||||
68
src/inputserviceclient.h
Normal file
68
src/inputserviceclient.h
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVariantList>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
class QDBusServiceWatcher;
|
||||||
|
|
||||||
|
class InputServiceClient : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(bool available READ available NOTIFY availableChanged)
|
||||||
|
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
|
||||||
|
Q_PROPERTY(QVariantList controllers READ controllers NOTIFY controllersChanged)
|
||||||
|
Q_PROPERTY(QVariantList profiles READ profiles NOTIFY profilesChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit InputServiceClient(QObject *parent = nullptr);
|
||||||
|
~InputServiceClient() override;
|
||||||
|
|
||||||
|
bool available() const;
|
||||||
|
QString lastError() const;
|
||||||
|
|
||||||
|
QVariantList controllers() const;
|
||||||
|
QVariantList profiles() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void refresh();
|
||||||
|
Q_INVOKABLE void refreshControllers();
|
||||||
|
Q_INVOKABLE void refreshProfiles();
|
||||||
|
|
||||||
|
Q_INVOKABLE bool setActiveProfile(const QString &controllerId, const QString &profileId);
|
||||||
|
Q_INVOKABLE QString createProfile(const QString &name);
|
||||||
|
Q_INVOKABLE bool deleteProfile(const QString &profileId);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void availableChanged();
|
||||||
|
void lastErrorChanged();
|
||||||
|
void controllersChanged();
|
||||||
|
void profilesChanged();
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void onControllerAdded(QVariantMap controller);
|
||||||
|
void onControllerChanged(QVariantMap controller);
|
||||||
|
void onControllerRemoved(const QString &controllerId);
|
||||||
|
void onProfilesChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ensureService();
|
||||||
|
void connectSignals();
|
||||||
|
void setAvailable(bool available);
|
||||||
|
void setLastError(const QString &error);
|
||||||
|
|
||||||
|
void upsertController(const QVariantMap &controller);
|
||||||
|
void removeControllerById(const QString &controllerId);
|
||||||
|
|
||||||
|
bool m_available = false;
|
||||||
|
QString m_lastError;
|
||||||
|
|
||||||
|
QVariantList m_controllers;
|
||||||
|
QVariantList m_profiles;
|
||||||
|
|
||||||
|
QDBusServiceWatcher *m_watcher = nullptr;
|
||||||
|
bool m_connectedSignals = false;
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue