Initial WindowGrid KWin effect for Plasma 6

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Sonnet 5.5 committed 2026-10-22 21:30:15 -07:00
commit 4da140193f
9 files changed
+1291

No files matched your search

+389
View File
@@ -0,0 +1,389 @@
/*
SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "windowgrid.h"
#include "core/output.h"
#include "core/inputdevice.h"
#include "core/rendertarget.h"
#include "core/renderviewport.h"
#include "effect/effecthandler.h"
#include "effect/effectwindow.h"
#include "input_event.h"
#include "opengl/glutils.h"
#include "window.h"
#include <KConfigGroup>
#include <KSharedConfig>
#include <QKeyEvent>
#include <QVector2D>
#include <algorithm>
#include <cmath>
namespace KWin
{
WindowGridEffect::WindowGridEffect()
{
reconfigure(ReconfigureAll);
const auto windows = effects->stackingOrder();
for (EffectWindow *w : windows) {
watchWindow(w);
}
connect(effects, &EffectsHandler::windowAdded, this, &WindowGridEffect::watchWindow);
}
WindowGridEffect::~WindowGridEffect()
{
if (m_state != State::Idle) {
finish(false);
}
}
void WindowGridEffect::reconfigure(ReconfigureFlags)
{
KSharedConfig::openConfig(QStringLiteral("kwinrc"))->reparseConfiguration();
const KConfigGroup cfg = KSharedConfig::openConfig(QStringLiteral("kwinrc"))->group(QStringLiteral("Effect-windowgrid"));
m_cfgColumns = std::clamp(cfg.readEntry("Columns", 6), 1, 64);
m_cfgRows = std::clamp(cfg.readEntry("Rows", 4), 1, 64);
m_useWorkArea = cfg.readEntry("UseWorkArea", true);
m_gridColor = cfg.readEntry("GridColor", QColor(255, 255, 255));
m_selectionColor = cfg.readEntry("SelectionColor", QColor(0x3d, 0xae, 0xe9));
m_opacity = std::clamp(cfg.readEntry("Opacity", 0.35), 0.0, 1.0);
}
bool WindowGridEffect::isActive() const
{
return m_state != State::Idle;
}
void WindowGridEffect::watchWindow(EffectWindow *w)
{
connect(w, &EffectWindow::windowStartUserMovedResized, this, &WindowGridEffect::onMoveStarted);
connect(w, &EffectWindow::windowFinishUserMovedResized, this, &WindowGridEffect::onMoveFinished);
}
void WindowGridEffect::onMoveStarted(EffectWindow *w)
{
if (m_state != State::Idle || !w->isUserMove() || !w->isMovable()) {
return;
}
m_state = State::Native;
m_window = w;
m_originalGeometry = w->frameGeometry();
m_cursor = effects->cursorPos();
m_rightHeld = false;
m_spaceHeld = false;
m_mouseConnection = connect(effects, &EffectsHandler::mouseChanged, this, &WindowGridEffect::onMouseChanged);
m_keyboardGrabbed = effects->grabKeyboard(this);
}
void WindowGridEffect::onMoveFinished(EffectWindow *w)
{
if (m_state == State::Native && w == m_window) {
finish(false);
}
}
void WindowGridEffect::onMouseChanged(const QPointF &pos, const QPointF &, Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, Qt::KeyboardModifiers, Qt::KeyboardModifiers)
{
if (m_state != State::Native || !m_window) {
return;
}
m_cursor = pos;
const auto pressed = [&](Qt::MouseButton b) {
return (buttons & b) && !(oldbuttons & b);
};
if (pressed(Qt::RightButton)) {
enterGrid(false);
} else if (pressed(Qt::MiddleButton)) {
m_grabOffset = pos - m_window->frameGeometry().topLeft();
if (m_mouseConnection) {
disconnect(m_mouseConnection);
}
m_state = State::Moving;
effects->startMouseInterception(this, Qt::SizeAllCursor);
effects->addRepaintFull();
}
}
void WindowGridEffect::enterGrid(bool viaSpace)
{
if (!m_window) {
return;
}
if (m_mouseConnection) {
disconnect(m_mouseConnection);
}
m_state = State::Grid;
m_screen = nullptr;
updateArea(m_cursor);
// taking over the pointer ends KWin's own interactive move
effects->startMouseInterception(this, Qt::SizeAllCursor);
setSelecting(!viaSpace, viaSpace, m_cursor);
effects->addRepaintFull();
}
void WindowGridEffect::finish(bool restoreGeometry)
{
const bool intercepting = m_state == State::Grid || m_state == State::Moving;
if (restoreGeometry && m_window && intercepting) {
m_window->window()->moveResize(RectF(m_originalGeometry.x(), m_originalGeometry.y(), m_originalGeometry.width(), m_originalGeometry.height()));
}
if (m_mouseConnection) {
disconnect(m_mouseConnection);
}
if (intercepting) {
effects->stopMouseInterception(this);
}
if (m_keyboardGrabbed) {
effects->ungrabKeyboard();
m_keyboardGrabbed = false;
}
m_state = State::Idle;
m_window = nullptr;
m_screen = nullptr;
m_rightHeld = false;
m_spaceHeld = false;
effects->addRepaintFull();
}
void WindowGridEffect::updateArea(const QPointF &pos)
{
LogicalOutput *screen = effects->screenAt(pos.toPoint());
if (!screen || screen == m_screen) {
return;
}
const bool first = m_screen == nullptr;
m_screen = screen;
m_area = m_useWorkArea ? QRectF(effects->clientArea(MaximizeArea, screen)) : QRectF(screen->geometryF());
// portrait monitors get the grid transposed
if (m_area.height() > m_area.width()) {
m_cols = m_cfgRows;
m_rows = m_cfgColumns;
} else {
m_cols = m_cfgColumns;
m_rows = m_cfgRows;
}
if (!first) {
m_start = m_end = cellAt(pos);
}
}
WindowGridEffect::Cell WindowGridEffect::cellAt(const QPointF &pos) const
{
const double cw = m_area.width() / m_cols;
const double ch = m_area.height() / m_rows;
Cell c;
c.col = std::clamp(int(std::floor((pos.x() - m_area.x()) / cw)), 0, m_cols - 1);
c.row = std::clamp(int(std::floor((pos.y() - m_area.y()) / ch)), 0, m_rows - 1);
return c;
}
QRectF WindowGridEffect::cellRect(Cell a, Cell b) const
{
const double cw = m_area.width() / m_cols;
const double ch = m_area.height() / m_rows;
const int c0 = std::min(a.col, b.col);
const int c1 = std::max(a.col, b.col);
const int r0 = std::min(a.row, b.row);
const int r1 = std::max(a.row, b.row);
const double x0 = std::round(m_area.x() + c0 * cw);
const double y0 = std::round(m_area.y() + r0 * ch);
const double x1 = std::round(m_area.x() + (c1 + 1) * cw);
const double y1 = std::round(m_area.y() + (r1 + 1) * ch);
return QRectF(x0, y0, x1 - x0, y1 - y0);
}
void WindowGridEffect::setSelecting(bool rightButton, bool space, const QPointF &pos)
{
m_rightHeld = rightButton;
m_spaceHeld = space;
if (m_rightHeld || m_spaceHeld) {
// while the selector is held the start cell follows the cursor
m_start = m_end = cellAt(pos);
}
}
void WindowGridEffect::applySelection()
{
if (!m_window) {
return;
}
const QRectF r = cellRect(m_start, m_end);
m_window->window()->moveResize(RectF(r.x(), r.y(), r.width(), r.height()));
}
void WindowGridEffect::moveWithCursor(const QPointF &pos)
{
if (!m_window) {
return;
}
const QPointF tl = pos - m_grabOffset;
const QSizeF size = m_window->frameGeometry().size();
m_window->window()->moveResize(RectF(tl.x(), tl.y(), size.width(), size.height()));
}
void WindowGridEffect::pointerMotion(PointerMotionEvent *event)
{
m_cursor = event->position;
if (m_state == State::Moving) {
moveWithCursor(m_cursor);
} else if (m_state == State::Grid) {
updateArea(m_cursor);
if (m_rightHeld || m_spaceHeld) {
m_start = m_end = cellAt(m_cursor);
} else {
m_end = cellAt(m_cursor);
applySelection();
}
}
effects->addRepaintFull();
}
void WindowGridEffect::pointerButton(PointerButtonEvent *event)
{
const bool down = event->state == PointerButtonState::Pressed;
m_cursor = event->position;
switch (event->button) {
case Qt::LeftButton:
if (!down) {
if (m_state == State::Grid) {
if (m_rightHeld || m_spaceHeld) {
m_end = m_start;
}
applySelection();
}
finish(false);
}
return;
case Qt::RightButton:
if (m_state == State::Grid) {
if (down) {
setSelecting(true, m_spaceHeld, m_cursor);
} else {
m_rightHeld = false;
if (!m_spaceHeld) {
m_end = cellAt(m_cursor);
applySelection();
}
}
}
break;
case Qt::MiddleButton:
if (down && m_state == State::Grid && m_window) {
m_grabOffset = m_cursor - m_window->frameGeometry().topLeft();
m_state = State::Moving;
}
break;
default:
break;
}
effects->addRepaintFull();
}
void WindowGridEffect::grabbedKeyboardEvent(QKeyEvent *e)
{
if (e->isAutoRepeat()) {
return;
}
const bool down = e->type() == QEvent::KeyPress;
if (e->key() == Qt::Key_Escape && down) {
if (m_state == State::Native && m_window) {
m_window->window()->cancelInteractiveMoveResize();
} else if (m_state != State::Idle) {
finish(true);
}
return;
}
if (e->key() != Qt::Key_Space) {
return;
}
if (m_state == State::Native && down) {
enterGrid(true);
} else if (m_state == State::Grid) {
if (down) {
setSelecting(m_rightHeld, true, m_cursor);
} else {
m_spaceHeld = false;
if (!m_rightHeld) {
m_end = cellAt(m_cursor);
applySelection();
}
}
effects->addRepaintFull();
}
}
bool WindowGridEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen)
{
if (!effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen)) {
return false;
}
if (m_state != State::Grid || screen != m_screen) {
return true;
}
const double scale = viewport.scale();
const double cw = m_area.width() / m_cols;
const double ch = m_area.height() / m_rows;
const double line = std::max(1.0, scale);
const auto addRect = [&](QList<QVector2D> &v, double x, double y, double w, double h) {
const double x0 = x * scale;
const double y0 = y * scale;
const double x1 = (x + w) * scale;
const double y1 = (y + h) * scale;
v.push_back(QVector2D(x1, y0));
v.push_back(QVector2D(x0, y0));
v.push_back(QVector2D(x0, y1));
v.push_back(QVector2D(x0, y1));
v.push_back(QVector2D(x1, y1));
v.push_back(QVector2D(x1, y0));
};
// selection fill, then the two corner cells a bit stronger, then the grid lines
QList<QVector2D> fill;
QList<QVector2D> ends;
QList<QVector2D> lines;
const QRectF sel = cellRect(m_start, m_end);
addRect(fill, sel.x(), sel.y(), sel.width(), sel.height());
for (const Cell &c : {m_start, m_end}) {
const QRectF r = cellRect(c, c);
addRect(ends, r.x(), r.y(), r.width(), r.height());
}
for (int i = 0; i <= m_cols; ++i) {
addRect(lines, std::round(m_area.x() + i * cw) - line / (2 * scale), m_area.y(), line / scale, m_area.height());
}
for (int j = 0; j <= m_rows; ++j) {
addRect(lines, m_area.x(), std::round(m_area.y() + j * ch) - line / (2 * scale), m_area.width(), line / scale);
}
GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer();
ShaderBinder binder(ShaderTrait::UniformColor | ShaderTrait::TransformColorspace);
binder.shader()->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, viewport.projectionMatrix());
binder.shader()->setColorspaceUniforms(ColorDescription::sRGB, renderTarget.colorDescription(), RenderingIntent::Perceptual);
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
const auto draw = [&](const QList<QVector2D> &verts, QColor color, double alpha) {
// premultiplied, matching the blend function above
color.setRgbF(color.redF() * alpha, color.greenF() * alpha, color.blueF() * alpha, alpha);
binder.shader()->setUniform(GLShader::ColorUniform::Color, color);
vbo->reset();
vbo->setVertices(verts);
vbo->render(GL_TRIANGLES);
};
draw(fill, m_selectionColor, m_opacity);
draw(ends, m_selectionColor, m_opacity);
draw(lines, m_gridColor, std::min(1.0, m_opacity * 1.5));
glDisable(GL_BLEND);
return true;
}
} // namespace KWin