diff --git a/src/controllers/controller.cpp b/src/controllers/controller.cpp index cfd8884db8a..ec41338ec06 100644 --- a/src/controllers/controller.cpp +++ b/src/controllers/controller.cpp @@ -73,6 +73,10 @@ bool Controller::applyPreset(bool initializeScripts) { if (success && initializeScripts) { m_pEngine->initializeScripts(scriptFiles); } + + if (initializeScripts) { + m_pEngine->loadModule(pPreset->moduleFileInfo()); + } return success; } @@ -143,4 +147,6 @@ void Controller::receive(const QByteArray data, mixxx::Duration timestamp) { QJSValue incomingDataFunction = m_pEngine->wrapFunctionCode(function, 2); m_pEngine->executeFunction(incomingDataFunction, data); } + + m_pEngine->handleInput(data, timestamp); } diff --git a/src/controllers/controllerpreset.h b/src/controllers/controllerpreset.h index 120d7b007e7..f129e99b7c4 100644 --- a/src/controllers/controllerpreset.h +++ b/src/controllers/controllerpreset.h @@ -57,6 +57,14 @@ class ControllerPreset { return m_scripts; } + void setModuleFileInfo(QFileInfo fileInfo) { + m_moduleFileInfo = fileInfo; + } + + QFileInfo moduleFileInfo() const { + return m_moduleFileInfo; + } + inline void setDirty(bool bDirty) { m_bDirty = bDirty; } @@ -178,6 +186,7 @@ class ControllerPreset { QString m_mixxxVersion; QList m_scripts; + QFileInfo m_moduleFileInfo; }; typedef QSharedPointer ControllerPresetPointer; diff --git a/src/controllers/controllerpresetfilehandler.cpp b/src/controllers/controllerpresetfilehandler.cpp index 6d147a6e885..f181c9081fc 100644 --- a/src/controllers/controllerpresetfilehandler.cpp +++ b/src/controllers/controllerpresetfilehandler.cpp @@ -144,6 +144,9 @@ void ControllerPresetFileHandler::addScriptFilesToPreset( preset->addScriptFile(filename, functionPrefix, file); scriptFile = scriptFile.nextSiblingElement("file"); } + + QString moduleFileName = controller.firstChildElement("module").text(); + preset->setModuleFileInfo(preset->dirPath().absoluteFilePath(moduleFileName)); } bool ControllerPresetFileHandler::writeDocument( diff --git a/src/controllers/controllerpresetinfoenumerator.cpp b/src/controllers/controllerpresetinfoenumerator.cpp index becbad20a4d..fdd876ff904 100644 --- a/src/controllers/controllerpresetinfoenumerator.cpp +++ b/src/controllers/controllerpresetinfoenumerator.cpp @@ -57,7 +57,7 @@ void PresetInfoEnumerator::loadSupportedPresets() { m_bulkPresets.clear(); for (const QString& dirPath : m_controllerDirPaths) { - QDirIterator it(dirPath); + QDirIterator it(dirPath, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); const QString path = it.filePath(); diff --git a/src/controllers/engine/controllerengine.cpp b/src/controllers/engine/controllerengine.cpp index 95713891187..ff9e4415f90 100644 --- a/src/controllers/engine/controllerengine.cpp +++ b/src/controllers/engine/controllerengine.cpp @@ -149,6 +149,10 @@ void ControllerEngine::gracefulShutdown() { qDebug() << "Invoking shutdown() hook in scripts"; callFunctionOnObjects(m_scriptFunctionPrefixes, "shutdown"); + if (m_shutdownFunction.isCallable()) { + executeFunction(m_shutdownFunction, QJSValueList{}); + } + // Prevents leaving decks in an unstable state // if the controller is shut down while scratching QHashIterator i(m_scratchTimers); @@ -190,6 +194,8 @@ void ControllerEngine::initializeScriptEngine() { // Create the Script Engine m_pScriptEngine = new QJSEngine(this); + m_pScriptEngine->installExtensions(QJSEngine::ConsoleExtension); + // Make this ControllerEngine instance available to scripts as 'engine'. QJSValue engineGlobalObject = m_pScriptEngine->globalObject(); ControllerEngineJSProxy* proxy = new ControllerEngineJSProxy(this); @@ -223,6 +229,55 @@ void ControllerEngine::uninitializeScriptEngine() { } } +void ControllerEngine::loadModule(QFileInfo moduleFileInfo) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) + m_moduleFileInfo = moduleFileInfo; + + QJSValue mod = m_pScriptEngine->importModule(moduleFileInfo.absoluteFilePath()); + if (mod.isError()) { + showScriptExceptionDialog(mod); + return; + } + + connect(&m_scriptWatcher, + &QFileSystemWatcher::fileChanged, + this, + &ControllerEngine::scriptHasChanged); + m_scriptWatcher.addPath(moduleFileInfo.absoluteFilePath()); + + QJSValue initFunction = mod.property("init"); + executeFunction(initFunction, QJSValueList{}); + + QJSValue handleInputFunction = mod.property("handleInput"); + if (handleInputFunction.isCallable()) { + m_handleInputFunction = handleInputFunction; + } else { + scriptErrorDialog( + "Controller JavaScript module exports no handleInput function.", + QStringLiteral("handleInput"), + true); + } + + QJSValue shutdownFunction = mod.property("shutdown"); + if (shutdownFunction.isCallable()) { + m_shutdownFunction = shutdownFunction; + } else { + qDebug() << "Module exports no shutdown function."; + } +#else + Q_UNUSED(moduleFileInfo); +#endif +} + +void ControllerEngine::handleInput(QByteArray data, mixxx::Duration timestamp) { + if (m_handleInputFunction.isCallable()) { + QJSValueList args; + args << byteArrayToScriptValue(data); + args << timestamp.toDoubleMillis(); + executeFunction(m_handleInputFunction, args); + } +} + bool ControllerEngine::loadScriptFiles(const QList& scripts) { bool scriptsEvaluatedCorrectly = true; for (const auto& script : scripts) { @@ -263,6 +318,7 @@ void ControllerEngine::reloadScripts() { qDebug() << "Re-initializing scripts"; initializeScripts(m_lastScriptFiles); + loadModule(m_moduleFileInfo); } void ControllerEngine::initializeScripts(const QList& scripts) { diff --git a/src/controllers/engine/controllerengine.h b/src/controllers/engine/controllerengine.h index af9eed4093e..bd58e1fe69e 100644 --- a/src/controllers/engine/controllerengine.h +++ b/src/controllers/engine/controllerengine.h @@ -26,6 +26,8 @@ class ControllerEngine : public QObject { ControllerEngine(Controller* controller); virtual ~ControllerEngine(); + void handleInput(QByteArray data, mixxx::Duration timestamp); + bool executeFunction(QJSValue functionObject, QJSValueList arguments); bool executeFunction(QJSValue functionObject, const QByteArray& data); @@ -103,6 +105,7 @@ class ControllerEngine : public QObject { virtual void timerEvent(QTimerEvent* event); public slots: + void loadModule(QFileInfo moduleFileInfo); bool loadScriptFiles(const QList& scripts); void initializeScripts(const QList& scripts); void gracefulShutdown(); @@ -146,6 +149,8 @@ class ControllerEngine : public QObject { double getDeckRate(const QString& group); Controller* m_pController; + QJSValue m_handleInputFunction; + QJSValue m_shutdownFunction; QList m_scriptFunctionPrefixes; QHash m_controlCache; struct TimerInfo { @@ -166,6 +171,7 @@ class ControllerEngine : public QObject { QJSValue m_byteArrayToScriptValueJSFunction; // Filesystem watcher for script auto-reload QFileSystemWatcher m_scriptWatcher; + QFileInfo m_moduleFileInfo; QList m_lastScriptFiles; bool m_bTesting; diff --git a/src/controllers/midi/midicontroller.cpp b/src/controllers/midi/midicontroller.cpp index a2ebeca7b08..d3371f037c6 100644 --- a/src/controllers/midi/midicontroller.cpp +++ b/src/controllers/midi/midicontroller.cpp @@ -202,6 +202,17 @@ void MidiController::commitTemporaryInputMappings() { void MidiController::receive(unsigned char status, unsigned char control, unsigned char value, mixxx::Duration timestamp) { + QByteArray byteArray; + byteArray.append(status); + byteArray.append(control); + byteArray.append(value); + + ControllerEngine* pEngine = getEngine(); + // pEngine is nullptr in tests. + if (pEngine) { + pEngine->handleInput(byteArray, timestamp); + } + // legacy stuff below // The rest of this function is for legacy mappings unsigned char channel = MidiUtils::channelFromStatus(status); @@ -471,6 +482,8 @@ double MidiController::computeValue( void MidiController::receive(QByteArray data, mixxx::Duration timestamp) { controllerDebug(MidiUtils::formatSysexMessage(getName(), data, timestamp)); + getEngine()->handleInput(data, timestamp); + // legacy stuff below MidiKey mappingKey(data.at(0), 0xFF);