File indexing completed on 2024-05-12 05:31:31

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