diff --git a/CMakeLists.txt b/CMakeLists.txt index 2061926f158..855dfb25e94 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -739,6 +739,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/controllers/controlpickermenu.cpp src/controllers/legacycontrollermappingfilehandler.cpp src/controllers/legacycontrollermapping.cpp + src/controllers/controllershareddata.cpp src/controllers/delegates/controldelegate.cpp src/controllers/delegates/midibytedelegate.cpp src/controllers/delegates/midichanneldelegate.cpp @@ -2099,6 +2100,7 @@ add_executable(mixxx-test src/test/controller_mapping_settings_test.cpp src/test/controllers/controller_columnid_regression_test.cpp src/test/controllerscriptenginelegacy_test.cpp + src/test/controllershareddata_test.cpp src/test/controlobjecttest.cpp src/test/controlobjectaliastest.cpp src/test/controlobjectscripttest.cpp diff --git a/res/controllers/engine-api.d.ts b/res/controllers/engine-api.d.ts index e40f67db0d2..b988f7233e2 100644 --- a/res/controllers/engine-api.d.ts +++ b/res/controllers/engine-api.d.ts @@ -25,6 +25,7 @@ declare interface ScriptConnection { declare namespace engine { type SettingValue = string | number | boolean; + type SharedDataValue = string | number | boolean | object | array | undefined; /** * Gets the value of a controller setting * The value is either set in the preferences dialog, @@ -72,6 +73,28 @@ declare namespace engine { */ function setParameter(group: string, name: string, newValue: number): void; + /** + * Gets the shared runtime data. + * @returns Runtime shared data value + */ + function getSharedData(): SharedDataValue; + + /** + * Override the the shared runtime data with a new value. + * + * It is suggested to make additive changes (e.g add new attribute to existing object) in order to ease integration with other controller mapping + * @param newValue Runtime shared data value to be set + */ + function setSharedData(newValue: SharedDataValue): void; + + /** + * Sets the control value specified with normalized range of 0..1 + * @param group Group of the control e.g. "[Channel1]" + * @param name Name of the control e.g. "play_indicator" + * @param newValue Value to be set, normalized to a range of 0..1 + */ + function setParameter(group: string, name: string, newValue: number): void; + /** * Normalizes a specified value using the range of the given control, * to the range of 0..1 @@ -112,6 +135,7 @@ declare namespace engine { function getDefaultParameter(group: string, name: string): number; type CoCallback = (value: number, group: string, name: string) => void + type RuntimeSharedDataCallback = (value: SharedDataValue) => void /** * Connects a specified Mixxx Control with a callback function, which is executed if the value of the control changes @@ -121,9 +145,18 @@ declare namespace engine { * @param group Group of the control e.g. "[Channel1]" * @param name Name of the control e.g. "play_indicator" * @param callback JS function, which will be called every time, the value of the connected control changes. - * @returns Returns script connection object on success, otherwise 'undefined'' + * @returns Returns script connection object on success, otherwise 'undefined' + */ + function makeConnection(group: string, name: string, callback: CoCallback): ScriptConnection | undefined; + + /** + * Register callback function to be triggered when the shared data is updated + * + * Note that local update will also trigger the callback. Make sure to make your callback safe against recursion. + * @param callback JS function, which will be called every time, the shared controller value changes. + * @returns Returns script connection object on success, otherwise 'undefined' */ - function makeConnection(group: string, name: string, callback: CoCallback): ScriptConnection |undefined; + function makeSharedDataConnection(callback: RuntimeSharedDataCallback): ScriptConnection | undefined; /** * Connects a specified Mixxx Control with a callback function, which is executed if the value of the control changes diff --git a/src/controllers/controller.cpp b/src/controllers/controller.cpp index 61a4bf8c002..0f8c87bd8d2 100644 --- a/src/controllers/controller.cpp +++ b/src/controllers/controller.cpp @@ -3,6 +3,7 @@ #include #include +#include "controllers/controllershareddata.h" #include "controllers/scripting/legacy/controllerscriptenginelegacy.h" #include "moc_controller.cpp" #include "util/cmdlineargs.h" @@ -62,7 +63,7 @@ void Controller::stopEngine() { m_pScriptEngineLegacy = nullptr; } -bool Controller::applyMapping() { +bool Controller::applyMapping(std::shared_ptr runtimeData) { qCInfo(m_logBase) << "Applying controller mapping..."; const std::shared_ptr pMapping = cloneMapping(); @@ -84,6 +85,11 @@ bool Controller::applyMapping() { m_pScriptEngineLegacy->setScriptFiles(scriptFiles); m_pScriptEngineLegacy->setSettings(pMapping->getSettings()); + + const auto& ns = pMapping->sharedDataNamespace(); + if (!ns.isEmpty() && runtimeData != nullptr) { + m_pScriptEngineLegacy->setSharedData(runtimeData->namespaced(ns)); + } return m_pScriptEngineLegacy->initialize(); } diff --git a/src/controllers/controller.h b/src/controllers/controller.h index 2c91e7d0ba7..a5194551a34 100644 --- a/src/controllers/controller.h +++ b/src/controllers/controller.h @@ -8,6 +8,7 @@ class ControllerJSProxy; class ControllerScriptEngineLegacy; +class ControllerSharedData; /// This is a base class representing a physical (or software) controller. It /// must be inherited by a class that implements it on some API. Note that the @@ -70,7 +71,7 @@ class Controller : public QObject { // this if they have an alternate way of handling such data.) virtual void receive(const QByteArray& data, mixxx::Duration timestamp); - virtual bool applyMapping(); + virtual bool applyMapping(std::shared_ptr runtimeData); virtual void slotBeforeEngineShutdown(); // Puts the controller in and out of learning mode. @@ -137,7 +138,6 @@ class Controller : public QObject { const RuntimeLoggingCategory m_logOutput; private: // but used by ControllerManager - virtual int open() = 0; virtual int close() = 0; // Requests that the device poll if it is a polling device. Returns true diff --git a/src/controllers/controllermanager.cpp b/src/controllers/controllermanager.cpp index 7dbac102ef3..ba9602756ce 100644 --- a/src/controllers/controllermanager.cpp +++ b/src/controllers/controllermanager.cpp @@ -6,7 +6,9 @@ #include "controllers/controller.h" #include "controllers/controllerlearningeventfilter.h" #include "controllers/controllermappinginfoenumerator.h" +#include "controllers/controllershareddata.h" #include "controllers/defs_controllers.h" +#include "controllers/scripting/legacy/controllerscriptenginelegacy.h" #include "moc_controllermanager.cpp" #include "util/cmdlineargs.h" #include "util/compatibility/qmutex.h" @@ -93,7 +95,8 @@ ControllerManager::ControllerManager(UserSettingsPointer pConfig) // its own event loop. m_pControllerLearningEventFilter(new ControllerLearningEventFilter()), m_pollTimer(this), - m_skipPoll(false) { + m_skipPoll(false), + m_pRuntimeData(std::make_shared(this)) { qRegisterMetaType>( "std::shared_ptr"); @@ -300,7 +303,12 @@ void ControllerManager::slotSetUpDevices() { qWarning() << "There was a problem opening" << name; continue; } - pController->applyMapping(); + VERIFY_OR_DEBUG_ASSERT(pController->getScriptEngine()) { + qWarning() << "Unable to acquire the controller engine. Has the " + "controller open successfully?"; + continue; + } + pController->applyMapping(m_pRuntimeData); } pollIfAnyControllersOpen(); @@ -392,7 +400,13 @@ void ControllerManager::openController(Controller* pController) { // If successfully opened the device, apply the mapping and save the // preference setting. if (result == 0) { - pController->applyMapping(); + VERIFY_OR_DEBUG_ASSERT(pController->getScriptEngine()) { + qWarning() << "Unable to acquire the controller engine. Has the " + "controller open successfully?"; + return; + } + + pController->applyMapping(m_pRuntimeData); // Update configuration to reflect controller is enabled. m_pConfig->setValue( diff --git a/src/controllers/controllermanager.h b/src/controllers/controllermanager.h index cfa040d496a..a012b1a2e69 100644 --- a/src/controllers/controllermanager.h +++ b/src/controllers/controllermanager.h @@ -14,6 +14,7 @@ class Controller; class ControllerLearningEventFilter; class MappingInfoEnumerator; class LegacyControllerMapping; +class ControllerSharedData; class ControllerEnumerator; /// Function to sort controllers by name @@ -86,4 +87,5 @@ class ControllerManager : public QObject { QSharedPointer m_pMainThreadUserMappingEnumerator; QSharedPointer m_pMainThreadSystemMappingEnumerator; bool m_skipPoll; + std::shared_ptr m_pRuntimeData; }; diff --git a/src/controllers/controllershareddata.cpp b/src/controllers/controllershareddata.cpp new file mode 100644 index 00000000000..895441b6131 --- /dev/null +++ b/src/controllers/controllershareddata.cpp @@ -0,0 +1,21 @@ +#include + +#include "moc_controllershareddata.cpp" + +ControllerNamespacedSharedData* ControllerSharedData::namespaced(const QString& ns) { + return new ControllerNamespacedSharedData(this, ns); +} + +ControllerNamespacedSharedData::ControllerNamespacedSharedData( + ControllerSharedData* parent, const QString& ns) + : QObject(parent), m_namespace(ns) { + connect(parent, + &ControllerSharedData::updated, + this, + [this](const QString& ns, const QVariant& value) { + if (ns != m_namespace) { + return; + } + emit updated(value); + }); +} diff --git a/src/controllers/controllershareddata.h b/src/controllers/controllershareddata.h new file mode 100644 index 00000000000..0fe0a14b6f8 --- /dev/null +++ b/src/controllers/controllershareddata.h @@ -0,0 +1,67 @@ +#pragma once + +#include + +#include "util/assert.h" + +class ControllerNamespacedSharedData; + +/// ControllerSharedData is a wrapper that allows controllers script runtimes +/// to share arbitrary data via a the JavaScript interface. Controllers don't +/// access this object directly, and instead uses the +/// ControllerNamespacedSharedData wrapper to isolate a specific namespace and +/// prevent potential clash +class ControllerSharedData : public QObject { + Q_OBJECT + public: + ControllerSharedData(QObject* parent) + : QObject(parent), m_value() { + } + + QVariant get(const QString& ns) const { + return m_value.value(ns); + } + + /// @brief Create a a namespace wrapper that can be used by a controller. + /// The caller is owning the wrapper + /// @param ns The namespace to restrict access to + /// @return The pointer to the newly allocated wrapper + ControllerNamespacedSharedData* namespaced(const QString& ns); + + public slots: + void set(const QString& ns, const QVariant& value) { + m_value[ns] = value; + emit updated(ns, m_value[ns]); + } + + signals: + void updated(const QString& ns, const QVariant& value); + + private: + QHash m_value; +}; + +/// ControllerNamespacedSharedData is a wrapper that restrict access to a given +/// namespace. It doesn't hold any data and can safely be deleted at all time, +/// but only provide the namespace abstraction for controller to interact with +/// via a the JavaScript interface +class ControllerNamespacedSharedData : public QObject { + Q_OBJECT + public: + ControllerNamespacedSharedData(ControllerSharedData* parent, const QString& ns); + + QVariant get() const { + return static_cast(parent())->get(m_namespace); + } + + public slots: + void set(const QVariant& value) { + static_cast(parent())->set(m_namespace, value); + } + + signals: + void updated(const QVariant& value); + + private: + QString m_namespace; +}; diff --git a/src/controllers/legacycontrollermapping.h b/src/controllers/legacycontrollermapping.h index 0e556c9a11e..24e4f179288 100644 --- a/src/controllers/legacycontrollermapping.h +++ b/src/controllers/legacycontrollermapping.h @@ -22,6 +22,7 @@ class LegacyControllerMapping { : m_productMatches(other.m_productMatches), m_bDirty(other.m_bDirty), m_deviceId(other.m_deviceId), + m_sharedDataNamespace(other.m_sharedDataNamespace), m_filePath(other.m_filePath), m_name(other.m_name), m_author(other.m_author), @@ -161,6 +162,15 @@ class LegacyControllerMapping { return m_deviceId; } + void setSharedDataNamespace(QString sharedDataNamespace) { + m_sharedDataNamespace = std::move(sharedDataNamespace); + setDirty(true); + } + + const QString& sharedDataNamespace() const { + return m_sharedDataNamespace; + } + void setFilePath(const QString& filePath) { m_filePath = filePath; setDirty(true); @@ -277,6 +287,7 @@ class LegacyControllerMapping { bool m_bDirty; QString m_deviceId; + QString m_sharedDataNamespace; QString m_filePath; QString m_name; QString m_author; diff --git a/src/controllers/legacycontrollermappingfilehandler.cpp b/src/controllers/legacycontrollermappingfilehandler.cpp index b8c6ab94321..ebab5f92253 100644 --- a/src/controllers/legacycontrollermappingfilehandler.cpp +++ b/src/controllers/legacycontrollermappingfilehandler.cpp @@ -210,6 +210,12 @@ void LegacyControllerMappingFileHandler::addScriptFilesToMapping( QString deviceId = controller.attribute("id", ""); mapping->setDeviceId(deviceId); + // Empty namespace is forbidden. If a controller wants to use shared data, + // they must specify an non-empty string. + QString sharedDataNamespace = controller.attribute("namespace", ""); + if (!sharedDataNamespace.isEmpty()) { + mapping->setSharedDataNamespace(deviceId); + } // See TODO in LegacyControllerMapping::DeviceDirection - `direction` should // only be used as a workaround till the bulk integration gets refactored diff --git a/src/controllers/midi/midicontroller.cpp b/src/controllers/midi/midicontroller.cpp index f815cf5d0d2..9b97864ccbd 100644 --- a/src/controllers/midi/midicontroller.cpp +++ b/src/controllers/midi/midicontroller.cpp @@ -76,9 +76,9 @@ bool MidiController::matchMapping(const MappingInfo& mapping) { return false; } -bool MidiController::applyMapping() { +bool MidiController::applyMapping(std::shared_ptr runtimeData) { // Handles the engine - bool result = Controller::applyMapping(); + bool result = Controller::applyMapping(std::move(runtimeData)); // Only execute this code if this is an output device if (isOutputDevice()) { diff --git a/src/controllers/midi/midicontroller.h b/src/controllers/midi/midicontroller.h index dab0a4dd3cd..b36d334a926 100644 --- a/src/controllers/midi/midicontroller.h +++ b/src/controllers/midi/midicontroller.h @@ -9,6 +9,7 @@ #include "controllers/softtakeover.h" class MidiOutputHandler; +class ControllerSharedData; class MidiInputHandleJSProxy final : public QObject { Q_OBJECT @@ -81,7 +82,7 @@ class MidiController : public Controller { void slotBeforeEngineShutdown() override; private slots: - bool applyMapping() override; + bool applyMapping(std::shared_ptr) override; void learnTemporaryInputMappings(const MidiInputMappings& mappings); void clearTemporaryInputMappings(); diff --git a/src/controllers/scripting/controllerscriptenginebase.cpp b/src/controllers/scripting/controllerscriptenginebase.cpp index eca4c67d669..b6fa05d57e1 100644 --- a/src/controllers/scripting/controllerscriptenginebase.cpp +++ b/src/controllers/scripting/controllerscriptenginebase.cpp @@ -218,3 +218,7 @@ void ControllerScriptEngineBase::errorDialogButton( void ControllerScriptEngineBase::throwJSError(const QString& message) { m_pJSEngine->throwError(message); } + +void ControllerScriptEngineBase::setSharedData(ControllerNamespacedSharedData* runtimeData) { + m_runtimeData = std::unique_ptr(runtimeData); +} diff --git a/src/controllers/scripting/controllerscriptenginebase.h b/src/controllers/scripting/controllerscriptenginebase.h index 0919d46eed3..972b757ef37 100644 --- a/src/controllers/scripting/controllerscriptenginebase.h +++ b/src/controllers/scripting/controllerscriptenginebase.h @@ -4,6 +4,7 @@ #include #include +#include "controllers/controllershareddata.h" #include "util/runtimeloggingcategory.h" class Controller; @@ -40,6 +41,12 @@ class ControllerScriptEngineBase : public QObject { return m_bTesting; } + /// Takes ownership of `runtimeData` + void setSharedData(ControllerNamespacedSharedData* runtimeData); + + ControllerNamespacedSharedData* getSharedData() { + return m_runtimeData.get(); + } signals: void beforeShutdown(); @@ -51,6 +58,7 @@ class ControllerScriptEngineBase : public QObject { bool m_bDisplayingExceptionDialog; std::shared_ptr m_pJSEngine; + std::unique_ptr m_runtimeData; Controller* m_pController; const RuntimeLoggingCategory m_logger; @@ -66,5 +74,6 @@ class ControllerScriptEngineBase : public QObject { void errorDialogButton(const QString& key, QMessageBox::StandardButton button); friend class ColorMapperJSProxy; + friend class ControllerSharedDataTest; friend class MidiControllerTest; }; diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h index 2491ff78a52..dde0e99f212 100644 --- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h @@ -71,5 +71,6 @@ class ControllerScriptEngineLegacy : public ControllerScriptEngineBase { friend class ControllerScriptInterfaceLegacy; friend class ControllerScriptEngineLegacyTest; + friend class ControllerSharedDataTest; friend class MidiControllerTest; }; diff --git a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp index 9325a29faa6..6bc4bd0aad8 100644 --- a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp +++ b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp @@ -2,6 +2,7 @@ #include "control/controlobject.h" #include "control/controlobjectscript.h" +#include "controllers/controllershareddata.h" #include "controllers/scripting/legacy/controllerscriptenginelegacy.h" #include "controllers/scripting/legacy/scriptconnectionjsproxy.h" #include "mixer/playermanager.h" @@ -47,6 +48,13 @@ ControllerScriptInterfaceLegacy::ControllerScriptInterfaceLegacy( m_spinbackActive[i] = false; m_softStartActive[i] = false; } + + if (m_pEngine->getSharedData()) { + connect(m_pEngine->getSharedData(), + &ControllerNamespacedSharedData::updated, + this, + &ControllerScriptInterfaceLegacy::onRuntimeDataUpdated); + } } ControllerScriptInterfaceLegacy::~ControllerScriptInterfaceLegacy() { @@ -164,6 +172,85 @@ void ControllerScriptInterfaceLegacy::setValue( } } +QJSValue ControllerScriptInterfaceLegacy::getSharedData() { + auto pJsEngine = m_pScriptEngineLegacy->jsEngine(); + VERIFY_OR_DEBUG_ASSERT(pJsEngine) { + return QJSValue(); + } + auto* pRuntimeData = m_pScriptEngineLegacy->getSharedData(); + + if (!pRuntimeData) { + qWarning() << "No runtime data available. Make sure a valid namespace is defined."; + return QJSValue(); + } + + return pJsEngine->toScriptValue(pRuntimeData->get()); +} + +void ControllerScriptInterfaceLegacy::setSharedData(const QJSValue& value) { + auto pJsEngine = m_pScriptEngineLegacy->jsEngine(); + VERIFY_OR_DEBUG_ASSERT(pJsEngine) { + return; + } + auto* pRuntimeData = m_pScriptEngineLegacy->getSharedData(); + + if (!pRuntimeData) { + qWarning() << "No runtime data available. Make sure a valid namespace is defined."; + return; + } + + pRuntimeData->set(value.toVariant()); + qDebug() << "runtime data set successfully"; +} + +QJSValue ControllerScriptInterfaceLegacy::makeSharedDataConnection(const QJSValue& callback) { + if (!callback.isCallable()) { + m_pScriptEngineLegacy->throwJSError( + "Tried to connect runtime data update handler" + " to an invalid callback. Make sure that your code contains no " + "syntax errors."); + return QJSValue(); + } + auto pJsEngine = m_pScriptEngineLegacy->jsEngine(); + VERIFY_OR_DEBUG_ASSERT(pJsEngine) { + return QJSValue(); + } + auto* pRuntimeData = m_pScriptEngineLegacy->getSharedData(); + + if (!pRuntimeData) { + qWarning() << "No runtime data available. Make sure a valid namespace is defined."; + return QJSValue(); + } + + ScriptConnection connection; + connection.engineJSProxy = this; + connection.controllerEngine = m_pScriptEngineLegacy; + connection.callback = callback; + connection.id = QUuid::createUuid(); + + m_runtimeDataConnections.append(connection); + + return pJsEngine->newQObject( + new ScriptRuntimeConnectionJSProxy(m_runtimeDataConnections.last())); +} + +void ControllerScriptInterfaceLegacy::onRuntimeDataUpdated(const QVariant& value) { + auto pJsEngine = m_pScriptEngineLegacy->jsEngine(); + const auto args = QJSValueList{ + pJsEngine->toScriptValue(value), + }; + + for (auto& connection : m_runtimeDataConnections) { + QJSValue result = connection.callback.call(args); + if (result.isError()) { + m_pScriptEngineLegacy->logOrThrowError( + QStringLiteral("Invocation of runtime data connection %1 " + "failed: %2") + .arg(connection.id.toString(), result.toString())); + } + } +} + double ControllerScriptInterfaceLegacy::getParameter(const QString& group, const QString& name) { ControlObjectScript* coScript = getControlObjectScript(group, name); if (coScript == nullptr) { @@ -318,10 +405,11 @@ bool ControllerScriptInterfaceLegacy::removeScriptConnection( void ControllerScriptInterfaceLegacy::triggerScriptConnection( const ScriptConnection& connection) { - VERIFY_OR_DEBUG_ASSERT(m_pScriptEngineLegacy->jsEngine()) { + VERIFY_OR_DEBUG_ASSERT(m_pScriptEngineLegacy->jsEngine() && connection.key.isValid()) { return; } + // TODO handle runtimeData connection ControlObjectScript* coScript = getControlObjectScript(connection.key.group, connection.key.item); if (coScript == nullptr) { @@ -335,6 +423,35 @@ void ControllerScriptInterfaceLegacy::triggerScriptConnection( connection.executeCallback(coScript->get()); } +bool ControllerScriptInterfaceLegacy::removeRuntimeDataConnection( + const ScriptConnection& connection) { + VERIFY_OR_DEBUG_ASSERT(m_pScriptEngineLegacy->jsEngine()) { + return false; + } + return m_runtimeDataConnections.removeAll(connection) > 0; +} + +void ControllerScriptInterfaceLegacy::triggerRuntimeDataConnection( + const ScriptConnection& connection) { + VERIFY_OR_DEBUG_ASSERT(m_pScriptEngineLegacy->jsEngine() || + !m_runtimeDataConnections.contains(connection)) { + return; + } + auto pJsEngine = m_pScriptEngineLegacy->jsEngine(); + + QJSValue func = connection.callback; // copy function because QJSValue::call is not const + auto args = QJSValueList{ + pJsEngine->toScriptValue(m_pScriptEngineLegacy->getSharedData()->get()), + }; + QJSValue result = func.call(args); + if (result.isError()) { + m_pScriptEngineLegacy->logOrThrowError( + QStringLiteral( + "Invocation of runtime data connection %1 failed: %2") + .arg(connection.id.toString(), result.toString())); + } +} + // This function is a legacy version of makeConnection with several alternate // ways of invoking it. The callback function can be passed either as a string of // JavaScript code that evaluates to a function or an actual JavaScript function. diff --git a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h index 4d3e280a6ff..e5b3113e984 100644 --- a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h +++ b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h @@ -25,6 +25,11 @@ class ControllerScriptInterfaceLegacy : public QObject { Q_INVOKABLE QJSValue getSetting(const QString& name); Q_INVOKABLE double getValue(const QString& group, const QString& name); Q_INVOKABLE void setValue(const QString& group, const QString& name, double newValue); + + Q_INVOKABLE QJSValue getSharedData(); + Q_INVOKABLE void setSharedData(const QJSValue& value); + Q_INVOKABLE QJSValue makeSharedDataConnection(const QJSValue& callback); + Q_INVOKABLE double getParameter(const QString& group, const QString& name); Q_INVOKABLE void setParameter(const QString& group, const QString& name, double newValue); Q_INVOKABLE double getParameterForValue( @@ -74,9 +79,17 @@ class ControllerScriptInterfaceLegacy : public QObject { /// Execute a ScriptConnection's JS callback void triggerScriptConnection(const ScriptConnection& conn); + /// Disconnect and remove a ScriptConnection's RuntimeData JS callback + bool removeRuntimeDataConnection(const ScriptConnection& conn); + /// Execute a ScriptConnection's RuntimeData JS callback + void triggerRuntimeDataConnection(const ScriptConnection& conn); + /// Handler for timers that scripts set. virtual void timerEvent(QTimerEvent* event); + private slots: + void onRuntimeDataUpdated(const QVariant& value); + private: QJSValue makeConnectionInternal(const QString& group, const QString& name, @@ -109,4 +122,8 @@ class ControllerScriptInterfaceLegacy : public QObject { ControllerScriptEngineLegacy* m_pScriptEngineLegacy; const RuntimeLoggingCategory m_logger; + + QList m_runtimeDataConnections; + + friend class ControllerSharedDataTest; }; diff --git a/src/controllers/scripting/legacy/scriptconnectionjsproxy.cpp b/src/controllers/scripting/legacy/scriptconnectionjsproxy.cpp index d17e131c459..169e5cfa41d 100644 --- a/src/controllers/scripting/legacy/scriptconnectionjsproxy.cpp +++ b/src/controllers/scripting/legacy/scriptconnectionjsproxy.cpp @@ -13,3 +13,17 @@ bool ScriptConnectionJSProxy::disconnect() { void ScriptConnectionJSProxy::trigger() { m_scriptConnection.engineJSProxy->triggerScriptConnection(m_scriptConnection); } + +bool ScriptRuntimeConnectionJSProxy::disconnect() { + // if the removeRuntimeDataConnection succeeded, the connection has been + // successfully disconnected + bool success = + m_scriptConnection.engineJSProxy->removeRuntimeDataConnection( + m_scriptConnection); + m_isConnected = !success; + return success; +} + +void ScriptRuntimeConnectionJSProxy::trigger() { + m_scriptConnection.engineJSProxy->triggerRuntimeDataConnection(m_scriptConnection); +} diff --git a/src/controllers/scripting/legacy/scriptconnectionjsproxy.h b/src/controllers/scripting/legacy/scriptconnectionjsproxy.h index 2b80b5e4c08..58a601e552b 100644 --- a/src/controllers/scripting/legacy/scriptconnectionjsproxy.h +++ b/src/controllers/scripting/legacy/scriptconnectionjsproxy.h @@ -21,11 +21,26 @@ class ScriptConnectionJSProxy : public QObject { bool readIsConnected() const { return m_isConnected; } - Q_INVOKABLE bool disconnect(); - Q_INVOKABLE void trigger(); + Q_INVOKABLE virtual bool disconnect(); + Q_INVOKABLE virtual void trigger(); private: - ScriptConnection m_scriptConnection; QString m_idString; + + protected: + ScriptConnection m_scriptConnection; bool m_isConnected; }; + +/// ScriptRuntimeConnectionJSProxy provides scripts with an interface to +/// controller runtime update callback. +class ScriptRuntimeConnectionJSProxy : public ScriptConnectionJSProxy { + Q_OBJECT + public: + ScriptRuntimeConnectionJSProxy(const ScriptConnection& conn) + : ScriptConnectionJSProxy(conn) { + } + + Q_INVOKABLE bool disconnect() override; + Q_INVOKABLE void trigger() override; +}; diff --git a/src/test/controller_mapping_validation_test.cpp b/src/test/controller_mapping_validation_test.cpp index 29be36d171c..964c90b11b7 100644 --- a/src/test/controller_mapping_validation_test.cpp +++ b/src/test/controller_mapping_validation_test.cpp @@ -123,7 +123,7 @@ bool LegacyControllerMappingValidationTest::testLoadMapping(const MappingInfo& m FakeController controller; controller.setMapping(pMapping); - bool result = controller.applyMapping(); + bool result = controller.applyMapping(std::shared_ptr(nullptr)); controller.stopEngine(); return result; } diff --git a/src/test/controllershareddata_test.cpp b/src/test/controllershareddata_test.cpp new file mode 100644 index 00000000000..fc483e96878 --- /dev/null +++ b/src/test/controllershareddata_test.cpp @@ -0,0 +1,168 @@ +#include "controllers/controllershareddata.h" + +#include +#include + +#include "control/controlobject.h" +#include "control/controlpotmeter.h" +#include "controllers/scripting/legacy/controllerscriptenginelegacy.h" +#include "controllers/scripting/legacy/controllerscriptinterfacelegacy.h" +#include "controllers/scripting/legacy/scriptconnection.h" +#include "controllers/softtakeover.h" +#include "preferences/usersettings.h" +#include "test/mixxxtest.h" +#include "util/color/colorpalette.h" +#include "util/time.h" + +const RuntimeLoggingCategory logger(QString("test").toLocal8Bit()); + +class ControllerSharedDataTest : public MixxxTest { + protected: + void SetUp() override { + mixxx::Time::setTestMode(true); + mixxx::Time::setTestElapsedTime(mixxx::Duration::fromMillis(10)); + pRuntimeData = std::make_shared(nullptr); + cEngineA = new ControllerScriptEngineLegacy(nullptr, logger); + cEngineA->setSharedData(pRuntimeData->namespaced("testNS")); + cEngineA->initialize(); + cEngineB = new ControllerScriptEngineLegacy(nullptr, logger); + cEngineB->setSharedData(pRuntimeData->namespaced("testNS")); + cEngineB->initialize(); + } + + void TearDown() override { + delete cEngineA; + delete cEngineB; + mixxx::Time::setTestMode(false); + } + + QJSValue evaluateA(const QString& code) { + return cEngineA->jsEngine()->evaluate(code); + } + + QJSValue evaluateB(const QString& code) { + return cEngineA->jsEngine()->evaluate(code); + } + + std::shared_ptr jsEngineA() { + return cEngineA->jsEngine(); + } + + std::shared_ptr jsEngineB() { + return cEngineB->jsEngine(); + } + + const QList& runtimeDataConnectionsEngineA() { + return static_cast( + jsEngineA()->globalObject().property("engine").toQObject()) + ->m_runtimeDataConnections; + } + + const QList& runtimeDataConnectionsEngineB() { + return static_cast( + jsEngineB()->globalObject().property("engine").toQObject()) + ->m_runtimeDataConnections; + } + + ControllerScriptEngineLegacy* cEngineA; + ControllerScriptEngineLegacy* cEngineB; + + std::shared_ptr pRuntimeData; +}; + +TEST_F(ControllerSharedDataTest, getSetRuntimeData) { + pRuntimeData->set("testNS", QVariant("foobar")); + EXPECT_TRUE(!evaluateA(R"--( +let data = engine.getSharedData(); +if (data !== "foobar") throw "Something is wrong"; +engine.setSharedData("barfoo"); +)--") + .isError()); + auto data = pRuntimeData->get("testNS"); + EXPECT_TRUE(data.canConvert()); + EXPECT_EQ(data.toString(), "barfoo"); + + EXPECT_TRUE(!evaluateB(R"--( +let data = engine.getSharedData(); +if (data !== "barfoo") throw "Something is wrong"; +engine.setSharedData("bazfuu"); +)--") + .isError()); + data = pRuntimeData->get("testNS"); + EXPECT_TRUE(data.canConvert()); + EXPECT_EQ(data.toString(), "bazfuu"); +} + +TEST_F(ControllerSharedDataTest, runtimeDataCallback) { + EXPECT_TRUE(!evaluateA(R"--( +engine.makeSharedDataConnection((data) => { + if (data !== "foobar") throw "Something is wrong"; + engine.setSharedData("bazfuu") +}); +)--") + .isError()); + pRuntimeData->set("testNS", QVariant("foobar")); + application()->processEvents(); + + auto data = pRuntimeData->get("testNS"); + EXPECT_TRUE(data.canConvert()); + EXPECT_EQ(data.toString(), "bazfuu"); +} + +TEST_F(ControllerSharedDataTest, canTrigger) { + EXPECT_TRUE(!evaluateA(R"--( +engine.makeSharedDataConnection((data) => { + if (data) return; + engine.setSharedData("bazfuu") +}).trigger(); +)--") + .isError()); + application()->processEvents(); + + auto data = pRuntimeData->get("testNS"); + EXPECT_TRUE(data.canConvert()); + EXPECT_EQ(data.toString(), "bazfuu"); +} + +TEST_F(ControllerSharedDataTest, canConnectDisconnect) { + EXPECT_TRUE(!evaluateA(R"--( +let con = engine.makeSharedDataConnection((data) => { + throw "Something is wrong"; +}); +if (!con.isConnected) throw "Something is wrong"; +con.disconnect() +if (con.isConnected) throw "Something is wrong"; +)--") + .isError()); + pRuntimeData->set("testNS", QVariant("foobar")); + application()->processEvents(); + + EXPECT_TRUE(runtimeDataConnectionsEngineA().isEmpty()); +} + +TEST_F(ControllerSharedDataTest, namespacePreventClash) { + pRuntimeData->set("testNS", QVariant("foobar")); + EXPECT_TRUE(!evaluateA(R"--( +let data = engine.getSharedData(); +if (data !== "foobar") throw "Something is wrong"; +engine.setSharedData("barfoo"); +)--") + .isError()); + auto data = pRuntimeData->get("testNS"); + EXPECT_TRUE(data.canConvert()); + EXPECT_EQ(data.toString(), "barfoo"); + + pRuntimeData->set("otherTestNS", QVariant("foobar")); + cEngineA->setSharedData(pRuntimeData->namespaced("otherTestNS")); + EXPECT_TRUE(!evaluateA(R"--( +let data = engine.getSharedData(); +if (data !== "foobar") throw "Something is wrong"; +engine.setSharedData("barfoo"); +)--") + .isError()); + data = pRuntimeData->get("otherTestNS"); + EXPECT_TRUE(data.canConvert()); + EXPECT_EQ(data.toString(), "barfoo"); +} + +// TODO test namespace