diff --git a/CMakeLists.txt b/CMakeLists.txt index dbc80325f..bf059b52f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,7 +61,7 @@ else() set(Qca_LIBRARY qca-qt5) set(KF5_REQUIRED_COMPONENTS I18n ConfigWidgets DBusAddons IconThemes Notifications KIO KCMUtils Service Solid Kirigami2 People WindowSystem) - set(KF5_OPTIONAL_COMPONENTS DocTools) + set(KF5_OPTIONAL_COMPONENTS DocTools Wayland) set_package_properties(KF5Kirigami2 PROPERTIES DESCRIPTION "QtQuick plugins to build user interfaces based on KDE UX guidelines" diff --git a/app/qml/mousepad.qml b/app/qml/mousepad.qml index 70719c91f..c389ba0ea 100644 --- a/app/qml/mousepad.qml +++ b/app/qml/mousepad.qml @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ -import QtQuick 2.2 +import QtQuick 2.15 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.1 import org.kde.kirigami 2.7 as Kirigami @@ -20,6 +20,10 @@ Kirigami.Page // Otherwise swiping on the MouseArea might trigger changing the page Kirigami.ColumnView.preventStealing: true + Component.onCompleted: { + PointerLocker.window = applicationWindow() + } + ColumnLayout { anchors.fill: parent @@ -34,6 +38,37 @@ Kirigami.Page acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + Button { + id: lockButton + anchors.centerIn: parent + text: i18n("Lock") + visible: !Kirigami.Settings.tabletMode && !PointerLocker.isLocked + onClicked: { + PointerLocker.isLocked = true + area.pressedPos = Qt.point(-1, -1); + } + } + Label { + anchors.centerIn: parent + visible: PointerLocker.isLocked + text: i18n("Press the right 'x' key or the left and right mouse buttons at the same time to unlock") + } + + Connections { + target: PointerLocker + onPointerMoved: { + if (!PointerLocker.isLocked) + return; + mousepad.pluginInterface.moveCursor(Qt.point(delta.x, delta.y)); + } + } + + // We don't want to see the lock button when using a touchscreen + TapHandler { + acceptedDevices: PointerDevice.TouchScreen + onTapped: lockButton.visible = false + } + onClicked: { var clickType = ""; var packet = {}; @@ -90,8 +125,20 @@ Kirigami.Page lastPos = Qt.point(mouse.x, mouse.y); } + Keys.onPressed: { + if (event.key == Qt.Key_X) { + PointerLocker.isLocked = false + event.accepted = true; + } + } onPressed: { - pressedPos = Qt.point(mouse.x, mouse.y); + if (PointerLocker.isLocked) { + if (pressedButtons === (Qt.LeftButton | Qt.RightButton)) { + PointerLocker.isLocked = false + } + } else { + pressedPos = Qt.point(mouse.x, mouse.y); + } } onWheel: { diff --git a/declarativeplugin/CMakeLists.txt b/declarativeplugin/CMakeLists.txt index ffd07f912..39158ec32 100644 --- a/declarativeplugin/CMakeLists.txt +++ b/declarativeplugin/CMakeLists.txt @@ -2,6 +2,7 @@ add_library(kdeconnectdeclarativeplugin SHARED kdeconnectdeclarativeplugin.cpp responsewaiter.cpp objectfactory.cpp + pointerlocker.cpp resources.qrc ) target_link_libraries(kdeconnectdeclarativeplugin @@ -10,6 +11,14 @@ target_link_libraries(kdeconnectdeclarativeplugin kdeconnectcore ) +if (TARGET KF5::WaylandClient) + target_sources(kdeconnectdeclarativeplugin PRIVATE pointerlockerwayland.cpp) + target_link_libraries(kdeconnectdeclarativeplugin KF5::WaylandClient) + target_compile_definitions(kdeconnectdeclarativeplugin PRIVATE -DWITH_WAYLAND=1) +else() + target_compile_definitions(kdeconnectdeclarativeplugin PRIVATE -DWITH_WAYLAND=0) +endif() + install(TARGETS kdeconnectdeclarativeplugin DESTINATION ${QML_INSTALL_DIR}/org/kde/kdeconnect) install(FILES qmldir DESTINATION ${QML_INSTALL_DIR}/org/kde/kdeconnect) diff --git a/declarativeplugin/kdeconnectdeclarativeplugin.cpp b/declarativeplugin/kdeconnectdeclarativeplugin.cpp index 2711cf101..b72e41eb7 100644 --- a/declarativeplugin/kdeconnectdeclarativeplugin.cpp +++ b/declarativeplugin/kdeconnectdeclarativeplugin.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "objectfactory.h" #include "responsewaiter.h" @@ -22,6 +23,10 @@ #include #include "core/kdeconnectpluginconfig.h" #include "interfaces/commandsmodel.h" +#include "pointerlocker.h" +#if WITH_WAYLAND == 1 +#include "pointerlockerwayland.h" +#endif QObject* createDBusResponse() { @@ -63,6 +68,16 @@ void KdeConnectDeclarativePlugin::registerTypes(const char* uri) return new DaemonDbusInterface; } ); + qmlRegisterSingletonType("org.kde.kdeconnect", 1, 0, "PointerLocker", [] (QQmlEngine *, QJSEngine *) -> QObject * { + AbstractPointerLocker *ret; +#if WITH_WAYLAND == 1 + if (qGuiApp->platformName() == QLatin1String("wayland")) + ret = new PointerLockerWayland; + else +#endif + ret = new PointerLockerQt; + return ret; + }); #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) qmlRegisterAnonymousType(uri, 1); diff --git a/declarativeplugin/pointerlocker.cpp b/declarativeplugin/pointerlocker.cpp new file mode 100644 index 000000000..e6ba9b5f1 --- /dev/null +++ b/declarativeplugin/pointerlocker.cpp @@ -0,0 +1,72 @@ +/* + SPDX-FileCopyrightText: 2018 Roman Gilg + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "pointerlocker.h" + +#include +#include +#include +#include + +#include +#include + +void AbstractPointerLocker::setWindow(QWindow* window) +{ + if (m_window == window) { + return; + } + m_window = window; + Q_EMIT windowChanged(); +} + +PointerLockerQt::PointerLockerQt(QObject *parent) + : AbstractPointerLocker(parent) +{ +} + +PointerLockerQt::~PointerLockerQt() = default; + +void PointerLockerQt::setLocked(bool lock) +{ + if (isLocked() == lock) { + return; + } + + if (lock) { + /* Cursor needs to be hidden such that Xwayland emulates warps. */ + QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor)); + m_originalPosition = QCursor::pos(); + m_window->installEventFilter(this); + Q_EMIT lockedChanged(true); + Q_EMIT lockEffectiveChanged(true); + } else { + m_window->removeEventFilter(this); + QGuiApplication::restoreOverrideCursor(); + Q_EMIT lockedChanged(false); + Q_EMIT lockEffectiveChanged(false); + } +} + +bool PointerLockerQt::isLocked() const +{ + return !m_originalPosition.isNull(); +} + +bool PointerLockerQt::eventFilter(QObject *watched, QEvent *event) +{ + if (watched != m_window || event->type() != QEvent::MouseMove || !isLocked()) { + return false; + } + + const auto newPos = QCursor::pos(); + const QPointF dist = newPos - m_originalPosition; + Q_EMIT pointerMoved({ dist.x(), dist.y() }); + QCursor::setPos(m_originalPosition); + + return true; +} diff --git a/declarativeplugin/pointerlocker.h b/declarativeplugin/pointerlocker.h new file mode 100644 index 000000000..6278009e6 --- /dev/null +++ b/declarativeplugin/pointerlocker.h @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef POINTERLOCKER_H +#define POINTERLOCKER_H + +#include +#include + +class AbstractPointerLocker : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool isSupported READ isSupported NOTIFY supportedChanged) + Q_PROPERTY(bool isLocked READ isLocked WRITE setLocked NOTIFY lockedChanged) + Q_PROPERTY(bool isLockEffective READ isLockEffective NOTIFY lockEffectiveChanged) + Q_PROPERTY(QWindow* window READ window WRITE setWindow NOTIFY windowChanged) +public: + AbstractPointerLocker(QObject *parent = nullptr) : QObject(parent) {} + + virtual void setLocked(bool locked) = 0; + virtual bool isLocked() const = 0; + virtual bool isLockEffective() const = 0; + virtual bool isSupported() const = 0; + + virtual void setWindow(QWindow *window); + QWindow *window() const { + return m_window; + } + +Q_SIGNALS: + void supportedChanged(bool isSupported); + void lockedChanged(bool isLocked); + void lockEffectiveChanged(bool isLockEffective); + void windowChanged(); + void pointerMoved(const QPointF &delta); + +protected: + QWindow *m_window = nullptr; +}; + +class PointerLockerQt : public AbstractPointerLocker +{ + Q_OBJECT +public: + PointerLockerQt(QObject *parent = nullptr); + ~PointerLockerQt() override; + + void setLocked(bool locked) override; + bool isLocked() const override; + bool isSupported() const override { return true; } + bool isLockEffective() const override { return isLocked(); } + +private: + bool eventFilter(QObject *watched, QEvent *event) override; + + QPoint m_originalPosition; + bool m_moving = false; + +}; + +#endif diff --git a/declarativeplugin/pointerlockerwayland.cpp b/declarativeplugin/pointerlockerwayland.cpp new file mode 100644 index 000000000..9042bb95d --- /dev/null +++ b/declarativeplugin/pointerlockerwayland.cpp @@ -0,0 +1,170 @@ +/* + SPDX-FileCopyrightText: 2018 Roman Gilg + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "pointerlockerwayland.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace KWayland::Client; + +PointerLockerWayland::PointerLockerWayland(QObject *parent) + : AbstractPointerLocker(parent) + , m_connectionThreadObject(ConnectionThread::fromApplication(this)) +{ + setupRegistry(); +} + +void PointerLockerWayland::setupRegistry() +{ + Registry *registry = new Registry(this); + + connect(registry, &Registry::compositorAnnounced, this, + [this, registry](quint32 name, quint32 version) { + m_compositor = registry->createCompositor(name, version, this); + } + ); + connect(registry, &Registry::relativePointerManagerUnstableV1Announced, this, + [this, registry](quint32 name, quint32 version) { + Q_ASSERT(!m_relativePointerManager); + m_relativePointerManager = registry->createRelativePointerManager(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] (bool hasPointer) { + delete m_pointer; + + if (!hasPointer) + return; + + m_pointer = m_seat->createPointer(this); + + delete m_relativePointer; + m_relativePointer = m_relativePointerManager->createRelativePointer(m_pointer, this); + connect(m_relativePointer, &RelativePointer::relativeMotion, + this, [this] (const QSizeF &delta) { + Q_EMIT pointerMoved({delta.width(), delta.height()}); + }); + } + ); + } + ); + 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 PointerLockerWayland::isLockEffective() const +{ + return m_lockedPointer && m_lockedPointer->isValid(); +} + +void PointerLockerWayland::enforceLock() +{ + if (!m_isLocked) { + return; + } + + QScopedPointer winSurface(Surface::fromWindow(m_window)); + if (!winSurface) { + qWarning() << "Locking a window that is not mapped"; + return; + } + auto *lockedPointer = m_pointerConstraints->lockPointer(winSurface.data(), + m_pointer, + nullptr, + PointerConstraints::LifeTime::Persistent, + this); + + if (!lockedPointer) { + qDebug() << "ERROR when receiving locked pointer!"; + return; + } + m_lockedPointer = lockedPointer; + + connect(lockedPointer, &LockedPointer::locked, this, [this] { + Q_EMIT lockEffectiveChanged(true); + }); + connect(lockedPointer, &LockedPointer::unlocked, this, [this] { + Q_EMIT lockEffectiveChanged(false); + }); +} + +void PointerLockerWayland::setLocked(bool lock) +{ + if (m_isLocked == lock) { + return; + } + + if (!isSupported()) { + qWarning() << "Locking before having our interfaces announced"; + return; + } + + m_isLocked = lock; + if (lock) { + enforceLock(); + } else { + cleanupLock(); + } + Q_EMIT lockedChanged(lock); +} + +void PointerLockerWayland::cleanupLock() +{ + if (!m_lockedPointer) { + return; + } + m_lockedPointer->release(); + m_lockedPointer->deleteLater(); + m_lockedPointer = nullptr; + Q_EMIT lockEffectiveChanged(false); +} + +void PointerLockerWayland::setWindow(QWindow* window) +{ + if (m_window == window) { + return; + } + cleanupLock(); + + if (m_window) { + disconnect(m_window, &QWindow::visibleChanged, this, &PointerLockerWayland::enforceLock); + } + AbstractPointerLocker::setWindow(window); + connect(m_window, &QWindow::visibleChanged, this, &PointerLockerWayland::enforceLock); + + if (m_isLocked) { + enforceLock(); + } +} diff --git a/declarativeplugin/pointerlockerwayland.h b/declarativeplugin/pointerlockerwayland.h new file mode 100644 index 000000000..d988fe5bc --- /dev/null +++ b/declarativeplugin/pointerlockerwayland.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef POINTERLOCKERWAYLAND_H +#define POINTERLOCKERWAYLAND_H + +#include "pointerlocker.h" + +namespace KWayland { +namespace Client { + +class ConnectionThread; +class Registry; +class Compositor; +class Seat; +class Pointer; +class PointerConstraints; +class LockedPointer; +class ConfinedPointer; +class RelativePointer; +class RelativePointerManager; + +} +} + +class PointerLockerWayland : public AbstractPointerLocker +{ + Q_OBJECT +public: + PointerLockerWayland(QObject *parent = nullptr); + + void setLocked(bool locked) override; + bool isLocked() const override { return m_isLocked; } + bool isLockEffective() const override; + bool isSupported() const override { + return m_pointerConstraints && m_relativePointerManager; + } + + void setWindow(QWindow * window) override; + +private: + void setupRegistry(); + void enforceLock(); + void cleanupLock(); + + bool m_isLocked = false; + 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::RelativePointer *m_relativePointer = nullptr; + KWayland::Client::RelativePointerManager *m_relativePointerManager = nullptr; + + KWayland::Client::LockedPointer *m_lockedPointer = nullptr; +}; + +#endif