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
This commit is contained in:
Vlad Zahorodnii 2023-11-25 23:46:57 +02:00
parent 5696081cf7
commit 831064f351
10 changed files with 459 additions and 0 deletions

View file

@ -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)

View file

@ -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
)

View file

@ -0,0 +1,18 @@
/*
SPDX-FileCopyrightText: 2023 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
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"

View file

@ -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"
}
}

View file

@ -0,0 +1,195 @@
/*
SPDX-FileCopyrightText: 2023 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
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 &region, 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"

View file

@ -0,0 +1,65 @@
/*
SPDX-FileCopyrightText: 2023 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
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 <QTimer>
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 &region, 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<GLTexture> m_cursorTexture;
bool m_cursorTextureDirty = false;
bool m_mouseHidden = false;
};
} // namespace KWin

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<kcfgfile arg="true"/>
<group name="Effect-shakecursor">
<entry name="TimeInterval" type="UInt">
<default>1000</default>
</entry>
<entry name="Sensitivity" type="Double">
<default>4</default>
</entry>
<entry name="Magnification" type="Double">
<default>2</default>
</entry>
</group>
</kcfg>

View file

@ -0,0 +1,8 @@
# SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
#
# SPDX-License-Identifier: CC0-1.0
File=shakecursorconfig.kcfg
ClassName=ShakeCursorConfig
NameSpace=KWin
Singleton=true

View file

@ -0,0 +1,85 @@
/*
SPDX-FileCopyrightText: 2023 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "shakedetector.h"
#include <cmath>
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<qreal> 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;
}

View file

@ -0,0 +1,43 @@
/*
SPDX-FileCopyrightText: 2023 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QMouseEvent>
#include <optional>
/**
* 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<qreal> 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<HistoryItem> m_history;
quint64 m_interval = 1000;
qreal m_sensitivity = 4;
};