1 | // Copyright (C) 2016 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 "statemachineloader_p.h" |
5 | |
6 | #include <QtScxml/qscxmlstatemachine.h> |
7 | #include <qqmlcontext.h> |
8 | #include <qqmlengine.h> |
9 | #include <qqmlinfo.h> |
10 | #include <qqmlfile.h> |
11 | #include <qbuffer.h> |
12 | |
13 | /*! |
14 | \qmltype StateMachineLoader |
15 | //! \nativetype QScxmlStateMachineLoader |
16 | \inqmlmodule QtScxml |
17 | |
18 | \brief Dynamically loads an SCXML document and instantiates the state machine. |
19 | |
20 | \since QtScxml 5.7 |
21 | */ |
22 | |
23 | QScxmlStateMachineLoader::QScxmlStateMachineLoader(QObject *parent) |
24 | : QObject(parent) |
25 | , m_implicitDataModel(nullptr) |
26 | { |
27 | } |
28 | |
29 | /*! |
30 | \qmlproperty ScxmlStateMachine StateMachineLoader::stateMachine |
31 | |
32 | The state machine instance. |
33 | */ |
34 | QT_PREPEND_NAMESPACE(QScxmlStateMachine) *QScxmlStateMachineLoader::stateMachine() const |
35 | { |
36 | return m_stateMachine; |
37 | } |
38 | |
39 | void QScxmlStateMachineLoader::setStateMachine(QScxmlStateMachine* stateMachine) |
40 | { |
41 | if (m_stateMachine.valueBypassingBindings() != stateMachine) { |
42 | delete m_stateMachine.valueBypassingBindings(); |
43 | m_stateMachine.setValueBypassingBindings(stateMachine); |
44 | } |
45 | } |
46 | |
47 | QBindable<QScxmlStateMachine*> QScxmlStateMachineLoader::bindableStateMachine() |
48 | { |
49 | return &m_stateMachine; |
50 | } |
51 | |
52 | /*! |
53 | \qmlproperty url StateMachineLoader::source |
54 | |
55 | The URL of the SCXML document to load. Only synchronously accessible URLs |
56 | are supported. |
57 | */ |
58 | QUrl QScxmlStateMachineLoader::source() |
59 | { |
60 | return m_source; |
61 | } |
62 | |
63 | void QScxmlStateMachineLoader::setSource(const QUrl &source) |
64 | { |
65 | if (!source.isValid()) |
66 | return; |
67 | |
68 | m_source.removeBindingUnlessInWrapper(); |
69 | |
70 | const QUrl oldSource = m_source.valueBypassingBindings(); |
71 | const auto *oldStateMachine = m_stateMachine.valueBypassingBindings(); |
72 | setStateMachine(nullptr); |
73 | m_implicitDataModel = nullptr; |
74 | |
75 | if (parse(source)) |
76 | m_source.setValueBypassingBindings(source); |
77 | else |
78 | m_source.setValueBypassingBindings(QUrl()); |
79 | |
80 | if (oldSource != m_source.valueBypassingBindings()) |
81 | m_source.notify(); |
82 | |
83 | if (oldStateMachine != m_stateMachine.valueBypassingBindings()) |
84 | m_stateMachine.notify(); |
85 | } |
86 | |
87 | QBindable<QUrl> QScxmlStateMachineLoader::bindableSource() |
88 | { |
89 | return &m_source; |
90 | } |
91 | |
92 | QVariantMap QScxmlStateMachineLoader::initialValues() const |
93 | { |
94 | return m_initialValues; |
95 | } |
96 | |
97 | void QScxmlStateMachineLoader::setInitialValues(const QVariantMap &initialValues) |
98 | { |
99 | m_initialValues.removeBindingUnlessInWrapper(); |
100 | if (initialValues == m_initialValues.valueBypassingBindings()) |
101 | return; |
102 | |
103 | m_initialValues.setValueBypassingBindings(initialValues); |
104 | |
105 | const auto stateMachine = m_stateMachine.valueBypassingBindings(); |
106 | if (stateMachine) |
107 | stateMachine->setInitialValues(initialValues); |
108 | m_initialValues.notify(); |
109 | } |
110 | |
111 | QBindable<QVariantMap> QScxmlStateMachineLoader::bindableInitialValues() |
112 | { |
113 | return &m_initialValues; |
114 | } |
115 | |
116 | QScxmlDataModel *QScxmlStateMachineLoader::dataModel() const |
117 | { |
118 | return m_dataModel; |
119 | } |
120 | |
121 | void QScxmlStateMachineLoader::setDataModel(QScxmlDataModel *dataModel) |
122 | { |
123 | m_dataModel.removeBindingUnlessInWrapper(); |
124 | if (dataModel == m_dataModel.valueBypassingBindings()) { |
125 | return; |
126 | } |
127 | m_dataModel.setValueBypassingBindings(dataModel); |
128 | const auto stateMachine = m_stateMachine.valueBypassingBindings(); |
129 | if (stateMachine) { |
130 | if (dataModel) |
131 | stateMachine->setDataModel(dataModel); |
132 | else |
133 | stateMachine->setDataModel(m_implicitDataModel); |
134 | } |
135 | m_dataModel.notify(); |
136 | } |
137 | |
138 | QBindable<QScxmlDataModel*> QScxmlStateMachineLoader::bindableDataModel() |
139 | { |
140 | return &m_dataModel; |
141 | } |
142 | |
143 | bool QScxmlStateMachineLoader::parse(const QUrl &source) |
144 | { |
145 | if (!QQmlFile::isSynchronous(url: source)) { |
146 | qmlWarning(me: this) << QStringLiteral("Cannot open '%1' for reading: only synchronous access is supported." ) |
147 | .arg(a: source.url()); |
148 | return false; |
149 | } |
150 | QQmlFile scxmlFile(QQmlEngine::contextForObject(this)->engine(), source); |
151 | if (scxmlFile.isError()) { |
152 | // the synchronous case can only fail when the file is not found (or not readable). |
153 | qmlWarning(me: this) << QStringLiteral("Cannot open '%1' for reading." ).arg(a: source.url()); |
154 | return false; |
155 | } |
156 | |
157 | QByteArray data(scxmlFile.dataByteArray()); |
158 | QBuffer buf(&data); |
159 | if (!buf.open(openMode: QIODevice::ReadOnly)) { |
160 | qmlWarning(me: this) << QStringLiteral("Cannot open input buffer for reading" ); |
161 | return false; |
162 | } |
163 | |
164 | QString fileName; |
165 | if (source.isLocalFile()) { |
166 | fileName = source.toLocalFile(); |
167 | } else if (source.scheme() == QStringLiteral("qrc" )) { |
168 | fileName = QStringLiteral(":" ) + source.path(); |
169 | } else { |
170 | qmlWarning(me: this) << QStringLiteral("%1 is neither a local nor a resource URL." ) |
171 | .arg(a: source.url()) |
172 | << QStringLiteral("Invoking services by relative path will not work." ); |
173 | } |
174 | |
175 | auto stateMachine = QScxmlStateMachine::fromData(data: &buf, fileName); |
176 | stateMachine->setParent(this); |
177 | m_implicitDataModel = stateMachine->dataModel(); |
178 | |
179 | if (stateMachine->parseErrors().isEmpty()) { |
180 | if (m_dataModel.value()) |
181 | stateMachine->setDataModel(m_dataModel.value()); |
182 | stateMachine->setInitialValues(m_initialValues.value()); |
183 | setStateMachine(stateMachine); |
184 | // as this is deferred any pending property updates to m_dataModel and m_initialValues |
185 | // should still occur before start(). |
186 | QMetaObject::invokeMethod(obj: m_stateMachine.valueBypassingBindings(), member: "start" , c: Qt::QueuedConnection); |
187 | return true; |
188 | } else { |
189 | qmlWarning(me: this) << QStringLiteral("Something went wrong while parsing '%1':" ) |
190 | .arg(a: source.url()) |
191 | << Qt::endl; |
192 | const auto errors = stateMachine->parseErrors(); |
193 | for (const QScxmlError &error : errors) { |
194 | qmlWarning(me: this) << error.toString(); |
195 | } |
196 | return false; |
197 | } |
198 | } |
199 | |