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:
parent
5696081cf7
commit
831064f351
10 changed files with 459 additions and 0 deletions
|
@ -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)
|
||||
|
|
17
src/plugins/shakecursor/CMakeLists.txt
Normal file
17
src/plugins/shakecursor/CMakeLists.txt
Normal 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
|
||||
)
|
18
src/plugins/shakecursor/main.cpp
Normal file
18
src/plugins/shakecursor/main.cpp
Normal 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"
|
9
src/plugins/shakecursor/metadata.json
Normal file
9
src/plugins/shakecursor/metadata.json
Normal 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"
|
||||
}
|
||||
}
|
195
src/plugins/shakecursor/shakecursor.cpp
Normal file
195
src/plugins/shakecursor/shakecursor.cpp
Normal 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 ®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"
|
65
src/plugins/shakecursor/shakecursor.h
Normal file
65
src/plugins/shakecursor/shakecursor.h
Normal 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 ®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
|
18
src/plugins/shakecursor/shakecursorconfig.kcfg
Normal file
18
src/plugins/shakecursor/shakecursorconfig.kcfg
Normal 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>
|
8
src/plugins/shakecursor/shakecursorconfig.kcfgc
Normal file
8
src/plugins/shakecursor/shakecursorconfig.kcfgc
Normal 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
|
85
src/plugins/shakecursor/shakedetector.cpp
Normal file
85
src/plugins/shakecursor/shakedetector.cpp
Normal 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;
|
||||
}
|
43
src/plugins/shakecursor/shakedetector.h
Normal file
43
src/plugins/shakecursor/shakedetector.h
Normal 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;
|
||||
};
|
Loading…
Reference in a new issue