1// Copyright (C) 2019 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "qquick3dloader_p.h"
5
6#include "qquick3dobject_p.h"
7
8#include <QtQml/qqmlinfo.h>
9
10#include <private/qqmlengine_p.h>
11#include <private/qqmlglobal_p.h>
12
13#include <private/qqmlcomponent_p.h>
14#include <private/qqmlincubator_p.h>
15
16QT_BEGIN_NAMESPACE
17
18void QQuick3DLoaderIncubator::statusChanged(QQmlIncubator::Status status)
19{
20 m_loader->incubatorStateChanged(status);
21}
22
23void QQuick3DLoaderIncubator::setInitialState(QObject *o)
24{
25 m_loader->setInitialState(o);
26}
27
28/*!
29 \qmltype Loader3D
30 \inqmlmodule QtQuick3D
31 \inherits Node
32
33 \brief Allows dynamic loading of a 3D subtree from a URL or Component.
34
35 Loader3D is used to dynamically load QML components for Qt Quick 3D.
36
37 Loader3D can load a
38 QML file (using the \l source property) or a \l Component object (using
39 the \l sourceComponent property). It is useful for delaying the creation
40 of a component until it is required: for example, when a component should
41 be created on demand, or when a component should not be created
42 unnecessarily for performance reasons.
43
44 \note Loader3D works the same way as \l Loader. The difference between the
45 two is that \l Loader provides a way to dynamically load objects that inherit
46 \l Item, whereas Loader3D provides a way to load objects that inherit \l Object3D
47 and is part of a 3D scene.
48*/
49
50QQuick3DLoader::QQuick3DLoader(QQuick3DNode *parent)
51 : QQuick3DNode(parent)
52 , m_item(nullptr)
53 , m_object(nullptr)
54 , m_itemContext(nullptr)
55 , m_incubator(nullptr)
56 , m_active(true)
57 , m_loadingFromSource(false)
58 , m_asynchronous(false)
59{
60}
61
62QQuick3DLoader::~QQuick3DLoader()
63{
64 delete m_itemContext;
65 m_itemContext = nullptr;
66 delete m_incubator;
67 m_incubator = nullptr;
68 disposeInitialPropertyValues();
69 clear();
70}
71
72/*!
73 \qmlproperty bool QtQuick3D::Loader3D::active
74 This property is \c true if the Loader3D is currently active.
75 The default value for this property is \c true.
76
77 If the Loader3D is inactive, changing the \l source or \l sourceComponent
78 will not cause the item to be instantiated until the Loader3D is made active.
79
80 Setting the value to inactive will cause any \l item loaded by the loader
81 to be released, but will not affect the \l source or \l sourceComponent.
82
83 The \l status of an inactive loader is always \c Null.
84
85 \sa source, sourceComponent
86 */
87
88bool QQuick3DLoader::active() const
89{
90 return m_active;
91}
92
93void QQuick3DLoader::setActive(bool newVal)
94{
95 if (m_active == newVal)
96 return;
97
98 m_active = newVal;
99 if (newVal) {
100 if (m_loadingFromSource) {
101 loadFromSource();
102 } else {
103 loadFromSourceComponent();
104 }
105 } else {
106 // cancel any current incubation
107 if (m_incubator) {
108 m_incubator->clear();
109 delete m_itemContext;
110 m_itemContext = nullptr;
111 }
112
113 // Prevent any bindings from running while waiting for deletion. Without
114 // this we may get transient errors from use of 'parent', for example.
115 QQmlContext *context = qmlContext(m_object);
116 if (context)
117 QQmlContextData::get(context)->clearContextRecursively();
118
119 if (m_item) {
120 // We can't delete immediately because our item may have triggered
121 // the Loader to load a different item.
122 m_item->setParentItem(nullptr);
123 m_item->setVisible(false);
124 m_item = nullptr;
125 }
126 if (m_object) {
127 m_object->deleteLater();
128 m_object = nullptr;
129 emit itemChanged();
130 }
131 emit statusChanged();
132 }
133 emit activeChanged();
134}
135
136void QQuick3DLoader::setSource(QQmlV4Function *args)
137{
138 bool ipvError = false;
139 args->setReturnValue(QV4::Encode::undefined());
140 QV4::Scope scope(args->v4engine());
141 QV4::ScopedValue ipv(scope, extractInitialPropertyValues(args, error: &ipvError));
142 if (ipvError)
143 return;
144
145 clear();
146 QUrl sourceUrl = resolveSourceUrl(args);
147 if (!ipv->isUndefined()) {
148 disposeInitialPropertyValues();
149 m_initialPropertyValues.set(engine: args->v4engine(), value: ipv);
150 }
151 m_qmlCallingContext.set(engine: scope.engine, obj: scope.engine->qmlContext());
152
153 setSource(sourceUrl, needsClear: false); // already cleared and set ipv above.
154}
155
156/*!
157 \qmlproperty url QtQuick3D::Loader3D::source
158 This property holds the URL of the QML component to instantiate.
159
160 To unload the currently loaded object, set this property to an empty string,
161 or set \l sourceComponent to \c undefined. Setting \c source to a
162 new URL will also cause the item created by the previous URL to be unloaded.
163
164 \sa sourceComponent, status, progress
165*/
166
167QUrl QQuick3DLoader::source() const
168{
169 return m_source;
170}
171
172void QQuick3DLoader::setSource(const QUrl &url)
173{
174 setSource(sourceUrl: url, needsClear: true);
175}
176
177/*!
178 \qmlproperty Component QtQuick3D::Loader3D::sourceComponent
179 This property holds the \l{Component} to instantiate.
180
181 \qml
182 Item {
183 Component {
184 id: redCube
185 Model {
186 source: "#Cube"
187 materials: DefaultMaterial {
188 diffuseColor: "red"
189 }
190 }
191 }
192
193 Loader3D { sourceComponent: redCube }
194 Loader3D { sourceComponent: redCube; x: 10 }
195 }
196 \endqml
197
198 To unload the currently loaded object, set this property to \c undefined.
199
200 \sa source, progress
201*/
202
203/*!
204 \qmlmethod object QtQuick3D::Loader3D::setSource(url source, object properties)
205
206 Creates an object instance of the given \a source component that will have
207 the given \a properties. The \a properties argument is optional. The instance
208 will be accessible via the \l item property once loading and instantiation
209 is complete.
210
211 If the \l active property is \c false at the time when this function is called,
212 the given \a source component will not be loaded but the \a source and initial
213 \a properties will be cached. When the loader is made \l active, an instance of
214 the \a source component will be created with the initial \a properties set.
215
216 Setting the initial property values of an instance of a component in this manner
217 will \b{not} trigger any associated \l{Behavior}s.
218
219 Note that the cached \a properties will be cleared if the \l source or \l sourceComponent
220 is changed after calling this function but prior to setting the loader \l active.
221
222 \sa source, active
223*/
224
225QQmlComponent *QQuick3DLoader::sourceComponent() const
226{
227 return m_component;
228}
229
230void QQuick3DLoader::setSourceComponent(QQmlComponent *comp)
231{
232 if (comp == m_component)
233 return;
234
235 clear();
236
237 m_component.setObject(obj: comp, parent: this);
238 m_loadingFromSource = false;
239
240 if (m_active)
241 loadFromSourceComponent();
242 else
243 emit sourceComponentChanged();
244}
245
246void QQuick3DLoader::resetSourceComponent()
247{
248 setSourceComponent(nullptr);
249}
250
251/*!
252 \qmlproperty enumeration QtQuick3D::Loader3D::status
253 \readonly
254
255 This property holds the status of QML loading. It can be one of:
256
257 \value Loader3D.Null The loader is inactive or no QML source has been set.
258 \value Loader3D.Ready The QML source has been loaded.
259 \value Loader3D.Loading The QML source is currently being loaded.
260 \value Loader3D.Error An error occurred while loading the QML source.
261
262 Use this status to provide an update or respond to the status change in some way.
263 For example, you could:
264
265 \list
266 \li Trigger a state change:
267 \qml
268 State { name: 'loaded'; when: loader.status == Loader3D.Ready }
269 \endqml
270
271 \li Implement an \c onStatusChanged signal handler:
272 \qml
273 Loader3D {
274 id: loader
275 onStatusChanged: if (loader.status == Loader3D.Ready) console.log('Loaded')
276 }
277 \endqml
278
279 \li Bind to the status value:
280 \qml
281 Text { text: loader.status == Loader3D.Ready ? 'Loaded' : 'Not loaded' }
282 \endqml
283 \endlist
284
285 Note that if the source is a local file, the status will initially be Ready (or Error). While
286 there will be no onStatusChanged signal in that case, the onLoaded will still be invoked.
287
288 \sa progress
289*/
290
291QQuick3DLoader::Status QQuick3DLoader::status() const
292{
293 if (!m_active)
294 return Null;
295
296 if (m_component) {
297 switch (m_component->status()) {
298 case QQmlComponent::Loading:
299 return Loading;
300 case QQmlComponent::Error:
301 return Error;
302 case QQmlComponent::Null:
303 return Null;
304 default:
305 break;
306 }
307 }
308
309 if (m_incubator) {
310 switch (m_incubator->status()) {
311 case QQmlIncubator::Loading:
312 return Loading;
313 case QQmlIncubator::Error:
314 return Error;
315 default:
316 break;
317 }
318 }
319
320 if (m_object)
321 return Ready;
322
323 return m_source.isEmpty() ? Null : Error;
324}
325
326/*!
327 \qmlsignal QtQuick3D::Loader3D::loaded()
328
329 This signal is emitted when the \l status becomes \c Loader3D.Ready, or on successful
330 initial load.
331
332 The corresponding handler is \c onLoaded.
333*/
334
335
336/*!
337 \qmlproperty real QtQuick3D::Loader3D::progress
338 \readonly
339
340 This property holds the progress of loading QML data from the network, from
341 0.0 (nothing loaded) to 1.0 (finished). Most QML files are quite small, so
342 this value will rapidly change from 0 to 1.
343
344 \sa status
345*/
346
347qreal QQuick3DLoader::progress() const
348{
349
350 if (m_object)
351 return 1.0;
352
353 if (m_component)
354 return m_component->progress();
355
356 return 0.0;
357}
358
359/*!
360\qmlproperty bool QtQuick3D::Loader3D::asynchronous
361
362This property holds whether the component will be instantiated asynchronously.
363By default it is \c false.
364
365When used in conjunction with the \l source property, loading and compilation
366will also be performed in a background thread.
367
368Loading asynchronously creates the objects declared by the component
369across multiple frames, and reduces the
370likelihood of glitches in animation. When loading asynchronously the status
371will change to Loader3D.Loading. Once the entire component has been created, the
372\l item will be available and the status will change to Loader.Ready.
373
374Changing the value of this property to \c false while an asynchronous load is in
375progress will force immediate, synchronous completion. This allows beginning an
376asynchronous load and then forcing completion if the Loader3D content must be
377accessed before the asynchronous load has completed.
378
379To avoid seeing the items loading progressively set \c visible appropriately, e.g.
380
381\code
382Loader3D {
383 source: "mycomponent.qml"
384 asynchronous: true
385 visible: status == Loader3D.Ready
386}
387\endcode
388
389Note that this property affects object instantiation only; it is unrelated to
390loading a component asynchronously via a network.
391*/
392
393bool QQuick3DLoader::asynchronous() const
394{
395 return m_asynchronous;
396}
397
398void QQuick3DLoader::setAsynchronous(bool a)
399{
400 if (m_asynchronous == a)
401 return;
402
403 m_asynchronous = a;
404
405 if (!m_asynchronous && isComponentComplete() && m_active) {
406 if (m_loadingFromSource && m_component && m_component->isLoading()) {
407 // Force a synchronous component load
408 QUrl currentSource = m_source;
409 clear();
410 m_source = currentSource;
411 loadFromSource();
412 } else if (m_incubator && m_incubator->isLoading()) {
413 m_incubator->forceCompletion();
414 }
415 }
416
417 emit asynchronousChanged();
418}
419
420/*!
421 \qmlproperty object QtQuick3D::Loader3D::item
422 \readonly
423 This property holds the top-level object that is currently loaded.
424*/
425QObject *QQuick3DLoader::item() const
426{
427 return m_object;
428}
429
430void QQuick3DLoader::componentComplete()
431{
432 QQuick3DNode::componentComplete();
433 if (active()) {
434 if (m_loadingFromSource)
435 createComponent();
436 load();
437 }
438}
439
440void QQuick3DLoader::sourceLoaded()
441{
442 if (!m_component || !m_component->errors().isEmpty()) {
443 if (m_component)
444 QQmlEnginePrivate::warning(qmlEngine(this), m_component->errors());
445 if (m_loadingFromSource)
446 emit sourceChanged();
447 else
448 emit sourceComponentChanged();
449 emit statusChanged();
450 emit progressChanged();
451 emit itemChanged(); //Like clearing source, emit itemChanged even if previous item was also null
452 disposeInitialPropertyValues(); // cleanup
453 return;
454 }
455
456 QQmlContext *creationContext = m_component->creationContext();
457 if (!creationContext) creationContext = qmlContext(this);
458 m_itemContext = new QQmlContext(creationContext);
459 m_itemContext->setContextObject(this);
460
461 delete m_incubator;
462 m_incubator = new QQuick3DLoaderIncubator(this, m_asynchronous ? QQmlIncubator::Asynchronous : QQmlIncubator::AsynchronousIfNested);
463
464 m_component->create(*m_incubator, context: m_itemContext);
465
466 if (m_incubator && m_incubator->status() == QQmlIncubator::Loading)
467 emit statusChanged();
468}
469
470void QQuick3DLoader::setSource(const QUrl &sourceUrl, bool needsClear)
471{
472 if (m_source == sourceUrl)
473 return;
474
475 if (needsClear)
476 clear();
477
478 m_source = sourceUrl;
479 m_loadingFromSource = true;
480
481 if (m_active)
482 loadFromSource();
483 else
484 emit sourceChanged();
485}
486
487void QQuick3DLoader::loadFromSource()
488{
489 if (m_source.isEmpty()) {
490 emit sourceChanged();
491 emit statusChanged();
492 emit progressChanged();
493 emit itemChanged();
494 return;
495 }
496
497 if (isComponentComplete()) {
498 if (!m_component)
499 createComponent();
500 load();
501 }
502}
503
504void QQuick3DLoader::loadFromSourceComponent()
505{
506 if (!m_component) {
507 emit sourceComponentChanged();
508 emit statusChanged();
509 emit progressChanged();
510 emit itemChanged();
511 return;
512 }
513
514 if (isComponentComplete())
515 load();
516}
517
518void QQuick3DLoader::clear()
519{
520 disposeInitialPropertyValues();
521
522 if (m_incubator)
523 m_incubator->clear();
524
525 delete m_itemContext;
526 m_itemContext = nullptr;
527
528 // Prevent any bindings from running while waiting for deletion. Without
529 // this we may get transient errors from use of 'parent', for example.
530 QQmlContext *context = qmlContext(m_object);
531 if (context)
532 QQmlContextData::get(context)->clearContextRecursively();
533
534 if (m_loadingFromSource && m_component) {
535 // disconnect since we deleteLater
536 QObject::disconnect(sender: m_component, SIGNAL(statusChanged(QQmlComponent::Status)),
537 receiver: this, SLOT(sourceLoaded()));
538 QObject::disconnect(sender: m_component, SIGNAL(progressChanged(qreal)),
539 receiver: this, SIGNAL(progressChanged()));
540 m_component->deleteLater();
541 m_component.setObject(obj: nullptr, parent: this);
542 } else if (m_component) {
543 m_component.setObject(obj: nullptr, parent: this);
544 }
545 m_source = QUrl();
546
547 if (m_item) {
548 // We can't delete immediately because our item may have triggered
549 // the Loader to load a different item.
550 m_item->setParentItem(nullptr);
551 m_item->setVisible(false);
552 m_item = nullptr;
553 }
554 if (m_object) {
555 m_object->deleteLater();
556 m_object = nullptr;
557 }
558}
559
560void QQuick3DLoader::load()
561{
562
563 if (!isComponentComplete() || !m_component)
564 return;
565
566 if (!m_component->isLoading()) {
567 sourceLoaded();
568 } else {
569 QObject::connect(sender: m_component, SIGNAL(statusChanged(QQmlComponent::Status)),
570 receiver: this, SLOT(sourceLoaded()));
571 QObject::connect(sender: m_component, SIGNAL(progressChanged(qreal)),
572 receiver: this, SIGNAL(progressChanged()));
573 emit statusChanged();
574 emit progressChanged();
575 if (m_loadingFromSource)
576 emit sourceChanged();
577 else
578 emit sourceComponentChanged();
579 emit itemChanged();
580 }
581}
582
583void QQuick3DLoader::incubatorStateChanged(QQmlIncubator::Status status)
584{
585 if (status == QQmlIncubator::Loading || status == QQmlIncubator::Null)
586 return;
587
588 if (status == QQmlIncubator::Ready) {
589 m_object = m_incubator->object();
590 m_item = qmlobject_cast<QQuick3DNode*>(object: m_object);
591 emit itemChanged();
592 m_incubator->clear();
593 } else if (status == QQmlIncubator::Error) {
594 if (!m_incubator->errors().isEmpty())
595 QQmlEnginePrivate::warning(qmlEngine(this), m_incubator->errors());
596 delete m_itemContext;
597 m_itemContext = nullptr;
598 delete m_incubator->object();
599 m_source = QUrl();
600 emit itemChanged();
601 }
602 if (m_loadingFromSource)
603 emit sourceChanged();
604 else
605 emit sourceComponentChanged();
606 emit statusChanged();
607 emit progressChanged();
608 if (status == QQmlIncubator::Ready)
609 emit loaded();
610 disposeInitialPropertyValues(); // cleanup
611}
612
613void QQuick3DLoader::setInitialState(QObject *obj)
614{
615 QQuick3DObject *item = qmlobject_cast<QQuick3DObject*>(object: obj);
616 if (item) {
617 item->setParentItem(this);
618 }
619 if (obj) {
620 QQml_setParent_noEvent(object: m_itemContext, parent: obj);
621 QQml_setParent_noEvent(object: obj, parent: this);
622 m_itemContext = nullptr;
623 }
624
625 if (m_initialPropertyValues.isUndefined())
626 return;
627
628 QQmlComponentPrivate *d = QQmlComponentPrivate::get(c: m_component);
629 Q_ASSERT(d && d->engine);
630 QV4::ExecutionEngine *v4 = d->engine->handle();
631 Q_ASSERT(v4);
632 QV4::Scope scope(v4);
633 QV4::ScopedValue ipv(scope, m_initialPropertyValues.value());
634 QV4::Scoped<QV4::QmlContext> qmlContext(scope, m_qmlCallingContext.value());
635 d->initializeObjectWithInitialProperties(qmlContext, valuemap: ipv, toCreate: obj, requiredProperties: QQmlIncubatorPrivate::get(incubator: m_incubator)->requiredProperties());
636}
637
638void QQuick3DLoader::disposeInitialPropertyValues()
639{
640
641}
642
643QUrl QQuick3DLoader::resolveSourceUrl(QQmlV4Function *args)
644{
645 QV4::Scope scope(args->v4engine());
646 QV4::ScopedValue v(scope, (*args)[0]);
647 QString arg = v->toQString();
648 if (arg.isEmpty())
649 return QUrl();
650
651 auto context = scope.engine->callingQmlContext();
652 Q_ASSERT(!context.isNull());
653 return context->resolvedUrl(QUrl(arg));
654}
655
656QV4::ReturnedValue QQuick3DLoader::extractInitialPropertyValues(QQmlV4Function *args, bool *error)
657{
658 QV4::Scope scope(args->v4engine());
659 QV4::ScopedValue valuemap(scope, QV4::Encode::undefined());
660 if (args->length() >= 2) {
661 QV4::ScopedValue v(scope, (*args)[1]);
662 if (!v->isObject() || v->as<QV4::ArrayObject>()) {
663 *error = true;
664 qmlWarning(me: this) << QQuick3DLoader::tr(s: "setSource: value is not an object");
665 } else {
666 *error = false;
667 valuemap = v;
668 }
669 }
670
671 return valuemap->asReturnedValue();
672}
673
674void QQuick3DLoader::createComponent()
675{
676 const QQmlComponent::CompilationMode mode = m_asynchronous
677 ? QQmlComponent::Asynchronous
678 : QQmlComponent::PreferSynchronous;
679 QQmlContext *context = qmlContext(this);
680 m_component.setObject(obj: new QQmlComponent(context->engine(),
681 context->resolvedUrl(m_source),
682 mode,
683 this),
684 parent: this);
685}
686
687QT_END_NAMESPACE
688
689#include "moc_qquick3dloader_p.cpp"
690

source code of qtquick3d/src/quick3d/qquick3dloader.cpp