kwin/effects/slide/slide.cpp
Vlad Zagorodniy ddd406dfd9 [effects] replace old slide effect with a new one
Summary:
The new slide effect tries to separate each virtual desktop
as much as possible. This separation makes the new slide
effect more intuitive than the old one.

Test Plan:
* switch between virtual desktops
* or, move a window to another virtual desktop

Reviewers: #vdg, #kwin, #plasma, graesslin, ngraham

Reviewed By: #kwin, #plasma, graesslin

Subscribers: mart, graesslin, abetts, ngraham, plasma-devel, kwin

Tags: #kwin

Differential Revision: https://phabricator.kde.org/D9638
2018-02-16 06:09:27 +02:00

537 lines
16 KiB
C++

/********************************************************************
KWin - the KDE window manager
This file is part of the KDE project.
Copyright (C) 2007 Lubos Lunak <l.lunak@kde.org>
Copyright (C) 2008 Lucas Murray <lmurray@undefinedfire.com>
Copyright (C) 2018 Vlad Zagorodniy <vladzzag@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/
// Qt
#include <QEasingCurve>
// KWayland
#include <KWayland/Server/surface_interface.h>
#include <KWayland/Server/blur_interface.h>
#include <KWayland/Server/contrast_interface.h>
#include "slide.h"
// KConfigSkeleton
#include "slideconfig.h"
namespace KWin
{
SlideEffect::SlideEffect()
{
initConfig<SlideConfig>();
reconfigure(ReconfigureAll);
QEasingCurve curve(QEasingCurve::OutCubic);
m_timeline.setEasingCurve(curve);
connect(effects, static_cast<void (EffectsHandler::*)(int,int,EffectWindow*)>(&EffectsHandler::desktopChanged),
this, &SlideEffect::desktopChanged);
connect(effects, &EffectsHandler::windowAdded,
this, &SlideEffect::windowAdded);
connect(effects, &EffectsHandler::windowDeleted,
this, &SlideEffect::windowDeleted);
connect(effects, &EffectsHandler::numberDesktopsChanged,
this, &SlideEffect::numberDesktopsChanged);
connect(effects, &EffectsHandler::numberScreensChanged,
this, &SlideEffect::numberScreensChanged);
}
bool SlideEffect::supported()
{
return effects->animationsSupported();
}
void SlideEffect::reconfigure(ReconfigureFlags)
{
SlideConfig::self()->read();
const int d = animationTime(
SlideConfig::duration() > 0 ? SlideConfig::duration() : 500);
m_timeline.setDuration(d);
m_hGap = SlideConfig::horizontalGap();
m_vGap = SlideConfig::verticalGap();
m_slideDocks = SlideConfig::slideDocks();
}
void SlideEffect::prePaintScreen(ScreenPrePaintData& data, int time)
{
if (m_active) {
m_timeline.setCurrentTime(m_timeline.currentTime() + time);
data.mask |= PAINT_SCREEN_TRANSFORMED
| PAINT_SCREEN_BACKGROUND_FIRST;
}
effects->prePaintScreen(data, time);
}
/**
* Wrap vector @p diff around grid @p w x @p h.
*
* Wrapping is done in such a way that magnitude of x and y component of vector
* @p diff is less than half of @p w and half of @p h, respectively. This will
* result in having the "shortest" path between two points.
*
* @param diff Vector between two points
* @param w Width of the desktop grid
* @param h Height of the desktop grid
*/
inline void wrapDiff(QPoint& diff, int w, int h)
{
if (diff.x() > w/2) {
diff.setX(diff.x() - w);
} else if (diff.x() < -w/2) {
diff.setX(diff.x() + w);
}
if (diff.y() > h/2) {
diff.setY(diff.y() - h);
} else if (diff.y() < -h/2) {
diff.setY(diff.y() + h);
}
}
inline QRegion buildClipRegion(QPoint pos, int w, int h)
{
const QSize screenSize = effects->virtualScreenSize();
QRegion r = QRect(pos, screenSize);
if (effects->optionRollOverDesktops()) {
r |= (r & QRect(-w, 0, w, h)).translated(w, 0); // W
r |= (r & QRect(w, 0, w, h)).translated(-w, 0); // E
r |= (r & QRect(0, -h, w, h)).translated(0, h); // N
r |= (r & QRect(0, h, w, h)).translated(0, -h); // S
r |= (r & QRect(-w, -h, w, h)).translated(w, h); // NW
r |= (r & QRect(w, -h, w, h)).translated(-w, h); // NE
r |= (r & QRect(w, h, w, h)).translated(-w, -h); // SE
r |= (r & QRect(-w, h, w, h)).translated(w, -h); // SW
}
return r;
}
void SlideEffect::paintScreen(int mask, QRegion region, ScreenPaintData& data)
{
if (! m_active) {
effects->paintScreen(mask, region, data);
return;
}
const bool wrap = effects->optionRollOverDesktops();
const int w = workspaceWidth();
const int h = workspaceHeight();
QPoint currentPos = m_startPos + m_diff * m_timeline.currentValue();
// When "Desktop navigation wraps around" checkbox is checked, currentPos
// can be outside the rectangle Rect{x:-w, y:-h, width:2*w, height: 2*h},
// so we map currentPos back to the rect.
if (wrap) {
currentPos.setX(currentPos.x() % w);
currentPos.setY(currentPos.y() % h);
}
QVector<int> visibleDesktops;
visibleDesktops.reserve(4); // 4 - maximum number of visible desktops
QRegion clipRegion = buildClipRegion(currentPos, w, h);
for (int i = 1; i <= effects->numberOfDesktops(); i++) {
QRect desktopGeo = desktopGeometry(i);
if (! clipRegion.contains(desktopGeo)) {
continue;
}
visibleDesktops << i;
}
// When we enter a virtual desktop that has a window in fullscreen mode,
// stacking order is fine. When we leave a virtual desktop that has
// a window in fullscreen mode, stacking order is no longer valid
// because panels are raised above the fullscreen window. Construct
// a list of fullscreen windows, so we can decide later whether
// docks should be visible on different virtual desktops.
if (m_slideDocks) {
const auto windows = effects->stackingOrder();
m_paintCtx.fullscreenWindows.clear();
for (EffectWindow* w : windows) {
if (! w->isFullScreen()) {
continue;
}
m_paintCtx.fullscreenWindows << w;
}
}
// If screen is painted with either PAINT_SCREEN_TRANSFORMED or
// PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS there is no clipping!!
// Push the screen geometry to the paint clipper so everything outside
// of the screen geometry is clipped.
PaintClipper pc(QRegion(effects->virtualScreenGeometry()));
// Screen is painted in several passes. Each painting pass paints
// a single virtual desktop. There could be either 2 or 4 painting
// passes, depending how an user moves between virtual desktops.
// Windows, such as docks or keep-above windows, are painted in
// the last pass so they are above other windows.
const int lastDesktop = visibleDesktops.last();
for (int desktop : qAsConst(visibleDesktops)) {
m_paintCtx.desktop = desktop;
m_paintCtx.lastPass = (lastDesktop == desktop);
m_paintCtx.translation = desktopCoords(desktop) - currentPos;
if (wrap) {
wrapDiff(m_paintCtx.translation, w, h);
}
effects->paintScreen(mask, region, data);
}
}
/**
* Decide whether given window @p w should be transformed/translated.
* @returns @c true if given window @p w should be transformed, otherwise @c false
*/
bool SlideEffect::isTranslated(const EffectWindow* w) const
{
if (w->isOnAllDesktops()) {
if (w->isDock()) {
return m_slideDocks;
}
return w->isDesktop();
} else if (w == m_movingWindow) {
return false;
} else if (w->isOnDesktop(m_paintCtx.desktop)) {
return true;
}
return false;
}
/**
* Decide whether given window @p w should be painted.
* @returns @c true if given window @p w should be painted, otherwise @c false
*/
bool SlideEffect::isPainted(const EffectWindow* w) const
{
if (w->isOnAllDesktops()) {
if (w->isDock()) {
if (! m_slideDocks) {
return m_paintCtx.lastPass;
}
for (const EffectWindow* fw : qAsConst(m_paintCtx.fullscreenWindows)) {
if (fw->isOnDesktop(m_paintCtx.desktop)
&& fw->screen() == w->screen()) {
return false;
}
}
return true;
}
// In order to make sure that 'keep above' windows are above
// other windows during transition to another virtual desktop,
// they should be painted in the last pass.
if (w->keepAbove()) {
return m_paintCtx.lastPass;
}
return true;
} else if (w == m_movingWindow) {
return m_paintCtx.lastPass;
} else if (w->isOnDesktop(m_paintCtx.desktop)) {
return true;
}
return false;
}
void SlideEffect::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time)
{
if (m_active) {
const bool painted = isPainted(w);
if (painted) {
w->enablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP);
} else {
w->disablePainting(EffectWindow::PAINT_DISABLED_BY_DESKTOP);
}
if (painted && isTranslated(w)) {
data.setTransformed();
}
}
effects->prePaintWindow(w, data, time);
}
void SlideEffect::paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data)
{
if (m_active && isTranslated(w)) {
data += m_paintCtx.translation;
}
effects->paintWindow(w, mask, region, data);
}
void SlideEffect::postPaintScreen()
{
if (m_active) {
if (m_timeline.currentValue() == 1) {
stop();
}
effects->addRepaintFull();
}
effects->postPaintScreen();
}
/**
* Get position of the top-left corner of desktop @p id within desktop grid with gaps.
* @param id ID of a virtual desktop
*/
QPoint SlideEffect::desktopCoords(int id) const
{
QPoint c = effects->desktopCoords(id);
QPoint gridPos = effects->desktopGridCoords(id);
c.setX(c.x() + m_hGap * gridPos.x());
c.setY(c.y() + m_vGap * gridPos.y());
return c;
}
/**
* Get geometry of desktop @p id within desktop grid with gaps.
* @param id ID of a virtual desktop
*/
QRect SlideEffect::desktopGeometry(int id) const
{
QRect g = effects->virtualScreenGeometry();
g.translate(desktopCoords(id));
return g;
}
/**
* Get width of a virtual desktop grid.
*/
int SlideEffect::workspaceWidth() const
{
int w = effects->workspaceWidth();
w += m_hGap * effects->desktopGridWidth();
return w;
}
/**
* Get height of a virtual desktop grid.
*/
int SlideEffect::workspaceHeight() const
{
int h = effects->workspaceHeight();
h += m_vGap * effects->desktopGridHeight();
return h;
}
bool SlideEffect::shouldForceBlur(const EffectWindow* w) const
{
// While there is an active fullscreen effect, the blur effect
// tends to do nothing, i.e. it doesn't blur behind windows.
// So, we should force the blur effect to blur by setting
// WindowForceBlurRole.
if (w->data(WindowForceBlurRole).toBool()) {
return false;
}
if (w->data(WindowBlurBehindRole).isValid()) {
return true;
}
if (w->decorationHasAlpha() && effects->decorationSupportsBlurBehind()) {
return true;
}
// FIXME: it should be something like this:
// if (surf) {
// return !surf->blur().isNull();
// }
KWayland::Server::SurfaceInterface* surf = w->surface();
if (surf && surf->blur()) {
return true;
}
// TODO: make it X11-specific(check _KDE_NET_WM_BLUR_BEHIND_REGION)
// or delete it in the future
return w->hasAlpha();
}
bool SlideEffect::shouldForceBackgroundContrast(const EffectWindow* w) const
{
// While there is an active fullscreen effect, the background
// contrast effect tends to do nothing, i.e. it doesn't change
// contrast. So, we should force the background contrast effect
// to change contrast by setting WindowForceBackgroundContrastRole.
if (w->data(WindowForceBackgroundContrastRole).toBool()) {
return false;
}
if (w->data(WindowBackgroundContrastRole).isValid()) {
return true;
}
// FIXME: it should be something like this:
// if (surf) {
// return !surf->contrast().isNull();
// }
KWayland::Server::SurfaceInterface* surf = w->surface();
if (surf && surf->contrast()) {
return true;
}
// TODO: make it X11-specific(check _KDE_NET_WM_BACKGROUND_CONTRAST_REGION)
// or delete it in the future
return w->hasAlpha()
&& w->isOnAllDesktops()
&& (w->isDock() || w->keepAbove());
}
bool SlideEffect::shouldElevate(const EffectWindow* w) const
{
// Static docks(i.e. this effect doesn't slide docks) should be elevated
// so they can properly animate themselves when an user enters or leaves
// a virtual desktop with a window in fullscreen mode.
return w->isDock() && !m_slideDocks;
}
void SlideEffect::start(int old, int current, EffectWindow* movingWindow)
{
m_movingWindow = movingWindow;
const bool wrap = effects->optionRollOverDesktops();
const int w = workspaceWidth();
const int h = workspaceHeight();
if (m_active) {
QPoint passed = m_diff * m_timeline.currentValue();
QPoint currentPos = m_startPos + passed;
QPoint delta = desktopCoords(current) - desktopCoords(old);
if (wrap) {
wrapDiff(delta, w, h);
}
m_diff += delta - passed;
m_startPos = currentPos;
m_timeline.setCurrentTime(0);
return;
}
const auto windows = effects->stackingOrder();
for (EffectWindow* w : windows) {
if (shouldForceBlur(w)) {
w->setData(WindowForceBlurRole, QVariant(true));
m_forcedRoles.blur << w;
}
if (shouldForceBackgroundContrast(w)) {
w->setData(WindowForceBackgroundContrastRole, QVariant(true));
m_forcedRoles.backgroundContrast << w;
}
if (shouldElevate(w)) {
effects->setElevatedWindow(w, true);
m_elevatedWindows << w;
}
}
m_diff = desktopCoords(current) - desktopCoords(old);
if (wrap) {
wrapDiff(m_diff, w, h);
}
m_startPos = desktopCoords(old);
m_timeline.setCurrentTime(0);
m_active = true;
effects->setActiveFullScreenEffect(this);
effects->addRepaintFull();
}
void SlideEffect::stop()
{
for (EffectWindow* w : m_forcedRoles.blur) {
w->setData(WindowForceBlurRole, QVariant(false));
}
m_forcedRoles.blur.clear();
for (EffectWindow* w : m_forcedRoles.backgroundContrast) {
w->setData(WindowForceBackgroundContrastRole, QVariant(false));
}
m_forcedRoles.backgroundContrast.clear();
for (EffectWindow* w : m_elevatedWindows) {
effects->setElevatedWindow(w, false);
}
m_elevatedWindows.clear();
m_paintCtx.fullscreenWindows.clear();
m_timeline.setCurrentTime(0);
m_movingWindow = nullptr;
m_active = false;
effects->setActiveFullScreenEffect(nullptr);
}
void SlideEffect::desktopChanged(int old, int current, EffectWindow* with)
{
if (effects->activeFullScreenEffect() && effects->activeFullScreenEffect() != this) {
return;
}
start(old, current, with);
}
void SlideEffect::windowAdded(EffectWindow *w)
{
if (! m_active) {
return;
}
if (shouldForceBlur(w)) {
w->setData(WindowForceBlurRole, QVariant(true));
m_forcedRoles.blur << w;
}
if (shouldForceBackgroundContrast(w)) {
w->setData(WindowForceBackgroundContrastRole, QVariant(true));
m_forcedRoles.backgroundContrast << w;
}
if (shouldElevate(w)) {
effects->setElevatedWindow(w, true);
m_elevatedWindows << w;
}
}
void SlideEffect::windowDeleted(EffectWindow *w)
{
if (! m_active) {
return;
}
if (w == m_movingWindow) {
m_movingWindow = nullptr;
}
m_forcedRoles.blur.removeAll(w);
m_forcedRoles.backgroundContrast.removeAll(w);
m_elevatedWindows.removeAll(w);
m_paintCtx.fullscreenWindows.removeAll(w);
}
void SlideEffect::numberDesktopsChanged(uint)
{
if (! m_active) {
return;
}
stop();
}
void SlideEffect::numberScreensChanged()
{
if (! m_active) {
return;
}
stop();
}
} // namespace