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 | |
13 | QT_BEGIN_NAMESPACE |
14 | |
15 | Q_LOGGING_CATEGORY(lcWindowContainer, "qt.quick.window.container") |
16 | |
17 | using 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 | |
86 | class QQuickWindowContainerPrivate : public QQuickImplicitSizeItemPrivate |
87 | { |
88 | Q_DECLARE_PUBLIC(QQuickWindowContainer) |
89 | protected: |
90 | bool transformChanged(QQuickItem *transformedItem) override; |
91 | |
92 | public: |
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 | */ |
109 | QQuickWindowContainer::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 | |
135 | QQuickWindowContainer::~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 | |
155 | void 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 | |
164 | void QQuickWindowContainer::classBegin() |
165 | { |
166 | qCDebug(lcWindowContainer) << "Class begin for"<< this; |
167 | |
168 | QQuickImplicitSizeItem::classBegin(); |
169 | } |
170 | |
171 | void 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 | |
182 | QWindow *QQuickWindowContainer::containedWindow() const |
183 | { |
184 | Q_D(const QQuickWindowContainer); |
185 | return d->window; |
186 | } |
187 | |
188 | void 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 | |
238 | void 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 | |
250 | static 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 | |
260 | void 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 | */ |
299 | void 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 | */ |
404 | QRectF 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 | */ |
435 | void 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 | |
462 | bool 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 | |
485 | void 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 | */ |
504 | void 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 | */ |
519 | void 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 | */ |
538 | void 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 | |
566 | bool 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 | |
590 | QT_END_NAMESPACE |
591 |
Definitions
- lcWindowContainer
- QQuickWindowContainerPrivate
- QQuickWindowContainer
- ~QQuickWindowContainer
- releaseResources
- classBegin
- componentComplete
- containedWindow
- setContainedWindow
- initializeContainedWindow
- sanitizeTransform
- syncWindowToItem
- updatePolish
- clipRect
- windowUpdated
- eventFilter
- windowDestroyed
- geometryChange
- itemChange
- parentWindowChanged
Learn Advanced QML with KDAB
Find out more