From 831064f351e310afe50dc7cdf11b9aac3ce9cfc7 Mon Sep 17 00:00:00 2001 From: Vlad Zahorodnii Date: Sat, 25 Nov 2023 23:46:57 +0200 Subject: [PATCH] plugins: Add shakecursor plugin This plugin helps you locate the cursor by enlarging it when the pointer is quickly moved back and forth. BUG: 432927 --- src/plugins/CMakeLists.txt | 1 + src/plugins/shakecursor/CMakeLists.txt | 17 ++ src/plugins/shakecursor/main.cpp | 18 ++ src/plugins/shakecursor/metadata.json | 9 + src/plugins/shakecursor/shakecursor.cpp | 195 ++++++++++++++++++ src/plugins/shakecursor/shakecursor.h | 65 ++++++ .../shakecursor/shakecursorconfig.kcfg | 18 ++ .../shakecursor/shakecursorconfig.kcfgc | 8 + src/plugins/shakecursor/shakedetector.cpp | 85 ++++++++ src/plugins/shakecursor/shakedetector.h | 43 ++++ 10 files changed, 459 insertions(+) create mode 100644 src/plugins/shakecursor/CMakeLists.txt create mode 100644 src/plugins/shakecursor/main.cpp create mode 100644 src/plugins/shakecursor/metadata.json create mode 100644 src/plugins/shakecursor/shakecursor.cpp create mode 100644 src/plugins/shakecursor/shakecursor.h create mode 100644 src/plugins/shakecursor/shakecursorconfig.kcfg create mode 100644 src/plugins/shakecursor/shakecursorconfig.kcfgc create mode 100644 src/plugins/shakecursor/shakedetector.cpp create mode 100644 src/plugins/shakecursor/shakedetector.h diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt index 7fe83d6e19..8b2faeaada 100644 --- a/src/plugins/CMakeLists.txt +++ b/src/plugins/CMakeLists.txt @@ -89,6 +89,7 @@ add_subdirectory(screenedge) add_subdirectory(screenshot) add_subdirectory(screentransform) add_subdirectory(sessionquit) +add_subdirectory(shakecursor) add_subdirectory(sheet) add_subdirectory(showfps) add_subdirectory(showpaint) diff --git a/src/plugins/shakecursor/CMakeLists.txt b/src/plugins/shakecursor/CMakeLists.txt new file mode 100644 index 0000000000..95caa22d02 --- /dev/null +++ b/src/plugins/shakecursor/CMakeLists.txt @@ -0,0 +1,17 @@ +kwin_add_builtin_effect(shakecursor) + +target_sources(shakecursor PRIVATE + main.cpp + shakecursor.cpp + shakedetector.cpp +) + +kconfig_add_kcfg_files(shakecursor + shakecursorconfig.kcfgc +) + +target_link_libraries(shakecursor PRIVATE + kwin + + KF6::ConfigGui +) diff --git a/src/plugins/shakecursor/main.cpp b/src/plugins/shakecursor/main.cpp new file mode 100644 index 0000000000..b0df83ce7c --- /dev/null +++ b/src/plugins/shakecursor/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "plugins/shakecursor/shakecursor.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(ShakeCursorEffect, + "metadata.json.stripped", + return ShakeCursorEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/src/plugins/shakecursor/metadata.json b/src/plugins/shakecursor/metadata.json new file mode 100644 index 0000000000..b4ec1fd9d3 --- /dev/null +++ b/src/plugins/shakecursor/metadata.json @@ -0,0 +1,9 @@ +{ + "KPlugin": { + "Category": "Accessibility", + "Description": "Makes the cursor larger when the pointer is quickly moved back and forth", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Shake Cursor" + } +} diff --git a/src/plugins/shakecursor/shakecursor.cpp b/src/plugins/shakecursor/shakecursor.cpp new file mode 100644 index 0000000000..4626c92a89 --- /dev/null +++ b/src/plugins/shakecursor/shakecursor.cpp @@ -0,0 +1,195 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "plugins/shakecursor/shakecursor.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "cursor.h" +#include "effect/effecthandler.h" +#include "input_event.h" +#include "opengl/gltexture.h" +#include "opengl/glutils.h" +#include "plugins/shakecursor/shakecursorconfig.h" + +namespace KWin +{ + +ShakeCursorEffect::ShakeCursorEffect() + : m_cursor(Cursors::self()->mouse()) +{ + input()->installInputEventSpy(this); + + m_resetCursorScaleTimer.setSingleShot(true); + connect(&m_resetCursorScaleTimer, &QTimer::timeout, this, [this]() { + update(Transaction{ + .magnification = 1.0, + }); + }); + + ShakeCursorConfig::instance(effects->config()); + reconfigure(ReconfigureAll); +} + +ShakeCursorEffect::~ShakeCursorEffect() +{ + showCursor(); +} + +bool ShakeCursorEffect::supported() +{ + if (!effects->waylandDisplay()) { + return false; + } + return effects->isOpenGLCompositing(); +} + +void ShakeCursorEffect::reconfigure(ReconfigureFlags flags) +{ + ShakeCursorConfig::self()->read(); + + m_shakeDetector.setInterval(ShakeCursorConfig::timeInterval()); + m_shakeDetector.setSensitivity(ShakeCursorConfig::sensitivity()); +} + +bool ShakeCursorEffect::isActive() const +{ + return m_cursorMagnification != 1.0; +} + +void ShakeCursorEffect::pointerEvent(MouseEvent *event) +{ + if (event->type() != QEvent::MouseMove) { + return; + } + + if (const auto shakeFactor = m_shakeDetector.update(event)) { + update(Transaction{ + .position = m_cursor->pos(), + .hotspot = m_cursor->hotspot(), + .size = m_cursor->geometry().size(), + .magnification = 1.0 + ShakeCursorConfig::magnification() * shakeFactor.value(), + }); + m_resetCursorScaleTimer.start(1000); + } else { + update(Transaction{ + .magnification = 1.0, + }); + m_resetCursorScaleTimer.stop(); + } +} + +GLTexture *ShakeCursorEffect::ensureCursorTexture() +{ + if (!m_cursorTexture || m_cursorTextureDirty) { + m_cursorTexture.reset(); + m_cursorTextureDirty = false; + const auto cursor = effects->cursorImage(); + if (!cursor.image().isNull()) { + m_cursorTexture = GLTexture::upload(cursor.image()); + if (!m_cursorTexture) { + return nullptr; + } + m_cursorTexture->setWrapMode(GL_CLAMP_TO_EDGE); + m_cursorTexture->setFilter(GL_LINEAR); + } + } + return m_cursorTexture.get(); +} + +void ShakeCursorEffect::markCursorTextureDirty() +{ + m_cursorTextureDirty = true; + + update(Transaction{ + .position = m_cursor->pos(), + .hotspot = m_cursor->hotspot(), + .size = m_cursor->geometry().size(), + .magnification = m_cursorMagnification, + .damaged = true, + }); +} + +void ShakeCursorEffect::showCursor() +{ + if (m_mouseHidden) { + disconnect(effects, &EffectsHandler::cursorShapeChanged, this, &ShakeCursorEffect::markCursorTextureDirty); + effects->showCursor(); + if (m_cursorTexture) { + effects->makeOpenGLContextCurrent(); + m_cursorTexture.reset(); + } + m_cursorTextureDirty = false; + m_mouseHidden = false; + } +} + +void ShakeCursorEffect::hideCursor() +{ + if (!m_mouseHidden) { + effects->hideCursor(); + connect(effects, &EffectsHandler::cursorShapeChanged, this, &ShakeCursorEffect::markCursorTextureDirty); + m_mouseHidden = true; + } +} + +void ShakeCursorEffect::update(const Transaction &transaction) +{ + if (transaction.magnification == 1.0) { + if (m_cursorMagnification == 1.0) { + return; + } + + const QRectF oldCursorGeometry = m_cursorGeometry; + showCursor(); + + m_cursorGeometry = QRectF(); + m_cursorMagnification = 1.0; + + effects->addRepaint(oldCursorGeometry); + } else { + const QRectF oldCursorGeometry = m_cursorGeometry; + hideCursor(); + + m_cursorMagnification = transaction.magnification; + m_cursorGeometry = QRectF(transaction.position - transaction.hotspot * transaction.magnification, transaction.size * transaction.magnification); + + if (transaction.damaged || oldCursorGeometry != m_cursorGeometry) { + effects->addRepaint(oldCursorGeometry.united(m_cursorGeometry)); + } + } +} + +void ShakeCursorEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const QRegion ®ion, Output *screen) +{ + effects->paintScreen(renderTarget, viewport, mask, region, screen); + + if (GLTexture *texture = ensureCursorTexture()) { + const bool clipping = region != infiniteRegion(); + const QRegion clipRegion = clipping ? viewport.mapToRenderTarget(region) : infiniteRegion(); + if (clipping) { + glEnable(GL_SCISSOR_TEST); + } + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + auto shader = ShaderManager::instance()->pushShader(ShaderTrait::MapTexture | ShaderTrait::TransformColorspace); + shader->setColorspaceUniformsFromSRGB(renderTarget.colorDescription()); + QMatrix4x4 mvp = viewport.projectionMatrix(); + mvp.translate(m_cursorGeometry.x() * viewport.scale(), m_cursorGeometry.y() * viewport.scale()); + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + texture->render(clipRegion, m_cursorGeometry.size(), viewport.scale(), clipping); + ShaderManager::instance()->popShader(); + glDisable(GL_BLEND); + + if (clipping) { + glDisable(GL_SCISSOR_TEST); + } + } +} + +} // namespace KWin + +#include "moc_shakecursor.cpp" diff --git a/src/plugins/shakecursor/shakecursor.h b/src/plugins/shakecursor/shakecursor.h new file mode 100644 index 0000000000..ad353c8a06 --- /dev/null +++ b/src/plugins/shakecursor/shakecursor.h @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effect.h" +#include "input_event_spy.h" +#include "plugins/shakecursor/shakedetector.h" + +#include + +namespace KWin +{ + +class Cursor; +class GLTexture; + +class ShakeCursorEffect : public Effect, public InputEventSpy +{ + Q_OBJECT + +public: + ShakeCursorEffect(); + ~ShakeCursorEffect() override; + + static bool supported(); + + void reconfigure(ReconfigureFlags flags) override; + void pointerEvent(MouseEvent *event) override; + bool isActive() const override; + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const QRegion ®ion, Output *screen) override; + +private: + GLTexture *ensureCursorTexture(); + void markCursorTextureDirty(); + + void showCursor(); + void hideCursor(); + + struct Transaction + { + QPointF position; + QPointF hotspot; + QSizeF size; + qreal magnification; + bool damaged = false; + }; + void update(const Transaction &transaction); + + QTimer m_resetCursorScaleTimer; + ShakeDetector m_shakeDetector; + + Cursor *m_cursor; + QRectF m_cursorGeometry; + qreal m_cursorMagnification = 1.0; + + std::unique_ptr m_cursorTexture; + bool m_cursorTextureDirty = false; + bool m_mouseHidden = false; +}; + +} // namespace KWin diff --git a/src/plugins/shakecursor/shakecursorconfig.kcfg b/src/plugins/shakecursor/shakecursorconfig.kcfg new file mode 100644 index 0000000000..50985c9a7c --- /dev/null +++ b/src/plugins/shakecursor/shakecursorconfig.kcfg @@ -0,0 +1,18 @@ + + + + + + 1000 + + + 4 + + + 2 + + + diff --git a/src/plugins/shakecursor/shakecursorconfig.kcfgc b/src/plugins/shakecursor/shakecursorconfig.kcfgc new file mode 100644 index 0000000000..76b2117589 --- /dev/null +++ b/src/plugins/shakecursor/shakecursorconfig.kcfgc @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +# +# SPDX-License-Identifier: CC0-1.0 + +File=shakecursorconfig.kcfg +ClassName=ShakeCursorConfig +NameSpace=KWin +Singleton=true diff --git a/src/plugins/shakecursor/shakedetector.cpp b/src/plugins/shakecursor/shakedetector.cpp new file mode 100644 index 0000000000..246e5d0c85 --- /dev/null +++ b/src/plugins/shakecursor/shakedetector.cpp @@ -0,0 +1,85 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "shakedetector.h" + +#include + +ShakeDetector::ShakeDetector() +{ +} + +quint64 ShakeDetector::interval() const +{ + return m_interval; +} + +void ShakeDetector::setInterval(quint64 interval) +{ + m_interval = interval; +} + +qreal ShakeDetector::sensitivity() const +{ + return m_sensitivity; +} + +void ShakeDetector::setSensitivity(qreal sensitivity) +{ + m_sensitivity = sensitivity; +} + +std::optional ShakeDetector::update(QMouseEvent *event) +{ + // Prune the old entries in the history. + auto it = m_history.begin(); + for (; it != m_history.end(); ++it) { + if (event->timestamp() - it->timestamp < m_interval) { + break; + } + } + if (it != m_history.begin()) { + m_history.erase(m_history.begin(), it); + } + + m_history.append(HistoryItem{ + .position = event->localPos(), + .timestamp = event->timestamp(), + }); + + qreal left = m_history[0].position.x(); + qreal top = m_history[0].position.y(); + qreal right = m_history[0].position.x(); + qreal bottom = m_history[0].position.y(); + qreal distance = 0; + + for (int i = 1; i < m_history.size(); ++i) { + // Compute the length of the mouse path. + const qreal deltaX = m_history.at(i).position.x() - m_history.at(i - 1).position.x(); + const qreal deltaY = m_history.at(i).position.y() - m_history.at(i - 1).position.y(); + distance += std::sqrt(deltaX * deltaX + deltaY * deltaY); + + // Compute the bounds of the mouse path. + left = std::min(left, m_history.at(i).position.x()); + top = std::min(top, m_history.at(i).position.y()); + right = std::max(right, m_history.at(i).position.x()); + bottom = std::max(bottom, m_history.at(i).position.y()); + } + + const qreal boundsWidth = right - left; + const qreal boundsHeight = bottom - top; + const qreal diagonal = std::sqrt(boundsWidth * boundsWidth + boundsHeight * boundsHeight); + if (diagonal == 0) { + return std::nullopt; + } + + const qreal shakeFactor = distance / diagonal; + if (shakeFactor > m_sensitivity) { + return shakeFactor - m_sensitivity; + } + + return std::nullopt; +} diff --git a/src/plugins/shakecursor/shakedetector.h b/src/plugins/shakecursor/shakedetector.h new file mode 100644 index 0000000000..305fdb2dab --- /dev/null +++ b/src/plugins/shakecursor/shakedetector.h @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +/** + * The ShakeDetector type provides a way to detect pointer shake gestures. + * + * Shake gestures are detected by comparing the length of the trail of the cursor within past N milliseconds + * with the length of the diagonal of the bounding rectangle of the trail. If the trail is longer + * than the diagonal by certain preconfigured factor, it's assumed that the user shook the pointer. + */ +class ShakeDetector +{ +public: + ShakeDetector(); + + std::optional update(QMouseEvent *event); + + quint64 interval() const; + void setInterval(quint64 interval); + + qreal sensitivity() const; + void setSensitivity(qreal sensitivity); + +private: + struct HistoryItem + { + QPointF position; + quint64 timestamp; + }; + + QList m_history; + quint64 m_interval = 1000; + qreal m_sensitivity = 4; +};