SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kcmtablet.h"
#include "calibrationtool.h"
#include "inputdevice.h"
#include "logging.h"
#include "tabletevents.h"
#include "tabletmoduledata.h"
#include <KConfigGroup>
#include <KLocalizedString>
#include <KPluginFactory>
#include <QGuiApplication>
#include <QMatrix4x4>
#include <QScreen>
#include <QStandardItemModel>
K_PLUGIN_FACTORY_WITH_JSON(TabletFactory, "kcm_tablet.json", registerPlugin<Tablet>(); registerPlugin<TabletModuleData>();)
class OrientationsModel : public QStandardItemModel
{
Q_OBJECT
public:
OrientationsModel()
{
auto addOrientation = [this](const QString &display, Qt::ScreenOrientation o) {
auto item = new QStandardItem(display);
item->setData(o, Qt::UserRole);
appendRow(item);
};
addOrientation(i18n("Default"), Qt::PrimaryOrientation);
addOrientation(i18n("Portrait"), Qt::PortraitOrientation);
addOrientation(i18n("Landscape"), Qt::LandscapeOrientation);
addOrientation(i18n("Inverted Portrait"), Qt::InvertedPortraitOrientation);
addOrientation(i18n("Inverted Landscape"), Qt::InvertedLandscapeOrientation);
setItemRoleNames({
{Qt::DisplayRole, "display"},
{Qt::UserRole, "value"},
});
}
Q_SCRIPTABLE int orientationAt(int row) const
{
return item(row)->data(Qt::UserRole).toInt();
}
Q_SCRIPTABLE int rowForOrientation(int orientation)
{
for (int i = 0, c = rowCount(); i < c; ++i) {
if (item(i)->data(Qt::UserRole) == orientation) {
return i;
}
}
return 0;
}
};
class OutputsModel : public QStandardItemModel
{
Q_OBJECT
public:
OutputsModel()
{
setItemRoleNames({
{Qt::DisplayRole, "display"},
{Qt::UserRole, "name"},
{Qt::UserRole + 1, "physicalSize"},
{Qt::UserRole + 2, "size"},
});
reset();
connect(qGuiApp, &QGuiApplication::screenAdded, this, &OutputsModel::reset);
connect(qGuiApp, &QGuiApplication::screenRemoved, this, &OutputsModel::reset);
}
void reset()
{
clear();
const auto screens = qGuiApp->screens();
auto it = new QStandardItem(i18n("Follow the Current Screen"));
it->setData(screens[0]->physicalSize(), Qt::UserRole + 1);
it->setData(screens[0]->size(), Qt::UserRole + 2);
appendRow(it);
it = new QStandardItem(i18n("All Screens"));
it->setData(screens[0]->virtualSize(), Qt::UserRole + 1);
it->setData(screens[0]->virtualSize(), Qt::UserRole + 2);
appendRow(it);
for (const auto screen : screens) {
auto geo = screen->geometry();
auto name = screen->model().isEmpty() ? screen->name() : screen->model();
auto it = new QStandardItem(i18nc("model - (x,y widthxheight)",
"%1 - (%2,%3 %4×%5)",
name,
QString::number(geo.x()),
QString::number(geo.y()),
QString::number(geo.width()),
QString::number(geo.height())));
it->setData(screen->name(), Qt::UserRole);
it->setData(screen->physicalSize(), Qt::UserRole + 1);
it->setData(screen->size(), Qt::UserRole + 2);
appendRow(it);
}
setItemRoleNames({
{Qt::DisplayRole, "display"},
{Qt::UserRole, "name"},
{Qt::UserRole + 1, "physicalSize"},
{Qt::UserRole + 2, "size"},
});
}
Q_SCRIPTABLE QString outputNameAt(int row) const
{
return item(row)->data(Qt::UserRole).toString();
}
Q_SCRIPTABLE int rowForDevice(KWinDevices::InputDevice *device)
{
if (!device) {
return 0;
} else if (device->isMapToWorkspace()) {
return 1;
} else {
return rowForOutputName(device->outputName());
}
}
Q_SCRIPTABLE int rowForOutputName(const QString &outputName)
{
for (int i = 0, c = rowCount(); i < c; ++i) {
if (item(i)->data(Qt::UserRole) == outputName) {
return i;
}
}
return 0;
}
Q_SCRIPTABLE bool isMapToWorkspaceAt(int row)
{
return row == 1;
}
};
class OutputsFittingModel : public QStandardItemModel
{
public:
OutputsFittingModel()
{
appendRow(new QStandardItem(i18nc("@item:inlistbox Stretch and fill to the screen", "Stretch and Fill")));
appendRow(new QStandardItem(i18nc("@item:inlistbox Keep aspect ratio and fit within the screen", "Keep Aspect Ratio and Fit")));
appendRow(new QStandardItem(i18nc("@item:inlistbox Map to portion of the screen", "Map to Portion")));
setItemRoleNames({{Qt::DisplayRole, "display"}});
}
};
class StylusButtonsModel : public QStandardItemModel
{
Q_OBJECT
Q_PROPERTY(WacomDeviceDatabase *db READ db WRITE setDb NOTIFY dbChanged REQUIRED)
Q_PROPERTY(KWinDevices::InputDevice *device READ device WRITE setDevice NOTIFY deviceChanged REQUIRED)
public:
enum CustomRoles {
NameRole = Qt::UserRole,
LabelRole,
ValueRole,
};
StylusButtonsModel()
{
setItemRoleNames({
{NameRole, "name"},
{LabelRole, "label"},
{ValueRole, "value"},
});
recalculateItems();
}
WacomDeviceDatabase *db() const
{
return m_db;
}
void setDb(WacomDeviceDatabase *db)
{
if (db != m_db) {
m_db = db;
Q_EMIT dbChanged();
recalculateItems();
}
}
KWinDevices::InputDevice *device() const
{
return m_device;
}
void setDevice(KWinDevices::InputDevice *device)
{
if (m_device != device) {
m_device = device;
Q_EMIT deviceChanged();
recalculateItems();
}
}
Q_SIGNALS:
void dbChanged();
void deviceChanged();
private:
void recalculateItems()
{
if (!m_device || !m_db) {
return;
}
int numButtons = 3;
const QString sysPath = QStringLiteral("/dev/input/%1").arg(m_device->sysName());
WacomError *error = libwacom_error_new();
auto device = libwacom_new_from_path(m_db, sysPath.toLatin1().constData(), WFALLBACK_GENERIC, error);
if (device == nullptr) {
qCWarning(KCM_TABLET()) << "Failed to find device in libwacom:" << libwacom_error_get_message(error);
} else {
int num_styli = 0;
const int *styli = libwacom_get_supported_styli(device, &num_styli);
if (num_styli > 0) {
numButtons = 0;
for (int i = 0; i < num_styli; i++) {
const auto stylus = libwacom_stylus_get_for_id(m_db, styli[i]);
if (stylus != nullptr) {
numButtons = std::max(numButtons, libwacom_stylus_get_num_buttons(stylus));
}
}
}
}
libwacom_error_free(&error);
if (numButtons > 3) {
numButtons = 3;
qCWarning(KCM_TABLET)
<< "More than 3 stylus buttons detected! This is currently not supported. If you have such a stylus, please file a bug report.";
}
clear();
for (int i = 0; i < numButtons; i++) {
auto item = new QStandardItem();
item->setData(i18nc("Stylus pen button", "Pen button %1:", i + 1), LabelRole);
item->setData(i18nc("@info Meant to be inserted into an existing sentence like 'configuring pen button X'", "pen button %1", i + 1), NameRole);
switch (i) {
case 0:
item->setData(0x14b, ValueRole);
break;
case 1:
item->setData(0x14c, ValueRole);
break;
case 2:
item->setData(0x149, ValueRole);
break;
default:
Q_UNREACHABLE();
}
appendRow(item);
}
}
WacomDeviceDatabase *m_db = nullptr;
KWinDevices::InputDevice *m_device = nullptr;
};
Tablet::Tablet(QObject *parent, const KPluginMetaData &metaData)
: KQuickManagedConfigModule(parent, metaData)
{
m_db = libwacom_database_new();
if (m_db == nullptr) {
qCWarning(KCM_TABLET) << "Failed to initialize libwacom database!";
}
m_tabletsModel = new TabletsModel(m_db, this);
qmlRegisterType<OutputsModel>("org.kde.plasma.tablet.kcm", 1, 0, "OutputsModel");
qmlRegisterType<OrientationsModel>("org.kde.plasma.tablet.kcm", 1, 0, "OrientationsModel");
qmlRegisterType<StylusButtonsModel>("org.kde.plasma.tablet.kcm", 1, 1, "StylusButtonsModel");
qmlRegisterType<TabletEvents>("org.kde.plasma.tablet.kcm", 1, 1, "TabletEvents");
qmlRegisterUncreatableType<KWinDevices::InputDevice>("org.kde.plasma.tablet.kcm", 1, 0, "InputDevice", "Access from C++ only");
qmlRegisterType<CalibrationTool>("org.kde.plasma.tablet.kcm", 1, 1, "CalibrationTool");
qmlRegisterType<InputSequence>("org.kde.plasma.tablet.kcm", 1, 1, "inputSequence");
qmlRegisterUncreatableMetaObject(InputSequence::staticMetaObject, "org.kde.plasma.tablet.kcm", 1, 1, "InputSequence", "Access to enums & flags only");
connect(m_tabletsModel, &TabletsModel::needsSaveChanged, this, &Tablet::refreshNeedsSave);
connect(this, &Tablet::settingsRestored, this, &Tablet::refreshNeedsSave);
}
Tablet::~Tablet()
{
if (m_db) {
libwacom_database_destroy(m_db);
}
}
void Tablet::refreshNeedsSave()
{
setNeedsSave(isSaveNeeded());
}
bool Tablet::isSaveNeeded() const
{
const auto cfg = KSharedConfig::openConfig("kcminputrc");
const KConfigGroup tabletGroup = cfg->group(QStringLiteral("Tablet"));
return !m_unsavedMappings.isEmpty() || m_tabletsModel->isSaveNeeded() || tabletGroup.readEntry(QStringLiteral("SyncWithMouse"), false) != m_syncWithMouse;
}
bool Tablet::isDefaults() const
{
if (!m_unsavedMappings.isEmpty())
return false;
const auto cfg = KSharedConfig::openConfig("kcminputrc");
if (cfg->group("ButtonRebinds").group("Tablet").isValid()) {
return false;
}
if (cfg->group("ButtonRebinds").group("TabletTool").isValid()) {
return false;
}
return m_tabletsModel->isDefaults();
}
void Tablet::load()
{
m_tabletsModel->load();
m_unsavedMappings.clear();
Q_EMIT settingsRestored();
const auto cfg = KSharedConfig::openConfig("kcminputrc");
const KConfigGroup tabletGroup = cfg->group(QStringLiteral("Tablet"));
m_syncWithMouse = tabletGroup.readEntry(QStringLiteral("SyncWithMouse"), false);
Q_EMIT syncWithMouseChanged();
refreshNeedsSave();
}
void Tablet::save()
{
m_tabletsModel->save();
auto generalGroup = KSharedConfig::openConfig("kcminputrc")->group("ButtonRebinds");
for (const auto &device : std::to_array<QString>({"Tablet", "TabletTool", "TabletDial"})) {
for (auto it = m_unsavedMappings[device].cbegin(), itEnd = m_unsavedMappings[device].cend(); it != itEnd; ++it) {
auto configGroup = generalGroup.group(device).group(it.key());
for (auto itDevice = it->cbegin(), itDeviceEnd = it->cend(); itDevice != itDeviceEnd; ++itDevice) {
const auto [button, group] = itDevice.key();
const auto key = itDevice->toConfigFormat();
if (key.isEmpty()) {
configGroup.deleteEntry(QString::number(button), KConfig::Notify);
} else {
configGroup.writeEntry(QString::number(button), key, KConfig::Notify);
}
}
}
}
for (const auto &device : std::to_array<QString>({"TabletRing"})) {
for (auto it = m_unsavedMappings[device].cbegin(), itEnd = m_unsavedMappings[device].cend(); it != itEnd; ++it) {
auto configGroup = generalGroup.group(device).group(it.key());
for (auto itDevice = it->cbegin(), itDeviceEnd = it->cend(); itDevice != itDeviceEnd; ++itDevice) {
const auto [button, mode] = itDevice.key();
auto modeGroup = configGroup.group(QString::number(mode));
const auto key = itDevice->toConfigFormat();
if (key.isEmpty()) {
modeGroup.deleteEntry(QString::number(button), KConfig::Notify);
} else {
modeGroup.writeEntry(QString::number(button), key, KConfig::Notify);
}
}
}
}
generalGroup.sync();
m_unsavedMappings.clear();
const auto cfg = KSharedConfig::openConfig("kcminputrc");
KConfigGroup tabletGroup = cfg->group(QStringLiteral("Tablet"));
tabletGroup.writeEntry(QStringLiteral("SyncWithMouse"), m_syncWithMouse, KConfig::Notify);
cfg->sync();
}
void Tablet::defaults()
{
m_tabletsModel->defaults();
m_unsavedMappings.clear();
m_syncWithMouse = false;
Q_EMIT settingsRestored();
Q_EMIT syncWithMouseChanged();
}
void Tablet::assignPadButtonMapping(const QString &deviceName, uint button, const InputSequence &keySequence)
{
m_unsavedMappings["Tablet"][deviceName][{button, 0}] = keySequence;
Q_EMIT settingsRestored();
}
void Tablet::assignPadDialMapping(const QString &deviceName, uint button, const InputSequence &keySequence)
{
m_unsavedMappings["TabletDial"][deviceName][{button, 0}] = keySequence;
Q_EMIT settingsRestored();
}
void Tablet::assignPadRingMapping(const QString &deviceName, uint button, uint group, const InputSequence &keySequence)
{
m_unsavedMappings["TabletRing"][deviceName][{button, group}] = keySequence;
Q_EMIT settingsRestored();
}
void Tablet::assignToolButtonMapping(const QString &deviceName, uint button, const InputSequence &keySequence)
{
m_unsavedMappings["TabletTool"][deviceName][{button, 0}] = keySequence;
Q_EMIT settingsRestored();
}
InputSequence Tablet::padButtonMapping(const QString &deviceName, uint button) const
{
if (deviceName.isEmpty()) {
return {};
}
if (const auto &device = m_unsavedMappings["Tablet"][deviceName]; device.contains({button, 0})) {
return device.value({button, 0});
}
const auto cfg = KSharedConfig::openConfig("kcminputrc");
const auto group = cfg->group("ButtonRebinds").group("Tablet").group(deviceName);
const auto sequence = group.readEntry(QString::number(button), QStringList());
return InputSequence(sequence);
}
InputSequence Tablet::padDialMapping(const QString &deviceName, uint button) const
{
if (deviceName.isEmpty()) {
return {};
}
if (const auto &device = m_unsavedMappings["TabletDial"][deviceName]; device.contains({button, 0})) {
return device.value({button, 0});
}
const auto cfg = KSharedConfig::openConfig("kcminputrc");
const auto group = cfg->group("ButtonRebinds").group("TabletDial").group(deviceName);
const auto sequence = group.readEntry(QString::number(button), QStringList());
return InputSequence(sequence);
}
InputSequence Tablet::padRingMapping(const QString &deviceName, uint button, uint mode) const
{
if (deviceName.isEmpty()) {
return {};
}
if (const auto &device = m_unsavedMappings["TabletRing"][deviceName]; device.contains({button, mode})) {
return device.value({button, mode});
}
const auto cfg = KSharedConfig::openConfig("kcminputrc");
const auto configGroup = cfg->group("ButtonRebinds").group("TabletRing").group(deviceName).group(QString::number(mode));
const auto sequence = configGroup.readEntry(QString::number(button), QStringList());
return InputSequence(sequence);
}
InputSequence Tablet::toolButtonMapping(const QString &deviceName, uint button) const
{
if (deviceName.isEmpty()) {
return {};
}
if (const auto &device = m_unsavedMappings["TabletTool"][deviceName]; device.contains({button, 0})) {
return device.value({button, 0});
}
const auto cfg = KSharedConfig::openConfig("kcminputrc");
const auto group = cfg->group("ButtonRebinds").group("TabletTool").group(deviceName);
const auto sequence = group.readEntry(QString::number(button), QStringList());
return InputSequence(sequence);
}
QString Tablet::toSerializedCurve(const QPointF &controlPoint1, const QPointF &controlPoint2)
{
QString sCurve;
sCurve += QString::number(controlPoint1.x());
sCurve += ",";
sCurve += QString::number(controlPoint1.y());
sCurve += ";";
sCurve += QString::number(controlPoint2.x());
sCurve += ",";
sCurve += QString::number(controlPoint2.y());
sCurve += ";";
return sCurve;
}
QList<QPointF> Tablet::fromSerializedCurve(const QString &curve)
{
const QStringList data = curve.split(';');
QList<QPointF> points;
for (const QString &pair : data) {
if (pair.indexOf(',') > -1) {
points.append({pair.section(',', 0, 0).toDouble(), pair.section(',', 1, 1).toDouble()});
}
}
return points;
}
WacomDeviceDatabase *Tablet::db() const
{
return m_db;
}
bool Tablet::syncWithMouse() const
{
return m_syncWithMouse;
}
void Tablet::setSyncWithMouse(bool value)
{
if (m_syncWithMouse != value) {
m_syncWithMouse = value;
Q_EMIT syncWithMouseChanged();
refreshNeedsSave();
}
}
TabletsModel *Tablet::tabletsModel() const
{
return m_tabletsModel;
}
#include "kcmtablet.moc"
#include "moc_kcmtablet.cpp"