1// Copyright (C) 2023 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquickwindowcontainer_p.h"
5
6#include <QtQuick/qquickrendercontrol.h>
7
8#include <QtQuick/private/qquickitem_p.h>
9#include <QtQuick/private/qquickrectangle_p.h>
10#include <QtQuick/private/qquickwindowmodule_p.h>
11#include <QtQuick/private/qquickimplicitsizeitem_p_p.h>
12
13QT_BEGIN_NAMESPACE
14
15Q_LOGGING_CATEGORY(lcWindowContainer, "qt.quick.window.container")
16
17using namespace Qt::StringLiterals;
18
19/*!
20 \qmltype WindowContainer
21 \inqmlmodule QtQuick
22 \ingroup qtquick-visual
23 \inherits Item
24 \since 6.8
25
26 \brief Allows embedding arbitrary QWindows into a Qt Quick scene.
27
28 The window will become a child of the item's window,
29 with its position, size, z-order, etc. managed by the item.
30
31 Sibling items with a higher z-order than the window container
32 will not automatically overlap the embedded window, as the
33 window lives on top of the Qt Quick scene. To work around this,
34 place the sibling items inside their own dedicated child window:
35
36 \code
37 Item {
38 id: someItem
39 WindowContainer {
40 window: foreignWindow
41 }
42 WindowContainer {
43 window: Window {
44 Item {
45 id: siblingItem
46 }
47 }
48 }
49 }
50 \endcode
51
52 Similarly, child Items of the window container will not automatically
53 overlap the embedded window. To work around this, place the child
54 item inside a dedicated child window.
55
56 \code
57 Item {
58 id: someItem
59 WindowContainer {
60 id: windowContainer
61 window: foreignWindow
62 WindowContainer {
63 window: Window {
64 Item {
65 id: childItem
66 }
67 }
68 }
69 }
70 }
71 \endcode
72
73 \note The window container does not interoperate with QQuickWidget,
74 QQuickWindow::setRenderTarget(), QQuickRenderControl, or similar
75 functionality.
76
77 \sa {QQuickWindow::}{parent()}
78*/
79
80/*!
81 \qmlproperty QWindow QtQuick::WindowContainer::window
82
83 This property holds the window to embed.
84*/
85
86class QQuickWindowContainerPrivate : public QQuickImplicitSizeItemPrivate
87{
88 Q_DECLARE_PUBLIC(QQuickWindowContainer)
89protected:
90 bool transformChanged(QQuickItem *transformedItem) override;
91
92public:
93 QWindow *window = nullptr;
94 QQuickWindowContainer::ContainerMode containerMode;
95};
96
97/*!
98 \internal
99
100 Creates a new window container.
101
102 The container mode determines who has the last word in what the state
103 of the contained window should be. If the window container is explicitly
104 requested by the user via WindowContainer, the properties are set on the
105 item, and the embedded window should match that. If the window container
106 is implicitly created by setting a visual parent on a Window, the properties
107 are set on the Window, and the window container should respect that.
108*/
109QQuickWindowContainer::QQuickWindowContainer(QQuickItem *parent, ContainerMode containerMode)
110 : QQuickImplicitSizeItem(*(new QQuickWindowContainerPrivate), parent)
111{
112 Q_D(QQuickWindowContainer);
113
114 qCDebug(lcWindowContainer).verbosity(verbosityLevel: 1) << "Creating window container"
115 << this << "with parent" << parent << "and" << containerMode;
116
117 d->containerMode = containerMode;
118
119 setFlag(flag: QQuickItem::ItemObservesViewport); // For clipping
120
121 connect(sender: this, signal: &QQuickItem::windowChanged,
122 context: this, slot: &QQuickWindowContainer::parentWindowChanged);
123
124 if (lcWindowContainer().isDebugEnabled()) {
125 auto *debugRectangle = new QQuickRectangle(this);
126 debugRectangle->setColor(QColor(255, 0, 255, 20));
127 auto *border = debugRectangle->border();
128 border->setColor(Qt::magenta);
129 border->setWidth(1.0);
130 QQuickItemPrivate *rectPrivate = QQuickItemPrivate::get(item: debugRectangle);
131 rectPrivate->anchors()->setFill(this);
132 }
133}
134
135QQuickWindowContainer::~QQuickWindowContainer()
136{
137 Q_D(const QQuickWindowContainer);
138 qCDebug(lcWindowContainer) << "Destructing window container" << this;
139
140 disconnect(receiver: this);
141 if (d->window) {
142 auto ownership = QJSEngine::objectOwnership(d->window);
143 qCDebug(lcWindowContainer) << "Contained window" << d->window
144 << "has" << (ownership == QQmlEngine::JavaScriptOwnership ?
145 "JavaScript" : "C++") << "ownership";
146 if (ownership == QQmlEngine::JavaScriptOwnership) {
147 delete d->window;
148 } else {
149 d->window->destroy();
150 d->window->setParent(nullptr);
151 }
152 }
153}
154
155void QQuickWindowContainer::releaseResources()
156{
157 Q_D(const QQuickWindowContainer);
158 qCDebug(lcWindowContainer) << "Destroying" << d->window
159 << "with platform window" << (d->window ? d->window->handle() : nullptr);
160 if (d->window)
161 d->window->destroy();
162}
163
164void QQuickWindowContainer::classBegin()
165{
166 qCDebug(lcWindowContainer) << "Class begin for" << this;
167
168 QQuickImplicitSizeItem::classBegin();
169}
170
171void QQuickWindowContainer::componentComplete()
172{
173 Q_D(const QQuickWindowContainer);
174
175 qCDebug(lcWindowContainer) << "Component completed for" << this;
176 QQuickImplicitSizeItem::componentComplete();
177
178 if (d->window)
179 initializeContainedWindow();
180}
181
182QWindow *QQuickWindowContainer::containedWindow() const
183{
184 Q_D(const QQuickWindowContainer);
185 return d->window;
186}
187
188void QQuickWindowContainer::setContainedWindow(QWindow *window)
189{
190 qCDebug(lcWindowContainer) << "Setting contained window for" << this << "to" << window;
191
192 Q_D(QQuickWindowContainer);
193
194 if (window == d->window)
195 return;
196
197 if (auto *previousWindow = d->window) {
198 qCDebug(lcWindowContainer) << "Decoupling container from" << d->window;
199 previousWindow->disconnect(receiver: this);
200 previousWindow->removeEventFilter(obj: this);
201 previousWindow->setParent(nullptr);
202 }
203
204 d->window = window;
205
206 if (d->window) {
207 if (d->containerMode == ItemControlsWindow) {
208 if (auto *quickWindow = qobject_cast<QQuickWindowQmlImpl*>(object: d->window)) {
209 // Make sure the Window reflects the window container as its visual parent
210 quickWindow->setVisualParent(this);
211 }
212 }
213
214 // When the window controls the container, we need to reflect any changes
215 // in the window back to the container, so they stay in sync. And when the
216 // container controls the window, we still want to reflect width/height as
217 // new implicit size, and override any other changes with the item state.
218 connect(sender: d->window, signal: &QWindow::xChanged, context: this, slot: &QQuickWindowContainer::windowUpdated);
219 connect(sender: d->window, signal: &QWindow::yChanged, context: this, slot: &QQuickWindowContainer::windowUpdated);
220 connect(sender: d->window, signal: &QWindow::widthChanged, context: this, slot: &QQuickWindowContainer::windowUpdated);
221 connect(sender: d->window, signal: &QWindow::heightChanged, context: this, slot: &QQuickWindowContainer::windowUpdated);
222 connect(sender: d->window, signal: &QWindow::visibleChanged, context: this, slot: &QQuickWindowContainer::windowUpdated);
223
224 connect(sender: d->window, signal: &QObject::destroyed, context: this, slot: &QQuickWindowContainer::windowDestroyed);
225
226 d->window->installEventFilter(filterObj: this);
227
228 if (d->componentComplete)
229 initializeContainedWindow();
230 } else {
231 // Reset state based on not having a window
232 syncWindowToItem();
233 }
234
235 emit containedWindowChanged(window: d->window);
236}
237
238void QQuickWindowContainer::initializeContainedWindow()
239{
240 Q_D(const QQuickWindowContainer);
241 Q_ASSERT(d->componentComplete);
242 Q_ASSERT(d->window);
243
244 qCDebug(lcWindowContainer) << "Doing initial sync between" << d->window << "and" << this;
245
246 syncWindowToItem();
247 polish();
248}
249
250static QTransform sanitizeTransform(const QTransform &transform)
251{
252 if (transform.isRotating()) {
253 // FIXME: Can we keep more here?
254 return QTransform::fromTranslate(dx: transform.dx(), dy: transform.dy());
255 }
256
257 return transform;
258}
259
260void QQuickWindowContainer::syncWindowToItem()
261{
262 Q_D(const QQuickWindowContainer);
263
264 const auto windowGeometry = d->window ? d->window->geometry() : QRect();
265
266 qCDebug(lcWindowContainer) << "Syncing window state from" << d->window
267 << "with geometry" << windowGeometry << "to" << this
268 << "with mode" << d->containerMode;
269
270 const auto transform = sanitizeTransform(transform: d->windowToItemTransform());
271
272 // The window might have a larger size than the item's natural
273 // size, if there's a scale applied somewhere in the hierarchy.
274 auto itemSize = d->window ? transform.mapRect(windowGeometry).size()
275 : QSize();
276
277 if (d->containerMode == WindowControlsItem) {
278 // When the Window controls the window container the position is
279 // set up front, when creating the window container, and from that
280 // point on set exclusively via the window container, so we skip
281 // setting the position here, and only set the size.
282 setSize(itemSize);
283 setVisible(d->window ? d->window->isVisible() : false);
284 } else {
285 // Position defined by item, so don't sync from window
286 // Visible defined by item, so don't sync from window
287 setImplicitWidth(itemSize.width());
288 setImplicitHeight(itemSize.height());
289 }
290}
291
292/*!
293 \internal
294
295 updatePolish() should perform any layout as required for this item.
296
297 For us, that means propagating the item's state to the window.
298*/
299void QQuickWindowContainer::updatePolish()
300{
301 Q_D(QQuickWindowContainer);
302
303 qCDebug(lcWindowContainer) << "Propagating" << this << "state"
304 << "to" << d->window;
305
306 auto *parentWindow = window();
307
308 // FIXME: If we are part of a QQuickWidget, we have a QQuickRenderControl,
309 // and should look up the parent window via that, and apply the offset we
310 // get to the item transform below. But at the moment it's not possible
311 // to observe changes to the offset, which is critical to support this
312 // for child windows.
313
314 if (!d->window || !parentWindow)
315 return;
316
317 if (d->window->parent() != parentWindow) {
318 qCDebug(lcWindowContainer) << "Updating window parent to" << parentWindow;
319 d->window->setParent(parentWindow);
320 }
321
322 auto transform = sanitizeTransform(transform: d->itemToWindowTransform());
323
324 // Find the window's geometry, based on the item's bounding rect,
325 // mapped to the scene. The mapping includes any x/y position set
326 // on the item itself, as well as any transforms applied to the item
327 // or its ancestor (scale, translation).
328 const QRectF itemSceneRect = transform.mapRect(boundingRect());
329 // FIXME: Rounding to a QRect here means we'll have some jitter or off
330 // placement when the underlying item is not on a integer coordinate.
331 QRect windowGeometry = itemSceneRect.toRect();
332 if (windowGeometry != d->window->geometry()) {
333 QRectF itemRect(position(), size());
334 qCDebug(lcWindowContainer) << "Updating window geometry to" << windowGeometry
335 << "based on item rect" << itemRect << "and scene rect" << itemSceneRect;
336 d->window->setGeometry(windowGeometry);
337 }
338
339 // Clip the container to its own and ancestor clip rects, by setting
340 // a mask on the window. This does not necessarily clip native windows,
341 // as QWindow::setMask() is not guaranteed to visually clip the window,
342 // only to mask input, but in most cases we should be good. For the
343 // cases where this fails, we can potentially use an intermediate window
344 // as parent of the contained window, if the platform allows clipping
345 // child windows to parent window geometry. We do not want to resize the
346 // contained window, as that will just fill the content into a smaller
347 // area.
348 const auto clipMask = [&]{
349 if (clipRect() == boundingRect())
350 return QRect();
351
352 // The clip rect has all the relevant transforms applied to it,
353 // except for the item's own scale. As the mask is in window
354 // local coordinates in the possibly scaled window, we need
355 // to apply the scale manually.
356 auto scaleTransform = QTransform::fromScale(dx: transform.m11(), dy: transform.m22());
357 auto rect = scaleTransform.mapRect(clipRect()).toRect();
358
359 // An empty clip rect means clip away everything, while for a
360 // window, an empty mask means mask nothing. Fake the former
361 // by setting a mask outside of the window's bounds. We have
362 // to do this check after rounding the clip rect to a QRect.
363 // FIXME: Verify this works on all platforms
364 if (rect.isEmpty())
365 return QRect(-1, -1, 1, 1);
366
367 return rect;
368 }();
369
370 if (clipMask != d->window->mask().boundingRect()) {
371 qCDebug(lcWindowContainer) << "Updating window clip mask to" << clipMask
372 << "based on clip rect" << clipRect();
373 d->window->setMask(clipMask);
374 }
375
376 // FIXME: Opacity support. Need to calculate effective opacity ourselves,
377 // and there doesn't seem to be any existing observer for opacity changes.
378 // Not all platforms implement opacity for child windows yet.
379
380 // FIXME: If a scale is applied to the item or its parents, we end up
381 // with a bigger item, and window, but we don't translate the scale to
382 // an increase device-pixel-ratio of the window. As a result, the window
383 // will likely just render more content, instead of the same content at
384 // a potentially higher density.
385
386 if (d->window->isVisible() != isVisible()) {
387 qCDebug(lcWindowContainer) << "Updating window visibility"
388 << "based on item visible" << isVisible();
389 d->window->setVisible(isVisible());
390 }
391}
392
393/*!
394 \internal
395
396 QQuickItem::clipRect() doesn't take ItemClipsChildrenToShape into
397 account, so a parent item that has clip:false, but ItemIsViewport
398 will still result in affecting the clip.
399
400 We want to stay consistent with the clipping in the scene graph,
401 which is based on QQuickItem::clip(), so we override the clipRect
402 to take ItemClipsChildrenToShape into account.
403*/
404QRectF QQuickWindowContainer::clipRect() const
405{
406 QRectF rect = boundingRect();
407
408 for (auto *viewport = viewportItem(); viewport; viewport = viewport->viewportItem()) {
409 if (viewport == this)
410 break;
411
412 if (viewport->flags().testFlag(flag: QQuickItem::ItemClipsChildrenToShape)) {
413 // FIXME: This fails to take into account viewports that override clipRect()
414 const auto mappedViewportRect = mapRectFromItem(item: viewport, rect: viewport->boundingRect());
415 rect = mappedViewportRect.intersected(r: rect);
416 }
417
418 if (viewport->viewportItem() == viewport)
419 break; // Content item returns itself as viewport
420 }
421
422 return rect;
423}
424
425// ----------------------- Window updates -----------------------
426
427/*!
428 \internal
429
430 Called when the contained QWindow is changed.
431
432 Depending on the sync mode we need to reflect these changes
433 to the item, or override them by applying the item state.
434*/
435void QQuickWindowContainer::windowUpdated()
436{
437 Q_D(const QQuickWindowContainer);
438
439 if (lcWindowContainer().isDebugEnabled()) {
440 auto metaMethod = sender()->metaObject()->method(index: senderSignalIndex());
441 auto signalName = QString::fromUtf8(ba: metaMethod.name());
442 qCDebug(lcWindowContainer).noquote() << d->window << signalName;
443 }
444
445 syncWindowToItem();
446
447 if (d->containerMode == ItemControlsWindow) {
448 qCDebug(lcWindowContainer) << "Overriding window state by polishing";
449 // Ideally we'd always call ensurePolished() here, to synchronously
450 // override the window state ASAP, rather than wait for polish to
451 // trigger it asynchronously, but due to QWindowPrivate::setVisible
452 // emitting visibleChanged before updating the platform window, we
453 // end up applying our override temporarily, only to have QWindowPrivate
454 // follow up with the original change to the platform window.
455 if (d->window->isVisible() != isVisible())
456 polish();
457 else
458 ensurePolished();
459 }
460}
461
462bool QQuickWindowContainer::eventFilter(QObject *object, QEvent *event)
463{
464 Q_D(const QQuickWindowContainer);
465 Q_ASSERT(object == d->window);
466
467 if (event->type() == QEvent::PlatformSurface) {
468 auto type = static_cast<QPlatformSurfaceEvent*>(event)->surfaceEventType();
469 if (type == QPlatformSurfaceEvent::SurfaceCreated) {
470 qCDebug(lcWindowContainer) << "Surface created for" << object;
471 syncWindowToItem();
472 // The surface creation has already resulted in the native window
473 // being added to its parent, on top of all other windows. We need
474 // to do a synchronous re-stacking of the windows here, to avoid
475 // leaving the window in the wrong position while waiting for the
476 // asynchronous callback to QQuickWindow::polishItems().
477 if (auto *quickWindow = qobject_cast<QQuickWindow*>(object: window()))
478 QQuickWindowPrivate::get(c: quickWindow)->updateChildWindowStackingOrder();
479 }
480 }
481
482 return QQuickImplicitSizeItem::eventFilter(watched: object, event);
483}
484
485void QQuickWindowContainer::windowDestroyed()
486{
487 Q_D(QQuickWindowContainer);
488 qCDebug(lcWindowContainer) << "Window" << (void*)d->window << "destroyed";
489
490 d->window->removeEventFilter(obj: this);
491 d->window = nullptr;
492
493 syncWindowToItem(); // Reset state based on not having a window
494 emit containedWindowChanged(window: d->window);
495}
496
497// ----------------------- Item updates -----------------------
498
499/*!
500 \internal
501
502 Called when the item's geometry has changed
503*/
504void QQuickWindowContainer::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
505{
506 qCDebug(lcWindowContainer) << this << "geometry changed from"
507 << oldGeometry << "to" << newGeometry;
508
509 QQuickImplicitSizeItem::geometryChange(newGeometry, oldGeometry);
510 if (newGeometry.isValid())
511 polish();
512}
513
514/*!
515 \internal
516
517 Called when the item's (effective) state has changed
518*/
519void QQuickWindowContainer::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data)
520{
521 switch (change) {
522 case ItemVisibleHasChanged:
523 qCDebug(lcWindowContainer) << "Visible changed for" << this << "to" << isVisible();
524 polish();
525 break;
526 default:
527 break;
528 }
529
530 QQuickImplicitSizeItem::itemChange(change, data);
531}
532
533/*!
534 \internal
535
536 Called when the window container item is moved to another window
537*/
538void QQuickWindowContainer::parentWindowChanged(QQuickWindow *parentWindow)
539{
540 qCDebug(lcWindowContainer) << this << "parent window changed to" << parentWindow;
541
542 Q_D(QQuickWindowContainer);
543
544 if (!parentWindow) {
545 // We have been removed from the window we were part of,
546 // possibly because the window is going away. We need to
547 // make sure the contained window is no longer a child of
548 // former window, as otherwise it will be wiped out along
549 // with it. We can't wait for updatePolish() to do that
550 // as polish has no effect when an item is not part of a
551 // window.
552 if (d->window) {
553 // The window should already be destroyed from the
554 // call to releaseResources(), which is part of the
555 // removal of an item from a scene, but just in case
556 // we do it here as well.
557 d->window->destroy();
558
559 d->window->setParent(nullptr);
560 }
561 } else {
562 polish();
563 }
564}
565
566bool QQuickWindowContainerPrivate::transformChanged(QQuickItem *transformedItem)
567{
568 Q_Q(QQuickWindowContainer);
569
570 if (this->componentComplete && this->window) {
571 auto *transformedItemPrivate = QQuickItemPrivate::get(item: transformedItem);
572 qCDebug(lcWindowContainer) << "Transform changed for" << transformedItem
573 << "with dirty state" << transformedItemPrivate->dirtyToString();
574
575 if (transformedItemPrivate->dirtyAttributes
576 & QQuickItemPrivate::BasicTransform) {
577 // For some reason scale transforms, which result in the window
578 // being resized, end up with the window lagging a frame or two
579 // behind the item. Polish synchronously instead, to mitigate
580 // this, even if it may result in the opposite situation.
581 q->ensurePolished();
582 } else {
583 q->polish();
584 }
585 }
586
587 return QQuickItemPrivate::transformChanged(transformedItem);
588}
589
590QT_END_NAMESPACE
591

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

source code of qtdeclarative/src/quick/items/qquickwindowcontainer.cpp