/**************************************************************************** ** ** Copyright (C) 2008-$THISYEAR$ $TROLLTECH$. All rights reserved. ** ** This file is part of the SCXML project on Trolltech Labs. ** ** $TROLLTECH_GPL_LICENSE$ ** ****************************************************************************/ /*! \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 #include #ifdef QT_GUI_LIB #include "qscxmlgui.h" #endif class QtScxmlSnoopInternal : public QObject { Q_OBJECT friend class QtScxmlSnoop; QtScxmlSnoopInternal(QObject* o) :QObject(o) { } ~QtScxmlSnoopInternal() { if (parent()) parent()->deleteLater(); } signals: void signal (const QVariantList &); }; class QtScxmlSnoop: public QObject { public: QtScxmlSnoopInternal* inobj; QtScxmlSnoop(QObject *obj, const char *aSignal):QObject(obj) { #ifdef Q_CC_BOR const int memberOffset = QObject::staticMetaObject.methodCount(); #else static const int memberOffset = QObject::staticMetaObject.methodCount(); #endif Q_ASSERT(obj); Q_ASSERT(aSignal); if (aSignal[0] - '0' != QSIGNAL_CODE) { qWarning("QtScxmlSnoop: Not a valid signal, use the SIGNAL macro"); return; } QByteArray ba = QMetaObject::normalizedSignature(aSignal + 1); const QMetaObject *mo = obj->metaObject(); int sigIndex = mo->indexOfMethod(ba.constData()); if (sigIndex < 0) { qWarning("QtScxmlSnoop: No such signal: '%s'", ba.constData()); return; } if (!QMetaObject::connect(obj, sigIndex, this, memberOffset, Qt::QueuedConnection, 0)) { qWarning("QtScxmlSnoop: QMetaObject::connect returned false. Unable to connect."); return; } sig = ba; QMetaMethod member = mo->method(sigIndex); QList params = member.parameterTypes(); for (int i = 0; i < params.count(); ++i) { int tp = QMetaType::type(params.at(i).constData()); if (tp == QMetaType::Void) qWarning("Don't know how to handle '%s', use qRegisterMetaType to register it.", params.at(i).constData()); args << tp; } inobj = new QtScxmlSnoopInternal (this); } inline bool isValid() const { return !sig.isEmpty(); } inline QByteArray signal() const { return sig; } int qt_metacall(QMetaObject::Call call, int id, void **a) { id = QObject::qt_metacall(call, id, a); if (id < 0) return id; if (call == QMetaObject::InvokeMetaMethod) { if (id == 0) { QVariantList list; for (int i = 0; i < args.count(); ++i) { QMetaType::Type type = static_cast(args.at(i)); QVariant v(type, a[i + 1]); list << v; } emit inobj->signal (list); } --id; } return id; } // the full, normalized signal name QByteArray sig; // holds the QMetaType types for the argument list of the signal QList args; }; QObject* q_snoopConnect ( QObject* sender, const char* signal, QObject* receiver, const char* method ) { QtScxmlSnoop* o = new QtScxmlSnoop(sender,signal); if (o->isValid()) { QObject::connect (o->inobj, SIGNAL(signal(QVariantList)),receiver,method); QObject::connect (receiver, SIGNAL(destroyed()), o, SLOT(deleteLater())); } return o; } class QScxmlPrivate { public: enum { MaxSnapshots = 200}; struct AnchorSnapshot { QAbstractState* state; QString location; QScriptValue snapshot; QString anchorType; }; void initScriptEngine(QScxml* thiz); 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; } class QScxmlSignalReceiver : public QObject { Q_OBJECT QScxml* scxml; QString eventName; public: QScxmlSignalReceiver(QScxml* s, QString ename) : QObject(s),scxml(s),eventName(ename) { } public Q_SLOTS: void receiveSignal(const QVariantList & pvals) { QStringList pnames; for (int i=0; i < pvals.count(); ++i) { pnames << QString::number(i); } QScxmlEvent* ev = new QScxmlEvent(eventName,pnames,pvals,QScriptValue()); ev->metaData.kind = QScxmlEvent::MetaData::Platform; scxml->postEvent(ev); } }; struct QScxmlFunctions { static QScriptValue connectSignalToEvent(QScriptContext* context, QScriptEngine*) { QScxml* scxml = qobject_cast(context->thisObject().toQObject()); if (scxml) { QObject* obj = context->argument(0).toQObject(); QString sig = ('0'+QSIGNAL_CODE)+context->argument(1).toString(); QString ename = context->argument(2).toString(); if (obj) { q_snoopConnect(obj,sig.toAscii().constData(),new QScxmlSignalReceiver(scxml,ename),SLOT(receiveSignal(QVariantList))); } } return QScriptValue(); } 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 postEvent(QScriptContext *context, QScriptEngine *) { 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); } }; void QScxmlPrivate::initScriptEngine(QScxml* thiz) { QScriptValue glob = scriptEng->globalObject(); QScriptValue scxmlObj = scriptEng->newQObject(thiz); glob.setProperty("In",scriptEng->newFunction(QScxmlFunctions::isInState)); // glob.setProperty("_rcvSig",scriptEng->newFunction(QScxmlFunctions::receiveSignal)); scxmlObj.setProperty("print",scriptEng->newFunction(QScxmlFunctions::script_print)); scxmlObj.setProperty("postEvent",scriptEng->newFunction(QScxmlFunctions::postEvent)); scxmlObj.setProperty("invoke",scriptEng->newFunction(QScxmlFunctions::invoke)); scxmlObj.setProperty("cssTime",scriptEng->newFunction(QScxmlFunctions::cssTime)); scxmlObj.setProperty("clone",scriptEng->newFunction(QScxmlFunctions::deepCopy)); scxmlObj.setProperty("setTimeout",scriptEng->newFunction(QScxmlFunctions::setTimeout)); scxmlObj.setProperty("clearTimeout",scriptEng->newFunction(QScxmlFunctions::clearTimeout)); scxmlObj.setProperty("connectSignalToEvent",scriptEng->newFunction(QScxmlFunctions::connectSignalToEvent)); QScriptValue dmObj = scriptEng->newObject(); glob.setProperty("_data",scriptEng->newObject()); glob.setProperty("_global",scriptEng->globalObject()); glob.setProperty("scxml",scxmlObj); } /*! \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. */ /*! \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) { } /*! \reimp */ void QScxmlTransition::onTransition(QEvent*) { } /*! \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. */ void QScxml::init() { 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())); pvt->initScriptEngine(this); #ifdef QT_GUI_LIB static QScxmlAutoInvokerFactory _s_menuInvokerFactory; static QScxmlAutoInvokerFactory _s_msgboxInvokerFactory; registerInvokerFactory(&_s_msgboxInvokerFactory); registerInvokerFactory(&_s_menuInvokerFactory); #endif } /*! Creates a new QScxml object, with parent \a parent. */ QScxml::QScxml(QObject* parent) : QStateMachine(parent) { pvt = new QScxmlPrivate; pvt->scriptEng = new QScriptEngine(this); init(); } /*! Creates a new QScxml object, with parent \a parent. The state machine will operate on script-engine \a eng. */ QScxml::QScxml(QScriptEngine* eng, QObject* parent) : QStateMachine(parent) { pvt = new QScxmlPrivate; pvt->scriptEng = eng; init(); } /*! \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",QScriptValue(se->metaData.target.toString())); eventObj.setProperty("targettype",qScriptValueFromValue(pvt->scriptEng,se->metaData.targetType)); eventObj.setProperty("invokeid",qScriptValueFromValue(pvt->scriptEng,se->metaData.invokeID)); eventObj.setProperty("origin",QScriptValue(se->metaData.origin.toString())); 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()]); } } static QString _q_configToString (QAbstractState* from,int level, const QSet & config) { QString str; if (from) { if (level >= 0) { for (int i=0; i < level; ++i) str += "\t"; QState* p = qobject_cast(from->parent()); char c = '$'; if (qobject_cast(from)) c = '^'; else if (qobject_cast(from)) c = '~'; else if (p) { if (p->childMode() == QState::ParallelStates) c = '{'; } str += QString("%1%2 %3\n").arg(config.contains(from)?">":" ").arg(c).arg(from->objectName()); } QObjectList ch = from->children(); foreach (QObject* o, ch) str += _q_configToString(qobject_cast(o),level+1,config); } return str; } /*! \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(); /* if (e->type() == QScxmlEvent::eventType()) { qDebug() << "\n" + _q_configToString(rootState(),-1,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; qDebug() << pvt->scriptEng->uncaughtException().toString(); 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(triggered()),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, target, targetType, eventName, delay, content; QStringList paramNames, paramVals; 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) { 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) 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("scxml.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 ) { eventName = QString("\"%1\"").arg(r.attributes().value("event").toString()); target = "'_internal'"; targetType = "scxml"; content = "{}"; paramNames.clear(); paramVals.clear(); } else if (r.name().toString().compare("send",Qt::CaseInsensitive) == 0) { paramNames.clear (); paramVals.clear(); content = "{}"; target = r.attributes().value("target").toString(); if (target == "") target = "\"\""; targetType = r.attributes().value("type").toString(); eventName = r.attributes().value("event").toString(); QStringList nameList = r.attributes().value("namelist").toString().split(" "); foreach (QString name,nameList) { if (name != "") { paramNames << name; paramVals << QString("_data.") + name; } } idLocation = r.attributes().value("idlocation").toString(); if (idLocation.isEmpty()) idLocation = r.attributes().value("sendid").toString(); delay = r.attributes().value("delay").toString(); if (delay == "") delay = "0"; else delay = QString("scxml.cssTime(%1)").arg(delay); } 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 = "scxml"; curExecContext.type = ScExecContext::StateEntry; curExecContext.state = curState; paramNames.clear (); paramVals.clear (); content = "{}"; target = r.attributes().value("src").toString(); if (target == "") target = "\"\""; targetType = r.attributes().value("type").toString(); } 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(triggered()),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) { paramNames << r.attributes().value("name").toString(); paramVals << r.attributes().value("expr").toString(); } else if (r.name().toString().compare("content",Qt::CaseInsensitive) == 0) { content = 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("if",Qt::CaseInsensitive) == 0) { curExecContext.script += "}\n"; } else if (r.name().toString().compare("send",Qt::CaseInsensitive) == 0 || r.name().toString().compare("raise",Qt::CaseInsensitive) == 0) { if (!idLocation.isEmpty()) curExecContext.script += idLocation + " = "; QString pnames; bool first = true; foreach (QString n, paramNames) { if (!first) pnames +=","; pnames += QString("\"%1\"").arg(n); first = false; } QString innerScript = QString("scxml.postEvent(%1,%2,\"%3\",[%4],[%5],%6);") .arg(eventName).arg(target).arg(targetType) .arg(pnames).arg(paramVals.join(",")).arg(content); if (target == "'_internal'") curExecContext.script += innerScript; else curExecContext.script += QString("scxml.setTimeout(function() {%1}, %2);") .arg(innerScript).arg(delay); idLocation = ""; } 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) { QString pnames; bool first = true; foreach (QString n, paramNames) { if (!first) pnames +=","; pnames += QString("\"%1\"").arg(n); first = false; } curExecContext.script += QString("_data.invoke_%1 = scxml.invoke(\"%2\",%3,[%4],[%5],%6); _data.invoke_%1.id = \"%1\";").arg(curState->objectName()).arg(targetType).arg(target).arg(pnames).arg(paramVals.join(",")).arg(content); 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) { if (device->bytesAvailable() == 0) { qWarning() << QString("File %1 invalid or not found").arg(filename); return NULL; } stateMachine = new QScxml(obj); // traverse through the states loadState(stateMachine,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('(')); int liop = sig.lastIndexOf('.'); QString obj = sig.left(liop); sig = sig.mid(liop+1); stateMachine->pvt->startScript += QString("scxml.connectSignalToEvent(%1,'%2',\"%3\");").arg(obj).arg(sig).arg(s); // 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