diff --git a/build/depends.py b/build/depends.py index 788157d9f0a2..de4b41e8dd37 100644 --- a/build/depends.py +++ b/build/depends.py @@ -250,6 +250,7 @@ def enabled_modules(build): # Keep alphabetized. 'QtConcurrent', 'QtWidgets', + 'QtQml', ]) if build.platform_is_windows: qt_modules.extend([ @@ -856,7 +857,6 @@ def sources(self, build): "controllers/controller.cpp", "controllers/controllerdebug.cpp", - "controllers/controllerengine.cpp", "controllers/controllerenumerator.cpp", "controllers/controllerlearningeventfilter.cpp", "controllers/controllermanager.cpp", @@ -872,6 +872,8 @@ def sources(self, build): "controllers/delegates/midiopcodedelegate.cpp", "controllers/delegates/midibytedelegate.cpp", "controllers/delegates/midioptionsdelegate.cpp", + "controllers/engine/controllerengine.cpp", + "controllers/engine/controllerenginejsproxy.cpp", "controllers/learningutils.cpp", "controllers/midi/midimessage.cpp", "controllers/midi/midiutils.cpp", diff --git a/src/control/controlobjectscript.cpp b/src/control/controlobjectscript.cpp index 29224173348b..ef65f2ae3691 100644 --- a/src/control/controlobjectscript.cpp +++ b/src/control/controlobjectscript.cpp @@ -55,7 +55,7 @@ void ControlObjectScript::removeScriptConnection(const ScriptConnection& conn) { } } -void ControlObjectScript::disconnectAllConnectionsToFunction(const QScriptValue& function) { +void ControlObjectScript::disconnectAllConnectionsToFunction(const QJSValue& function) { // Make a local copy of m_scriptConnections because items are removed within the loop. QList connections = m_scriptConnections; for (const auto& conn: connections) { diff --git a/src/control/controlobjectscript.h b/src/control/controlobjectscript.h index 15e29aedf68b..23e8169dcf5c 100644 --- a/src/control/controlobjectscript.h +++ b/src/control/controlobjectscript.h @@ -1,7 +1,7 @@ #ifndef CONTROLOBJECTSCRIPT_H #define CONTROLOBJECTSCRIPT_H -#include "controllers/controllerengine.h" +#include "controllers/engine/controllerengine.h" #include "controllers/controllerdebug.h" #include "control/controlproxy.h" @@ -20,7 +20,7 @@ class ControlObjectScript : public ControlProxy { return m_scriptConnections.size(); }; inline ScriptConnection firstConnection() { return m_scriptConnections.first(); }; - void disconnectAllConnectionsToFunction(const QScriptValue& function); + void disconnectAllConnectionsToFunction(const QJSValue& function); // Called from update(); void emitValueChanged() override { diff --git a/src/controllers/controller.cpp b/src/controllers/controller.cpp index e30ff7a5f63a..c4d6c27f85e0 100644 --- a/src/controllers/controller.cpp +++ b/src/controllers/controller.cpp @@ -6,7 +6,7 @@ */ #include -#include +#include #include "controllers/controller.h" #include "controllers/controllerdebug.h" @@ -136,7 +136,7 @@ void Controller::receive(const QByteArray data, mixxx::Duration timestamp) { continue; } function.append(".incomingData"); - QScriptValue incomingData = m_pEngine->wrapFunctionCode(function, 2); + QJSValue incomingData = m_pEngine->wrapFunctionCode(function, 2); if (!m_pEngine->execute(incomingData, data, timestamp)) { qWarning() << "Controller: Invalid script function" << function; } diff --git a/src/controllers/controller.h b/src/controllers/controller.h index 9cc6be386ce2..56f63d55958c 100644 --- a/src/controllers/controller.h +++ b/src/controllers/controller.h @@ -12,7 +12,7 @@ #ifndef CONTROLLER_H #define CONTROLLER_H -#include "controllers/controllerengine.h" +#include "controllers/engine/controllerengine.h" #include "controllers/controllervisitor.h" #include "controllers/controllerpreset.h" #include "controllers/controllerpresetinfo.h" @@ -92,7 +92,7 @@ class Controller : public QObject, ConstControllerPresetVisitor { protected: // The length parameter is here for backwards compatibility for when scripts // were required to specify it. - Q_INVOKABLE void send(QList data, unsigned int length = 0); + void send(QList data, unsigned int length = 0); // To be called in sub-class' open() functions after opening the device but // before starting any input polling/processing. @@ -162,10 +162,30 @@ class Controller : public QObject, ConstControllerPresetVisitor { bool m_bLearning; QTime m_userActivityInhibitTimer; + friend class ControllerJSProxy; // accesses lots of our stuff, but in the same thread friend class ControllerManager; // For testing friend class ControllerPresetValidationTest; }; +// An object of this class gets exposed to the JS engine, so the methods of this class +// constitute the api that is provided to scripts under "controller" object. +// See comments on ControllerEngineJSProxy. +class ControllerJSProxy: public QObject { + public: + ControllerJSProxy(Controller* m_pController) + : m_pController(m_pController) { + } + + // The length parameter is here for backwards compatibility for when scripts + // were required to specify it. + Q_INVOKABLE void send(QList data, unsigned int length = 0) { + m_pController->send(data, length); + } + + private: + Controller* m_pController; +}; + #endif diff --git a/src/controllers/controllerengine.cpp b/src/controllers/engine/controllerengine.cpp similarity index 84% rename from src/controllers/controllerengine.cpp rename to src/controllers/engine/controllerengine.cpp index b9c069a53daa..dc0e1c040c67 100644 --- a/src/controllers/controllerengine.cpp +++ b/src/controllers/engine/controllerengine.cpp @@ -6,8 +6,10 @@ email : spappalardo@mixxx.org ***************************************************************************/ -#include "controllers/controllerengine.h" +#include "controllers/engine/controllerengine.h" +#include "controllers/engine/controllerenginejsproxy.h" +#include "controllers/engine/controllerengineexceptions.h" #include "controllers/controller.h" #include "controllers/controllerdebug.h" #include "control/controlobject.h" @@ -32,7 +34,7 @@ const double kAlphaBetaDt = kScratchTimerMs / 1000.0; ControllerEngine::ControllerEngine(Controller* controller) : m_pEngine(nullptr), m_pController(controller), - m_bPopups(false), + m_bPopups(true), m_pBaClass(nullptr) { // Handle error dialog buttons qRegisterMetaType("QMessageBox::StandardButton"); @@ -67,7 +69,7 @@ ControllerEngine::~ControllerEngine() { // Delete the script engine, first clearing the pointer so that // other threads will not get the dead pointer after we delete it. if (m_pEngine != nullptr) { - QScriptEngine *engine = m_pEngine; + QJSEngine *engine = m_pEngine; m_pEngine = nullptr; engine->deleteLater(); } @@ -79,41 +81,41 @@ Input: - Output: - -------- ------------------------------------------------------ */ void ControllerEngine::callFunctionOnObjects(QList scriptFunctionPrefixes, - const QString& function, QScriptValueList args) { - const QScriptValue global = m_pEngine->globalObject(); + const QString& function, QJSValueList args) { + const QJSValue global = m_pEngine->globalObject(); for (const QString& prefixName : scriptFunctionPrefixes) { - QScriptValue prefix = global.property(prefixName); - if (!prefix.isValid() || !prefix.isObject()) { + QJSValue prefix = global.property(prefixName); + if (!prefix.isObject()) { qWarning() << "ControllerEngine: No" << prefixName << "object in script"; continue; } - QScriptValue init = prefix.property(function); - if (!init.isValid() || !init.isFunction()) { + QJSValue init = prefix.property(function); + if (!init.isCallable()) { qWarning() << "ControllerEngine:" << prefixName << "has no" << function << " method"; continue; } controllerDebug("ControllerEngine: Executing" << prefixName << "." << function); - init.call(prefix, args); + init.callWithInstance(prefix, args); } } /* ------------------------------------------------------------------ -Purpose: Turn a snippet of JS into a QScriptValue function. +Purpose: Turn a snippet of JS into a QJSValue function. Wrapping it in an anonymous function allows any JS that evaluates to a function to be used in MIDI mapping XML files and ensures the function is executed with the correct 'this' object. Input: QString snippet of JS that evaluates to a function, int number of arguments that the function takes -Output: QScriptValue of JS snippet wrapped in an anonymous function +Output: QJSValue of JS snippet wrapped in an anonymous function ------------------------------------------------------------------- */ -QScriptValue ControllerEngine::wrapFunctionCode(const QString& codeSnippet, +QJSValue ControllerEngine::wrapFunctionCode(const QString& codeSnippet, int numberOfArgs) { - QScriptValue wrappedFunction; + QJSValue wrappedFunction; - QHash::const_iterator i = + QHash::const_iterator i = m_scriptWrappedFunctionCache.find(codeSnippet); if (i != m_scriptWrappedFunctionCache.end()) { @@ -126,23 +128,13 @@ QScriptValue ControllerEngine::wrapFunctionCode(const QString& codeSnippet, QString wrapperArgs = wrapperArgList.join(","); QString wrappedCode = "(function (" + wrapperArgs + ") { (" + codeSnippet + ")(" + wrapperArgs + "); })"; - wrappedFunction = m_pEngine->evaluate(wrappedCode); - checkException(); + + wrappedFunction = evaluateProgram(wrappedCode); m_scriptWrappedFunctionCache[codeSnippet] = wrappedFunction; } return wrappedFunction; } -QScriptValue ControllerEngine::getThisObjectInFunctionCall() { - QScriptContext *ctxt = m_pEngine->currentContext(); - // Our current context is a function call. We want to grab the 'this' - // from the caller's context, so we walk up the stack. - if (ctxt) { - ctxt = ctxt->parentContext(); - } - return ctxt ? ctxt->thisObject() : QScriptValue(); -} - /* -------- ------------------------------------------------------ Purpose: Shuts down scripts in an orderly fashion (stops timers then executes shutdown functions) @@ -198,24 +190,27 @@ bool ControllerEngine::isReady() { void ControllerEngine::initializeScriptEngine() { // Create the Script Engine - m_pEngine = new QScriptEngine(this); + m_pEngine = new QJSEngine(this); // Make this ControllerEngine instance available to scripts as 'engine'. - QScriptValue engineGlobalObject = m_pEngine->globalObject(); - engineGlobalObject.setProperty("engine", m_pEngine->newQObject(this)); + QJSValue engineGlobalObject = m_pEngine->globalObject(); + ControllerEngineJSProxy* proxy = new ControllerEngineJSProxy(this); + engineGlobalObject.setProperty("engine", m_pEngine->newQObject(proxy)); if (m_pController) { qDebug() << "Controller in script engine is:" << m_pController->getName(); + ControllerJSProxy* controllerProxy = new ControllerJSProxy(m_pController); + // Make the Controller instance available to scripts - engineGlobalObject.setProperty("controller", m_pEngine->newQObject(m_pController)); + engineGlobalObject.setProperty("controller", m_pEngine->newQObject(controllerProxy)); // ...under the legacy name as well - engineGlobalObject.setProperty("midi", m_pEngine->newQObject(m_pController)); + engineGlobalObject.setProperty("midi", m_pEngine->newQObject(controllerProxy)); } - m_pBaClass = new ByteArrayClass(m_pEngine); - engineGlobalObject.setProperty("ByteArray", m_pBaClass->constructor()); +// m_pBaClass = new ByteArrayClass(m_pEngine); +// engineGlobalObject.setProperty("ByteArray", m_pBaClass->constructor()); } /* -------- ------------------------------------------------------ @@ -230,7 +225,7 @@ bool ControllerEngine::loadScriptFiles(const QList& scriptPaths, // scriptPaths holds the paths to search in when we're looking for scripts bool result = true; for (const ControllerPreset::ScriptFileInfo& script : scripts) { - if (!evaluate(script.name, scriptPaths)) { + if (!evaluateScriptFile(script.name, scriptPaths)) { result = false; } @@ -261,7 +256,7 @@ void ControllerEngine::scriptHasChanged(const QString& scriptFilename) { // Delete the script engine, first clearing the pointer so that // other threads will not get the dead pointer after we delete it. if (m_pEngine != nullptr) { - QScriptEngine *engine = m_pEngine; + QJSEngine *engine = m_pEngine; m_pEngine = nullptr; engine->deleteLater(); } @@ -289,9 +284,9 @@ void ControllerEngine::initializeScripts(const QListgetName()); - args << QScriptValue(ControllerDebug::enabled()); + QJSValueList args; + args << QJSValue(m_pController->getName()); + args << QJSValue(ControllerDebug::enabled()); // Call the init method for all the prefixes. callFunctionOnObjects(m_scriptFunctionPrefixes, "init", args); @@ -299,79 +294,43 @@ void ControllerEngine::initializeScripts(const QList dummy; - bool ret = evaluate(filepath, dummy); - - return ret; -} - -bool ControllerEngine::syntaxIsValid(const QString& scriptCode) { - if (m_pEngine == nullptr) { - return false; - } - - QScriptSyntaxCheckResult result = m_pEngine->checkSyntax(scriptCode); - QString error = ""; - switch (result.state()) { - case (QScriptSyntaxCheckResult::Valid): break; - case (QScriptSyntaxCheckResult::Intermediate): - error = "Incomplete code"; - break; - case (QScriptSyntaxCheckResult::Error): - error = "Syntax error"; - break; - } - if (error!="") { - error = QString("%1: %2 at line %3, column %4 of script code:\n%5\n") - .arg(error, - result.errorMessage(), - QString::number(result.errorLineNumber()), - QString::number(result.errorColumnNumber()), - scriptCode); - - scriptErrorDialog(error); - return false; - } - return true; -} - /* -------- ------------------------------------------------------ Purpose: Evaluate & run script code Input: 'this' object if applicable, Code string Output: false if an exception -------- ------------------------------------------------------ */ -bool ControllerEngine::internalExecute(QScriptValue thisObject, +bool ControllerEngine::internalExecute(QJSValue thisObject, const QString& scriptCode) { // A special version of safeExecute since we're evaluating strings, not actual functions // (execute() would print an error that it's not a function every time a timer fires.) - if (m_pEngine == nullptr) { - return false; - } + QJSValue scriptFunction; - if (!syntaxIsValid(scriptCode)) { + try { + scriptFunction = evaluateProgram(scriptCode); + } catch (EvaluationException& exception) { + presentErrorDialogForEvaluationException(exception); + qDebug() << "Exception evaluating:" << scriptCode; return false; + } catch (NullEngineException& exception) { + qDebug() << "ControllerEngine::execute: No script engine exists!"; + return false; } - QScriptValue scriptFunction = m_pEngine->evaluate(scriptCode); - - if (checkException()) { - qDebug() << "Exception evaluating:" << scriptCode; + if (!scriptFunction.isCallable()) { + // scriptCode was plain code called in evaluate above return false; } - if (!scriptFunction.isFunction()) { - // scriptCode was plain code called in evaluate above + return internalExecute(thisObject, scriptFunction, QJSValueList()); +} + +bool ControllerEngine::internalExecute(const QString& scriptCode) { + if (m_pEngine == nullptr) { + qDebug() << "ControllerEngine::execute: No script engine exists!"; return false; } - return internalExecute(thisObject, scriptFunction, QScriptValueList()); + return internalExecute(m_pEngine->globalObject(), scriptCode); } /* -------- ------------------------------------------------------ @@ -379,13 +338,8 @@ Purpose: Evaluate & run script code Input: 'this' object if applicable, Code string Output: false if an exception -------- ------------------------------------------------------ */ -bool ControllerEngine::internalExecute(QScriptValue thisObject, QScriptValue functionObject, - QScriptValueList args) { - if (m_pEngine == nullptr) { - qDebug() << "ControllerEngine::execute: No script engine exists!"; - return false; - } - +bool ControllerEngine::internalExecute(QJSValue thisObject, QJSValue functionObject, + QJSValueList args) { if (functionObject.isError()) { qDebug() << "ControllerEngine::internalExecute:" << functionObject.toString(); @@ -393,7 +347,7 @@ bool ControllerEngine::internalExecute(QScriptValue thisObject, QScriptValue fun } // If it's not a function, we're done. - if (!functionObject.isFunction()) { + if (!functionObject.isCallable()) { qDebug() << "ControllerEngine::internalExecute:" << functionObject.toVariant() << "Not a function"; @@ -401,16 +355,28 @@ bool ControllerEngine::internalExecute(QScriptValue thisObject, QScriptValue fun } // If it does happen to be a function, call it. - QScriptValue rc = functionObject.call(thisObject, args); - if (!rc.isValid()) { - qDebug() << "QScriptValue is not a function or ..."; + QJSValue rc = functionObject.callWithInstance(thisObject, args); + + try { + handleEvaluationException(rc); + } catch (EvaluationException& exception) { + presentErrorDialogForEvaluationException(exception); + return false; + } + return true; +} + +bool ControllerEngine::internalExecute(QJSValue functionObject, + QJSValueList args) { + if (m_pEngine == nullptr) { + qDebug() << "ControllerEngine::execute: No script engine exists!"; return false; } - return !checkException(); + return internalExecute(m_pEngine->globalObject(), functionObject, args); } -bool ControllerEngine::execute(QScriptValue functionObject, +bool ControllerEngine::execute(QJSValue functionObject, unsigned char channel, unsigned char control, unsigned char value, @@ -421,63 +387,77 @@ bool ControllerEngine::execute(QScriptValue functionObject, if (m_pEngine == nullptr) { return false; } - QScriptValueList args; - args << QScriptValue(channel); - args << QScriptValue(control); - args << QScriptValue(value); - args << QScriptValue(status); - args << QScriptValue(group); + QJSValueList args; + args << QJSValue(channel); + args << QJSValue(control); + args << QJSValue(value); + args << QJSValue(status); + args << QJSValue(group); return internalExecute(m_pEngine->globalObject(), functionObject, args); } -bool ControllerEngine::execute(QScriptValue function, const QByteArray data, +bool ControllerEngine::execute(QJSValue function, const QByteArray data, mixxx::Duration timestamp) { Q_UNUSED(timestamp); if (m_pEngine == nullptr) { return false; } - QScriptValueList args; - args << m_pBaClass->newInstance(data); - args << QScriptValue(data.size()); + QJSValueList args; +// args << m_pBaClass->newInstance(data); + args << QJSValue(data.size()); return internalExecute(m_pEngine->globalObject(), function, args); } -/* -------- ------------------------------------------------------ - Purpose: Check to see if a script threw an exception - Input: QScriptValue returned from call(scriptFunctionName) - Output: true if there was an exception - -------- ------------------------------------------------------ */ -bool ControllerEngine::checkException() { - if (m_pEngine == nullptr) { - return false; - } - - if (m_pEngine->hasUncaughtException()) { - QScriptValue exception = m_pEngine->uncaughtException(); - QString errorMessage = exception.toString(); - QString line = QString::number(m_pEngine->uncaughtExceptionLineNumber()); - QStringList backtrace = m_pEngine->uncaughtExceptionBacktrace(); - QString filename = exception.property("fileName").toString(); +// Check if a script evaluation threw an exception. If so, register that the source +// file threw and error and show error dialog. +// +// Input: QJSValue returned from evaluation +// Output: true if there was an exception, false otherwise. +QJSValue ControllerEngine::evaluateProgram(const QString& program, const QString& fileName, + int lineNumber) { + if (m_pEngine == nullptr) { + throw NullEngineException(); + } + + QJSValue evaluationResult = m_pEngine->evaluate(program, fileName, lineNumber); + handleEvaluationException(evaluationResult); + + return evaluationResult; +} - QStringList error; - error << (filename.isEmpty() ? "" : filename) << errorMessage << line; - m_scriptErrors.insert((filename.isEmpty() ? "passed code" : filename), error); +void ControllerEngine::handleEvaluationException(QJSValue evaluationResult) { + // TODO: add test for this + if (evaluationResult.isError()) { + // TODO: compare with message property + QString errorMessage = evaluationResult.toString(); + QString line = evaluationResult.property("lineNumber").toString(); + QString backtrace = evaluationResult.property("stack").toString(); + QString filename = evaluationResult.property("filename").toString(); + + QStringList error; + error << (filename.isEmpty() ? "" : filename) << errorMessage << line; + m_scriptErrors.insert((filename.isEmpty() ? "passed code" : filename), error); + + throw EvaluationException(errorMessage, line, backtrace, filename); + } +} - QString errorText = tr("Uncaught exception at line %1 in file %2: %3") - .arg(line, (filename.isEmpty() ? "" : filename), errorMessage); +void ControllerEngine::presentErrorDialogForEvaluationException(EvaluationException exception) { + QString filename = exception.filename; + QString errorMessage = exception.errorMessage; + QString line = exception.line; + QString backtrace = exception.backtrace; - if (filename.isEmpty()) - errorText = tr("Uncaught exception at line %1 in passed code: %2") - .arg(line, errorMessage); + QString errorText = tr("Uncaught exception at line %1 in file %2: %3") + .arg(line, (filename.isEmpty() ? "" : filename), errorMessage); - scriptErrorDialog(ControllerDebug::enabled() ? - QString("%1\nBacktrace:\n%2") - .arg(errorText, backtrace.join("\n")) : errorText); + if (filename.isEmpty()) + errorText = tr("Uncaught exception at line %1 in passed code: %2") + .arg(line, errorMessage); - m_pEngine->clearExceptions(); - return true; - } - return false; + scriptErrorDialog(ControllerDebug::enabled() ? + QString("%1\nBacktrace:\n%2") + .arg(errorText, backtrace) : errorText); } /* -------- ------------------------------------------------------ @@ -703,11 +683,11 @@ void ControllerEngine::log(QString message) { // The script should store this object to call its // 'disconnect' and 'trigger' methods as needed. // If unsuccessful, returns undefined. -QScriptValue ControllerEngine::makeConnection(QString group, QString name, - const QScriptValue callback) { +QJSValue ControllerEngine::makeConnection(QString group, QString name, + const QJSValue callback) { VERIFY_OR_DEBUG_ASSERT(m_pEngine != nullptr) { qWarning() << "Tried to connect script callback, but there is no script engine!"; - return QScriptValue(); + return QJSValue(); } ControlObjectScript* coScript = getControlObjectScript(group, name); @@ -715,29 +695,26 @@ QScriptValue ControllerEngine::makeConnection(QString group, QString name, qWarning() << "ControllerEngine: script tried to connect to ControlObject (" + group + ", " + name + ") which is non-existent, ignoring."; - return QScriptValue(); + return QJSValue(); } - if (!callback.isFunction()) { + if (!callback.isCallable()) { qWarning() << "Tried to connect (" + group + ", " + name + ")" << "to an invalid callback, ignoring."; - return QScriptValue(); + return QJSValue(); } ScriptConnection connection; connection.key = ConfigKey(group, name); connection.controllerEngine = this; connection.callback = callback; - connection.context = getThisObjectInFunctionCall(); connection.id = QUuid::createUuid(); if (coScript->addScriptConnection(connection)) { - return m_pEngine->newQObject( - new ScriptConnectionInvokableWrapper(connection), - QScriptEngine::ScriptOwnership); + return m_pEngine->newQObject(new ScriptConnectionInvokableWrapper(connection)); } - return QScriptValue(); + return QJSValue(); } /* -------- ------------------------------------------------------ @@ -745,12 +722,12 @@ QScriptValue ControllerEngine::makeConnection(QString group, QString name, Input: the value of the connected ControlObject to pass to the callback -------- ------------------------------------------------------ */ void ScriptConnection::executeCallback(double value) const { - QScriptValueList args; - args << QScriptValue(value); - args << QScriptValue(key.group); - args << QScriptValue(key.item); - QScriptValue func = callback; // copy function because QScriptValue::call is not const - QScriptValue result = func.call(context, args); + QJSValueList args; + args << QJSValue(value); + args << QJSValue(key.group); + args << QJSValue(key.item); + QJSValue func = callback; // copy function because QScriptValue::call is not const + QJSValue result = func.call(args); if (result.isError()) { qWarning() << "ControllerEngine: Invocation of connection " << id.toString() << "connected to (" + key.group + ", " + key.item + ") failed:" @@ -807,13 +784,13 @@ void ScriptConnectionInvokableWrapper::trigger() { // it is disconnected. // WARNING: These behaviors are quirky and confusing, so if you change this function, // be sure to run the ControllerEngineTest suite to make sure you do not break old scripts. -QScriptValue ControllerEngine::connectControl( - QString group, QString name, const QScriptValue passedCallback, bool disconnect) { +QJSValue ControllerEngine::connectControl( + QString group, QString name, const QJSValue passedCallback, bool disconnect) { // The passedCallback may or may not actually be a function, so when // the actual callback function is found, store it in this variable. - QScriptValue actualCallbackFunction; + QJSValue actualCallbackFunction; - if (passedCallback.isFunction()) { + if (passedCallback.isCallable()) { if (!disconnect) { // skip all the checks below and just make the connection return makeConnection(group, name, passedCallback); @@ -834,7 +811,7 @@ QScriptValue ControllerEngine::connectControl( } // This is inconsistent with other failures, which return false. // QScriptValue() with no arguments is undefined in JavaScript. - return QScriptValue(); + return QJSValue(); } if (passedCallback.isString()) { @@ -842,15 +819,24 @@ QScriptValue ControllerEngine::connectControl( // before evaluating the code string. VERIFY_OR_DEBUG_ASSERT(m_pEngine != nullptr) { qWarning() << "Tried to connect script callback, but there is no script engine!"; - return QScriptValue(false); + return QJSValue(false); } - actualCallbackFunction = m_pEngine->evaluate(passedCallback.toString()); + bool exceptionHappened = false; - if (checkException() || !actualCallbackFunction.isFunction()) { + try { + actualCallbackFunction = evaluateProgram(passedCallback.toString()); + } catch (EvaluationException& exception) { + exceptionHappened = true; + } catch (NullEngineException& exception) { + qDebug() << "ControllerEngine::execute: No script engine exists!"; + exceptionHappened = true; + } + + if (exceptionHappened || !actualCallbackFunction.isCallable()) { qWarning() << "Could not evaluate callback function:" << passedCallback.toString(); - return QScriptValue(false); + return QJSValue(false); } if (coScript->countConnections() > 0 && !disconnect) { @@ -866,9 +852,7 @@ QScriptValue ControllerEngine::connectControl( "use engine.makeConnection. Returning reference to connection " + connection.id.toString(); - return m_pEngine->newQObject( - new ScriptConnectionInvokableWrapper(connection), - QScriptEngine::ScriptOwnership); + return m_pEngine->newQObject(new ScriptConnectionInvokableWrapper(connection)); } } else if (passedCallback.isQObject()) { // Assume a ScriptConnection and assume that the script author @@ -886,7 +870,7 @@ QScriptValue ControllerEngine::connectControl( (ScriptConnectionInvokableWrapper*)qobject; proxy->disconnect(); } - return QScriptValue(false); + return QJSValue(false); } // Support removing connections by passing "true" as the last parameter @@ -899,7 +883,7 @@ QScriptValue ControllerEngine::connectControl( // disconnect all ScriptConnections connected to the // callback function, even though there may be multiple connections. coScript->disconnectAllConnectionsToFunction(actualCallbackFunction); - return QScriptValue(true); + return QJSValue(true); } // If execution gets this far without returning, make @@ -926,7 +910,7 @@ void ControllerEngine::trigger(QString group, QString name) { Input: Script filename Output: false if the script file has errors or doesn't exist -------- ------------------------------------------------------ */ -bool ControllerEngine::evaluate(const QString& scriptName, QList scriptPaths) { +bool ControllerEngine::evaluateScriptFile(const QString& scriptName, QList scriptPaths) { if (m_pEngine == nullptr) { return false; } @@ -977,45 +961,30 @@ bool ControllerEngine::evaluate(const QString& scriptName, QList script scriptCode.append('\n'); input.close(); - // Check syntax - QScriptSyntaxCheckResult result = m_pEngine->checkSyntax(scriptCode); - QString error = ""; - switch (result.state()) { - case (QScriptSyntaxCheckResult::Valid): break; - case (QScriptSyntaxCheckResult::Intermediate): - error = "Incomplete code"; - break; - case (QScriptSyntaxCheckResult::Error): - error = "Syntax error"; - break; - } - if (error != "") { - error = QString("%1 at line %2, column %3 in file %4: %5") - .arg(error, - QString::number(result.errorLineNumber()), - QString::number(result.errorColumnNumber()), - filename, result.errorMessage()); - - qWarning() << "ControllerEngine:" << error; - if (m_bPopups) { - ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties(); - props->setType(DLG_WARNING); - props->setTitle("Controller script file error"); - props->setText(QString("There was an error in the controller script file %1.").arg(filename)); - props->setInfoText("The functionality provided by this script file will be disabled."); - props->setDetails(error); - - ErrorDialogHandler::instance()->requestErrorDialog(props); - } - return false; - } - // Evaluate the code - QScriptValue scriptFunction = m_pEngine->evaluate(scriptCode, filename); - - // Record errors - if (checkException()) { - return false; + try { + QJSValue scriptFunction = evaluateProgram(scriptCode, filename); + } catch (EvaluationException& exception) { + QString error = QString("Evaluation error at line %1 in file %2: %3") + .arg(exception.line, + exception.filename, + exception.errorMessage); + + qWarning() << "ControllerEngine:" << error; + if (m_bPopups) { + ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties(); + props->setType(DLG_WARNING); + props->setTitle("Controller script file error"); + props->setText(QString("There was an error in the controller script file %1.").arg(filename)); + props->setInfoText("The functionality provided by this script file will be disabled."); + props->setDetails(error); + + ErrorDialogHandler::instance()->requestErrorDialog(props); + } + return false; + } catch (NullEngineException& exception) { + qDebug() << "ControllerEngine::execute: No script engine exists!"; + return false; } return true; @@ -1039,9 +1008,9 @@ const QStringList ControllerEngine::getErrors(const QString& filename) { whether it should fire just once Output: The timer's ID, 0 if starting it failed -------- ------------------------------------------------------ */ -int ControllerEngine::beginTimer(int interval, QScriptValue timerCallback, +int ControllerEngine::beginTimer(int interval, QJSValue timerCallback, bool oneShot) { - if (!timerCallback.isFunction() && !timerCallback.isString()) { + if (!timerCallback.isCallable() && !timerCallback.isString()) { qWarning() << "Invalid timer callback provided to beginTimer." << "Valid callbacks are strings and functions."; return 0; @@ -1059,7 +1028,6 @@ int ControllerEngine::beginTimer(int interval, QScriptValue timerCallback, int timerId = startTimer(interval); TimerInfo info; info.callback = timerCallback; - info.context = getThisObjectInFunctionCall(); info.oneShot = oneShot; m_timers[timerId] = info; if (timerId == 0) { @@ -1120,10 +1088,9 @@ void ControllerEngine::timerEvent(QTimerEvent *event) { } if (timerTarget.callback.isString()) { - internalExecute(timerTarget.context, timerTarget.callback.toString()); - } else if (timerTarget.callback.isFunction()) { - internalExecute(timerTarget.context, timerTarget.callback, - QScriptValueList()); + internalExecute(timerTarget.callback.toString()); + } else if (timerTarget.callback.isCallable()) { + internalExecute(timerTarget.callback, QJSValueList()); } } diff --git a/src/controllers/controllerengine.h b/src/controllers/engine/controllerengine.h similarity index 66% rename from src/controllers/controllerengine.h rename to src/controllers/engine/controllerengine.h index 6b00c46172b2..1ed42e82425b 100644 --- a/src/controllers/controllerengine.h +++ b/src/controllers/engine/controllerengine.h @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include "bytearrayclass.h" #include "preferences/usersettings.h" @@ -24,6 +26,8 @@ class Controller; class ControlObjectScript; class ControllerEngine; +class ControllerEngineJSProxy; +class EvaluationException; // ScriptConnection represents a connection between // a ControlObject and a script callback function that gets executed when @@ -32,9 +36,8 @@ class ScriptConnection { public: ConfigKey key; QUuid id; - QScriptValue callback; + QJSValue callback; ControllerEngine *controllerEngine; - QScriptValue context; void executeCallback(double value) const; @@ -90,8 +93,8 @@ class ControllerEngine : public QObject { } // Wrap a snippet of JS code in an anonymous function - QScriptValue wrapFunctionCode(const QString& codeSnippet, int numberOfArgs); - QScriptValue getThisObjectInFunctionCall(); + // Throws EvaluationException and NullEngineException. + QJSValue wrapFunctionCode(const QString& codeSnippet, int numberOfArgs); // Look up registered script function prefixes const QList& getScriptFunctionPrefixes() { return m_scriptFunctionPrefixes; }; @@ -101,45 +104,42 @@ class ControllerEngine : public QObject { void triggerScriptConnection(const ScriptConnection conn); protected: - Q_INVOKABLE double getValue(QString group, QString name); - Q_INVOKABLE void setValue(QString group, QString name, double newValue); - Q_INVOKABLE double getParameter(QString group, QString name); - Q_INVOKABLE void setParameter(QString group, QString name, double newValue); - Q_INVOKABLE double getParameterForValue(QString group, QString name, double value); - Q_INVOKABLE void reset(QString group, QString name); - Q_INVOKABLE double getDefaultValue(QString group, QString name); - Q_INVOKABLE double getDefaultParameter(QString group, QString name); - Q_INVOKABLE QScriptValue makeConnection(QString group, QString name, - const QScriptValue callback); + double getValue(QString group, QString name); + void setValue(QString group, QString name, double newValue); + double getParameter(QString group, QString name); + void setParameter(QString group, QString name, double newValue); + double getParameterForValue(QString group, QString name, double value); + void reset(QString group, QString name); + double getDefaultValue(QString group, QString name); + double getDefaultParameter(QString group, QString name); + QJSValue makeConnection(QString group, QString name, + const QJSValue callback); // DEPRECATED: Use makeConnection instead. - Q_INVOKABLE QScriptValue connectControl(QString group, QString name, - const QScriptValue passedCallback, + QJSValue connectControl(QString group, QString name, + const QJSValue passedCallback, bool disconnect = false); // Called indirectly by the objects returned by connectControl - Q_INVOKABLE void trigger(QString group, QString name); - Q_INVOKABLE void log(QString message); - Q_INVOKABLE int beginTimer(int interval, QScriptValue scriptCode, bool oneShot = false); - Q_INVOKABLE void stopTimer(int timerId); - Q_INVOKABLE void scratchEnable(int deck, int intervalsPerRev, double rpm, - double alpha, double beta, bool ramp = true); - Q_INVOKABLE void scratchTick(int deck, int interval); - Q_INVOKABLE void scratchDisable(int deck, bool ramp = true); - Q_INVOKABLE bool isScratching(int deck); - Q_INVOKABLE void softTakeover(QString group, QString name, bool set); - Q_INVOKABLE void softTakeoverIgnoreNextValue(QString group, QString name); - Q_INVOKABLE void brake(int deck, bool activate, double factor=1.0, double rate=1.0); - Q_INVOKABLE void spinback(int deck, bool activate, double factor=1.8, double rate=-10.0); - Q_INVOKABLE void softStart(int deck, bool activate, double factor=1.0); + void trigger(QString group, QString name); + void log(QString message); + int beginTimer(int interval, QJSValue scriptCode, bool oneShot = false); + void stopTimer(int timerId); + void scratchEnable(int deck, int intervalsPerRev, double rpm, + double alpha, double beta, bool ramp = true); + void scratchTick(int deck, int interval); + void scratchDisable(int deck, bool ramp = true); + bool isScratching(int deck); + void softTakeover(QString group, QString name, bool set); + void softTakeoverIgnoreNextValue(QString group, QString name); + void brake(int deck, bool activate, double factor=1.0, double rate=1.0); + void spinback(int deck, bool activate, double factor=1.8, double rate=-10.0); + void softStart(int deck, bool activate, double factor=1.0); // Handler for timers that scripts set. virtual void timerEvent(QTimerEvent *event); public slots: - // Evaluate a script file - bool evaluate(const QString& filepath); - // Execute a basic MIDI message callback. - bool execute(QScriptValue function, + bool execute(QJSValue function, unsigned char channel, unsigned char control, unsigned char value, @@ -148,7 +148,7 @@ class ControllerEngine : public QObject { mixxx::Duration timestamp); // Execute a byte array callback. - bool execute(QScriptValue function, const QByteArray data, + bool execute(QJSValue function, const QByteArray data, mixxx::Duration timestamp); // Evaluates all provided script files and returns true if no script errors @@ -167,11 +167,14 @@ class ControllerEngine : public QObject { void errorDialogButton(const QString& key, QMessageBox::StandardButton button); private: - bool syntaxIsValid(const QString& scriptCode); - bool evaluate(const QString& scriptName, QList scriptPaths); - bool internalExecute(QScriptValue thisObject, const QString& scriptCode); - bool internalExecute(QScriptValue thisObject, QScriptValue functionObject, - QScriptValueList arguments); +// bool syntaxIsValid(const QString& scriptCode); + bool evaluateScriptFile(const QString& scriptName, QList scriptPaths); + bool internalExecute(QJSValue thisObject, const QString& scriptCode); + bool internalExecute(const QString& scriptCode); + bool internalExecute(QJSValue thisObject, QJSValue functionObject, + QJSValueList arguments); + bool internalExecute(QJSValue functionObject, + QJSValueList arguments); void initializeScriptEngine(); void scriptErrorDialog(const QString& detailedError); @@ -179,9 +182,14 @@ class ControllerEngine : public QObject { // Stops and removes all timers (for shutdown). void stopAllTimers(); - void callFunctionOnObjects(QList, const QString&, QScriptValueList args = QScriptValueList()); - bool checkException(); - QScriptEngine *m_pEngine; + void callFunctionOnObjects(QList, const QString&, QJSValueList args = QJSValueList()); + // Throws EvaluationException and NullEngineException. + QJSValue evaluateProgram(const QString& program, const QString& fileName = QString(), + int lineNumber = 1); + // Throws EvaluationException + void handleEvaluationException(QJSValue evaluationResult); + void presentErrorDialogForEvaluationException(EvaluationException exception); + QJSEngine *m_pEngine; ControlObjectScript* getControlObjectScript(const QString& group, const QString& name); @@ -197,8 +205,7 @@ class ControllerEngine : public QObject { QMap m_scriptErrors; QHash m_controlCache; struct TimerInfo { - QScriptValue callback; - QScriptValue context; + QJSValue callback; bool oneShot; }; QHash m_timers; @@ -212,11 +219,12 @@ class ControllerEngine : public QObject { QVarLengthArray m_ramp, m_brakeActive, m_softStartActive; QVarLengthArray m_scratchFilters; QHash m_scratchTimers; - QHash m_scriptWrappedFunctionCache; + QHash m_scriptWrappedFunctionCache; // Filesystem watcher for script auto-reload QFileSystemWatcher m_scriptWatcher; QList m_lastScriptPaths; + friend class ControllerEngineJSProxy; friend class ControllerEngineTest; }; diff --git a/src/controllers/engine/controllerengineexceptions.h b/src/controllers/engine/controllerengineexceptions.h new file mode 100644 index 000000000000..abe10e76b94a --- /dev/null +++ b/src/controllers/engine/controllerengineexceptions.h @@ -0,0 +1,22 @@ +#ifndef CONTROLLERENGINEEXCEPTIONS_H +#define CONTROLLERENGINEEXCEPTIONS_H + +#include + +#include + +class NullEngineException: public std::exception {}; + +class EvaluationException: public std::exception { + public: + EvaluationException(QString errorMessage, QString line, + QString backtrace, QString filename) + : errorMessage(errorMessage), line(line), backtrace(backtrace), filename(filename) {}; + + QString errorMessage; + QString line; + QString backtrace; + QString filename; +}; + +#endif diff --git a/src/controllers/engine/controllerenginejsproxy.cpp b/src/controllers/engine/controllerenginejsproxy.cpp new file mode 100644 index 000000000000..d78ae71e2fa8 --- /dev/null +++ b/src/controllers/engine/controllerenginejsproxy.cpp @@ -0,0 +1,116 @@ +#include "controllerenginejsproxy.h" +#include "controllers/engine/controllerengine.h" + +ControllerEngineJSProxy::ControllerEngineJSProxy(ControllerEngine* m_pEngine) + : m_pEngine(m_pEngine) { + +} + +ControllerEngineJSProxy::~ControllerEngineJSProxy() { + +} + +double ControllerEngineJSProxy::getValue(QString group, QString name) { + return m_pEngine->getValue(group, name); +} + +void ControllerEngineJSProxy::setValue(QString group, QString name, + double newValue) { + m_pEngine->setValue(group, name, newValue); +} + +double ControllerEngineJSProxy::getParameter(QString group, QString name) { + return m_pEngine->getParameter(group, name); +} + +void ControllerEngineJSProxy::setParameter(QString group, QString name, + double newValue) { + m_pEngine->setParameter(group, name, newValue); +} + +double ControllerEngineJSProxy::getParameterForValue(QString group, + QString name, double value) { + return m_pEngine->getParameterForValue(group, name, value); +} + +void ControllerEngineJSProxy::reset(QString group, QString name) { + m_pEngine->reset(group, name); +} + +double ControllerEngineJSProxy::getDefaultValue(QString group, QString name) { + return m_pEngine->getDefaultValue(group, name); +} + +double ControllerEngineJSProxy::getDefaultParameter(QString group, + QString name) { + return m_pEngine->getDefaultParameter(group, name); +} + +QJSValue ControllerEngineJSProxy::makeConnection(QString group, QString name, + const QJSValue callback) { + return m_pEngine->makeConnection(group, name, callback); +} + +QJSValue ControllerEngineJSProxy::connectControl(QString group, QString name, + const QJSValue passedCallback, bool disconnect) { + return m_pEngine->connectControl(group, name, passedCallback, disconnect); +} + +void ControllerEngineJSProxy::trigger(QString group, QString name) { + m_pEngine->trigger(group, name); +} + +void ControllerEngineJSProxy::log(QString message) { + m_pEngine->log(message); +} + +int ControllerEngineJSProxy::beginTimer(int interval, QJSValue scriptCode, + bool oneShot) { + return m_pEngine->beginTimer(interval, scriptCode, oneShot); +} + +void ControllerEngineJSProxy::stopTimer(int timerId) { + m_pEngine->stopTimer(timerId); +} + +void ControllerEngineJSProxy::scratchEnable(int deck, int intervalsPerRev, + double rpm, double alpha, double beta, bool ramp) { + m_pEngine->scratchEnable(deck, intervalsPerRev, rpm, alpha, beta, ramp); +} + +void ControllerEngineJSProxy::scratchTick(int deck, int interval) { + m_pEngine->scratchTick(deck, interval); +} + +void ControllerEngineJSProxy::scratchDisable(int deck, bool ramp) { + m_pEngine->scratchDisable(deck, ramp); +} + +bool ControllerEngineJSProxy::isScratching(int deck) { + return m_pEngine->isScratching(deck); +} + +void ControllerEngineJSProxy::softTakeover(QString group, QString name, + bool set) { + m_pEngine->softTakeover(group, name, set); +} + +void ControllerEngineJSProxy::softTakeoverIgnoreNextValue(QString group, + QString name) { + m_pEngine->softTakeoverIgnoreNextValue(group, name); +} + +void ControllerEngineJSProxy::brake(int deck, bool activate, double factor, + double rate) { + m_pEngine->brake(deck, activate, factor, rate); +} + +void ControllerEngineJSProxy::spinback(int deck, bool activate, double factor, + double rate) { + m_pEngine->spinback(deck, activate, factor, rate); +} + +void ControllerEngineJSProxy::softStart(int deck, bool activate, + double factor) { + m_pEngine->softStart(deck, activate, factor); +} diff --git a/src/controllers/engine/controllerenginejsproxy.h b/src/controllers/engine/controllerenginejsproxy.h new file mode 100644 index 000000000000..ac3e078e9f77 --- /dev/null +++ b/src/controllers/engine/controllerenginejsproxy.h @@ -0,0 +1,59 @@ +#ifndef CONTROLLERENGINEJSPROXY_H +#define CONTROLLERENGINEJSPROXY_H + +#include +#include + +class ControllerEngine; + +// An object of this class gets exposed to the JS engine, so the methods of this class +// constitute the api that is provided to scripts under "engine" object. +// +// The implementation simply forwards its method calls to the ControllerEngine. +// We cannot expose ControllerEngine directly to the JS engine because the JS engine would take +// ownership of ControllerEngine. This is problematic when we reload a script file, because we +// destroy the existing JS engine to create a new one. Then, since the JS engine owns ControllerEngine +// it will try to delete it. See this Qt bug: https://bugreports.qt.io/browse/QTBUG-41171 +class ControllerEngineJSProxy: public QObject { + Q_OBJECT + public: + + ControllerEngineJSProxy(ControllerEngine* m_pEngine); + + virtual ~ControllerEngineJSProxy(); + + Q_INVOKABLE double getValue(QString group, QString name); + Q_INVOKABLE void setValue(QString group, QString name, double newValue); + Q_INVOKABLE double getParameter(QString group, QString name); + Q_INVOKABLE void setParameter(QString group, QString name, double newValue); + Q_INVOKABLE double getParameterForValue(QString group, QString name, double value); + Q_INVOKABLE void reset(QString group, QString name); + Q_INVOKABLE double getDefaultValue(QString group, QString name); + Q_INVOKABLE double getDefaultParameter(QString group, QString name); + Q_INVOKABLE QJSValue makeConnection(QString group, QString name, + const QJSValue callback); + // DEPRECATED: Use makeConnection instead. + Q_INVOKABLE QJSValue connectControl(QString group, QString name, + const QJSValue passedCallback, + bool disconnect = false); + // Called indirectly by the objects returned by connectControl + Q_INVOKABLE void trigger(QString group, QString name); + Q_INVOKABLE void log(QString message); + Q_INVOKABLE int beginTimer(int interval, QJSValue scriptCode, bool oneShot = false); + Q_INVOKABLE void stopTimer(int timerId); + Q_INVOKABLE void scratchEnable(int deck, int intervalsPerRev, double rpm, + double alpha, double beta, bool ramp = true); + Q_INVOKABLE void scratchTick(int deck, int interval); + Q_INVOKABLE void scratchDisable(int deck, bool ramp = true); + Q_INVOKABLE bool isScratching(int deck); + Q_INVOKABLE void softTakeover(QString group, QString name, bool set); + Q_INVOKABLE void softTakeoverIgnoreNextValue(QString group, QString name); + Q_INVOKABLE void brake(int deck, bool activate, double factor=1.0, double rate=1.0); + Q_INVOKABLE void spinback(int deck, bool activate, double factor=1.8, double rate=-10.0); + Q_INVOKABLE void softStart(int deck, bool activate, double factor=1.0); + + private: + ControllerEngine* m_pEngine; +}; + +#endif // CONTROLLERENGINEJSPROXY_H diff --git a/src/controllers/midi/midicontroller.cpp b/src/controllers/midi/midicontroller.cpp index 8ebf383c6119..b7f1e4546be8 100644 --- a/src/controllers/midi/midicontroller.cpp +++ b/src/controllers/midi/midicontroller.cpp @@ -243,7 +243,7 @@ void MidiController::processInputMapping(const MidiInputMapping& mapping, return; } - QScriptValue function = pEngine->wrapFunctionCode(mapping.control.item, 5); + QJSValue function = pEngine->wrapFunctionCode(mapping.control.item, 5); if (!pEngine->execute(function, channel, control, value, status, mapping.control.group, timestamp)) { qDebug() << "MidiController: Invalid script function" @@ -491,7 +491,7 @@ void MidiController::processInputMapping(const MidiInputMapping& mapping, if (pEngine == NULL) { return; } - QScriptValue function = pEngine->wrapFunctionCode(mapping.control.item, 2); + QJSValue function = pEngine->wrapFunctionCode(mapping.control.item, 2); if (!pEngine->execute(function, data, timestamp)) { qDebug() << "MidiController: Invalid script function" << mapping.control.item;