/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2009 Marco Martin notmart @gmail.com SPDX-FileCopyrightText: 2018 Vlad Zahorodnii SPDX-License-Identifier: GPL-2.0-or-later */ #include "slidingpopups.h" #include "slidingpopupsconfig.h" #include #include #include #include #include #include #include Q_DECLARE_METATYPE(KWindowEffects::SlideFromLocation) namespace KWin { SlidingPopupsEffect::SlidingPopupsEffect() { initConfig(); KWaylandServer::Display *display = effects->waylandDisplay(); if (display) { display->createSlideManager(this)->create(); } m_slideLength = QFontMetrics(qApp->font()).height() * 8; m_atom = effects->announceSupportProperty("_KDE_SLIDE", this); connect(effects, &EffectsHandler::windowAdded, this, &SlidingPopupsEffect::slotWindowAdded); connect(effects, &EffectsHandler::windowClosed, this, &SlidingPopupsEffect::slideOut); connect(effects, &EffectsHandler::windowDeleted, this, &SlidingPopupsEffect::slotWindowDeleted); connect(effects, &EffectsHandler::propertyNotify, this, &SlidingPopupsEffect::slotPropertyNotify); connect(effects, &EffectsHandler::windowShown, this, &SlidingPopupsEffect::slideIn); connect(effects, &EffectsHandler::windowHidden, this, &SlidingPopupsEffect::slideOut); connect(effects, &EffectsHandler::xcbConnectionChanged, this, [this] { m_atom = effects->announceSupportProperty(QByteArrayLiteral("_KDE_SLIDE"), this); } ); connect(effects, qOverload(&EffectsHandler::desktopChanged), this, &SlidingPopupsEffect::stopAnimations); connect(effects, &EffectsHandler::activeFullScreenEffectChanged, this, &SlidingPopupsEffect::stopAnimations); reconfigure(ReconfigureAll); } SlidingPopupsEffect::~SlidingPopupsEffect() { } bool SlidingPopupsEffect::supported() { return effects->animationsSupported(); } void SlidingPopupsEffect::reconfigure(ReconfigureFlags flags) { Q_UNUSED(flags) SlidingPopupsConfig::self()->read(); m_slideInDuration = std::chrono::milliseconds( static_cast(animationTime(SlidingPopupsConfig::slideInTime() != 0 ? SlidingPopupsConfig::slideInTime() : 150))); m_slideOutDuration = std::chrono::milliseconds( static_cast(animationTime(SlidingPopupsConfig::slideOutTime() != 0 ? SlidingPopupsConfig::slideOutTime() : 250))); auto animationIt = m_animations.begin(); while (animationIt != m_animations.end()) { const auto duration = ((*animationIt).kind == AnimationKind::In) ? m_slideInDuration : m_slideOutDuration; (*animationIt).timeLine.setDuration(duration); ++animationIt; } auto dataIt = m_animationsData.begin(); while (dataIt != m_animationsData.end()) { (*dataIt).slideInDuration = m_slideInDuration; (*dataIt).slideOutDuration = m_slideOutDuration; ++dataIt; } } void SlidingPopupsEffect::prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) { auto animationIt = m_animations.find(w); if (animationIt == m_animations.end()) { effects->prePaintWindow(w, data, time); return; } (*animationIt).timeLine.update(std::chrono::milliseconds(time)); data.setTransformed(); w->enablePainting(EffectWindow::PAINT_DISABLED | EffectWindow::PAINT_DISABLED_BY_DELETE); effects->prePaintWindow(w, data, time); } void SlidingPopupsEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) { auto animationIt = m_animations.constFind(w); if (animationIt == m_animations.constEnd()) { effects->paintWindow(w, mask, region, data); return; } const AnimationData &animData = m_animationsData[w]; const int slideLength = (animData.slideLength > 0) ? animData.slideLength : m_slideLength; const QRect screenRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop()); int splitPoint = 0; const QRect geo = w->expandedGeometry(); const qreal t = (*animationIt).timeLine.value(); switch (animData.location) { case Location::Left: if (slideLength < geo.width()) { data.multiplyOpacity(t); } data.translate(-interpolate(qMin(geo.width(), slideLength), 0.0, t)); splitPoint = geo.width() - (geo.x() + geo.width() - screenRect.x() - animData.offset); region &= QRegion(geo.x() + splitPoint, geo.y(), geo.width() - splitPoint, geo.height()); break; case Location::Top: if (slideLength < geo.height()) { data.multiplyOpacity(t); } data.translate(0.0, -interpolate(qMin(geo.height(), slideLength), 0.0, t)); splitPoint = geo.height() - (geo.y() + geo.height() - screenRect.y() - animData.offset); region &= QRegion(geo.x(), geo.y() + splitPoint, geo.width(), geo.height() - splitPoint); break; case Location::Right: if (slideLength < geo.width()) { data.multiplyOpacity(t); } data.translate(interpolate(qMin(geo.width(), slideLength), 0.0, t)); splitPoint = screenRect.x() + screenRect.width() - geo.x() - animData.offset; region &= QRegion(geo.x(), geo.y(), splitPoint, geo.height()); break; case Location::Bottom: default: if (slideLength < geo.height()) { data.multiplyOpacity(t); } data.translate(0.0, interpolate(qMin(geo.height(), slideLength), 0.0, t)); splitPoint = screenRect.y() + screenRect.height() - geo.y() - animData.offset; region &= QRegion(geo.x(), geo.y(), geo.width(), splitPoint); } effects->paintWindow(w, mask, region, data); } void SlidingPopupsEffect::postPaintWindow(EffectWindow *w) { auto animationIt = m_animations.find(w); if (animationIt != m_animations.end()) { if ((*animationIt).timeLine.done()) { if (w->isDeleted()) { w->unrefWindow(); } else { w->setData(WindowForceBackgroundContrastRole, QVariant()); w->setData(WindowForceBlurRole, QVariant()); } m_animations.erase(animationIt); } w->addRepaintFull(); } effects->postPaintWindow(w); } void SlidingPopupsEffect::slotWindowAdded(EffectWindow *w) { //X11 if (m_atom != XCB_ATOM_NONE) { slotPropertyNotify(w, m_atom); } //Wayland if (auto surf = w->surface()) { slotWaylandSlideOnShowChanged(w); connect(surf, &KWaylandServer::SurfaceInterface::slideOnShowHideChanged, this, [this, surf] { slotWaylandSlideOnShowChanged(effects->findWindow(surf)); }); } if (auto internal = w->internalWindow()) { internal->installEventFilter(this); setupInternalWindowSlide(w); } slideIn(w); } void SlidingPopupsEffect::slotWindowDeleted(EffectWindow *w) { m_animations.remove(w); m_animationsData.remove(w); } void SlidingPopupsEffect::slotPropertyNotify(EffectWindow *w, long atom) { if (!w || atom != m_atom || m_atom == XCB_ATOM_NONE) { return; } // _KDE_SLIDE atom format(each field is an uint32_t): // [] [] [] // // If offset is equal to -1, this effect will decide what offset to use // given edge of the screen, from which the window has to slide. // // If slide in duration is equal to 0 milliseconds, the default slide in // duration will be used. Same with the slide out duration. // // NOTE: If only slide in duration has been provided, then it will be // also used as slide out duration. I.e. if you provided only slide in // duration, then slide in duration == slide out duration. const QByteArray rawAtomData = w->readProperty(m_atom, m_atom, 32); if (rawAtomData.isEmpty()) { // Property was removed, thus also remove the effect for window if (w->data(WindowClosedGrabRole).value() == this) { w->setData(WindowClosedGrabRole, QVariant()); } m_animations.remove(w); m_animationsData.remove(w); return; } // Offset and location are required. if (static_cast(rawAtomData.size()) < sizeof(uint32_t) * 2) { return; } const auto *atomData = reinterpret_cast(rawAtomData.data()); AnimationData &animData = m_animationsData[w]; animData.offset = atomData[0]; switch (atomData[1]) { case 0: // West animData.location = Location::Left; break; case 1: // North animData.location = Location::Top; break; case 2: // East animData.location = Location::Right; break; case 3: // South default: animData.location = Location::Bottom; break; } if (static_cast(rawAtomData.size()) >= sizeof(uint32_t) * 3) { animData.slideInDuration = std::chrono::milliseconds(atomData[2]); if (static_cast(rawAtomData.size()) >= sizeof(uint32_t) * 4) { animData.slideOutDuration = std::chrono::milliseconds(atomData[3]); } else { animData.slideOutDuration = animData.slideInDuration; } } else { animData.slideInDuration = m_slideInDuration; animData.slideOutDuration = m_slideOutDuration; } if (static_cast(rawAtomData.size()) >= sizeof(uint32_t) * 5) { animData.slideLength = atomData[4]; } else { animData.slideLength = 0; } setupAnimData(w); } void SlidingPopupsEffect::setupAnimData(EffectWindow *w) { const QRect screenRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop()); const QRect windowGeo = w->geometry(); AnimationData &animData = m_animationsData[w]; if (animData.offset == -1) { switch (animData.location) { case Location::Left: animData.offset = qMax(windowGeo.left() - screenRect.left(), 0); break; case Location::Top: animData.offset = qMax(windowGeo.top() - screenRect.top(), 0); break; case Location::Right: animData.offset = qMax(screenRect.right() - windowGeo.right(), 0); break; case Location::Bottom: default: animData.offset = qMax(screenRect.bottom() - windowGeo.bottom(), 0); break; } } // sanitize switch (animData.location) { case Location::Left: animData.offset = qMax(windowGeo.left() - screenRect.left(), animData.offset); break; case Location::Top: animData.offset = qMax(windowGeo.top() - screenRect.top(), animData.offset); break; case Location::Right: animData.offset = qMax(screenRect.right() - windowGeo.right(), animData.offset); break; case Location::Bottom: default: animData.offset = qMax(screenRect.bottom() - windowGeo.bottom(), animData.offset); break; } animData.slideInDuration = (animData.slideInDuration.count() != 0) ? animData.slideInDuration : m_slideInDuration; animData.slideOutDuration = (animData.slideOutDuration.count() != 0) ? animData.slideOutDuration : m_slideOutDuration; // Grab the window, so other windowClosed effects will ignore it w->setData(WindowClosedGrabRole, QVariant::fromValue(static_cast(this))); } void SlidingPopupsEffect::slotWaylandSlideOnShowChanged(EffectWindow* w) { if (!w) { return; } KWaylandServer::SurfaceInterface *surf = w->surface(); if (!surf) { return; } if (surf->slideOnShowHide()) { AnimationData &animData = m_animationsData[w]; animData.offset = surf->slideOnShowHide()->offset(); switch (surf->slideOnShowHide()->location()) { case KWaylandServer::SlideInterface::Location::Top: animData.location = Location::Top; break; case KWaylandServer::SlideInterface::Location::Left: animData.location = Location::Left; break; case KWaylandServer::SlideInterface::Location::Right: animData.location = Location::Right; break; case KWaylandServer::SlideInterface::Location::Bottom: default: animData.location = Location::Bottom; break; } animData.slideLength = 0; animData.slideInDuration = m_slideInDuration; animData.slideOutDuration = m_slideOutDuration; setupAnimData(w); } } void SlidingPopupsEffect::setupInternalWindowSlide(EffectWindow *w) { if (!w) { return; } auto internal = w->internalWindow(); if (!internal) { return; } const QVariant slideProperty = internal->property("kwin_slide"); if (!slideProperty.isValid()) { return; } Location location; switch (slideProperty.value()) { case KWindowEffects::BottomEdge: location = Location::Bottom; break; case KWindowEffects::TopEdge: location = Location::Top; break; case KWindowEffects::RightEdge: location = Location::Right; break; case KWindowEffects::LeftEdge: location = Location::Left; break; default: return; } AnimationData &animData = m_animationsData[w]; animData.location = location; bool intOk = false; animData.offset = internal->property("kwin_slide_offset").toInt(&intOk); if (!intOk) { animData.offset = -1; } animData.slideLength = 0; animData.slideInDuration = m_slideInDuration; animData.slideOutDuration = m_slideOutDuration; setupAnimData(w); } bool SlidingPopupsEffect::eventFilter(QObject *watched, QEvent *event) { auto internal = qobject_cast(watched); if (internal && event->type() == QEvent::DynamicPropertyChange) { QDynamicPropertyChangeEvent *pe = static_cast(event); if (pe->propertyName() == "kwin_slide" || pe->propertyName() == "kwin_slide_offset") { if (auto w = effects->findWindow(internal)) { setupInternalWindowSlide(w); } } } return false; } void SlidingPopupsEffect::slideIn(EffectWindow *w) { if (effects->activeFullScreenEffect()) { return; } if (!w->isVisible()) { return; } auto dataIt = m_animationsData.constFind(w); if (dataIt == m_animationsData.constEnd()) { return; } Animation &animation = m_animations[w]; animation.kind = AnimationKind::In; animation.timeLine.setDirection(TimeLine::Forward); animation.timeLine.setDuration((*dataIt).slideInDuration); animation.timeLine.setEasingCurve(QEasingCurve::OutCubic); // If the opposite animation (Out) was active and it had shorter duration, // at this point, the timeline can end up in the "done" state. Thus, we have // to reset it. if (animation.timeLine.done()) { animation.timeLine.reset(); } w->setData(WindowAddedGrabRole, QVariant::fromValue(static_cast(this))); w->setData(WindowForceBackgroundContrastRole, QVariant(true)); w->setData(WindowForceBlurRole, QVariant(true)); w->addRepaintFull(); } void SlidingPopupsEffect::slideOut(EffectWindow *w) { if (effects->activeFullScreenEffect()) { return; } if (!w->isVisible()) { return; } auto dataIt = m_animationsData.constFind(w); if (dataIt == m_animationsData.constEnd()) { return; } if (w->isDeleted()) { w->refWindow(); } Animation &animation = m_animations[w]; animation.kind = AnimationKind::Out; animation.timeLine.setDirection(TimeLine::Backward); animation.timeLine.setDuration((*dataIt).slideOutDuration); // this is effectively InCubic because the direction is reversed animation.timeLine.setEasingCurve(QEasingCurve::OutCubic); // If the opposite animation (In) was active and it had shorter duration, // at this point, the timeline can end up in the "done" state. Thus, we have // to reset it. if (animation.timeLine.done()) { animation.timeLine.reset(); } w->setData(WindowClosedGrabRole, QVariant::fromValue(static_cast(this))); w->setData(WindowForceBackgroundContrastRole, QVariant(true)); w->setData(WindowForceBlurRole, QVariant(true)); w->addRepaintFull(); } void SlidingPopupsEffect::stopAnimations() { for (auto it = m_animations.constBegin(); it != m_animations.constEnd(); ++it) { EffectWindow *w = it.key(); if (w->isDeleted()) { w->unrefWindow(); } else { w->setData(WindowForceBackgroundContrastRole, QVariant()); w->setData(WindowForceBlurRole, QVariant()); } } m_animations.clear(); } bool SlidingPopupsEffect::isActive() const { return !m_animations.isEmpty(); } } // namespace