Commit 278e5b9a authored by Aleix Pol Gonzalez's avatar Aleix Pol Gonzalez 🐧 Committed by Aleix Pol Gonzalez
Browse files

kcms/tablet: Add support for binding pad's buttons to keyboard events

Since many apps won't bind them, this way they can be used to trigger
features meant for the keyboard.
parent e67d1b9b
......@@ -42,6 +42,13 @@ find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS
)
if (QT_MAJOR_VERSION EQUAL "5")
find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS X11Extras)
find_package(Qt5 ${QT_MIN_VERSION} CONFIG OPTIONAL_COMPONENTS WaylandClient)
find_package(QtWaylandScanner)
set_package_properties(QtWaylandScanner PROPERTIES
TYPE REQUIRED
PURPOSE "Required for building KWin with Wayland support"
)
endif()
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS
......@@ -114,6 +121,18 @@ set_package_properties(KF5QQC2DesktopStyle PROPERTIES
TYPE RUNTIME
)
find_package(WaylandProtocols 1.25)
set_package_properties(WaylandProtocols PROPERTIES
TYPE REQUIRED
PURPOSE "Collection of Wayland protocols that add functionality not available in the Wayland core protocol"
URL "https://gitlab.freedesktop.org/wayland/wayland-protocols/"
)
find_package(Wayland 1.2)
set_package_properties(Wayland PROPERTIES
TYPE REQUIRED
PURPOSE "Required for building Tablet input KCM"
)
find_package(LibKWorkspace ${PROJECT_VERSION} CONFIG REQUIRED)
find_package(LibNotificationManager ${PROJECT_VERSION} CONFIG REQUIRED)
find_package(LibTaskManager ${PROJECT_VERSION} CONFIG REQUIRED)
......
......@@ -15,6 +15,15 @@ ecm_qt_declare_logging_category(common_SRCS
EXPORT
kcm_tablet
)
if (QT_MAJOR_VERSION EQUAL "5")
ecm_add_qtwayland_client_protocol(common_SRCS
PROTOCOL ${WaylandProtocols_DATADIR}/unstable/tablet/tablet-unstable-v2.xml
BASENAME tablet-unstable-v2
)
else()
qt6_generate_wayland_protocol_client_sources(kcm_tablet FILES
${WaylandProtocols_DATADIR}/unstable/tablet/tablet-unstable-v2.xml)
endif()
ecm_qt_install_logging_categories(
EXPORT kcm_tablet
DESTINATION "${KDE_INSTALL_LOGGINGCATEGORIESDIR}"
......@@ -29,6 +38,7 @@ target_sources(kcm_tablet PRIVATE
kcmtablet.cpp
devicesmodel.cpp
inputdevice.cpp
tabletevents.cpp
)
target_link_libraries(kcm_tablet
......@@ -38,6 +48,9 @@ target_link_libraries(kcm_tablet
KF5::QuickAddons
Qt::DBus
Qt::WaylandClient
Qt::GuiPrivate
Wayland::Client
)
kpackage_install_package(package kcm_tablet kcms)
......@@ -11,8 +11,9 @@
#include "logging.h"
DevicesModel::DevicesModel(QObject *parent)
DevicesModel::DevicesModel(const QByteArray &kind, QObject *parent)
: QAbstractListModel(parent)
, m_kind(kind)
{
m_deviceManager = new QDBusInterface(QStringLiteral("org.kde.KWin"),
QStringLiteral("/org/kde/KWin/InputDevice"),
......@@ -95,9 +96,9 @@ void DevicesModel::addDevice(const QString &sysName, bool tellModel)
QStringLiteral("org.kde.KWin.InputDevice"),
QDBusConnection::sessionBus(),
this);
QVariant reply = deviceIface.property("tabletTool");
QVariant reply = deviceIface.property(m_kind);
if (reply.isValid() && reply.toBool()) {
InputDevice *dev = new InputDevice(sysName);
InputDevice *dev = new InputDevice(sysName, this);
connect(dev, &InputDevice::needsSaveChanged, this, &DevicesModel::needsSaveChanged);
if (tellModel) {
......
......@@ -14,7 +14,7 @@ class DevicesModel : public QAbstractListModel
{
Q_OBJECT
public:
DevicesModel(QObject *parent = nullptr);
DevicesModel(const QByteArray &kind, QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
QVariant data(const QModelIndex &index, int role) const override;
......@@ -40,4 +40,5 @@ private:
QVector<InputDevice *> m_devices;
QDBusInterface *m_deviceManager;
QByteArray m_kind;
};
......@@ -50,7 +50,8 @@ bool InputDevice::Prop<T>::changed() const
return m_value.has_value() && m_value.value() != m_configValue;
}
InputDevice::InputDevice(QString dbusName)
InputDevice::InputDevice(const QString &dbusName, QObject *parent)
: QObject(parent)
{
m_iface.reset(new OrgKdeKWinInputDeviceInterface(QStringLiteral("org.kde.KWin"),
QStringLiteral("/org/kde/KWin/InputDevice/") + dbusName,
......@@ -62,7 +63,9 @@ InputDevice::InputDevice(QString dbusName)
connect(this, &InputDevice::outputAreaChanged, this, &InputDevice::needsSaveChanged);
}
InputDevice::~InputDevice() = default;
InputDevice::~InputDevice()
{
}
void InputDevice::save()
{
......
......@@ -17,6 +17,8 @@ class InputDevice : public QObject
{
Q_OBJECT
Q_PROPERTY(QString sysName READ sysName CONSTANT)
Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(QSizeF size READ size CONSTANT)
Q_PROPERTY(bool supportsLeftHanded READ supportsLeftHanded CONSTANT)
Q_PROPERTY(bool leftHanded READ isLeftHanded WRITE setLeftHanded NOTIFY leftHandedChanged)
......@@ -27,7 +29,7 @@ class InputDevice : public QObject
Q_PROPERTY(QRectF outputArea READ outputArea WRITE setOutputArea NOTIFY outputAreaChanged)
public:
InputDevice(QString dbusName);
InputDevice(const QString &dbusName, QObject *parent);
~InputDevice() override;
void load();
......@@ -116,6 +118,7 @@ private:
, m_changedSignalFunction(nullptr)
, m_device(device)
{
Q_ASSERT(m_device);
int idx = OrgKdeKWinInputDeviceInterface::staticMetaObject.indexOfProperty(propName);
Q_ASSERT(idx >= 0);
m_prop = OrgKdeKWinInputDeviceInterface::staticMetaObject.property(idx);
......@@ -141,6 +144,7 @@ private:
{
m_value = {};
value();
m_configValue = m_value;
}
void set(T newVal);
......@@ -172,7 +176,7 @@ private:
const SupportedFunction m_supportedFunction;
const ChangedSignal m_changedSignalFunction;
InputDevice *const m_device;
T m_configValue = {};
mutable std::optional<T> m_configValue;
mutable std::optional<T> m_value;
};
......
......@@ -7,6 +7,9 @@
#include "kcmtablet.h"
#include "devicesmodel.h"
#include "inputdevice.h"
#include "tabletevents.h"
#include <KConfigGroup>
#include <KLocalizedString>
#include <KPluginFactory>
#include <QGuiApplication>
......@@ -120,14 +123,18 @@ public:
Tablet::Tablet(QObject *parent, const KPluginMetaData &metaData, const QVariantList &list)
: ManagedConfigModule(parent, metaData, list)
, m_devicesModel(new DevicesModel(this))
, m_toolsModel(new DevicesModel("tabletTool", this))
, m_padsModel(new DevicesModel("tabletPad", this))
{
qmlRegisterType<OutputsModel>("org.kde.plasma.tablet.kcm", 1, 0, "OutputsModel");
qmlRegisterType<OrientationsModel>("org.kde.plasma.tablet.kcm", 1, 0, "OrientationsModel");
qmlRegisterType<OutputsFittingModel>("org.kde.plasma.tablet.kcm", 1, 1, "OutputsFittingModel");
qmlRegisterType<TabletEvents>("org.kde.plasma.tablet.kcm", 1, 1, "TabletEvents");
qmlRegisterAnonymousType<InputDevice>("org.kde.plasma.tablet.kcm", 1);
connect(m_devicesModel, &DevicesModel::needsSaveChanged, this, &Tablet::refreshNeedsSave);
connect(m_toolsModel, &DevicesModel::needsSaveChanged, this, &Tablet::refreshNeedsSave);
connect(m_padsModel, &DevicesModel::needsSaveChanged, this, &Tablet::refreshNeedsSave);
connect(this, &Tablet::buttonMappingChanged, this, &Tablet::refreshNeedsSave);
}
Tablet::~Tablet() = default;
......@@ -139,30 +146,109 @@ void Tablet::refreshNeedsSave()
bool Tablet::isSaveNeeded() const
{
return m_devicesModel->isSaveNeeded();
return !m_unsavedMappings.isEmpty() || m_toolsModel->isSaveNeeded() || m_padsModel->isSaveNeeded();
}
bool Tablet::isDefaults() const
{
return m_devicesModel->isDefaults();
if (!m_unsavedMappings.isEmpty())
return false;
const auto cfg = KSharedConfig::openConfig("kcminputrc");
const auto group = cfg->group("ButtonRebinds").group("Tablet");
if (group.isValid()) {
return false;
}
return m_toolsModel->isDefaults() && m_padsModel->isDefaults();
}
void Tablet::load()
{
m_devicesModel->load();
m_toolsModel->load();
m_padsModel->load();
m_unsavedMappings.clear();
Q_EMIT buttonMappingChanged();
}
void Tablet::save()
{
m_devicesModel->save();
m_toolsModel->save();
m_padsModel->save();
const auto cfg = KSharedConfig::openConfig("kcminputrc");
auto tabletGroup = cfg->group("ButtonRebinds").group("Tablet");
for (auto it = m_unsavedMappings.cbegin(), itEnd = m_unsavedMappings.cend(); it != itEnd; ++it) {
auto group = tabletGroup.group(it.key());
for (auto itDevice = it->cbegin(), itDeviceEnd = it->cend(); itDevice != itDeviceEnd; ++itDevice) {
const auto key = itDevice->toString(QKeySequence::PortableText);
const auto button = QString::number(itDevice.key());
if (key.isEmpty()) {
group.deleteEntry(button, KConfig::Notify);
} else {
group.writeEntry(button, QStringList{"Key", key}, KConfig::Notify);
}
}
}
tabletGroup.sync();
m_unsavedMappings.clear();
}
void Tablet::defaults()
{
m_devicesModel->defaults();
m_toolsModel->defaults();
m_padsModel->defaults();
const auto tabletGroup = KSharedConfig::openConfig("kcminputrc")->group("ButtonRebinds").group("Tablet");
const auto tablets = tabletGroup.groupList();
for (const auto &tablet : tablets) {
const auto buttons = tabletGroup.group(tablet).keyList();
for (const auto &button : buttons) {
m_unsavedMappings[tablet][button.toUInt()] = {};
}
}
for (auto it = m_unsavedMappings.begin(), itEnd = m_unsavedMappings.end(); it != itEnd; ++it) {
for (auto itDevice = it->begin(), itDeviceEnd = it->end(); itDevice != itDeviceEnd; ++itDevice) {
*itDevice = {};
}
}
Q_EMIT buttonMappingChanged();
}
void Tablet::assignPadButtonMapping(const QString &deviceName, uint button, const QKeySequence &keySequence)
{
m_unsavedMappings[deviceName][button] = keySequence;
Q_EMIT buttonMappingChanged();
}
QKeySequence Tablet::padButtonMapping(const QString &deviceName, uint button) const
{
if (deviceName.isEmpty()) {
return {};
}
if (const auto &device = m_unsavedMappings[deviceName]; device.contains(button)) {
return device.value(button);
}
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());
if (sequence.size() != 2) {
return {};
}
return QKeySequence(sequence.constLast());
}
DevicesModel *Tablet::toolsModel() const
{
return m_toolsModel;
}
DevicesModel *Tablet::devicesModel() const
DevicesModel *Tablet::padsModel() const
{
return m_devicesModel;
return m_padsModel;
}
#include "kcmtablet.moc"
......@@ -9,6 +9,7 @@
#include <KQuickAddons/ManagedConfigModule>
#include <KSharedConfig>
#include <QKeySequence>
#include "devicesmodel.h"
......@@ -18,7 +19,8 @@ class TabletData;
class Tablet : public KQuickAddons::ManagedConfigModule
{
Q_OBJECT
Q_PROPERTY(DevicesModel *devicesModel READ devicesModel CONSTANT)
Q_PROPERTY(DevicesModel *toolsModel READ toolsModel CONSTANT)
Q_PROPERTY(DevicesModel *padsModel READ padsModel CONSTANT)
public:
explicit Tablet(QObject *parent, const KPluginMetaData &metaData, const QVariantList &list);
......@@ -30,10 +32,19 @@ public:
bool isSaveNeeded() const override;
bool isDefaults() const override;
DevicesModel *devicesModel() const;
DevicesModel *toolsModel() const;
DevicesModel *padsModel() const;
Q_SCRIPTABLE void assignPadButtonMapping(const QString &deviceName, uint button, const QKeySequence &keySequence);
Q_SCRIPTABLE QKeySequence padButtonMapping(const QString &deviceName, uint button) const;
Q_SIGNALS:
void buttonMappingChanged();
private:
void refreshNeedsSave();
DevicesModel *m_devicesModel;
DevicesModel *const m_toolsModel;
DevicesModel *const m_padsModel;
QHash<QString, QHash<uint, QKeySequence>> m_unsavedMappings;
};
/*
SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-FileCopyrightText: 2022 David Redondo <kde@david-redondo.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.6 as Kirigami
import org.kde.plasma.tablet.kcm 1.1
import org.kde.kcm 1.3
import org.kde.kquickcontrols 2.0
Kirigami.FormLayout
{
id: root
required property string path
required property string name
TabletEvents {
id: tabletEvents
anchors.fill: parent
onPadButtonsChanged: {
if (!path.endsWith(root.path)) {
return;
}
buttonsRepeater.model = buttonCount
}
}
Repeater {
id: buttonsRepeater
model: tabletEvents.padButtons
delegate: RowLayout {
Layout.fillWidth: true
QQC2.Label {
id: buttonLabel
text: i18nd("kcmtablet", "Button %1", modelData + 1)
}
Connections {
target: tabletEvents
function onPadButtonReceived(path, button, pressed) {
if (button !== modelData || !path.endsWith(root.path)) {
return;
}
buttonLabel.font.bold = pressed
}
}
KeySequenceItem {
id: seq
keySequence: kcm.padButtonMapping(root.name, modelData)
Connections {
target: kcm
function onButtonMappingChanged() {
seq.keySequence = kcm.padButtonMapping(root.name, modelData)
}
}
modifierlessAllowed: true
multiKeyShortcutsAllowed: false
checkForConflictsAgainst: ShortcutType.None
onCaptureFinished: {
kcm.assignPadButtonMapping(root.name, modelData, keySequence)
}
}
}
}
}
......@@ -7,7 +7,7 @@
import QtQuick 2.15
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.3 as QQC2
import org.kde.kirigami 2.6 as Kirigami
import org.kde.kirigami 2.19 as Kirigami
import org.kde.plasma.tablet.kcm 1.1
import org.kde.kcm 1.3
......@@ -33,10 +33,14 @@ SimpleKCM {
QQC2.ComboBox {
id: combo
Kirigami.FormData.label: i18ndc("kcmtablet", "@label:listbox The device we are configuring", "Device:")
model: kcm.devicesModel
model: kcm.toolsModel
onCountChanged: if (count > 0 && currentIndex < 0) {
currentIndex = 0;
}
onCurrentIndexChanged: {
parent.device = kcm.devicesModel.deviceAt(combo.currentIndex)
parent.device = kcm.toolsModel.deviceAt(combo.currentIndex)
const initialOutputArea = form.device.outputArea;
if (initialOutputArea === Qt.rect(0, 0, 1, 1)) {
......@@ -202,5 +206,29 @@ SimpleKCM {
, Math.floor(outputAreaView.outputAreaSetting.width * outputItem.outputSize.width)
, Math.floor(outputAreaView.outputAreaSetting.height * outputItem.outputSize.height))
}
Kirigami.Separator {
Layout.fillWidth: true
}
property QtObject padDevice: null
QQC2.ComboBox {
Kirigami.FormData.label: i18ndc("kcmtablet", "@label:listbox The pad we are configuring", "Pad:")
model: kcm.padsModel
onCurrentIndexChanged: {
parent.padDevice = kcm.padsModel.deviceAt(currentIndex)
}
onCountChanged: if (count > 0 && currentIndex < 0) {
currentIndex = 0;
}
}
RebindButtons {
Layout.fillWidth: true
path: form.padDevice.sysName
name: form.padDevice.name
}
}
}
/*
SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "tabletevents.h"
#include "qwayland-tablet-unstable-v2.h"
#include <QQuickWindow>
#include <QWaylandClientExtensionTemplate>
#include <qguiapplication.h>
#include <qpa/qplatformnativeinterface.h>
class TabletPad : public QObject, public QtWayland::zwp_tablet_pad_v2
{
public:
TabletPad(TabletEvents *events, ::zwp_tablet_pad_v2 *t)
: QObject(events)
, QtWayland::zwp_tablet_pad_v2(t)
, m_events(events)
{
}
void zwp_tablet_pad_v2_path(const QString &path) override
{
m_path = path;
}
void zwp_tablet_pad_v2_buttons(uint32_t buttons) override
{
m_buttons = buttons;
}
void zwp_tablet_pad_v2_done() override
{
m_events->padButtonsChanged(m_path, m_buttons);
}
void zwp_tablet_pad_v2_button(uint32_t /*time*/, uint32_t button, uint32_t state) override
{
Q_EMIT m_events->padButtonReceived(m_path, button, state);
}
TabletEvents *const m_events;
QString m_path;
uint m_buttons = 0;
};
class Tool : public QObject, public QtWayland::zwp_tablet_tool_v2
{
public:
Tool(TabletEvents *const events, ::zwp_tablet_tool_v2 *t)
: QObject(events)
, QtWayland::zwp_tablet_tool_v2(t)
, m_events(events)
{
}
void zwp_tablet_tool_v2_hardware_serial(uint32_t hardware_serial_hi, uint32_t hardware_serial_lo) override
{
m_hardware_serial_hi = hardware_serial_hi;
m_hardware_serial_lo = hardware_serial_lo;
}
void zwp_tablet_tool_v2_button(uint32_t /*serial*/, uint32_t button, uint32_t state) override
{
Q_EMIT m_events->toolButtonReceived(m_hardware_serial_hi, m_hardware_serial_lo, button, state);
}
uint32_t m_hardware_serial_hi = 0;
uint32_t m_hardware_serial_lo = 0;
TabletEvents *const m_events;
};
class TabletManager : public QWaylandClientExtensionTemplate<TabletManager>, public QtWayland::zwp_tablet_manager_v2
{
public:
TabletManager(TabletEvents *q)
: QWaylandClientExtensionTemplate<TabletManager>(ZWP_TABLET_MANAGER_V2_GET_TABLET_SEAT_SINCE_VERSION)
, q(q)
{
setParent(q);
#if QTWAYLANDCLIENT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
initialize();
#else
// QWaylandClientExtensionTemplate invokes this with a QueuedConnection but we want it called immediately
QMetaObject::invokeMethod(this, "addRegistryListener", Qt::DirectConnection);
#endif
Q_ASSERT(isInitialized());
}
TabletEvents *const q;
};
class TabletSeat : public QObject, public QtWayland::zwp_tablet_seat_v2
{
public:
TabletSeat(TabletEvents *events, ::zwp_tablet_seat_v2 *seat)
: QObject(events)
, QtWayland::zwp_tablet_seat_v2(seat)
, m_events(events)
{
}
void zwp_tablet_seat_v2_tool_added(struct ::zwp_tablet_tool_v2 *id) override
{
new Tool(m_events, id);
}
void zwp_tablet_seat_v2_pad_added(struct ::zwp_tablet_pad_v2 *id) override
{
new TabletPad(m_events, id);
}
TabletEvents *const m_events;
};
TabletEvents::TabletEvents(QQuickItem *parent)
: QQuickItem(parent)
{
auto seat = static_cast<wl_seat *>(QGuiApplication::platformNativeInterface()->nativeResourceForIntegration("wl_seat"));
auto tabletClient = new TabletManager(this);
auto _seat = tabletClient->get_tablet_seat(seat);