You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

437 lines
16 KiB

/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "tablet_input.h"
#include "backends/libinput/device.h"
#include "cursorsource.h"
#include "decorations/decoratedclient.h"
#include "input_event.h"
#include "input_event_spy.h"
#include "osd.h"
#include "pointer_input.h"
#include "wayland/tablet_v2.h"
#include "wayland_server.h"
#include "window.h"
#include "workspace.h"
#include <KDecoration2/Decoration>
#include <KGlobalAccel>
#include <KLocalizedString>
#include <QAction>
#include <QHoverEvent>
#include <QWindow>
namespace KWin
{
class SurfaceCursor : public Cursor
{
public:
explicit SurfaceCursor(TabletToolV2Interface *tool)
: Cursor()
{
setParent(tool);
connect(tool, &TabletToolV2Interface::cursorChanged, this, [this](const TabletCursorSourceV2 &cursor) {
if (auto surfaceCursor = std::get_if<TabletSurfaceCursorV2 *>(&cursor)) {
// If the cursor is unset, fallback to the cross cursor.
if ((*surfaceCursor) && (*surfaceCursor)->enteredSerial()) {
if (!m_surfaceSource) {
m_surfaceSource = std::make_unique<SurfaceCursorSource>();
}
m_surfaceSource->update((*surfaceCursor)->surface(), (*surfaceCursor)->hotspot());
setSource(m_surfaceSource.get());
return;
}
}
QByteArray shape;
if (auto shapeCursor = std::get_if<QByteArray>(&cursor)) {
shape = *shapeCursor;
} else {
shape = QByteArrayLiteral("cross");
}
static WaylandCursorImage defaultCursor;
if (!m_shapeSource) {
m_shapeSource = std::make_unique<ShapeCursorSource>();
}
m_shapeSource->setTheme(defaultCursor.theme());
m_shapeSource->setShape(shape);
setSource(m_shapeSource.get());
});
}
private:
std::unique_ptr<ShapeCursorSource> m_shapeSource;
std::unique_ptr<SurfaceCursorSource> m_surfaceSource;
};
TabletInputRedirection::TabletInputRedirection(InputRedirection *parent)
: InputDeviceHandler(parent)
{
}
TabletInputRedirection::~TabletInputRedirection() = default;
void TabletInputRedirection::init()
{
Q_ASSERT(!inited());
setInited(true);
InputDeviceHandler::init();
connect(workspace(), &QObject::destroyed, this, [this] {
setInited(false);
});
connect(waylandServer(), &QObject::destroyed, this, [this] {
setInited(false);
});
const auto devices = input()->devices();
for (InputDevice *device : devices) {
integrateDevice(device);
}
connect(input(), &InputRedirection::deviceAdded, this, &TabletInputRedirection::integrateDevice);
connect(input(), &InputRedirection::deviceRemoved, this, &TabletInputRedirection::removeDevice);
auto tabletNextOutput = new QAction(this);
tabletNextOutput->setProperty("componentName", QStringLiteral("kwin"));
tabletNextOutput->setText(i18n("Move the tablet to the next output"));
tabletNextOutput->setObjectName(QStringLiteral("Move Tablet to Next Output"));
KGlobalAccel::setGlobalShortcut(tabletNextOutput, QList<QKeySequence>());
connect(tabletNextOutput, &QAction::triggered, this, &TabletInputRedirection::trackNextOutput);
}
static TabletSeatV2Interface *findTabletSeat()
{
auto server = waylandServer();
if (!server) {
return nullptr;
}
TabletManagerV2Interface *manager = server->tabletManagerV2();
return manager->seat(waylandServer()->seat());
}
void TabletInputRedirection::integrateDevice(InputDevice *inputDevice)
{
auto device = qobject_cast<LibInput::Device *>(inputDevice);
if (!device || (!device->isTabletTool() && !device->isTabletPad())) {
return;
}
TabletSeatV2Interface *tabletSeat = findTabletSeat();
if (!tabletSeat) {
qCCritical(KWIN_CORE) << "Could not find tablet seat";
return;
}
struct udev_device *const udev_device = libinput_device_get_udev_device(device->device());
const char *devnode = udev_device_get_syspath(udev_device);
auto deviceGroup = libinput_device_get_device_group(device->device());
auto tablet = static_cast<TabletV2Interface *>(libinput_device_group_get_user_data(deviceGroup));
if (!tablet) {
tablet = tabletSeat->addTablet(device->vendor(), device->product(), device->sysName(), device->name(), {QString::fromUtf8(devnode)});
libinput_device_group_set_user_data(deviceGroup, tablet);
}
if (device->isTabletPad()) {
const int buttonsCount = libinput_device_tablet_pad_get_num_buttons(device->device());
const int ringsCount = libinput_device_tablet_pad_get_num_rings(device->device());
const int stripsCount = libinput_device_tablet_pad_get_num_strips(device->device());
const int modes = libinput_device_tablet_pad_get_num_mode_groups(device->device());
auto firstGroup = libinput_device_tablet_pad_get_mode_group(device->device(), 0);
tabletSeat->addPad(device->sysName(), device->name(), {QString::fromUtf8(devnode)}, buttonsCount, ringsCount, stripsCount, modes, libinput_tablet_pad_mode_group_get_mode(firstGroup), tablet);
}
}
void TabletInputRedirection::removeDevice(InputDevice *inputDevice)
{
auto device = qobject_cast<LibInput::Device *>(inputDevice);
if (device) {
auto deviceGroup = libinput_device_get_device_group(device->device());
libinput_device_group_set_user_data(deviceGroup, nullptr);
TabletSeatV2Interface *tabletSeat = findTabletSeat();
if (tabletSeat) {
tabletSeat->removeDevice(device->sysName());
} else {
qCCritical(KWIN_CORE) << "Could not find tablet to remove" << device->sysName();
}
}
}
void TabletInputRedirection::trackNextOutput()
{
const auto outputs = workspace()->outputs();
if (outputs.isEmpty()) {
return;
}
int tabletToolCount = 0;
InputDevice *changedDevice = nullptr;
const auto devices = input()->devices();
for (const auto device : devices) {
if (!device->isTabletTool()) {
continue;
}
tabletToolCount++;
if (device->outputName().isEmpty()) {
device->setOutputName(outputs.constFirst()->name());
changedDevice = device;
continue;
}
auto it = std::find_if(outputs.begin(), outputs.end(), [device](const auto &output) {
return output->name() == device->outputName();
});
++it;
auto nextOutput = it == outputs.end() ? outputs.first() : *it;
device->setOutputName(nextOutput->name());
changedDevice = device;
}
const QString message = tabletToolCount == 1 ? i18n("Tablet moved to %1", changedDevice->outputName()) : i18n("Tablets switched outputs");
OSD::show(message, QStringLiteral("input-tablet"), 5000);
}
static TabletToolV2Interface::Type getType(const TabletToolId &toolId)
{
using Type = TabletToolV2Interface::Type;
switch (toolId.m_toolType) {
case InputRedirection::Pen:
return Type::Pen;
case InputRedirection::Eraser:
return Type::Eraser;
case InputRedirection::Brush:
return Type::Brush;
case InputRedirection::Pencil:
return Type::Pencil;
case InputRedirection::Airbrush:
return Type::Airbrush;
case InputRedirection::Finger:
return Type::Finger;
case InputRedirection::Mouse:
return Type::Mouse;
case InputRedirection::Lens:
return Type::Lens;
case InputRedirection::Totem:
return Type::Totem;
}
return Type::Pen;
}
TabletToolV2Interface *TabletInputRedirection::ensureTabletTool(const TabletToolId &toolId)
{
TabletSeatV2Interface *tabletSeat = findTabletSeat();
if (auto tool = tabletSeat->toolByHardwareSerial(toolId.m_serialId, getType(toolId))) {
return tool;
}
const auto f = [](InputRedirection::Capability cap) {
switch (cap) {
case InputRedirection::Tilt:
return TabletToolV2Interface::Tilt;
case InputRedirection::Pressure:
return TabletToolV2Interface::Pressure;
case InputRedirection::Distance:
return TabletToolV2Interface::Distance;
case InputRedirection::Rotation:
return TabletToolV2Interface::Rotation;
case InputRedirection::Slider:
return TabletToolV2Interface::Slider;
case InputRedirection::Wheel:
return TabletToolV2Interface::Wheel;
}
return TabletToolV2Interface::Wheel;
};
QList<TabletToolV2Interface::Capability> ifaceCapabilities;
ifaceCapabilities.resize(toolId.m_capabilities.size());
std::transform(toolId.m_capabilities.constBegin(), toolId.m_capabilities.constEnd(), ifaceCapabilities.begin(), f);
TabletToolV2Interface *tool = tabletSeat->addTool(getType(toolId), toolId.m_serialId, toolId.m_uniqueId, ifaceCapabilities, toolId.deviceSysName);
const auto cursor = new SurfaceCursor(tool);
Cursors::self()->addCursor(cursor);
m_cursorByTool[tool] = cursor;
return tool;
}
void TabletInputRedirection::tabletToolEvent(KWin::InputRedirection::TabletEventType type, const QPointF &pos,
qreal pressure, int xTilt, int yTilt, qreal rotation, bool tipDown,
bool tipNear, const TabletToolId &toolId,
std::chrono::microseconds time,
InputDevice *device)
{
if (!inited()) {
return;
}
input()->setLastInputHandler(this);
m_lastPosition = pos;
QEvent::Type t;
switch (type) {
case InputRedirection::Axis:
t = QEvent::TabletMove;
break;
case InputRedirection::Tip:
t = tipDown ? QEvent::TabletPress : QEvent::TabletRelease;
break;
case InputRedirection::Proximity:
t = tipNear ? QEvent::TabletEnterProximity : QEvent::TabletLeaveProximity;
break;
}
auto tool = ensureTabletTool(toolId);
switch (t) {
case QEvent::TabletEnterProximity:
case QEvent::TabletPress:
case QEvent::TabletMove:
m_cursorByTool[tool]->setPos(pos);
break;
default:
break;
}
update();
workspace()->setActiveOutput(pos);
const auto button = m_tipDown ? Qt::LeftButton : Qt::NoButton;
// TODO: Not correct, but it should work fine. In long term, we need to stop using QTabletEvent.
const QPointingDevice *dev = QPointingDevice::primaryPointingDevice();
TabletEvent ev(t, dev, pos, pos, pressure,
xTilt, yTilt,
0, // tangentialPressure
rotation,
0, // z
Qt::NoModifier, button, button, toolId, device);
ev.setTimestamp(std::chrono::duration_cast<std::chrono::milliseconds>(time).count());
input()->processSpies(std::bind(&InputEventSpy::tabletToolEvent, std::placeholders::_1, &ev));
input()->processFilters(
std::bind(&InputEventFilter::tabletToolEvent, std::placeholders::_1, &ev));
m_tipDown = tipDown;
m_tipNear = tipNear;
}
void KWin::TabletInputRedirection::tabletToolButtonEvent(uint button, bool isPressed, const TabletToolId &toolId, std::chrono::microseconds time, InputDevice *device)
{
TabletToolButtonEvent event{
.device = device,
.button = button,
.pressed = isPressed,
.toolId = toolId,
.time = time,
};
input()->processSpies(std::bind(&InputEventSpy::tabletToolButtonEvent, std::placeholders::_1, &event));
input()->processFilters(std::bind(&InputEventFilter::tabletToolButtonEvent, std::placeholders::_1, &event));
input()->setLastInputHandler(this);
}
void KWin::TabletInputRedirection::tabletPadButtonEvent(uint button, bool isPressed, std::chrono::microseconds time, InputDevice *device)
{
TabletPadButtonEvent event{
.device = device,
.button = button,
.pressed = isPressed,
.time = time,
};
input()->processSpies(std::bind(&InputEventSpy::tabletPadButtonEvent, std::placeholders::_1, &event));
input()->processFilters(std::bind(&InputEventFilter::tabletPadButtonEvent, std::placeholders::_1, &event));
input()->setLastInputHandler(this);
}
void KWin::TabletInputRedirection::tabletPadStripEvent(int number, int position, bool isFinger, std::chrono::microseconds time, InputDevice *device)
{
TabletPadStripEvent event{
.device = device,
.number = number,
.position = position,
.isFinger = isFinger,
.time = time,
};
input()->processSpies(std::bind(&InputEventSpy::tabletPadStripEvent, std::placeholders::_1, &event));
input()->processFilters(std::bind(&InputEventFilter::tabletPadStripEvent, std::placeholders::_1, &event));
input()->setLastInputHandler(this);
}
void KWin::TabletInputRedirection::tabletPadRingEvent(int number, int position, bool isFinger, std::chrono::microseconds time, InputDevice *device)
{
TabletPadRingEvent event{
.device = device,
.number = number,
.position = position,
.isFinger = isFinger,
.time = time,
};
input()->processSpies(std::bind(&InputEventSpy::tabletPadRingEvent, std::placeholders::_1, &event));
input()->processFilters(std::bind(&InputEventFilter::tabletPadRingEvent, std::placeholders::_1, &event));
input()->setLastInputHandler(this);
}
bool TabletInputRedirection::focusUpdatesBlocked()
{
return input()->isSelectingWindow();
}
void TabletInputRedirection::cleanupDecoration(Decoration::DecoratedClientImpl *old,
Decoration::DecoratedClientImpl *now)
{
disconnect(m_decorationGeometryConnection);
m_decorationGeometryConnection = QMetaObject::Connection();
disconnect(m_decorationDestroyedConnection);
m_decorationDestroyedConnection = QMetaObject::Connection();
if (old) {
// send leave event to old decoration
QHoverEvent event(QEvent::HoverLeave, QPointF(), QPointF());
QCoreApplication::instance()->sendEvent(old->decoration(), &event);
}
if (!now) {
// left decoration
return;
}
const auto pos = m_lastPosition - now->window()->pos();
QHoverEvent event(QEvent::HoverEnter, pos, pos);
QCoreApplication::instance()->sendEvent(now->decoration(), &event);
now->window()->processDecorationMove(pos, m_lastPosition);
m_decorationGeometryConnection = connect(
decoration()->window(), &Window::frameGeometryChanged, this, [this]() {
// ensure maximize button gets the leave event when maximizing/restore a window, see BUG 385140
const auto oldDeco = decoration();
update();
if (oldDeco && oldDeco == decoration() && !decoration()->window()->isInteractiveMove() && !decoration()->window()->isInteractiveResize()) {
// position of window did not change, we need to send HoverMotion manually
const QPointF p = m_lastPosition - decoration()->window()->pos();
QHoverEvent event(QEvent::HoverMove, p, p);
QCoreApplication::instance()->sendEvent(decoration()->decoration(), &event);
}
},
Qt::QueuedConnection);
// if our decoration gets destroyed whilst it has focus, we pass focus on to the same client
m_decorationDestroyedConnection = connect(now, &QObject::destroyed, this, &TabletInputRedirection::update, Qt::QueuedConnection);
}
void TabletInputRedirection::focusUpdate(Window *focusOld, Window *focusNow)
{
// This method is left blank intentionally.
}
}
#include "moc_tablet_input.cpp"