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:
Erik Verbruggen 2015-06-18 16:45:32 +02:00 committed by Erik Verbruggen
parent 5329d739ee
commit 5fd9fe02ff
7 changed files with 166 additions and 21 deletions

View File

@ -46,12 +46,13 @@
// //
#include <private/qobject_p.h> #include <private/qobject_p.h>
#include <QtCore/qabstractstate.h>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
class QStateMachine; class QStateMachine;
class QAbstractState; class QState;
class QAbstractStatePrivate : public QObjectPrivate class QAbstractStatePrivate : public QObjectPrivate
{ {
Q_DECLARE_PUBLIC(QAbstractState) Q_DECLARE_PUBLIC(QAbstractState)

View File

@ -37,6 +37,7 @@
#include "qabstracttransition_p.h" #include "qabstracttransition_p.h"
#include "qabstractstate.h" #include "qabstractstate.h"
#include "qhistorystate.h"
#include "qstate.h" #include "qstate.h"
#include "qstatemachine.h" #include "qstatemachine.h"
@ -135,10 +136,12 @@ QAbstractTransitionPrivate::QAbstractTransitionPrivate()
QStateMachine *QAbstractTransitionPrivate::machine() const QStateMachine *QAbstractTransitionPrivate::machine() const
{ {
QState *source = sourceState(); if (QState *source = sourceState())
if (!source)
return 0;
return source->machine(); 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) bool QAbstractTransitionPrivate::callEventTest(QEvent *e)

View File

@ -50,7 +50,7 @@ QT_BEGIN_NAMESPACE
A history state is a pseudo-state that represents the child state that the 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 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 other child states of the parent state. QHistoryState is part of \l{The
State Machine Framework}. State Machine Framework}.
@ -79,10 +79,21 @@ QT_BEGIN_NAMESPACE
s1->addTransition(button, SIGNAL(clicked()), s1h); s1->addTransition(button, SIGNAL(clicked()), s1h);
\endcode \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 By default a history state is shallow, meaning that it won't remember nested
states. This can be configured through the historyType property. states. This can be configured through the historyType property.
*/ */
/*!
\property QHistoryState::defaultTransition
\brief the default transition of this history state
*/
/*! /*!
\property QHistoryState::defaultState \property QHistoryState::defaultState
@ -113,11 +124,19 @@ QT_BEGIN_NAMESPACE
*/ */
QHistoryStatePrivate::QHistoryStatePrivate() QHistoryStatePrivate::QHistoryStatePrivate()
: QAbstractStatePrivate(HistoryState), : QAbstractStatePrivate(HistoryState)
defaultState(0), historyType(QHistoryState::ShallowHistory) , 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. 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 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. state to transition to if the parent state has never been entered before.
@ -150,7 +196,7 @@ QHistoryState::~QHistoryState()
QAbstractState *QHistoryState::defaultState() const QAbstractState *QHistoryState::defaultState() const
{ {
Q_D(const QHistoryState); 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()); "to this history state's group (%p)", state, parentState());
return; return;
} }
if (d->defaultState != state) { if (!d->defaultTransition
d->defaultState = state; || 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()); emit defaultStateChanged(QHistoryState::QPrivateSignal());
} }
} }

View File

@ -41,11 +41,13 @@ QT_BEGIN_NAMESPACE
#ifndef QT_NO_STATEMACHINE #ifndef QT_NO_STATEMACHINE
class QAbstractTransition;
class QHistoryStatePrivate; class QHistoryStatePrivate;
class Q_CORE_EXPORT QHistoryState : public QAbstractState class Q_CORE_EXPORT QHistoryState : public QAbstractState
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QAbstractState* defaultState READ defaultState WRITE setDefaultState NOTIFY defaultStateChanged) 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) Q_PROPERTY(HistoryType historyType READ historyType WRITE setHistoryType NOTIFY historyTypeChanged)
public: public:
enum HistoryType { enum HistoryType {
@ -58,6 +60,9 @@ public:
QHistoryState(HistoryType type, QState *parent = Q_NULLPTR); QHistoryState(HistoryType type, QState *parent = Q_NULLPTR);
~QHistoryState(); ~QHistoryState();
QAbstractTransition *defaultTransition() const;
void setDefaultTransition(QAbstractTransition *transition);
QAbstractState *defaultState() const; QAbstractState *defaultState() const;
void setDefaultState(QAbstractState *state); void setDefaultState(QAbstractState *state);
@ -65,6 +70,7 @@ public:
void setHistoryType(HistoryType type); void setHistoryType(HistoryType type);
Q_SIGNALS: Q_SIGNALS:
void defaultTransitionChanged(QPrivateSignal);
void defaultStateChanged(QPrivateSignal); void defaultStateChanged(QPrivateSignal);
void historyTypeChanged(QPrivateSignal); void historyTypeChanged(QPrivateSignal);

View File

@ -47,11 +47,12 @@
#include "private/qabstractstate_p.h" #include "private/qabstractstate_p.h"
#include <QtCore/qabstracttransition.h>
#include <QtCore/qhistorystate.h>
#include <QtCore/qlist.h> #include <QtCore/qlist.h>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
class QHistoryState;
class QHistoryStatePrivate : public QAbstractStatePrivate class QHistoryStatePrivate : public QAbstractStatePrivate
{ {
Q_DECLARE_PUBLIC(QHistoryState) Q_DECLARE_PUBLIC(QHistoryState)
@ -62,11 +63,28 @@ public:
static QHistoryStatePrivate *get(QHistoryState *q) static QHistoryStatePrivate *get(QHistoryState *q)
{ return q->d_func(); } { return q->d_func(); }
QAbstractState *defaultState; QAbstractTransition *defaultTransition;
QHistoryState::HistoryType historyType; QHistoryState::HistoryType historyType;
QList<QAbstractState*> configuration; 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 QT_END_NAMESPACE
#endif #endif

View File

@ -365,9 +365,9 @@ static QList<QAbstractState *> getEffectiveTargetStates(QAbstractTransition *tra
if (!historyConfiguration.isEmpty()) { if (!historyConfiguration.isEmpty()) {
// There is a saved history, so apply that. // There is a saved history, so apply that.
targets.unite(historyConfiguration.toSet()); targets.unite(historyConfiguration.toSet());
} else if (QAbstractState *defaultState = historyState->defaultState()) { } else if (QAbstractTransition *defaultTransition = historyState->defaultTransition()) {
// Qt does not support initial transitions, but uses the default state of the history state for this. // No saved history, take all default transition targets.
targets.insert(defaultState); targets.unite(defaultTransition->targetStates().toSet());
} else { } else {
// Woops, we found a history state without a default state. That's not valid! // Woops, we found a history state without a default state. That's not valid!
QStateMachinePrivate *m = QStateMachinePrivate::get(historyState->machine()); 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)->callOnEntry(event);
QAbstractStatePrivate::get(s)->emitEntered(); 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. // Emit propertiesAssigned signal if the state has no animated properties.
{ {
@ -1091,8 +1098,8 @@ void QStateMachinePrivate::addDescendantStatesToEnter(QAbstractState *state,
#endif #endif
} else { } else {
QList<QAbstractState*> defaultHistoryContent; QList<QAbstractState*> defaultHistoryContent;
if (QHistoryStatePrivate::get(h)->defaultState) if (QAbstractTransition *t = QHistoryStatePrivate::get(h)->defaultTransition)
defaultHistoryContent.append(QHistoryStatePrivate::get(h)->defaultState); defaultHistoryContent = t->targetStates();
if (defaultHistoryContent.isEmpty()) { if (defaultHistoryContent.isEmpty()) {
setError(QStateMachine::NoDefaultStateInHistoryStateError, h); setError(QStateMachine::NoDefaultStateInHistoryStateError, h);
@ -1118,8 +1125,10 @@ void QStateMachinePrivate::addDescendantStatesToEnter(QAbstractState *state,
if (QAbstractState *initial = toStandardState(state)->initialState()) { if (QAbstractState *initial = toStandardState(state)->initialState()) {
Q_ASSERT(initial->machine() == q_func()); Q_ASSERT(initial->machine() == q_func());
// FIXME:
// Qt does not support initial transitions (which is a problem for parallel states). // 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. // The way it simulates this for other states, is by having a single initial state.
// See also the FIXME in enterStates.
statesForDefaultEntry.insert(initial); statesForDefaultEntry.insert(initial);
addDescendantStatesToEnter(initial, statesToEnter, statesForDefaultEntry); addDescendantStatesToEnter(initial, statesToEnter, statesForDefaultEntry);

View File

@ -250,6 +250,7 @@ private slots:
void internalTransition(); void internalTransition();
void conflictingTransition(); void conflictingTransition();
void qtbug_46059(); void qtbug_46059();
void qtbug_46703();
}; };
class TestState : public QState class TestState : public QState
@ -6485,5 +6486,59 @@ void tst_QStateMachine::qtbug_46059()
QVERIFY(machine.isRunning()); 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) QTEST_MAIN(tst_QStateMachine)
#include "tst_qstatemachine.moc" #include "tst_qstatemachine.moc"