QStateMachine: add defaultTransition in QHistoryState
The history state had the limitation that it was hard (or impossible) to use when more than one default state had to be entered. For example, using it in a parallel state was impossible without ending up in an infinite loop. This patch changes the QHistoryState to only have an initial transition, and the state selection algorithm is changed accordingly. It also brings QStateMachine closer to the SCXML standard. The existing defaultState is implemented on top of the defaultTransition: when used, a new transition, with the default state as its target, is set as the defaultTransition. Task-number: QTBUG-46703 Change-Id: Ifbb44e4f0f26b72e365af4c94753e4483f9850e7 Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@theqtcompany.com>
This commit is contained in:
parent
5329d739ee
commit
5fd9fe02ff
@ -46,12 +46,13 @@
|
||||
//
|
||||
|
||||
#include <private/qobject_p.h>
|
||||
#include <QtCore/qabstractstate.h>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class QStateMachine;
|
||||
|
||||
class QAbstractState;
|
||||
class QState;
|
||||
class QAbstractStatePrivate : public QObjectPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(QAbstractState)
|
||||
|
@ -37,6 +37,7 @@
|
||||
|
||||
#include "qabstracttransition_p.h"
|
||||
#include "qabstractstate.h"
|
||||
#include "qhistorystate.h"
|
||||
#include "qstate.h"
|
||||
#include "qstatemachine.h"
|
||||
|
||||
@ -135,10 +136,12 @@ QAbstractTransitionPrivate::QAbstractTransitionPrivate()
|
||||
|
||||
QStateMachine *QAbstractTransitionPrivate::machine() const
|
||||
{
|
||||
QState *source = sourceState();
|
||||
if (!source)
|
||||
return 0;
|
||||
if (QState *source = sourceState())
|
||||
return source->machine();
|
||||
Q_Q(const QAbstractTransition);
|
||||
if (QHistoryState *parent = qobject_cast<QHistoryState *>(q->parent()))
|
||||
return parent->machine();
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool QAbstractTransitionPrivate::callEventTest(QEvent *e)
|
||||
|
@ -50,7 +50,7 @@ QT_BEGIN_NAMESPACE
|
||||
|
||||
A history state is a pseudo-state that represents the child state that the
|
||||
parent state was in the last time the parent state was exited. A transition
|
||||
with a history state as its target is in fact a transition to one of the
|
||||
with a history state as its target is in fact a transition to one or more
|
||||
other child states of the parent state. QHistoryState is part of \l{The
|
||||
State Machine Framework}.
|
||||
|
||||
@ -79,10 +79,21 @@ QT_BEGIN_NAMESPACE
|
||||
s1->addTransition(button, SIGNAL(clicked()), s1h);
|
||||
\endcode
|
||||
|
||||
If more than one default state has to be entered, or if the transition to the default state(s)
|
||||
has to be acted upon, the defaultTransition should be set instead. Note that the eventTest()
|
||||
method of that transition will never be called: the selection and execution of the transition is
|
||||
done automatically when entering the history state.
|
||||
|
||||
By default a history state is shallow, meaning that it won't remember nested
|
||||
states. This can be configured through the historyType property.
|
||||
*/
|
||||
|
||||
/*!
|
||||
\property QHistoryState::defaultTransition
|
||||
|
||||
\brief the default transition of this history state
|
||||
*/
|
||||
|
||||
/*!
|
||||
\property QHistoryState::defaultState
|
||||
|
||||
@ -113,11 +124,19 @@ QT_BEGIN_NAMESPACE
|
||||
*/
|
||||
|
||||
QHistoryStatePrivate::QHistoryStatePrivate()
|
||||
: QAbstractStatePrivate(HistoryState),
|
||||
defaultState(0), historyType(QHistoryState::ShallowHistory)
|
||||
: QAbstractStatePrivate(HistoryState)
|
||||
, defaultTransition(0)
|
||||
, historyType(QHistoryState::ShallowHistory)
|
||||
{
|
||||
}
|
||||
|
||||
DefaultStateTransition::DefaultStateTransition(QHistoryState *source, QAbstractState *target)
|
||||
: QAbstractTransition()
|
||||
{
|
||||
setParent(source);
|
||||
setTargetState(target);
|
||||
}
|
||||
|
||||
/*!
|
||||
Constructs a new shallow history state with the given \a parent state.
|
||||
*/
|
||||
@ -143,6 +162,33 @@ QHistoryState::~QHistoryState()
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns this history state's default transition. The default transition is
|
||||
taken when the history state has never been entered before. The target states
|
||||
of the default transition therefore make up the default state.
|
||||
*/
|
||||
QAbstractTransition *QHistoryState::defaultTransition() const
|
||||
{
|
||||
Q_D(const QHistoryState);
|
||||
return d->defaultTransition;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets this history state's default transition to be the given \a transition.
|
||||
This will set the source state of the \a transition to the history state.
|
||||
|
||||
Note that the eventTest method of the \a transition will never be called.
|
||||
*/
|
||||
void QHistoryState::setDefaultTransition(QAbstractTransition *transition)
|
||||
{
|
||||
Q_D(QHistoryState);
|
||||
if (d->defaultTransition != transition) {
|
||||
d->defaultTransition = transition;
|
||||
transition->setParent(this);
|
||||
emit defaultTransitionChanged(QHistoryState::QPrivateSignal());
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns this history state's default state. The default state indicates the
|
||||
state to transition to if the parent state has never been entered before.
|
||||
@ -150,7 +196,7 @@ QHistoryState::~QHistoryState()
|
||||
QAbstractState *QHistoryState::defaultState() const
|
||||
{
|
||||
Q_D(const QHistoryState);
|
||||
return d->defaultState;
|
||||
return d->defaultTransition ? d->defaultTransition->targetState() : Q_NULLPTR;
|
||||
}
|
||||
|
||||
/*!
|
||||
@ -168,8 +214,15 @@ void QHistoryState::setDefaultState(QAbstractState *state)
|
||||
"to this history state's group (%p)", state, parentState());
|
||||
return;
|
||||
}
|
||||
if (d->defaultState != state) {
|
||||
d->defaultState = state;
|
||||
if (!d->defaultTransition
|
||||
|| d->defaultTransition->targetStates().size() != 1
|
||||
|| d->defaultTransition->targetStates().first() != state) {
|
||||
if (!d->defaultTransition || !qobject_cast<DefaultStateTransition*>(d->defaultTransition)) {
|
||||
d->defaultTransition = new DefaultStateTransition(this, state);
|
||||
emit defaultTransitionChanged(QHistoryState::QPrivateSignal());
|
||||
} else {
|
||||
d->defaultTransition->setTargetState(state);
|
||||
}
|
||||
emit defaultStateChanged(QHistoryState::QPrivateSignal());
|
||||
}
|
||||
}
|
||||
|
@ -41,11 +41,13 @@ QT_BEGIN_NAMESPACE
|
||||
|
||||
#ifndef QT_NO_STATEMACHINE
|
||||
|
||||
class QAbstractTransition;
|
||||
class QHistoryStatePrivate;
|
||||
class Q_CORE_EXPORT QHistoryState : public QAbstractState
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QAbstractState* defaultState READ defaultState WRITE setDefaultState NOTIFY defaultStateChanged)
|
||||
Q_PROPERTY(QAbstractTransition* defaultTransition READ defaultTransition WRITE setDefaultTransition NOTIFY defaultTransitionChanged)
|
||||
Q_PROPERTY(HistoryType historyType READ historyType WRITE setHistoryType NOTIFY historyTypeChanged)
|
||||
public:
|
||||
enum HistoryType {
|
||||
@ -58,6 +60,9 @@ public:
|
||||
QHistoryState(HistoryType type, QState *parent = Q_NULLPTR);
|
||||
~QHistoryState();
|
||||
|
||||
QAbstractTransition *defaultTransition() const;
|
||||
void setDefaultTransition(QAbstractTransition *transition);
|
||||
|
||||
QAbstractState *defaultState() const;
|
||||
void setDefaultState(QAbstractState *state);
|
||||
|
||||
@ -65,6 +70,7 @@ public:
|
||||
void setHistoryType(HistoryType type);
|
||||
|
||||
Q_SIGNALS:
|
||||
void defaultTransitionChanged(QPrivateSignal);
|
||||
void defaultStateChanged(QPrivateSignal);
|
||||
void historyTypeChanged(QPrivateSignal);
|
||||
|
||||
|
@ -47,11 +47,12 @@
|
||||
|
||||
#include "private/qabstractstate_p.h"
|
||||
|
||||
#include <QtCore/qabstracttransition.h>
|
||||
#include <QtCore/qhistorystate.h>
|
||||
#include <QtCore/qlist.h>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class QHistoryState;
|
||||
class QHistoryStatePrivate : public QAbstractStatePrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(QHistoryState)
|
||||
@ -62,11 +63,28 @@ public:
|
||||
static QHistoryStatePrivate *get(QHistoryState *q)
|
||||
{ return q->d_func(); }
|
||||
|
||||
QAbstractState *defaultState;
|
||||
QAbstractTransition *defaultTransition;
|
||||
QHistoryState::HistoryType historyType;
|
||||
QList<QAbstractState*> configuration;
|
||||
};
|
||||
|
||||
class DefaultStateTransition: public QAbstractTransition
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DefaultStateTransition(QHistoryState *source, QAbstractState *target);
|
||||
|
||||
protected:
|
||||
// It doesn't matter whether this transition matches any event or not. It is always associated
|
||||
// with a QHistoryState, and as soon as the state-machine detects that it enters a history
|
||||
// state, it will handle this transition as a special case. The history state itself is never
|
||||
// entered either: either the stored configuration will be used, or the target(s) of this
|
||||
// transition are used.
|
||||
virtual bool eventTest(QEvent *event) { Q_UNUSED(event); return false; }
|
||||
virtual void onTransition(QEvent *event) { Q_UNUSED(event); }
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif
|
||||
|
@ -365,9 +365,9 @@ static QList<QAbstractState *> getEffectiveTargetStates(QAbstractTransition *tra
|
||||
if (!historyConfiguration.isEmpty()) {
|
||||
// There is a saved history, so apply that.
|
||||
targets.unite(historyConfiguration.toSet());
|
||||
} else if (QAbstractState *defaultState = historyState->defaultState()) {
|
||||
// Qt does not support initial transitions, but uses the default state of the history state for this.
|
||||
targets.insert(defaultState);
|
||||
} else if (QAbstractTransition *defaultTransition = historyState->defaultTransition()) {
|
||||
// No saved history, take all default transition targets.
|
||||
targets.unite(defaultTransition->targetStates().toSet());
|
||||
} else {
|
||||
// Woops, we found a history state without a default state. That's not valid!
|
||||
QStateMachinePrivate *m = QStateMachinePrivate::get(historyState->machine());
|
||||
@ -978,9 +978,16 @@ void QStateMachinePrivate::enterStates(QEvent *event, const QList<QAbstractState
|
||||
|
||||
QAbstractStatePrivate::get(s)->callOnEntry(event);
|
||||
QAbstractStatePrivate::get(s)->emitEntered();
|
||||
if (statesForDefaultEntry.contains(s)) {
|
||||
// ### executeContent(s.initial.transition.children())
|
||||
}
|
||||
|
||||
// FIXME:
|
||||
// See the "initial transitions" comment in addDescendantStatesToEnter first, then implement:
|
||||
// if (statesForDefaultEntry.contains(s)) {
|
||||
// // ### executeContent(s.initial.transition.children())
|
||||
// }
|
||||
Q_UNUSED(statesForDefaultEntry);
|
||||
|
||||
if (QHistoryState *h = toHistoryState(s))
|
||||
QAbstractTransitionPrivate::get(h->defaultTransition())->callOnTransition(event);
|
||||
|
||||
// Emit propertiesAssigned signal if the state has no animated properties.
|
||||
{
|
||||
@ -1091,8 +1098,8 @@ void QStateMachinePrivate::addDescendantStatesToEnter(QAbstractState *state,
|
||||
#endif
|
||||
} else {
|
||||
QList<QAbstractState*> defaultHistoryContent;
|
||||
if (QHistoryStatePrivate::get(h)->defaultState)
|
||||
defaultHistoryContent.append(QHistoryStatePrivate::get(h)->defaultState);
|
||||
if (QAbstractTransition *t = QHistoryStatePrivate::get(h)->defaultTransition)
|
||||
defaultHistoryContent = t->targetStates();
|
||||
|
||||
if (defaultHistoryContent.isEmpty()) {
|
||||
setError(QStateMachine::NoDefaultStateInHistoryStateError, h);
|
||||
@ -1118,8 +1125,10 @@ void QStateMachinePrivate::addDescendantStatesToEnter(QAbstractState *state,
|
||||
if (QAbstractState *initial = toStandardState(state)->initialState()) {
|
||||
Q_ASSERT(initial->machine() == q_func());
|
||||
|
||||
// FIXME:
|
||||
// Qt does not support initial transitions (which is a problem for parallel states).
|
||||
// The way it simulates this for other states, is by having a single initial state.
|
||||
// See also the FIXME in enterStates.
|
||||
statesForDefaultEntry.insert(initial);
|
||||
|
||||
addDescendantStatesToEnter(initial, statesToEnter, statesForDefaultEntry);
|
||||
|
@ -250,6 +250,7 @@ private slots:
|
||||
void internalTransition();
|
||||
void conflictingTransition();
|
||||
void qtbug_46059();
|
||||
void qtbug_46703();
|
||||
};
|
||||
|
||||
class TestState : public QState
|
||||
@ -6485,5 +6486,59 @@ void tst_QStateMachine::qtbug_46059()
|
||||
QVERIFY(machine.isRunning());
|
||||
}
|
||||
|
||||
void tst_QStateMachine::qtbug_46703()
|
||||
{
|
||||
QStateMachine machine;
|
||||
QState root(&machine);
|
||||
QHistoryState h(&root);
|
||||
QState p(QState::ParallelStates, &root);
|
||||
QState a(&p);
|
||||
QState a1(&a);
|
||||
QState a2(&a);
|
||||
QState a3(&a);
|
||||
QState b(&p);
|
||||
QState b1(&b);
|
||||
QState b2(&b);
|
||||
|
||||
machine.setObjectName("machine");
|
||||
root.setObjectName("root");
|
||||
h.setObjectName("h");
|
||||
p.setObjectName("p");
|
||||
a.setObjectName("a");
|
||||
a1.setObjectName("a1");
|
||||
a2.setObjectName("a2");
|
||||
a3.setObjectName("a3");
|
||||
b.setObjectName("b");
|
||||
b1.setObjectName("b1");
|
||||
b2.setObjectName("b2");
|
||||
|
||||
machine.setInitialState(&root);
|
||||
root.setInitialState(&h);
|
||||
a.setInitialState(&a3);
|
||||
b.setInitialState(&b1);
|
||||
struct : public QAbstractTransition {
|
||||
virtual bool eventTest(QEvent *) { return false; }
|
||||
virtual void onTransition(QEvent *) {}
|
||||
} defaultTransition;
|
||||
defaultTransition.setTargetStates(QList<QAbstractState*>() << &a2 << &b2);
|
||||
h.setDefaultTransition(&defaultTransition);
|
||||
|
||||
machine.start();
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
QTRY_COMPARE(machine.configuration().contains(&root), true);
|
||||
QTRY_COMPARE(machine.configuration().contains(&h), false);
|
||||
QTRY_COMPARE(machine.configuration().contains(&p), true);
|
||||
QTRY_COMPARE(machine.configuration().contains(&a), true);
|
||||
QTRY_COMPARE(machine.configuration().contains(&a1), false);
|
||||
QTRY_COMPARE(machine.configuration().contains(&a2), true);
|
||||
QTRY_COMPARE(machine.configuration().contains(&a3), false);
|
||||
QTRY_COMPARE(machine.configuration().contains(&b), true);
|
||||
QTRY_COMPARE(machine.configuration().contains(&b1), false);
|
||||
QTRY_COMPARE(machine.configuration().contains(&b2), true);
|
||||
|
||||
QVERIFY(machine.isRunning());
|
||||
}
|
||||
|
||||
QTEST_MAIN(tst_QStateMachine)
|
||||
#include "tst_qstatemachine.moc"
|
||||
|
Loading…
Reference in New Issue
Block a user