diff --git a/pointer_input.cpp b/pointer_input.cpp index 68c9e7e08..955fd8e9a 100644 --- a/pointer_input.cpp +++ b/pointer_input.cpp @@ -1,1353 +1,1383 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2013, 2016 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "pointer_input.h" #include "platform.h" #include "effects.h" #include "input_event.h" #include "input_event_spy.h" #include "osd.h" #include "screens.h" #include "shell_client.h" #include "wayland_cursor_theme.h" #include "wayland_server.h" #include "workspace.h" #include "decorations/decoratedclient.h" #include "screens.h" // KDecoration #include // KWayland #include #include #include #include #include #include #include #include // screenlocker #include #include #include #include // Wayland #include #include namespace KWin { static Qt::MouseButton buttonToQtMouseButton(uint32_t button) { switch (button) { case BTN_LEFT: return Qt::LeftButton; case BTN_MIDDLE: return Qt::MiddleButton; case BTN_RIGHT: return Qt::RightButton; case BTN_SIDE: // in QtWayland mapped like that return Qt::ExtraButton1; case BTN_EXTRA: // in QtWayland mapped like that return Qt::ExtraButton2; case BTN_BACK: return Qt::BackButton; case BTN_FORWARD: return Qt::ForwardButton; case BTN_TASK: return Qt::TaskButton; // mapped like that in QtWayland case 0x118: return Qt::ExtraButton6; case 0x119: return Qt::ExtraButton7; case 0x11a: return Qt::ExtraButton8; case 0x11b: return Qt::ExtraButton9; case 0x11c: return Qt::ExtraButton10; case 0x11d: return Qt::ExtraButton11; case 0x11e: return Qt::ExtraButton12; case 0x11f: return Qt::ExtraButton13; } // all other values get mapped to ExtraButton24 // this is actually incorrect but doesn't matter in our usage // KWin internally doesn't use these high extra buttons anyway // it's only needed for recognizing whether buttons are pressed // if multiple buttons are mapped to the value the evaluation whether // buttons are pressed is correct and that's all we care about. return Qt::ExtraButton24; } static bool screenContainsPos(const QPointF &pos) { for (int i = 0; i < screens()->count(); ++i) { if (screens()->geometry(i).contains(pos.toPoint())) { return true; } } return false; } static QPointF confineToBoundingBox(const QPointF &pos, const QRectF &boundingBox) { return QPointF( qBound(boundingBox.left(), pos.x(), boundingBox.right() - 1.0), qBound(boundingBox.top(), pos.y(), boundingBox.bottom() - 1.0) ); } PointerInputRedirection::PointerInputRedirection(InputRedirection* parent) : InputDeviceHandler(parent) , m_cursor(nullptr) , m_supportsWarping(Application::usesLibinput()) { } PointerInputRedirection::~PointerInputRedirection() = default; void PointerInputRedirection::init() { Q_ASSERT(!m_inited); m_cursor = new CursorImage(this); m_inited = true; connect(m_cursor, &CursorImage::changed, kwinApp()->platform(), &Platform::cursorChanged); emit m_cursor->changed(); connect(workspace(), &Workspace::stackingOrderChanged, this, &PointerInputRedirection::update); connect(workspace(), &Workspace::clientMinimizedChanged, this, &PointerInputRedirection::update); connect(screens(), &Screens::changed, this, &PointerInputRedirection::updateAfterScreenChange); if (waylandServer()->hasScreenLockerIntegration()) { connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, [this] { waylandServer()->seat()->cancelPointerPinchGesture(); waylandServer()->seat()->cancelPointerSwipeGesture(); update(); } ); } connect(workspace(), &QObject::destroyed, this, [this] { m_inited = false; }); connect(waylandServer(), &QObject::destroyed, this, [this] { m_inited = false; }); connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::dragEnded, this, [this] { // need to force a focused pointer change waylandServer()->seat()->setFocusedPointerSurface(nullptr); m_window.clear(); update(); } ); connect(this, &PointerInputRedirection::internalWindowChanged, this, [this] { disconnect(m_internalWindowConnection); m_internalWindowConnection = QMetaObject::Connection(); if (m_internalWindow) { m_internalWindowConnection = connect(m_internalWindow.data(), &QWindow::visibleChanged, this, [this] (bool visible) { if (!visible) { update(); } } ); } } ); connect(this, &PointerInputRedirection::decorationChanged, this, [this] { disconnect(m_decorationGeometryConnection); m_decorationGeometryConnection = QMetaObject::Connection(); if (m_decoration) { m_decorationGeometryConnection = connect(m_decoration->client(), &AbstractClient::geometryChanged, this, [this] { // ensure maximize button gets the leave event when maximizing/restore a window, see BUG 385140 const auto oldDeco = m_decoration; update(); if (oldDeco && oldDeco == m_decoration && !m_decoration->client()->isMove() && !m_decoration->client()->isResize() && !areButtonsPressed()) { // position of window did not change, we need to send HoverMotion manually const QPointF p = m_pos - m_decoration->client()->pos(); QHoverEvent event(QEvent::HoverMove, p, p); QCoreApplication::instance()->sendEvent(m_decoration->decoration(), &event); } }, Qt::QueuedConnection); } } ); // connect the move resize of all window auto setupMoveResizeConnection = [this] (AbstractClient *c) { connect(c, &AbstractClient::clientStartUserMovedResized, this, &PointerInputRedirection::updateOnStartMoveResize); connect(c, &AbstractClient::clientFinishUserMovedResized, this, &PointerInputRedirection::update); }; const auto clients = workspace()->allClientList(); std::for_each(clients.begin(), clients.end(), setupMoveResizeConnection); connect(workspace(), &Workspace::clientAdded, this, setupMoveResizeConnection); connect(waylandServer(), &WaylandServer::shellClientAdded, this, setupMoveResizeConnection); // warp the cursor to center of screen warp(screens()->geometry().center()); updateAfterScreenChange(); } void PointerInputRedirection::updateOnStartMoveResize() { breakPointerConstraints(m_window ? m_window->surface() : nullptr); disconnectPointerConstraintsConnection(); m_window.clear(); waylandServer()->seat()->setFocusedPointerSurface(nullptr); } void PointerInputRedirection::updateToReset() { if (m_internalWindow) { disconnect(m_internalWindowConnection); m_internalWindowConnection = QMetaObject::Connection(); QEvent event(QEvent::Leave); QCoreApplication::sendEvent(m_internalWindow.data(), &event); m_internalWindow.clear(); } if (m_decoration) { QHoverEvent event(QEvent::HoverLeave, QPointF(), QPointF()); QCoreApplication::instance()->sendEvent(m_decoration->decoration(), &event); m_decoration.clear(); } if (m_window) { if (AbstractClient *c = qobject_cast(m_window.data())) { c->leaveEvent(); } disconnect(m_windowGeometryConnection); m_windowGeometryConnection = QMetaObject::Connection(); breakPointerConstraints(m_window->surface()); disconnectPointerConstraintsConnection(); m_window.clear(); } waylandServer()->seat()->setFocusedPointerSurface(nullptr); } void PointerInputRedirection::processMotion(const QPointF &pos, uint32_t time, LibInput::Device *device) { processMotion(pos, QSizeF(), QSizeF(), time, 0, device); } 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->processMotion(pos.pos, pos.delta, pos.deltaNonAccelerated, pos.time, pos.timeUsec, nullptr); } } } static bool isPositionBlocked() { return s_counter > 0; } static void schedulePosition(const QPointF &pos, const QSizeF &delta, const QSizeF &deltaNonAccelerated, uint32_t time, quint64 timeUsec) { s_scheduledPositions.append({pos, delta, deltaNonAccelerated, time, timeUsec}); } private: static int s_counter; struct ScheduledPosition { QPointF pos; QSizeF delta; QSizeF deltaNonAccelerated; quint32 time; quint64 timeUsec; }; static QVector s_scheduledPositions; PointerInputRedirection *m_pointer; }; int PositionUpdateBlocker::s_counter = 0; QVector PositionUpdateBlocker::s_scheduledPositions; void PointerInputRedirection::processMotion(const QPointF &pos, const QSizeF &delta, const QSizeF &deltaNonAccelerated, uint32_t time, quint64 timeUsec, LibInput::Device *device) { if (!m_inited) { return; } if (PositionUpdateBlocker::isPositionBlocked()) { PositionUpdateBlocker::schedulePosition(pos, delta, deltaNonAccelerated, time, timeUsec); return; } PositionUpdateBlocker blocker(this); updatePosition(pos); MouseEvent event(QEvent::MouseMove, m_pos, Qt::NoButton, m_qtButtons, m_input->keyboardModifiers(), time, delta, deltaNonAccelerated, timeUsec, device); event.setModifiersRelevantForGlobalShortcuts(m_input->modifiersRelevantForGlobalShortcuts()); m_input->processSpies(std::bind(&InputEventSpy::pointerEvent, std::placeholders::_1, &event)); m_input->processFilters(std::bind(&InputEventFilter::pointerEvent, std::placeholders::_1, &event, 0)); } void PointerInputRedirection::processButton(uint32_t button, InputRedirection::PointerButtonState state, uint32_t time, LibInput::Device *device) { updateButton(button, state); QEvent::Type type; switch (state) { case InputRedirection::PointerButtonReleased: type = QEvent::MouseButtonRelease; break; case InputRedirection::PointerButtonPressed: type = QEvent::MouseButtonPress; break; default: Q_UNREACHABLE(); return; } MouseEvent event(type, m_pos, buttonToQtMouseButton(button), m_qtButtons, m_input->keyboardModifiers(), time, QSizeF(), QSizeF(), 0, device); event.setModifiersRelevantForGlobalShortcuts(m_input->modifiersRelevantForGlobalShortcuts()); event.setNativeButton(button); m_input->processSpies(std::bind(&InputEventSpy::pointerEvent, std::placeholders::_1, &event)); if (!m_inited) { return; } m_input->processFilters(std::bind(&InputEventFilter::pointerEvent, std::placeholders::_1, &event, button)); } void PointerInputRedirection::processAxis(InputRedirection::PointerAxis axis, qreal delta, uint32_t time, LibInput::Device *device) { if (delta == 0) { return; } emit m_input->pointerAxisChanged(axis, delta); WheelEvent wheelEvent(m_pos, delta, (axis == InputRedirection::PointerAxisHorizontal) ? Qt::Horizontal : Qt::Vertical, m_qtButtons, m_input->keyboardModifiers(), time, device); wheelEvent.setModifiersRelevantForGlobalShortcuts(m_input->modifiersRelevantForGlobalShortcuts()); m_input->processSpies(std::bind(&InputEventSpy::wheelEvent, std::placeholders::_1, &wheelEvent)); if (!m_inited) { return; } m_input->processFilters(std::bind(&InputEventFilter::wheelEvent, std::placeholders::_1, &wheelEvent)); } void PointerInputRedirection::processSwipeGestureBegin(int fingerCount, quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!m_inited) { return; } m_input->processSpies(std::bind(&InputEventSpy::swipeGestureBegin, std::placeholders::_1, fingerCount, time)); m_input->processFilters(std::bind(&InputEventFilter::swipeGestureBegin, std::placeholders::_1, fingerCount, time)); } void PointerInputRedirection::processSwipeGestureUpdate(const QSizeF &delta, quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!m_inited) { return; } m_input->processSpies(std::bind(&InputEventSpy::swipeGestureUpdate, std::placeholders::_1, delta, time)); m_input->processFilters(std::bind(&InputEventFilter::swipeGestureUpdate, std::placeholders::_1, delta, time)); } void PointerInputRedirection::processSwipeGestureEnd(quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!m_inited) { return; } m_input->processSpies(std::bind(&InputEventSpy::swipeGestureEnd, std::placeholders::_1, time)); m_input->processFilters(std::bind(&InputEventFilter::swipeGestureEnd, std::placeholders::_1, time)); } void PointerInputRedirection::processSwipeGestureCancelled(quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!m_inited) { return; } m_input->processSpies(std::bind(&InputEventSpy::swipeGestureCancelled, std::placeholders::_1, time)); m_input->processFilters(std::bind(&InputEventFilter::swipeGestureCancelled, std::placeholders::_1, time)); } void PointerInputRedirection::processPinchGestureBegin(int fingerCount, quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!m_inited) { return; } m_input->processSpies(std::bind(&InputEventSpy::pinchGestureBegin, std::placeholders::_1, fingerCount, time)); m_input->processFilters(std::bind(&InputEventFilter::pinchGestureBegin, std::placeholders::_1, fingerCount, time)); } void PointerInputRedirection::processPinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!m_inited) { return; } m_input->processSpies(std::bind(&InputEventSpy::pinchGestureUpdate, std::placeholders::_1, scale, angleDelta, delta, time)); m_input->processFilters(std::bind(&InputEventFilter::pinchGestureUpdate, std::placeholders::_1, scale, angleDelta, delta, time)); } void PointerInputRedirection::processPinchGestureEnd(quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!m_inited) { return; } m_input->processSpies(std::bind(&InputEventSpy::pinchGestureEnd, std::placeholders::_1, time)); m_input->processFilters(std::bind(&InputEventFilter::pinchGestureEnd, std::placeholders::_1, time)); } void PointerInputRedirection::processPinchGestureCancelled(quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!m_inited) { return; } m_input->processSpies(std::bind(&InputEventSpy::pinchGestureCancelled, std::placeholders::_1, time)); m_input->processFilters(std::bind(&InputEventFilter::pinchGestureCancelled, std::placeholders::_1, time)); } bool PointerInputRedirection::areButtonsPressed() const { for (auto state : m_buttons) { if (state == InputRedirection::PointerButtonPressed) { return true; } } return false; } static bool s_cursorUpdateBlocking = false; void PointerInputRedirection::update() { if (!m_inited) { return; } if (waylandServer()->seat()->isDragPointer()) { // ignore during drag and drop return; } if (input()->isSelectingWindow()) { return; } if (areButtonsPressed()) { return; } Toplevel *t = m_input->findToplevel(m_pos.toPoint()); const auto oldDeco = m_decoration; updateInternalWindow(m_pos); if (!m_internalWindow) { updateDecoration(t, m_pos); } else { updateDecoration(waylandServer()->findClient(m_internalWindow), m_pos); if (m_decoration) { disconnect(m_internalWindowConnection); m_internalWindowConnection = QMetaObject::Connection(); QEvent event(QEvent::Leave); QCoreApplication::sendEvent(m_internalWindow.data(), &event); m_internalWindow.clear(); } } if (m_decoration || m_internalWindow) { t = nullptr; } if (m_decoration != oldDeco) { emit decorationChanged(); } auto oldWindow = m_window; if (!oldWindow.isNull() && t == m_window.data()) { return; } auto seat = waylandServer()->seat(); // disconnect old surface if (oldWindow) { if (AbstractClient *c = qobject_cast(oldWindow.data())) { c->leaveEvent(); } disconnect(m_windowGeometryConnection); m_windowGeometryConnection = QMetaObject::Connection(); breakPointerConstraints(oldWindow->surface()); disconnectPointerConstraintsConnection(); } if (AbstractClient *c = qobject_cast(t)) { // only send enter if it wasn't on deco for the same client before if (m_decoration.isNull() || m_decoration->client() != c) { c->enterEvent(m_pos.toPoint()); workspace()->updateFocusMousePosition(m_pos.toPoint()); } } if (t && t->surface()) { m_window = QPointer(t); // TODO: add convenient API to update global pos together with updating focused surface warpXcbOnSurfaceLeft(t->surface()); s_cursorUpdateBlocking = true; seat->setFocusedPointerSurface(nullptr); s_cursorUpdateBlocking = false; seat->setPointerPos(m_pos.toPoint()); seat->setFocusedPointerSurface(t->surface(), t->inputTransformation()); m_windowGeometryConnection = connect(t, &Toplevel::geometryChanged, this, [this] { if (m_window.isNull()) { return; } // TODO: can we check on the client instead? if (workspace()->getMovingClient()) { // don't update while moving return; } auto seat = waylandServer()->seat(); if (m_window.data()->surface() != seat->focusedPointerSurface()) { return; } seat->setFocusedPointerSurfaceTransformation(m_window.data()->inputTransformation()); } ); m_constraintsConnection = connect(m_window->surface(), &KWayland::Server::SurfaceInterface::pointerConstraintsChanged, this, &PointerInputRedirection::updatePointerConstraints); m_constraintsActivatedConnection = connect(workspace(), &Workspace::clientActivated, this, &PointerInputRedirection::updatePointerConstraints); // check whether a pointer confinement/lock fires m_blockConstraint = false; updatePointerConstraints(); } else { m_window.clear(); warpXcbOnSurfaceLeft(nullptr); seat->setFocusedPointerSurface(nullptr); t = nullptr; } } void PointerInputRedirection::breakPointerConstraints(KWayland::Server::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::breakPointerConstraints() { breakPointerConstraints(m_window ? m_window->surface() : nullptr); } 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(); } template static QRegion getConstraintRegion(Toplevel *t, T *constraint) { const QRegion windowShape = t->inputShape(); const QRegion windowRegion = windowShape.isEmpty() ? QRegion(0, 0, t->clientSize().width(), t->clientSize().height()) : windowShape; const QRegion intersected = constraint->region().isEmpty() ? windowRegion : windowRegion.intersected(constraint->region()); return intersected.translated(t->pos() + t->clientPos()); } void PointerInputRedirection::setEnableConstraints(bool set) { if (m_enableConstraints == set) { return; } m_enableConstraints = set; updatePointerConstraints(); } void PointerInputRedirection::updatePointerConstraints() { if (m_window.isNull()) { return; } const auto s = m_window->surface(); if (!s) { return; } if (s != waylandServer()->seat()->focusedPointerSurface()) { return; } if (!supportsWarping()) { return; } if (m_blockConstraint) { return; } const bool canConstrain = m_enableConstraints && m_window == workspace()->activeClient(); const auto cf = s->confinedPointer(); if (cf) { if (cf->isConfined()) { if (!canConstrain) { cf->setConfined(false); m_confined = false; disconnectConfinedPointerRegionConnection(); } return; } const QRegion r = getConstraintRegion(m_window.data(), cf.data()); if (canConstrain && r.contains(m_pos.toPoint())) { cf->setConfined(true); m_confined = true; m_confinedPointerRegionConnection = connect(cf.data(), &KWayland::Server::ConfinedPointerInterface::regionChanged, this, [this] { if (!m_window) { return; } const auto s = m_window->surface(); if (!s) { return; } const auto cf = s->confinedPointer(); if (!getConstraintRegion(m_window.data(), cf.data()).contains(m_pos.toPoint())) { // 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; } } } ); OSD::show(i18nc("notification about mouse pointer confined", "Pointer motion confined to the current window.\nTo release pointer hold Escape for 3 seconds."), QStringLiteral("preferences-desktop-mouse"), 5000); 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) && m_window) { + processMotion(m_window->pos() - m_window->clientContentPos() + hint, waylandServer()->seat()->timestamp()); + } } return; } const QRegion r = getConstraintRegion(m_window.data(), lock.data()); if (canConstrain && r.contains(m_pos.toPoint())) { 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.data(), &KWayland::Server::LockedPointerInterface::aboutToBeUnbound, this, + [this, lock]() { + const auto hint = lock->cursorPositionHint(); + if (hint.x() < 0 || hint.y() < 0 || !m_window) { + return; + } + auto globalHint = m_window->pos() - m_window->clientContentPos() + hint; + + // When the resource finally goes away, reposition the cursor according to the hint + connect(lock.data(), &KWayland::Server::LockedPointerInterface::unbound, this, + [this, globalHint]() { + processMotion(globalHint, waylandServer()->seat()->timestamp()); + }); + } + ); OSD::show(i18nc("notification about mouse pointer locked", "Pointer locked to current position.\nTo end pointer lock hold Escape for 3 seconds."), QStringLiteral("preferences-desktop-mouse"), 5000); // 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(); } } void PointerInputRedirection::warpXcbOnSurfaceLeft(KWayland::Server::SurfaceInterface *newSurface) { auto xc = waylandServer()->xWaylandConnection(); if (!xc) { // No XWayland, no point in warping the x cursor return; } const auto c = kwinApp()->x11Connection(); if (!c) { return; } static bool s_hasXWayland119 = xcb_get_setup(c)->release_number >= 11900000; if (s_hasXWayland119) { return; } if (newSurface && newSurface->client() == xc) { // new window is an X window return; } auto s = waylandServer()->seat()->focusedPointerSurface(); if (!s || s->client() != xc) { // pointer was not on an X window return; } // warp pointer to 0/0 to trigger leave events on previously focused X window xcb_warp_pointer(c, XCB_WINDOW_NONE, kwinApp()->x11RootWindow(), 0, 0, 0, 0, 0, 0), xcb_flush(c); } QPointF PointerInputRedirection::applyPointerConfinement(const QPointF &pos) const { if (!m_window) { return pos; } auto s = m_window->surface(); if (!s) { return pos; } auto cf = s->confinedPointer(); if (!cf) { return pos; } if (!cf->isConfined()) { return pos; } const QRegion confinementRegion = getConstraintRegion(m_window.data(), cf.data()); if (confinementRegion.contains(pos.toPoint())) { return pos; } QPointF p = pos; // allow either x or y to pass p = QPointF(m_pos.x(), pos.y()); if (confinementRegion.contains(p.toPoint())) { return p; } p = QPointF(pos.x(), m_pos.y()); if (confinementRegion.contains(p.toPoint())) { return p; } return m_pos; } void PointerInputRedirection::updatePosition(const QPointF &pos) { if (m_locked) { // locked pointer should not move return; } // verify that at least one screen contains the pointer position QPointF p = pos; if (!screenContainsPos(p)) { const QRectF unitedScreensGeometry = screens()->geometry(); p = confineToBoundingBox(p, unitedScreensGeometry); if (!screenContainsPos(p)) { const QRectF currentScreenGeometry = screens()->geometry(screens()->number(m_pos.toPoint())); p = confineToBoundingBox(p, currentScreenGeometry); } } p = applyPointerConfinement(p); if (p == m_pos) { // didn't change due to confinement return; } // verify screen confinement if (!screenContainsPos(p)) { return; } m_pos = p; emit m_input->globalPointerChanged(m_pos); } void PointerInputRedirection::updateButton(uint32_t button, InputRedirection::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() == InputRedirection::PointerButtonReleased) { continue; } m_qtButtons |= buttonToQtMouseButton(it.key()); } emit m_input->pointerButtonStateChanged(button, state); } void PointerInputRedirection::warp(const QPointF &pos) { if (supportsWarping()) { kwinApp()->platform()->warpPointer(pos); processMotion(pos, waylandServer()->seat()->timestamp()); } } bool PointerInputRedirection::supportsWarping() const { if (!m_inited) { return false; } if (m_supportsWarping) { return true; } if (kwinApp()->platform()->supportsPointerWarping()) { return true; } return false; } void PointerInputRedirection::updateAfterScreenChange() { if (!m_inited) { return; } if (screenContainsPos(m_pos)) { // pointer still on a screen return; } // pointer no longer on a screen, reposition to closes screen const QPointF pos = screens()->geometry(screens()->number(m_pos.toPoint())).center(); // TODO: better way to get timestamps processMotion(pos, waylandServer()->seat()->timestamp()); } QImage PointerInputRedirection::cursorImage() const { if (!m_inited) { return QImage(); } return m_cursor->image(); } QPoint PointerInputRedirection::cursorHotSpot() const { if (!m_inited) { return QPoint(); } return m_cursor->hotSpot(); } void PointerInputRedirection::markCursorAsRendered() { if (!m_inited) { return; } m_cursor->markAsRendered(); } void PointerInputRedirection::setEffectsOverrideCursor(Qt::CursorShape shape) { if (!m_inited) { return; } // current pointer focus window should get a leave event update(); m_cursor->setEffectsOverrideCursor(shape); } void PointerInputRedirection::removeEffectsOverrideCursor() { if (!m_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 (!m_inited) { return; } // send leave to current pointer focus window updateToReset(); m_cursor->setWindowSelectionCursor(shape); } void PointerInputRedirection::removeWindowSelectionCursor() { if (!m_inited) { return; } update(); m_cursor->removeWindowSelectionCursor(); } CursorImage::CursorImage(PointerInputRedirection *parent) : QObject(parent) , m_pointer(parent) { connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::focusedPointerChanged, this, &CursorImage::update); connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::dragStarted, this, &CursorImage::updateDrag); connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::dragEnded, this, [this] { disconnect(m_drag.connection); reevaluteSource(); } ); if (waylandServer()->hasScreenLockerIntegration()) { connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, &CursorImage::reevaluteSource); } connect(m_pointer, &PointerInputRedirection::decorationChanged, this, &CursorImage::updateDecoration); // connect the move resize of all window auto setupMoveResizeConnection = [this] (AbstractClient *c) { connect(c, &AbstractClient::moveResizedChanged, this, &CursorImage::updateMoveResize); }; const auto clients = workspace()->allClientList(); std::for_each(clients.begin(), clients.end(), setupMoveResizeConnection); connect(workspace(), &Workspace::clientAdded, this, setupMoveResizeConnection); connect(waylandServer(), &WaylandServer::shellClientAdded, this, setupMoveResizeConnection); loadThemeCursor(Qt::ArrowCursor, &m_fallbackCursor); if (m_cursorTheme) { connect(m_cursorTheme, &WaylandCursorTheme::themeChanged, this, [this] { m_cursors.clear(); m_cursorsByName.clear(); loadThemeCursor(Qt::ArrowCursor, &m_fallbackCursor); updateDecorationCursor(); updateMoveResize(); // TODO: update effects } ); } m_surfaceRenderedTimer.start(); } CursorImage::~CursorImage() = default; void CursorImage::markAsRendered() { if (m_currentSource == CursorSource::DragAndDrop) { // always sending a frame rendered to the drag icon surface to not freeze QtWayland (see https://bugreports.qt.io/browse/QTBUG-51599 ) if (auto ddi = waylandServer()->seat()->dragSource()) { if (auto s = ddi->icon()) { s->frameRendered(m_surfaceRenderedTimer.elapsed()); } } auto p = waylandServer()->seat()->dragPointer(); if (!p) { return; } auto c = p->cursor(); if (!c) { return; } auto cursorSurface = c->surface(); if (cursorSurface.isNull()) { return; } cursorSurface->frameRendered(m_surfaceRenderedTimer.elapsed()); return; } if (m_currentSource != CursorSource::LockScreen && m_currentSource != CursorSource::PointerSurface) { return; } auto p = waylandServer()->seat()->focusedPointer(); if (!p) { return; } auto c = p->cursor(); if (!c) { return; } auto cursorSurface = c->surface(); if (cursorSurface.isNull()) { return; } cursorSurface->frameRendered(m_surfaceRenderedTimer.elapsed()); } void CursorImage::update() { if (s_cursorUpdateBlocking) { return; } using namespace KWayland::Server; disconnect(m_serverCursor.connection); auto p = waylandServer()->seat()->focusedPointer(); if (p) { m_serverCursor.connection = connect(p, &PointerInterface::cursorChanged, this, &CursorImage::updateServerCursor); } else { m_serverCursor.connection = QMetaObject::Connection(); reevaluteSource(); } } void CursorImage::updateDecoration() { disconnect(m_decorationConnection); auto deco = m_pointer->decoration(); AbstractClient *c = deco.isNull() ? nullptr : deco->client(); if (c) { m_decorationConnection = connect(c, &AbstractClient::moveResizeCursorChanged, this, &CursorImage::updateDecorationCursor); } else { m_decorationConnection = QMetaObject::Connection(); } updateDecorationCursor(); } void CursorImage::updateDecorationCursor() { m_decorationCursor.image = QImage(); m_decorationCursor.hotSpot = QPoint(); auto deco = m_pointer->decoration(); if (AbstractClient *c = deco.isNull() ? nullptr : deco->client()) { loadThemeCursor(c->cursor(), &m_decorationCursor); if (m_currentSource == CursorSource::Decoration) { emit changed(); } } reevaluteSource(); } void CursorImage::updateMoveResize() { m_moveResizeCursor.image = QImage(); m_moveResizeCursor.hotSpot = QPoint(); if (AbstractClient *c = workspace()->getMovingClient()) { loadThemeCursor(c->isMove() ? Qt::SizeAllCursor : Qt::SizeBDiagCursor, &m_moveResizeCursor); if (m_currentSource == CursorSource::MoveResize) { emit changed(); } } reevaluteSource(); } void CursorImage::updateServerCursor() { m_serverCursor.image = QImage(); m_serverCursor.hotSpot = QPoint(); reevaluteSource(); const bool needsEmit = m_currentSource == CursorSource::LockScreen || m_currentSource == CursorSource::PointerSurface; auto p = waylandServer()->seat()->focusedPointer(); if (!p) { if (needsEmit) { emit changed(); } return; } auto c = p->cursor(); if (!c) { if (needsEmit) { emit changed(); } return; } auto cursorSurface = c->surface(); if (cursorSurface.isNull()) { if (needsEmit) { emit changed(); } return; } auto buffer = cursorSurface.data()->buffer(); if (!buffer) { if (needsEmit) { emit changed(); } return; } m_serverCursor.hotSpot = c->hotspot(); m_serverCursor.image = buffer->data().copy(); m_serverCursor.image.setDevicePixelRatio(cursorSurface->scale()); if (needsEmit) { emit changed(); } } void CursorImage::loadTheme() { if (m_cursorTheme) { return; } // check whether we can create it if (waylandServer()->internalShmPool()) { m_cursorTheme = new WaylandCursorTheme(waylandServer()->internalShmPool(), this); connect(waylandServer(), &WaylandServer::terminatingInternalClientConnection, this, [this] { delete m_cursorTheme; m_cursorTheme = nullptr; } ); } } void CursorImage::setEffectsOverrideCursor(Qt::CursorShape shape) { loadThemeCursor(shape, &m_effectsCursor); if (m_currentSource == CursorSource::EffectsOverride) { emit changed(); } reevaluteSource(); } void CursorImage::removeEffectsOverrideCursor() { reevaluteSource(); } void CursorImage::setWindowSelectionCursor(const QByteArray &shape) { if (shape.isEmpty()) { loadThemeCursor(Qt::CrossCursor, &m_windowSelectionCursor); } else { loadThemeCursor(shape, &m_windowSelectionCursor); } if (m_currentSource == CursorSource::WindowSelector) { emit changed(); } reevaluteSource(); } void CursorImage::removeWindowSelectionCursor() { reevaluteSource(); } void CursorImage::updateDrag() { using namespace KWayland::Server; disconnect(m_drag.connection); m_drag.cursor.image = QImage(); m_drag.cursor.hotSpot = QPoint(); reevaluteSource(); if (auto p = waylandServer()->seat()->dragPointer()) { m_drag.connection = connect(p, &PointerInterface::cursorChanged, this, &CursorImage::updateDragCursor); } else { m_drag.connection = QMetaObject::Connection(); } updateDragCursor(); } void CursorImage::updateDragCursor() { m_drag.cursor.image = QImage(); m_drag.cursor.hotSpot = QPoint(); const bool needsEmit = m_currentSource == CursorSource::DragAndDrop; QImage additionalIcon; if (auto ddi = waylandServer()->seat()->dragSource()) { if (auto dragIcon = ddi->icon()) { if (auto buffer = dragIcon->buffer()) { additionalIcon = buffer->data().copy(); } } } auto p = waylandServer()->seat()->dragPointer(); if (!p) { if (needsEmit) { emit changed(); } return; } auto c = p->cursor(); if (!c) { if (needsEmit) { emit changed(); } return; } auto cursorSurface = c->surface(); if (cursorSurface.isNull()) { if (needsEmit) { emit changed(); } return; } auto buffer = cursorSurface.data()->buffer(); if (!buffer) { if (needsEmit) { emit changed(); } return; } m_drag.cursor.hotSpot = c->hotspot(); m_drag.cursor.image = buffer->data().copy(); if (needsEmit) { emit changed(); } // TODO: add the cursor image } void CursorImage::loadThemeCursor(CursorShape shape, Image *image) { loadThemeCursor(shape, m_cursors, image); } void CursorImage::loadThemeCursor(const QByteArray &shape, Image *image) { loadThemeCursor(shape, m_cursorsByName, image); } template void CursorImage::loadThemeCursor(const T &shape, QHash &cursors, Image *image) { loadTheme(); if (!m_cursorTheme) { return; } auto it = cursors.constFind(shape); if (it == cursors.constEnd()) { image->image = QImage(); image->hotSpot = QPoint(); wl_cursor_image *cursor = m_cursorTheme->get(shape); if (!cursor) { return; } wl_buffer *b = wl_cursor_image_get_buffer(cursor); if (!b) { return; } waylandServer()->internalClientConection()->flush(); waylandServer()->dispatch(); auto buffer = KWayland::Server::BufferInterface::get(waylandServer()->internalConnection()->getResource(KWayland::Client::Buffer::getId(b))); if (!buffer) { return; } auto scale = screens()->maxScale(); int hotSpotX = qRound(cursor->hotspot_x / scale); int hotSpotY = qRound(cursor->hotspot_y / scale); QImage img = buffer->data().copy(); img.setDevicePixelRatio(scale); it = decltype(it)(cursors.insert(shape, {img, QPoint(hotSpotX, hotSpotY)})); } image->hotSpot = it.value().hotSpot; image->image = it.value().image; } void CursorImage::reevaluteSource() { if (waylandServer()->seat()->isDragPointer()) { // TODO: touch drag? setSource(CursorSource::DragAndDrop); return; } if (waylandServer()->isScreenLocked()) { setSource(CursorSource::LockScreen); return; } if (input()->isSelectingWindow()) { setSource(CursorSource::WindowSelector); return; } if (effects && static_cast(effects)->isMouseInterception()) { setSource(CursorSource::EffectsOverride); return; } if (workspace() && workspace()->getMovingClient()) { setSource(CursorSource::MoveResize); return; } if (!m_pointer->decoration().isNull()) { setSource(CursorSource::Decoration); return; } if (!m_pointer->window().isNull() && waylandServer()->seat()->focusedPointer()) { setSource(CursorSource::PointerSurface); return; } setSource(CursorSource::Fallback); } void CursorImage::setSource(CursorSource source) { if (m_currentSource == source) { return; } m_currentSource = source; emit changed(); } QImage CursorImage::image() const { switch (m_currentSource) { case CursorSource::EffectsOverride: return m_effectsCursor.image; case CursorSource::MoveResize: return m_moveResizeCursor.image; case CursorSource::LockScreen: case CursorSource::PointerSurface: // lockscreen also uses server cursor image return m_serverCursor.image; case CursorSource::Decoration: return m_decorationCursor.image; case CursorSource::DragAndDrop: return m_drag.cursor.image; case CursorSource::Fallback: return m_fallbackCursor.image; case CursorSource::WindowSelector: return m_windowSelectionCursor.image; default: Q_UNREACHABLE(); } } QPoint CursorImage::hotSpot() const { switch (m_currentSource) { case CursorSource::EffectsOverride: return m_effectsCursor.hotSpot; case CursorSource::MoveResize: return m_moveResizeCursor.hotSpot; case CursorSource::LockScreen: case CursorSource::PointerSurface: // lockscreen also uses server cursor image return m_serverCursor.hotSpot; case CursorSource::Decoration: return m_decorationCursor.hotSpot; case CursorSource::DragAndDrop: return m_drag.cursor.hotSpot; case CursorSource::Fallback: return m_fallbackCursor.hotSpot; case CursorSource::WindowSelector: return m_windowSelectionCursor.hotSpot; default: Q_UNREACHABLE(); } } } diff --git a/pointer_input.h b/pointer_input.h index 05d7f4c6e..d3374de5c 100644 --- a/pointer_input.h +++ b/pointer_input.h @@ -1,255 +1,257 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2013, 2016 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_POINTER_INPUT_H #define KWIN_POINTER_INPUT_H #include "input.h" #include #include #include #include class QWindow; namespace KWayland { namespace Server { class SurfaceInterface; } } namespace KWin { class CursorImage; class InputRedirection; class Toplevel; class WaylandCursorTheme; class CursorShape; namespace Decoration { class DecoratedClientImpl; } namespace LibInput { class Device; } class KWIN_EXPORT PointerInputRedirection : public InputDeviceHandler { Q_OBJECT public: explicit PointerInputRedirection(InputRedirection *parent); virtual ~PointerInputRedirection(); void init(); void update(); void updateAfterScreenChange(); bool supportsWarping() const; void warp(const QPointF &pos); QPointF pos() const { return m_pos; } Qt::MouseButtons buttons() const { return m_qtButtons; } QImage cursorImage() const; QPoint cursorHotSpot() const; void markCursorAsRendered(); void setEffectsOverrideCursor(Qt::CursorShape shape); void removeEffectsOverrideCursor(); void setWindowSelectionCursor(const QByteArray &shape); void removeWindowSelectionCursor(); void updatePointerConstraints(); void breakPointerConstraints(); /* This is only used for ESC pressing */ void blockPointerConstraints() { m_blockConstraint = true; } void setEnableConstraints(bool set); bool isConstrained() const { return m_confined || m_locked; } /** * @internal */ void processMotion(const QPointF &pos, uint32_t time, LibInput::Device *device = nullptr); /** * @internal **/ void processMotion(const QPointF &pos, const QSizeF &delta, const QSizeF &deltaNonAccelerated, uint32_t time, quint64 timeUsec, LibInput::Device *device); /** * @internal */ void processButton(uint32_t button, InputRedirection::PointerButtonState state, uint32_t time, LibInput::Device *device = nullptr); /** * @internal */ void processAxis(InputRedirection::PointerAxis axis, qreal delta, uint32_t time, LibInput::Device *device = nullptr); /** * @internal */ void processSwipeGestureBegin(int fingerCount, quint32 time, KWin::LibInput::Device *device = nullptr); /** * @internal */ void processSwipeGestureUpdate(const QSizeF &delta, quint32 time, KWin::LibInput::Device *device = nullptr); /** * @internal */ void processSwipeGestureEnd(quint32 time, KWin::LibInput::Device *device = nullptr); /** * @internal */ void processSwipeGestureCancelled(quint32 time, KWin::LibInput::Device *device = nullptr); /** * @internal */ void processPinchGestureBegin(int fingerCount, quint32 time, KWin::LibInput::Device *device = nullptr); /** * @internal */ void processPinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time, KWin::LibInput::Device *device = nullptr); /** * @internal */ void processPinchGestureEnd(quint32 time, KWin::LibInput::Device *device = nullptr); /** * @internal */ void processPinchGestureCancelled(quint32 time, KWin::LibInput::Device *device = nullptr); private: void updateOnStartMoveResize(); void updateToReset(); void updatePosition(const QPointF &pos); void updateButton(uint32_t button, InputRedirection::PointerButtonState state); void warpXcbOnSurfaceLeft(KWayland::Server::SurfaceInterface *surface); QPointF applyPointerConfinement(const QPointF &pos) const; void disconnectConfinedPointerRegionConnection(); + void disconnectLockedPointerAboutToBeUnboundConnection(); void disconnectPointerConstraintsConnection(); void breakPointerConstraints(KWayland::Server::SurfaceInterface *surface); bool areButtonsPressed() const; CursorImage *m_cursor; bool m_inited = false; bool m_supportsWarping; QPointF m_pos; QHash m_buttons; Qt::MouseButtons m_qtButtons; QMetaObject::Connection m_windowGeometryConnection; QMetaObject::Connection m_internalWindowConnection; QMetaObject::Connection m_constraintsConnection; QMetaObject::Connection m_constraintsActivatedConnection; QMetaObject::Connection m_confinedPointerRegionConnection; + QMetaObject::Connection m_lockedPointerAboutToBeUnboundConnection; QMetaObject::Connection m_decorationGeometryConnection; bool m_confined = false; bool m_locked = false; bool m_blockConstraint = false; bool m_enableConstraints = true; }; class CursorImage : public QObject { Q_OBJECT public: explicit CursorImage(PointerInputRedirection *parent = nullptr); virtual ~CursorImage(); void setEffectsOverrideCursor(Qt::CursorShape shape); void removeEffectsOverrideCursor(); void setWindowSelectionCursor(const QByteArray &shape); void removeWindowSelectionCursor(); QImage image() const; QPoint hotSpot() const; void markAsRendered(); Q_SIGNALS: void changed(); private: void reevaluteSource(); void update(); void updateServerCursor(); void updateDecoration(); void updateDecorationCursor(); void updateMoveResize(); void updateDrag(); void updateDragCursor(); void loadTheme(); struct Image { QImage image; QPoint hotSpot; }; void loadThemeCursor(CursorShape shape, Image *image); void loadThemeCursor(const QByteArray &shape, Image *image); template void loadThemeCursor(const T &shape, QHash &cursors, Image *image); enum class CursorSource { LockScreen, EffectsOverride, MoveResize, PointerSurface, Decoration, DragAndDrop, Fallback, WindowSelector }; void setSource(CursorSource source); PointerInputRedirection *m_pointer; CursorSource m_currentSource = CursorSource::Fallback; WaylandCursorTheme *m_cursorTheme = nullptr; struct { QMetaObject::Connection connection; QImage image; QPoint hotSpot; } m_serverCursor; Image m_effectsCursor; Image m_decorationCursor; QMetaObject::Connection m_decorationConnection; Image m_fallbackCursor; Image m_moveResizeCursor; Image m_windowSelectionCursor; QHash m_cursors; QHash m_cursorsByName; QElapsedTimer m_surfaceRenderedTimer; struct { Image cursor; QMetaObject::Connection connection; } m_drag; }; } #endif diff --git a/tests/pointerconstraintstest.cpp b/tests/pointerconstraintstest.cpp index 9f6df023f..8334d3b1a 100644 --- a/tests/pointerconstraintstest.cpp +++ b/tests/pointerconstraintstest.cpp @@ -1,411 +1,417 @@ /******************************************************************** Copyright 2018 Roman Gilg This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . *********************************************************************/ #include "pointerconstraintstest.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KWayland::Client; WaylandBackend::WaylandBackend(QObject *parent) : Backend(parent) , m_connectionThreadObject(ConnectionThread::fromApplication(this)) { setMode(Mode::Wayland); } void WaylandBackend::init(QQuickView *view) { Backend::init(view); Registry *registry = new Registry(this); setupRegistry(registry); } void WaylandBackend::setupRegistry(Registry *registry) { connect(registry, &Registry::compositorAnnounced, this, [this, registry](quint32 name, quint32 version) { m_compositor = registry->createCompositor(name, version, this); } ); connect(registry, &Registry::seatAnnounced, this, [this, registry](quint32 name, quint32 version) { m_seat = registry->createSeat(name, version, this); if (m_seat->hasPointer()) { m_pointer = m_seat->createPointer(this); } connect(m_seat, &Seat::hasPointerChanged, this, [this]() { delete m_pointer; m_pointer = m_seat->createPointer(this); } ); } ); connect(registry, &Registry::pointerConstraintsUnstableV1Announced, this, [this, registry](quint32 name, quint32 version) { m_pointerConstraints = registry->createPointerConstraints(name, version, this); } ); connect(registry, &Registry::interfacesAnnounced, this, [this] { Q_ASSERT(m_compositor); Q_ASSERT(m_seat); Q_ASSERT(m_pointerConstraints); } ); registry->create(m_connectionThreadObject); registry->setup(); } bool WaylandBackend::isLocked() { return m_lockedPointer && m_lockedPointer->isValid(); } bool WaylandBackend::isConfined() { return m_confinedPointer && m_confinedPointer->isValid(); } static PointerConstraints::LifeTime lifeTime(bool persistent) { return persistent ? PointerConstraints::LifeTime::Persistent : PointerConstraints::LifeTime::OneShot; } void WaylandBackend::lockRequest(bool persistent, QRect region) { if (isLocked()) { if (!errorsAllowed()) { qDebug() << "Abort locking because already locked. Allow errors to test relocking (and crashing)."; return; } qDebug() << "Trying to lock although already locked. Crash expected."; } if (isConfined()) { if (!errorsAllowed()) { qDebug() << "Abort locking because already confined. Allow errors to test locking while being confined (and crashing)."; return; } qDebug() << "Trying to lock although already confined. Crash expected."; } qDebug() << "------ Lock requested ------"; qDebug() << "Persistent:" << persistent << "| Region:" << region; QScopedPointer winSurface(Surface::fromWindow(view())); QScopedPointer wlRegion(m_compositor->createRegion(this)); wlRegion->add(region); auto *lockedPointer = m_pointerConstraints->lockPointer(winSurface.data(), m_pointer, wlRegion.data(), lifeTime(persistent), this); if (!lockedPointer) { qDebug() << "ERROR when receiving locked pointer!"; return; } m_lockedPointer = lockedPointer; m_lockedPointerPersistent = persistent; + connect(lockedPointer, &LockedPointer::locked, this, [this]() { qDebug() << "------ LOCKED! ------"; + if(lockHint()) { + m_lockedPointer->setCursorPositionHint(QPointF(10., 10.)); + forceSurfaceCommit(); + } + Q_EMIT lockChanged(true); }); connect(lockedPointer, &LockedPointer::unlocked, this, [this]() { qDebug() << "------ UNLOCKED! ------"; if (!m_lockedPointerPersistent) { cleanupLock(); } Q_EMIT lockChanged(false); }); } void WaylandBackend::unlockRequest() { if (!m_lockedPointer) { qDebug() << "Unlock requested, but there is no lock. Abort."; return; } qDebug() << "------ Unlock requested ------"; cleanupLock(); Q_EMIT lockChanged(false); } void WaylandBackend::cleanupLock() { if (!m_lockedPointer) { return; } m_lockedPointer->release(); m_lockedPointer->deleteLater(); m_lockedPointer = nullptr; } void WaylandBackend::confineRequest(bool persistent, QRect region) { if (isConfined()) { if (!errorsAllowed()) { qDebug() << "Abort confining because already confined. Allow errors to test reconfining (and crashing)."; return; } qDebug() << "Trying to lock although already locked. Crash expected."; } if (isLocked()) { if (!errorsAllowed()) { qDebug() << "Abort confining because already locked. Allow errors to test confining while being locked (and crashing)."; return; } qDebug() << "Trying to confine although already locked. Crash expected."; } qDebug() << "------ Confine requested ------"; qDebug() << "Persistent:" << persistent << "| Region:" << region; QScopedPointer winSurface(Surface::fromWindow(view())); QScopedPointer wlRegion(m_compositor->createRegion(this)); wlRegion->add(region); auto *confinedPointer = m_pointerConstraints->confinePointer(winSurface.data(), m_pointer, wlRegion.data(), lifeTime(persistent), this); if (!confinedPointer) { qDebug() << "ERROR when receiving confined pointer!"; return; } m_confinedPointer = confinedPointer; m_confinedPointerPersistent = persistent; connect(confinedPointer, &ConfinedPointer::confined, this, [this]() { qDebug() << "------ CONFINED! ------"; Q_EMIT confineChanged(true); }); connect(confinedPointer, &ConfinedPointer::unconfined, this, [this]() { qDebug() << "------ UNCONFINED! ------"; if (!m_confinedPointerPersistent) { cleanupConfine(); } Q_EMIT confineChanged(false); }); } void WaylandBackend::unconfineRequest() { if (!m_confinedPointer) { qDebug() << "Unconfine requested, but there is no confine. Abort."; return; } qDebug() << "------ Unconfine requested ------"; cleanupConfine(); Q_EMIT confineChanged(false); } void WaylandBackend::cleanupConfine() { if (!m_confinedPointer) { return; } m_confinedPointer->release(); m_confinedPointer->deleteLater(); m_confinedPointer = nullptr; } XBackend::XBackend(QObject *parent) : Backend(parent) { setMode(Mode::X); if (m_xcbConn) { xcb_disconnect(m_xcbConn); free(m_xcbConn); } } void XBackend::init(QQuickView *view) { Backend::init(view); m_xcbConn = xcb_connect(NULL, NULL); if (!m_xcbConn) { qDebug() << "Could not open XCB connection."; } } void XBackend::lockRequest(bool persistent, QRect region) { Q_UNUSED(persistent); Q_UNUSED(region); auto winId = view()->winId(); /* Cursor needs to be hidden such that Xwayland emulates warps. */ QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor)); auto cookie = xcb_warp_pointer_checked(m_xcbConn, /* connection */ XCB_NONE, /* src_w */ winId, /* dest_w */ 0, /* src_x */ 0, /* src_y */ 0, /* src_width */ 0, /* src_height */ 20, /* dest_x */ 20 /* dest_y */ ); xcb_flush(m_xcbConn); xcb_generic_error_t *error = xcb_request_check(m_xcbConn, cookie); if (error) { qDebug() << "Lock (warp) failed with XCB error:" << error->error_code; free(error); return; } qDebug() << "LOCK (warp)"; Q_EMIT lockChanged(true); } void XBackend::unlockRequest() { /* Xwayland unlocks the pointer, when the cursor is shown again. */ QGuiApplication::restoreOverrideCursor(); qDebug() << "------ Unlock requested ------"; Q_EMIT lockChanged(false); } void XBackend::confineRequest(bool persistent, QRect region) { Q_UNUSED(persistent); Q_UNUSED(region); int error; if (!tryConfine(error)) { qDebug() << "Confine (grab) failed with XCB error:" << error; return; } qDebug() << "CONFINE (grab)"; Q_EMIT confineChanged(true); } void XBackend::unconfineRequest() { auto cookie = xcb_ungrab_pointer_checked(m_xcbConn, XCB_CURRENT_TIME); xcb_flush(m_xcbConn); xcb_generic_error_t *error = xcb_request_check(m_xcbConn, cookie); if (error) { qDebug() << "Unconfine failed with XCB error:" << error->error_code; free(error); return; } qDebug() << "UNCONFINE (ungrab)"; Q_EMIT confineChanged(false); } void XBackend::hideAndConfineRequest(bool confineBeforeHide) { if (!confineBeforeHide) { QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor)); } int error; if (!tryConfine(error)) { qDebug() << "Confine failed with XCB error:" << error; if (!confineBeforeHide) { QGuiApplication::restoreOverrideCursor(); } return; } if (confineBeforeHide) { QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor)); } qDebug() << "HIDE AND CONFINE (lock)"; Q_EMIT confineChanged(true); } void XBackend::undoHideRequest() { QGuiApplication::restoreOverrideCursor(); qDebug() << "UNDO HIDE AND CONFINE (unlock)"; } bool XBackend::tryConfine(int &error) { auto winId = view()->winId(); auto cookie = xcb_grab_pointer(m_xcbConn, /* display */ 1, /* owner_events */ winId, /* grab_window */ 0, /* event_mask */ XCB_GRAB_MODE_ASYNC, /* pointer_mode */ XCB_GRAB_MODE_ASYNC, /* keyboard_mode */ winId, /* confine_to */ XCB_NONE, /* cursor */ XCB_CURRENT_TIME /* time */ ); xcb_flush(m_xcbConn); xcb_generic_error_t *e = nullptr; auto *reply = xcb_grab_pointer_reply(m_xcbConn, cookie, &e); if (!reply) { error = e->error_code; free(e); return false; } free(reply); return true; } int main(int argc, char **argv) { QGuiApplication app(argc, argv); Backend *backend; if (app.platformName() == QStringLiteral("wayland")) { qDebug() << "Starting up: Wayland native mode"; backend = new WaylandBackend(&app); } else { qDebug() << "Starting up: Xserver/Xwayland legacy mode"; backend = new XBackend(&app); } QQuickView view; QQmlContext* context = view.engine()->rootContext(); context->setContextProperty(QStringLiteral("org_kde_kwin_tests_pointerconstraints_backend"), backend); view.setSource(QUrl::fromLocalFile(QStringLiteral(DIR) +QStringLiteral("/pointerconstraintstest.qml"))); view.show(); backend->init(&view); return app.exec(); } diff --git a/tests/pointerconstraintstest.h b/tests/pointerconstraintstest.h index b63fdb22f..7d828763c 100644 --- a/tests/pointerconstraintstest.h +++ b/tests/pointerconstraintstest.h @@ -1,169 +1,178 @@ /******************************************************************** Copyright 2018 Roman Gilg This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . *********************************************************************/ #ifndef POINTERCONSTRAINTSTEST_H #define POINTERCONSTRAINTSTEST_H #include #include #include namespace KWayland { namespace Client { class ConnectionThread; class Registry; class Compositor; class Seat; class Pointer; class PointerConstraints; class LockedPointer; class ConfinedPointer; } } class MainWindow; class Backend : public QObject { Q_OBJECT public: Backend(QObject *parent = nullptr) : QObject(parent) {} Q_PROPERTY(int mode READ mode CONSTANT) + Q_PROPERTY(bool lockHint MEMBER m_lockHint NOTIFY lockHintChanged) Q_PROPERTY(bool errorsAllowed READ errorsAllowed WRITE setErrorsAllowed NOTIFY errorsAllowedChanged) virtual void init(QQuickView *view) { m_view = view; } int mode() const { return (int)m_mode; } + + bool lockHint() const { + return m_lockHint; + } bool errorsAllowed() const { return m_errorsAllowed; } void setErrorsAllowed(bool set) { if (m_errorsAllowed == set) { return; } m_errorsAllowed = set; Q_EMIT errorsAllowedChanged(); } Q_INVOKABLE virtual void lockRequest(bool persistent = true, QRect region = QRect()) { Q_UNUSED(persistent); Q_UNUSED(region); } Q_INVOKABLE virtual void unlockRequest() {} Q_INVOKABLE virtual void confineRequest(bool persistent = true, QRect region = QRect()) { Q_UNUSED(persistent); Q_UNUSED(region); } Q_INVOKABLE virtual void unconfineRequest() {} Q_INVOKABLE virtual void hideAndConfineRequest(bool confineBeforeHide = false) { Q_UNUSED(confineBeforeHide); } Q_INVOKABLE virtual void undoHideRequest() {} Q_SIGNALS: void confineChanged(bool confined); void lockChanged(bool locked); + void lockHintChanged(); void errorsAllowedChanged(); + void forceSurfaceCommit(); protected: enum class Mode { Wayland = 0, X = 1 }; QQuickView* view() const { return m_view; } void setMode(Mode set) { m_mode = set; } private: QQuickView *m_view; Mode m_mode; + + bool m_lockHint = true; bool m_errorsAllowed = false; }; class WaylandBackend : public Backend { Q_OBJECT public: WaylandBackend(QObject *parent = nullptr); void init(QQuickView *view) override; void lockRequest(bool persistent, QRect region) override; void unlockRequest() override; void confineRequest(bool persistent, QRect region) override; void unconfineRequest() override; private: void setupRegistry(KWayland::Client::Registry *registry); bool isLocked(); bool isConfined(); void cleanupLock(); void cleanupConfine(); KWayland::Client::ConnectionThread *m_connectionThreadObject; KWayland::Client::Compositor *m_compositor = nullptr; KWayland::Client::Seat *m_seat = nullptr; KWayland::Client::Pointer *m_pointer = nullptr; KWayland::Client::PointerConstraints *m_pointerConstraints = nullptr; KWayland::Client::LockedPointer *m_lockedPointer = nullptr; bool m_lockedPointerPersistent = false; KWayland::Client::ConfinedPointer *m_confinedPointer = nullptr; bool m_confinedPointerPersistent = false; }; class XBackend : public Backend { Q_OBJECT public: XBackend(QObject *parent = nullptr); void init(QQuickView *view) override; void lockRequest(bool persistent, QRect region) override; void unlockRequest() override; void confineRequest(bool persistent, QRect region) override; void unconfineRequest() override; void hideAndConfineRequest(bool confineBeforeHide) override; void undoHideRequest() override; private: bool tryConfine(int &error); xcb_connection_t *m_xcbConn = nullptr; }; #endif diff --git a/tests/pointerconstraintstest.qml b/tests/pointerconstraintstest.qml index 6ea865a19..934fc653e 100644 --- a/tests/pointerconstraintstest.qml +++ b/tests/pointerconstraintstest.qml @@ -1,190 +1,219 @@ /******************************************************************** Copyright 2018 Roman Gilg This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . *********************************************************************/ import QtQuick 2.10 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.1 ColumnLayout { /* for margins */ ColumnLayout { id: root focus: true Layout.margins: 20 function lock() { org_kde_kwin_tests_pointerconstraints_backend.lockRequest(lockPersChck.checked, root.activRect()); } function confine() { org_kde_kwin_tests_pointerconstraints_backend.confineRequest(confPersChck.checked, root.activRect()); } function unlock() { org_kde_kwin_tests_pointerconstraints_backend.unlockRequest(); } function unconfine() { org_kde_kwin_tests_pointerconstraints_backend.unconfineRequest(); } function hideAndConfine() { org_kde_kwin_tests_pointerconstraints_backend.hideAndConfineRequest(); } function undoHideAndConfine() { org_kde_kwin_tests_pointerconstraints_backend.undoHideRequest(); } property bool waylandNative: org_kde_kwin_tests_pointerconstraints_backend.mode === 0 Keys.onPressed: { if (event.key === Qt.Key_L) { root.lock(); event.accepted = true; } else if (event.key === Qt.Key_C) { root.confine(); event.accepted = true; } else if (event.key === Qt.Key_K) { root.unlock(); event.accepted = true; } else if (event.key === Qt.Key_X) { root.unconfine(); event.accepted = true; } else if (event.key === Qt.Key_H) { root.hideAndConfine(); event.accepted = true; } else if (event.key === Qt.Key_G) { root.undoHideAndConfine(); event.accepted = true; } } function activRect() { if (fullWindowChck.checked) { return Qt.rect(0, 0, -1, -1); } return activArea.rect(); } + Connections { + target: org_kde_kwin_tests_pointerconstraints_backend + onForceSurfaceCommit: { + forceCommitRect.visible = true + } + } + + Rectangle { + id: forceCommitRect + width: 10 + height: 10 + color: "red" + visible: false + + Timer { + interval: 500 + running: forceCommitRect.visible + repeat: false + onTriggered: forceCommitRect.visible = false; + } + } + GridLayout { columns: 2 rowSpacing: 10 columnSpacing: 10 Button { id: lockButton text: "Lock pointer" onClicked: root.lock() } CheckBox { id: lockPersChck text: "Persistent lock" checked: root.waylandNative enabled: root.waylandNative } Button { id: confButton text: "Confine pointer" onClicked: root.confine() } CheckBox { id: confPersChck text: "Persistent confine" checked: root.waylandNative enabled: root.waylandNative } Button { id: hideConfButton text: "Hide and confine pointer" onClicked: root.hideAndConfine() visible: !root.waylandNative } CheckBox { id: confBeforeHideChck text: "Confine first, then hide" checked: false visible: !root.waylandNative } } + CheckBox { + id: lockHintChck + text: "Send position hint on lock" + checked: root.waylandNative + enabled: root.waylandNative + onCheckedChanged: org_kde_kwin_tests_pointerconstraints_backend.lockHint = checked; + } CheckBox { id: restrAreaChck text: "Restrict input area (not yet implemented)" enabled: false } CheckBox { id: fullWindowChck text: "Full window area activates" checked: !root.waylandNative enabled: root.waylandNative } CheckBox { id: errorsChck text: "Allow critical errors" checked: false enabled: root.waylandNative onCheckedChanged: org_kde_kwin_tests_pointerconstraints_backend.errorsAllowed = checked; } Item { width: childrenRect.width height: childrenRect.height Rectangle { id: activArea width: 400 height: 200 enabled: root.waylandNative && !fullWindowChck.checked function rect() { var globalPt = mapToGlobal(x, y); return Qt.rect(globalPt.x, globalPt.y, width, height); } border.color: enabled ? "black" : "lightgrey" border.width: 2 Label { anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter text: "Activation area" } } Button { id: unconfButton anchors.horizontalCenter: activArea.horizontalCenter anchors.verticalCenter: activArea.verticalCenter text: "Unconfine pointer" onClicked: root.unconfine() } } Label { text: "Lock: L / Unlock: K" } Label { text: "Confine: C / Unconfine: X" } Label { text: "Hide cursor and confine pointer: H / undo hide: G" visible: !root.waylandNative } } }