File indexing completed on 2024-11-10 04:57:10

0001 /*
0002     KWin - the KDE window manager
0003     This file is part of the KDE project.
0004 
0005     SPDX-FileCopyrightText: 2009 Marco Martin notmart @gmail.com
0006     SPDX-FileCopyrightText: 2018 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "slidingpopups.h"
0012 #include "slidingpopupsconfig.h"
0013 
0014 #include "effect/effecthandler.h"
0015 #include "wayland/display.h"
0016 #include "wayland/slide.h"
0017 #include "wayland/surface.h"
0018 
0019 #include <QFontMetrics>
0020 #include <QGuiApplication>
0021 #include <QTimer>
0022 #include <QWindow>
0023 
0024 #include <KWindowEffects>
0025 
0026 Q_DECLARE_METATYPE(KWindowEffects::SlideFromLocation)
0027 
0028 namespace KWin
0029 {
0030 
0031 SlideManagerInterface *SlidingPopupsEffect::s_slideManager = nullptr;
0032 QTimer *SlidingPopupsEffect::s_slideManagerRemoveTimer = nullptr;
0033 
0034 SlidingPopupsEffect::SlidingPopupsEffect()
0035 {
0036     SlidingPopupsConfig::instance(effects->config());
0037 
0038     Display *display = effects->waylandDisplay();
0039     if (display) {
0040         if (!s_slideManagerRemoveTimer) {
0041             s_slideManagerRemoveTimer = new QTimer(QCoreApplication::instance());
0042             s_slideManagerRemoveTimer->setSingleShot(true);
0043             s_slideManagerRemoveTimer->callOnTimeout([]() {
0044                 s_slideManager->remove();
0045                 s_slideManager = nullptr;
0046             });
0047         }
0048         s_slideManagerRemoveTimer->stop();
0049         if (!s_slideManager) {
0050             s_slideManager = new SlideManagerInterface(display, s_slideManagerRemoveTimer);
0051         }
0052     }
0053 
0054     m_slideLength = QFontMetrics(QGuiApplication::font()).height() * 8;
0055 
0056     m_atom = effects->announceSupportProperty("_KDE_SLIDE", this);
0057     connect(effects, &EffectsHandler::windowAdded, this, &SlidingPopupsEffect::slotWindowAdded);
0058     connect(effects, &EffectsHandler::windowClosed, this, &SlidingPopupsEffect::slotWindowClosed);
0059     connect(effects, &EffectsHandler::windowDeleted, this, &SlidingPopupsEffect::slotWindowDeleted);
0060     connect(effects, &EffectsHandler::propertyNotify, this, &SlidingPopupsEffect::slotPropertyNotify);
0061     connect(effects, &EffectsHandler::xcbConnectionChanged, this, [this]() {
0062         m_atom = effects->announceSupportProperty(QByteArrayLiteral("_KDE_SLIDE"), this);
0063     });
0064     connect(effects, &EffectsHandler::desktopChanged,
0065             this, &SlidingPopupsEffect::stopAnimations);
0066     connect(effects, &EffectsHandler::activeFullScreenEffectChanged,
0067             this, &SlidingPopupsEffect::stopAnimations);
0068     connect(effects, &EffectsHandler::screenLockingChanged,
0069             this, &SlidingPopupsEffect::stopAnimations);
0070 
0071     reconfigure(ReconfigureAll);
0072 
0073     const QList<EffectWindow *> windows = effects->stackingOrder();
0074     for (EffectWindow *window : windows) {
0075         setupSlideData(window);
0076     }
0077 }
0078 
0079 SlidingPopupsEffect::~SlidingPopupsEffect()
0080 {
0081     // When compositing is restarted, avoid removing the manager immediately.
0082     if (s_slideManager) {
0083         s_slideManagerRemoveTimer->start(1000);
0084     }
0085 
0086     // Cancel animations here while both m_animations and m_animationsData are still valid.
0087     // slotWindowDeleted may access m_animationsData when an animation is removed.
0088     m_animations.clear();
0089     m_animationsData.clear();
0090 }
0091 
0092 bool SlidingPopupsEffect::supported()
0093 {
0094     return effects->animationsSupported();
0095 }
0096 
0097 void SlidingPopupsEffect::reconfigure(ReconfigureFlags flags)
0098 {
0099     SlidingPopupsConfig::self()->read();
0100     m_slideInDuration = std::chrono::milliseconds(
0101         static_cast<int>(animationTime(SlidingPopupsConfig::slideInTime() != 0 ? SlidingPopupsConfig::slideInTime() : 150)));
0102     m_slideOutDuration = std::chrono::milliseconds(
0103         static_cast<int>(animationTime(SlidingPopupsConfig::slideOutTime() != 0 ? SlidingPopupsConfig::slideOutTime() : 250)));
0104 
0105     auto animationIt = m_animations.begin();
0106     while (animationIt != m_animations.end()) {
0107         const auto duration = ((*animationIt).kind == AnimationKind::In)
0108             ? m_slideInDuration
0109             : m_slideOutDuration;
0110         (*animationIt).timeLine.setDuration(duration);
0111         ++animationIt;
0112     }
0113 
0114     auto dataIt = m_animationsData.begin();
0115     while (dataIt != m_animationsData.end()) {
0116         (*dataIt).slideInDuration = m_slideInDuration;
0117         (*dataIt).slideOutDuration = m_slideOutDuration;
0118         ++dataIt;
0119     }
0120 }
0121 
0122 void SlidingPopupsEffect::prePaintWindow(EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime)
0123 {
0124     auto animationIt = m_animations.find(w);
0125     if (animationIt == m_animations.end()) {
0126         effects->prePaintWindow(w, data, presentTime);
0127         return;
0128     }
0129 
0130     (*animationIt).timeLine.advance(presentTime);
0131     data.setTransformed();
0132 
0133     effects->prePaintWindow(w, data, presentTime);
0134 }
0135 
0136 void SlidingPopupsEffect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, QRegion region, WindowPaintData &data)
0137 {
0138     auto animationIt = m_animations.constFind(w);
0139     if (animationIt == m_animations.constEnd()) {
0140         effects->paintWindow(renderTarget, viewport, w, mask, region, data);
0141         return;
0142     }
0143 
0144     const AnimationData &animData = m_animationsData[w];
0145     const qreal slideLength = (animData.slideLength > 0) ? animData.slideLength : m_slideLength;
0146 
0147     const QRectF screenRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop());
0148     int splitPoint = 0;
0149     const QRectF geo = w->expandedGeometry();
0150     const qreal t = (*animationIt).timeLine.value();
0151 
0152     switch (animData.location) {
0153     case Location::Left:
0154         if (slideLength < geo.width()) {
0155             data.multiplyOpacity(t);
0156         }
0157         data.translate(-interpolate(std::min(geo.width(), slideLength), 0.0, t));
0158         splitPoint = geo.width() - (geo.x() + geo.width() - screenRect.x() - animData.offset);
0159         region &= QRegion(geo.x() + splitPoint, geo.y(), geo.width() - splitPoint, geo.height());
0160         break;
0161     case Location::Top:
0162         if (slideLength < geo.height()) {
0163             data.multiplyOpacity(t);
0164         }
0165         data.translate(0.0, -interpolate(std::min(geo.height(), slideLength), 0.0, t));
0166         splitPoint = geo.height() - (geo.y() + geo.height() - screenRect.y() - animData.offset);
0167         region &= QRegion(geo.x(), geo.y() + splitPoint, geo.width(), geo.height() - splitPoint);
0168         break;
0169     case Location::Right:
0170         if (slideLength < geo.width()) {
0171             data.multiplyOpacity(t);
0172         }
0173         data.translate(interpolate(std::min(geo.width(), slideLength), 0.0, t));
0174         splitPoint = screenRect.x() + screenRect.width() - geo.x() - animData.offset;
0175         region &= QRegion(geo.x(), geo.y(), splitPoint, geo.height());
0176         break;
0177     case Location::Bottom:
0178     default:
0179         if (slideLength < geo.height()) {
0180             data.multiplyOpacity(t);
0181         }
0182         data.translate(0.0, interpolate(std::min(geo.height(), slideLength), 0.0, t));
0183         splitPoint = screenRect.y() + screenRect.height() - geo.y() - animData.offset;
0184         region &= QRegion(geo.x(), geo.y(), geo.width(), splitPoint);
0185     }
0186 
0187     effects->paintWindow(renderTarget, viewport, w, mask, region, data);
0188 }
0189 
0190 void SlidingPopupsEffect::postPaintWindow(EffectWindow *w)
0191 {
0192     auto animationIt = m_animations.find(w);
0193     if (animationIt != m_animations.end()) {
0194         effects->addRepaint(w->expandedGeometry());
0195         if ((*animationIt).timeLine.done()) {
0196             if (!w->isDeleted()) {
0197                 w->setData(WindowForceBackgroundContrastRole, QVariant());
0198                 w->setData(WindowForceBlurRole, QVariant());
0199             }
0200             m_animations.erase(animationIt);
0201         }
0202     }
0203 
0204     effects->postPaintWindow(w);
0205 }
0206 
0207 void SlidingPopupsEffect::setupSlideData(EffectWindow *w)
0208 {
0209     connect(w, &EffectWindow::windowFrameGeometryChanged, this, &SlidingPopupsEffect::slotWindowFrameGeometryChanged);
0210     connect(w, &EffectWindow::windowShown, this, &SlidingPopupsEffect::slideIn);
0211     connect(w, &EffectWindow::windowHidden, this, &SlidingPopupsEffect::slideOut);
0212 
0213     // X11
0214     if (m_atom != XCB_ATOM_NONE) {
0215         slotPropertyNotify(w, m_atom);
0216     }
0217 
0218     // Wayland
0219     if (auto surf = w->surface()) {
0220         slotWaylandSlideOnShowChanged(w);
0221         connect(surf, &SurfaceInterface::slideOnShowHideChanged, this, [this, surf] {
0222             slotWaylandSlideOnShowChanged(effects->findWindow(surf));
0223         });
0224     }
0225 
0226     if (auto internal = w->internalWindow()) {
0227         internal->installEventFilter(this);
0228         setupInternalWindowSlide(w);
0229     }
0230 }
0231 
0232 void SlidingPopupsEffect::slotWindowAdded(EffectWindow *w)
0233 {
0234     setupSlideData(w);
0235     if (!w->isHidden()) {
0236         slideIn(w);
0237     }
0238 }
0239 
0240 void SlidingPopupsEffect::slotWindowClosed(EffectWindow *w)
0241 {
0242     if (!w->isHidden()) {
0243         slideOut(w);
0244     }
0245 }
0246 
0247 void SlidingPopupsEffect::slotWindowDeleted(EffectWindow *w)
0248 {
0249     m_animationsData.remove(w);
0250 }
0251 
0252 void SlidingPopupsEffect::slotPropertyNotify(EffectWindow *w, long atom)
0253 {
0254     if (!w || atom != m_atom || m_atom == XCB_ATOM_NONE) {
0255         return;
0256     }
0257 
0258     // _KDE_SLIDE atom format(each field is an uint32_t):
0259     // <offset> <location> [<slide in duration>] [<slide out duration>] [<slide length>]
0260     //
0261     // If offset is equal to -1, this effect will decide what offset to use
0262     // given edge of the screen, from which the window has to slide.
0263     //
0264     // If slide in duration is equal to 0 milliseconds, the default slide in
0265     // duration will be used. Same with the slide out duration.
0266     //
0267     // NOTE: If only slide in duration has been provided, then it will be
0268     // also used as slide out duration. I.e. if you provided only slide in
0269     // duration, then slide in duration == slide out duration.
0270 
0271     const QByteArray rawAtomData = w->readProperty(m_atom, m_atom, 32);
0272 
0273     if (rawAtomData.isEmpty()) {
0274         // Property was removed, thus also remove the effect for window
0275         if (w->data(WindowClosedGrabRole).value<void *>() == this) {
0276             w->setData(WindowClosedGrabRole, QVariant());
0277         }
0278         m_animations.remove(w);
0279         m_animationsData.remove(w);
0280         return;
0281     }
0282 
0283     // Offset and location are required.
0284     if (static_cast<size_t>(rawAtomData.size()) < sizeof(uint32_t) * 2) {
0285         return;
0286     }
0287 
0288     const auto *atomData = reinterpret_cast<const uint32_t *>(rawAtomData.data());
0289     AnimationData &animData = m_animationsData[w];
0290     animData.offset = atomData[0];
0291 
0292     switch (atomData[1]) {
0293     case 0: // West
0294         animData.location = Location::Left;
0295         break;
0296     case 1: // North
0297         animData.location = Location::Top;
0298         break;
0299     case 2: // East
0300         animData.location = Location::Right;
0301         break;
0302     case 3: // South
0303     default:
0304         animData.location = Location::Bottom;
0305         break;
0306     }
0307 
0308     if (static_cast<size_t>(rawAtomData.size()) >= sizeof(uint32_t) * 3) {
0309         animData.slideInDuration = std::chrono::milliseconds(atomData[2]);
0310         if (static_cast<size_t>(rawAtomData.size()) >= sizeof(uint32_t) * 4) {
0311             animData.slideOutDuration = std::chrono::milliseconds(atomData[3]);
0312         } else {
0313             animData.slideOutDuration = animData.slideInDuration;
0314         }
0315     } else {
0316         animData.slideInDuration = m_slideInDuration;
0317         animData.slideOutDuration = m_slideOutDuration;
0318     }
0319 
0320     if (static_cast<size_t>(rawAtomData.size()) >= sizeof(uint32_t) * 5) {
0321         animData.slideLength = atomData[4];
0322     } else {
0323         animData.slideLength = 0;
0324     }
0325 
0326     setupAnimData(w);
0327 }
0328 
0329 void SlidingPopupsEffect::slotWindowFrameGeometryChanged(EffectWindow *w, const QRectF &)
0330 {
0331     if (w == effects->inputPanel()) {
0332         setupInputPanelSlide();
0333     }
0334 }
0335 
0336 void SlidingPopupsEffect::setupAnimData(EffectWindow *w)
0337 {
0338     const QRectF screenRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop());
0339     const QRectF windowGeo = w->frameGeometry();
0340     AnimationData &animData = m_animationsData[w];
0341 
0342     if (animData.offset == -1) {
0343         switch (animData.location) {
0344         case Location::Left:
0345             animData.offset = std::max<qreal>(windowGeo.left() - screenRect.left(), 0);
0346             break;
0347         case Location::Top:
0348             animData.offset = std::max<qreal>(windowGeo.top() - screenRect.top(), 0);
0349             break;
0350         case Location::Right:
0351             animData.offset = std::max<qreal>(screenRect.right() - windowGeo.right(), 0);
0352             break;
0353         case Location::Bottom:
0354         default:
0355             animData.offset = std::max<qreal>(screenRect.bottom() - windowGeo.bottom(), 0);
0356             break;
0357         }
0358     }
0359     // sanitize
0360     switch (animData.location) {
0361     case Location::Left:
0362         animData.offset = std::max<qreal>(windowGeo.left() - screenRect.left(), animData.offset);
0363         break;
0364     case Location::Top:
0365         animData.offset = std::max<qreal>(windowGeo.top() - screenRect.top(), animData.offset);
0366         break;
0367     case Location::Right:
0368         animData.offset = std::max<qreal>(screenRect.right() - windowGeo.right(), animData.offset);
0369         break;
0370     case Location::Bottom:
0371     default:
0372         animData.offset = std::max<qreal>(screenRect.bottom() - windowGeo.bottom(), animData.offset);
0373         break;
0374     }
0375 
0376     animData.slideInDuration = (animData.slideInDuration.count() != 0)
0377         ? animData.slideInDuration
0378         : m_slideInDuration;
0379 
0380     animData.slideOutDuration = (animData.slideOutDuration.count() != 0)
0381         ? animData.slideOutDuration
0382         : m_slideOutDuration;
0383 
0384     // Grab the window, so other windowClosed effects will ignore it
0385     w->setData(WindowClosedGrabRole, QVariant::fromValue(static_cast<void *>(this)));
0386 }
0387 
0388 void SlidingPopupsEffect::slotWaylandSlideOnShowChanged(EffectWindow *w)
0389 {
0390     if (!w) {
0391         return;
0392     }
0393 
0394     SurfaceInterface *surf = w->surface();
0395     if (!surf) {
0396         return;
0397     }
0398 
0399     if (surf->slideOnShowHide()) {
0400         AnimationData &animData = m_animationsData[w];
0401 
0402         animData.offset = surf->slideOnShowHide()->offset();
0403 
0404         switch (surf->slideOnShowHide()->location()) {
0405         case SlideInterface::Location::Top:
0406             animData.location = Location::Top;
0407             break;
0408         case SlideInterface::Location::Left:
0409             animData.location = Location::Left;
0410             break;
0411         case SlideInterface::Location::Right:
0412             animData.location = Location::Right;
0413             break;
0414         case SlideInterface::Location::Bottom:
0415         default:
0416             animData.location = Location::Bottom;
0417             break;
0418         }
0419         animData.slideLength = 0;
0420         animData.slideInDuration = m_slideInDuration;
0421         animData.slideOutDuration = m_slideOutDuration;
0422 
0423         setupAnimData(w);
0424     }
0425 }
0426 
0427 void SlidingPopupsEffect::setupInternalWindowSlide(EffectWindow *w)
0428 {
0429     if (!w) {
0430         return;
0431     }
0432     auto internal = w->internalWindow();
0433     if (!internal) {
0434         return;
0435     }
0436     const QVariant slideProperty = internal->property("kwin_slide");
0437     if (!slideProperty.isValid()) {
0438         return;
0439     }
0440     Location location;
0441     switch (slideProperty.value<KWindowEffects::SlideFromLocation>()) {
0442     case KWindowEffects::BottomEdge:
0443         location = Location::Bottom;
0444         break;
0445     case KWindowEffects::TopEdge:
0446         location = Location::Top;
0447         break;
0448     case KWindowEffects::RightEdge:
0449         location = Location::Right;
0450         break;
0451     case KWindowEffects::LeftEdge:
0452         location = Location::Left;
0453         break;
0454     default:
0455         return;
0456     }
0457     AnimationData &animData = m_animationsData[w];
0458     animData.location = location;
0459     bool intOk = false;
0460     animData.offset = internal->property("kwin_slide_offset").toInt(&intOk);
0461     if (!intOk) {
0462         animData.offset = -1;
0463     }
0464     animData.slideLength = 0;
0465     animData.slideInDuration = m_slideInDuration;
0466     animData.slideOutDuration = m_slideOutDuration;
0467 
0468     setupAnimData(w);
0469 }
0470 
0471 void SlidingPopupsEffect::setupInputPanelSlide()
0472 {
0473     auto w = effects->inputPanel();
0474 
0475     if (!w || effects->isInputPanelOverlay()) {
0476         return;
0477     }
0478 
0479     AnimationData &animData = m_animationsData[w];
0480     animData.location = Location::Bottom;
0481     animData.offset = 0;
0482     animData.slideLength = 0;
0483     animData.slideInDuration = m_slideInDuration;
0484     animData.slideOutDuration = m_slideOutDuration;
0485 
0486     setupAnimData(w);
0487 
0488     slideIn(w);
0489 }
0490 
0491 bool SlidingPopupsEffect::eventFilter(QObject *watched, QEvent *event)
0492 {
0493     auto internal = qobject_cast<QWindow *>(watched);
0494     if (internal && event->type() == QEvent::DynamicPropertyChange) {
0495         QDynamicPropertyChangeEvent *pe = static_cast<QDynamicPropertyChangeEvent *>(event);
0496         if (pe->propertyName() == "kwin_slide" || pe->propertyName() == "kwin_slide_offset") {
0497             if (auto w = effects->findWindow(internal)) {
0498                 setupInternalWindowSlide(w);
0499             }
0500         }
0501     }
0502     return false;
0503 }
0504 
0505 void SlidingPopupsEffect::slideIn(EffectWindow *w)
0506 {
0507     if (effects->activeFullScreenEffect()) {
0508         return;
0509     }
0510 
0511     if (!w->isVisible()) {
0512         return;
0513     }
0514 
0515     auto dataIt = m_animationsData.constFind(w);
0516     if (dataIt == m_animationsData.constEnd()) {
0517         return;
0518     }
0519 
0520     Animation &animation = m_animations[w];
0521     animation.kind = AnimationKind::In;
0522     animation.timeLine.setDirection(TimeLine::Forward);
0523     animation.timeLine.setDuration((*dataIt).slideInDuration);
0524     animation.timeLine.setEasingCurve(QEasingCurve::OutCubic);
0525 
0526     // If the opposite animation (Out) was active and it had shorter duration,
0527     // at this point, the timeline can end up in the "done" state. Thus, we have
0528     // to reset it.
0529     if (animation.timeLine.done()) {
0530         animation.timeLine.reset();
0531     }
0532 
0533     w->setData(WindowAddedGrabRole, QVariant::fromValue(static_cast<void *>(this)));
0534     w->setData(WindowForceBackgroundContrastRole, QVariant(true));
0535     w->setData(WindowForceBlurRole, QVariant(true));
0536 
0537     w->addRepaintFull();
0538 }
0539 
0540 void SlidingPopupsEffect::slideOut(EffectWindow *w)
0541 {
0542     if (effects->activeFullScreenEffect()) {
0543         return;
0544     }
0545 
0546     if (!w->isVisible()) {
0547         return;
0548     }
0549 
0550     auto dataIt = m_animationsData.constFind(w);
0551     if (dataIt == m_animationsData.constEnd()) {
0552         return;
0553     }
0554 
0555     Animation &animation = m_animations[w];
0556     animation.deletedRef = EffectWindowDeletedRef(w);
0557     animation.visibleRef = EffectWindowVisibleRef(w, EffectWindow::PAINT_DISABLED);
0558     animation.kind = AnimationKind::Out;
0559     animation.timeLine.setDirection(TimeLine::Backward);
0560     animation.timeLine.setDuration((*dataIt).slideOutDuration);
0561     // this is effectively InCubic because the direction is reversed
0562     animation.timeLine.setEasingCurve(QEasingCurve::OutCubic);
0563 
0564     // If the opposite animation (In) was active and it had shorter duration,
0565     // at this point, the timeline can end up in the "done" state. Thus, we have
0566     // to reset it.
0567     if (animation.timeLine.done()) {
0568         animation.timeLine.reset();
0569     }
0570 
0571     w->setData(WindowClosedGrabRole, QVariant::fromValue(static_cast<void *>(this)));
0572     w->setData(WindowForceBackgroundContrastRole, QVariant(true));
0573     w->setData(WindowForceBlurRole, QVariant(true));
0574 
0575     w->addRepaintFull();
0576 }
0577 
0578 void SlidingPopupsEffect::stopAnimations()
0579 {
0580     for (auto it = m_animations.constBegin(); it != m_animations.constEnd(); ++it) {
0581         EffectWindow *w = it.key();
0582 
0583         if (!w->isDeleted()) {
0584             w->setData(WindowForceBackgroundContrastRole, QVariant());
0585             w->setData(WindowForceBlurRole, QVariant());
0586         }
0587     }
0588 
0589     m_animations.clear();
0590 }
0591 
0592 bool SlidingPopupsEffect::isActive() const
0593 {
0594     return !m_animations.isEmpty();
0595 }
0596 
0597 } // namespace
0598 
0599 #include "moc_slidingpopups.cpp"