From d0441f605434a89b53735427e4e81182c65debbd Mon Sep 17 00:00:00 2001 From: Noam Rosenthal Date: Mon, 8 Jun 2009 12:27:03 -0700 Subject: scxml for 4.6 --- src/qscxml.cpp | 1428 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1428 insertions(+) create mode 100644 src/qscxml.cpp (limited to 'src/qscxml.cpp') diff --git a/src/qscxml.cpp b/src/qscxml.cpp new file mode 100644 index 0000000..8190056 --- /dev/null +++ b/src/qscxml.cpp @@ -0,0 +1,1428 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain +** additional rights. These rights are described in the Nokia Qt LGPL +** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this +** package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ +/*! + \class QScxml + \reentrant + + \brief The QScxml class provides a way to use scripting with the Qt State Machine Framework. + + \ingroup sctools + + Though can be used alone, QScxml is mainly a runtime helper to using the + state-machine framework with SCXML files. + + + \sa QStateMachine +*/ + +#include "qscxml.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef QT_GUI_LIB +#include "qscxmlgui.h" +#endif + + +class QScxmlPrivate +{ + public: + + enum { MaxSnapshots = 200}; + + struct AnchorSnapshot + { + QAbstractState* state; + QString location; + QScriptValue snapshot; + QString anchorType; + }; + + + QScriptEngine* scriptEng; + QList invokerFactories; + QUrl burl; + QString sessionID; + QString startScript; + + + QStack snapshotStack; + QMultiHash anchorTransitions; + QHash curSnapshot; + + + static QHash sessions; +}; +QHash QScxmlPrivate::sessions; + +class QScxmlTimer : public QObject +{ + Q_OBJECT + public: + QScxmlTimer(QScriptEngine* engine, const QScriptValue & scr, int delay) : QObject(engine),script(scr) + { + startTimer(delay); + } + protected: + void timerEvent(QTimerEvent*) + { + if (script.isFunction()) + script.call(); + else if (script.isString()) + script.engine()->evaluate(script.toString()); + } + + private: + QScriptValue script; + +}; + +static QScriptValue _q_deepCopy(const QScriptValue & val) +{ + if (val.isObject() || val.isArray()) { + QScriptValue v = val.isArray() ? val.engine()->newArray() : val.engine()->newObject(); + v.setData(val.data()); + QScriptValueIterator it (val); + while (it.hasNext()) { + it.next(); + v.setProperty(it.name(), _q_deepCopy(it.value())); + } + return v; + } else + return val; +} + + +struct QScxmlFunctions +{ +static QScriptValue cssTime(QScriptContext *context, QScriptEngine *engine) +{ + QString str; + if (context->argumentCount() > 0) + str = context->argument(0).toString(); + if (str == "") { + return qScriptValueFromValue(engine,0); + } + else if (str.endsWith("ms")) { + return qScriptValueFromValue(engine,(str.left(str.length()-2).toInt())); + } + else if (str.endsWith("s")) { + return qScriptValueFromValue(engine,(str.left(str.length()-1).toInt())*1000); + } + else { + return qScriptValueFromValue(engine, (str.toInt())); + } +} +static QScriptValue setTimeout(QScriptContext *context, QScriptEngine *engine) +{ + if (context->argumentCount() < 2) + return QScriptValue(); + int timeout = context->argument(1).toInt32(); + QScxmlTimer* tmr = new QScxmlTimer(engine,context->argument(0),timeout); + return engine->newQObject(tmr); +} +static QScriptValue script_print(QScriptContext *context, QScriptEngine *) +{ + if (context->argumentCount() > 0) + qDebug() << context->argument(0).toString(); + return QScriptValue(); +} +static QScriptValue clearTimeout(QScriptContext *context, QScriptEngine *) +{ + if (context->argumentCount() > 0) { + QObject* obj = context->argument(0).toQObject(); + obj->deleteLater(); + } + return QScriptValue(); +} + +static QScriptValue deepCopy(QScriptContext *context, QScriptEngine *) +{ + if (context->argumentCount() == 0) + return QScriptValue(); + else + return _q_deepCopy(context->argument(0)); +} + +static QScriptValue receiveSignal(QScriptContext *context, QScriptEngine *engine) +{ + QString eventName = context->thisObject().property("e").toString(); + if (!eventName.isEmpty()) { + QScxml* scxml = qobject_cast(engine->globalObject().property("scxml.stateMachine").toQObject()); + if (scxml) { + QStringList pnames; + QVariantList pvals; + for (int i=0; i < context->argumentCount(); ++i) { + pnames << QString::number(i); + pvals << context->argument(i).toVariant(); + } + QScxmlEvent* ev = new QScxmlEvent(eventName,pnames,pvals,QScriptValue()); + ev->metaData.kind = QScxmlEvent::MetaData::Platform; + scxml->postEvent(ev); + } + } + return QScriptValue(); +} + +static QScriptValue postEvent(QScriptContext *context, QScriptEngine *engine) +{ + QScxml* scxml = qobject_cast(engine->globalObject().property("scxml").toQObject()); + if (scxml) { + QString eventName,target,type; + QStringList pnames; + QVariantList pvals; + QScriptValue cnt; + if (context->argumentCount() > 0) + eventName = context->argument(0).toString(); + if (context->argumentCount() > 1) + target = context->argument(1).toString(); + if (context->argumentCount() > 2) + type = context->argument(2).toString(); + + if (!eventName.isEmpty() || !target.isEmpty()) { + if (context->argumentCount() > 3) + qScriptValueToSequence(context->argument(3),pnames); + if (context->argumentCount() > 4) { + QScriptValueIterator it (context->argument(4)); + while (it.hasNext()) { + it.next(); + pvals.append(it.value().toVariant()); + } + } if (context->argumentCount() > 5) + cnt = context->argument(5); + QScxmlEvent* ev = new QScxmlEvent(eventName,pnames,pvals,cnt); + if (type == "scxml" || type == "") { + bool ok = true; + if (target == "_internal") { + ev->metaData.kind = QScxmlEvent::MetaData::Internal; + scxml->postInternalEvent(ev); + } else if (target == "scxml" || target == "") { + ev->metaData.kind = QScxmlEvent::MetaData::External; + scxml->postEvent(ev); + } else if (target == "_parent") { + QScxmlInvoker* p = qobject_cast(scxml->parent()); + if (p) + p->postParentEvent(ev); + else + ok = false; + } else { + QScxml* session = QScxmlPrivate::sessions[target]; + if (session) { + session->postEvent(ev); + } else + ok = false; + } + if (!ok) + scxml->postNamedEvent("error.targetunavailable"); + + } else { + scxml->postNamedEvent("error.send.typeinvalid"); + } + } + } + return QScriptValue(); +} + +// scxml.invoke (type, target, paramNames, paramValues, content) +static QScriptValue invoke(QScriptContext *context, QScriptEngine *engine) +{ + QScxml* scxml = qobject_cast(engine->globalObject().property("scxml.stateMachine").toQObject()); + if (scxml) { + QString type,target; + QStringList pnames; + QVariantList pvals; + QScriptValue cnt; + if (context->argumentCount() > 0) + type = context->argument(0).toString(); + if (type.isEmpty()) + type = "scxml"; + if (context->argumentCount() > 1) + target = context->argument(1).toString(); + if (context->argumentCount() > 2) + qScriptValueToSequence(context->argument(2),pnames); + if (context->argumentCount() > 3) { + QScriptValueIterator it (context->argument(3)); + while (it.hasNext()) { + it.next(); + pvals.append(it.value().toVariant()); + } + } if (context->argumentCount() > 4) + cnt = context->argument(4); + + + + QScxmlInvokerFactory* invf = NULL; + for (int i=0; i < scxml->pvt->invokerFactories.count() && invf == NULL; ++i) + if (scxml->pvt->invokerFactories[i]->isTypeSupported(type)) + invf = scxml->pvt->invokerFactories[i]; + if (invf) { + QScxmlEvent* ev = new QScxmlEvent("",pnames,pvals,cnt); + ev->metaData.origin = scxml->baseUrl(); + ev->metaData.target = target; + ev->metaData.targetType = type; + ev->metaData.originType = "scxml"; + ev->metaData.kind = QScxmlEvent::MetaData::External; + QScxmlInvoker* inv = invf->createInvoker(ev,scxml); + if (inv) + inv->activate(); + return engine->newQObject(inv); + } else { + scxml->postNamedEvent("error.invalidtargettype"); + } + + } + return QScriptValue(); +} + + +static QScriptValue isInState(QScriptContext *context, QScriptEngine *engine) +{ + QScxml* scxml = qobject_cast(engine->globalObject().property("scxml.stateMachine").toQObject()); + if (scxml) { + if (context->argumentCount() > 0) { + QString name = context->argument(0).toString(); + if (!name.isEmpty()) { + QSet cfg = scxml->configuration(); + foreach (QAbstractState* st, cfg) { + if (st->objectName() == name) + return qScriptValueFromValue(engine,true); + } + } + } + } + return qScriptValueFromValue(engine,false); + +} + +}; +/*! + \class QScxmlEvent + \brief The QScxmlEvent class stands for a general named event with a list of parameter names and parameter values. + + Encapsulates an event that conforms to the SCXML definition of events. + + \ingroup sctools + +*/ +/*! \enum QScxmlEvent::MetaData::Kind + + This enum specifies the kind (or context) of the event. + \value Platform An event coming from the itself, such as a script error. + \value Internal An event sent with a or . + \value External An event sent from an invoker, directly from C++, or from a element. +*/ + +/*! + Returns the name of the event. + */ + QString QScxmlEvent::eventName() const +{ + return ename; +} + /*! + Return a list containing the parameter names. + */ +QStringList QScxmlEvent::paramNames () const +{ + return pnames; +} + /*! + Return a list containing the parameter values. + */ +QVariantList QScxmlEvent::paramValues () const +{ + return pvals; +} + /*! + Return a QtScript object that can be passed as an additional parameter. + */ +QScriptValue QScxmlEvent::content () const +{ + return cnt; +} + /*! + Returns the parameter value equivalent to parameter \a name. + */ +QVariant QScxmlEvent::param (const QString & name) const +{ + int idx = pnames.indexOf(name); + if (idx >= 0) + return pvals[idx]; + else + return QVariant(); +} +/*! + Creates a QScxmlEvent named \a name, with parameter names \a paramNames, parameter values \a paramValues, and + a QtScript object \a content as an additional parameter. +*/ +QScxmlEvent::QScxmlEvent( + const QString & name, + const QStringList & paramNames, + const QVariantList & paramValues, + const QScriptValue & content) + + : QEvent(QScxmlEvent::eventType()),ename(name),pnames(paramNames),pvals(paramValues),cnt(content) +{ + metaData.kind = MetaData::Internal; +} + +/*! \class QScxmlTransition + \brief The QScxmlTransition class stands for a transition that responds to QScxmlEvent, and can be made conditional with a \l conditionExpression. + Equivalent to the SCXML transition tag. + + \ingroup sctools + */ +/*! \property QScxmlTransition::eventPrefix + The event prefix to be used when testing if the transition needs to be invoked. + Uses SCXML prefix matching. Use * to handle any event. + */ +/*! \property QScxmlTransition::conditionExpression + A QtScript expression that's evaluated to test whether the transition needs to be invoked. + */ + +/*! + Creates a new QScxmlTransition from \a state, that uses \a machine to evaluate the conditions. + */ +QScxmlTransition::QScxmlTransition (QState* state,QScxml* machine) + : QAbstractTransition(state),scxml(machine) +{ +} + +/*! + \internal + */ +bool QScxmlTransition::eventTest(QEvent *e) +{ + QScriptEngine* engine = scxml->scriptEngine(); + QString ev; + + if (e) { + if (e->type() == QScxmlEvent::eventType()) { + ev = ((QScxmlEvent*)e)->eventName(); + } + if (!(eventPrefix() == "*" || eventPrefix() == ev || ev.startsWith(eventPrefix()+"."))) + return false; + } + + + if (!conditionExpression().isEmpty()) { + + QScriptValue v = engine->evaluate(conditionExpression(),scxml->baseUrl().toLocalFile()); + if (engine->hasUncaughtException()) { + + qDebug() << engine->uncaughtException().toString(); + QScxmlEvent* e = new QScxmlEvent("error.illegalcond", + QStringList()<< "error" << "expr" << "line" << "backtrace", + QVariantList() + << QVariant(engine->uncaughtException().toString()) + << QVariant(conditionExpression()) + << QVariant(engine->uncaughtExceptionLineNumber()) + << QVariant(engine->uncaughtExceptionBacktrace())); + e->metaData.kind = QScxmlEvent::MetaData::Platform; + scxml->postEvent(e); + engine->clearExceptions(); + return false; + } + return v.toBoolean(); + } + + return true; +} + +class QScxmlDefaultInvoker : public QScxmlInvoker +{ + Q_OBJECT + + + public: + QScxmlDefaultInvoker(QScxmlEvent* ievent, QScxml* p) : QScxmlInvoker(ievent,p),cancelled(false),childSm(0) + { + childSm = QScxml::load (ievent->metaData.origin.resolved(ievent->metaData.target).toLocalFile(),this); + if (childSm == NULL) { + postParentEvent("error.targetunavailable"); + } else { + connect(childSm,SIGNAL(finished()),this,SLOT(deleteLater())); + + } + } + + + + static void initInvokerFactory(QScxml*) {} + + static bool isTypeSupported(const QString & t) { return t.isEmpty() || t.toLower() == "scxml"; } + + public Q_SLOTS: + void activate () + { + if (childSm) + childSm->start(); + } + + void cancel () + { + cancelled = true; + if (childSm) + childSm->stop(); + + } + + private: + bool cancelled; + QScxml* childSm; +}; +class QScxmlBindingInvoker : public QScxmlInvoker +{ + Q_OBJECT + QScriptValue content; + QScriptValue stored; + + public: + QScxmlBindingInvoker(QScxmlEvent* ievent, QScxml* p) : QScxmlInvoker(ievent,p) + { + } + + static void initInvokerFactory(QScxml*) {} + + static bool isTypeSupported(const QString & t) { return t.toLower() == "q-bindings"; } + + public Q_SLOTS: + void activate () + { + QScriptEngine* engine = ((QScxml*)parent())->scriptEngine(); + QScriptValue content = initEvent->content(); + if (content.isArray()) { + stored = content.engine()->newArray(content.property("length").toInt32()); + + QScriptValueIterator it (content); + for (int i=0; it.hasNext(); ++i) { + it.next(); + if (it.value().isArray()) { + QScriptValue object = it.value().property(0); + QString property = it.value().property(1).toString(); + QScriptValue val = it.value().property(2); + QScriptValue arr = engine->newArray(3); + arr.setProperty("0",it.value().property(0)); + arr.setProperty("1",it.value().property(1)); + if (object.isQObject()) { + QObject* o = object.toQObject(); + arr.setProperty("2",engine->newVariant(o->property(property.toAscii().constData()))); + o->setProperty(property.toAscii().constData(),val.toVariant()); + } else if (object.isObject()) { + arr.setProperty("2",object.property(property)); + object.setProperty(property,val); + } + stored.setProperty(i,arr); + } + } + } + } + + void cancel () + { + if (stored.isArray()) { + QScriptValueIterator it (stored); + while (it.hasNext()) { + it.next(); + if (it.value().isArray()) { + QScriptValue object = it.value().property(0); + QString property = it.value().property(1).toString(); + QScriptValue val = it.value().property(2); + if (object.isQObject()) { + QObject* o = object.toQObject(); + o->setProperty(property.toAscii().constData(),val.toVariant()); + } else if (object.isObject()) { + object.setProperty(property,val); + } + } + } + } + } +}; + +/*! +\fn QScxmlInvoker::~QScxmlInvoker() +*/ + + +/*! +\fn QScxml::eventTriggered(const QString & name) + +This signal is emitted when external event \a name is handled in the state machine. +*/ + +/*! + Creates a new QScxml object, with parent \a parent. + */ + +QScxml::QScxml(QObject* parent) + : QStateMachine(parent) +{ + pvt = new QScxmlPrivate; + pvt->scriptEng = new QScriptEngine(this); + QScriptValue glob = pvt->scriptEng->globalObject(); + QScriptValue utilObj = pvt->scriptEng->newObject(); + glob.setProperty("In",pvt->scriptEng->newFunction(QScxmlFunctions::isInState)); + glob.setProperty("_rcvSig",pvt->scriptEng->newFunction(QScxmlFunctions::receiveSignal)); + glob.setProperty("print",pvt->scriptEng->newFunction(QScxmlFunctions::script_print)); + utilObj.setProperty("postEvent",pvt->scriptEng->newFunction(QScxmlFunctions::postEvent)); + utilObj.setProperty("invoke",pvt->scriptEng->newFunction(QScxmlFunctions::invoke)); + utilObj.setProperty("cssTime",pvt->scriptEng->newFunction(QScxmlFunctions::cssTime)); + utilObj.setProperty("stateMachine",pvt->scriptEng->newQObject(this)); + utilObj.setProperty("clone",pvt->scriptEng->newFunction(QScxmlFunctions::deepCopy)); + utilObj.setProperty("setTimeout",pvt->scriptEng->newFunction(QScxmlFunctions::setTimeout)); + utilObj.setProperty("clearTimeout",pvt->scriptEng->newFunction(QScxmlFunctions::clearTimeout)); + QScriptValue dmObj = pvt->scriptEng->newObject(); + glob.setProperty("_data",pvt->scriptEng->newObject()); + glob.setProperty("_global",pvt->scriptEng->globalObject()); + glob.setProperty("scxml",utilObj); + glob.setProperty("connectSignalToEvent",pvt->scriptEng->evaluate("function(sig,ev) {sig.connect({'e':ev},_rcvSig);}")); + static QScxmlAutoInvokerFactory _s_defaultInvokerFactory; + static QScxmlAutoInvokerFactory _s_bindingInvokerFactory; + registerInvokerFactory(&_s_defaultInvokerFactory); + registerInvokerFactory(&_s_bindingInvokerFactory); + connect(this,SIGNAL(started()),this,SLOT(registerSession())); + connect(this,SIGNAL(stopped()),this,SLOT(unregisterSession())); +#ifdef QT_GUI_LIB + static QScxmlAutoInvokerFactory _s_msgboxInvokerFactory; + static QScxmlAutoInvokerFactory _s_menuInvokerFactory; + registerInvokerFactory(&_s_msgboxInvokerFactory); + registerInvokerFactory(&_s_menuInvokerFactory); +#endif +} + +/*! \internal */ +void QScxml::beginSelectTransitions(QEvent* ev) +{ + QScriptValue eventObj = pvt->scriptEng->newObject(); + if (ev) { + if (ev->type() == QScxmlEvent::eventType()) { + QScxmlEvent* se = (QScxmlEvent*)ev; + eventObj.setProperty("name",qScriptValueFromValue(pvt->scriptEng,se->eventName())); + eventObj.setProperty("target",qScriptValueFromValue(pvt->scriptEng,QVariant::fromValue(se->metaData.target))); + eventObj.setProperty("targettype",qScriptValueFromValue(pvt->scriptEng,se->metaData.targetType)); + eventObj.setProperty("invokeid",qScriptValueFromValue(pvt->scriptEng,se->metaData.invokeID)); + eventObj.setProperty("origin",QScriptValue(qScriptValueFromValue(pvt->scriptEng,QVariant::fromValue(se->metaData.origin)))); + eventObj.setProperty("originType",qScriptValueFromValue(pvt->scriptEng,se->metaData.originType)); + switch (se->metaData.kind) { + case QScxmlEvent::MetaData::Internal: + eventObj.setProperty("kind",qScriptValueFromValue(pvt->scriptEng, "internal")); + break; + case QScxmlEvent::MetaData::External: + eventObj.setProperty("kind",qScriptValueFromValue(pvt->scriptEng, "external")); + break; + case QScxmlEvent::MetaData::Platform: + eventObj.setProperty("kind",qScriptValueFromValue(pvt->scriptEng, "platform")); + default: + break; + + } + + QScriptValue dataObj = pvt->scriptEng->newObject(); + int i=0; + foreach (QString s, se->paramNames()) { + QScriptValue v = qScriptValueFromValue(pvt->scriptEng, se->paramValues()[i]); + dataObj.setProperty(QString::number(i),v); + dataObj.setProperty(s,v); + ++i; + } + eventObj.setProperty("data",dataObj); + emit eventTriggered(se->eventName()); + } + } + scriptEngine()->globalObject().setProperty("_event",eventObj); + + QHash curTargets; + + for (int i = pvt->snapshotStack.size()-1; i >= 0 && curTargets.size() < pvt->anchorTransitions.keys().size(); --i) { + if (!curTargets.contains(pvt->snapshotStack.at(i).anchorType)) { + curTargets[pvt->snapshotStack.at(i).anchorType] = pvt->snapshotStack.at(i).state; + } + } + for (QMultiHash::const_iterator it = pvt->anchorTransitions.constBegin(); it != pvt->anchorTransitions.constEnd(); ++it) { + it.value()->setTargetState(curTargets[it.key()]); + } + +} + +/*! \internal */ +void QScxml::endMicrostep(QEvent*) +{ + scriptEngine()->globalObject().setProperty("_event",QScriptValue()); + for (QHash::iterator + it = pvt->curSnapshot.begin(); + it != pvt->curSnapshot.end(); ++it) { + + pvt->snapshotStack.push(it.value()); + + } + if (pvt->snapshotStack.size() > QScxmlPrivate::MaxSnapshots) { + pvt->snapshotStack.remove(0,pvt->snapshotStack.size()-100); + } + pvt->curSnapshot.clear(); +} + +/*! Returns the script engine attached to the state-machine. */ +QScriptEngine* QScxml::scriptEngine () const +{ + return pvt->scriptEng; +} + +/*! + Registers object \a o to the script engine attached to the state machine. + The object can be accessible from global variable \a name. If \a name is not provided, + the object's name is used. If \a recursive is true, all the object's decendants are registered + as global objects, with their respective object names as variable names. +*/ +void QScxml::registerObject (QObject* o, const QString & name, bool recursive) +{ + QString n(name); + if (n.isEmpty()) + n = o->objectName(); + if (!n.isEmpty()) + pvt->scriptEng->globalObject().setProperty(n,pvt->scriptEng->newQObject(o)); + if (recursive) { + QObjectList ol = o->findChildren(); + foreach (QObject* oo, ol) { + if (!oo->objectName().isEmpty()) + registerObject(oo); + } + } +} + +/*! + Posts a QScxmlEvent named \a event, with no payload. + \sa QScxmlEvent + */ +void QScxml::postNamedEvent(const QString & event) +{ + QScxmlEvent* e = new QScxmlEvent(event); + e->metaData.kind = QScxmlEvent::MetaData::External; + postEvent(e); +} +/*! + Executes script \a s in the attached script engine. + If the script fails, a "error.illegalvalue" event is posted to the state machine. +*/ + +void QScxml::executeScript (const QString & s) +{ + pvt->scriptEng->evaluate (s,baseUrl().toLocalFile()); + if (pvt->scriptEng->hasUncaughtException()) { + QScxmlEvent* e = new QScxmlEvent("error.illegalvalue", + QStringList()<< "error" << "expr" << "line" << "backtrace", + QVariantList() + << QVariant(pvt->scriptEng->uncaughtException().toString()) + << QVariant(s) + << QVariant(pvt->scriptEng->uncaughtExceptionLineNumber()) + << QVariant(pvt->scriptEng->uncaughtExceptionBacktrace())); + e->metaData.kind = QScxmlEvent::MetaData::Platform; + postEvent(e); + pvt->scriptEng->clearExceptions(); + } +} + +/*! + Enabled invoker factory \a f to be called from tags. + */ + +void QScxml::registerInvokerFactory (QScxmlInvokerFactory* f) +{ + pvt->invokerFactories << f; + f->init(this); +} + +/*! \class QScxmlInvoker + \brief The QScxmlInvoker class an invoker, which the state-machine context can activate or cancel + with an tag. + + \ingroup sctools + + An invoker is a object that represents an external component that the state machine + can activate when the encompassing state is entered, or cancel when the encompassing + state is exited from. + */ + +/*! \fn QScxmlInvoker::QScxmlInvoker(QScxmlEvent* ievent, QStateMachine* parent) + When reimplementing the constructor, always use the two parameters (\a ievent and \a parent), + as they're called from QScxmlInvokerFactory. +*/ + +/*! \fn QScxmlInvoker::activate() + This function is called when the encompassing state is entered. + The call to this function from the state-machine context is asynchronous, to make sure + that the state is not exited during the same step in which it's entered. + +*/ + +/*! \fn QScxmlInvoker::cancel() + Reimplement this function to allow for asynchronous cancellation of the invoker. + It's the invoker's responsibility to delete itself after this function has been called. + The default implementation deletes the invoker. +*/ + +/*! \fn QScxml* QScxmlInvoker::parentStateMachine() + Returns the state machine encompassing the invoker. + */ + +/*! + Posts an event \a e to the state machine encompassing the invoker. + */ +void QScxmlInvoker::postParentEvent (QScxmlEvent* e) +{ + e->metaData.origin = initEvent->metaData.target; + e->metaData.target = initEvent->metaData.origin; + e->metaData.originType = initEvent->metaData.targetType; + e->metaData.targetType = initEvent->metaData.originType; + e->metaData.kind = QScxmlEvent::MetaData::External; + e->metaData.invokeID = initEvent->metaData.invokeID; + parentStateMachine()->postEvent(e); +} +/*! \overload + Posts a QScxmlEvent named \a e to the encompassing state machine. + */ +void QScxmlInvoker::postParentEvent(const QString & e) +{ + QScxmlEvent* ev = new QScxmlEvent(e); + ev->metaData.kind = QScxmlEvent::MetaData::External; + postParentEvent(ev); +} +/*! \internal */ +QScxml::~QScxml() +{ + delete pvt; +} + +QString QScxmlInvoker::id () const +{ + return initEvent->metaData.invokeID; +} +void QScxmlInvoker::setID(const QString & id) +{ + initEvent->metaData.invokeID = id; +} + +QScxmlInvoker::~QScxmlInvoker() +{ + if (cancelled) + postParentEvent("CancelResponse"); + else + postParentEvent(QString("done.invoke.%1").arg(initEvent->metaData.invokeID)); +} +/*! + \property QScxml::baseUrl + The url used to resolve scripts and invoke urls. +*/ +QUrl QScxml::baseUrl() const +{ + return pvt->burl; +} + +void QScxml::setBaseUrl(const QUrl & u) +{ + pvt->burl = u; +} + +void QScxml::registerSession() +{ + pvt->sessionID = QUuid::createUuid().toString(); + pvt->sessions[pvt->sessionID] = this; + pvt->scriptEng->globalObject().setProperty("_sessionid",qScriptValueFromValue(scriptEngine(), pvt->sessionID)); + executeScript(pvt->startScript); +} + +void QScxml::unregisterSession() +{ + pvt->scriptEng->globalObject().setProperty("_sessionid",QScriptValue()); + pvt->sessions.remove(pvt->sessionID); +} + +/*! + Returns a statically-generated event type to be used by SCXML events. +*/ +QEvent::Type QScxmlEvent::eventType() +{ + static QEvent::Type _t = (QEvent::Type)QEvent::registerEventType(QEvent::User+200); + return _t; +} +const char SCXML_NAMESPACE [] = "http://www.w3.org/2005/07/scxml"; + + + +struct ScTransitionInfo +{ + + QScxmlTransition* transition; + QStringList targets; + QString anchor; + QString script; + ScTransitionInfo() : transition(NULL) {} +}; + +class QScxmlScriptExec : public QObject +{ + Q_OBJECT + QString script; + QScxml* scxml; + public: + QScxmlScriptExec(const QString & scr, QScxml* scx) : script(scr),scxml(scx) + { + } + public Q_SLOTS: + void exec() + { + scxml->executeScript(script); + } +}; + +struct ScStateInfo +{ + QString initial; +}; + +struct ScHistoryInfo +{ + QHistoryState* hstate; + QString defaultStateID; +}; + +struct ScExecContext +{ + QScxml* sm; + QString script; + enum {None, StateEntry,StateExit,Transition } type; + QScxmlTransition* trans; + QAbstractState* state; + ScExecContext() : sm(NULL),type(None),trans(NULL),state(NULL) + { + } + + void applyScript() + { + if (!script.isEmpty()) { + QScxmlScriptExec* exec = new QScxmlScriptExec(script,sm); + switch(type) { + case StateEntry: + QObject::connect(state,SIGNAL(entered()),exec,SLOT(exec())); + break; + case StateExit: + QObject::connect(state,SIGNAL(exited()),exec,SLOT(exec())); + break; + case Transition: + QObject::connect(trans,SIGNAL(activated()),exec,SLOT(exec())); + break; + default: + delete exec; + break; + } + } + } +}; + +class QScxmlLoader +{ + public: + QScxml* stateMachine; + + QList transitions; + QHash stateInfo; + QList historyInfo; + QHash stateByID; + QSet signalEvents; + QSet statesWithFinal; + void loadState (QState* state, QIODevice* dev, const QString & stateID,const QString & filename); + QScxml* load (QIODevice* device, QObject* obj = NULL, const QString & filename = ""); + + QScriptValue evaluateFile (const QString & fn) + { + QFile f (fn); + f.open(QIODevice::ReadOnly); + return stateMachine->scriptEngine()->evaluate(QString::fromUtf8(f.readAll()),fn); + } +}; + +class QScxmlAnchorSave : public QObject +{ + Q_OBJECT + public: + QScxml* sm; + QScxmlPrivate* pvt; + QScxmlPrivate::AnchorSnapshot anchorSnapshot; + QScxmlAnchorSave(QScxml* p,QScxmlPrivate* pv, + const QString & type, const QString & loc, + QAbstractState* s) : + QObject(p),sm(p),pvt(pv) + { + anchorSnapshot.anchorType = type; + anchorSnapshot.location = loc; + anchorSnapshot.state = s; + } + + public Q_SLOTS: + + void save() + { + if (!anchorSnapshot.location.isEmpty()) { + anchorSnapshot.snapshot = _q_deepCopy(sm->scriptEngine()->evaluate(anchorSnapshot.location)); + } + pvt->curSnapshot[anchorSnapshot.anchorType] = anchorSnapshot; + } +}; + +class QScxmlAnchorRestore : public QObject +{ + Q_OBJECT + public: + QScxml* sm; + QScxmlPrivate* pvt; + QString anchorType; + QScxmlAnchorRestore(QScxml* p,QScxmlPrivate* pv, + const QString & type) : + QObject(p),sm(p),pvt(pv),anchorType(type) + { + + } + + public Q_SLOTS: + void restore () + { + pvt->curSnapshot.clear(); + while (!pvt->snapshotStack.isEmpty()) { + QScxmlPrivate::AnchorSnapshot s = pvt->snapshotStack.pop(); + if (s.anchorType == anchorType) { + if (s.location != "") { + sm->scriptEngine()->globalObject().setProperty("_snapshot",s.snapshot); + sm->scriptEngine()->evaluate(QString ("%1 = _snapshot;").arg(s.location)); + sm->scriptEngine()->globalObject().setProperty("_snapshot",QScriptValue()); + } + break; + } + } + } +}; + +static QString sanitize (const QString & str) +{ + return str; +// return QString("eval(unescape(\"%1\"))"). +// arg(QString::fromAscii(str.trimmed().toUtf8().toPercentEncoding(QByteArray("[]()<>;:#/'`_-., \t@!^&*{}")))); +} +static QString sanitize (const QStringRef & str) +{ + return sanitize(str.toString()); +} + +void QScxmlLoader::loadState ( + QState* stateParam, + QIODevice *dev, + const QString & stateID, + const QString & filename) +{ + QXmlStreamReader r (dev); + QState* curState = NULL; + ScExecContext curExecContext; + curExecContext.sm = stateMachine; + QState* topLevelState = NULL; + QHistoryState* curHistoryState = NULL; + QString initialID = ""; + QString idLocation; + QScxmlTransition* curTransition = NULL; + bool inRoot = true; + while (!r.atEnd()) { + r.readNext(); + if (r.hasError()) { + qDebug() << QString("SCXML read error at line %1, column %2: %3").arg(r.lineNumber()).arg(r. columnNumber()).arg(r.errorString()); + return; + } + if (r.namespaceUri() == SCXML_NAMESPACE || r.namespaceUri() == "") { + if (r.isStartElement()) { + if (r.name().toString().compare("scxml",Qt::CaseInsensitive) == 0) { + if (stateID == "") { + topLevelState = curState = stateParam; + stateInfo[curState].initial = r.attributes().value("initial").toString(); + if (curState == stateMachine->rootState()) { + stateMachine->scriptEngine()->globalObject().setProperty("_name",qScriptValueFromValue(stateMachine->scriptEngine(),r.attributes().value("name").toString())); + } + + } + } else if (r.name().toString().compare("state",Qt::CaseInsensitive) == 0 || r.name().toString().compare("parallel",Qt::CaseInsensitive) == 0) { + inRoot = false; + QString id = r.attributes().value("id").toString(); + QState* newState = NULL; + if (curState) { + newState= new QState(r.name().toString().compare("parallel",Qt::CaseInsensitive) == 0 ? QState::ParallelStates : QState::ExclusiveStates, + curState); + } else if (id == stateID) { + topLevelState = newState = stateParam; + + } + if (newState) { + stateInfo[newState].initial = r.attributes().value("initial").toString(); + newState->setObjectName(id); + if (!id.isEmpty() && stateInfo[curState].initial == id) { + + if (curState == stateMachine->rootState()) + stateMachine->setInitialState(newState); + else + curState->setInitialState(newState); + } + QString src = r.attributes().value("src").toString(); + if (!src.isEmpty()) { + int refidx = src.indexOf('#'); + QString srcfile, refid; + if (refidx > 0) { + srcfile = src.left(refidx); + refid = src.mid(refidx+1); + } else + srcfile = src; + srcfile = QDir::cleanPath( QFileInfo(filename).dir().absoluteFilePath(srcfile)); + QFile newFile (srcfile); + if (newFile.exists()) { + newFile.open(QIODevice::ReadOnly); + loadState(newState,&newFile,refid,srcfile); + } + } + initialID = r.attributes().value("initial").toString(); + stateByID[id] = newState; + curState = newState; + curExecContext.state = newState; + } + + } else if (r.name().toString().compare("initial",Qt::CaseInsensitive) == 0) { + if (curState && stateInfo[curState].initial == "") { + QState* newState = new QState(curState); + curState->setInitialState(newState); + } + } else if (r.name().toString().compare("history",Qt::CaseInsensitive) == 0) { + if (curState) { + QString id = r.attributes().value("id").toString(); + curHistoryState = new QHistoryState(r.attributes().value("type") == "shallow" ? QHistoryState::ShallowHistory : QHistoryState::DeepHistory,curState); + curHistoryState->setObjectName(id); + stateByID[id] = curHistoryState; + } + } else if (r.name().toString().compare("final",Qt::CaseInsensitive) == 0) { + if (curState) { + QString id = r.attributes().value("id").toString(); + QFinalState* f = new QFinalState(curState); + f->setObjectName(id); + curExecContext.state = f; + statesWithFinal.insert(curState); + QState* gp = qobject_cast(curState->parentState()); + if (gp->childMode() == QState::ParallelStates) { + statesWithFinal.insert(gp); + } + stateByID[id] = f; + } + } else if (r.name().toString().compare("script",Qt::CaseInsensitive) == 0) { + QString txt = r.readElementText().trimmed(); + if (curExecContext.type == ScExecContext::None && curState == topLevelState) { + stateMachine->executeScript(txt); + } else + curExecContext.script += txt; + } else if (r.name().toString().compare("log",Qt::CaseInsensitive) == 0) { + curExecContext.script += + QString("print('[' + %1 + '][' + %2 + ']' + %3);") + .arg(sanitize(r.attributes().value("label"))) + .arg(sanitize(r.attributes().value("level"))) + .arg(sanitize(r.attributes().value("expr"))); + + } else if (r.name().toString().compare("assign",Qt::CaseInsensitive) == 0) { + QString locattr = r.attributes().value("location").toString(); + if (locattr.isEmpty()) { + locattr = r.attributes().value("dataid").toString(); + if (!locattr.isEmpty()) + locattr = "_data." + locattr; + } + if (!locattr.isEmpty()) { + curExecContext.script += QString ("%1 = %2;").arg(locattr).arg(sanitize(r.attributes().value("expr"))); + } + } else if (r.name().toString().compare("if",Qt::CaseInsensitive) == 0) { + curExecContext.script += QString("if (%1) {").arg(sanitize(r.attributes().value("cond"))); + } else if (r.name().toString().compare("elseif",Qt::CaseInsensitive) == 0) { + curExecContext.script += QString("} elseif (%1) {").arg(sanitize(r.attributes().value("cond"))); + } else if (r.name().toString().compare("else",Qt::CaseInsensitive) == 0) { + curExecContext.script += " } else { "; + } else if (r.name().toString().compare("cancel",Qt::CaseInsensitive) == 0) { + curExecContext.script += QString("scxml.clearTimeout (%1);").arg(sanitize(r.attributes().value("id"))); + } else if (r.name().toString().compare("onentry",Qt::CaseInsensitive) == 0) { + curExecContext.type = ScExecContext::StateEntry; + curExecContext.script = ""; + } else if (r.name().toString().compare("onexit",Qt::CaseInsensitive) == 0) { + curExecContext.type = ScExecContext::StateExit; + curExecContext.script = ""; + } else if (r.name().toString().compare("raise",Qt::CaseInsensitive) == 0 || r.name().toString().compare("event",Qt::CaseInsensitive) == 0 ) { + QString ev = r.attributes().value("event").toString(); + if (ev.isEmpty()) + ev = r.attributes().value("name").toString(); + curExecContext.script += + QString("{" + "var paramNames = []; var paramValues = []; " + "var content = ''; var eventName='%1'; " + "var target = '_internal'; var targetType = 'scxml'; ").arg(ev); + + } else if (r.name().toString().compare("send",Qt::CaseInsensitive) == 0) { + QString type = r.attributes().value("type").toString(); + if (type.isEmpty()) + type = r.attributes().value("targettype").toString(); + curExecContext.script += + QString("{" + "var paramNames = [%1]; var paramValues = []; " + "var content = ''; var eventName=%2; " + "var targetType = %3; var target = %4;") + .arg(r.attributes().value("namelist").toString().replace(" ",",")) + .arg(sanitize(r.attributes().value("event").toString())) + .arg(type.isEmpty() ? "'scxml'" : sanitize(r.attributes().value("type"))) + .arg(r.attributes().value("target").length() ? sanitize(r.attributes().value("target")) : "''"); + idLocation = r.attributes().value("idlocation").toString(); + if (idLocation.isEmpty()) + idLocation = r.attributes().value("sendid").toString(); + + curExecContext.script += QString("var delay = %1; ").arg(r.attributes().value("delay").length() + ? QString("scxml.cssTime(%1)").arg(sanitize(r.attributes().value("delay"))) + : "0"); + } else if (r.name().toString().compare("invoke",Qt::CaseInsensitive) == 0) { + idLocation = r.attributes().value("idlocation").toString(); + if (idLocation.isEmpty()) + idLocation = r.attributes().value("invokeid").toString(); + QObject::connect (curState, SIGNAL(exited()),new QScxmlScriptExec(QString("invoke_%1.cancel();").arg(curState->objectName()),stateMachine),SLOT(exec())); + + QString type = r.attributes().value("type").toString(); + if (type.isEmpty()) + type = r.attributes().value("targettype").toString(); + curExecContext.type = ScExecContext::StateEntry; + curExecContext.state = curState; + curExecContext.script = + QString("{" + "var paramNames = []; var paramValues = []; " + "var content = ''; " + "var srcType = \"%1\"; var src = %2;") + .arg(type.length() ? type : "scxml") + .arg(r.attributes().value("src").length() ? sanitize(r.attributes().value("target")) : "\"\""); + + + } else if (r.name().toString().compare("transition",Qt::CaseInsensitive) == 0) { + if (curHistoryState) { + ScHistoryInfo inf; + inf.hstate = curHistoryState; + inf.defaultStateID = r.attributes().value("target").toString(); + historyInfo.append(inf); + } else { + ScTransitionInfo inf; + inf.targets = r.attributes().value("target").toString().split(' '); + curExecContext.type = ScExecContext::Transition; + curExecContext.script = ""; + curTransition = new QScxmlTransition(curState,stateMachine); + curTransition->setConditionExpression(r.attributes().value("cond").toString()); + curTransition->setEventPrefix(r.attributes().value("event").toString()); + curExecContext.trans = curTransition; + QString anc = r.attributes().value("anchor").toString(); + if (!anc.isEmpty()) { + stateMachine->pvt->anchorTransitions.insert(anc,curTransition); + QObject::connect (curTransition, SIGNAL(activated()),new QScxmlAnchorRestore(stateMachine,stateMachine->pvt,anc),SLOT(restore())); + } + inf.transition = curTransition; + transitions.append(inf); + if (curTransition->eventPrefix().startsWith("q-signal:")) { + signalEvents.insert(curTransition->eventPrefix()); + } + curTransition->setObjectName(QString ("%1 to %2 on %3 if %4 (anchor=%5)").arg(curState->objectName()).arg(inf.targets.join(" ")).arg(curTransition->eventPrefix()).arg(curTransition->conditionExpression()).arg(anc)); + } + } else if (r.name().toString().compare("anchor",Qt::CaseInsensitive) == 0) { + QObject::connect(curState,SIGNAL(exited()),new QScxmlAnchorSave(stateMachine,stateMachine->pvt,r.attributes().value("type").toString(),r.attributes().value("snapshot").toString(),curState),SLOT(save())); + } else if (r.name().toString().compare("data",Qt::CaseInsensitive) == 0) { + QScriptValue val = qScriptValueFromValue(stateMachine->scriptEngine(),"") ; + QString id = r.attributes().value("id").toString(); + if (r.attributes().value("src").length()) + val = evaluateFile(QFileInfo(filename).dir().absoluteFilePath(r.attributes().value("src").toString())); + else { + if (r.attributes().value("expr").length()) { + val = stateMachine->scriptEngine()->evaluate(r.attributes().value("expr").toString()); + } else { + QString t = r.readElementText(); + if (!t.isEmpty()) + val = stateMachine->scriptEngine()->evaluate(t); + } + } + stateMachine->scriptEngine()->evaluate("_data") + .setProperty(id,val); + } else if (r.name().toString().compare("param",Qt::CaseInsensitive) == 0) { + curExecContext.script += + QString("paramNames[paramNames.length] = \"%1\";") + .arg(r.attributes().value("name").toString()); + curExecContext.script += + QString("paramValues[paramValues.length] = %1;") + .arg(sanitize(r.attributes().value("expr"))); + + } else if (r.name().toString().compare("content",Qt::CaseInsensitive) == 0) { + curExecContext.script += QString("content = %1; ").arg(sanitize(r.readElementText())); + } + } else if (r.isEndElement()) { + if (r.name().toString().compare("state",Qt::CaseInsensitive) == 0 || r.name().toString().compare("parallel",Qt::CaseInsensitive) == 0) { + if (curState == topLevelState) { + return; + } else { + curState = qobject_cast(curState->parent()); + curExecContext.state = curState; + } + } else if (r.name().toString().compare("history",Qt::CaseInsensitive) == 0) { + curHistoryState = NULL; + } else if (r.name().toString().compare("final",Qt::CaseInsensitive) == 0) { + curExecContext.state = (curExecContext.state->parentState()); + } else if (r.name().toString().compare("send",Qt::CaseInsensitive) == 0) { + if (!idLocation.isEmpty()) + curExecContext.script += idLocation + " = "; + curExecContext.script += QString("scxml.setTimeout(function() { " + "scxml.postEvent(" + "eventName,target,targetType,paramNames,paramValues,content" + ");" + "}, delay); }"); + idLocation = ""; + } else if (r.name().toString().compare("raise",Qt::CaseInsensitive) == 0) { + curExecContext.script += "scxml.postEvent(eventName,target,targetType,paramNames,paramValues,content); }"; + } else if ( + r.name().toString().compare("onentry",Qt::CaseInsensitive) == 0 + || r.name().toString().compare("onexit",Qt::CaseInsensitive) == 0 + || r.name().toString().compare("scxml",Qt::CaseInsensitive) == 0) { + curExecContext.state = curState; + curExecContext.type = r.name().toString().compare("onexit",Qt::CaseInsensitive)==0 ? ScExecContext::StateExit : ScExecContext::StateEntry; + curExecContext.applyScript(); + curExecContext.type = ScExecContext::None; + } else if (r.name().toString().compare("transition",Qt::CaseInsensitive) == 0) { + if (!curHistoryState) { + curExecContext.trans = curTransition; + curExecContext.type = ScExecContext::Transition; + curExecContext.applyScript(); + } + + ScTransitionInfo* ti = &(transitions.last()); + if (!curExecContext.script.isEmpty() && ti->anchor != "") + ti->script = curExecContext.script; + curExecContext.type = ScExecContext::None; + } else if (r.name().toString().compare("invoke",Qt::CaseInsensitive) == 0) { + curExecContext.script += QString("invoke_%1 = scxml.invoke(srcType,src,paramNames,paramValues,content); }").arg(curState->objectName()); + if (!idLocation.isEmpty()) { + curExecContext.script += QString("%1 = invoke_%2;").arg(idLocation).arg(curState->objectName()); + } + curExecContext.state = curState; + curExecContext.type = ScExecContext::StateEntry; + curExecContext.applyScript(); + idLocation = ""; + curExecContext.type = ScExecContext::None; + } + } + } + } +} + + +QScxml* QScxmlLoader::load(QIODevice* device, QObject* obj, const QString & filename) +{ + stateMachine = new QScxml(obj); + // traverse through the states + loadState(stateMachine->rootState(),device,"",filename); + + // resolve history default state + foreach (ScHistoryInfo h, historyInfo) { + h.hstate->setDefaultState(stateByID[h.defaultStateID]); + } + foreach (QString s, signalEvents) { + QString sig = s; + sig = sig.mid(sig.indexOf(':')+1); + sig = sig.left(sig.indexOf('(')); + QString scr = QString("%1.connect({e:\"%2\"},_rcvSig);\n").arg(sig).arg(s); + stateMachine->pvt->startScript += scr; + } + + foreach (QState* s, statesWithFinal) { + QObject::connect(s,SIGNAL(finished()),stateMachine,SLOT(handleStateFinished())); + } + + // resolve transitions + + foreach (ScTransitionInfo t, transitions) { + QList states; + if (!t.targets.isEmpty()) { + foreach (QString s, t.targets) { + if (!s.trimmed().isEmpty()) { + QAbstractState* st = stateByID[s]; + if (st) + states.append(st); + } + } + t.transition->setTargetStates(states); + } + } + + return stateMachine; +} + +void QScxml::handleStateFinished() +{ + QState* state = qobject_cast(sender()); + if (state) { + postEvent(new QScxmlEvent("done.state." + state->objectName())); + } +} + +/*! + Loads a state machine from an scxml file located at \a filename, with parent object \a o. + */ +QScxml* QScxml::load (const QString & filename, QObject* o) +{ + QScxmlLoader l; + QFile f (filename); + f.open(QIODevice::ReadOnly); + return l.load(&f,o,filename); +} + +#include "qscxml.moc" -- cgit v1.2.3