/**************************************************************************** ** ** Copyright (C) 2008-2009 Nokia Corporation and/or its subsidiary(-ies). ** Contact: Qt Software Information (qt-info@nokia.com) ** ** This file is part of the SCXML project on Trolltech Labs. ** ** This file may be used under the terms of the GNU General Public ** License version 2.0 or 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 GNU ** General Public Licensing requirements will be met: ** http://www.fsf.org/licensing/licenses/info/GPLv2.html and ** 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. ** ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. ** ****************************************************************************/ /*! \class QScxml \brief The QScxml class provides a way to use scripting with the Qt State Machine Framework. Though can be used alone, QScxml is mainly a runtime helper to using the state-machine framework with SCXML files. \sa QStateMachine */ #ifndef QT_NO_STATEMACHINE #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) { QTimer::singleShot(delay,this,SLOT(exec())); } protected Q_SLOTS: void exec() { if (script.isFunction()) script.call(); else if (script.isString()) script.engine()->evaluate(script.toString()); deleteLater(); } 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").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(context->thisObject().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(context->thisObject().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(context->thisObject().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. */ /*! \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. */ /*! \fn QScxmlTransition::onTransition(QEvent* e) Reimplements onTransition with even \a e, to activate the transition signal. */ /*! \fn QScxmlTransition::activated() Emitted when the transition is activated, after exiting the old states before entering the new states. */ /*! \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()) { QScxmlEvent* e = new QScxmlEvent("error.illegalcond", QStringList()<< "error" << "expr" << "line" << "backtrace", QVariantList() << QVariant(engine->uncaughtException().toString()) << QVariant(conditionExpression()) << QVariant(engine->uncaughtExceptionLineNumber()) << QVariant(engine->uncaughtExceptionBacktrace())); qDebug() << engine->uncaughtException().toString(); 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 scxmlObj = pvt->scriptEng->newQObject(this); 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)); scxmlObj.setProperty("postEvent",pvt->scriptEng->newFunction(QScxmlFunctions::postEvent)); scxmlObj.setProperty("invoke",pvt->scriptEng->newFunction(QScxmlFunctions::invoke)); scxmlObj.setProperty("cssTime",pvt->scriptEng->newFunction(QScxmlFunctions::cssTime)); scxmlObj.setProperty("clone",pvt->scriptEng->newFunction(QScxmlFunctions::deepCopy)); scxmlObj.setProperty("setTimeout",pvt->scriptEng->newFunction(QScxmlFunctions::setTimeout)); scxmlObj.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",scxmlObj); 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(); // qDebug() << configuration(); } /*! 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) { // qDebug() << "Executing\n-----------------------------\n"<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(); } // qDebug() <<"\n--------------------\n"; } /*! 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. 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; } /*! returns the id for this invoker */ 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) { 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("_data.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("_data.invoke_%1 = scxml.invoke(srcType,src,paramNames,paramValues,content); }").arg(curState->objectName()); if (!idLocation.isEmpty()) { curExecContext.script += QString("%1 = _data.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" #endif