summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/src/images/statemachine-button-history.pngbin91677 -> 8493 bytes
-rw-r--r--doc/src/images/statemachine-button-nested.pngbin73774 -> 7051 bytes
-rw-r--r--doc/src/images/statemachine-button.pngbin32767 -> 4233 bytes
-rw-r--r--doc/src/images/statemachine-customevents.pngbin0 -> 2544 bytes
-rw-r--r--doc/src/images/statemachine-finished.pngbin37907 -> 5518 bytes
-rw-r--r--doc/src/images/statemachine-nonparallel.pngbin68482 -> 5350 bytes
-rw-r--r--doc/src/images/statemachine-parallel.pngbin99587 -> 8631 bytes
-rw-r--r--doc/src/statemachine.qdoc172
-rw-r--r--examples/statemachine/errorstate/mainwindow.cpp76
-rw-r--r--examples/statemachine/errorstateplugins/random_ai/random_ai_plugin.h2
-rw-r--r--examples/statemachine/errorstateplugins/seek_ai/seek_ai.h2
-rw-r--r--examples/statemachine/errorstateplugins/spin_ai/spin_ai.h2
-rw-r--r--examples/statemachine/errorstateplugins/spin_ai_with_error/spin_ai_with_error.h2
-rw-r--r--src/corelib/statemachine/qstate.cpp2
-rw-r--r--tests/auto/qstatemachine/tst_qstatemachine.cpp21
15 files changed, 234 insertions, 45 deletions
diff --git a/doc/src/images/statemachine-button-history.png b/doc/src/images/statemachine-button-history.png
index cd66478..7f51cae 100644
--- a/doc/src/images/statemachine-button-history.png
+++ b/doc/src/images/statemachine-button-history.png
Binary files differ
diff --git a/doc/src/images/statemachine-button-nested.png b/doc/src/images/statemachine-button-nested.png
index 60360d1..762ac14 100644
--- a/doc/src/images/statemachine-button-nested.png
+++ b/doc/src/images/statemachine-button-nested.png
Binary files differ
diff --git a/doc/src/images/statemachine-button.png b/doc/src/images/statemachine-button.png
index 75d9e53..10102bd 100644
--- a/doc/src/images/statemachine-button.png
+++ b/doc/src/images/statemachine-button.png
Binary files differ
diff --git a/doc/src/images/statemachine-customevents.png b/doc/src/images/statemachine-customevents.png
new file mode 100644
index 0000000..62a4222
--- /dev/null
+++ b/doc/src/images/statemachine-customevents.png
Binary files differ
diff --git a/doc/src/images/statemachine-finished.png b/doc/src/images/statemachine-finished.png
index 802621e..0ac081d 100644
--- a/doc/src/images/statemachine-finished.png
+++ b/doc/src/images/statemachine-finished.png
Binary files differ
diff --git a/doc/src/images/statemachine-nonparallel.png b/doc/src/images/statemachine-nonparallel.png
index 1fe60d8..f9850a7 100644
--- a/doc/src/images/statemachine-nonparallel.png
+++ b/doc/src/images/statemachine-nonparallel.png
Binary files differ
diff --git a/doc/src/images/statemachine-parallel.png b/doc/src/images/statemachine-parallel.png
index 1868792..a65c297 100644
--- a/doc/src/images/statemachine-parallel.png
+++ b/doc/src/images/statemachine-parallel.png
Binary files differ
diff --git a/doc/src/statemachine.qdoc b/doc/src/statemachine.qdoc
index abc089d..16eae39 100644
--- a/doc/src/statemachine.qdoc
+++ b/doc/src/statemachine.qdoc
@@ -34,10 +34,10 @@
The State Machine framework provides an API and execution model that can be
used to effectively embed the elements and semantics of statecharts in Qt
- applications. The framework integrates tightly with Qt's existing event
- system and meta-object system; for example, transitions between states can
- be triggered by signals, and states can be configured to set properties and
- invoke methods on QObjects.
+ applications. The framework integrates tightly with Qt's meta-object system;
+ for example, transitions between states can be triggered by signals, and
+ states can be configured to set properties and invoke methods on QObjects.
+ Qt's event system is used to drive the state machines.
\section1 A Simple State Machine
@@ -53,34 +53,49 @@
\endomit
The following snippet shows the code needed to create such a state machine.
+ First, we create the state machine and states:
\code
QStateMachine machine;
QState *s1 = new QState();
QState *s2 = new QState();
QState *s3 = new QState();
+ \endcode
+
+ Then, we create the transitions by using the QState::addTransition()
+ function:
+ \code
s1->addTransition(button, SIGNAL(clicked()), s2);
s2->addTransition(button, SIGNAL(clicked()), s3);
s3->addTransition(button, SIGNAL(clicked()), s1);
+ \endcode
+ Next, we add the states to the machine and set the machine's initial state:
+
+ \code
machine.addState(s1);
machine.addState(s2);
machine.addState(s3);
machine.setInitialState(s1);
+ \endcode
+ Finally, we start the state machine:
+
+ \code
machine.start();
\endcode
- Once the state machine has been set up, you need to start it by calling
- QStateMachine::start(). The state machine executes asynchronously, i.e. it
- becomes part of your application's event loop.
+ The state machine executes asynchronously, i.e. it becomes part of your
+ application's event loop.
+
+ \section1 Doing Useful Work on State Entry and Exit
- The above state machine is perfectly fine, but it doesn't \e do anything; it
- merely transitions from one state to another. The QState::assignProperty()
- function can be used to have a state set a property of a QObject when the
- state is entered. In the following snippet, the value that should be
- assigned to a QLabel's text property is specified for each state:
+ The above state machine merely transitions from one state to another, it
+ doesn't perform any operations. The QState::assignProperty() function can be
+ used to have a state set a property of a QObject when the state is
+ entered. In the following snippet, the value that should be assigned to a
+ QLabel's text property is specified for each state:
\code
s1->assignProperty(label, "text", "In state s1");
@@ -91,20 +106,26 @@
When any of the states is entered, the label's text will be changed
accordingly.
- The QActionState::entered() signal is emitted when the state is entered. In the
+ The QState::entered() signal is emitted when the state is entered, and the
+ QState::exited() signal is emitted when the state is exited. In the
following snippet, the button's showMaximized() slot will be called when
- state \c s3 is entered:
+ state \c s3 is entered, and the button's showMinimized() slot will be called
+ when \c s3 is exited:
\code
QObject::connect(s3, SIGNAL(entered()), button, SLOT(showMaximized()));
+ QObject::connect(s3, SIGNAL(exited()), button, SLOT(showMinimized()));
\endcode
- \section1 Sharing Transitions By Grouping States
+ \section1 State Machines That Finish
The state machine defined in the previous section never finishes. In order
for a state machine to be able to finish, it needs to have a top-level \e
- final state. When the state machine enters a top-level final state, the
- machine will emit the finished() signal and halt.
+ final state (QFinalState object). When the state machine enters a top-level
+ final state, the machine will emit the QStateMachine::finished() signal and
+ halt.
+
+ \section1 Sharing Transitions By Grouping States
Assume we wanted the user to be able to quit the application at any time by
clicking a Quit button. In order to achieve this, we need to create a final
@@ -166,6 +187,9 @@
s12>addTransition(quitButton, SIGNAL(clicked()), s12);
\endcode
+ A transition can have any state as its target, i.e. the target state does
+ not have to be on the same level in the state hierarchy as the source state.
+
\section1 Using History States to Save and Restore the Current State
Imagine that we wanted to add an "interrupt" mechanism to the example
@@ -252,10 +276,16 @@
QState *s12 = new QState(s1);
\endcode
+ When a parallel state group is entered, all its child states will be
+ simultaneously entered. Transitions within the individual child states
+ operate normally. However, any of the child states may take a transition
+ outside the parent state. When this happens, the parent state and all of its
+ child states are exited.
+
\section1 Detecting that a Composite State has Finished
- A child state can be final; when a final child state is entered, the parent
- state emits the QState::finished() signal.
+ A child state can be final (a QFinalState object); when a final child state
+ is entered, the parent state emits the QState::finished() signal.
\img statemachine-finished.png
\omit
@@ -264,7 +294,105 @@
This is useful when you want to hide the internal details of a state;
i.e. the only thing the outside world should be able to do is enter the
- state, and get a notification when the state has finished (i.e. when a final
- child state has been entered).
+ state, and get a notification when the state has completed its work.
+
+ For parallel state groups, the QState::finished() signal is emitted when \e
+ all the child states have entered final states.
+
+ \section1 Events, Transitions and Guards
+
+ A QStateMachine runs its own event loop. For signal transitions
+ (QSignalTransition objects), QStateMachine automatically posts a
+ QSignalEvent to itself when it intercepts the corresponding signal;
+ similarly, for QObject event transitions (QEventTransition objects) a
+ QWrappedEvent is posted.
+
+ You can post your own events to the state machine using
+ QStateMachine::postEvent().
+
+ When posting a custom event to the state machine, you typically also have
+ one or more custom transitions that can be triggered from events of that
+ type. To create such a transition, you subclass QAbstractTransition and
+ reimplement QAbstractTransition::eventTest(), where you check if an event
+ matches your event type (and optionally other criteria, e.g. attributes of
+ the event object).
+
+ Here we define our own custom event type, \c StringEvent, for posting
+ strings to the state machine:
+
+ \code
+ struct StringEvent : public QEvent
+ {
+ StringEvent(const QString &val)
+ : QEvent(QEvent::Type(QEvent::User+1)),
+ value(val) {}
+
+ QString value;
+ };
+ \endcode
+
+ Next, we define a transition that only triggers when the event's string
+ matches a particular string (a \e guarded transition):
- */
+ \code
+ class StringTransition : public QAbstractTransition
+ {
+ public:
+ StringTransition(const QString &value)
+ : m_value(value) {}
+
+ protected:
+ virtual bool eventTest(QEvent *e) const
+ {
+ if (e->type() != QEvent::Type(QEvent::User+1)) // StringEvent
+ return false;
+ StringEvent *se = static_cast<StringEvent*>(e);
+ return (m_value == se->value);
+ }
+
+ virtual void onTransition(QEvent *) {}
+
+ private:
+ QString m_value;
+ };
+ \endcode
+
+ In the eventTest() reimplementation, we first check if the event type is the
+ desired one; if so, we cast the event to a StringEvent and perform the
+ string comparison.
+
+ The following is a statechart that uses the custom event and transition:
+
+ \img statemachine-customevents.png
+ \omit
+ \caption This is a caption
+ \endomit
+
+ Here's what the implementation of the statechart looks like:
+
+ \code
+ QStateMachine machine;
+ QState *s1 = new QState();
+ QState *s2 = new QState();
+ QFinalState *done = new QFinalState();
+
+ StringTransition *t1 = new StringTransition("Hello");
+ t1->setTargetState(s2);
+ s1->addTransition(t1);
+ StringTransition *t2 = new StringTransition("world");
+ t2->setTargetState(done);
+ s2->addTransition(t2);
+
+ machine.addState(s1);
+ machine.addState(s2);
+ machine.addState(done);
+ machine.setInitialState(s1);
+ \endcode
+
+ Once the machine is started, we can post events to it.
+
+ \code
+ machine.postEvent(new StringEvent("Hello"));
+ machine.postEvent(new StringEvent("world"));
+ \endcode
+*/
diff --git a/examples/statemachine/errorstate/mainwindow.cpp b/examples/statemachine/errorstate/mainwindow.cpp
index 39b8663..07719bc 100644
--- a/examples/statemachine/errorstate/mainwindow.cpp
+++ b/examples/statemachine/errorstate/mainwindow.cpp
@@ -12,6 +12,9 @@
#include <QTimer>
#include <QFileDialog>
#include <QPluginLoader>
+#include <QApplication>
+#include <QInputDialog>
+#include <QMessageBox>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), m_scene(0), m_machine(0), m_runningState(0), m_started(false)
@@ -193,25 +196,62 @@ void MainWindow::addTank()
{
Q_ASSERT(!m_spawns.isEmpty());
- QString fileName = QFileDialog::getOpenFileName(this, "Select plugin file", "plugins/", "*.dll");
- QPluginLoader loader(fileName);
+ QDir pluginsDir(qApp->applicationDirPath());
+#if defined(Q_OS_WIN)
+ if (pluginsDir.dirName().toLower() == "debug" || pluginsDir.dirName().toLower() == "release")
+ pluginsDir.cdUp();
+#elif defined(Q_OS_MAC)
+ if (pluginsDir.dirName() == "MacOS") {
+ pluginsDir.cdUp();
+ pluginsDir.cdUp();
+ pluginsDir.cdUp();
+ }
+#endif
+
+ pluginsDir.cd("plugins");
+
+ QStringList itemNames;
+ QList<Plugin *> items;
+ foreach (QString fileName, pluginsDir.entryList(QDir::Files)) {
+ QPluginLoader loader(pluginsDir.absoluteFilePath(fileName));
+ QObject *possiblePlugin = loader.instance();
+ if (Plugin *plugin = qobject_cast<Plugin *>(possiblePlugin)) {
+ QString objectName = possiblePlugin->objectName();
+ if (objectName.isEmpty())
+ objectName = fileName;
+
+ itemNames.append(objectName);
+ items.append(plugin);
+ }
+ }
+
+ if (items.isEmpty()) {
+ QMessageBox::information(this, "No tank types found", "Please build the errorstateplugins directory");
+ return;
+ }
+
+ bool ok;
+ QString selectedName = QInputDialog::getItem(this, "Select a tank type", "Tank types",
+ itemNames, 0, false, &ok);
- Plugin *plugin = qobject_cast<Plugin *>(loader.instance());
- if (plugin != 0) {
- TankItem *tankItem = m_spawns.takeLast();
- m_scene->addItem(tankItem);
- connect(tankItem, SIGNAL(cannonFired()), this, SLOT(addRocket()));
- if (m_spawns.isEmpty())
- emit mapFull();
-
- QState *region = new QState(m_runningState);
- QState *pluginState = plugin->create(region, tankItem);
- region->setInitialState(pluginState);
-
- // If the plugin has an error it is disabled
- QState *errorState = new QState(region);
- errorState->assignProperty(tankItem, "enabled", false);
- pluginState->setErrorState(errorState);
+ if (ok && !selectedName.isEmpty()) {
+ int idx = itemNames.indexOf(selectedName);
+ if (Plugin *plugin = idx >= 0 ? items.at(idx) : 0) {
+ TankItem *tankItem = m_spawns.takeLast();
+ m_scene->addItem(tankItem);
+ connect(tankItem, SIGNAL(cannonFired()), this, SLOT(addRocket()));
+ if (m_spawns.isEmpty())
+ emit mapFull();
+
+ QState *region = new QState(m_runningState);
+ QState *pluginState = plugin->create(region, tankItem);
+ region->setInitialState(pluginState);
+
+ // If the plugin has an error it is disabled
+ QState *errorState = new QState(region);
+ errorState->assignProperty(tankItem, "enabled", false);
+ pluginState->setErrorState(errorState);
+ }
}
}
diff --git a/examples/statemachine/errorstateplugins/random_ai/random_ai_plugin.h b/examples/statemachine/errorstateplugins/random_ai/random_ai_plugin.h
index 3db464b..10e6f48 100644
--- a/examples/statemachine/errorstateplugins/random_ai/random_ai_plugin.h
+++ b/examples/statemachine/errorstateplugins/random_ai/random_ai_plugin.h
@@ -56,6 +56,8 @@ class RandomAiPlugin: public QObject, public Plugin
Q_OBJECT
Q_INTERFACES(Plugin)
public:
+ RandomAiPlugin() { setObjectName("Random"); }
+
virtual QState *create(QState *parentState, QObject *tank);
};
diff --git a/examples/statemachine/errorstateplugins/seek_ai/seek_ai.h b/examples/statemachine/errorstateplugins/seek_ai/seek_ai.h
index 34d203e..a1b5749 100644
--- a/examples/statemachine/errorstateplugins/seek_ai/seek_ai.h
+++ b/examples/statemachine/errorstateplugins/seek_ai/seek_ai.h
@@ -196,6 +196,8 @@ class SeekAi: public QObject, public Plugin
Q_OBJECT
Q_INTERFACES(Plugin)
public:
+ SeekAi() { setObjectName("Seek and destroy"); }
+
virtual QState *create(QState *parentState, QObject *tank);
};
diff --git a/examples/statemachine/errorstateplugins/spin_ai/spin_ai.h b/examples/statemachine/errorstateplugins/spin_ai/spin_ai.h
index 4b4629c..6e220ed 100644
--- a/examples/statemachine/errorstateplugins/spin_ai/spin_ai.h
+++ b/examples/statemachine/errorstateplugins/spin_ai/spin_ai.h
@@ -38,6 +38,8 @@ class SpinAi: public QObject, public Plugin
Q_OBJECT
Q_INTERFACES(Plugin)
public:
+ SpinAi() { setObjectName("Spin and destroy"); }
+
virtual QState *create(QState *parentState, QObject *tank);
};
diff --git a/examples/statemachine/errorstateplugins/spin_ai_with_error/spin_ai_with_error.h b/examples/statemachine/errorstateplugins/spin_ai_with_error/spin_ai_with_error.h
index 9a96a8b..d520455 100644
--- a/examples/statemachine/errorstateplugins/spin_ai_with_error/spin_ai_with_error.h
+++ b/examples/statemachine/errorstateplugins/spin_ai_with_error/spin_ai_with_error.h
@@ -38,6 +38,8 @@ class SpinAiWithError: public QObject, public Plugin
Q_OBJECT
Q_INTERFACES(Plugin)
public:
+ SpinAiWithError() { setObjectName("Spin and destroy with runtime error in state machine"); }
+
virtual QState *create(QState *parentState, QObject *tank);
};
diff --git a/src/corelib/statemachine/qstate.cpp b/src/corelib/statemachine/qstate.cpp
index 5f61865..3a3bfc3 100644
--- a/src/corelib/statemachine/qstate.cpp
+++ b/src/corelib/statemachine/qstate.cpp
@@ -285,7 +285,7 @@ void QState::setErrorState(QAbstractState *state)
"to a different state machine");
return;
}
- if (state->machine() != 0 && state->machine()->rootState() == state) {
+ if (state != 0 && state->machine() != 0 && state->machine()->rootState() == state) {
qWarning("QStateMachine::setErrorState: root state cannot be error state");
return;
}
diff --git a/tests/auto/qstatemachine/tst_qstatemachine.cpp b/tests/auto/qstatemachine/tst_qstatemachine.cpp
index dbc67d1..9058cb6 100644
--- a/tests/auto/qstatemachine/tst_qstatemachine.cpp
+++ b/tests/auto/qstatemachine/tst_qstatemachine.cpp
@@ -1253,6 +1253,8 @@ void tst_QStateMachine::assignPropertyWithAnimation()
{
QStateMachine machine;
QObject obj;
+ obj.setProperty("foo", 321);
+ obj.setProperty("bar", 654);
QState *s1 = new QState(machine.rootState());
s1->assignProperty(&obj, "foo", 123);
QState *s2 = new QState(machine.rootState());
@@ -1276,6 +1278,8 @@ void tst_QStateMachine::assignPropertyWithAnimation()
{
QStateMachine machine;
QObject obj;
+ obj.setProperty("foo", 321);
+ obj.setProperty("bar", 654);
QState *s1 = new QState(machine.rootState());
s1->assignProperty(&obj, "foo", 123);
QState *s2 = new QState(machine.rootState());
@@ -1302,6 +1306,8 @@ void tst_QStateMachine::assignPropertyWithAnimation()
{
QStateMachine machine;
QObject obj;
+ obj.setProperty("foo", 321);
+ obj.setProperty("bar", 654);
QState *s1 = new QState(machine.rootState());
s1->assignProperty(&obj, "foo", 123);
s1->assignProperty(&obj, "bar", 321);
@@ -1329,6 +1335,8 @@ void tst_QStateMachine::assignPropertyWithAnimation()
{
QStateMachine machine;
QObject obj;
+ obj.setProperty("foo", 321);
+ obj.setProperty("bar", 654);
QState *s1 = new QState(machine.rootState());
QCOMPARE(s1->childMode(), QState::ExclusiveStates);
QCOMPARE(s1->initialState(), (QAbstractState*)0);
@@ -2521,6 +2529,7 @@ void tst_QStateMachine::nestedTargetStateForAnimation()
QAbstractTransition *at = s2Child->addTransition(new EventTransition(QEvent::User, s2Child2));
QPropertyAnimation *animation = new QPropertyAnimation(object, "bar", s2);
+ animation->setDuration(2000);
connect(animation, SIGNAL(finished()), &counter, SLOT(slot()));
at->addAnimation(animation);
@@ -2533,10 +2542,11 @@ void tst_QStateMachine::nestedTargetStateForAnimation()
animation = new QPropertyAnimation(object, "bar", s2);
connect(animation, SIGNAL(finished()), &counter, SLOT(slot()));
at->addAnimation(animation);
-
+
QState *s3 = new QState(machine.rootState());
+ s2->addTransition(s2Child, SIGNAL(polished()), s3);
+
QObject::connect(s3, SIGNAL(entered()), QCoreApplication::instance(), SLOT(quit()));
- s2->addTransition(s2, SIGNAL(polished()), s3);
machine.setInitialState(s1);
machine.start();
@@ -2707,9 +2717,12 @@ void tst_QStateMachine::removeDefaultAnimation()
{
QStateMachine machine;
+ QObject propertyHolder;
+ propertyHolder.setProperty("foo", 0);
+
QCOMPARE(machine.defaultAnimations().size(), 0);
- QPropertyAnimation *anim = new QPropertyAnimation(this, "foo");
+ QPropertyAnimation *anim = new QPropertyAnimation(&propertyHolder, "foo");
machine.addDefaultAnimation(anim);
@@ -2722,7 +2735,7 @@ void tst_QStateMachine::removeDefaultAnimation()
machine.addDefaultAnimation(anim);
- QPropertyAnimation *anim2 = new QPropertyAnimation(this, "foo");
+ QPropertyAnimation *anim2 = new QPropertyAnimation(&propertyHolder, "foo");
machine.addDefaultAnimation(anim2);
QCOMPARE(machine.defaultAnimations().size(), 2);