File indexing completed on 2025-04-27 11:33:14

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