Commit e3997397 authored by Aleix Pol Gonzalez's avatar Aleix Pol Gonzalez 🐧
Browse files

Provide a KCM for Drawing Tablets

Otherwise called Wacom tablets, that is.
This is using the same InputDevice dbus interfaces we use for the mouse
and touchpad kcms.
parent 5b17afe1
......@@ -30,6 +30,7 @@ add_subdirectory(kded)
add_subdirectory(runners)
add_subdirectory(spellchecking)
add_subdirectory(qtquicksettings)
add_subdirectory(tablet)
add_subdirectory(workspaceoptions)
if (KF5Baloo_FOUND)
......
add_definitions(-DTRANSLATION_DOMAIN=\"kcmtablet\")
include(ECMQtDeclareLoggingCategory)
ecm_qt_declare_logging_category(common_SRCS
HEADER
logging.h
IDENTIFIER
KCM_TABLET
CATEGORY_NAME
kcm_tablet
DEFAULT_SEVERITY
Critical
DESCRIPTION
"KCM for tablet input"
EXPORT
kcm_tablet
)
ecm_qt_install_logging_categories(
EXPORT kcm_tablet
DESTINATION "${KDE_INSTALL_LOGGINGCATEGORIESDIR}"
)
qt_add_dbus_interface(common_SRCS "${KWIN_INPUTDEVICE_INTERFACE}" InputDevice_interface)
kcoreaddons_add_plugin(kcm_tablet INSTALL_NAMESPACE "kcms")
target_sources(kcm_tablet PRIVATE
${common_SRCS}
kcmtablet.cpp
devicesmodel.cpp
inputdevice.cpp
)
target_link_libraries(kcm_tablet
KF5::CoreAddons
KF5::ConfigCore
KF5::I18n
KF5::QuickAddons
Qt::DBus
)
install(FILES kcm_tablet.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR})
kcoreaddons_desktop_to_json(kcm_tablet "kcm_tablet.desktop")
kpackage_install_package(package kcm_tablet kcms)
#! /usr/bin/env bash
$EXTRACTRC `find -name \*.ui` >> rc.cpp || exit 11
$XGETTEXT `find . -name "*.cpp" -o -name "*.qml"` -o $podir/kcm_tablet.pot
rm -f rc.cpp
/*
SPDX-FileCopyrightText: 2018 Roman Gilg <subdiff@gmail.com>
SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "devicesmodel.h"
#include "inputdevice.h"
#include <QDBusInterface>
#include "logging.h"
DevicesModel *DevicesModel::self()
{
static DevicesModel s_self;
return &s_self;
}
DevicesModel::DevicesModel(QObject *parent)
: QAbstractListModel(parent)
{
m_deviceManager = new QDBusInterface(QStringLiteral("org.kde.KWin"),
QStringLiteral("/org/kde/KWin/InputDevice"),
QStringLiteral("org.kde.KWin.InputDeviceManager"),
QDBusConnection::sessionBus(),
this);
resetModel();
m_deviceManager->connection().connect(QStringLiteral("org.kde.KWin"),
QStringLiteral("/org/kde/KWin/InputDevice"),
QStringLiteral("org.kde.KWin.InputDeviceManager"),
QStringLiteral("deviceAdded"),
this,
SLOT(onDeviceAdded(QString)));
m_deviceManager->connection().connect(QStringLiteral("org.kde.KWin"),
QStringLiteral("/org/kde/KWin/InputDevice"),
QStringLiteral("org.kde.KWin.InputDeviceManager"),
QStringLiteral("deviceRemoved"),
this,
SLOT(onDeviceRemoved(QString)));
}
void DevicesModel::resetModel()
{
beginResetModel();
qDeleteAll(m_devices);
m_devices.clear();
QStringList devicesSysNames;
const QVariant reply = m_deviceManager->property("devicesSysNames");
if (reply.isValid()) {
qCDebug(KCM_TABLET) << "Devices list received successfully from KWin.";
devicesSysNames = reply.toStringList();
} else {
qCCritical(KCM_TABLET) << "Error on receiving device list from KWin.";
return;
}
for (const QString &sysname : devicesSysNames) {
addDevice(sysname, false);
}
endResetModel();
}
QVariant DevicesModel::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid) || index.column() != 0)
return {};
switch (role) {
case Qt::DisplayRole:
return m_devices.at(index.row())->name();
case Qt::UserRole:
return QVariant::fromValue<QObject *>(m_devices.at(index.row()));
}
return {};
}
int DevicesModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : m_devices.count();
}
void DevicesModel::onDeviceAdded(const QString &sysName)
{
if (std::any_of(m_devices.constBegin(), m_devices.constEnd(), [sysName](InputDevice *t) {
return t->sysName() == sysName;
})) {
return;
}
addDevice(sysName, true);
}
void DevicesModel::addDevice(const QString &sysName, bool tellModel)
{
QDBusInterface deviceIface(QStringLiteral("org.kde.KWin"),
QStringLiteral("/org/kde/KWin/InputDevice/") + sysName,
QStringLiteral("org.kde.KWin.InputDevice"),
QDBusConnection::sessionBus(),
this);
QVariant reply = deviceIface.property("tabletTool");
if (reply.isValid() && reply.toBool()) {
InputDevice *dev = new InputDevice(sysName);
connect(dev, &InputDevice::needsSaveChanged, this, &DevicesModel::needsSaveChanged);
if (tellModel) {
beginInsertRows({}, m_devices.count(), m_devices.count());
}
m_devices.append(dev);
qCDebug(KCM_TABLET).nospace() << "Device connected: " << dev->name() << " (" << dev->sysName() << ")";
if (tellModel) {
endInsertRows();
}
}
}
void DevicesModel::onDeviceRemoved(const QString &sysName)
{
QVector<InputDevice *>::const_iterator it = std::find_if(m_devices.constBegin(), m_devices.constEnd(), [sysName](InputDevice *t) {
return t->sysName() == sysName;
});
if (it == m_devices.cend()) {
return;
}
InputDevice *dev = static_cast<InputDevice *>(*it);
qCDebug(KCM_TABLET).nospace() << "Device disconnected: " << dev->name() << " (" << dev->sysName() << ")";
int index = m_devices.indexOf(*it);
beginRemoveRows({}, index, index);
m_devices.removeAt(index);
endInsertRows();
}
InputDevice *DevicesModel::deviceAt(int row) const
{
return m_devices.value(row, nullptr);
}
QHash<int, QByteArray> DevicesModel::roleNames() const
{
return {
{Qt::DisplayRole, "display"},
};
}
void DevicesModel::defaults()
{
for (auto device : qAsConst(m_devices)) {
device->defaults();
}
}
void DevicesModel::load()
{
for (auto device : qAsConst(m_devices)) {
device->load();
}
}
void DevicesModel::save()
{
for (auto device : qAsConst(m_devices)) {
device->save();
}
}
bool DevicesModel::isDefaults() const
{
return std::all_of(m_devices.constBegin(), m_devices.constEnd(), [](InputDevice *dev) {
return dev->isDefaults();
});
}
bool DevicesModel::isSaveNeeded() const
{
return std::any_of(m_devices.constBegin(), m_devices.constEnd(), [](InputDevice *dev) {
return dev->isSaveNeeded();
});
}
/*
SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QAbstractListModel>
class QDBusInterface;
class InputDevice;
class DevicesModel : public QAbstractListModel
{
Q_OBJECT
public:
DevicesModel(QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent) const override;
Q_SCRIPTABLE InputDevice *deviceAt(int row) const;
void load();
void save();
void defaults();
bool isSaveNeeded() const;
bool isDefaults() const;
static DevicesModel *self();
private Q_SLOTS:
void onDeviceAdded(const QString &sysName);
void onDeviceRemoved(const QString &sysName);
Q_SIGNALS:
void needsSaveChanged();
private:
void addDevice(const QString &sysname, bool tellModel);
void resetModel();
QVector<InputDevice *> m_devices;
QDBusInterface *m_deviceManager;
};
/*
SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "inputdevice.h"
#include "InputDevice_interface.h"
#include <QDBusError>
#include <QDBusInterface>
#include <QVector>
#include "logging.h"
template<typename T>
bool InputDevice::Prop<T>::save()
{
if (!isSupported() || !m_value || m_prop.isConstant()) {
qCDebug(KCM_TABLET) << "skipping" << this << m_value.has_value() << isSupported() << m_prop.name();
return false;
}
auto iface = m_device->m_iface.data();
const bool ret = m_prop.write(iface, *m_value);
if (ret) {
m_configValue = *m_value;
}
return ret;
}
template<typename T>
void InputDevice::Prop<T>::set(T newVal)
{
if (!m_value) {
value();
}
Q_ASSERT(isSupported());
if (m_value != newVal) {
m_value = newVal;
if (m_changedSignalFunction) {
(m_device->*m_changedSignalFunction)();
}
}
}
template<typename T>
bool InputDevice::Prop<T>::changed() const
{
return m_value.has_value() && m_value.value() != m_configValue;
}
InputDevice::InputDevice(QString dbusName)
{
m_iface.reset(new OrgKdeKWinInputDeviceInterface(QStringLiteral("org.kde.KWin"),
QStringLiteral("/org/kde/KWin/InputDevice/") + dbusName,
QDBusConnection::sessionBus(),
this));
connect(this, &InputDevice::leftHandedChanged, this, &InputDevice::needsSaveChanged);
connect(this, &InputDevice::orientationChanged, this, &InputDevice::needsSaveChanged);
connect(this, &InputDevice::outputNameChanged, this, &InputDevice::needsSaveChanged);
}
InputDevice::~InputDevice() = default;
void InputDevice::save()
{
m_orientation.save();
m_outputName.save();
m_leftHanded.save();
}
bool InputDevice::isSaveNeeded() const
{
return m_leftHanded.changed() || m_orientation.changed() || m_outputName.changed();
}
void InputDevice::defaults()
{
m_leftHanded.resetFromDefaults();
m_orientation.resetFromDefaults();
m_outputName.resetFromDefaults();
}
bool InputDevice::isDefaults() const
{
return m_leftHanded.isDefaults() && m_orientation.isDefaults() && m_outputName.isDefaults();
}
void InputDevice::load()
{
m_orientation.resetFromSaved();
m_leftHanded.resetFromSaved();
m_outputName.resetFromSaved();
}
/*
SPDX-FileCopyrightText: 2018 Roman Gilg <subdiff@gmail.com>
SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef KWINWAYLANDDEVICE_H
#define KWINWAYLANDDEVICE_H
#include "InputDevice_interface.h"
#include <QObject>
#include <QString>
#include <optional>
class InputDevice : public QObject
{
Q_OBJECT
Q_PROPERTY(bool supportsLeftHanded READ supportsLeftHanded CONSTANT)
Q_PROPERTY(bool leftHanded READ isLeftHanded WRITE setLeftHanded NOTIFY leftHandedChanged)
Q_PROPERTY(bool supportsOrientation READ supportsOrientation CONSTANT)
Q_PROPERTY(int orientation READ orientation WRITE setOrientation NOTIFY orientationChanged)
Q_PROPERTY(QString outputName READ outputName WRITE setOutputName NOTIFY outputNameChanged)
public:
InputDevice(QString dbusName);
~InputDevice() override;
void load();
void save();
void defaults();
bool isSaveNeeded() const;
bool isDefaults() const;
QString name() const
{
return m_name.value();
}
QString sysName() const
{
return m_sysName.value();
}
bool isLeftHanded() const
{
return m_leftHanded.value();
}
bool supportsLeftHanded() const
{
return m_leftHanded.isSupported();
}
void setLeftHanded(bool set)
{
m_leftHanded.set(set);
}
bool supportsOrientation() const
{
return m_orientation.isSupported();
}
int orientation() const
{
return m_orientation.value();
}
void setOrientation(int ori)
{
m_orientation.set(ori);
}
QString outputName() const
{
return m_outputName.value();
}
void setOutputName(const QString &outputName)
{
m_outputName.set(outputName);
}
Q_SIGNALS:
void needsSaveChanged();
void leftHandedChanged();
void orientationChanged();
void outputNameChanged();
private:
template<typename T>
struct Prop {
typedef T (OrgKdeKWinInputDeviceInterface::*ValueFunction)() const;
typedef bool (OrgKdeKWinInputDeviceInterface::*SupportedFunction)() const;
typedef bool (OrgKdeKWinInputDeviceInterface::*SetterFunction)(const T &value);
typedef void (InputDevice::*ChangedSignal)();
explicit Prop(InputDevice *device, const char *propName, ValueFunction defaultValueFunction, SupportedFunction supported, ChangedSignal changedSignal)
: m_defaultValueFunction(defaultValueFunction)
, m_supportedFunction(supported)
, m_changedSignalFunction(changedSignal)
, m_device(device)
{
int idx = OrgKdeKWinInputDeviceInterface::staticMetaObject.indexOfProperty(propName);
if (idx < 0) {
qDebug() << "there is no" << propName;
}
Q_ASSERT(idx >= 0);
m_prop = OrgKdeKWinInputDeviceInterface::staticMetaObject.property(idx);
}
explicit Prop(InputDevice *device, const char *propName)
: m_defaultValueFunction(nullptr)
, m_supportedFunction(nullptr)
, m_changedSignalFunction(nullptr)
, m_device(device)
{
int idx = OrgKdeKWinInputDeviceInterface::staticMetaObject.indexOfProperty(propName);
Q_ASSERT(idx >= 0);
m_prop = OrgKdeKWinInputDeviceInterface::staticMetaObject.property(idx);
}
T value() const
{
if (!m_value.has_value()) {
auto iface = m_device->m_iface.data();
if (isSupported()) {
m_value = m_prop.read(iface).value<T>();
}
}
return m_value ? m_value.value() : T();
}
void resetFromDefaults()
{
if (isSupported()) {
set(defaultValue());
}
}
void resetFromSaved()
{
m_value = {};
value();
}
void set(T newVal);
T defaultValue() const
{
return m_defaultValueFunction ? (m_device->m_iface.data()->*m_defaultValueFunction)() : T();
}
bool changed() const;
void set(const Prop<T> &p)
{
set(p.value);
}
bool isSupported() const
{
auto iface = m_device->m_iface.data();
return !m_supportedFunction || (iface->*m_supportedFunction)();
}
bool save();
bool isDefaults() const
{
return m_value == defaultValue();
}
private:
QMetaProperty m_prop;
const ValueFunction m_defaultValueFunction;
const SupportedFunction m_supportedFunction;
const ChangedSignal m_changedSignalFunction;
InputDevice *const m_device;
T m_configValue = {};
mutable std::optional<T> m_value;
};
//
// general
Prop<QString> m_name = Prop<QString>(this, "name");
Prop<QString> m_sysName = Prop<QString>(this, "sysName");
Prop<bool> m_leftHanded = Prop<bool>(this,
"leftHanded",
&OrgKdeKWinInputDeviceInterface::leftHandedEnabledByDefault,
&OrgKdeKWinInputDeviceInterface::supportsLeftHanded,
&InputDevice::leftHandedChanged);
Prop<int> m_orientation =
Prop<int>(this, "orientationDBus", nullptr, &OrgKdeKWinInputDeviceInterface::supportsCalibrationMatrix, &InputDevice::orientationChanged);
Prop<QString> m_outputName = Prop<QString>(this, "outputName", nullptr, nullptr, &InputDevice::outputNameChanged);
QScopedPointer<OrgKdeKWinInputDeviceInterface> m_iface;
};
#endif // KWINWAYLANDDEVICE_H
[Desktop Entry]
Name=Drawing Tablet
Icon=preferences-desktop-tablet
Type=Service
X-KDE-PluginInfo-License=GPL
X-KDE-PluginInfo-Name=kcm_tablet
X-KDE-ServiceTypes=Plasma/Generic
X-KDE-FormFactors=tablet,handset,desktop
X-KDE-Keywords=drawing,tablet,wacom,pen
Categories=Qt;KDE;X-KDE-settings-hardware
X-Plasma-API=declarativeappletscript
X-Plasma-MainScript=ui/main.qml
X-KDE-Library=kcm_tablet
X-KDE-ServiceTypes=KCModule
X-KDE-ParentApp=kcontrol
X-KDE-System-Settings-Parent-Category=input-devices
X-KDE-OnlyShowOnQtPlatforms=wayland
/*
SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kcmtablet.h"
#include "devicesmodel.h"
#include "inputdevice.h"
#include <KAboutData>
#include <KLocalizedString>
#include <KPluginFactory>
#include <QGuiApplication>
#include <QScreen>
#include <QStandardItemModel>
K_PLUGIN_FACTORY_WITH_JSON(TabletFactory, "kcm_tablet.json", registerPlugin<Tablet>();)