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