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