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 "qquickstategroup_p.h" |
5 | |
6 | #include "qquicktransition_p.h" |
7 | |
8 | #include <private/qqmlbinding_p.h> |
9 | #include <private/qqmlglobal_p.h> |
10 | |
11 | #include <QtCore/qstringlist.h> |
12 | #include <QtCore/qdebug.h> |
13 | #include <QtCore/qvector.h> |
14 | |
15 | #include <private/qobject_p.h> |
16 | #include <qqmlinfo.h> |
17 | |
18 | QT_BEGIN_NAMESPACE |
19 | |
20 | using namespace Qt::StringLiterals; |
21 | |
22 | Q_DECLARE_LOGGING_CATEGORY(lcStates) |
23 | |
24 | class QQuickStateGroupPrivate : public QObjectPrivate |
25 | { |
26 | Q_DECLARE_PUBLIC(QQuickStateGroup) |
27 | public: |
28 | QQuickStateGroupPrivate() |
29 | : nullState(nullptr), componentComplete(true), |
30 | ignoreTrans(false), applyingState(false), unnamedCount(0) {} |
31 | |
32 | QString currentState; |
33 | QQuickState *nullState; |
34 | |
35 | static void append_state(QQmlListProperty<QQuickState> *list, QQuickState *state); |
36 | static qsizetype count_state(QQmlListProperty<QQuickState> *list); |
37 | static QQuickState *at_state(QQmlListProperty<QQuickState> *list, qsizetype index); |
38 | static void clear_states(QQmlListProperty<QQuickState> *list); |
39 | static void replace_states(QQmlListProperty<QQuickState> *list, qsizetype index, QQuickState *state); |
40 | static void removeLast_states(QQmlListProperty<QQuickState> *list); |
41 | |
42 | static void append_transition(QQmlListProperty<QQuickTransition> *list, QQuickTransition *state); |
43 | static qsizetype count_transitions(QQmlListProperty<QQuickTransition> *list); |
44 | static QQuickTransition *at_transition(QQmlListProperty<QQuickTransition> *list, qsizetype index); |
45 | static void clear_transitions(QQmlListProperty<QQuickTransition> *list); |
46 | |
47 | QList<QQuickState *> states; |
48 | QList<QQuickTransition *> transitions; |
49 | |
50 | bool componentComplete; |
51 | bool ignoreTrans; |
52 | bool applyingState; |
53 | int unnamedCount; |
54 | |
55 | QQuickTransition *findTransition(const QString &from, const QString &to); |
56 | void setCurrentStateInternal(const QString &state, bool = false); |
57 | bool updateAutoState(); |
58 | }; |
59 | |
60 | /*! |
61 | \qmltype StateGroup |
62 | \instantiates QQuickStateGroup |
63 | \inqmlmodule QtQuick |
64 | \ingroup qtquick-states |
65 | \brief Provides built-in state support for non-Item types. |
66 | |
67 | Item (and all derived types) provides built in support for states and transitions |
68 | via its \l{Item::state}{state}, \l{Item::states}{states} and \l{Item::transitions}{transitions} properties. StateGroup provides an easy way to |
69 | use this support in other (non-Item-derived) types. |
70 | |
71 | \qml |
72 | MyCustomObject { |
73 | StateGroup { |
74 | id: myStateGroup |
75 | states: State { |
76 | name: "state1" |
77 | // ... |
78 | } |
79 | transitions: Transition { |
80 | // ... |
81 | } |
82 | } |
83 | |
84 | onSomethingHappened: myStateGroup.state = "state1"; |
85 | } |
86 | \endqml |
87 | |
88 | \sa {Qt Quick States}{Qt Quick States}, {Animation and Transitions in Qt Quick}{Transitions}, {Qt QML} |
89 | */ |
90 | |
91 | QQuickStateGroup::QQuickStateGroup(QObject *parent) |
92 | : QObject(*(new QQuickStateGroupPrivate), parent) |
93 | { |
94 | } |
95 | |
96 | QQuickStateGroup::~QQuickStateGroup() |
97 | { |
98 | Q_D(const QQuickStateGroup); |
99 | for (int i = 0; i < d->states.size(); ++i) { |
100 | if (d->states.at(i)) |
101 | d->states.at(i)->setStateGroup(nullptr); |
102 | } |
103 | if (d->nullState) |
104 | d->nullState->setStateGroup(nullptr); |
105 | } |
106 | |
107 | QList<QQuickState *> QQuickStateGroup::states() const |
108 | { |
109 | Q_D(const QQuickStateGroup); |
110 | return d->states; |
111 | } |
112 | |
113 | /*! |
114 | \qmlproperty list<State> QtQuick::StateGroup::states |
115 | This property holds a list of states defined by the state group. |
116 | |
117 | \qml |
118 | StateGroup { |
119 | states: [ |
120 | State { |
121 | // State definition... |
122 | }, |
123 | State { |
124 | // ... |
125 | } |
126 | // Other states... |
127 | ] |
128 | } |
129 | \endqml |
130 | |
131 | \sa {Qt Quick States}{Qt Quick States} |
132 | */ |
133 | QQmlListProperty<QQuickState> QQuickStateGroup::statesProperty() |
134 | { |
135 | Q_D(QQuickStateGroup); |
136 | return QQmlListProperty<QQuickState>(this, &d->states, |
137 | &QQuickStateGroupPrivate::append_state, |
138 | &QQuickStateGroupPrivate::count_state, |
139 | &QQuickStateGroupPrivate::at_state, |
140 | &QQuickStateGroupPrivate::clear_states, |
141 | &QQuickStateGroupPrivate::replace_states, |
142 | &QQuickStateGroupPrivate::removeLast_states); |
143 | } |
144 | |
145 | void QQuickStateGroupPrivate::append_state(QQmlListProperty<QQuickState> *list, QQuickState *state) |
146 | { |
147 | QQuickStateGroup *_this = static_cast<QQuickStateGroup *>(list->object); |
148 | _this->d_func()->states.append(t: state); |
149 | if (state) |
150 | state->setStateGroup(_this); |
151 | } |
152 | |
153 | qsizetype QQuickStateGroupPrivate::count_state(QQmlListProperty<QQuickState> *list) |
154 | { |
155 | QQuickStateGroup *_this = static_cast<QQuickStateGroup *>(list->object); |
156 | return _this->d_func()->states.size(); |
157 | } |
158 | |
159 | QQuickState *QQuickStateGroupPrivate::at_state(QQmlListProperty<QQuickState> *list, qsizetype index) |
160 | { |
161 | QQuickStateGroup *_this = static_cast<QQuickStateGroup *>(list->object); |
162 | return _this->d_func()->states.at(i: index); |
163 | } |
164 | |
165 | void QQuickStateGroupPrivate::clear_states(QQmlListProperty<QQuickState> *list) |
166 | { |
167 | QQuickStateGroup *_this = static_cast<QQuickStateGroup *>(list->object); |
168 | _this->d_func()->setCurrentStateInternal(state: QString(), true); |
169 | for (qsizetype i = 0; i < _this->d_func()->states.size(); ++i) { |
170 | if (_this->d_func()->states.at(i)) |
171 | _this->d_func()->states.at(i)->setStateGroup(nullptr); |
172 | } |
173 | _this->d_func()->states.clear(); |
174 | } |
175 | |
176 | void QQuickStateGroupPrivate::replace_states(QQmlListProperty<QQuickState> *list, qsizetype index, QQuickState *state) |
177 | { |
178 | auto *self = static_cast<QQuickStateGroup *>(list->object); |
179 | auto *d = self->d_func(); |
180 | auto *oldState = d->states.at(i: index); |
181 | if (oldState != state) { |
182 | if (oldState) |
183 | oldState->setStateGroup(nullptr); |
184 | state->setStateGroup(self); |
185 | d->states.replace(i: index, t: state); |
186 | if (!oldState || d->currentState == oldState->name()) |
187 | d->setCurrentStateInternal(state: state->name(), true); |
188 | } |
189 | } |
190 | |
191 | void QQuickStateGroupPrivate::removeLast_states(QQmlListProperty<QQuickState> *list) |
192 | { |
193 | auto *d = static_cast<QQuickStateGroup *>(list->object)->d_func(); |
194 | if (d->currentState == d->states.last()->name()) |
195 | d->setCurrentStateInternal(state: d->states.size() > 1 ? d->states.first()->name() : QString(), true); |
196 | d->states.last()->setStateGroup(nullptr); |
197 | d->states.removeLast(); |
198 | } |
199 | |
200 | /*! |
201 | \qmlproperty list<Transition> QtQuick::StateGroup::transitions |
202 | This property holds a list of transitions defined by the state group. |
203 | |
204 | \qml |
205 | StateGroup { |
206 | transitions: [ |
207 | Transition { |
208 | // ... |
209 | }, |
210 | Transition { |
211 | // ... |
212 | } |
213 | // ... |
214 | ] |
215 | } |
216 | \endqml |
217 | |
218 | \sa {Animation and Transitions in Qt Quick}{Transitions} |
219 | */ |
220 | QQmlListProperty<QQuickTransition> QQuickStateGroup::transitionsProperty() |
221 | { |
222 | Q_D(QQuickStateGroup); |
223 | return QQmlListProperty<QQuickTransition>(this, &d->transitions, &QQuickStateGroupPrivate::append_transition, |
224 | &QQuickStateGroupPrivate::count_transitions, |
225 | &QQuickStateGroupPrivate::at_transition, |
226 | &QQuickStateGroupPrivate::clear_transitions); |
227 | } |
228 | |
229 | void QQuickStateGroupPrivate::append_transition(QQmlListProperty<QQuickTransition> *list, QQuickTransition *trans) |
230 | { |
231 | QQuickStateGroup *_this = static_cast<QQuickStateGroup *>(list->object); |
232 | if (trans) |
233 | _this->d_func()->transitions.append(t: trans); |
234 | } |
235 | |
236 | qsizetype QQuickStateGroupPrivate::count_transitions(QQmlListProperty<QQuickTransition> *list) |
237 | { |
238 | QQuickStateGroup *_this = static_cast<QQuickStateGroup *>(list->object); |
239 | return _this->d_func()->transitions.size(); |
240 | } |
241 | |
242 | QQuickTransition *QQuickStateGroupPrivate::at_transition(QQmlListProperty<QQuickTransition> *list, qsizetype index) |
243 | { |
244 | QQuickStateGroup *_this = static_cast<QQuickStateGroup *>(list->object); |
245 | return _this->d_func()->transitions.at(i: index); |
246 | } |
247 | |
248 | void QQuickStateGroupPrivate::clear_transitions(QQmlListProperty<QQuickTransition> *list) |
249 | { |
250 | QQuickStateGroup *_this = static_cast<QQuickStateGroup *>(list->object); |
251 | _this->d_func()->transitions.clear(); |
252 | } |
253 | |
254 | /*! |
255 | \qmlproperty string QtQuick::StateGroup::state |
256 | |
257 | This property holds the name of the current state of the state group. |
258 | |
259 | This property is often used in scripts to change between states. For |
260 | example: |
261 | |
262 | \qml |
263 | function toggle() { |
264 | if (button.state == 'On') |
265 | button.state = 'Off'; |
266 | else |
267 | button.state = 'On'; |
268 | } |
269 | \endqml |
270 | |
271 | If the state group is in its base state (i.e. no explicit state has been |
272 | set), \c state will be a blank string. Likewise, you can return a |
273 | state group to its base state by setting its current state to \c ''. |
274 | |
275 | \sa {Qt Quick States}{Qt Quick States} |
276 | */ |
277 | QString QQuickStateGroup::state() const |
278 | { |
279 | Q_D(const QQuickStateGroup); |
280 | return d->currentState; |
281 | } |
282 | |
283 | void QQuickStateGroup::setState(const QString &state) |
284 | { |
285 | Q_D(QQuickStateGroup); |
286 | if (d->currentState == state) |
287 | return; |
288 | |
289 | d->setCurrentStateInternal(state); |
290 | } |
291 | |
292 | void QQuickStateGroup::classBegin() |
293 | { |
294 | Q_D(QQuickStateGroup); |
295 | d->componentComplete = false; |
296 | } |
297 | |
298 | void QQuickStateGroup::componentComplete() |
299 | { |
300 | Q_D(QQuickStateGroup); |
301 | d->componentComplete = true; |
302 | |
303 | QVarLengthArray<QString, 4> names; |
304 | names.reserve(sz: d->states.size()); |
305 | for (int ii = 0; ii < d->states.size(); ++ii) { |
306 | QQuickState *state = d->states.at(i: ii); |
307 | if (!state->isNamed()) |
308 | state->setName(QLatin1String("anonymousState" ) + QString::number(++d->unnamedCount)); |
309 | |
310 | QString stateName = state->name(); |
311 | if (names.contains(t: stateName)) { |
312 | qmlWarning(me: state->parent()) << "Found duplicate state name: " << stateName; |
313 | } else { |
314 | names.append(t: std::move(stateName)); |
315 | } |
316 | } |
317 | |
318 | if (d->updateAutoState()) { |
319 | return; |
320 | } else if (!d->currentState.isEmpty()) { |
321 | QString cs = d->currentState; |
322 | d->currentState.clear(); |
323 | d->setCurrentStateInternal(state: cs, true); |
324 | } |
325 | } |
326 | |
327 | /*! |
328 | Returns true if the state was changed, otherwise false. |
329 | */ |
330 | bool QQuickStateGroup::updateAutoState() |
331 | { |
332 | Q_D(QQuickStateGroup); |
333 | return d->updateAutoState(); |
334 | } |
335 | |
336 | bool QQuickStateGroupPrivate::updateAutoState() |
337 | { |
338 | Q_Q(QQuickStateGroup); |
339 | if (!componentComplete) |
340 | return false; |
341 | |
342 | bool revert = false; |
343 | for (int ii = 0; ii < states.size(); ++ii) { |
344 | QQuickState *state = states.at(i: ii); |
345 | if (state->isWhenKnown()) { |
346 | if (state->isNamed()) { |
347 | bool whenValue = state->when(); |
348 | const QQmlProperty whenProp(state, u"when"_s ); |
349 | const auto potentialWhenBinding = QQmlAnyBinding::ofProperty(prop: whenProp); |
350 | Q_ASSERT(!potentialWhenBinding.isUntypedPropertyBinding()); |
351 | |
352 | // if there is a binding, the value in when might not be up-to-date at this point |
353 | // so we manually re-evaluate the binding |
354 | QQmlAbstractBinding *abstractBinding = potentialWhenBinding.asAbstractBinding(); |
355 | if (abstractBinding && abstractBinding->kind() == QQmlAbstractBinding::QmlBinding) { |
356 | QQmlBinding *binding = static_cast<QQmlBinding *>(abstractBinding); |
357 | if (binding->hasValidContext()) { |
358 | const auto boolType = QMetaType::fromType<bool>(); |
359 | const bool isUndefined = !binding->evaluate(result: &whenValue, type: boolType); |
360 | if (isUndefined) |
361 | whenValue = false; |
362 | } |
363 | } |
364 | |
365 | if (whenValue) { |
366 | qCDebug(lcStates) << "Setting auto state due to expression" ; |
367 | if (currentState != state->name()) { |
368 | q->setState(state->name()); |
369 | return true; |
370 | } else { |
371 | return false; |
372 | } |
373 | } else if (state->name() == currentState) { |
374 | revert = true; |
375 | } |
376 | } |
377 | } |
378 | } |
379 | if (revert) { |
380 | bool rv = !currentState.isEmpty(); |
381 | q->setState(QString()); |
382 | return rv; |
383 | } else { |
384 | return false; |
385 | } |
386 | } |
387 | |
388 | QQuickTransition *QQuickStateGroupPrivate::findTransition(const QString &from, const QString &to) |
389 | { |
390 | QQuickTransition *highest = nullptr; |
391 | int score = 0; |
392 | bool reversed = false; |
393 | bool done = false; |
394 | |
395 | for (int ii = 0; !done && ii < transitions.size(); ++ii) { |
396 | QQuickTransition *t = transitions.at(i: ii); |
397 | if (!t->enabled()) |
398 | continue; |
399 | for (int ii = 0; ii < 2; ++ii) |
400 | { |
401 | if (ii && (!t->reversible() || |
402 | (t->fromState() == QLatin1String("*" ) && |
403 | t->toState() == QLatin1String("*" )))) |
404 | break; |
405 | const QString fromStateStr = t->fromState(); |
406 | const QString toStateStr = t->toState(); |
407 | |
408 | auto fromState = QStringView{fromStateStr}.split(sep: QLatin1Char(',')); |
409 | for (int jj = 0; jj < fromState.size(); ++jj) |
410 | fromState[jj] = fromState.at(i: jj).trimmed(); |
411 | auto toState = QStringView{toStateStr}.split(sep: QLatin1Char(',')); |
412 | for (int jj = 0; jj < toState.size(); ++jj) |
413 | toState[jj] = toState.at(i: jj).trimmed(); |
414 | if (ii == 1) |
415 | qSwap(value1&: fromState, value2&: toState); |
416 | int tScore = 0; |
417 | const QString asterisk = QStringLiteral("*" ); |
418 | if (fromState.contains(t: QStringView(from))) |
419 | tScore += 2; |
420 | else if (fromState.contains(t: QStringView(asterisk))) |
421 | tScore += 1; |
422 | else |
423 | continue; |
424 | |
425 | if (toState.contains(t: QStringView(to))) |
426 | tScore += 2; |
427 | else if (toState.contains(t: QStringView(asterisk))) |
428 | tScore += 1; |
429 | else |
430 | continue; |
431 | |
432 | if (ii == 1) |
433 | reversed = true; |
434 | else |
435 | reversed = false; |
436 | |
437 | if (tScore == 4) { |
438 | highest = t; |
439 | done = true; |
440 | break; |
441 | } else if (tScore > score) { |
442 | score = tScore; |
443 | highest = t; |
444 | } |
445 | } |
446 | } |
447 | |
448 | if (highest) |
449 | highest->setReversed(reversed); |
450 | |
451 | return highest; |
452 | } |
453 | |
454 | void QQuickStateGroupPrivate::setCurrentStateInternal(const QString &state, |
455 | bool ignoreTrans) |
456 | { |
457 | Q_Q(QQuickStateGroup); |
458 | if (!componentComplete) { |
459 | currentState = state; |
460 | return; |
461 | } |
462 | |
463 | if (applyingState) { |
464 | qmlWarning(me: q) << "Can't apply a state change as part of a state definition." ; |
465 | return; |
466 | } |
467 | |
468 | applyingState = true; |
469 | |
470 | QQuickTransition *transition = ignoreTrans ? nullptr : findTransition(from: currentState, to: state); |
471 | if (lcStates().isDebugEnabled()) { |
472 | qCDebug(lcStates) << this << "changing state from:" << currentState << "to:" << state; |
473 | if (transition) |
474 | qCDebug(lcStates) << " using transition" << transition->fromState() |
475 | << transition->toState(); |
476 | } |
477 | |
478 | QQuickState *oldState = nullptr; |
479 | if (!currentState.isEmpty()) { |
480 | for (int ii = 0; ii < states.size(); ++ii) { |
481 | if (states.at(i: ii)->name() == currentState) { |
482 | oldState = states.at(i: ii); |
483 | break; |
484 | } |
485 | } |
486 | } |
487 | |
488 | currentState = state; |
489 | emit q->stateChanged(currentState); |
490 | |
491 | QQuickState *newState = nullptr; |
492 | for (int ii = 0; ii < states.size(); ++ii) { |
493 | if (states.at(i: ii)->name() == currentState) { |
494 | newState = states.at(i: ii); |
495 | break; |
496 | } |
497 | } |
498 | |
499 | if (oldState == nullptr || newState == nullptr) { |
500 | if (!nullState) { |
501 | nullState = new QQuickState; |
502 | QQml_setParent_noEvent(object: nullState, parent: q); |
503 | nullState->setStateGroup(q); |
504 | } |
505 | if (!oldState) oldState = nullState; |
506 | if (!newState) newState = nullState; |
507 | } |
508 | |
509 | newState->apply(transition, revert: oldState); |
510 | applyingState = false; //### consider removing this (don't allow state changes in transition) |
511 | } |
512 | |
513 | QQuickState *QQuickStateGroup::findState(const QString &name) const |
514 | { |
515 | Q_D(const QQuickStateGroup); |
516 | for (int i = 0; i < d->states.size(); ++i) { |
517 | QQuickState *state = d->states.at(i); |
518 | if (state->name() == name) |
519 | return state; |
520 | } |
521 | |
522 | return nullptr; |
523 | } |
524 | |
525 | void QQuickStateGroup::removeState(QQuickState *state) |
526 | { |
527 | Q_D(QQuickStateGroup); |
528 | d->states.removeOne(t: state); |
529 | } |
530 | |
531 | void QQuickStateGroup::stateAboutToComplete() |
532 | { |
533 | Q_D(QQuickStateGroup); |
534 | d->applyingState = false; |
535 | } |
536 | |
537 | QT_END_NAMESPACE |
538 | |
539 | |
540 | #include "moc_qquickstategroup_p.cpp" |
541 | |