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_STATIC_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 setFocusPolicy(Qt::TabFocus);
121
122 connect(sender: this, signal: &QQuickItem::windowChanged,
123 context: this, slot: &QQuickWindowContainer::parentWindowChanged);
124
125 if (lcWindowContainer().isDebugEnabled()) {
126 auto *debugRectangle = new QQuickRectangle(this);
127 debugRectangle->setColor(QColor(255, 0, 255, 20));
128 auto *border = debugRectangle->border();
129 border->setColor(Qt::magenta);
130 border->setWidth(1.0);
131 QQuickItemPrivate *rectPrivate = QQuickItemPrivate::get(item: debugRectangle);
132 rectPrivate->anchors()->setFill(this);
133 }
134}
135
136QQuickWindowContainer::~QQuickWindowContainer()
137{
138 Q_D(const QQuickWindowContainer);
139 qCDebug(lcWindowContainer) << "Destructing window container" << this;
140
141 disconnect(receiver: this);
142 if (d->window) {
143 auto ownership = QJSEngine::objectOwnership(d->window);
144 qCDebug(lcWindowContainer) << "Contained window" << d->window
145 << "has" << (ownership == QQmlEngine::JavaScriptOwnership ?
146 "JavaScript" : "C++") << "ownership";
147 if (ownership == QQmlEngine::JavaScriptOwnership) {
148 delete d->window;
149 } else {
150 d->window->destroy();
151 d->window->setParent(nullptr);
152 }
153 }
154}
155
156void QQuickWindowContainer::releaseResources()
157{
158 Q_D(const QQuickWindowContainer);
159 qCDebug(lcWindowContainer) << "Destroying" << d->window
160 << "with platform window" << (d->window ? d->window->handle() : nullptr);
161 if (d->window)
162 d->window->destroy();
163}
164
165void QQuickWindowContainer::classBegin()
166{
167 qCDebug(lcWindowContainer) << "Class begin for" << this;
168
169 QQuickImplicitSizeItem::classBegin();
170}
171
172void QQuickWindowContainer::componentComplete()
173{
174 Q_D(const QQuickWindowContainer);
175
176 qCDebug(lcWindowContainer) << "Component completed for" << this;
177 QQuickImplicitSizeItem::componentComplete();
178
179 if (d->window)
180 initializeContainedWindow();
181}
182
183QWindow *QQuickWindowContainer::containedWindow() const
184{
185 Q_D(const QQuickWindowContainer);
186 return d->window;
187}
188
189void QQuickWindowContainer::setContainedWindow(QWindow *window)
190{
191 qCDebug(lcWindowContainer) << "Setting contained window for" << this << "to" << window;
192
193 Q_D(QQuickWindowContainer);
194
195 if (window == d->window)
196 return;
197
198 if (auto *previousWindow = d->window) {
199 qCDebug(lcWindowContainer) << "Decoupling container from" << d->window;
200 previousWindow->disconnect(receiver: this);
201 previousWindow->removeEventFilter(obj: this);
202 previousWindow->setParent(nullptr);
203 }
204
205 d->window = window;
206
207 if (d->window) {
208 if (d->containerMode == ItemControlsWindow) {
209 if (auto *quickWindow = qobject_cast<QQuickWindowQmlImpl*>(object: d->window)) {
210 // Make sure the Window reflects the window container as its visual parent
211 quickWindow->setVisualParent(this);
212 }
213 }
214
215 // When the window controls the container, we need to reflect any changes
216 // in the window back to the container, so they stay in sync. And when the
217 // container controls the window, we still want to reflect width/height as
218 // new implicit size, and override any other changes with the item state.
219 connect(sender: d->window, signal: &QWindow::xChanged, context: this, slot: &QQuickWindowContainer::windowUpdated);
220 connect(sender: d->window, signal: &QWindow::yChanged, context: this, slot: &QQuickWindowContainer::windowUpdated);
221 connect(sender: d->window, signal: &QWindow::widthChanged, context: this, slot: &QQuickWindowContainer::windowUpdated);
222 connect(sender: d->window, signal: &QWindow::heightChanged, context: this, slot: &QQuickWindowContainer::windowUpdated);
223 connect(sender: d->window, signal: &QWindow::visibleChanged, context: this, slot: &QQuickWindowContainer::windowUpdated);
224
225 connect(sender: d->window, signal: &QObject::destroyed, context: this, slot: &QQuickWindowContainer::windowDestroyed);
226
227 d->window->installEventFilter(filterObj: this);
228
229 if (d->componentComplete)
230 initializeContainedWindow();
231 } else {
232 // Reset state based on not having a window
233 syncWindowToItem();
234 }
235
236 emit containedWindowChanged(window: d->window);
237}
238
239void QQuickWindowContainer::initializeContainedWindow()
240{
241 Q_D(const QQuickWindowContainer);
242 Q_ASSERT(d->componentComplete);
243 Q_ASSERT(d->window);
244
245 qCDebug(lcWindowContainer) << "Doing initial sync between" << d->window << "and" << this;
246
247 syncWindowToItem();
248 polish();
249}
250
251static QTransform sanitizeTransform(const QTransform &transform)
252{
253 if (transform.isRotating()) {
254 // FIXME: Can we keep more here?
255 return QTransform::fromTranslate(dx: transform.dx(), dy: transform.dy());
256 }
257
258 return transform;
259}
260
261void QQuickWindowContainer::syncWindowToItem()
262{
263 Q_D(const QQuickWindowContainer);
264
265 const auto windowGeometry = d->window ? d->window->geometry() : QRect();
266
267 qCDebug(lcWindowContainer) << "Syncing window state from" << d->window
268 << "with geometry" << windowGeometry << "to" << this
269 << "with mode" << d->containerMode;
270
271 const auto transform = sanitizeTransform(transform: d->windowToItemTransform());
272
273 // The window might have a larger size than the item's natural
274 // size, if there's a scale applied somewhere in the hierarchy.
275 auto itemSize = d->window ? transform.mapRect(windowGeometry).size()
276 : QSize();
277
278 if (d->containerMode == WindowControlsItem) {
279 // When the Window controls the window container the position is
280 // set up front, when creating the window container, and from that
281 // point on set exclusively via the window container, so we skip
282 // setting the position here, and only set the size.
283 setSize(itemSize);
284 setVisible(d->window ? d->window->isVisible() : false);
285 } else {
286 // Position defined by item, so don't sync from window
287 // Visible defined by item, so don't sync from window
288 setImplicitWidth(itemSize.width());
289 setImplicitHeight(itemSize.height());
290 }
291}
292
293/*!
294 \internal
295
296 updatePolish() should perform any layout as required for this item.
297
298 For us, that means propagating the item's state to the window.
299*/
300void QQuickWindowContainer::updatePolish()
301{
302 Q_D(QQuickWindowContainer);
303
304 qCDebug(lcWindowContainer) << "Propagating" << this << "state"
305 << "to" << d->window;
306
307 auto *parentWindow = window();
308
309 // FIXME: If we are part of a QQuickWidget, we have a QQuickRenderControl,
310 // and should look up the parent window via that, and apply the offset we
311 // get to the item transform below. But at the moment it's not possible
312 // to observe changes to the offset, which is critical to support this
313 // for child windows.
314
315 if (!d->window || !parentWindow)
316 return;
317
318 if (d->window->parent() != parentWindow) {
319 qCDebug(lcWindowContainer) << "Updating window parent to" << parentWindow;
320 d->window->setParent(parentWindow);
321 }
322
323 auto transform = sanitizeTransform(transform: d->itemToWindowTransform());
324
325 // Find the window's geometry, based on the item's bounding rect,
326 // mapped to the scene. The mapping includes any x/y position set
327 // on the item itself, as well as any transforms applied to the item
328 // or its ancestor (scale, translation).
329 const QRectF itemSceneRect = transform.mapRect(boundingRect());
330 // FIXME: Rounding to a QRect here means we'll have some jitter or off
331 // placement when the underlying item is not on a integer coordinate.
332 QRect windowGeometry = itemSceneRect.toRect();
333 if (windowGeometry != d->window->geometry()) {
334 QRectF itemRect(position(), size());
335 qCDebug(lcWindowContainer) << "Updating window geometry to" << windowGeometry
336 << "based on item rect" << itemRect << "and scene rect" << itemSceneRect;
337 d->window->setGeometry(windowGeometry);
338 }
339
340 // Clip the container to its own and ancestor clip rects, by setting
341 // a mask on the window. This does not necessarily clip native windows,
342 // as QWindow::setMask() is not guaranteed to visually clip the window,
343 // only to mask input, but in most cases we should be good. For the
344 // cases where this fails, we can potentially use an intermediate window
345 // as parent of the contained window, if the platform allows clipping
346 // child windows to parent window geometry. We do not want to resize the
347 // contained window, as that will just fill the content into a smaller
348 // area.
349 const auto clipMask = [&]{
350 if (clipRect() == boundingRect())
351 return QRect();
352
353 // The clip rect has all the relevant transforms applied to it,
354 // except for the item's own scale. As the mask is in window
355 // local coordinates in the possibly scaled window, we need
356 // to apply the scale manually.
357 auto scaleTransform = QTransform::fromScale(dx: transform.m11(), dy: transform.m22());
358 auto rect = scaleTransform.mapRect(clipRect()).toRect();
359
360 // An empty clip rect means clip away everything, while for a
361 // window, an empty mask means mask nothing. Fake the former
362 // by setting a mask outside of the window's bounds. We have
363 // to do this check after rounding the clip rect to a QRect.
364 // FIXME: Verify this works on all platforms
365 if (rect.isEmpty())
366 return QRect(-1, -1, 1, 1);
367
368 return rect;
369 }();
370
371 if (clipMask != d->window->mask().boundingRect()) {
372 qCDebug(lcWindowContainer) << "Updating window clip mask to" << clipMask
373 << "based on clip rect" << clipRect();
374 d->window->setMask(clipMask);
375 }
376
377 // FIXME: Opacity support. Need to calculate effective opacity ourselves,
378 // and there doesn't seem to be any existing observer for opacity changes.
379 // Not all platforms implement opacity for child windows yet.
380
381 // FIXME: If a scale is applied to the item or its parents, we end up
382 // with a bigger item, and window, but we don't translate the scale to
383 // an increase device-pixel-ratio of the window. As a result, the window
384 // will likely just render more content, instead of the same content at
385 // a potentially higher density.
386
387 if (d->window->isVisible() != isVisible()) {
388 qCDebug(lcWindowContainer) << "Updating window visibility"
389 << "based on item visible" << isVisible();
390 d->window->setVisible(isVisible());
391 }
392}
393
394/*!
395 \internal
396
397 QQuickItem::clipRect() doesn't take ItemClipsChildrenToShape into
398 account, so a parent item that has clip:false, but ItemIsViewport
399 will still result in affecting the clip.
400
401 We want to stay consistent with the clipping in the scene graph,
402 which is based on QQuickItem::clip(), so we override the clipRect
403 to take ItemClipsChildrenToShape into account.
404*/
405QRectF QQuickWindowContainer::clipRect() const
406{
407 QRectF rect = boundingRect();
408
409 for (auto *viewport = viewportItem(); viewport; viewport = viewport->viewportItem()) {
410 if (viewport == this)
411 break;
412
413 if (viewport->flags().testFlag(flag: QQuickItem::ItemClipsChildrenToShape)) {
414 // FIXME: This fails to take into account viewports that override clipRect()
415 const auto mappedViewportRect = mapRectFromItem(item: viewport, rect: viewport->boundingRect());
416 rect = mappedViewportRect.intersected(r: rect);
417 }
418
419 if (viewport->viewportItem() == viewport)
420 break; // Content item returns itself as viewport
421 }
422
423 return rect;
424}
425
426// ----------------------- Window updates -----------------------
427
428/*!
429 \internal
430
431 Called when the contained QWindow is changed.
432
433 Depending on the sync mode we need to reflect these changes
434 to the item, or override them by applying the item state.
435*/
436void QQuickWindowContainer::windowUpdated()
437{
438 Q_D(const QQuickWindowContainer);
439
440 if (lcWindowContainer().isDebugEnabled()) {
441 auto metaMethod = sender()->metaObject()->method(index: senderSignalIndex());
442 auto signalName = QString::fromUtf8(ba: metaMethod.name());
443 qCDebug(lcWindowContainer).noquote() << d->window << signalName;
444 }
445
446 syncWindowToItem();
447
448 if (d->containerMode == ItemControlsWindow) {
449 qCDebug(lcWindowContainer) << "Overriding window state by polishing";
450 // Ideally we'd always call ensurePolished() here, to synchronously
451 // override the window state ASAP, rather than wait for polish to
452 // trigger it asynchronously, but due to QWindowPrivate::setVisible
453 // emitting visibleChanged before updating the platform window, we
454 // end up applying our override temporarily, only to have QWindowPrivate
455 // follow up with the original change to the platform window.
456 if (d->window->isVisible() != isVisible())
457 polish();
458 else
459 ensurePolished();
460 }
461}
462
463bool QQuickWindowContainer::eventFilter(QObject *object, QEvent *event)
464{
465 Q_D(const QQuickWindowContainer);
466 Q_ASSERT(object == d->window);
467
468 if (event->type() == QEvent::PlatformSurface) {
469 auto type = static_cast<QPlatformSurfaceEvent*>(event)->surfaceEventType();
470 if (type == QPlatformSurfaceEvent::SurfaceCreated) {
471 qCDebug(lcWindowContainer) << "Surface created for" << object;
472 syncWindowToItem();
473 // The surface creation has already resulted in the native window
474 // being added to its parent, on top of all other windows. We need
475 // to do a synchronous re-stacking of the windows here, to avoid
476 // leaving the window in the wrong position while waiting for the
477 // asynchronous callback to QQuickWindow::polishItems().
478 if (auto *quickWindow = qobject_cast<QQuickWindow*>(object: window()))
479 QQuickWindowPrivate::get(c: quickWindow)->updateChildWindowStackingOrder();
480 }
481 }
482
483 return QQuickImplicitSizeItem::eventFilter(watched: object, event);
484}
485
486void QQuickWindowContainer::focusInEvent(QFocusEvent *event)
487{
488 Q_D(QQuickWindowContainer);
489 if (d->window) {
490 const auto reason = event->reason();
491 QWindowPrivate::FocusTarget target = QWindowPrivate::FocusTarget::Current;
492 if (reason == Qt::TabFocusReason)
493 target = QWindowPrivate::FocusTarget::First;
494 else if (reason == Qt::BacktabFocusReason)
495 target = QWindowPrivate::FocusTarget::Last;
496 QWindowPrivate::get(window: d->window)->setFocusToTarget(target, reason);
497 d->window->requestActivate();
498 }
499}
500
501void QQuickWindowContainer::windowDestroyed()
502{
503 Q_D(QQuickWindowContainer);
504 qCDebug(lcWindowContainer) << "Window" << (void*)d->window << "destroyed";
505
506 d->window->removeEventFilter(obj: this);
507 d->window = nullptr;
508
509 syncWindowToItem(); // Reset state based on not having a window
510 emit containedWindowChanged(window: d->window);
511}
512
513// ----------------------- Item updates -----------------------
514
515/*!
516 \internal
517
518 Called when the item's geometry has changed
519*/
520void QQuickWindowContainer::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
521{
522 qCDebug(lcWindowContainer) << this << "geometry changed from"
523 << oldGeometry << "to" << newGeometry;
524
525 QQuickImplicitSizeItem::geometryChange(newGeometry, oldGeometry);
526 if (newGeometry.isValid())
527 polish();
528}
529
530/*!
531 \internal
532
533 Called when the item's (effective) state has changed
534*/
535void QQuickWindowContainer::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data)
536{
537 switch (change) {
538 case ItemVisibleHasChanged:
539 qCDebug(lcWindowContainer) << "Visible changed for" << this << "to" << isVisible();
540 polish();
541 break;
542 default:
543 break;
544 }
545
546 QQuickImplicitSizeItem::itemChange(change, data);
547}
548
549/*!
550 \internal
551
552 Called when the window container item is moved to another window
553*/
554void QQuickWindowContainer::parentWindowChanged(QQuickWindow *parentWindow)
555{
556 qCDebug(lcWindowContainer) << this << "parent window changed to" << parentWindow;
557
558 Q_D(QQuickWindowContainer);
559
560 if (!parentWindow) {
561 // We have been removed from the window we were part of,
562 // possibly because the window is going away. We need to
563 // make sure the contained window is no longer a child of
564 // former window, as otherwise it will be wiped out along
565 // with it. We can't wait for updatePolish() to do that
566 // as polish has no effect when an item is not part of a
567 // window.
568 if (d->window) {
569 // The window should already be destroyed from the
570 // call to releaseResources(), which is part of the
571 // removal of an item from a scene, but just in case
572 // we do it here as well.
573 d->window->destroy();
574
575 d->window->setParent(nullptr);
576 }
577 } else {
578 polish();
579 }
580}
581
582bool QQuickWindowContainerPrivate::transformChanged(QQuickItem *transformedItem)
583{
584 Q_Q(QQuickWindowContainer);
585
586 if (this->componentComplete && this->window) {
587 auto *transformedItemPrivate = QQuickItemPrivate::get(item: transformedItem);
588 qCDebug(lcWindowContainer) << "Transform changed for" << transformedItem
589 << "with dirty state" << transformedItemPrivate->dirtyToString();
590
591 if (transformedItemPrivate->dirtyAttributes
592 & QQuickItemPrivate::BasicTransform) {
593 // For some reason scale transforms, which result in the window
594 // being resized, end up with the window lagging a frame or two
595 // behind the item. Polish synchronously instead, to mitigate
596 // this, even if it may result in the opposite situation.
597 q->ensurePolished();
598 } else {
599 q->polish();
600 }
601 }
602
603 return QQuickItemPrivate::transformChanged(transformedItem);
604}
605
606QT_END_NAMESPACE
607
608#include "moc_qquickwindowcontainer_p.cpp"
609

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