diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b836179f73d..d48f3d88375c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2776,6 +2776,8 @@ if(QML) src/qml/qmlplayerproxy.cpp src/qml/qmlvisibleeffectsmodel.cpp src/qml/qmlwaveformoverview.cpp + src/qml/mixxxcontroller.cpp + src/qml/mixxxscreen.cpp # The following sources need to be in this target to get QML_ELEMENT properly interpreted src/control/controlmodel.cpp src/control/controlsortfiltermodel.cpp diff --git a/res/controllers/Denon-DN-S3700.midi.xml b/res/controllers/Denon-DN-S3700.midi.xml new file mode 100644 index 000000000000..56a8794e3a41 --- /dev/null +++ b/res/controllers/Denon-DN-S3700.midi.xml @@ -0,0 +1,13 @@ + + + + Denon DN-S3700 (QML) + christophehenry + Controller preset for Denon DN-S3700 turntable + + + + + + + diff --git a/res/controllers/Denon-DN-S3700.qml b/res/controllers/Denon-DN-S3700.qml new file mode 100644 index 000000000000..05597dfcd521 --- /dev/null +++ b/res/controllers/Denon-DN-S3700.qml @@ -0,0 +1,20 @@ +import QtQml + +import "Mixxx" + +MixxxController { + id: controller + + onInit: console.error(`Starting controller ${controller.controllerId} with debug mode ${controller.debugMode}`) + onShutdown: console.error(`Shutting down ${controller.controllerId} with debug mode ${controller.debugMode}`) + + MixxxScreen { + screenId: "screen 7" + splashOff: 5000 + onInit: console.error(`MixxxScreen.screenId=${screenId}, MixxxScreen.splashOff=${splashOff}`) + transformFrame: (frame, timestamp, area) => { + console.error(frame) + return new ArrayBuffer(0) + } + } +} diff --git a/src/controllers/legacycontrollermapping.h b/src/controllers/legacycontrollermapping.h index 3c74c7c85889..e3fbfa227af1 100644 --- a/src/controllers/legacycontrollermapping.h +++ b/src/controllers/legacycontrollermapping.h @@ -19,6 +19,7 @@ #include "controllers/legacycontrollersettingslayout.h" #include "defs_urls.h" #include "preferences/usersettings.h" +#include "qml/mixxxcontroller.h" #include "util/assert.h" /// This class represents a controller mapping, containing the data elements that @@ -374,6 +375,7 @@ class LegacyControllerMapping { #ifdef MIXXX_USE_QML QList m_modules; QList m_screens; + QList m_mixxxControllers; #endif QList m_scripts; DeviceDirections m_deviceDirection; diff --git a/src/controllers/legacycontrollermappingfilehandler.cpp b/src/controllers/legacycontrollermappingfilehandler.cpp index 7782ec8186ca..f9354d1f8325 100644 --- a/src/controllers/legacycontrollermappingfilehandler.cpp +++ b/src/controllers/legacycontrollermappingfilehandler.cpp @@ -390,7 +390,8 @@ void LegacyControllerMappingFileHandler::addScriptFilesToMapping( QFileInfo file = findScriptFile(mapping, filename, systemMappingsPath); if (file.suffix() == "qml") { #ifdef MIXXX_USE_QML - QString identifier = scriptFile.attribute("identifier", ""); + QString identifier = scriptFile.attribute( + "identifier", scriptFile.attribute("functionprefix", "")); mapping->addScriptFile(LegacyControllerMapping::ScriptFileInfo{ filename, identifier, diff --git a/src/controllers/scripting/controllerscriptenginebase.cpp b/src/controllers/scripting/controllerscriptenginebase.cpp index e31faad90887..e4ff0873fbb0 100644 --- a/src/controllers/scripting/controllerscriptenginebase.cpp +++ b/src/controllers/scripting/controllerscriptenginebase.cpp @@ -24,6 +24,7 @@ ControllerScriptEngineBase::ControllerScriptEngineBase( m_bAbortOnWarning(false), #ifdef MIXXX_USE_QML m_bQmlMode(false), + m_mixxxControllerEngineInterface(this), #endif m_bTesting(false) { // Handle error dialog buttons @@ -31,6 +32,10 @@ ControllerScriptEngineBase::ControllerScriptEngineBase( } #ifdef MIXXX_USE_QML +void ControllerScriptEngineBase::declareScreen(mixxx::qml::MixxxScreen& screen) { + std::make_shared(screen); +} + void ControllerScriptEngineBase::registerTrackCollectionManager( std::shared_ptr pTrackCollectionManager) { s_pTrackCollectionManager = std::move(pTrackCollectionManager); @@ -60,6 +65,7 @@ bool ControllerScriptEngineBase::initialize() { #ifdef MIXXX_USE_QML } else { auto pQmlEngine = std::make_shared(this); + pQmlEngine->setProperty("controllerEngineInterface", m_mixxxControllerEngineInterface); pQmlEngine->addImportPath(QStringLiteral(":/mixxx.org/imports")); if (s_pTrackCollectionManager) { mixxx::qml::AsyncImageProvider* pImageProvider = new mixxx::qml::AsyncImageProvider( @@ -288,3 +294,9 @@ void ControllerScriptEngineBase::errorDialogButton( void ControllerScriptEngineBase::throwJSError(const QString& message) { m_pJSEngine->throwError(message); } + +#ifdef MIXXX_USE_QML +void mixxx::qml::MixxxControllerEngineInterface::declareScreen(MixxxScreen& screen) { + m_controllerScriptEngine->declareScreen(screen); +} +#endif diff --git a/src/controllers/scripting/controllerscriptenginebase.h b/src/controllers/scripting/controllerscriptenginebase.h index 2129184b641b..d7d9fdf35e9b 100644 --- a/src/controllers/scripting/controllerscriptenginebase.h +++ b/src/controllers/scripting/controllerscriptenginebase.h @@ -10,12 +10,14 @@ #include "util/runtimeloggingcategory.h" #ifdef MIXXX_USE_QML #include "controllers/controllerenginethreadcontrol.h" +#include "qml/mixxxscreen.h" #endif class Controller; class QJSEngine; #ifdef MIXXX_USE_QML class TrackCollectionManager; +class MixxxControllerEngineInterface; #endif /// ControllerScriptEngineBase manages the JavaScript engine for controller scripts. @@ -38,6 +40,8 @@ class ControllerScriptEngineBase : public QObject { #ifdef MIXXX_USE_QML /// Precondition: QML.isValid() == true void showQMLExceptionDialog(const QQmlError& evaluationResult, bool bFatal = false); + + void declareScreen(mixxx::qml::MixxxScreen& screen); #endif void throwJSError(const QString& message); @@ -94,6 +98,8 @@ class ControllerScriptEngineBase : public QObject { #ifdef MIXXX_USE_QML private: static inline std::shared_ptr s_pTrackCollectionManager; + QHash> m_screens; + MixxxControllerEngineInterface m_mixxxControllerEngineInterface; protected: /// Pause the GUI main thread. Pause is required by rendering @@ -119,3 +125,23 @@ class ControllerScriptEngineBase : public QObject { friend class ColorMapperJSProxy; friend class MidiControllerTest; }; + +#ifdef MIXXX_USE_QML +namespace mixxx { +namespace qml { + +class MixxxControllerEngineInterface : QObject { + Q_OBJECT + public: + MixxxControllerEngineInterface(ControllerScriptEngineBase* controllerScriptEngine) + : QObject(controllerScriptEngine), m_controllerScriptEngine(controllerScriptEngine) { + } + void declareScreen(MixxxScreen& screen); + + private: + ControllerScriptEngineBase* m_controllerScriptEngine; +}; + +} // namespace qml +} // namespace mixxx +#endif diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp index 9cede503565b..029997ba7ada 100644 --- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp @@ -33,12 +33,12 @@ const QByteArray kScreenTransformFunctionUntypedSignature = "transformFrame(QVariant,QVariant)"); const QByteArray kScreenTransformFunctionTypedSignature = QMetaObject::normalizedSignature("transformFrame(QVariant,QDateTime)"); -const QByteArray kScreenInitFunctionUntypedSignature = +const QByteArray kQmlComponentInitFunctionUntypedSignature = QMetaObject::normalizedSignature( "init(QVariant,QVariant)"); -const QByteArray kScreenInitFunctionTypedSignature = +const QByteArray kQmlComponentFunctionTypedSignature = QMetaObject::normalizedSignature("init(QString,bool)"); -const QByteArray kScreenShutdownFunctionSignature = +const QByteArray kQmlComponentShutdownFunctionSignature = QMetaObject::normalizedSignature("shutdown()"); } // anonymous namespace #endif @@ -128,6 +128,10 @@ bool ControllerScriptEngineLegacy::callShutdownFunction() { } #ifdef MIXXX_USE_QML + for (const auto& controller : m_mixxxController) { + emit controller->shutdown(); + } + if (!m_bQmlMode) { #endif return callFunctionOnObjects(m_scriptFunctionPrefixes, "shutdown"); @@ -154,7 +158,7 @@ bool ControllerScriptEngineLegacy::callShutdownFunction() { } QMetaMethod shutdownFunction; - int methodIdx = metaObject->indexOfMethod(kScreenShutdownFunctionSignature); + int methodIdx = metaObject->indexOfMethod(kQmlComponentShutdownFunctionSignature); if (methodIdx == -1 || !metaObject->method(methodIdx).isValid()) { qCDebug(m_logger) << "QML Scene for screen" << screenIdentifier @@ -201,6 +205,11 @@ bool ControllerScriptEngineLegacy::callInitFunction() { qCDebug(m_logger) << "Unhandled controller JS error is:" << m_pJSEngine->catchError().toString(); } + + for (const auto& controller : m_mixxxController) { + emit controller->init(); + } + QHashIterator> i(m_rootItems); bool success = true; while (i.hasNext()) { @@ -218,12 +227,12 @@ bool ControllerScriptEngineLegacy::callInitFunction() { QMetaMethod initFunction; bool typed = false; - int methodIdx = metaObject->indexOfMethod(kScreenInitFunctionUntypedSignature); + int methodIdx = metaObject->indexOfMethod(kQmlComponentInitFunctionUntypedSignature); if (methodIdx == -1 || !metaObject->method(methodIdx).isValid()) { qCDebug(m_logger) << "QML Scene for screen" << screenIdentifier << "has no valid untyped init method."; - methodIdx = metaObject->indexOfMethod(kScreenInitFunctionTypedSignature); + methodIdx = metaObject->indexOfMethod(kQmlComponentFunctionTypedSignature); typed = true; } @@ -251,6 +260,7 @@ bool ControllerScriptEngineLegacy::callInitFunction() { // connection QQmlEngine::warnings -> // ControllerScriptEngineBase::handleQMLErrors } + return success; } #endif @@ -439,10 +449,6 @@ bool ControllerScriptEngineLegacy::initialize() { qCWarning(m_logger) << "Controller mapping has QML library definitions but no " "QML files to use it. Ignoring."; } - - // If we encounter a failure while loading a scene, we will need to properly - // stop the screen threads before shutting down. - bool sceneBindingHasFailure = false; #endif for (const LegacyControllerMapping::ScriptFileInfo& script : std::as_const(m_scriptFiles)) { #ifdef MIXXX_USE_QML @@ -457,49 +463,13 @@ bool ControllerScriptEngineLegacy::initialize() { } #ifdef MIXXX_USE_QML } else { - if (script.identifier.isEmpty()) { - while (!availableScreens.isEmpty()) { - QString screenIdentifier(availableScreens.firstKey()); - if (!bindSceneToScreen(script, - screenIdentifier, - availableScreens.take(screenIdentifier))) { - sceneBindingHasFailure = true; - } - } - } else { - if (!availableScreens.contains(script.identifier)) { - qCCritical(m_logger) << "Not screen" << script.identifier << "found!"; + const auto result = instanciateQMLComponent(script, availableScreens); - sceneBindingHasFailure = true; - break; - } - if (!bindSceneToScreen(script, - script.identifier, - availableScreens.take(script.identifier))) { - sceneBindingHasFailure = true; - } + if (!result) { + shutdown(); + return false; } } - } - - if (!availableScreens.isEmpty()) { - if (!sceneBindingHasFailure) { - qCWarning(m_logger) - << "Found screen with no QML scene able to run on it. Ignoring" - << availableScreens.size() << "screens"; - } - - while (!availableScreens.isEmpty()) { - auto pScreen = availableScreens.take(availableScreens.firstKey()); - VERIFY_OR_DEBUG_ASSERT(!pScreen->isValid() || - !pScreen->isRunning() || pScreen->stop()) { - qCWarning(m_logger) << "Unable to stop the screen"; - }; - } - } - if (sceneBindingHasFailure) { - shutdown(); - return false; #endif } @@ -579,15 +549,9 @@ void ControllerScriptEngineLegacy::extractTransformFunction( } bool ControllerScriptEngineLegacy::bindSceneToScreen( - const LegacyControllerMapping::ScriptFileInfo& qmlFile, + const std::shared_ptr pScene, const QString& screenIdentifier, std::shared_ptr pScreen) { - // Like for Javascript, if the script is invalid, it should be watched so the user can fix it - // without having to restart Mixxx. So, add it to the watcher before - // evaluating it. - watchFilePath(qmlFile.file.absoluteFilePath()); - - auto pScene = loadQMLFile(qmlFile, pScreen); if (!pScene) { VERIFY_OR_DEBUG_ASSERT(!pScreen->isValid() || !pScreen->isRunning() || pScreen->stop()) { @@ -740,6 +704,7 @@ void ControllerScriptEngineLegacy::shutdown() { callShutdownFunction(); #ifdef MIXXX_USE_QML + m_engineThreadControl.setCanPause(false); // Wait till the splash off animation has finished rendering. std::chrono::milliseconds maxSplashOffDuration{}; @@ -863,34 +828,40 @@ bool ControllerScriptEngineLegacy::evaluateScriptFile(const QFileInfo& scriptFil } #ifdef MIXXX_USE_QML -std::shared_ptr ControllerScriptEngineLegacy::loadQMLFile( +bool ControllerScriptEngineLegacy::instanciateQMLComponent( const LegacyControllerMapping::ScriptFileInfo& qmlScript, - std::shared_ptr pScreen) { + QMap> + availableScreens) { + // Like for Javascript, if the script is invalid, it should be watched so the user can fix it + // without having to restart Mixxx. So, add it to the watcher before + // evaluating it. VERIFY_OR_DEBUG_ASSERT(m_pJSEngine || qmlScript.type != LegacyControllerMapping::ScriptFileInfo::Type::Qml) { - return nullptr; + return false; } + watchFilePath(qmlScript.file.absoluteFilePath()); + QQmlComponent qmlComponent = QQmlComponent( std::dynamic_pointer_cast(m_pJSEngine).get()); - QFile scene = QFile(qmlScript.file.absoluteFilePath()); - if (!scene.exists()) { - qCWarning(m_logger) << "Unable to load the QML scene:" << qmlScript.file.absoluteFilePath() + QFile file = QFile(qmlScript.file.absoluteFilePath()); + if (!file.exists()) { + qCWarning(m_logger) << "Unable to load the QML file:" << qmlScript.file.absoluteFilePath() << "does not exist."; - return nullptr; + return false; } QDir dir(m_resourcePath + "/qml/"); - scene.open(QIODevice::ReadOnly); - qmlComponent.setData(scene.readAll(), + file.open(QIODevice::ReadOnly); + qmlComponent.setData(file.readAll(), // Obfuscate the scene filename to make it appear in the QML folder. // This allows a smooth integration with QML components. QUrl::fromLocalFile( dir.absoluteFilePath(qmlScript.file.fileName()))); - scene.close(); + file.close(); while (qmlComponent.isLoading()) { qCDebug(m_logger) << "Waiting for component " @@ -902,47 +873,104 @@ std::shared_ptr ControllerScriptEngineLegacy::loadQMLFile( if (qmlComponent.isError()) { const QList errorList = qmlComponent.errors(); for (const QQmlError& error : errorList) { - qCWarning(m_logger) << "Unable to load the QML scene:" << error.url() + qCWarning(m_logger) << "Unable to load the QML file:" << error.url() << "at line" << error.line() << ", error: " << error; showQMLExceptionDialog(error, true); } - return nullptr; + return false; } VERIFY_OR_DEBUG_ASSERT(qmlComponent.isReady()) { qCWarning(m_logger) << "QMLComponent isn't ready although synchronous load was requested."; - return nullptr; + return false; } - QObject* pRootObject = qmlComponent.createWithInitialProperties( - QVariantMap{{"screenId", pScreen->info().identifier}}); - if (qmlComponent.isError()) { - const QList errorList = qmlComponent.errors(); - for (const QQmlError& error : errorList) { - qCWarning(m_logger) << error.url() << error.line() << error; + QObject* createdComp = qmlComponent.create(); + + const std::shared_ptr controller = + std::shared_ptr( + qobject_cast(createdComp)); + + if (controller) { + qmlComponent.setInitialProperties(controller.get(), + QVariantMap{{"controllerId", + m_pController ? m_pController->getName() + : QString{}}, + {"debugMode", m_logger().isDebugEnabled()}}); + qmlComponent.completeCreate(); + if (qmlComponent.isError()) { + const QList errorList = qmlComponent.errors(); + for (const QQmlError& error : errorList) { + qCWarning(m_logger) << error.url() << error.line() << error; + } + delete createdComp; + return false; } - return nullptr; + m_mixxxController.append(controller); + return true; } - std::shared_ptr rootItem = - std::shared_ptr(qobject_cast(pRootObject)); - if (!rootItem) { - qWarning("run: Not a QQuickItem"); - delete pRootObject; - return nullptr; + qDebug() << qmlScript.file.absoluteFilePath() << "is not a MixxxController"; + + std::shared_ptr quickItem = + std::shared_ptr(qobject_cast(createdComp)); + + if (!quickItem) { + qWarning("run: Component is neither a MixxxController not a QQuickItem"); + delete createdComp; + return false; } - watchFilePath(qmlScript.file.absoluteFilePath()); + bool sceneBindingHasFailure = false; + + QString identifier = ""; + + if (qmlScript.identifier.isEmpty()) { + while (!availableScreens.isEmpty()) { + identifier = availableScreens.firstKey(); + } + } else { + identifier = qmlScript.identifier; + } + + const auto pScreen = availableScreens.take(identifier); + if (!availableScreens.contains(qmlScript.identifier)) { + qCCritical(m_logger) << "Not screen" << qmlScript.identifier << "found!"; + delete createdComp; + return false; + } + + qmlComponent.setInitialProperties(quickItem.get(), + QVariantMap{{"screenId", pScreen->info().identifier}}); + qmlComponent.completeCreate(); + + if (qmlComponent.isError() || !bindSceneToScreen(quickItem, identifier, pScreen)) { + if (!availableScreens.isEmpty()) { + qCWarning(m_logger) + << "Found screen with no QML scene able to run on it. Ignoring" + << availableScreens.size() << "screens"; + + while (!availableScreens.isEmpty()) { + VERIFY_OR_DEBUG_ASSERT(!pScreen->isValid() || + !pScreen->isRunning() || pScreen->stop()) { + qCWarning(m_logger) << "Unable to stop the screen"; + } + } + } + + delete createdComp; + return false; + } // The root item is ready. Associate it with the window. if (!m_bTesting) { - rootItem->setParentItem(pScreen->quickWindow()->contentItem()); + quickItem->setParentItem(pScreen->quickWindow()->contentItem()); - rootItem->setWidth(pScreen->quickWindow()->width()); - rootItem->setHeight(pScreen->quickWindow()->height()); + quickItem->setWidth(pScreen->quickWindow()->width()); + quickItem->setHeight(pScreen->quickWindow()->height()); } - return rootItem; + return true; } #endif diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h index b0605f4b584e..93f788df8e03 100644 --- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h @@ -6,10 +6,13 @@ #include #ifdef MIXXX_USE_QML #include + +#include "qml/mixxxscreen.h" #endif #include "controllers/legacycontrollermapping.h" #include "controllers/scripting/controllerscriptenginebase.h" +#include "qml/mixxxcontroller.h" #ifdef MIXXX_USE_QML class QQuickItem; @@ -79,14 +82,15 @@ class ControllerScriptEngineLegacy : public ControllerScriptEngineBase { bool evaluateScriptFile(const QFileInfo& scriptFile); #ifdef MIXXX_USE_QML bool bindSceneToScreen( - const LegacyControllerMapping::ScriptFileInfo& qmlFile, + const std::shared_ptr pScene, const QString& screenIdentifier, std::shared_ptr pScreen); void extractTransformFunction(const QMetaObject* metaObject, const QString& screenIdentifier); - std::shared_ptr loadQMLFile( + bool instanciateQMLComponent( const LegacyControllerMapping::ScriptFileInfo& qmlScript, - std::shared_ptr pScreen); + QMap> + availableScreens); struct TransformScreenFrameFunction { QMetaMethod method; @@ -111,6 +115,7 @@ class ControllerScriptEngineLegacy : public ControllerScriptEngineBase { QJSValue m_makeArrayBufferWrapperFunction; QList m_scriptFunctionPrefixes; #ifdef MIXXX_USE_QML + QList> m_mixxxController; QHash> m_renderingScreens; // Contains all the scenes loaded for this mapping. Key is the scene // identifier (LegacyControllerMapping::ScreenInfo::identifier), value in diff --git a/src/qml/mixxxcontroller.cpp b/src/qml/mixxxcontroller.cpp new file mode 100644 index 000000000000..f67432aa190a --- /dev/null +++ b/src/qml/mixxxcontroller.cpp @@ -0,0 +1,48 @@ +#include "mixxxcontroller.h" + +namespace mixxx { +namespace qml { + +void MixxxController::componentComplete() { + QObject::connect(this, + &MixxxController::init, + this, + &MixxxController::initChildrenComponents); + QObject::connect(this, + &MixxxController::shutdown, + this, + &MixxxController::shutdownChildrenComponents); +} + +void MixxxController::initChildrenComponents() { + for (auto* childComponent : m_children) { + const auto* const meta = childComponent->metaObject(); + const auto idx = meta->indexOfMethod(kInitSignature); + if (idx >= 0) { + const auto method = meta->method(idx); + if (method.isValid()) { + method.invoke(childComponent, Qt::DirectConnection); + } else { + // TODO: log? + } + } + } +} + +void MixxxController::shutdownChildrenComponents() { + for (auto* childComponent : m_children) { + const auto* const meta = childComponent->metaObject(); + const auto idx = meta->indexOfMethod(kShutdownSignature); + if (idx >= 0) { + const auto method = meta->method(idx); + if (method.isValid()) { + method.invoke(childComponent, Qt::DirectConnection); + } else { + // TODO: log? + } + } + } +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/mixxxcontroller.h b/src/qml/mixxxcontroller.h new file mode 100644 index 000000000000..925059eb2443 --- /dev/null +++ b/src/qml/mixxxcontroller.h @@ -0,0 +1,50 @@ +#ifndef MIXXX_MIXXXCONTROLLER_H +#define MIXXX_MIXXXCONTROLLER_H + +#include + +#include +#include + +namespace mixxx { +namespace qml { + +class MixxxController : public QObject, public QQmlParserStatus { + Q_OBJECT + QML_ELEMENT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(QString controllerId MEMBER m_controllerId) + Q_PROPERTY(bool debugMode MEMBER m_debugMode) + Q_PROPERTY(QQmlListProperty childComponents MEMBER m_pChildren) + + Q_CLASSINFO("DefaultProperty", "childComponents") + + public: + explicit MixxxController(QObject* parent = nullptr) + : QObject(parent), m_pChildren(this, &m_children){}; + void classBegin() override{}; + void componentComplete() override; + + signals: + void init(); + void shutdown(); + + private: + QString m_controllerId; + bool m_debugMode; + QList m_children; + QQmlListProperty m_pChildren; + + static inline QByteArray kInitSignature = QMetaObject::normalizedSignature("init()"); + static inline QByteArray kShutdownSignature = QMetaObject::normalizedSignature("shutdown()"); + + private slots: + void initChildrenComponents(); + void shutdownChildrenComponents(); +}; + +} // namespace qml +} // namespace mixxx + +#endif // MIXXX_MIXXXCONTROLLER_H diff --git a/src/qml/mixxxscreen.cpp b/src/qml/mixxxscreen.cpp new file mode 100644 index 000000000000..3bd7fe7adfaa --- /dev/null +++ b/src/qml/mixxxscreen.cpp @@ -0,0 +1,90 @@ +#include "mixxxscreen.h" + +#include "controllers/scripting/controllerscriptenginebase.h" +#include "util/assert.h" + +namespace mixxx { +namespace qml { + +void MixxxScreen::componentComplete() { + auto* const context = QQmlEngine::contextForObject(this); + VERIFY_OR_DEBUG_ASSERT(context) { + return; + } + + m_engine = context->engine(); + const MixxxControllerEngineInterface controllerEngineInterface = + qobject_cast( + m_engine->property("controllerEngineInterface")); + VERIFY_OR_DEBUG_ASSERT(controllerEngineInterface) { + return; + } + controllerEngineInterface.declareScreen(this); +} + +QString MixxxScreen::screenId() { + return m_screenId; +} + +int MixxxScreen::width() { + return m_size.width(); +} + +void MixxxScreen::setWidth(int value) { + m_size = QSize(value, m_size.height()); +} + +int MixxxScreen::height() { + return m_size.height(); +} + +void MixxxScreen::setHeight(int value) { + m_size = QSize(m_size.width(), value); +} + +uint MixxxScreen::splashOff() { + return m_splashOff.count(); +} + +void MixxxScreen::setSplashOff(uint value) { + m_splashOff = std::chrono::milliseconds(value); +} + +QJSValue MixxxScreen::jsTransformFrame() { + return m_transformFunc; +} + +void MixxxScreen::setJsTransformFrame(QJSValue value) { + if (!value.isCallable()) { + qWarning() << "transformFrame is not a valid function"; + return; + } + + m_transformFunc = value; + emit jsTransformFrameChanged(); +} + +const std::unique_ptr MixxxScreen::transform( + const QByteArray& frame, QDateTime timestamp, QRect area) { + VERIFY_OR_DEBUG_ASSERT(m_engine) { + return std::make_unique(frame); + } + + if (m_transformFunc.isUndefined()) { // Default implementation + return std::make_unique(frame); + } + const auto transformResult = + m_transformFunc.call(QJSValueList{m_engine->toScriptValue(frame), + m_engine->toScriptValue(timestamp), + m_engine->toScriptValue(area)}); + if (!transformResult.isVariant()) { + qWarning() << "transformFrame did not return a valid result"; + return std::make_unique(frame); + } + const auto result = transformResult.toVariant().toByteArray(); + qDebug() << "transformFrame returned a result of size" << result.length(); + return std::make_unique(result); +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/mixxxscreen.h b/src/qml/mixxxscreen.h new file mode 100644 index 000000000000..6aab3e914c1e --- /dev/null +++ b/src/qml/mixxxscreen.h @@ -0,0 +1,106 @@ +// +// Created by augier on 15/07/24. +// + +#ifndef MIXXX_MIXXXSCREEN_H +#define MIXXX_MIXXXSCREEN_H + +#include + +#include +#include +#include +#include + +#include "mixxxcontroller.h" + +namespace mixxx { +namespace qml { + +class MixxxScreen : public QObject, public QQmlParserStatus { + Q_OBJECT + QML_ELEMENT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(QString screenId READ screenId MEMBER m_screenId REQUIRED) + Q_PROPERTY(int width READ width WRITE setWidth) + Q_PROPERTY(int height READ height WRITE setHeight) + Q_PROPERTY(uint targetFps MEMBER m_targetFps) + Q_PROPERTY(uint msaa MEMBER m_msaa) + Q_PROPERTY(uint splashOff READ splashOff WRITE setSplashOff) + Q_PROPERTY(QImage::Format pixelType MEMBER m_pixelType) + Q_PROPERTY(ColorEndian endian MEMBER m_endian) + Q_PROPERTY(bool reversedColor MEMBER m_reversedColor) + Q_PROPERTY(bool rawData MEMBER m_rawData) + Q_PROPERTY(QJSValue transformFrame READ jsTransformFrame WRITE + setJsTransformFrame NOTIFY jsTransformFrameChanged) + + Q_PROPERTY(QQuickItem* item MEMBER m_item) + + Q_CLASSINFO("DefaultProperty", "item") + + public: + enum class ColorEndian { + Big = static_cast(std::endian::big), + Little = static_cast(std::endian::little), + }; + Q_ENUM(ColorEndian) + + explicit MixxxScreen(MixxxController* parent = nullptr) + : QObject(parent) { + } + + void classBegin() override{}; + void componentComplete() override; + + QString screenId(); + int width(); + void setWidth(int value); + int height(); + void setHeight(int value); + uint splashOff(); + void setSplashOff(uint value); + QJSValue jsTransformFrame(); + void setJsTransformFrame(QJSValue value); + const std::unique_ptr transform( + const QByteArray& frame, QDateTime timestamp, QRect area); + + signals: + void init(); + void shutdown(); + void jsTransformFrameChanged(); + + private: + QQmlEngine* m_engine = nullptr; + // The screen identifier. + QString m_screenId; + // The size of the screen. + QSize m_size = QSize(0, 0); + // The maximum FPS to render. + uint m_targetFps = 30; + // The MSAA value to use for render. + uint m_msaa = 1; + // The rendering grace time given when the + // screen is requested to shutdown. + std::chrono::milliseconds m_splashOff = std::chrono::milliseconds( + 3000); + // The pixel encoding format. + QImage::Format m_pixelType = + QImage::Format_RGB888; + // The pixel endian format. + ColorEndian m_endian = ColorEndian::Little; + // Whether or not the RGB is swapped BGR. + bool m_reversedColor = false; + // Whether or not the screen is allowed to receive bare data, + // not transformed. + bool m_rawData = false; + // The item to render + QQuickItem* m_item; + // Transform function + QJSValue m_transformFunc; +}; + +} // namespace qml +} // namespace mixxx + +#endif // MIXXX_MIXXXSCREEN_H