390 lines
12 KiB
C++
390 lines
12 KiB
C++
/*
|
|
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
|