/* 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 #include #include #include #include #include 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 &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 fill; QList ends; QList 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 &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