This plugin helps you locate the cursor by enlarging it when the pointer is quickly moved back and forth. BUG: 432927wilder/Plasma/6.2
parent
5696081cf7
commit
831064f351
10 changed files with 459 additions and 0 deletions
@ -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 |
||||
) |
||||
@ -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" |
||||
@ -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" |
||||
} |
||||
} |
||||
@ -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 ®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" |
||||
@ -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 ®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<GLTexture> m_cursorTexture; |
||||
bool m_cursorTextureDirty = false; |
||||
bool m_mouseHidden = false; |
||||
}; |
||||
|
||||
} // namespace KWin
|
||||
@ -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> |
||||
@ -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 |
||||
@ -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; |
||||
} |
||||
@ -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; |
||||
}; |
||||
Loading…
Reference in new issue