diff --git a/CMakeLists.txt b/CMakeLists.txt index 92c6fc01c9c..2932a8dc05e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -998,6 +998,7 @@ add_library( 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 @@ -2411,6 +2412,7 @@ add_executable( 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 3a8022ce9db..1923eea3e89 100644 --- a/res/controllers/engine-api.d.ts +++ b/res/controllers/engine-api.d.ts @@ -36,6 +36,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, @@ -83,6 +84,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 @@ -123,6 +146,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 @@ -132,9 +156,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 70c3fd2a16f..dba0a4773c9 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" @@ -79,7 +80,8 @@ void Controller::stopEngine() { emit engineStopped(); } -bool Controller::applyMapping(const QString& resourcePath) { +bool Controller::applyMapping(const QString& resourcePath, + std::shared_ptr runtimeData) { qCInfo(m_logBase) << "Applying controller mapping..."; // Load the script code into the engine @@ -107,6 +109,11 @@ bool Controller::applyMapping(const QString& resourcePath) { #else Q_UNUSED(resourcePath); #endif + + 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 bd585aff51b..2ed18573d43 100644 --- a/src/controllers/controller.h +++ b/src/controllers/controller.h @@ -8,6 +8,7 @@ class ControllerJSProxy; class ControllerScriptEngineLegacy; +class ControllerSharedData; enum class PhysicalTransportProtocol { UNKNOWN, @@ -103,7 +104,8 @@ 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(const QString& resourcePath); + virtual bool applyMapping(const QString& resourcePath, + std::shared_ptr runtimeData); virtual void slotBeforeEngineShutdown(); // Puts the controller in and out of learning mode. @@ -170,7 +172,6 @@ class Controller : public QObject { virtual bool sendBytes(const QByteArray& data) = 0; 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 d2969596c4e..c18ed849f73 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" @@ -94,7 +96,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"); @@ -301,7 +304,12 @@ void ControllerManager::slotSetUpDevices() { qWarning() << "There was a problem opening" << name; continue; } - pController->applyMapping(m_pConfig->getResourcePath()); + VERIFY_OR_DEBUG_ASSERT(pController->getScriptEngine()) { + qWarning() << "Unable to acquire the controller engine. Has the " + "controller open successfully?"; + continue; + } + pController->applyMapping(m_pConfig->getResourcePath(), m_pRuntimeData); } pollIfAnyControllersOpen(); @@ -393,7 +401,13 @@ void ControllerManager::openController(Controller* pController) { // If successfully opened the device, apply the mapping and save the // preference setting. if (result == 0) { - pController->applyMapping(m_pConfig->getResourcePath()); + VERIFY_OR_DEBUG_ASSERT(pController->getScriptEngine()) { + qWarning() << "Unable to acquire the controller engine. Has the " + "controller open successfully?"; + return; + } + + pController->applyMapping(m_pConfig->getResourcePath(), 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..f56d4143eb5 --- /dev/null +++ b/src/controllers/controllershareddata.cpp @@ -0,0 +1,22 @@ +#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..3fbd0422554 --- /dev/null +++ b/src/controllers/controllershareddata.h @@ -0,0 +1,68 @@ +#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 17dec263389..319e33e8723 100644 --- a/src/controllers/legacycontrollermapping.h +++ b/src/controllers/legacycontrollermapping.h @@ -33,6 +33,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), @@ -243,6 +244,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); @@ -359,6 +369,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 7782ec8186c..b117e109ccb 100644 --- a/src/controllers/legacycontrollermappingfilehandler.cpp +++ b/src/controllers/legacycontrollermappingfilehandler.cpp @@ -360,6 +360,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(sharedDataNamespace); + } // 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 1cc4f6feba1..718714c1181 100644 --- a/src/controllers/midi/midicontroller.cpp +++ b/src/controllers/midi/midicontroller.cpp @@ -76,9 +76,10 @@ bool MidiController::matchMapping(const MappingInfo& mapping) { return false; } -bool MidiController::applyMapping(const QString& resourcePath) { +bool MidiController::applyMapping(const QString& resourcePath, + std::shared_ptr runtimeData) { // Handles the engine - bool result = Controller::applyMapping(resourcePath); + bool result = Controller::applyMapping(resourcePath, 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 b9d29890b29..517a13fb76f 100644 --- a/src/controllers/midi/midicontroller.h +++ b/src/controllers/midi/midicontroller.h @@ -8,6 +8,7 @@ #include "controllers/softtakeover.h" class MidiOutputHandler; +class ControllerSharedData; class MidiInputHandleJSProxy final : public QObject { Q_OBJECT @@ -84,7 +85,7 @@ class MidiController : public Controller { void slotBeforeEngineShutdown() override; private slots: - bool applyMapping(const QString& resourcePath) override; + bool applyMapping(const QString& resourcePath, 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 075ef6f5a8a..0ef65114a4f 100644 --- a/src/controllers/scripting/controllerscriptenginebase.cpp +++ b/src/controllers/scripting/controllerscriptenginebase.cpp @@ -288,3 +288,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 2129184b641..dcacb092b6f 100644 --- a/src/controllers/scripting/controllerscriptenginebase.h +++ b/src/controllers/scripting/controllerscriptenginebase.h @@ -7,6 +7,7 @@ #include #include +#include "controllers/controllershareddata.h" #include "util/runtimeloggingcategory.h" #ifdef MIXXX_USE_QML #include "controllers/controllerenginethreadcontrol.h" @@ -53,6 +54,13 @@ class ControllerScriptEngineBase : public QObject { return m_bTesting; } + /// Takes ownership of `runtimeData` + void setSharedData(ControllerNamespacedSharedData* runtimeData); + + ControllerNamespacedSharedData* getSharedData() { + return m_runtimeData.get(); + } + #ifdef MIXXX_USE_QML static void registerTrackCollectionManager( std::shared_ptr pTrackCollectionManager); @@ -80,6 +88,7 @@ class ControllerScriptEngineBase : public QObject { bool m_bErrorsAreFatal; #endif std::shared_ptr m_pJSEngine; + std::unique_ptr m_runtimeData; Controller* m_pController; const RuntimeLoggingCategory m_logger; @@ -118,4 +127,5 @@ class ControllerScriptEngineBase : public QObject { friend class ColorMapperJSProxy; friend class MidiControllerTest; + friend class ControllerSharedDataTest; }; diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h index 6baa2a80b11..4c749804475 100644 --- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h @@ -140,4 +140,5 @@ class ControllerScriptEngineLegacy : public ControllerScriptEngineBase { friend class ControllerScriptEngineLegacyTest; friend class MidiControllerTest; + friend class ControllerSharedDataTest; }; diff --git a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp index 611f9b26f95..86ae3941c1f 100644 --- a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp +++ b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp @@ -6,6 +6,7 @@ #include "control/controlobject.h" #include "control/controlobjectscript.h" #include "control/controlpotmeter.h" +#include "controllers/controllershareddata.h" #include "controllers/scripting/legacy/controllerscriptenginelegacy.h" #include "controllers/scripting/legacy/scriptconnectionjsproxy.h" #include "mixer/playermanager.h" @@ -52,6 +53,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() { @@ -169,6 +177,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) { @@ -323,10 +410,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) { @@ -340,6 +428,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 76e44f72e6a..58aa6f29177 100644 --- a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h +++ b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h @@ -57,6 +57,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( @@ -113,9 +118,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, @@ -151,4 +164,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 4d59f39e83d..fa1aca22bfe 100644 --- a/src/controllers/scripting/legacy/scriptconnectionjsproxy.h +++ b/src/controllers/scripting/legacy/scriptconnectionjsproxy.h @@ -21,10 +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: + 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 7d8661f5cca..4583d6c81af 100644 --- a/src/test/controller_mapping_validation_test.cpp +++ b/src/test/controller_mapping_validation_test.cpp @@ -199,7 +199,7 @@ bool LegacyControllerMappingValidationTest::testLoadMapping(const MappingInfo& m FakeController controller; controller.setMapping(pMapping); - bool result = controller.applyMapping("./res"); + bool result = controller.applyMapping("./res", 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..cd73bd7b58a --- /dev/null +++ b/src/test/controllershareddata_test.cpp @@ -0,0 +1,170 @@ +#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" + +using namespace std::chrono_literals; + +const RuntimeLoggingCategory logger(QString("test").toLocal8Bit()); + +class ControllerSharedDataTest : public MixxxTest { + protected: + void SetUp() override { + mixxx::Time::setTestMode(true); + mixxx::Time::addTestTime(10ms); + 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