/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2013, 2016 Martin Gräßlin SPDX-FileCopyrightText: 2018 Roman Gilg SPDX-FileCopyrightText: 2019 Vlad Zahorodnii SPDX-FileCopyrightText: 2023 Harald Sitter SPDX-License-Identifier: GPL-2.0-or-later */ #include "pointer_input.h" #include "config-kwin.h" #include "core/output.h" #include "cursorsource.h" #include "decorations/decoratedclient.h" #include "effect/effecthandler.h" #include "input_event.h" #include "input_event_spy.h" #include "mousebuttons.h" #include "osd.h" #include "screenedge.h" #include "wayland/abstract_data_source.h" #include "wayland/display.h" #include "wayland/pointer.h" #include "wayland/pointerconstraints_v1.h" #include "wayland/seat.h" #include "wayland/surface.h" #include "wayland_server.h" #include "window.h" #include "workspace.h" // KDecoration #include // screenlocker #if KWIN_BUILD_SCREENLOCKER #include #endif #if KWIN_BUILD_X11 #include "x11window.h" #endif #include #include #include #include #include #include namespace KWin { static bool screenContainsPos(const QPointF &pos) { const auto outputs = workspace()->outputs(); for (const Output *output : outputs) { if (output->geometry().contains(flooredPoint(pos))) { return true; } } return false; } static QPointF confineToBoundingBox(const QPointF &pos, const QRectF &boundingBox) { return QPointF( std::clamp(pos.x(), boundingBox.left(), boundingBox.right() - 1.0), std::clamp(pos.y(), boundingBox.top(), boundingBox.bottom() - 1.0)); } PointerInputRedirection::PointerInputRedirection(InputRedirection *parent) : InputDeviceHandler(parent) , m_cursor(nullptr) { } PointerInputRedirection::~PointerInputRedirection() = default; CursorTheme PointerInputRedirection::cursorTheme() const { return m_cursor->theme(); } void PointerInputRedirection::init() { Q_ASSERT(!inited()); waylandServer()->seat()->setHasPointer(input()->hasPointer()); connect(input(), &InputRedirection::hasPointerChanged, waylandServer()->seat(), &SeatInterface::setHasPointer); m_cursor = new CursorImage(this); setInited(true); InputDeviceHandler::init(); if (!input()->hasPointer()) { Cursors::self()->hideCursor(); } connect(input(), &InputRedirection::hasPointerChanged, this, []() { if (input()->hasPointer()) { Cursors::self()->showCursor(); } else { Cursors::self()->hideCursor(); } }); connect(m_cursor, &CursorImage::changed, Cursors::self()->mouse(), [this] { Cursors::self()->mouse()->setSource(m_cursor->source()); m_cursor->updateCursorOutputs(m_pos); }); Q_EMIT m_cursor->changed(); connect(workspace(), &Workspace::outputsChanged, this, &PointerInputRedirection::updateAfterScreenChange); #if KWIN_BUILD_SCREENLOCKER if (kwinApp()->supportsLockScreen()) { connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, [this]() { if (waylandServer()->seat()->hasPointer()) { waylandServer()->seat()->cancelPointerPinchGesture(); waylandServer()->seat()->cancelPointerSwipeGesture(); } update(); }); } #endif connect(workspace(), &QObject::destroyed, this, [this] { setInited(false); }); connect(waylandServer(), &QObject::destroyed, this, [this] { setInited(false); }); connect(waylandServer()->seat(), &SeatInterface::dragEnded, this, [this]() { // need to force a focused pointer change setFocus(nullptr); update(); }); // connect the move resize of all window auto setupMoveResizeConnection = [this](Window *window) { connect(window, &Window::interactiveMoveResizeStarted, this, &PointerInputRedirection::updateOnStartMoveResize); connect(window, &Window::interactiveMoveResizeFinished, this, &PointerInputRedirection::update); }; const auto clients = workspace()->windows(); std::for_each(clients.begin(), clients.end(), setupMoveResizeConnection); connect(workspace(), &Workspace::windowAdded, this, setupMoveResizeConnection); // warp the cursor to center of screen containing the workspace center if (const Output *output = workspace()->outputAt(workspace()->geometry().center())) { warp(output->geometry().center()); } updateAfterScreenChange(); } void PointerInputRedirection::updateOnStartMoveResize() { breakPointerConstraints(focus() ? focus()->surface() : nullptr); disconnectPointerConstraintsConnection(); setFocus(nullptr); } void PointerInputRedirection::updateToReset() { if (decoration()) { QHoverEvent event(QEvent::HoverLeave, QPointF(), QPointF()); QCoreApplication::instance()->sendEvent(decoration()->decoration(), &event); setDecoration(nullptr); } if (focus()) { if (focus()->isClient()) { focus()->pointerLeaveEvent(); } disconnect(m_focusGeometryConnection); m_focusGeometryConnection = QMetaObject::Connection(); breakPointerConstraints(focus()->surface()); disconnectPointerConstraintsConnection(); setFocus(nullptr); } } class PositionUpdateBlocker { public: PositionUpdateBlocker(PointerInputRedirection *pointer) : m_pointer(pointer) { s_counter++; } ~PositionUpdateBlocker() { s_counter--; if (s_counter == 0) { if (!s_scheduledPositions.isEmpty()) { const auto pos = s_scheduledPositions.takeFirst(); m_pointer->processMotionInternal(pos.pos, pos.delta, pos.deltaNonAccelerated, pos.time, nullptr, pos.type); } } } static bool isPositionBlocked() { return s_counter > 0; } static void schedulePosition(const QPointF &pos, const QPointF &delta, const QPointF &deltaNonAccelerated, std::chrono::microseconds time, PointerInputRedirection::MotionType type) { s_scheduledPositions.append({pos, delta, deltaNonAccelerated, time, type}); } private: static int s_counter; struct ScheduledPosition { QPointF pos; QPointF delta; QPointF deltaNonAccelerated; std::chrono::microseconds time; PointerInputRedirection::MotionType type; }; static QList s_scheduledPositions; PointerInputRedirection *m_pointer; }; int PositionUpdateBlocker::s_counter = 0; QList PositionUpdateBlocker::s_scheduledPositions; void PointerInputRedirection::processMotionAbsolute(const QPointF &pos, std::chrono::microseconds time, InputDevice *device) { processMotionInternal(pos, QPointF(), QPointF(), time, device, MotionType::Motion); } void PointerInputRedirection::processWarp(const QPointF &pos, std::chrono::microseconds time, InputDevice *device) { processMotionInternal(pos, QPointF(), QPointF(), time, device, MotionType::Warp); } void PointerInputRedirection::processMotion(const QPointF &delta, const QPointF &deltaNonAccelerated, std::chrono::microseconds time, InputDevice *device) { processMotionInternal(m_pos + delta, delta, deltaNonAccelerated, time, device, MotionType::Motion); } void PointerInputRedirection::processMotionInternal(const QPointF &pos, const QPointF &delta, const QPointF &deltaNonAccelerated, std::chrono::microseconds time, InputDevice *device, MotionType type) { input()->setLastInputHandler(this); if (!inited()) { return; } if (PositionUpdateBlocker::isPositionBlocked()) { PositionUpdateBlocker::schedulePosition(pos, delta, deltaNonAccelerated, time, type); return; } PositionUpdateBlocker blocker(this); updatePosition(pos, time); MouseEvent event(QEvent::MouseMove, m_pos, Qt::NoButton, m_qtButtons, input()->keyboardModifiers(), time, delta, deltaNonAccelerated, device, type == MotionType::Warp); event.setModifiersRelevantForGlobalShortcuts(input()->modifiersRelevantForGlobalShortcuts()); update(); input()->processSpies(std::bind(&InputEventSpy::pointerMotion, std::placeholders::_1, &event)); input()->processFilters(std::bind(&InputEventFilter::pointerMotion, std::placeholders::_1, &event)); } void PointerInputRedirection::processButton(uint32_t button, InputDevice::PointerButtonState state, std::chrono::microseconds time, InputDevice *device) { input()->setLastInputHandler(this); if (!inited()) { return; } QEvent::Type type; switch (state) { case InputDevice::PointerButtonReleased: type = QEvent::MouseButtonRelease; break; case InputDevice::PointerButtonPressed: type = QEvent::MouseButtonPress; update(); break; default: Q_UNREACHABLE(); return; } updateButton(button, state); MouseEvent event(type, m_pos, buttonToQtMouseButton(button), m_qtButtons, input()->keyboardModifiers(), time, QPointF(), QPointF(), device, false); event.setModifiersRelevantForGlobalShortcuts(input()->modifiersRelevantForGlobalShortcuts()); event.setNativeButton(button); input()->processSpies(std::bind(&InputEventSpy::pointerButton, std::placeholders::_1, &event)); input()->processFilters(std::bind(&InputEventFilter::pointerButton, std::placeholders::_1, &event)); if (state == InputDevice::PointerButtonReleased) { update(); } } void PointerInputRedirection::processAxis(InputDevice::PointerAxis axis, qreal delta, qint32 deltaV120, InputDevice::PointerAxisSource source, bool inverted, std::chrono::microseconds time, InputDevice *device) { input()->setLastInputHandler(this); if (!inited()) { return; } update(); Q_EMIT input()->pointerAxisChanged(axis, delta); WheelEvent event{ .device = device, .position = m_pos, .delta = delta, .deltaV120 = deltaV120, .orientation = (axis == InputDevice::PointerAxisHorizontal) ? Qt::Horizontal : Qt::Vertical, .source = source, .buttons = m_qtButtons, .modifiers = input()->keyboardModifiers(), .modifiersRelevantForGlobalShortcuts = input()->modifiersRelevantForGlobalShortcuts(), .inverted = inverted, .timestamp = time, }; input()->processSpies(std::bind(&InputEventSpy::wheelEvent, std::placeholders::_1, &event)); input()->processFilters(std::bind(&InputEventFilter::wheelEvent, std::placeholders::_1, &event)); } void PointerInputRedirection::processSwipeGestureBegin(int fingerCount, std::chrono::microseconds time, KWin::InputDevice *device) { input()->setLastInputHandler(this); if (!inited()) { return; } input()->processSpies(std::bind(&InputEventSpy::swipeGestureBegin, std::placeholders::_1, fingerCount, time)); input()->processFilters(std::bind(&InputEventFilter::swipeGestureBegin, std::placeholders::_1, fingerCount, time)); } void PointerInputRedirection::processSwipeGestureUpdate(const QPointF &delta, std::chrono::microseconds time, KWin::InputDevice *device) { input()->setLastInputHandler(this); if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::swipeGestureUpdate, std::placeholders::_1, delta, time)); input()->processFilters(std::bind(&InputEventFilter::swipeGestureUpdate, std::placeholders::_1, delta, time)); } void PointerInputRedirection::processSwipeGestureEnd(std::chrono::microseconds time, KWin::InputDevice *device) { input()->setLastInputHandler(this); if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::swipeGestureEnd, std::placeholders::_1, time)); input()->processFilters(std::bind(&InputEventFilter::swipeGestureEnd, std::placeholders::_1, time)); } void PointerInputRedirection::processSwipeGestureCancelled(std::chrono::microseconds time, KWin::InputDevice *device) { input()->setLastInputHandler(this); if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::swipeGestureCancelled, std::placeholders::_1, time)); input()->processFilters(std::bind(&InputEventFilter::swipeGestureCancelled, std::placeholders::_1, time)); } void PointerInputRedirection::processPinchGestureBegin(int fingerCount, std::chrono::microseconds time, KWin::InputDevice *device) { input()->setLastInputHandler(this); if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::pinchGestureBegin, std::placeholders::_1, fingerCount, time)); input()->processFilters(std::bind(&InputEventFilter::pinchGestureBegin, std::placeholders::_1, fingerCount, time)); } void PointerInputRedirection::processPinchGestureUpdate(qreal scale, qreal angleDelta, const QPointF &delta, std::chrono::microseconds time, KWin::InputDevice *device) { input()->setLastInputHandler(this); if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::pinchGestureUpdate, std::placeholders::_1, scale, angleDelta, delta, time)); input()->processFilters(std::bind(&InputEventFilter::pinchGestureUpdate, std::placeholders::_1, scale, angleDelta, delta, time)); } void PointerInputRedirection::processPinchGestureEnd(std::chrono::microseconds time, KWin::InputDevice *device) { input()->setLastInputHandler(this); if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::pinchGestureEnd, std::placeholders::_1, time)); input()->processFilters(std::bind(&InputEventFilter::pinchGestureEnd, std::placeholders::_1, time)); } void PointerInputRedirection::processPinchGestureCancelled(std::chrono::microseconds time, KWin::InputDevice *device) { input()->setLastInputHandler(this); if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::pinchGestureCancelled, std::placeholders::_1, time)); input()->processFilters(std::bind(&InputEventFilter::pinchGestureCancelled, std::placeholders::_1, time)); } void PointerInputRedirection::processHoldGestureBegin(int fingerCount, std::chrono::microseconds time, KWin::InputDevice *device) { if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::holdGestureBegin, std::placeholders::_1, fingerCount, time)); input()->processFilters(std::bind(&InputEventFilter::holdGestureBegin, std::placeholders::_1, fingerCount, time)); } void PointerInputRedirection::processHoldGestureEnd(std::chrono::microseconds time, KWin::InputDevice *device) { if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::holdGestureEnd, std::placeholders::_1, time)); input()->processFilters(std::bind(&InputEventFilter::holdGestureEnd, std::placeholders::_1, time)); } void PointerInputRedirection::processHoldGestureCancelled(std::chrono::microseconds time, KWin::InputDevice *device) { if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::holdGestureCancelled, std::placeholders::_1, time)); input()->processFilters(std::bind(&InputEventFilter::holdGestureCancelled, std::placeholders::_1, time)); } void PointerInputRedirection::processFrame(KWin::InputDevice *device) { if (!inited()) { return; } input()->processFilters(std::bind(&InputEventFilter::pointerFrame, std::placeholders::_1)); } bool PointerInputRedirection::areButtonsPressed() const { for (auto state : m_buttons) { if (state == InputDevice::PointerButtonPressed) { return true; } } return false; } bool PointerInputRedirection::focusUpdatesBlocked() { if (waylandServer()->seat()->isDragPointer()) { // ignore during drag and drop return true; } if (waylandServer()->seat()->isTouchSequence()) { // ignore during touch operations return true; } if (input()->isSelectingWindow()) { return true; } if (areButtonsPressed()) { return true; } return false; } void PointerInputRedirection::cleanupDecoration(Decoration::DecoratedClientImpl *old, Decoration::DecoratedClientImpl *now) { disconnect(m_decorationGeometryConnection); m_decorationGeometryConnection = QMetaObject::Connection(); disconnect(m_decorationDestroyedConnection); m_decorationDestroyedConnection = QMetaObject::Connection(); disconnect(m_decorationClosedConnection); m_decorationClosedConnection = QMetaObject::Connection(); if (old) { // send leave event to old decoration QHoverEvent event(QEvent::HoverLeave, QPointF(-1, -1), QPointF()); QCoreApplication::instance()->sendEvent(old->decoration(), &event); } if (!now) { // left decoration return; } auto pos = m_pos - now->window()->pos(); QHoverEvent event(QEvent::HoverEnter, pos, QPointF(-1, -1)); QCoreApplication::instance()->sendEvent(now->decoration(), &event); now->window()->processDecorationMove(pos, m_pos); 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() && !areButtonsPressed()) { // position of window did not change, we need to send HoverMotion manually const QPointF p = m_pos - decoration()->window()->pos(); QHoverEvent event(QEvent::HoverMove, p, p); QCoreApplication::instance()->sendEvent(decoration()->decoration(), &event); } }); auto resetDecoration = [this]() { setDecoration(nullptr); // explicitly reset decoration if focus updates are blocked update(); }; m_decorationClosedConnection = connect(decoration()->window(), &Window::closed, this, resetDecoration); m_decorationDestroyedConnection = connect(now, &QObject::destroyed, this, resetDecoration); } void PointerInputRedirection::focusUpdate(Window *focusOld, Window *focusNow) { if (focusOld && focusOld->isClient()) { focusOld->pointerLeaveEvent(); breakPointerConstraints(focusOld->surface()); disconnectPointerConstraintsConnection(); } disconnect(m_focusGeometryConnection); m_focusGeometryConnection = QMetaObject::Connection(); if (focusNow && focusNow->isClient()) { focusNow->pointerEnterEvent(m_pos); } auto seat = waylandServer()->seat(); if (!focusNow || !focusNow->surface()) { seat->notifyPointerLeave(); return; } seat->notifyPointerEnter(focusNow->surface(), m_pos, focusNow->inputTransformation()); m_focusGeometryConnection = connect(focusNow, &Window::inputTransformationChanged, this, [this]() { waylandServer()->seat()->setFocusedPointerSurfaceTransformation(focus()->inputTransformation()); }); m_constraintsConnection = connect(focusNow->surface(), &SurfaceInterface::pointerConstraintsChanged, this, &PointerInputRedirection::updatePointerConstraints); m_constraintsActivatedConnection = connect(workspace(), &Workspace::windowActivated, this, &PointerInputRedirection::updatePointerConstraints); updatePointerConstraints(); } void PointerInputRedirection::breakPointerConstraints(SurfaceInterface *surface) { // cancel pointer constraints if (surface) { auto c = surface->confinedPointer(); if (c && c->isConfined()) { c->setConfined(false); } auto l = surface->lockedPointer(); if (l && l->isLocked()) { l->setLocked(false); } } disconnectConfinedPointerRegionConnection(); m_confined = false; m_locked = false; } void PointerInputRedirection::disconnectConfinedPointerRegionConnection() { disconnect(m_confinedPointerRegionConnection); m_confinedPointerRegionConnection = QMetaObject::Connection(); } void PointerInputRedirection::disconnectLockedPointerAboutToBeUnboundConnection() { disconnect(m_lockedPointerAboutToBeUnboundConnection); m_lockedPointerAboutToBeUnboundConnection = QMetaObject::Connection(); } void PointerInputRedirection::disconnectPointerConstraintsConnection() { disconnect(m_constraintsConnection); m_constraintsConnection = QMetaObject::Connection(); disconnect(m_constraintsActivatedConnection); m_constraintsActivatedConnection = QMetaObject::Connection(); } void PointerInputRedirection::setEnableConstraints(bool set) { if (m_enableConstraints == set) { return; } m_enableConstraints = set; updatePointerConstraints(); } void PointerInputRedirection::updatePointerConstraints() { if (!focus()) { return; } const auto s = focus()->surface(); if (!s) { return; } if (s != waylandServer()->seat()->focusedPointerSurface()) { return; } if (!supportsWarping()) { return; } const bool canConstrain = m_enableConstraints && focus() == workspace()->activeWindow(); const auto cf = s->confinedPointer(); if (cf) { if (cf->isConfined()) { if (!canConstrain) { cf->setConfined(false); m_confined = false; disconnectConfinedPointerRegionConnection(); } return; } if (canConstrain && cf->region().contains(flooredPoint(focus()->mapToLocal(m_pos)))) { cf->setConfined(true); m_confined = true; m_confinedPointerRegionConnection = connect(cf, &ConfinedPointerV1Interface::regionChanged, this, [this]() { if (!focus()) { return; } const auto s = focus()->surface(); if (!s) { return; } const auto cf = s->confinedPointer(); if (!cf->region().contains(flooredPoint(focus()->mapToLocal(m_pos)))) { // pointer no longer in confined region, break the confinement cf->setConfined(false); m_confined = false; } else { if (!cf->isConfined()) { cf->setConfined(true); m_confined = true; } } }); return; } } else { m_confined = false; disconnectConfinedPointerRegionConnection(); } const auto lock = s->lockedPointer(); if (lock) { if (lock->isLocked()) { if (!canConstrain) { const auto hint = lock->cursorPositionHint(); lock->setLocked(false); m_locked = false; disconnectLockedPointerAboutToBeUnboundConnection(); if (!(hint.x() < 0 || hint.y() < 0) && focus()) { processWarp(focus()->mapFromLocal(hint), waylandServer()->seat()->timestamp()); } } return; } if (canConstrain && lock->region().contains(flooredPoint(focus()->mapToLocal(m_pos)))) { lock->setLocked(true); m_locked = true; // The client might cancel pointer locking from its side by unbinding the LockedPointerInterface. // In this case the cached cursor position hint must be fetched before the resource goes away m_lockedPointerAboutToBeUnboundConnection = connect(lock, &LockedPointerV1Interface::aboutToBeDestroyed, this, [this, lock]() { const auto hint = lock->cursorPositionHint(); if (hint.x() < 0 || hint.y() < 0 || !focus()) { return; } auto globalHint = focus()->mapFromLocal(hint); // When the resource finally goes away, reposition the cursor according to the hint connect(lock, &LockedPointerV1Interface::destroyed, this, [this, globalHint]() { processWarp(globalHint, waylandServer()->seat()->timestamp()); }); }); // TODO: connect to region change - is it needed at all? If the pointer is locked it's always in the region } } else { m_locked = false; disconnectLockedPointerAboutToBeUnboundConnection(); } } QPointF PointerInputRedirection::applyPointerConfinement(const QPointF &pos) const { if (!focus()) { return pos; } auto s = focus()->surface(); if (!s) { return pos; } auto cf = s->confinedPointer(); if (!cf) { return pos; } if (!cf->isConfined()) { return pos; } const QPointF localPos = focus()->mapToLocal(pos); if (cf->region().contains(flooredPoint(localPos))) { return pos; } const QPointF currentPos = focus()->mapToLocal(m_pos); // allow either x or y to pass QPointF p(currentPos.x(), localPos.y()); if (cf->region().contains(flooredPoint(p))) { return focus()->mapFromLocal(p); } p = QPointF(localPos.x(), currentPos.y()); if (cf->region().contains(flooredPoint(p))) { return focus()->mapFromLocal(p); } return m_pos; } PointerInputRedirection::EdgeBarrierType PointerInputRedirection::edgeBarrierType(const QPointF &pos, const QRectF &lastOutputGeometry) const { constexpr qreal cornerThreshold = 15; const auto moveResizeWindow = workspace()->moveResizeWindow(); const bool onCorner = (pos - lastOutputGeometry.topLeft()).manhattanLength() <= cornerThreshold || (pos - lastOutputGeometry.bottomLeft()).manhattanLength() <= cornerThreshold || (pos - lastOutputGeometry.topRight()).manhattanLength() <= cornerThreshold || (pos - lastOutputGeometry.bottomRight()).manhattanLength() <= cornerThreshold; if (moveResizeWindow && moveResizeWindow->isInteractiveMove()) { return EdgeBarrierType::WindowMoveBarrier; } else if (moveResizeWindow && moveResizeWindow->isInteractiveResize()) { return EdgeBarrierType::WindowResizeBarrier; } else if (options->cornerBarrier() && onCorner) { return EdgeBarrierType::CornerBarrier; } else if (workspace()->screenEdges()->inApproachGeometry(pos.toPoint())) { return EdgeBarrierType::EdgeElementBarrier; } else { return EdgeBarrierType::NormalBarrier; } } qreal PointerInputRedirection::edgeBarrier(EdgeBarrierType type) const { const auto barrierWidth = options->edgeBarrier(); switch (type) { case EdgeBarrierType::WindowMoveBarrier: case EdgeBarrierType::WindowResizeBarrier: return 1.5 * barrierWidth; case EdgeBarrierType::EdgeElementBarrier: return 2 * barrierWidth; case EdgeBarrierType::CornerBarrier: return 2000; case EdgeBarrierType::NormalBarrier: return barrierWidth; default: Q_UNREACHABLE(); return 0; } } QPointF PointerInputRedirection::applyEdgeBarrier(const QPointF &pos, const Output *currentOutput, std::chrono::microseconds time) { // optimization to avoid looping over all outputs if (exclusiveContains(currentOutput->geometry(), m_pos)) { m_movementInEdgeBarrier = QPointF(); return pos; } const Output *lastOutput = workspace()->outputAt(m_pos); QPointF newPos = confineToBoundingBox(pos, lastOutput->geometry()); const auto type = edgeBarrierType(newPos, lastOutput->geometry()); if (m_lastEdgeBarrierType != type) { m_movementInEdgeBarrier = QPointF(); } m_lastEdgeBarrierType = type; const auto barrierWidth = edgeBarrier(type); const qreal returnSpeed = barrierWidth / 10.0 /* px/s */ / 1000'000.0; // px/us std::chrono::microseconds timeDiff(time - m_lastMoveTime); qreal returnDistance = returnSpeed * timeDiff.count(); const auto euclideanLength = [](const QPointF &point) { return std::sqrt(point.x() * point.x() + point.y() * point.y()); }; const auto shorten = [euclideanLength](const QPointF &point, const qreal distance) { const qreal length = euclideanLength(point); if (length <= distance) { return QPointF(); } return point * (1 - distance / length); }; m_movementInEdgeBarrier += (pos - newPos); m_movementInEdgeBarrier = shorten(m_movementInEdgeBarrier, returnDistance); if (euclideanLength(m_movementInEdgeBarrier) > barrierWidth) { newPos += shorten(m_movementInEdgeBarrier, barrierWidth); m_movementInEdgeBarrier = QPointF(); } return newPos; } void PointerInputRedirection::updatePosition(const QPointF &pos, std::chrono::microseconds time) { m_lastMoveTime = time; if (m_locked) { // locked pointer should not move return; } // verify that at least one screen contains the pointer position const Output *currentOutput = workspace()->outputAt(pos); QPointF p = confineToBoundingBox(pos, currentOutput->geometry()); p = applyEdgeBarrier(p, currentOutput, time); p = applyPointerConfinement(p); if (p == m_pos) { // didn't change due to confinement return; } // verify screen confinement if (!screenContainsPos(p)) { return; } m_pos = p; workspace()->setActiveOutput(m_pos); m_cursor->updateCursorOutputs(m_pos); Q_EMIT input()->globalPointerChanged(m_pos); } void PointerInputRedirection::updateButton(uint32_t button, InputDevice::PointerButtonState state) { m_buttons[button] = state; // update Qt buttons m_qtButtons = Qt::NoButton; for (auto it = m_buttons.constBegin(); it != m_buttons.constEnd(); ++it) { if (it.value() == InputDevice::PointerButtonReleased) { continue; } m_qtButtons |= buttonToQtMouseButton(it.key()); } Q_EMIT input()->pointerButtonStateChanged(button, state); } void PointerInputRedirection::warp(const QPointF &pos) { if (supportsWarping()) { processWarp(pos, waylandServer()->seat()->timestamp()); } } bool PointerInputRedirection::supportsWarping() const { return inited(); } void PointerInputRedirection::updateAfterScreenChange() { if (!inited()) { return; } Output *output = nullptr; if (m_lastOutputWasPlaceholder) { // previously we've positioned our pointer on a placeholder screen, try // to get us onto the real "primary" screen instead. output = workspace()->outputOrder().at(0); } else { if (screenContainsPos(m_pos)) { // pointer still on a screen return; } // pointer no longer on a screen, reposition to closes screen output = workspace()->outputAt(m_pos); } m_lastOutputWasPlaceholder = output->isPlaceholder(); // TODO: better way to get timestamps processMotionAbsolute(output->geometry().center(), waylandServer()->seat()->timestamp()); } QPointF PointerInputRedirection::position() const { return m_pos; } void PointerInputRedirection::setEffectsOverrideCursor(Qt::CursorShape shape) { if (!inited()) { return; } // current pointer focus window should get a leave event update(); m_cursor->setEffectsOverrideCursor(shape); } void PointerInputRedirection::removeEffectsOverrideCursor() { if (!inited()) { return; } // cursor position might have changed while there was an effect in place update(); m_cursor->removeEffectsOverrideCursor(); } void PointerInputRedirection::setWindowSelectionCursor(const QByteArray &shape) { if (!inited()) { return; } // send leave to current pointer focus window updateToReset(); m_cursor->setWindowSelectionCursor(shape); } void PointerInputRedirection::removeWindowSelectionCursor() { if (!inited()) { return; } update(); m_cursor->removeWindowSelectionCursor(); } CursorImage::CursorImage(PointerInputRedirection *parent) : QObject(parent) , m_pointer(parent) { m_effectsCursor = std::make_unique(); m_fallbackCursor = std::make_unique(); m_moveResizeCursor = std::make_unique(); m_windowSelectionCursor = std::make_unique(); m_decoration.cursor = std::make_unique(); m_serverCursor.surface = std::make_unique(); m_serverCursor.shape = std::make_unique(); m_dragCursor = std::make_unique(); #if KWIN_BUILD_SCREENLOCKER if (kwinApp()->supportsLockScreen()) { connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, &CursorImage::reevaluteSource); } #endif connect(m_pointer, &PointerInputRedirection::decorationChanged, this, &CursorImage::updateDecoration); // connect the move resize of all window auto setupMoveResizeConnection = [this](Window *window) { connect(window, &Window::moveResizedChanged, this, &CursorImage::updateMoveResize); connect(window, &Window::moveResizeCursorChanged, this, &CursorImage::updateMoveResize); }; const auto clients = workspace()->windows(); std::for_each(clients.begin(), clients.end(), setupMoveResizeConnection); connect(workspace(), &Workspace::windowAdded, this, setupMoveResizeConnection); m_fallbackCursor->setShape(Qt::ArrowCursor); m_effectsCursor->setTheme(m_waylandImage.theme()); m_fallbackCursor->setTheme(m_waylandImage.theme()); m_moveResizeCursor->setTheme(m_waylandImage.theme()); m_windowSelectionCursor->setTheme(m_waylandImage.theme()); m_decoration.cursor->setTheme(m_waylandImage.theme()); m_serverCursor.shape->setTheme(m_waylandImage.theme()); m_dragCursor->setTheme(m_waylandImage.theme()); connect(&m_waylandImage, &WaylandCursorImage::themeChanged, this, [this] { m_effectsCursor->setTheme(m_waylandImage.theme()); m_fallbackCursor->setTheme(m_waylandImage.theme()); m_moveResizeCursor->setTheme(m_waylandImage.theme()); m_windowSelectionCursor->setTheme(m_waylandImage.theme()); m_decoration.cursor->setTheme(m_waylandImage.theme()); m_serverCursor.shape->setTheme(m_waylandImage.theme()); m_dragCursor->setTheme(m_waylandImage.theme()); }); connect(waylandServer()->seat(), &SeatInterface::dragStarted, this, [this]() { m_dragCursor->setShape(Qt::ForbiddenCursor); connect(waylandServer()->seat()->dragSource(), &AbstractDataSource::dndActionChanged, this, &CursorImage::updateDragCursor); connect(waylandServer()->seat()->dragSource(), &AbstractDataSource::acceptedChanged, this, &CursorImage::updateDragCursor); reevaluteSource(); }); PointerInterface *pointer = waylandServer()->seat()->pointer(); connect(pointer, &PointerInterface::focusedSurfaceChanged, this, &CursorImage::handleFocusedSurfaceChanged); reevaluteSource(); } CursorImage::~CursorImage() = default; void CursorImage::updateCursorOutputs(const QPointF &pos) { if (m_currentSource == m_serverCursor.surface.get()) { auto cursorSurface = m_serverCursor.surface->surface(); if (cursorSurface) { const QRectF cursorGeometry(pos - m_currentSource->hotspot(), m_currentSource->size()); cursorSurface->setOutputs(waylandServer()->display()->outputsIntersecting(cursorGeometry.toAlignedRect()), waylandServer()->display()->largestIntersectingOutput(cursorGeometry.toAlignedRect())); } } } void CursorImage::handleFocusedSurfaceChanged() { PointerInterface *pointer = waylandServer()->seat()->pointer(); disconnect(m_serverCursor.connection); if (pointer->focusedSurface()) { m_serverCursor.connection = connect(pointer, &PointerInterface::cursorChanged, this, &CursorImage::updateServerCursor); } else { m_serverCursor.connection = QMetaObject::Connection(); reevaluteSource(); } } void CursorImage::updateDecoration() { disconnect(m_decoration.connection); auto deco = m_pointer->decoration(); Window *window = deco ? deco->window() : nullptr; if (window) { m_decoration.connection = connect(window, &Window::moveResizeCursorChanged, this, &CursorImage::updateDecorationCursor); } else { m_decoration.connection = QMetaObject::Connection(); } updateDecorationCursor(); } void CursorImage::updateDecorationCursor() { auto deco = m_pointer->decoration(); if (Window *window = deco ? deco->window() : nullptr) { m_decoration.cursor->setShape(window->cursor().name()); } reevaluteSource(); } void CursorImage::updateMoveResize() { if (Window *window = workspace()->moveResizeWindow()) { m_moveResizeCursor->setShape(window->cursor().name()); } reevaluteSource(); } void CursorImage::updateDragCursor() { AbstractDataSource *dragSource = waylandServer()->seat()->dragSource(); if (dragSource && dragSource->isAccepted()) { switch (dragSource->selectedDndAction()) { case DataDeviceManagerInterface::DnDAction::None: m_dragCursor->setShape(Qt::ClosedHandCursor); break; case DataDeviceManagerInterface::DnDAction::Copy: m_dragCursor->setShape(Qt::DragCopyCursor); break; case DataDeviceManagerInterface::DnDAction::Move: m_dragCursor->setShape(Qt::DragMoveCursor); break; case DataDeviceManagerInterface::DnDAction::Ask: // Cursor themes don't have anything better in the themes yet // a dnd-drag-ask is proposed m_dragCursor->setShape(Qt::ClosedHandCursor); break; } } else { m_dragCursor->setShape(Qt::ForbiddenCursor); } reevaluteSource(); } void CursorImage::updateServerCursor(const PointerCursor &cursor) { if (auto surfaceCursor = std::get_if(&cursor)) { m_serverCursor.surface->update((*surfaceCursor)->surface(), (*surfaceCursor)->hotspot()); m_serverCursor.cursor = m_serverCursor.surface.get(); } else if (auto shapeCursor = std::get_if(&cursor)) { m_serverCursor.shape->setShape(*shapeCursor); m_serverCursor.cursor = m_serverCursor.shape.get(); } reevaluteSource(); } void CursorImage::setEffectsOverrideCursor(Qt::CursorShape shape) { m_effectsCursor->setShape(shape); reevaluteSource(); } void CursorImage::removeEffectsOverrideCursor() { reevaluteSource(); } void CursorImage::setWindowSelectionCursor(const QByteArray &shape) { if (shape.isEmpty()) { m_windowSelectionCursor->setShape(Qt::CrossCursor); } else { m_windowSelectionCursor->setShape(shape); } reevaluteSource(); } void CursorImage::removeWindowSelectionCursor() { reevaluteSource(); } WaylandCursorImage::WaylandCursorImage(QObject *parent) : QObject(parent) { Cursor *pointerCursor = Cursors::self()->mouse(); updateCursorTheme(); connect(pointerCursor, &Cursor::themeChanged, this, &WaylandCursorImage::updateCursorTheme); connect(workspace(), &Workspace::outputsChanged, this, &WaylandCursorImage::updateCursorTheme); } CursorTheme WaylandCursorImage::theme() const { return m_cursorTheme; } void WaylandCursorImage::updateCursorTheme() { const Cursor *pointerCursor = Cursors::self()->mouse(); qreal targetDevicePixelRatio = 1; const auto outputs = workspace()->outputs(); for (const Output *output : outputs) { if (output->scale() > targetDevicePixelRatio) { targetDevicePixelRatio = output->scale(); } } m_cursorTheme = CursorTheme(pointerCursor->themeName(), pointerCursor->themeSize(), targetDevicePixelRatio); if (m_cursorTheme.isEmpty()) { qCWarning(KWIN_CORE) << "Failed to load cursor theme" << pointerCursor->themeName(); m_cursorTheme = CursorTheme(Cursor::defaultThemeName(), Cursor::defaultThemeSize(), targetDevicePixelRatio); if (m_cursorTheme.isEmpty()) { qCWarning(KWIN_CORE) << "Failed to load cursor theme" << Cursor::defaultThemeName(); m_cursorTheme = CursorTheme(Cursor::fallbackThemeName(), Cursor::defaultThemeSize(), targetDevicePixelRatio); } } if (m_cursorTheme.isEmpty()) { qCWarning(KWIN_CORE) << "Unable to load any cursor theme"; } Q_EMIT themeChanged(); } void CursorImage::reevaluteSource() { if (waylandServer()->isScreenLocked()) { setSource(m_serverCursor.cursor); return; } if (waylandServer()->seat()->isDrag()) { setSource(m_dragCursor.get()); return; } if (input()->isSelectingWindow()) { setSource(m_windowSelectionCursor.get()); return; } if (effects && effects->isMouseInterception()) { setSource(m_effectsCursor.get()); return; } if (workspace() && workspace()->moveResizeWindow()) { setSource(m_moveResizeCursor.get()); return; } if (m_pointer->decoration()) { setSource(m_decoration.cursor.get()); return; } const PointerInterface *pointer = waylandServer()->seat()->pointer(); if (pointer && pointer->focusedSurface()) { setSource(m_serverCursor.cursor); return; } setSource(m_fallbackCursor.get()); } CursorSource *CursorImage::source() const { return m_currentSource; } void CursorImage::setSource(CursorSource *source) { if (m_currentSource == source) { return; } m_currentSource = source; Q_EMIT changed(); } CursorTheme CursorImage::theme() const { return m_waylandImage.theme(); } InputRedirectionCursor::InputRedirectionCursor() : Cursor() , m_currentButtons(Qt::NoButton) { Cursors::self()->setMouse(this); connect(input(), &InputRedirection::globalPointerChanged, this, &InputRedirectionCursor::slotPosChanged); connect(input(), &InputRedirection::pointerButtonStateChanged, this, &InputRedirectionCursor::slotPointerButtonChanged); #ifndef KCMRULES connect(input(), &InputRedirection::keyboardModifiersChanged, this, &InputRedirectionCursor::slotModifiersChanged); #endif } InputRedirectionCursor::~InputRedirectionCursor() { } void InputRedirectionCursor::doSetPos() { if (input()->supportsPointerWarping()) { input()->warpPointer(currentPos()); } slotPosChanged(input()->globalPointer()); Q_EMIT posChanged(currentPos()); } void InputRedirectionCursor::slotPosChanged(const QPointF &pos) { const QPointF oldPos = currentPos(); updatePos(pos); Q_EMIT mouseChanged(pos, oldPos, m_currentButtons, m_currentButtons, input()->keyboardModifiers(), input()->keyboardModifiers()); } void InputRedirectionCursor::slotModifiersChanged(Qt::KeyboardModifiers mods, Qt::KeyboardModifiers oldMods) { Q_EMIT mouseChanged(currentPos(), currentPos(), m_currentButtons, m_currentButtons, mods, oldMods); } void InputRedirectionCursor::slotPointerButtonChanged() { const Qt::MouseButtons oldButtons = m_currentButtons; m_currentButtons = input()->qtButtonStates(); const QPointF pos = currentPos(); Q_EMIT mouseChanged(pos, pos, m_currentButtons, oldButtons, input()->keyboardModifiers(), input()->keyboardModifiers()); } } #include "moc_pointer_input.cpp"