File indexing completed on 2025-03-23 11:14:00
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 ®ion, 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>