File indexing completed on 2024-05-19 16:34:36

0001 /*
0002     SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "kwinquickeffect.h"
0008 
0009 #include "logging_p.h"
0010 #include "sharedqmlengine.h"
0011 
0012 #include <QQmlEngine>
0013 #include <QQmlIncubator>
0014 #include <QQuickItem>
0015 #include <QQuickWindow>
0016 #include <QWindow>
0017 
0018 namespace KWin
0019 {
0020 
0021 class QuickSceneViewIncubator : public QQmlIncubator
0022 {
0023 public:
0024     QuickSceneViewIncubator(const std::function<void(QuickSceneViewIncubator *)> &statusChangedCallback)
0025         : QQmlIncubator(QQmlIncubator::Asynchronous)
0026         , m_statusChangedCallback(statusChangedCallback)
0027     {
0028     }
0029 
0030     void statusChanged(QQmlIncubator::Status status) override
0031     {
0032         m_statusChangedCallback(this);
0033     }
0034 
0035 private:
0036     std::function<void(QuickSceneViewIncubator *)> m_statusChangedCallback;
0037 };
0038 
0039 class QuickSceneEffectPrivate
0040 {
0041 public:
0042     static QuickSceneEffectPrivate *get(QuickSceneEffect *effect)
0043     {
0044         return effect->d.get();
0045     }
0046     bool isItemOnScreen(QQuickItem *item, EffectScreen *screen) const;
0047 
0048     SharedQmlEngine::Ptr qmlEngine;
0049     std::unique_ptr<QQmlComponent> qmlComponent;
0050     QUrl source;
0051     std::map<EffectScreen *, std::unique_ptr<QQmlIncubator>> incubators;
0052     std::map<EffectScreen *, std::unique_ptr<QuickSceneView>> views;
0053     QPointer<QuickSceneView> mouseImplicitGrab;
0054     bool running = false;
0055     std::unique_ptr<QWindow> dummyWindow;
0056 };
0057 
0058 bool QuickSceneEffectPrivate::isItemOnScreen(QQuickItem *item, EffectScreen *screen) const
0059 {
0060     if (!item || !screen || !views.contains(screen)) {
0061         return false;
0062     }
0063 
0064     const auto &view = views.at(screen);
0065     return item->window() == view->window();
0066 }
0067 
0068 QuickSceneView::QuickSceneView(QuickSceneEffect *effect, EffectScreen *screen)
0069     : OffscreenQuickView(effect, QuickSceneEffectPrivate::get(effect)->dummyWindow.get())
0070     , m_effect(effect)
0071     , m_screen(screen)
0072 {
0073     setGeometry(screen->geometry());
0074     connect(screen, &EffectScreen::geometryChanged, this, [this, screen]() {
0075         setGeometry(screen->geometry());
0076     });
0077 }
0078 
0079 QuickSceneView::~QuickSceneView()
0080 {
0081 }
0082 
0083 QQuickItem *QuickSceneView::rootItem() const
0084 {
0085     return m_rootItem.get();
0086 }
0087 
0088 void QuickSceneView::setRootItem(QQuickItem *item)
0089 {
0090     Q_ASSERT_X(item, "setRootItem", "root item cannot be null");
0091     m_rootItem.reset(item);
0092     m_rootItem->setParentItem(contentItem());
0093 
0094     auto updateSize = [this]() {
0095         m_rootItem->setSize(contentItem()->size());
0096     };
0097     updateSize();
0098     connect(contentItem(), &QQuickItem::widthChanged, m_rootItem.get(), updateSize);
0099     connect(contentItem(), &QQuickItem::heightChanged, m_rootItem.get(), updateSize);
0100 }
0101 
0102 QuickSceneEffect *QuickSceneView::effect() const
0103 {
0104     return m_effect;
0105 }
0106 
0107 EffectScreen *QuickSceneView::screen() const
0108 {
0109     return m_screen;
0110 }
0111 
0112 bool QuickSceneView::isDirty() const
0113 {
0114     return m_dirty;
0115 }
0116 
0117 void QuickSceneView::markDirty()
0118 {
0119     m_dirty = true;
0120 }
0121 
0122 void QuickSceneView::resetDirty()
0123 {
0124     m_dirty = false;
0125 }
0126 
0127 void QuickSceneView::scheduleRepaint()
0128 {
0129     markDirty();
0130     effects->addRepaint(geometry());
0131 }
0132 
0133 QuickSceneEffect::QuickSceneEffect(QObject *parent)
0134     : Effect(parent)
0135     , d(new QuickSceneEffectPrivate)
0136 {
0137 }
0138 
0139 QuickSceneEffect::~QuickSceneEffect()
0140 {
0141 }
0142 
0143 bool QuickSceneEffect::supported()
0144 {
0145     return effects->compositingType() == OpenGLCompositing;
0146 }
0147 
0148 void QuickSceneEffect::checkItemDraggedOutOfScreen(QQuickItem *item)
0149 {
0150     const QRectF globalGeom = QRectF(item->mapToGlobal(QPointF(0, 0)), QSizeF(item->width(), item->height()));
0151     QList<EffectScreen *> screens;
0152 
0153     for (const auto &[screen, view] : d->views) {
0154         if (!d->isItemOnScreen(item, screen) && screen->geometry().intersects(globalGeom.toRect())) {
0155             screens << screen;
0156         }
0157     }
0158 
0159     Q_EMIT itemDraggedOutOfScreen(item, screens);
0160 }
0161 
0162 void QuickSceneEffect::checkItemDroppedOutOfScreen(const QPointF &globalPos, QQuickItem *item)
0163 {
0164     const auto it = std::find_if(d->views.begin(), d->views.end(), [this, globalPos, item](const auto &view) {
0165         EffectScreen *screen = view.first;
0166         return !d->isItemOnScreen(item, screen) && screen->geometry().contains(globalPos.toPoint());
0167     });
0168     if (it != d->views.end()) {
0169         Q_EMIT itemDroppedOutOfScreen(globalPos, item, it->first);
0170     }
0171 }
0172 
0173 bool QuickSceneEffect::eventFilter(QObject *watched, QEvent *event)
0174 {
0175     if (event->type() == QEvent::CursorChange) {
0176         if (const QWindow *window = qobject_cast<QWindow *>(watched)) {
0177             effects->defineCursor(window->cursor().shape());
0178         }
0179     }
0180     return false;
0181 }
0182 
0183 bool QuickSceneEffect::isRunning() const
0184 {
0185     return d->running;
0186 }
0187 
0188 void QuickSceneEffect::setRunning(bool running)
0189 {
0190     if (d->running != running) {
0191         if (running) {
0192             startInternal();
0193         } else {
0194             stopInternal();
0195         }
0196     }
0197 }
0198 
0199 QUrl QuickSceneEffect::source() const
0200 {
0201     return d->source;
0202 }
0203 
0204 void QuickSceneEffect::setSource(const QUrl &url)
0205 {
0206     if (isRunning()) {
0207         qWarning() << "Cannot change QuickSceneEffect.source while running";
0208         return;
0209     }
0210     if (d->source != url) {
0211         d->source = url;
0212         d->qmlComponent.reset();
0213     }
0214 }
0215 
0216 QuickSceneView *QuickSceneEffect::viewForScreen(EffectScreen *screen) const
0217 {
0218     const auto it = d->views.find(screen);
0219     return it == d->views.end() ? nullptr : it->second.get();
0220 }
0221 
0222 QuickSceneView *QuickSceneEffect::viewAt(const QPoint &pos) const
0223 {
0224     const auto it = std::find_if(d->views.begin(), d->views.end(), [pos](const auto &view) {
0225         return view.second->geometry().contains(pos);
0226     });
0227     return it == d->views.end() ? nullptr : it->second.get();
0228 }
0229 
0230 QuickSceneView *QuickSceneEffect::activeView() const
0231 {
0232     auto it = std::find_if(d->views.begin(), d->views.end(), [](const auto &view) {
0233         return view.second->window()->activeFocusItem();
0234     });
0235     if (it == d->views.end()) {
0236         it = d->views.find(effects->activeScreen());
0237     }
0238     return it == d->views.end() ? nullptr : it->second.get();
0239 }
0240 
0241 KWin::QuickSceneView *QuickSceneEffect::getView(Qt::Edge edge)
0242 {
0243     auto screenView = activeView();
0244 
0245     QuickSceneView *candidate = nullptr;
0246 
0247     for (const auto &[screen, view] : d->views) {
0248         switch (edge) {
0249         case Qt::LeftEdge:
0250             if (view->geometry().left() < screenView->geometry().left()) {
0251                 // Look for the nearest view from the current
0252                 if (!candidate || view->geometry().left() > candidate->geometry().left() || (view->geometry().left() == candidate->geometry().left() && view->geometry().top() > candidate->geometry().top())) {
0253                     candidate = view.get();
0254                 }
0255             }
0256             break;
0257         case Qt::TopEdge:
0258             if (view->geometry().top() < screenView->geometry().top()) {
0259                 if (!candidate || view->geometry().top() > candidate->geometry().top() || (view->geometry().top() == candidate->geometry().top() && view->geometry().left() > candidate->geometry().left())) {
0260                     candidate = view.get();
0261                 }
0262             }
0263             break;
0264         case Qt::RightEdge:
0265             if (view->geometry().right() > screenView->geometry().right()) {
0266                 if (!candidate || view->geometry().right() < candidate->geometry().right() || (view->geometry().right() == candidate->geometry().right() && view->geometry().top() > candidate->geometry().top())) {
0267                     candidate = view.get();
0268                 }
0269             }
0270             break;
0271         case Qt::BottomEdge:
0272             if (view->geometry().bottom() > screenView->geometry().bottom()) {
0273                 if (!candidate || view->geometry().bottom() < candidate->geometry().bottom() || (view->geometry().bottom() == candidate->geometry().bottom() && view->geometry().left() > candidate->geometry().left())) {
0274                     candidate = view.get();
0275                 }
0276             }
0277             break;
0278         }
0279     }
0280 
0281     return candidate;
0282 }
0283 
0284 void QuickSceneEffect::activateView(QuickSceneView *view)
0285 {
0286     if (!view) {
0287         return;
0288     }
0289 
0290     auto *av = activeView();
0291     // Already properly active?
0292     if (view == av && av->window()->activeFocusItem()) {
0293         return;
0294     }
0295 
0296     for (const auto &[screen, otherView] : d->views) {
0297         if (otherView.get() == view && !view->window()->activeFocusItem()) {
0298             QFocusEvent focusEvent(QEvent::FocusIn, Qt::ActiveWindowFocusReason);
0299             qApp->sendEvent(view->window(), &focusEvent);
0300         } else if (otherView.get() != view && otherView->window()->activeFocusItem()) {
0301             QFocusEvent focusEvent(QEvent::FocusOut, Qt::ActiveWindowFocusReason);
0302             qApp->sendEvent(otherView->window(), &focusEvent);
0303         }
0304     }
0305 
0306     Q_EMIT activeViewChanged(view);
0307 }
0308 
0309 // Screen views are repainted just before kwin performs its compositing cycle to avoid stalling for vblank
0310 void QuickSceneEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime)
0311 {
0312     if (effects->waylandDisplay()) {
0313         const auto it = d->views.find(data.screen);
0314         if (it != d->views.end() && it->second->isDirty()) {
0315             it->second->update();
0316             it->second->resetDirty();
0317         }
0318     } else {
0319         for (const auto &[screen, screenView] : d->views) {
0320             if (screenView->isDirty()) {
0321                 screenView->update();
0322                 screenView->resetDirty();
0323             }
0324         }
0325     }
0326 }
0327 
0328 void QuickSceneEffect::paintScreen(int mask, const QRegion &region, ScreenPaintData &data)
0329 {
0330     if (effects->waylandDisplay()) {
0331         const auto it = d->views.find(data.screen());
0332         if (it != d->views.end()) {
0333             effects->renderOffscreenQuickView(it->second.get());
0334         }
0335     } else {
0336         for (const auto &[screen, screenView] : d->views) {
0337             effects->renderOffscreenQuickView(screenView.get());
0338         }
0339     }
0340 }
0341 
0342 bool QuickSceneEffect::isActive() const
0343 {
0344     return !d->views.empty() && !effects->isScreenLocked();
0345 }
0346 
0347 QVariantMap QuickSceneEffect::initialProperties(EffectScreen *screen)
0348 {
0349     return QVariantMap();
0350 }
0351 
0352 void QuickSceneEffect::handleScreenAdded(EffectScreen *screen)
0353 {
0354     addScreen(screen);
0355 }
0356 
0357 void QuickSceneEffect::handleScreenRemoved(EffectScreen *screen)
0358 {
0359     d->views.erase(screen);
0360     d->incubators.erase(screen);
0361 }
0362 
0363 void QuickSceneEffect::addScreen(EffectScreen *screen)
0364 {
0365     auto properties = initialProperties(screen);
0366     properties["width"] = screen->geometry().width();
0367     properties["height"] = screen->geometry().height();
0368 
0369     auto incubator = new QuickSceneViewIncubator([this, screen](QuickSceneViewIncubator *incubator) {
0370         if (incubator->isReady()) {
0371             auto view = new QuickSceneView(this, screen);
0372             view->setRootItem(qobject_cast<QQuickItem *>(incubator->object()));
0373             if (view->contentItem()) {
0374                 view->contentItem()->setFocus(false);
0375             }
0376             view->setAutomaticRepaint(false);
0377             connect(view, &QuickSceneView::repaintNeeded, this, [view]() {
0378                 effects->addRepaint(view->geometry());
0379             });
0380             connect(view, &QuickSceneView::renderRequested, view, &QuickSceneView::scheduleRepaint);
0381             connect(view, &QuickSceneView::sceneChanged, view, &QuickSceneView::scheduleRepaint);
0382             view->scheduleRepaint();
0383             d->views[screen].reset(view);
0384         } else if (incubator->isError()) {
0385             qCWarning(LIBKWINEFFECTS) << "Could not create a view for QML file" << d->qmlComponent->url();
0386             qCWarning(LIBKWINEFFECTS) << incubator->errors();
0387         }
0388     });
0389 
0390     incubator->setInitialProperties(properties);
0391     d->incubators[screen].reset(incubator);
0392     d->qmlComponent->create(*incubator);
0393 }
0394 
0395 void QuickSceneEffect::startInternal()
0396 {
0397     if (effects->activeFullScreenEffect()) {
0398         return;
0399     }
0400 
0401     if (Q_UNLIKELY(d->source.isEmpty())) {
0402         qWarning() << "QuickSceneEffect.source is empty. Did you forget to call setSource()?";
0403         return;
0404     }
0405 
0406     if (!d->qmlEngine) {
0407         d->qmlEngine = SharedQmlEngine::engine();
0408     }
0409 
0410     if (!d->qmlComponent) {
0411         d->qmlComponent.reset(new QQmlComponent(d->qmlEngine.get()));
0412         d->qmlComponent->loadUrl(d->source);
0413         if (d->qmlComponent->isError()) {
0414             qWarning().nospace() << "Failed to load " << d->source << ": " << d->qmlComponent->errors();
0415             d->qmlComponent.reset();
0416             return;
0417         }
0418     }
0419 
0420     effects->setActiveFullScreenEffect(this);
0421     d->running = true;
0422 
0423     // Install an event filter to monitor cursor shape changes.
0424     qApp->installEventFilter(this);
0425 
0426     // This is an ugly hack to make hidpi rendering work as expected on wayland until we switch
0427     // to Qt 6.3 or newer. See https://codereview.qt-project.org/c/qt/qtdeclarative/+/361506
0428     if (effects->waylandDisplay()) {
0429         d->dummyWindow.reset(new QWindow());
0430         d->dummyWindow->setOpacity(0);
0431         d->dummyWindow->resize(1, 1);
0432         d->dummyWindow->setFlag(Qt::FramelessWindowHint);
0433         d->dummyWindow->setVisible(true);
0434         d->dummyWindow->requestActivate();
0435     }
0436 
0437     const QList<EffectScreen *> screens = effects->screens();
0438     for (EffectScreen *screen : screens) {
0439         addScreen(screen);
0440     }
0441 
0442     // Ensure one view has an active focus item
0443     activateView(activeView());
0444 
0445     connect(effects, &EffectsHandler::screenAdded, this, &QuickSceneEffect::handleScreenAdded);
0446     connect(effects, &EffectsHandler::screenRemoved, this, &QuickSceneEffect::handleScreenRemoved);
0447 
0448     effects->grabKeyboard(this);
0449     effects->startMouseInterception(this, Qt::ArrowCursor);
0450 }
0451 
0452 void QuickSceneEffect::stopInternal()
0453 {
0454     disconnect(effects, &EffectsHandler::screenAdded, this, &QuickSceneEffect::handleScreenAdded);
0455     disconnect(effects, &EffectsHandler::screenRemoved, this, &QuickSceneEffect::handleScreenRemoved);
0456 
0457     d->incubators.clear();
0458     d->views.clear();
0459     d->dummyWindow.reset();
0460     d->running = false;
0461     qApp->removeEventFilter(this);
0462     effects->ungrabKeyboard();
0463     effects->stopMouseInterception(this);
0464     effects->setActiveFullScreenEffect(nullptr);
0465     effects->addRepaintFull();
0466 }
0467 
0468 void QuickSceneEffect::windowInputMouseEvent(QEvent *event)
0469 {
0470     Qt::MouseButtons buttons;
0471     QPoint globalPosition;
0472     if (QMouseEvent *mouseEvent = dynamic_cast<QMouseEvent *>(event)) {
0473         buttons = mouseEvent->buttons();
0474         globalPosition = mouseEvent->globalPos();
0475     } else if (QWheelEvent *wheelEvent = dynamic_cast<QWheelEvent *>(event)) {
0476         buttons = wheelEvent->buttons();
0477         globalPosition = wheelEvent->globalPosition().toPoint();
0478     } else {
0479         return;
0480     }
0481 
0482     if (buttons) {
0483         if (!d->mouseImplicitGrab) {
0484             d->mouseImplicitGrab = viewAt(globalPosition);
0485         }
0486     }
0487 
0488     QuickSceneView *target = d->mouseImplicitGrab;
0489     if (!target) {
0490         target = viewAt(globalPosition);
0491     }
0492 
0493     if (!buttons) {
0494         d->mouseImplicitGrab = nullptr;
0495     }
0496 
0497     if (target) {
0498         if (buttons) {
0499             activateView(target);
0500         }
0501         target->forwardMouseEvent(event);
0502     }
0503 }
0504 
0505 void QuickSceneEffect::grabbedKeyboardEvent(QKeyEvent *keyEvent)
0506 {
0507     auto *screenView = activeView();
0508 
0509     if (screenView) {
0510         // ActiveView may not have an activeFocusItem yet
0511         activateView(screenView);
0512         screenView->forwardKeyEvent(keyEvent);
0513     }
0514 }
0515 
0516 bool QuickSceneEffect::touchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time)
0517 {
0518     for (const auto &[screen, screenView] : d->views) {
0519         if (screenView->geometry().contains(pos.toPoint())) {
0520             activateView(screenView.get());
0521             return screenView->forwardTouchDown(id, pos, time);
0522         }
0523     }
0524     return false;
0525 }
0526 
0527 bool QuickSceneEffect::touchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time)
0528 {
0529     for (const auto &[screen, screenView] : d->views) {
0530         if (screenView->geometry().contains(pos.toPoint())) {
0531             return screenView->forwardTouchMotion(id, pos, time);
0532         }
0533     }
0534     return false;
0535 }
0536 
0537 bool QuickSceneEffect::touchUp(qint32 id, std::chrono::microseconds time)
0538 {
0539     for (const auto &[screen, screenView] : d->views) {
0540         if (screenView->forwardTouchUp(id, time)) {
0541             return true;
0542         }
0543     }
0544     return false;
0545 }
0546 
0547 } // namespace KWin
0548 
0549 #include <moc_kwinquickeffect.cpp>