/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez 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 #include #include #include #include #include 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(&cursor)) { // If the cursor is unset, fallback to the cross cursor. if ((*surfaceCursor) && (*surfaceCursor)->enteredSerial()) { if (!m_surfaceSource) { m_surfaceSource = std::make_unique(); } m_surfaceSource->update((*surfaceCursor)->surface(), (*surfaceCursor)->hotspot()); setSource(m_surfaceSource.get()); return; } } QByteArray shape; if (auto shapeCursor = std::get_if(&cursor)) { shape = *shapeCursor; } else { shape = QByteArrayLiteral("cross"); } static WaylandCursorImage defaultCursor; if (!m_shapeSource) { m_shapeSource = std::make_unique(); } m_shapeSource->setTheme(defaultCursor.theme()); m_shapeSource->setShape(shape); setSource(m_shapeSource.get()); }); } private: std::unique_ptr m_shapeSource; std::unique_ptr 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()); 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(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(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->addTabletPad(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(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 &tabletToolId) { using Type = TabletToolV2Interface::Type; switch (tabletToolId.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 &tabletToolId) { TabletSeatV2Interface *tabletSeat = findTabletSeat(); if (auto tool = tabletSeat->toolByHardwareSerial(tabletToolId.m_serialId, getType(tabletToolId))) { 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 ifaceCapabilities; ifaceCapabilities.resize(tabletToolId.m_capabilities.size()); std::transform(tabletToolId.m_capabilities.constBegin(), tabletToolId.m_capabilities.constEnd(), ifaceCapabilities.begin(), f); TabletToolV2Interface *tool = tabletSeat->addTool(getType(tabletToolId), tabletToolId.m_serialId, tabletToolId.m_uniqueId, ifaceCapabilities, tabletToolId.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 &tabletToolId, std::chrono::microseconds time) { 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(tabletToolId); 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, tabletToolId); ev.setTimestamp(std::chrono::duration_cast(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 &tabletToolId, std::chrono::microseconds time) { input()->processSpies(std::bind(&InputEventSpy::tabletToolButtonEvent, std::placeholders::_1, button, isPressed, tabletToolId, time)); input()->processFilters(std::bind(&InputEventFilter::tabletToolButtonEvent, std::placeholders::_1, button, isPressed, tabletToolId, time)); input()->setLastInputHandler(this); } void KWin::TabletInputRedirection::tabletPadButtonEvent(uint button, bool isPressed, const TabletPadId &tabletPadId, std::chrono::microseconds time) { input()->processSpies(std::bind(&InputEventSpy::tabletPadButtonEvent, std::placeholders::_1, button, isPressed, tabletPadId, time)); input()->processFilters(std::bind(&InputEventFilter::tabletPadButtonEvent, std::placeholders::_1, button, isPressed, tabletPadId, time)); input()->setLastInputHandler(this); } void KWin::TabletInputRedirection::tabletPadStripEvent(int number, int position, bool isFinger, const TabletPadId &tabletPadId, std::chrono::microseconds time) { input()->processSpies(std::bind(&InputEventSpy::tabletPadStripEvent, std::placeholders::_1, number, position, isFinger, tabletPadId, time)); input()->processFilters(std::bind(&InputEventFilter::tabletPadStripEvent, std::placeholders::_1, number, position, isFinger, tabletPadId, time)); input()->setLastInputHandler(this); } void KWin::TabletInputRedirection::tabletPadRingEvent(int number, int position, bool isFinger, const TabletPadId &tabletPadId, std::chrono::microseconds time) { input()->processSpies(std::bind(&InputEventSpy::tabletPadRingEvent, std::placeholders::_1, number, position, isFinger, tabletPadId, time)); input()->processFilters(std::bind(&InputEventFilter::tabletPadRingEvent, std::placeholders::_1, number, position, isFinger, tabletPadId, time)); 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"