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 <QtCore/qabstractstate.h>
QT_BEGIN_NAMESPACE
class QStateMachine;
class QAbstractState;
class QState;
class QAbstractStatePrivate : public QObjectPrivate
{
Q_DECLARE_PUBLIC(QAbstractState)

View File

@ -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;
return source->machine();
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)

View File

@ -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());
}
}

View File

@ -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);

View File

@ -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

View File

@ -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);

View File

@ -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"