diff --git a/.github/workflows/build-checks.yml b/.github/workflows/build-checks.yml
index c40f5653de04..63453b11df75 100644
--- a/.github/workflows/build-checks.yml
+++ b/.github/workflows/build-checks.yml
@@ -94,6 +94,7 @@ jobs:
-DCMAKE_BUILD_TYPE=Debug \
-DOPTIMIZE=off \
-DQT6=ON \
+ -DQML=ON \
-DCOVERAGE=ON \
-DWARNINGS_FATAL=OFF \
-DDEBUG_ASSERTIONS_FATAL=OFF \
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e09a15cdec8b..dbbc428761e6 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -47,6 +47,7 @@ jobs:
-DMACOS_BUNDLE=ON
-DMODPLUG=ON
-DQT6=ON
+ -DQML=OFF
-DWAVPACK=ON
-DVCPKG_TARGET_TRIPLET=x64-osx-min1100-release
-DVCPKG_DEFAULT_HOST_TRIPLET=x64-osx-min1100-release
@@ -71,6 +72,7 @@ jobs:
-DMACOS_BUNDLE=ON
-DMODPLUG=ON
-DQT6=ON
+ -DQML=OFF
-DWAVPACK=ON
-DVCPKG_TARGET_TRIPLET=arm64-osx-min1100-release
-DVCPKG_DEFAULT_HOST_TRIPLET=x64-osx-min1100-release
@@ -101,6 +103,7 @@ jobs:
-DMEDIAFOUNDATION=ON
-DMODPLUG=ON
-DQT6=ON
+ -DQML=OFF
-DWAVPACK=ON
-DVCPKG_TARGET_TRIPLET=x64-windows-release
-DVCPKG_DEFAULT_HOST_TRIPLET=x64-windows-release
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 52e1265a36be..86dced62515b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1494,6 +1494,13 @@ if (NOT QML)
src/control/controlmodel.cpp
src/control/controlsortfiltermodel.cpp
)
+else()
+ target_sources(mixxx-lib PRIVATE
+ # The following source depends of QML being available but aren't part of the new QML UI
+ src/controllers/rendering/controllerrenderingengine.cpp
+ src/controllers/controllerenginethreadcontrol.cpp
+ src/controllers/controllerscreenpreview.cpp
+ )
endif()
if(QOPENGL)
target_sources(mixxx-lib PRIVATE
@@ -2176,7 +2183,14 @@ add_executable(mixxx-test
src/test/wwidgetstack_test.cpp
src/test/waveform_upgrade_test.cpp
src/util/moc_included_test.cpp
+ src/test/helpers/log_test.cpp
)
+if (QML)
+ target_sources(mixxx-test PRIVATE
+ src/test/controller_mapping_file_handler_test.cpp
+ src/test/controllerrenderingengine_test.cpp
+ )
+endif()
find_package(GTest CONFIG REQUIRED)
set_target_properties(mixxx-test PROPERTIES AUTOMOC ON)
target_link_libraries(mixxx-test PRIVATE mixxx-lib mixxx-gitinfostore GTest::gtest GTest::gmock)
diff --git a/res/controllers/Dummy Device Screen.hid.xml b/res/controllers/Dummy Device Screen.hid.xml
new file mode 100644
index 000000000000..90526cc1a33f
--- /dev/null
+++ b/res/controllers/Dummy Device Screen.hid.xml
@@ -0,0 +1,22 @@
+
+
+
+ Dummy Device (Screens)
+ A. Colombier
+ Dummy device screens
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/controllers/DummyDeviceDefaultScreen.qml b/res/controllers/DummyDeviceDefaultScreen.qml
new file mode 100755
index 000000000000..c216f2259d6f
--- /dev/null
+++ b/res/controllers/DummyDeviceDefaultScreen.qml
@@ -0,0 +1,305 @@
+import QtQuick 2.15
+import QtQuick.Window 2.3
+
+import QtQuick.Controls 2.15
+import QtQuick.Shapes 1.11
+import QtQuick.Layouts 1.3
+import QtQuick.Window 2.15
+
+import Qt5Compat.GraphicalEffects
+
+import Mixxx 1.0 as Mixxx
+import Mixxx.Controls 1.0 as MixxxControls
+
+import "." as Skin
+
+Item {
+ id: root
+
+ required property string screenId
+ property color fontColor: Qt.rgba(242/255,242/255,242/255, 1)
+ property color smallBoxBorder: Qt.rgba(44/255,44/255,44/255, 1)
+
+ property string group: "[Channel1]"
+ property var deckPlayer: Mixxx.PlayerManager.getPlayer(root.group)
+
+ function init(controlerName, isDebug) {
+ console.log(`Screen ${root.screenId} has started`)
+ switch (root.screenId) {
+ case "jog":
+ loader.sourceComponent = jog
+ break;
+ default:
+ loader.sourceComponent = main
+ }
+ }
+
+ function shutdown() {
+ console.log(`Screen ${root.screenId} is stopping`)
+ loader.sourceComponent = splash
+ }
+
+ // function transformFrame(input: ArrayBuffer, timestamp: date) {
+ function transformFrame(input, timestamp) {
+ return new ArrayBuffer(0);
+ }
+
+ Mixxx.ControlProxy {
+ group: root.group
+ key: "track_loaded"
+
+ onValueChanged: (value) => {
+ deckPlayer = Mixxx.PlayerManager.getPlayer(root.group)
+ }
+ }
+
+ Timer {
+ id: channelchange
+
+ interval: 2000
+ repeat: true
+ running: true
+
+ onTriggered: {
+ root.group = root.group === "[Channel1]" ? "[Channel2]" : "[Channel1]"
+ deckPlayer = Mixxx.PlayerManager.getPlayer(root.group)
+ }
+ }
+
+ Component {
+ id: splash
+ Rectangle {
+ color: "black"
+ anchors.fill: parent
+ Image {
+ anchors.fill: parent
+ fillMode: Image.PreserveAspectFit
+ source: "../images/templates/logo_mixxx.png"
+ }
+ }
+ }
+
+ Component {
+ id: jog
+
+ Rectangle {
+ anchors.fill: parent
+ color: "black"
+
+ Image {
+ id: artwork
+ anchors.fill: parent
+ visible: deckPlayer.trackLocationUrl.toString().length !== 0
+
+ source: deckPlayer.coverArtUrl ?? "../images/templates/logo_mixxx.png"
+ height: 100
+ width: 100
+ fillMode: Image.PreserveAspectFit
+ }
+
+ Text {
+ visible: deckPlayer.trackLocationUrl.toString().length === 0
+
+ text: qsTr("No Track Loaded")
+ font.pixelSize: 12
+ font.family: "Noto Sans"
+ font.letterSpacing: -1
+ color: "white"
+ }
+ }
+ }
+
+ Component {
+ id: main
+
+ Rectangle {
+ id: debugValue
+ anchors.fill: parent
+ color: 'black'
+
+ antialiasing: true
+
+ ColumnLayout {
+ id: column
+ anchors.fill: parent
+ anchors.leftMargin: 0
+ anchors.rightMargin: 0
+ anchors.topMargin: 0
+ anchors.bottomMargin: 0
+ spacing: 6
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 0
+
+ Repeater {
+ id: debugColor
+
+ model: [
+ "black",
+ "white",
+ "red",
+ "green",
+ "blue",
+ Qt.rgba(0, 1, 1),
+ ]
+
+ Rectangle {
+ required property var modelData
+
+ color: modelData
+ Layout.fillWidth: true
+ height: 80
+ }
+ }
+ }
+
+ RowLayout {
+ anchors.leftMargin: 6
+ anchors.rightMargin: 6
+ anchors.topMargin: 6
+ anchors.bottomMargin: 6
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ spacing: 6
+
+ Rectangle {
+ color: 'transparent'
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Text {
+ text: qsTr("Group")
+ font.pixelSize: 24
+ font.family: "Noto Sans"
+ font.letterSpacing: -1
+ color: fontColor
+ }
+ }
+
+ Rectangle {
+ color: 'transparent'
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Text {
+ text: `${root.group}`
+ font.pixelSize: 24
+ font.family: "Noto Sans"
+ font.letterSpacing: -1
+ color: fontColor
+ }
+ }
+ }
+
+ RowLayout {
+ anchors.leftMargin: 6
+ anchors.rightMargin: 6
+ anchors.topMargin: 6
+ anchors.bottomMargin: 6
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ spacing: 6
+
+ Rectangle {
+ color: 'transparent'
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Text {
+ text: qsTr("Widget")
+ font.pixelSize: 24
+ font.family: "Noto Sans"
+ font.letterSpacing: -1
+ color: fontColor
+ }
+ }
+
+ Rectangle {
+ color: 'transparent'
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ Skin.HotcueButton {
+ anchors.fill: parent
+
+ hotcueNumber: 1
+ group: root.group
+ }
+ }
+ }
+
+ Repeater {
+ model: [{
+ controllerKey: "beatloop_size",
+ title: "Beatloop Size"
+ }, {
+ controllerKey: "track_samples",
+ title: "Track sample"
+ }, {
+ controllerKey: "track_samplerate",
+ title: "Track sample rate"
+ }, {
+ controllerKey: "playposition",
+ title: "Play position"
+ }, {
+ controllerKey: "rate_ratio",
+ title: "Rate ratio"
+ }, {
+ controllerKey: "waveform_zoom",
+ title: "Waveform zoom"
+ }
+ ]
+
+ RowLayout {
+ id: row
+ anchors.leftMargin: 6
+ anchors.rightMargin: 6
+ anchors.topMargin: 6
+ anchors.bottomMargin: 6
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ spacing: 6
+ required property var modelData
+
+ Mixxx.ControlProxy {
+ id: mixxxValue
+ group: root.group
+ key: modelData.controllerKey
+ }
+
+ Rectangle {
+ color: 'transparent'
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Text {
+ text: qsTr(modelData.title)
+ font.pixelSize: 24
+ font.family: "Noto Sans"
+ font.letterSpacing: -1
+ color: fontColor
+ }
+ }
+
+ Rectangle {
+ color: 'transparent'
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Text {
+ text: `${mixxxValue.value}`
+ font.pixelSize: 24
+ font.family: "Noto Sans"
+ font.letterSpacing: -1
+ color: fontColor
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ Loader {
+ id: loader
+ anchors.fill: parent
+ sourceComponent: splash
+ }
+}
diff --git a/src/controllers/controller.cpp b/src/controllers/controller.cpp
index 61a4bf8c0025..9cca33f14ad2 100644
--- a/src/controllers/controller.cpp
+++ b/src/controllers/controller.cpp
@@ -44,11 +44,12 @@ void Controller::startEngine()
qCWarning(m_logBase) << "Controller: Engine already exists! Restarting:";
stopEngine();
}
- m_pScriptEngineLegacy = new ControllerScriptEngineLegacy(this, m_logBase);
- QObject::connect(m_pScriptEngineLegacy,
+ m_pScriptEngineLegacy = std::make_shared(this, m_logBase);
+ QObject::connect(m_pScriptEngineLegacy.get(),
&ControllerScriptEngineBase::beforeShutdown,
this,
&Controller::slotBeforeEngineShutdown);
+ emit engineStarted(m_pScriptEngineLegacy.get());
}
void Controller::stopEngine() {
@@ -57,12 +58,11 @@ void Controller::stopEngine() {
qCWarning(m_logBase) << "Controller::stopEngine(): No engine exists!";
return;
}
-
- delete m_pScriptEngineLegacy;
- m_pScriptEngineLegacy = nullptr;
+ m_pScriptEngineLegacy.reset();
+ emit engineStopped();
}
-bool Controller::applyMapping() {
+bool Controller::applyMapping(const QString& resourcePath) {
qCInfo(m_logBase) << "Applying controller mapping...";
const std::shared_ptr pMapping = cloneMapping();
@@ -84,6 +84,13 @@ bool Controller::applyMapping() {
m_pScriptEngineLegacy->setScriptFiles(scriptFiles);
m_pScriptEngineLegacy->setSettings(pMapping->getSettings());
+#ifdef MIXXX_USE_QML
+ m_pScriptEngineLegacy->setModulePaths(pMapping->getModules());
+ m_pScriptEngineLegacy->setInfoScreens(pMapping->getInfoScreens());
+ m_pScriptEngineLegacy->setResourcePath(resourcePath);
+#else
+ Q_UNUSED(resourcePath);
+#endif
return m_pScriptEngineLegacy->initialize();
}
diff --git a/src/controllers/controller.h b/src/controllers/controller.h
index 2c91e7d0ba7e..dcc7ba05032c 100644
--- a/src/controllers/controller.h
+++ b/src/controllers/controller.h
@@ -53,12 +53,22 @@ class Controller : public QObject {
return m_bLearning;
}
+ inline std::shared_ptr getScriptEngine() const {
+ return m_pScriptEngineLegacy;
+ }
+
virtual bool matchMapping(const MappingInfo& mapping) = 0;
signals:
/// Emitted when the controller is opened or closed.
void openChanged(bool bOpen);
+ /// Emitted when the controller has started a new engine.
+ void engineStarted(const ControllerScriptEngineLegacy* engine);
+
+ /// Emitted when the controller has stopped its engine.
+ void engineStopped();
+
// Making these slots protected/private ensures that other parts of Mixxx can
// only signal them which allows us to use no locks.
protected slots:
@@ -70,7 +80,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(const QString& resourcePath);
virtual void slotBeforeEngineShutdown();
// Puts the controller in and out of learning mode.
@@ -99,10 +109,6 @@ class Controller : public QObject {
// were required to specify it.
virtual void send(const QList& data, unsigned int length = 0);
- // This must be reimplemented by sub-classes desiring to send raw bytes to a
- // controller.
- virtual void sendBytes(const QByteArray& data) = 0;
-
// To be called in sub-class' open() functions after opening the device but
// before starting any input polling/processing.
virtual void startEngine();
@@ -114,9 +120,6 @@ class Controller : public QObject {
// To be called when receiving events
void triggerActivity();
- inline ControllerScriptEngineLegacy* getScriptEngine() const {
- return m_pScriptEngineLegacy;
- }
inline void setDeviceCategory(const QString& deviceCategory) {
m_sDeviceCategory = deviceCategory;
}
@@ -136,6 +139,11 @@ class Controller : public QObject {
const RuntimeLoggingCategory m_logInput;
const RuntimeLoggingCategory m_logOutput;
+ public:
+ // This must be reimplemented by sub-classes desiring to send raw bytes to a
+ // controller.
+ virtual void sendBytes(const QByteArray& data) = 0;
+
private: // but used by ControllerManager
virtual int open() = 0;
@@ -151,7 +159,9 @@ class Controller : public QObject {
}
private:
- ControllerScriptEngineLegacy* m_pScriptEngineLegacy;
+ /// Controllers have multiple ownership over an engine.
+ /// A ControllerScriptEngine must not own a controller.
+ std::shared_ptr m_pScriptEngineLegacy;
// Verbose and unique description of device type, defaults to empty
QString m_sDeviceCategory;
diff --git a/src/controllers/controllerenginethreadcontrol.cpp b/src/controllers/controllerenginethreadcontrol.cpp
new file mode 100644
index 000000000000..24afdd78ed5e
--- /dev/null
+++ b/src/controllers/controllerenginethreadcontrol.cpp
@@ -0,0 +1,97 @@
+#include "controllers/controllerenginethreadcontrol.h"
+
+#include
+
+#include "moc_controllerenginethreadcontrol.cpp"
+#include "util/assert.h"
+#include "util/logger.h"
+#include "util/mutex.h"
+#include "util/thread_affinity.h"
+
+namespace {
+const mixxx::Logger kLogger("ControllerEngineThreadControl");
+constexpr int kMaxPauseDurationMilliseconds = 1000;
+} // namespace
+
+ControllerEngineThreadControl::ControllerEngineThreadControl(QObject* parent)
+ : QObject(parent) {
+}
+bool ControllerEngineThreadControl::pause() {
+ VERIFY_OR_DEBUG_ASSERT_THIS_QOBJECT_THREAD_ANTI_AFFINITY() {
+ return false;
+ }
+ const auto lock = lockMutex(&m_pauseMutex);
+ m_pauseCount++;
+
+ if (m_canPause && !m_isPaused) {
+ emit pauseRequested();
+ }
+
+ while (m_canPause && !m_isPaused) {
+ if (!m_isPausedCondition.wait(&m_pauseMutex, kMaxPauseDurationMilliseconds)) {
+ kLogger.warning() << "Pause request timed out!";
+ m_pauseCount--;
+ return false;
+ }
+ }
+ return !m_canPause || m_isPaused;
+}
+void ControllerEngineThreadControl::resume() {
+ VERIFY_OR_DEBUG_ASSERT_THIS_QOBJECT_THREAD_ANTI_AFFINITY() {
+ return;
+ }
+ const auto lock = lockMutex(&m_pauseMutex);
+ if (m_pauseCount > 0) {
+ m_pauseCount--;
+ }
+ m_isPaused = m_pauseCount > 0;
+ m_isPausedCondition.wakeOne();
+}
+void ControllerEngineThreadControl::setCanPause(bool canPause) {
+ DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY();
+ auto lock = lockMutex(&m_pauseMutex);
+ m_canPause = canPause;
+
+ if (m_canPause) {
+ connect(this,
+ &ControllerEngineThreadControl::pauseRequested,
+ this,
+ &ControllerEngineThreadControl::doPause,
+ Qt::UniqueConnection);
+ } else {
+ // New signals may have been queued emitted requesting for pause, so we
+ // manually process the event loop now to clear and handle those, before
+ // disabling pausing. Without this, thread requesting pause will stay
+ // stuck waiting on the condvar.
+ lock.unlock();
+ QCoreApplication::processEvents();
+ lock.relock();
+
+ disconnect(this,
+ &ControllerEngineThreadControl::pauseRequested,
+ this,
+ &ControllerEngineThreadControl::doPause);
+
+ m_isPaused = false;
+ m_pauseCount = 0;
+ m_isPausedCondition.wakeOne();
+ }
+}
+void ControllerEngineThreadControl::doPause() {
+ VERIFY_OR_DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY() {
+ return;
+ }
+ const auto lock = lockMutex(&m_pauseMutex);
+ m_isPaused = m_pauseCount > 0;
+ m_isPausedCondition.wakeOne();
+
+ while (m_canPause && m_isPaused) {
+ VERIFY_OR_DEBUG_ASSERT(m_isPausedCondition.wait(
+ &m_pauseMutex, kMaxPauseDurationMilliseconds)) {
+ kLogger.warning() << "Engine pause timed out!";
+ m_isPaused = false;
+ m_pauseCount = 0;
+ };
+ }
+ m_isPausedCondition.wakeAll();
+}
diff --git a/src/controllers/controllerenginethreadcontrol.h b/src/controllers/controllerenginethreadcontrol.h
new file mode 100644
index 000000000000..ae5cba333f7e
--- /dev/null
+++ b/src/controllers/controllerenginethreadcontrol.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include
+#include
+#include
+
+/// @brief A ControllerEngineThreadControl can be used to orchestrate thread
+/// pausing on a ControllerScriptEngineBase. Pausing is required by the
+/// rendering thread (https://doc.qt.io/qt-6/qquickrendercontrol.html#sync).
+/// This offscreen render thread is used to pause the main "GUI thread" for onboard
+/// screens.
+/// The Qt documentation isn't completely clear about this, but after
+/// testing, it appears that the "GUI main thread" is the thread where the QML
+/// engine lives in (also the main thread if we were using a
+/// QMLApplication, which isn't the case here)
+class ControllerEngineThreadControl : public QObject {
+ Q_OBJECT
+ public:
+ explicit ControllerEngineThreadControl(QObject* parent = nullptr);
+
+ public slots:
+ // The following slots may be used by the rendering engine to pause the thread.
+ // They must be called from a different thread than ControllerEngineThreadControl's.
+ bool pause();
+ void resume();
+
+ // Change whether or not it is possible to pause the thread. Should be
+ // called from the same thread as ControllerEngineThreadControl.
+ void setCanPause(bool canPause);
+ private slots:
+ // Used to effectively pause the thread. Must be called from the same thread
+ // as ControllerEngineThreadControl.
+ void doPause();
+
+ signals:
+ void pauseRequested();
+
+ private:
+ QWaitCondition m_isPausedCondition;
+ QMutex m_pauseMutex;
+ int m_pauseCount{0};
+ bool m_isPaused{false};
+ bool m_canPause{false};
+};
diff --git a/src/controllers/controllermanager.cpp b/src/controllers/controllermanager.cpp
index 7dbac102ef32..073ee897cbb5 100644
--- a/src/controllers/controllermanager.cpp
+++ b/src/controllers/controllermanager.cpp
@@ -300,7 +300,7 @@ void ControllerManager::slotSetUpDevices() {
qWarning() << "There was a problem opening" << name;
continue;
}
- pController->applyMapping();
+ pController->applyMapping(m_pConfig->getResourcePath());
}
pollIfAnyControllersOpen();
@@ -392,7 +392,7 @@ void ControllerManager::openController(Controller* pController) {
// If successfully opened the device, apply the mapping and save the
// preference setting.
if (result == 0) {
- pController->applyMapping();
+ pController->applyMapping(m_pConfig->getResourcePath());
// Update configuration to reflect controller is enabled.
m_pConfig->setValue(
diff --git a/src/controllers/controllerscreenpreview.cpp b/src/controllers/controllerscreenpreview.cpp
new file mode 100644
index 000000000000..30b655317bb1
--- /dev/null
+++ b/src/controllers/controllerscreenpreview.cpp
@@ -0,0 +1,69 @@
+#include "controllers/controllerscreenpreview.h"
+
+#include
+#include
+
+#include "moc_controllerscreenpreview.cpp"
+#include "util/time.h"
+
+namespace {
+/// Number of sample frame timestamp sample to perform a smooth average FPS label.
+constexpr double kFrameSmoothAverageFactor = 20;
+} // namespace
+
+using Clock = std::chrono::steady_clock;
+
+ControllerScreenPreview::ControllerScreenPreview(
+ QWidget* parent, const LegacyControllerMapping::ScreenInfo& screen)
+ : QWidget(parent),
+ m_screenInfo(screen),
+ m_pFrame(make_parented(this)),
+ m_pStat(make_parented(tr("FPS: n/a"), this)) {
+ m_pFrame->setFixedSize(screen.size);
+ setMaximumWidth(screen.size.width());
+ m_pStat->setAlignment(Qt::AlignRight);
+ auto pLayout = make_parented(this);
+ auto* pBottomLayout = new QHBoxLayout();
+ pLayout->addWidget(m_pFrame);
+ pBottomLayout->addWidget(make_parented(
+ QStringLiteral("\"%0\"")
+ .arg(m_screenInfo.identifier.isEmpty()
+ ? tr("Unnamed")
+ : m_screenInfo.identifier),
+ this));
+ pBottomLayout->addWidget(m_pStat);
+ pLayout->addLayout(pBottomLayout);
+ pLayout->addSpacerItem(new QSpacerItem(
+ 1, 40, QSizePolicy::Minimum, QSizePolicy::Expanding));
+}
+void ControllerScreenPreview::updateFrame(
+ const LegacyControllerMapping::ScreenInfo& screen, const QImage& frame) {
+ if (m_screenInfo.identifier != screen.identifier) {
+ return;
+ }
+ m_pFrame->setPixmap(QPixmap::fromImage(frame));
+
+ auto currentTimestamp = Clock::now();
+ if (m_lastFrameTimestamp == Clock::time_point()) {
+ m_lastFrameTimestamp = currentTimestamp;
+ return;
+ }
+
+ if (m_averageFrameDuration == 0) {
+ m_averageFrameDuration =
+ std::chrono::duration_cast(
+ currentTimestamp - m_lastFrameTimestamp)
+ .count();
+ } else {
+ m_averageFrameDuration = std::lerp(m_averageFrameDuration,
+ std::chrono::duration_cast(
+ currentTimestamp - m_lastFrameTimestamp)
+ .count(),
+ 1.0 / kFrameSmoothAverageFactor);
+ }
+ m_lastFrameTimestamp = currentTimestamp;
+ m_pStat->setText(tr("FPS: %0/%1")
+ .arg(QString::number(static_cast(
+ 1000000 / m_averageFrameDuration)),
+ QString::number(m_screenInfo.target_fps)));
+}
diff --git a/src/controllers/controllerscreenpreview.h b/src/controllers/controllerscreenpreview.h
new file mode 100644
index 000000000000..6c4ef24784a0
--- /dev/null
+++ b/src/controllers/controllerscreenpreview.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "controllers/scripting/legacy/controllerscriptenginelegacy.h"
+#include "util/duration.h"
+#include "util/parented_ptr.h"
+
+/// Widget to preview controller screen, used in preference window. This is
+/// useful to help when developing new screen layout, without inducing any wear
+/// and tear on a hardware device, or allow testing when not owning a device
+/// using onboard screens. This can also be used to provide debugging
+/// information as user can easily take a screenshot of what they see on the
+/// device.
+class ControllerScreenPreview : public QWidget {
+ Q_OBJECT
+ public:
+ ControllerScreenPreview(QWidget* parent,
+ const LegacyControllerMapping::ScreenInfo& screen);
+ public slots:
+ void updateFrame(const LegacyControllerMapping::ScreenInfo& screen, const QImage& frame);
+
+ private:
+ LegacyControllerMapping::ScreenInfo m_screenInfo;
+
+ parented_ptr m_pFrame;
+ parented_ptr m_pStat;
+
+ double m_averageFrameDuration;
+ using Clock = std::chrono::steady_clock;
+ Clock::time_point m_lastFrameTimestamp;
+};
diff --git a/src/controllers/defs_controllers.h b/src/controllers/defs_controllers.h
index 7e3471671b97..5872f3953a86 100644
--- a/src/controllers/defs_controllers.h
+++ b/src/controllers/defs_controllers.h
@@ -25,5 +25,4 @@ inline QString userMappingsPath(UserSettingsPointer pConfig) {
#define HID_MAPPING_EXTENSION ".hid.xml"
#define MIDI_MAPPING_EXTENSION ".midi.xml"
#define BULK_MAPPING_EXTENSION ".bulk.xml"
-#define REQUIRED_SCRIPT_FILE "common-controller-scripts.js"
#define XML_SCHEMA_VERSION "1"
diff --git a/src/controllers/dlgprefcontroller.cpp b/src/controllers/dlgprefcontroller.cpp
index 49bf0788393c..d58fbf6b6731 100644
--- a/src/controllers/dlgprefcontroller.cpp
+++ b/src/controllers/dlgprefcontroller.cpp
@@ -13,13 +13,18 @@
#include "controllers/controllermappinginfoenumerator.h"
#include "controllers/controlleroutputmappingtablemodel.h"
#include "controllers/controlpickermenu.h"
+#ifdef MIXXX_USE_QML
+#include "controllers/controllerscreenpreview.h"
+#endif
#include "controllers/defs_controllers.h"
#include "controllers/dlgcontrollerlearning.h"
#include "controllers/midi/legacymidicontrollermapping.h"
#include "controllers/midi/midicontroller.h"
+#include "controllers/scripting/legacy/controllerscriptenginelegacy.h"
#include "defs_urls.h"
#include "moc_dlgprefcontroller.cpp"
#include "preferences/usersettings.h"
+#include "util/cmdlineargs.h"
#include "util/desktophelper.h"
#include "util/parented_ptr.h"
#include "util/string.h"
@@ -33,6 +38,29 @@ QString mappingNameToPath(const QString& directory, const QString& mappingName)
return directory + fileName + kMappingExt;
}
+const QString kBuiltinFileSuffix =
+ QStringLiteral(" (") + QObject::tr("built-in") + QStringLiteral(")");
+
+/// @brief Format a controller file to display attributes (system, missing) in the UI.
+/// @return The formatted string.
+QString formatFilePath(UserSettingsPointer pConfig,
+ QColor linkColor,
+ const QString& name,
+ const QFileInfo& file) {
+ QString systemMappingPath = resourceMappingsPath(pConfig);
+ QString scriptFileLink = coloredLinkString(
+ linkColor,
+ name,
+ file.absoluteFilePath());
+ if (!file.exists()) {
+ scriptFileLink +=
+ QStringLiteral(" (") + QObject::tr("missing") + QStringLiteral(")");
+ } else if (file.absoluteFilePath().startsWith(systemMappingPath)) {
+ scriptFileLink += kBuiltinFileSuffix;
+ }
+ return scriptFileLink;
+}
+
} // namespace
DlgPrefController::DlgPrefController(
@@ -109,6 +137,19 @@ DlgPrefController::DlgPrefController(
&ControllerManager::mappingApplied,
this,
&DlgPrefController::enableWizardAndIOTabs);
+#ifdef MIXXX_USE_QML
+ if (CmdlineArgs::Instance()
+ .getControllerPreviewScreens()) {
+ connect(m_pController,
+ &Controller::engineStarted,
+ this,
+ &DlgPrefController::slotShowPreviewScreens);
+ connect(m_pController,
+ &Controller::engineStopped,
+ this,
+ &DlgPrefController::slotClearPreviewScreens);
+ }
+#endif
// Open script file links
connect(m_ui.labelLoadedMappingScriptFileLinks,
@@ -379,33 +420,23 @@ QString DlgPrefController::mappingFileLinks(
return QString();
}
- const QString builtinFileSuffix = QStringLiteral(" (") + tr("built-in") + QStringLiteral(")");
- QString systemMappingPath = resourceMappingsPath(m_pConfig);
QStringList linkList;
- QString xmlFileLink = coloredLinkString(
+ linkList.append(formatFilePath(m_pConfig,
m_pLinkColor,
QFileInfo(pMapping->filePath()).fileName(),
- pMapping->filePath());
- if (pMapping->filePath().startsWith(systemMappingPath)) {
- xmlFileLink += builtinFileSuffix;
- }
- linkList << xmlFileLink;
-
+ QFileInfo(pMapping->filePath())));
for (const auto& script : pMapping->getScriptFiles()) {
- QString scriptFileLink = coloredLinkString(
+ linkList.append(formatFilePath(m_pConfig,
m_pLinkColor,
script.name,
- script.file.absoluteFilePath());
- if (!script.file.exists()) {
- scriptFileLink +=
- QStringLiteral(" (") + tr("missing") + QStringLiteral(")");
- } else if (script.file.absoluteFilePath().startsWith(
- systemMappingPath)) {
- scriptFileLink += builtinFileSuffix;
- }
-
- linkList << scriptFileLink;
+ QFileInfo(script.file.absoluteFilePath())));
+ }
+#ifdef MIXXX_USE_QML
+ for (const auto& qmlLibrary : pMapping->getModules()) {
+ auto fileInfo = QFileInfo(qmlLibrary.dirinfo.absoluteFilePath());
+ linkList.append(formatFilePath(m_pConfig, m_pLinkColor, fileInfo.fileName(), fileInfo));
}
+#endif
return linkList.join("
");
}
@@ -629,6 +660,7 @@ void DlgPrefController::slotMappingSelected(int chosenIndex) {
}
m_ui.groupBoxSettings->setVisible(false);
+ m_ui.groupBoxScreens->setVisible(false);
} else { // User picked a mapping
m_ui.chkEnabledDevice->setEnabled(true);
@@ -832,6 +864,43 @@ void DlgPrefController::initTableView(QTableView* pTable) {
pTable->setAlternatingRowColors(true);
}
+#ifdef MIXXX_USE_QML
+void DlgPrefController::slotShowPreviewScreens(
+ const ControllerScriptEngineLegacy* scriptEngine) {
+ QLayoutItem* pItem;
+ VERIFY_OR_DEBUG_ASSERT(m_ui.groupBoxScreens->layout()) {
+ return;
+ }
+ while ((pItem = m_ui.groupBoxScreens->layout()->takeAt(0)) != nullptr) {
+ delete pItem->widget();
+ delete pItem;
+ }
+
+ if (!m_pMapping) {
+ return;
+ }
+
+ m_ui.groupBoxScreens->setVisible(
+ scriptEngine != nullptr && !m_pMapping->getInfoScreens().empty());
+ if (!scriptEngine) {
+ return;
+ }
+
+ auto screens = m_pMapping->getInfoScreens();
+
+ for (const LegacyControllerMapping::ScreenInfo& screen : std::as_const(screens)) {
+ ControllerScreenPreview* pPreviewScreen =
+ new ControllerScreenPreview(m_ui.groupBoxScreens, screen);
+ m_ui.groupBoxScreens->layout()->addWidget(pPreviewScreen);
+
+ connect(scriptEngine,
+ &ControllerScriptEngineLegacy::previewRenderedScreen,
+ pPreviewScreen,
+ &ControllerScreenPreview::updateFrame);
+ }
+}
+#endif
+
void DlgPrefController::slotShowMapping(std::shared_ptr pMapping) {
m_ui.labelLoadedMapping->setText(mappingName(pMapping));
m_ui.labelLoadedMappingDescription->setText(mappingDescription(pMapping));
@@ -875,6 +944,19 @@ void DlgPrefController::slotShowMapping(std::shared_ptr
m_pMapping = pMapping;
}
+#ifdef MIXXX_USE_QML
+ if (pMapping &&
+ CmdlineArgs::Instance()
+ .getControllerPreviewScreens() &&
+ pMapping &&
+ !pMapping->getInfoScreens().isEmpty()) {
+ slotShowPreviewScreens(m_pController->getScriptEngine().get());
+ } else
+#endif
+ {
+ m_ui.groupBoxScreens->setVisible(false);
+ }
+
// Inputs tab
ControllerInputMappingTableModel* pInputModel =
new ControllerInputMappingTableModel(this,
diff --git a/src/controllers/dlgprefcontroller.h b/src/controllers/dlgprefcontroller.h
index 9a3fbd7e6ed4..292a0406afec 100644
--- a/src/controllers/dlgprefcontroller.h
+++ b/src/controllers/dlgprefcontroller.h
@@ -18,6 +18,9 @@ class ControllerOutputMappingTableModel;
class ControlPickerMenu;
class DlgControllerLearning;
class MappingInfoEnumerator;
+#ifdef MIXXX_USE_QML
+class ControllerScriptEngineLegacy;
+#endif
/// Configuration dialog for a single DJ controller
class DlgPrefController : public DlgPreferencePage {
@@ -61,6 +64,15 @@ class DlgPrefController : public DlgPreferencePage {
void slotStopLearning();
void enableWizardAndIOTabs(bool enable);
+#ifdef MIXXX_USE_QML
+ // Onboard screen controller.
+ void slotShowPreviewScreens(const ControllerScriptEngineLegacy* scriptEngine);
+ // Wrapper used on shutdown.
+ void slotClearPreviewScreens() {
+ slotShowPreviewScreens(nullptr);
+ }
+#endif
+
// Input mappings
void addInputMapping();
void showLearningWizard();
diff --git a/src/controllers/dlgprefcontrollerdlg.ui b/src/controllers/dlgprefcontrollerdlg.ui
index 9b296011d893..9e83f1b5cab9 100644
--- a/src/controllers/dlgprefcontrollerdlg.ui
+++ b/src/controllers/dlgprefcontrollerdlg.ui
@@ -6,8 +6,8 @@
0
0
- 507
- 437
+ 902
+ 591
@@ -26,116 +26,16 @@
0
+
+
+ 0
+ 0
+
+
Controller Setup
-
- -
-
-
-
- 0
- 0
-
-
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 50
- 50
-
-
-
-
- 50
- 50
-
-
-
- (icon)
-
-
-
- -
-
-
-
-
-
- (Warning message goes here)
-
-
- true
-
-
- true
-
-
- Qt::TextBrowserInteraction
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 16777215
- 16777215
-
-
-
- Load Mapping:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- comboBoxMapping
-
-
-
- -
-
-
- true
-
-
-
- 0
- 0
-
-
-
-
-
-
- (device category goes here)
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
+
-
@@ -152,73 +52,8 @@
- -
-
-
- Click to start the Controller Learning wizard.
-
-
-
-
-
- Learning Wizard (MIDI Only)
-
-
- false
-
-
- false
-
-
-
- -
-
-
- true
-
-
-
- 0
- 0
-
-
-
-
- 14
- 75
- true
-
-
-
- Controller Name
-
-
-
- -
-
-
- Enabled
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Expanding
-
-
-
- 20
- 40
-
-
-
-
-
-
+
Mapping Info
@@ -431,6 +266,31 @@
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ Load Mapping:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ comboBoxMapping
+
+
+
-
@@ -448,6 +308,160 @@
+ -
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Expanding
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ (device category goes here)
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ Enabled
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+ 14
+ 75
+ true
+
+
+
+ Controller Name
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 50
+ 50
+
+
+
+
+ 50
+ 50
+
+
+
+ (icon)
+
+
+
+ -
+
+
+
+
+
+ (Warning message goes here)
+
+
+ true
+
+
+ true
+
+
+ Qt::TextBrowserInteraction
+
+
+
+
+
+
+ -
+
+
+ Click to start the Controller Learning wizard.
+
+
+
+
+
+ Learning Wizard (MIDI Only)
+
+
+ false
+
+
+ false
+
+
+
+ -
+
+
+ Screens preview
+
+
+
+
diff --git a/src/controllers/legacycontrollermapping.h b/src/controllers/legacycontrollermapping.h
index 0e556c9a11e7..3c74c7c85889 100644
--- a/src/controllers/legacycontrollermapping.h
+++ b/src/controllers/legacycontrollermapping.h
@@ -4,11 +4,22 @@
#include
#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#ifdef MIXXX_USE_QML
+#include
+#endif
#include "controllers/legacycontrollersettings.h"
#include "controllers/legacycontrollersettingslayout.h"
#include "defs_urls.h"
#include "preferences/usersettings.h"
+#include "util/assert.h"
/// This class represents a controller mapping, containing the data elements that
/// make it up.
@@ -35,6 +46,10 @@ class LegacyControllerMapping {
m_settingsLayout(other.m_settingsLayout.get() != nullptr
? other.m_settingsLayout->clone()
: nullptr),
+#ifdef MIXXX_USE_QML
+ m_modules(other.m_modules),
+ m_screens(other.m_screens),
+#endif
m_scripts(other.m_scripts),
m_deviceDirection(other.m_deviceDirection) {
}
@@ -43,14 +58,20 @@ class LegacyControllerMapping {
virtual std::shared_ptr clone() const = 0;
struct ScriptFileInfo {
- ScriptFileInfo()
- : builtin(false) {
- }
-
- QString name;
- QString functionPrefix;
- QFileInfo file;
- bool builtin;
+ enum class Type {
+ Javascript,
+#ifdef MIXXX_USE_QML
+ Qml,
+#endif
+ };
+
+ QString name; // Name of the script file to add.
+ QString identifier; // The script's function prefix with Javascript OR
+ // the screen identifier this QML should be run for
+ // (or empty string).
+ QFileInfo file; // A FileInfo object pointing to the script file.
+ Type type; // A ScriptFileInfo::Type the specify script file type.
+ bool builtin; // If this is true, the script won't be written to the XML.
};
// TODO (xxx): this is a temporary solution to address devices that don't
@@ -66,21 +87,46 @@ class LegacyControllerMapping {
};
Q_DECLARE_FLAGS(DeviceDirections, DeviceDirection)
+#ifdef MIXXX_USE_QML
+ struct QMLModuleInfo {
+ QMLModuleInfo(const QFileInfo& aDirinfo,
+ bool isBuiltin)
+ : dirinfo(aDirinfo),
+ builtin(isBuiltin) {
+ }
+
+ QFileInfo dirinfo;
+ bool builtin;
+ };
+
+ struct ScreenInfo {
+ // Defining a custom enum here as std::endian contains `native` which is
+ // confusing and will have unpredictable behaviour depending of the
+ // platform.
+ enum class ColorEndian {
+ Big = static_cast(std::endian::big),
+ Little = static_cast(std::endian::little),
+ };
+
+ QString identifier; // The screen identifier.
+ QSize size; // The size of the screen.
+ uint target_fps; // The maximum FPS to render.
+ uint msaa; // The MSAA value to use for render.
+ std::chrono::milliseconds
+ splash_off; // The rendering grace time given when the screen is
+ // requested to shutdown.
+ QImage::Format pixelFormat; // The pixel encoding format.
+ ColorEndian endian; // The pixel endian format.
+ bool reversedColor; // Whether or not the RGB is swapped BGR.
+ bool rawData; // Whether or not the screen is allowed to receive bare
+ // data, not transformed.
+ };
+#endif
+
/// Adds a script file to the list of controller scripts for this mapping.
- /// @param filename Name of the script file to add
- /// @param functionprefix The script's function prefix (or empty string)
- /// @param file A FileInfo object pointing to the script file
- /// @param builtin If this is true, the script won't be written to the XML
- void addScriptFile(const QString& name,
- const QString& functionprefix,
- const QFileInfo& file,
- bool builtin = false) {
- ScriptFileInfo info;
- info.name = name;
- info.functionPrefix = functionprefix;
- info.file = file;
- info.builtin = builtin;
- m_scripts.append(info);
+ /// @param info The script info to add.
+ virtual void addScriptFile(ScriptFileInfo info) {
+ m_scripts.append(std::move(info));
setDirty(true);
}
@@ -144,6 +190,42 @@ class LegacyControllerMapping {
return m_deviceDirection;
}
+#ifdef MIXXX_USE_QML
+ /// Adds a custom QML module file to the list of controller modules for this mapping.
+ /// @param dirInfo A FileInfo of the directory or QML module
+ /// @param builtin If this is true, the script won't be written to the XML
+ virtual void addModule(const QFileInfo& dirInfo,
+ bool builtin = false) {
+ m_modules.append(QMLModuleInfo(
+ dirInfo,
+ builtin));
+ setDirty(true);
+ }
+
+ const QList& getModules() const {
+ return m_modules;
+ }
+
+ /// @brief Adds a screen info where QML will be rendered.
+ /// @param identifier The screen identifier
+ /// @param size the size of the screen
+ /// @param targetFps the maximum FPS to render
+ /// @param msaa the MSAA value to use for render
+ /// @param splashoff the rendering grace time given when the screen is requested to shutdown
+ /// @param pixelFormat the pixel encoding format
+ /// @param endian the pixel endian format
+ /// @param reversedColor whether or not the RGB is swapped BGR
+ /// @param rawData whether or not the screen is allowed to reserve bare data, not transformed
+ virtual void addScreenInfo(ScreenInfo info) {
+ m_screens.append(std::move(info));
+ setDirty(true);
+ }
+
+ const QList& getInfoScreens() const {
+ return m_screens;
+ }
+#endif
+
void setDirty(bool bDirty) {
m_bDirty = bDirty;
}
@@ -289,6 +371,10 @@ class LegacyControllerMapping {
QList> m_settings;
std::unique_ptr m_settingsLayout;
+#ifdef MIXXX_USE_QML
+ QList m_modules;
+ QList m_screens;
+#endif
QList m_scripts;
DeviceDirections m_deviceDirection;
};
diff --git a/src/controllers/legacycontrollermappingfilehandler.cpp b/src/controllers/legacycontrollermappingfilehandler.cpp
index b8c6ab94321b..7782ec8186ca 100644
--- a/src/controllers/legacycontrollermappingfilehandler.cpp
+++ b/src/controllers/legacycontrollermappingfilehandler.cpp
@@ -2,13 +2,67 @@
#include "controllers/defs_controllers.h"
#include "controllers/midi/legacymidicontrollermappingfilehandler.h"
+#include "util/logger.h"
#include "util/xml.h"
#ifdef __HID__
#include "controllers/hid/legacyhidcontrollermappingfilehandler.h"
#endif
+#ifdef MIXXX_USE_QML
+QMap LegacyControllerMappingFileHandler::kSupportedPixelFormat = {
+ {"RBG", QImage::Format_RGB888},
+ {"RBGA", QImage::Format_RGBA8888},
+ {"RGB565", QImage::Format_RGB16},
+};
+
+QMap
+ LegacyControllerMappingFileHandler::kEndianFormat = {
+ {"big", LegacyControllerMapping::ScreenInfo::ColorEndian::Big},
+ {"little",
+ LegacyControllerMapping::ScreenInfo::ColorEndian::
+ Little},
+};
+#endif
namespace {
+const mixxx::Logger kLogger("LegacyControllerMappingFileHandler");
+
+const QString kRequiredScriptFile = QStringLiteral("common-controller-scripts.js");
+
+#ifdef MIXXX_USE_QML
+
+/// Find a module directory (QML) in the mapping or system path.
+///
+/// @param mapping The controller mapping the module directory belongs to.
+/// @param dirname The module directory name.
+/// @param systemMappingsPath The system mappings path to use as fallback.
+/// @return Returns a QFileInfo object. If the script was not found in either
+/// of the search directories, the QFileInfo object might point to a
+/// non-existing file.
+QFileInfo findLibraryPath(
+ const LegacyControllerMapping& mapping,
+ const QString& dirname,
+ const QDir& systemMappingsPath) {
+ // Always try to load module directory from the mapping's directory first
+ QFileInfo dir = QFileInfo(mapping.dirPath().absoluteFilePath(dirname));
+
+ // If the module directory does not exist, try to find it in the fallback dir
+ if (!dir.isDir()) {
+ dir = QFileInfo(systemMappingsPath.absoluteFilePath(dirname));
+ }
+ return dir;
+}
+
+/// @brief Parse a string that contain a boolean value in human representation
+/// @param value the string containing the boolean setting
+/// @return true for string value "true", false otherwise
+bool parseHumanBoolean(const QString& value, bool* ok) {
+ if (ok) {
+ *ok = value == QStringLiteral("true") || value == QStringLiteral("false");
+ }
+ return value == QStringLiteral("true");
+}
+#endif
/// Find script file in the mapping or system path.
///
@@ -31,6 +85,102 @@ QFileInfo findScriptFile(std::shared_ptr mapping,
return file;
}
+#ifdef MIXXX_USE_QML
+#define LOG_IF_NOT_OK(FIELD, TYPE) \
+ if (!ok) { \
+ kLogger.warning().nospace() \
+ << "Unable to parse the field \"" << FIELD << "\" as " << TYPE \
+ << " in the screen definition."; \
+ }
+/// @brief Parse the screen info from the XML definition and add it to the mapping object
+/// @param screen the screen XML definition
+/// @param mapping the mapping being parsed
+/// @return true if the screen definition could be parse, false otherwise
+bool parseAndAddScreenDefinition(const QDomElement& screen, LegacyControllerMapping* mapping) {
+ bool ok;
+ QString identifier = screen.attribute("identifier", "");
+ uint targetFps = screen.attribute("targetFps", "30").toUInt(&ok);
+ LOG_IF_NOT_OK("targetFps", "an unsigned integer");
+ uint msaa = screen.attribute("msaa", "1").toUInt(&ok);
+ LOG_IF_NOT_OK("msaa", "an unsigned integer");
+ QString pixelFormatName = screen.attribute("pixelType", "RBG");
+ QString endianName = screen.attribute("endian", "little");
+ bool reversedColor = parseHumanBoolean(
+ screen.attribute("reversed", "false").toLower().trimmed(), &ok);
+ LOG_IF_NOT_OK("reversed", "a boolean");
+ bool rawData = parseHumanBoolean(screen.attribute("raw", "false").toLower().trimmed(), &ok);
+ LOG_IF_NOT_OK("raw", "a boolean");
+ uint splashOff = screen.attribute("splashoff", "0").toUInt(&ok);
+ LOG_IF_NOT_OK("splashoff", "an unsigned integer");
+
+ if (!targetFps || targetFps > LegacyControllerMappingFileHandler::kMaxTargetFps) {
+ kLogger.warning()
+ << "Invalid target FPS. Target FPS must be between 1 and"
+ << LegacyControllerMappingFileHandler::kMaxTargetFps;
+ return false;
+ }
+ if (!msaa || msaa > LegacyControllerMappingFileHandler::kMaxMsaa) {
+ kLogger.warning()
+ << "Invalid MSAA value. MSAA value must be between 1 and"
+ << LegacyControllerMappingFileHandler::kMaxMsaa;
+ return false;
+ }
+
+ if (splashOff > LegacyControllerMappingFileHandler::kMaxSplashOffDuration) {
+ kLogger.warning()
+ << QString("Invalid splashoff duration. Splashoff duration "
+ "must be "
+ "between 0 and %1. Clamping to %2")
+ .arg(QString::number(
+ LegacyControllerMappingFileHandler::
+ kMaxSplashOffDuration),
+ QString::number(
+ LegacyControllerMappingFileHandler::
+ kMaxSplashOffDuration));
+ splashOff = LegacyControllerMappingFileHandler::kMaxSplashOffDuration;
+ }
+
+ if (!LegacyControllerMappingFileHandler::kSupportedPixelFormat.contains(pixelFormatName)) {
+ kLogger.warning() << "Unsupported pixel format" << pixelFormatName;
+ return false;
+ }
+
+ if (!LegacyControllerMappingFileHandler::kEndianFormat.contains(endianName)) {
+ kLogger.warning() << "Unknown endian format" << endianName;
+ return false;
+ }
+
+ QImage::Format pixelFormat =
+ LegacyControllerMappingFileHandler::kSupportedPixelFormat.value(
+ pixelFormatName);
+ auto endian = LegacyControllerMappingFileHandler::kEndianFormat.value(endianName);
+
+ uint width = screen.attribute("width", "0").toUInt(&ok);
+ LOG_IF_NOT_OK("width", "an unsigned integer");
+ uint height = screen.attribute("height", "0").toUInt(&ok);
+ LOG_IF_NOT_OK("height", "an unsigned integer");
+
+ if (!width || !height) {
+ kLogger.warning() << "Invalid screen size. Screen size must have a width "
+ "and height above 1 pixel";
+ return false;
+ }
+
+ kLogger.debug() << "Adding screen" << identifier;
+ mapping->addScreenInfo(LegacyControllerMapping::ScreenInfo{
+ identifier,
+ QSize(width, height),
+ targetFps,
+ msaa,
+ std::chrono::milliseconds(splashOff),
+ pixelFormat,
+ endian,
+ reversedColor,
+ rawData});
+ return true;
+}
+#endif
+
} // namespace
// static
@@ -227,20 +377,68 @@ void LegacyControllerMappingFileHandler::addScriptFilesToMapping(
.firstChildElement("file");
// Default currently required file
- mapping->addScriptFile(REQUIRED_SCRIPT_FILE,
+ mapping->addScriptFile(LegacyControllerMapping::ScriptFileInfo{
+ kRequiredScriptFile,
"",
- findScriptFile(mapping, REQUIRED_SCRIPT_FILE, systemMappingsPath),
- true);
+ findScriptFile(mapping, kRequiredScriptFile, systemMappingsPath),
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true});
// Look for additional ones
while (!scriptFile.isNull()) {
- QString functionPrefix = scriptFile.attribute("functionprefix", "");
QString filename = scriptFile.attribute("filename", "");
QFileInfo file = findScriptFile(mapping, filename, systemMappingsPath);
-
- mapping->addScriptFile(filename, functionPrefix, file);
+ if (file.suffix() == "qml") {
+#ifdef MIXXX_USE_QML
+ QString identifier = scriptFile.attribute("identifier", "");
+ mapping->addScriptFile(LegacyControllerMapping::ScriptFileInfo{
+ filename,
+ identifier,
+ file,
+ LegacyControllerMapping::ScriptFileInfo::Type::Qml,
+ false});
+#else
+ kLogger.warning()
+ << "Unsupported render scene for file" << file.filePath()
+ << ". Mixxx isn't built with QML support";
+ return;
+#endif
+ } else {
+ QString functionPrefix = scriptFile.attribute("functionprefix", "");
+ mapping->addScriptFile(LegacyControllerMapping::ScriptFileInfo{filename,
+ functionPrefix,
+ file,
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ false});
+ }
scriptFile = scriptFile.nextSiblingElement("file");
}
+
+#ifdef MIXXX_USE_QML
+ // Build a list of QML files to load
+ QDomElement screen = controller.firstChildElement("screens")
+ .firstChildElement("screen");
+
+ // Look for additional ones
+ while (!screen.isNull()) {
+ if (!parseAndAddScreenDefinition(screen, mapping.get())) {
+ return;
+ }
+ screen = screen.nextSiblingElement("screen");
+ }
+ // Build a list of QML files to load
+ QDomElement qmlLibrary = controller.firstChildElement("qmllibraries")
+ .firstChildElement("library");
+
+ // Look for additional ones
+ while (!qmlLibrary.isNull()) {
+ QString libFilename = qmlLibrary.attribute("path", "");
+ QFileInfo path = findLibraryPath(*mapping, libFilename, systemMappingsPath);
+ kLogger.debug() << "Adding QML directory " << libFilename;
+ mapping->addModule(path);
+ qmlLibrary = qmlLibrary.nextSiblingElement("library");
+ }
+#endif
}
bool LegacyControllerMappingFileHandler::writeDocument(
@@ -323,8 +521,8 @@ QDomDocument LegacyControllerMappingFileHandler::buildRootWithScripts(
if (script.builtin) {
continue;
}
- qDebug() << "writing script block for" << filename;
- QString functionPrefix = script.functionPrefix;
+ kLogger.debug() << "writing script block for" << filename;
+ QString functionPrefix = script.identifier;
QDomElement scriptFile = doc.createElement("file");
scriptFile.setAttribute("filename", filename);
diff --git a/src/controllers/legacycontrollermappingfilehandler.h b/src/controllers/legacycontrollermappingfilehandler.h
index 73e5eea584c3..383585f0eda9 100644
--- a/src/controllers/legacycontrollermappingfilehandler.h
+++ b/src/controllers/legacycontrollermappingfilehandler.h
@@ -3,10 +3,18 @@
#include
#include
#include
+#ifdef MIXXX_USE_QML
+#include
+#include
+#include
+
+#include "controllers/legacycontrollermapping.h"
+#else
+class LegacyControllerMapping;
+#endif
class QFileInfo;
class QDir;
-class LegacyControllerMapping;
class LegacyControllerSettingsLayoutContainer;
/// The LegacyControllerMappingFileHandler is used for serializing/deserializing the
@@ -48,10 +56,10 @@ class LegacyControllerMappingFileHandler {
void parseMappingSettings(const QDomElement& root,
LegacyControllerMapping* mapping) const;
- /// Adds script files from XML to the LegacyControllerMapping.
+ /// Adds script files and QML scenes from XML to the LegacyControllerMapping.
///
/// This function parses the supplied QDomElement structure, finds the
- /// matching script files inside the search paths and adds them to
+ /// matching script files and QML scenes inside the search paths and adds them to
/// LegacyControllerMapping.
///
/// @param root The root node of the XML document for the mapping.
@@ -84,5 +92,18 @@ class LegacyControllerMappingFileHandler {
const QString& filePath,
const QDir& systemMappingPath) = 0;
+#ifdef MIXXX_USE_QML
+ public:
+ static QMap kSupportedPixelFormat;
+ static QMap kEndianFormat;
+ // Maximum target frame per request for a screen controller
+ static constexpr int kMaxTargetFps = 240;
+ // Maximum MSAA value that can be used
+ static constexpr int kMaxMsaa = 16;
+ // Maximum time allowed for a screen to run a splash off animation
+ static constexpr int kMaxSplashOffDuration = 3000;
+
+ friend class ControllerRenderingEngineTest;
+#endif
friend class LegacyControllerMappingSettingsTest_parseSettingBlock_Test;
};
diff --git a/src/controllers/midi/midicontroller.cpp b/src/controllers/midi/midicontroller.cpp
index f815cf5d0d2f..3130f6594fed 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(const QString& resourcePath) {
// Handles the engine
- bool result = Controller::applyMapping();
+ bool result = Controller::applyMapping(resourcePath);
// Only execute this code if this is an output device
if (isOutputDevice()) {
@@ -301,7 +301,7 @@ void MidiController::processInputMapping(const MidiInputMapping& mapping,
MidiOpCode opCode = MidiUtils::opCodeFromStatus(status);
if (mapping.options.testFlag(MidiOption::Script)) {
- ControllerScriptEngineLegacy* pEngine = getScriptEngine();
+ auto pEngine = getScriptEngine();
if (pEngine == nullptr) {
return;
}
@@ -595,7 +595,7 @@ void MidiController::processInputMapping(const MidiInputMapping& mapping,
mixxx::Duration timestamp) {
// Custom script handler
if (mapping.options.testFlag(MidiOption::Script)) {
- ControllerScriptEngineLegacy* pEngine = getScriptEngine();
+ auto pEngine = getScriptEngine();
if (pEngine == nullptr) {
return;
}
diff --git a/src/controllers/midi/midicontroller.h b/src/controllers/midi/midicontroller.h
index dab0a4dd3cd4..fabc0df85ab7 100644
--- a/src/controllers/midi/midicontroller.h
+++ b/src/controllers/midi/midicontroller.h
@@ -81,7 +81,7 @@ class MidiController : public Controller {
void slotBeforeEngineShutdown() override;
private slots:
- bool applyMapping() override;
+ bool applyMapping(const QString& resourcePath) override;
void learnTemporaryInputMappings(const MidiInputMappings& mappings);
void clearTemporaryInputMappings();
diff --git a/src/controllers/rendering/controllerrenderingengine.cpp b/src/controllers/rendering/controllerrenderingengine.cpp
new file mode 100644
index 000000000000..d2c19a0ac7aa
--- /dev/null
+++ b/src/controllers/rendering/controllerrenderingengine.cpp
@@ -0,0 +1,391 @@
+#include "controllers/rendering/controllerrenderingengine.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "controllers/controller.h"
+#include "controllers/controllerenginethreadcontrol.h"
+#include "controllers/scripting/legacy/controllerscriptenginelegacy.h"
+#include "controllers/scripting/legacy/controllerscriptinterfacelegacy.h"
+#include "moc_controllerrenderingengine.cpp"
+#include "qml/qmlwaveformoverview.h"
+#include "util/cmdlineargs.h"
+#include "util/logger.h"
+#include "util/thread_affinity.h"
+#include "util/time.h"
+#include "util/timer.h"
+
+// Used in the renderFrame method to properly abort the rendering and terminate the engine.
+#define VERIFY_OR_TERMINATE(cond, msg) \
+ VERIFY_OR_DEBUG_ASSERT(cond) { \
+ kLogger.warning() << msg; \
+ m_pThread->quit(); \
+ return; \
+ }
+
+namespace {
+const mixxx::Logger kLogger("ControllerRenderingEngine");
+} // anonymous namespace
+
+using Clock = std::chrono::steady_clock;
+
+ControllerRenderingEngine::ControllerRenderingEngine(
+ const LegacyControllerMapping::ScreenInfo& info,
+ gsl::not_null engineThreadControl)
+ : QObject(),
+ m_screenInfo(info),
+ m_GLDataFormat(GL_RGBA),
+ m_GLDataType(GL_UNSIGNED_BYTE),
+ m_isValid(true),
+ m_pEngineThreadControl(engineThreadControl) {
+ switch (m_screenInfo.pixelFormat) {
+ case QImage::Format_RGB16:
+ m_GLDataFormat = GL_RGB;
+ m_GLDataType = m_screenInfo.reversedColor
+ ? GL_UNSIGNED_SHORT_5_6_5_REV
+ : GL_UNSIGNED_SHORT_5_6_5;
+ break;
+ case QImage::Format_RGB888:
+ m_GLDataFormat = m_screenInfo.reversedColor ? GL_BGR : GL_RGB;
+ m_GLDataType = GL_UNSIGNED_BYTE;
+ break;
+ case QImage::Format_RGBA8888:
+ m_GLDataFormat = m_screenInfo.reversedColor ? GL_BGRA : GL_RGBA;
+ m_GLDataType = GL_UNSIGNED_BYTE;
+ break;
+ default:
+ m_isValid = false;
+ DEBUG_ASSERT(!"Unsupported format");
+ }
+
+ if (!m_isValid) {
+ return;
+ }
+
+ prepare();
+}
+
+void ControllerRenderingEngine::prepare() {
+ m_pThread = std::make_unique();
+ m_pThread->setObjectName("ControllerScreenRenderer");
+
+#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
+ [[maybe_unused]] bool successful = moveToThread(m_pThread.get());
+ DEBUG_ASSERT(successful);
+#else
+ moveToThread(m_pThread.get());
+#endif
+
+ // these at first sight weird-looking connections facilitate thread-safe communication.
+ connect(this,
+ &ControllerRenderingEngine::sendFrameDataRequested,
+ this,
+ &ControllerRenderingEngine::send);
+ connect(m_pThread.get(),
+ &QThread::finished,
+ this,
+ &ControllerRenderingEngine::finish);
+
+ m_pThread->start(QThread::NormalPriority);
+}
+
+ControllerRenderingEngine::~ControllerRenderingEngine() {
+ DEBUG_ASSERT_THIS_QOBJECT_THREAD_ANTI_AFFINITY();
+ m_pThread->wait();
+ VERIFY_OR_DEBUG_ASSERT(!m_fbo) {
+ kLogger.critical() << "The ControllerEngine is being deleted but hasn't been "
+ "cleaned up. Brace for impact";
+ };
+}
+
+void ControllerRenderingEngine::start() {
+ VERIFY_OR_DEBUG_ASSERT(!thread()->isFinished() && !thread()->isInterruptionRequested()) {
+ kLogger.critical() << "Render thread has or is about to terminate. Cannot "
+ "start this render anymore.";
+ return;
+ }
+ QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest));
+}
+bool ControllerRenderingEngine::isRunning() const {
+ return m_pThread && m_pThread->isRunning();
+}
+
+void ControllerRenderingEngine::requestEngineSetup(std::shared_ptr qmlEngine) {
+ DEBUG_ASSERT(!m_quickWindow);
+ VERIFY_OR_DEBUG_ASSERT(qmlEngine) {
+ kLogger.critical() << "No QML engine was passed!";
+ return;
+ }
+ VERIFY_OR_DEBUG_ASSERT_THIS_QOBJECT_THREAD_ANTI_AFFINITY() {
+ kLogger.warning() << "Unable to setup OpenGL rendering context from the same "
+ "thread as the render object";
+ return;
+ }
+
+ QMetaObject::invokeMethod(
+ this,
+ [this, qmlEngine]() {
+ setup(qmlEngine);
+ },
+ // This invocation will block the current thread!
+ Qt::BlockingQueuedConnection);
+
+ if (m_quickWindow) {
+ m_renderControl->prepareThread(m_pThread.get());
+ }
+}
+
+void ControllerRenderingEngine::requestSendingFrameData(
+ Controller* controller, const QByteArray& frame) {
+ emit sendFrameDataRequested(controller, frame);
+}
+
+void ControllerRenderingEngine::setup(std::shared_ptr qmlEngine) {
+ VERIFY_OR_DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY() {
+ kLogger.warning() << "The ControllerRenderingEngine setup must be done by its own thread!";
+ return;
+ }
+ QSurfaceFormat format;
+ format.setSamples(m_screenInfo.msaa);
+ format.setDepthBufferSize(16);
+ format.setStencilBufferSize(8);
+
+ m_context = std::make_unique();
+ m_context->setFormat(format);
+ VERIFY_OR_DEBUG_ASSERT(m_context->create()) {
+ kLogger.warning() << "Unable to initialize controller screen rendering. Giving up";
+ return;
+ }
+ connect(m_context.get(),
+ &QOpenGLContext::aboutToBeDestroyed,
+ this,
+ &ControllerRenderingEngine::finish);
+
+ m_offscreenSurface = std::make_unique();
+ m_offscreenSurface->setFormat(m_context->format());
+
+ // offscreen surface needs to be created from application main thread.
+ VERIFY_OR_DEBUG_ASSERT(QMetaObject::invokeMethod(
+ qApp,
+ [this] {
+ m_offscreenSurface->create();
+ },
+ // This invocation will block the current thread!
+ Qt::BlockingQueuedConnection) &&
+ m_offscreenSurface->isValid()) {
+ kLogger.warning() << "Unable to create the OffscreenSurface for controller "
+ "screen rendering. Giving up";
+ m_offscreenSurface.reset();
+ return;
+ }
+
+ m_renderControl = std::make_unique(this);
+ m_quickWindow = std::make_unique(m_renderControl.get());
+
+ if (!qmlEngine->incubationController()) {
+ qmlEngine->setIncubationController(m_quickWindow->incubationController());
+ }
+
+ m_quickWindow->setGeometry(0, 0, m_screenInfo.size.width(), m_screenInfo.size.height());
+}
+
+void ControllerRenderingEngine::finish() {
+ DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY();
+ emit stopping();
+
+ m_isValid = false;
+
+ if (m_context && m_context->isValid()) {
+ disconnect(m_context.get(),
+ &QOpenGLContext::aboutToBeDestroyed,
+ this,
+ &ControllerRenderingEngine::finish);
+ m_context->makeCurrent(m_offscreenSurface.get());
+ m_renderControl.reset();
+
+ std::shared_ptr pOffscreenSurface = std::move(m_offscreenSurface);
+ QMetaObject::invokeMethod(
+ qApp,
+ [pOffscreenSurface] {
+ pOffscreenSurface->destroy();
+ });
+ m_quickWindow.reset();
+
+ // Free the engine and FBO.
+ m_fbo.reset();
+
+ m_context->doneCurrent();
+ }
+ m_context.reset();
+}
+
+void ControllerRenderingEngine::renderFrame() {
+ ScopedTimer t(QStringLiteral("ControllerRenderingEngine::renderFrame"));
+ if (!m_isValid) {
+ DEBUG_ASSERT(!"Trying to render frame on an invalid engine");
+ return;
+ }
+
+ VERIFY_OR_TERMINATE(m_offscreenSurface->isValid(), "OffscreenSurface isn't valid anymore.");
+ VERIFY_OR_TERMINATE(m_context->isValid(), "GLContext isn't valid anymore.");
+ VERIFY_OR_TERMINATE(m_context->makeCurrent(m_offscreenSurface.get()),
+ "Couldn't make the GLContext current to the OffscreenSurface.");
+
+ if (!m_fbo) {
+ ScopedTimer t(QStringLiteral("ControllerRenderingEngine::renderFrame::initFBO"));
+ VERIFY_OR_TERMINATE(
+ QOpenGLFramebufferObject::hasOpenGLFramebufferObjects(),
+ "OpenGL doesn't support FBO");
+
+ m_fbo = std::make_unique(
+ m_screenInfo.size, QOpenGLFramebufferObject::CombinedDepthStencil);
+
+ GLenum glError;
+ glError = m_context->functions()->glGetError();
+
+ VERIFY_OR_TERMINATE(glError == GL_NO_ERROR, "GLError: " << glError);
+ VERIFY_OR_TERMINATE(m_fbo->isValid(), "Failed to initialize FBO");
+
+ m_quickWindow->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(m_context.get()));
+
+ VERIFY_OR_TERMINATE(m_renderControl->initialize(),
+ "Failed to initialize redirected Qt Quick rendering");
+
+ m_quickWindow->setRenderTarget(QQuickRenderTarget::fromOpenGLTexture(m_fbo->texture(),
+ m_screenInfo.size));
+
+ m_quickWindow->setGeometry(0, 0, m_screenInfo.size.width(), m_screenInfo.size.height());
+ }
+
+ m_nextFrameStart = Clock::now();
+
+ m_renderControl->beginFrame();
+
+ if (m_pEngineThreadControl) {
+ m_pEngineThreadControl->pause();
+ }
+
+ m_renderControl->polishItems();
+
+ {
+ ScopedTimer t(QStringLiteral("ControllerRenderingEngine::renderFrame::sync"));
+ VERIFY_OR_DEBUG_ASSERT(m_renderControl->sync()) {
+ kLogger.warning() << "Couldn't sync the render control. Scene may be stuck";
+ };
+ }
+
+ if (m_pEngineThreadControl) {
+ m_pEngineThreadControl->resume();
+ }
+ QImage fboImage(m_screenInfo.size, m_screenInfo.pixelFormat);
+
+ VERIFY_OR_DEBUG_ASSERT(m_fbo->bind()) {
+ kLogger.warning() << "Couldn't bind the FBO.";
+ }
+ GLenum glError;
+ m_context->functions()->glFlush();
+ glError = m_context->functions()->glGetError();
+ VERIFY_OR_TERMINATE(glError == GL_NO_ERROR, "GLError: " << glError);
+ if (static_cast(m_screenInfo.endian) != std::endian::native) {
+ m_context->functions()->glPixelStorei(GL_PACK_SWAP_BYTES, GL_TRUE);
+ }
+ glError = m_context->functions()->glGetError();
+ VERIFY_OR_TERMINATE(glError == GL_NO_ERROR, "GLError: " << glError);
+
+ QDateTime timestamp = QDateTime::currentDateTime();
+ m_renderControl->render();
+ m_renderControl->endFrame();
+
+ // Flush any remaining GL errors.
+ while ((glError = m_context->functions()->glGetError()) != GL_NO_ERROR) {
+ kLogger.debug() << "Retrieved a previously unhandled GL error: " << glError;
+ }
+ {
+ ScopedTimer t(QStringLiteral("ControllerRenderingEngine::renderFrame::glReadPixels"));
+ m_context->functions()->glReadPixels(0,
+ 0,
+ m_screenInfo.size.width(),
+ m_screenInfo.size.height(),
+ m_GLDataFormat,
+ m_GLDataType,
+ fboImage.bits());
+ }
+ glError = m_context->functions()->glGetError();
+ VERIFY_OR_TERMINATE(glError == GL_NO_ERROR, "GLError: " << glError);
+ VERIFY_OR_DEBUG_ASSERT(!fboImage.isNull()) {
+ kLogger.warning() << "Screen frame is null!";
+ }
+ VERIFY_OR_DEBUG_ASSERT(m_fbo->release()) {
+ kLogger.debug() << "Couldn't release the FBO.";
+ }
+
+ fboImage.mirror(false, true);
+
+ emit frameRendered(m_screenInfo, fboImage.copy(), timestamp);
+
+ m_context->doneCurrent();
+}
+
+bool ControllerRenderingEngine::stop() {
+ m_pThread->quit();
+ return m_pThread->wait();
+}
+
+void ControllerRenderingEngine::send(Controller* controller, const QByteArray& frame) {
+ DEBUG_ASSERT_THIS_QOBJECT_THREAD_AFFINITY();
+ ScopedTimer t(QStringLiteral("ControllerRenderingEngine::send"));
+ if (!frame.isEmpty()) {
+ controller->sendBytes(frame);
+ }
+
+ if (CmdlineArgs::Instance()
+ .getControllerDebug()) {
+ auto endOfFrameCycle = Clock::now();
+ kLogger.debug()
+ << "Frame took "
+ << std::chrono::duration_cast(
+ endOfFrameCycle - m_nextFrameStart)
+ .count()
+ << "milliseconds and frame has" << frame.size() << "bytes";
+ }
+
+ m_nextFrameStart += std::chrono::microseconds(1000000 / m_screenInfo.target_fps);
+
+ auto durationToWaitBeforeFrame =
+ std::chrono::duration_cast(
+ m_nextFrameStart - Clock::now());
+
+ if (durationToWaitBeforeFrame > std::chrono::milliseconds(0)) {
+ if (CmdlineArgs::Instance()
+ .getControllerDebug()) {
+ kLogger.debug() << "Waiting for "
+ << durationToWaitBeforeFrame.count()
+ << "milliseconds before rendering next frame";
+ }
+ QTimer::singleShot(durationToWaitBeforeFrame,
+ Qt::PreciseTimer,
+ this,
+ &ControllerRenderingEngine::renderFrame);
+ } else {
+ QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest));
+ }
+}
+
+bool ControllerRenderingEngine::event(QEvent* event) {
+ // In case there is a request for update (e.g using QWindow::requestUpdate),
+ // we emit the signal to request rendering using the engine.
+ if (event->type() == QEvent::UpdateRequest) {
+ renderFrame();
+ return true;
+ }
+
+ return QObject::event(event);
+}
diff --git a/src/controllers/rendering/controllerrenderingengine.h b/src/controllers/rendering/controllerrenderingengine.h
new file mode 100644
index 000000000000..770e2b722716
--- /dev/null
+++ b/src/controllers/rendering/controllerrenderingengine.h
@@ -0,0 +1,109 @@
+#pragma once
+
+#include
+
+#include
+#include
+#include
+
+#include "controllers/legacycontrollermapping.h"
+#include "preferences/configobject.h"
+#include "util/time.h"
+
+class Controller;
+class ControllerEngineThreadControl;
+class QOffscreenSurface;
+class QOpenGLContext;
+class QOpenGLFramebufferObject;
+class QQmlEngine;
+class QQuickRenderControl;
+class QQuickWindow;
+class QThread;
+
+/// @brief This class is used to host the rendering of a screen controller,
+/// using and existing QML Engine running under a ControllerScriptEngineBase.
+class ControllerRenderingEngine : public QObject {
+ Q_OBJECT
+ public:
+ ControllerRenderingEngine(const LegacyControllerMapping::ScreenInfo& info,
+ gsl::not_null engineThreadControl);
+ // Destructor will wait for the ControllerRenderingEngine's thread to
+ // complete. It should be called from the Controller thread.
+ ~ControllerRenderingEngine();
+
+ bool event(QEvent* event) override;
+
+ QSize screenSize() const {
+ return m_screenInfo.size;
+ }
+
+ bool isValid() const {
+ return m_isValid;
+ }
+
+ bool isRunning() const;
+
+ // pointer lives as long as the `ControllerRenderingEngine` instance it is retrieved from.
+ QQuickWindow* quickWindow() const {
+ return m_quickWindow.get();
+ }
+
+ const LegacyControllerMapping::ScreenInfo& info() const {
+ return m_screenInfo;
+ }
+
+ public slots:
+ // Request sending frame data to the device. The task will be run in the
+ // rendering event loop. This method should only be called once received the
+ // `frameRendered` signal.
+ virtual void requestSendingFrameData(Controller* controller, const QByteArray& frame);
+ // Request setting up the rendering context for QML engine and wait till it
+ // is completed. The task will be run in the rendering event loop to ensure
+ // thread affinity of engine components. `isValid` can be used to ensure
+ // that the setup was successful.
+ void requestEngineSetup(std::shared_ptr qmlEngine);
+ void start();
+ virtual bool stop();
+
+ private slots:
+ void finish();
+ void renderFrame();
+ void setup(std::shared_ptr qmlEngine);
+ void send(Controller* controller, const QByteArray& frame);
+
+ signals:
+ void frameRendered(const LegacyControllerMapping::ScreenInfo& screeninfo,
+ QImage frame,
+ const QDateTime& timestamp);
+ void stopping();
+ /// @brief Request the screen thread to send a frame to the device.
+ /// @param controller the controller to send the frame to.
+ /// @param frame the frame data, ready to be sent.
+ void sendFrameDataRequested(Controller* controller, const QByteArray& frame);
+
+ private:
+ virtual void prepare();
+
+ std::chrono::time_point m_nextFrameStart;
+
+ LegacyControllerMapping::ScreenInfo m_screenInfo;
+
+ std::unique_ptr m_pThread;
+
+ std::unique_ptr m_context;
+ std::unique_ptr m_offscreenSurface;
+ std::unique_ptr m_renderControl;
+ std::unique_ptr m_quickWindow;
+
+ std::unique_ptr m_fbo;
+
+ GLenum m_GLDataFormat;
+ GLenum m_GLDataType;
+
+ bool m_isValid;
+ // Engine control is owned by ControllerScriptEngineBase. The assumption is
+ // made that ControllerScriptEngineBase always outlive
+ // ControllerRenderingEngine as it is in charge of stopping and joining the
+ // thread.
+ gsl::not_null m_pEngineThreadControl;
+};
diff --git a/src/controllers/scripting/controllerscriptenginebase.cpp b/src/controllers/scripting/controllerscriptenginebase.cpp
index eca4c67d6696..e31faad90887 100644
--- a/src/controllers/scripting/controllerscriptenginebase.cpp
+++ b/src/controllers/scripting/controllerscriptenginebase.cpp
@@ -5,7 +5,14 @@
#include "controllers/controller.h"
#include "controllers/scripting/colormapperjsproxy.h"
#include "errordialoghandler.h"
+#ifdef MIXXX_USE_QML
+#include
+#endif
+
#include "moc_controllerscriptenginebase.cpp"
+#ifdef MIXXX_USE_QML
+#include "qml/asyncimageprovider.h"
+#endif
#include "util/cmdlineargs.h"
ControllerScriptEngineBase::ControllerScriptEngineBase(
@@ -15,11 +22,27 @@ ControllerScriptEngineBase::ControllerScriptEngineBase(
m_pController(controller),
m_logger(logger),
m_bAbortOnWarning(false),
+#ifdef MIXXX_USE_QML
+ m_bQmlMode(false),
+#endif
m_bTesting(false) {
// Handle error dialog buttons
qRegisterMetaType("QMessageBox::StandardButton");
}
+#ifdef MIXXX_USE_QML
+void ControllerScriptEngineBase::registerTrackCollectionManager(
+ std::shared_ptr pTrackCollectionManager) {
+ s_pTrackCollectionManager = std::move(pTrackCollectionManager);
+}
+
+void ControllerScriptEngineBase::handleQMLErrors(const QList& qmlErrors) {
+ for (const QQmlError& error : std::as_const(qmlErrors)) {
+ showQMLExceptionDialog(error, m_bErrorsAreFatal);
+ }
+}
+#endif
+
bool ControllerScriptEngineBase::initialize() {
VERIFY_OR_DEBUG_ASSERT(!m_pJSEngine) {
return false;
@@ -28,9 +51,32 @@ bool ControllerScriptEngineBase::initialize() {
m_bAbortOnWarning = CmdlineArgs::Instance().getControllerAbortOnWarning();
// Create the Script Engine
- m_pJSEngine = std::make_shared(this);
+#ifdef MIXXX_USE_QML
+ if (!m_bQmlMode) {
+#endif
+ m_pJSEngine = std::make_shared(this);
- m_pJSEngine->installExtensions(QJSEngine::ConsoleExtension);
+ m_pJSEngine->installExtensions(QJSEngine::ConsoleExtension);
+#ifdef MIXXX_USE_QML
+ } else {
+ auto pQmlEngine = std::make_shared(this);
+ pQmlEngine->addImportPath(QStringLiteral(":/mixxx.org/imports"));
+ if (s_pTrackCollectionManager) {
+ mixxx::qml::AsyncImageProvider* pImageProvider = new mixxx::qml::AsyncImageProvider(
+ s_pTrackCollectionManager);
+ pQmlEngine->addImageProvider(mixxx::qml::AsyncImageProvider::kProviderName,
+ pImageProvider);
+ } else {
+ DEBUG_ASSERT(!"TrackCollectionManager is missing");
+ qCWarning(m_logger) << "TrackCollectionManager hasn't been registered yet";
+ }
+ connect(pQmlEngine.get(),
+ &QQmlEngine::warnings,
+ this,
+ &ControllerScriptEngineBase::handleQMLErrors);
+ m_pJSEngine = std::move(pQmlEngine);
+ }
+#endif
QJSValue engineGlobalObject = m_pJSEngine->globalObject();
@@ -126,6 +172,30 @@ void ControllerScriptEngineBase::showScriptExceptionDialog(
}
}
+#ifdef MIXXX_USE_QML
+void ControllerScriptEngineBase::showQMLExceptionDialog(
+ const QQmlError& error, bool bFatalError) {
+ VERIFY_OR_DEBUG_ASSERT(error.isValid()) {
+ return;
+ }
+
+ QString filename = error.url().isLocalFile() ? error.url().toLocalFile()
+ : error.url().toString();
+
+ if (filename.isEmpty()) {
+ filename = QStringLiteral("");
+ }
+ QString errorText = QStringLiteral("Uncaught exception: %1:%2: %3")
+ .arg(filename, QString::number(error.line()), error.description());
+
+ qCWarning(m_logger) << "ControllerScriptHandlerBase:" << errorText;
+
+ if (!m_bDisplayingExceptionDialog) {
+ scriptErrorDialog(errorText, errorText, bFatalError);
+ }
+}
+#endif
+
void ControllerScriptEngineBase::logOrThrowError(const QString& errorMessage) {
if (m_bAbortOnWarning) {
throwJSError(errorMessage);
diff --git a/src/controllers/scripting/controllerscriptenginebase.h b/src/controllers/scripting/controllerscriptenginebase.h
index 0919d46eed3a..2129184b641b 100644
--- a/src/controllers/scripting/controllerscriptenginebase.h
+++ b/src/controllers/scripting/controllerscriptenginebase.h
@@ -2,12 +2,21 @@
#include
#include
+#include
+#include
+#include
#include
#include "util/runtimeloggingcategory.h"
+#ifdef MIXXX_USE_QML
+#include "controllers/controllerenginethreadcontrol.h"
+#endif
class Controller;
class QJSEngine;
+#ifdef MIXXX_USE_QML
+class TrackCollectionManager;
+#endif
/// ControllerScriptEngineBase manages the JavaScript engine for controller scripts.
/// ControllerScriptModuleEngine implements the current system using JS modules.
@@ -26,6 +35,10 @@ class ControllerScriptEngineBase : public QObject {
/// Shows a UI dialog notifying of a script evaluation error.
/// Precondition: QJSValue.isError() == true
void showScriptExceptionDialog(const QJSValue& evaluationResult, bool bFatal = false);
+#ifdef MIXXX_USE_QML
+ /// Precondition: QML.isValid() == true
+ void showQMLExceptionDialog(const QQmlError& evaluationResult, bool bFatal = false);
+#endif
void throwJSError(const QString& message);
bool willAbortOnWarning() const {
@@ -40,6 +53,10 @@ class ControllerScriptEngineBase : public QObject {
return m_bTesting;
}
+#ifdef MIXXX_USE_QML
+ static void registerTrackCollectionManager(
+ std::shared_ptr pTrackCollectionManager);
+#endif
signals:
void beforeShutdown();
@@ -49,7 +66,19 @@ class ControllerScriptEngineBase : public QObject {
void scriptErrorDialog(const QString& detailedError, const QString& key, bool bFatal = false);
void logOrThrowError(const QString& errorMessage);
+#ifdef MIXXX_USE_QML
+ inline void setQMLMode(bool qmlFlag) {
+ m_bQmlMode = qmlFlag;
+ }
+ inline void setErrorsAreFatal(bool errorsAreFatal) {
+ m_bErrorsAreFatal = errorsAreFatal;
+ }
+#endif
+
bool m_bDisplayingExceptionDialog;
+#ifdef MIXXX_USE_QML
+ bool m_bErrorsAreFatal;
+#endif
std::shared_ptr m_pJSEngine;
Controller* m_pController;
@@ -57,13 +86,35 @@ class ControllerScriptEngineBase : public QObject {
bool m_bAbortOnWarning;
+#ifdef MIXXX_USE_QML
+ bool m_bQmlMode;
+#endif
bool m_bTesting;
+#ifdef MIXXX_USE_QML
+ private:
+ static inline std::shared_ptr s_pTrackCollectionManager;
+
+ protected:
+ /// Pause the GUI main thread. Pause is required by rendering
+ /// thread (https://doc.qt.io/qt-6/qquickrendercontrol.html#sync). This
+ /// offscreen render thread to pause the main "GUI thread" for onboard
+ /// screens
+ /// The documentation isn't completely clear about this, but after
+ /// testing, it appears that the "GUI main thread" is the thread where the QML
+ /// engine leaves in (also the main thread if we were using a
+ /// QMLApplication, which isn't the case here)
+ ControllerEngineThreadControl m_engineThreadControl;
+#endif
+
protected slots:
void reload();
private slots:
void errorDialogButton(const QString& key, QMessageBox::StandardButton button);
+#ifdef MIXXX_USE_QML
+ void handleQMLErrors(const QList& qmlErrors);
+#endif
friend class ColorMapperJSProxy;
friend class MidiControllerTest;
diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp
index 66cb55b4a06e..4dd7fb9f4b9f 100644
--- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp
+++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp
@@ -1,12 +1,53 @@
#include "controllers/scripting/legacy/controllerscriptenginelegacy.h"
+#ifdef MIXXX_USE_QML
+#include
+#include
+#include
+#include
+#include
+#include
+
+// Prevent conflict with methods called 'emit' in source
+#pragma push_macro("emit")
+#undef emit
+#include
+#pragma pop_macro("emit")
+#endif
+
#include "control/controlobject.h"
#include "controllers/controller.h"
+#ifdef MIXXX_USE_QML
+#include "controllers/rendering/controllerrenderingengine.h"
+#endif
#include "controllers/scripting/colormapperjsproxy.h"
#include "controllers/scripting/legacy/controllerscriptinterfacelegacy.h"
#include "errordialoghandler.h"
#include "mixer/playermanager.h"
#include "moc_controllerscriptenginelegacy.cpp"
+#ifdef MIXXX_USE_QML
+#include "util/assert.h"
+#include "util/cmdlineargs.h"
+
+using Clock = std::chrono::steady_clock;
+#endif
+
+#ifdef MIXXX_USE_QML
+namespace {
+const QByteArray kScreenTransformFunctionUntypedSignature =
+ QMetaObject::normalizedSignature(
+ "transformFrame(QVariant,QVariant)");
+const QByteArray kScreenTransformFunctionTypedSignature =
+ QMetaObject::normalizedSignature("transformFrame(QVariant,QDateTime)");
+const QByteArray kScreenInitFunctionUntypedSignature =
+ QMetaObject::normalizedSignature(
+ "init(QVariant,QVariant)");
+const QByteArray kScreenInitFunctionTypedSignature =
+ QMetaObject::normalizedSignature("init(QString,bool)");
+const QByteArray kScreenShutdownFunctionSignature =
+ QMetaObject::normalizedSignature("shutdown()");
+} // anonymous namespace
+#endif
ControllerScriptEngineLegacy::ControllerScriptEngineLegacy(
Controller* controller, const RuntimeLoggingCategory& logger)
@@ -14,13 +55,41 @@ ControllerScriptEngineLegacy::ControllerScriptEngineLegacy(
connect(&m_fileWatcher,
&QFileSystemWatcher::fileChanged,
this,
+ [this](const QString& changedFile) {
+ qCDebug(m_logger) << "File" << changedFile << "has been changed.";
+ // This is to prevent double-reload when a file is updated twice
+ // in a row as part of the normal saving process. See note in
+ // QFileSystemWatcher::fileChanged documentation.
+ if (m_fileWatcher.removePath(changedFile)) {
+ reload();
+ }
+ });
+#ifdef MIXXX_USE_QML
+ connect(&m_fileWatcher,
+ &QFileSystemWatcher::directoryChanged,
+ this,
&ControllerScriptEngineLegacy::reload);
+#endif
}
ControllerScriptEngineLegacy::~ControllerScriptEngineLegacy() {
shutdown();
}
+void ControllerScriptEngineLegacy::watchFilePath(const QString& path) {
+ if (m_fileWatcher.files().contains(path) || m_fileWatcher.directories().contains(path)) {
+ qCDebug(m_logger) << "File" << path << "is already being watch for controller auto-reload";
+ return;
+ }
+
+ if (!m_fileWatcher.addPath(path)) {
+ qCWarning(m_logger) << "Failed to watch script file"
+ << path;
+ } else {
+ qCDebug(m_logger) << "Watching file" << path << "for controller auto-reload";
+ }
+}
+
bool ControllerScriptEngineLegacy::callFunctionOnObjects(
const QList& scriptFunctionPrefixes,
const QString& function,
@@ -57,6 +126,142 @@ bool ControllerScriptEngineLegacy::callFunctionOnObjects(
return success;
}
+bool ControllerScriptEngineLegacy::callShutdownFunction() {
+ // There is no js engine if the mapping was not loaded from a file but by
+ // creating a new, empty mapping LegacyMidiControllerMapping with the wizard
+ if (!m_pJSEngine) {
+ return true;
+ }
+
+#ifdef MIXXX_USE_QML
+ if (!m_bQmlMode) {
+#endif
+ return callFunctionOnObjects(m_scriptFunctionPrefixes, "shutdown");
+#ifdef MIXXX_USE_QML
+ } else {
+ VERIFY_OR_DEBUG_ASSERT(!m_pJSEngine->hasError()) {
+ qCWarning(m_logger) << "Controller JS engine has an unhandled error.";
+ qCDebug(m_logger) << "Unhandled controller JS error is:"
+ << m_pJSEngine->catchError().toString();
+ }
+ QHashIterator> i(m_rootItems);
+ bool success = true;
+ while (i.hasNext()) {
+ i.next();
+ const QMetaObject* metaObject = i.value()->metaObject();
+ const QString& screenIdentifier = i.key();
+
+ VERIFY_OR_DEBUG_ASSERT(metaObject) {
+ qCWarning(m_logger)
+ << "Invalid meta object for screen" << screenIdentifier
+ << "It may be that an unhandled issue occurred when importing "
+ "the scene.";
+ continue;
+ }
+
+ QMetaMethod shutdownFunction;
+ int methodIdx = metaObject->indexOfMethod(kScreenShutdownFunctionSignature);
+
+ if (methodIdx == -1 || !metaObject->method(methodIdx).isValid()) {
+ qCDebug(m_logger) << "QML Scene for screen" << screenIdentifier
+ << "has no valid shutdown method.";
+ continue;
+ }
+
+ shutdownFunction = metaObject->method(methodIdx);
+
+ qCDebug(m_logger) << "Executing shutdown on QML Scene " << screenIdentifier;
+
+ VERIFY_OR_DEBUG_ASSERT(!m_pJSEngine->hasError()) {
+ qCWarning(m_logger) << "Controller JS engine has an unhandled error. Discarding.";
+ qCDebug(m_logger) << "Controller JS error is:"
+ << m_pJSEngine->catchError().toString();
+ }
+
+ success &= shutdownFunction.invoke(i.value().get(),
+ Qt::DirectConnection);
+ // Error handling is done in ControllerScriptEngineBase, with the
+ // connection QQmlEngine::warnings ->
+ // ControllerScriptEngineBase::handleQMLErrors
+ }
+ return success;
+ }
+#endif
+}
+bool ControllerScriptEngineLegacy::callInitFunction() {
+ // m_pController is nullptr in tests.
+ const auto controllerName = m_pController ? m_pController->getName() : QString{};
+
+#ifdef MIXXX_USE_QML
+ if (!m_bQmlMode) {
+#endif
+ const auto args = QJSValueList{
+ controllerName,
+ m_logger().isDebugEnabled(),
+ };
+ return callFunctionOnObjects(m_scriptFunctionPrefixes, "init", args, true);
+#ifdef MIXXX_USE_QML
+ } else {
+ VERIFY_OR_DEBUG_ASSERT(!m_pJSEngine->hasError()) {
+ qCWarning(m_logger) << "Controller JS engine has an unhandled error.";
+ qCDebug(m_logger) << "Unhandled controller JS error is:"
+ << m_pJSEngine->catchError().toString();
+ }
+ QHashIterator> i(m_rootItems);
+ bool success = true;
+ while (i.hasNext()) {
+ i.next();
+ const QMetaObject* metaObject = i.value()->metaObject();
+ const QString& screenIdentifier = i.key();
+
+ VERIFY_OR_DEBUG_ASSERT(metaObject) {
+ qCWarning(m_logger)
+ << "Invalid meta object for screen" << screenIdentifier
+ << "It may be that an unhandled issue occurred when importing "
+ "the scene.";
+ continue;
+ }
+
+ QMetaMethod initFunction;
+ bool typed = false;
+ int methodIdx = metaObject->indexOfMethod(kScreenInitFunctionUntypedSignature);
+
+ 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);
+ typed = true;
+ }
+
+ initFunction = metaObject->method(methodIdx);
+
+ if (!initFunction.isValid()) {
+ qCDebug(m_logger) << "QML Scene for screen" << screenIdentifier
+ << "has no valid typed init method. Skipping.";
+ continue;
+ }
+
+ qCDebug(m_logger) << "Executing init on QML Scene " << screenIdentifier;
+ if (typed) {
+ success &= initFunction.invoke(i.value().get(),
+ Qt::DirectConnection,
+ Q_ARG(QString, controllerName),
+ Q_ARG(bool, m_logger().isDebugEnabled()));
+ } else {
+ success &= initFunction.invoke(i.value().get(),
+ Qt::DirectConnection,
+ Q_ARG(QVariant, controllerName),
+ Q_ARG(QVariant, m_logger().isDebugEnabled()));
+ }
+ // Error handling is done in ControllerScriptEngineBase, with the
+ // connection QQmlEngine::warnings ->
+ // ControllerScriptEngineBase::handleQMLErrors
+ }
+ return success;
+ }
+#endif
+}
+
QJSValue ControllerScriptEngineLegacy::wrapFunctionCode(
const QString& codeSnippet, int numberOfArgs) {
// This function is called from outside the controller engine, so we can't
@@ -90,6 +295,25 @@ QJSValue ControllerScriptEngineLegacy::wrapFunctionCode(
return wrappedFunction;
}
+#ifdef MIXXX_USE_QML
+void ControllerScriptEngineLegacy::setModulePaths(
+ const QList& modules) {
+ const QStringList paths = m_fileWatcher.files();
+ if (!paths.isEmpty()) {
+ m_fileWatcher.removePaths(paths);
+ }
+
+ m_modules = modules;
+}
+void ControllerScriptEngineLegacy::setInfoScreens(
+ const QList& screens) {
+ m_rootItems.clear();
+ m_renderingScreens.clear();
+ m_transformScreenFrameFunctions.clear();
+ m_infoScreens = screens;
+}
+#endif
+
void ControllerScriptEngineLegacy::setScriptFiles(
const QList& scripts) {
const QStringList paths = m_fileWatcher.files();
@@ -97,6 +321,16 @@ void ControllerScriptEngineLegacy::setScriptFiles(
m_fileWatcher.removePaths(paths);
}
m_scriptFiles = scripts;
+
+#ifdef MIXXX_USE_QML
+ setQMLMode(std::any_of(std::execution::par_unseq,
+ m_scriptFiles.cbegin(),
+ m_scriptFiles.cend(),
+ [](const auto& scriptFileInfo) {
+ return scriptFileInfo.type ==
+ LegacyControllerMapping::ScriptFileInfo::Type::Qml;
+ }));
+#endif
}
void ControllerScriptEngineLegacy::setSettings(
@@ -116,6 +350,57 @@ bool ControllerScriptEngineLegacy::initialize() {
return false;
}
+#ifdef MIXXX_USE_QML
+ // During the initialisation, any QML errors are considered fatal.
+ setErrorsAreFatal(true);
+ QMap> availableScreens;
+
+ if (m_bQmlMode) {
+ for (const LegacyControllerMapping::ScreenInfo& screen : std::as_const(m_infoScreens)) {
+ VERIFY_OR_DEBUG_ASSERT(!availableScreens.contains(screen.identifier)) {
+ qCWarning(m_logger) << "A controller screen already contains the "
+ "identifier "
+ << screen.identifier;
+ return false;
+ }
+ availableScreens.insert(screen.identifier,
+ std::make_shared(screen, &m_engineThreadControl));
+
+ if (!availableScreens.value(screen.identifier)->isValid()) {
+ qCWarning(m_logger) << "Unable to start the screen render for" << screen.identifier;
+ return false;
+ }
+
+ // For testing, do not actually initialize the rendering engine, just check for
+ // compatibility above.
+ if (m_bTesting) {
+ continue;
+ }
+
+ // Rename the ControllerRenderingEngine with the actual screen
+ // identifier to help debugging.
+ availableScreens.value(screen.identifier)
+ ->thread()
+ ->setObjectName(
+ QString("CtrlScreen_%1").arg(screen.identifier));
+ availableScreens.value(screen.identifier)
+ ->requestEngineSetup(
+ std::dynamic_pointer_cast(m_pJSEngine));
+
+ if (!availableScreens.value(screen.identifier)->isValid()) {
+ qCWarning(m_logger) << QString(
+ "Unable to setup the screen render for %1.")
+ .arg(screen.identifier);
+ availableScreens.value(screen.identifier)->stop();
+ return false;
+ }
+ }
+ } else if (!m_infoScreens.isEmpty()) {
+ qCWarning(m_logger) << "Controller mapping has screen definitions but no QML "
+ "files to render on it. Ignoring.";
+ }
+#endif
+
// Binary data is passed from the Controller as a QByteArray, which
// QJSEngine::toScriptValue converts to an ArrayBuffer in JavaScript.
// ArrayBuffer cannot be accessed with the [] operator in JS; it needs
@@ -139,15 +424,90 @@ bool ControllerScriptEngineLegacy::initialize() {
engineGlobalObject.setProperty(
"engine", m_pJSEngine->newQObject(legacyScriptInterface));
+#ifdef MIXXX_USE_QML
+ if (m_bQmlMode) {
+ for (const LegacyControllerMapping::QMLModuleInfo& module :
+ std::as_const(m_modules)) {
+ auto path = module.dirinfo.absoluteFilePath();
+ QDirIterator it(path,
+ {"*.qml"},
+ QDir::Files,
+ QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ watchFilePath(it.next());
+ }
+ watchFilePath(path);
+ auto pQmlEngine = std::dynamic_pointer_cast(m_pJSEngine);
+ pQmlEngine->addImportPath(path);
+ qCWarning(m_logger) << pQmlEngine->importPathList();
+ }
+ } else if (!m_modules.isEmpty()) {
+ 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)) {
- if (!evaluateScriptFile(script.file)) {
- shutdown();
- return false;
+#ifdef MIXXX_USE_QML
+ if (script.type == LegacyControllerMapping::ScriptFileInfo::Type::Javascript) {
+#endif
+ if (!evaluateScriptFile(script.file)) {
+ shutdown();
+ return false;
+ }
+ if (!script.identifier.isEmpty()) {
+ m_scriptFunctionPrefixes.append(script.identifier);
+ }
+#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!";
+
+ sceneBindingHasFailure = true;
+ break;
+ }
+ if (!bindSceneToScreen(script,
+ script.identifier,
+ availableScreens.take(script.identifier))) {
+ sceneBindingHasFailure = true;
+ }
+ }
}
- if (!script.functionPrefix.isEmpty()) {
- m_scriptFunctionPrefixes.append(script.functionPrefix);
+ }
+
+ 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
+ }
// For testing, do not actually initialize the scripts, just check for
// syntax errors above.
@@ -165,26 +525,257 @@ bool ControllerScriptEngineLegacy::initialize() {
wrapFunctionCode(functionName, 2)));
}
- // m_pController is nullptr in tests.
- const auto controllerName = m_pController ? m_pController->getName() : QString{};
- const auto args = QJSValueList{
- controllerName,
- m_logger().isDebugEnabled(),
- };
- if (!callFunctionOnObjects(m_scriptFunctionPrefixes, "init", args, true)) {
+#ifdef MIXXX_USE_QML
+ m_engineThreadControl.setCanPause(true);
+ for (const auto& pScreen : std::as_const(m_renderingScreens)) {
+ pScreen->start();
+ }
+#endif
+
+ if (!callInitFunction()) {
shutdown();
return false;
}
+#ifdef MIXXX_USE_QML
+ // At runtime, QML errors aren't considered fatal anymore now that the engine has started.
+ setErrorsAreFatal(false);
+#endif
+
+ return true;
+}
+
+#ifdef MIXXX_USE_QML
+void ControllerScriptEngineLegacy::extractTransformFunction(
+ const QMetaObject* metaObject, const QString& screenIdentifier) {
+ VERIFY_OR_DEBUG_ASSERT(metaObject) {
+ qCWarning(m_logger)
+ << "Invalid meta object for screen" << screenIdentifier
+ << "It may be that an unhandled issue occurred when importing "
+ "the scene.";
+ return;
+ }
+
+ QMetaMethod transformFunction;
+ bool typed = false;
+ int methodIdx = metaObject->indexOfMethod(kScreenTransformFunctionUntypedSignature);
+
+ if (methodIdx == -1 || !metaObject->method(methodIdx).isValid()) {
+ qCDebug(m_logger) << "QML Scene for screen" << screenIdentifier
+ << "has no valid untyped transformFrame method.";
+ methodIdx = metaObject->indexOfMethod(kScreenTransformFunctionTypedSignature);
+ typed = true;
+ }
+
+ transformFunction = metaObject->method(methodIdx);
+
+ if (!transformFunction.isValid()) {
+ qCDebug(m_logger) << "QML Scene for screen" << screenIdentifier
+ << "has no valid typed transformFrame method. The "
+ "frame data will be sent "
+ "untransformed";
+ QStringList methods;
+ for (int i = metaObject->methodOffset(); i < metaObject->methodCount(); ++i) {
+ methods << QString::fromLatin1(metaObject->method(i).methodSignature());
+ }
+ qCDebug(m_logger) << "Found methods are: " << methods.join(", ");
+ }
+
+ m_transformScreenFrameFunctions.insert(screenIdentifier,
+ TransformScreenFrameFunction{transformFunction, typed});
+}
+
+bool ControllerScriptEngineLegacy::bindSceneToScreen(
+ const LegacyControllerMapping::ScriptFileInfo& qmlFile,
+ 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()) {
+ qCWarning(m_logger) << "Unable to stop the screen";
+ };
+ return false;
+ }
+ const QMetaObject* metaObject = pScene->metaObject();
+
+ extractTransformFunction(metaObject, screenIdentifier);
+ connect(pScreen.get(),
+ &ControllerRenderingEngine::frameRendered,
+ this,
+ &ControllerScriptEngineLegacy::handleScreenFrame);
+ m_renderingScreens.insert(screenIdentifier, pScreen);
+ m_rootItems.insert(screenIdentifier, pScene);
+ // In case a rendering issue occurs, we need to shutdown the controller
+ // since its only purpose is to render screens. This might not be the case
+ // in the future controller modules
+ connect(pScreen.get(),
+ &ControllerRenderingEngine::stopping,
+ this,
+ &ControllerScriptEngineLegacy::shutdown);
return true;
}
+void ControllerScriptEngineLegacy::handleScreenFrame(
+ const LegacyControllerMapping::ScreenInfo& screenInfo,
+ const QImage& frame,
+ const QDateTime& timestamp) {
+ VERIFY_OR_DEBUG_ASSERT(
+ m_transformScreenFrameFunctions.contains(screenInfo.identifier) ||
+ m_renderingScreens.contains(screenInfo.identifier)) {
+ qCWarning(m_logger) << "Unable to find transform function info for the given screen";
+ return;
+ };
+ VERIFY_OR_DEBUG_ASSERT(m_rootItems.contains(screenInfo.identifier)) {
+ qCWarning(m_logger) << "Unable to find a root item for the given screen";
+ return;
+ };
+
+ if (CmdlineArgs::Instance().getControllerPreviewScreens()) {
+ QImage screenDebug(frame);
+
+ switch (screenInfo.endian) {
+ case LegacyControllerMapping::ScreenInfo::ColorEndian::Big:
+ qFromBigEndian(frame.constBits(),
+ frame.sizeInBytes() / 2,
+ screenDebug.bits());
+ break;
+ case LegacyControllerMapping::ScreenInfo::ColorEndian::Little:
+ qFromLittleEndian(frame.constBits(),
+ frame.sizeInBytes() / 2,
+ screenDebug.bits());
+ break;
+ default:
+ break;
+ }
+ if (screenInfo.reversedColor) {
+ screenDebug.rgbSwap();
+ }
+
+ emit previewRenderedScreen(screenInfo, screenDebug);
+ }
+
+ QByteArray input(std::bit_cast(frame.constBits()), frame.sizeInBytes());
+ const TransformScreenFrameFunction& transformMethod =
+ m_transformScreenFrameFunctions[screenInfo.identifier];
+
+ if (!transformMethod.method.isValid() && screenInfo.rawData) {
+ m_renderingScreens[screenInfo.identifier]->requestSendingFrameData(m_pController, input);
+ return;
+ }
+
+ if (!transformMethod.method.isValid()) {
+ qCWarning(m_logger)
+ << "Could not find a valid transform function but the screen "
+ "doesn't accept raw data. Aborting screen rendering.";
+ m_renderingScreens[screenInfo.identifier]->stop();
+ return;
+ }
+
+ QVariant returnedValue;
+
+ VERIFY_OR_DEBUG_ASSERT(!m_pJSEngine->hasError()) {
+ qCWarning(m_logger) << "Controller JS engine has an unhandled error. Discarding.";
+ qCDebug(m_logger) << "Controller JS error is:" << m_pJSEngine->catchError().toString();
+ }
+ // During the frame transformation, any QML errors are considered fatal.
+ setErrorsAreFatal(true);
+ bool isSuccessful = transformMethod.typed
+ ? transformMethod.method.invoke(
+ m_rootItems.value(screenInfo.identifier).get(),
+ Qt::DirectConnection,
+ Q_RETURN_ARG(QVariant, returnedValue),
+ Q_ARG(QVariant, input),
+ Q_ARG(QDateTime, timestamp))
+ : transformMethod.method.invoke(
+ m_rootItems.value(screenInfo.identifier).get(),
+ Qt::DirectConnection,
+ Q_RETURN_ARG(QVariant, returnedValue),
+ Q_ARG(QVariant, input),
+ Q_ARG(QVariant, timestamp));
+ setErrorsAreFatal(false);
+
+ if (!isSuccessful) {
+ qCWarning(m_logger) << "Could not transform rendering buffer for screen"
+ << screenInfo.identifier;
+
+ // We manually stop the screen before we trigger the shutdown procedure
+ // as this last one may continue rendering process in order to perform
+ // screen splash off.
+ shutdown();
+ return;
+ }
+ if (!isSuccessful || !returnedValue.isValid()) {
+ qCWarning(m_logger) << "Could not transform rendering buffer. The transform "
+ "function didn't return the expected Array. Stopping "
+ "rendering on this screen";
+ return;
+ }
+
+ QByteArray transformedFrame;
+
+ if (returnedValue.canView()) {
+ transformedFrame = returnedValue.view();
+ } else if (returnedValue.canConvert()) {
+ transformedFrame = returnedValue.toByteArray();
+ } else {
+ qCWarning(m_logger) << "Unable to interpret the returned data " << returnedValue;
+ return;
+ }
+
+ if (CmdlineArgs::Instance().getControllerDebug()) {
+ qCDebug(m_logger) << "Transform screen data for screen " << screenInfo.identifier
+ << "(first 64 bytes)"
+ << QByteArray(transformedFrame.toHex(' '), 128);
+ m_pController->sendBytes(returnedValue.view());
+ }
+
+ m_renderingScreens[screenInfo.identifier]->requestSendingFrameData(
+ m_pController, transformedFrame);
+}
+#endif
+
void ControllerScriptEngineLegacy::shutdown() {
- // There is no js engine if the mapping was not loaded from a file but by
- // creating a new, empty mapping LegacyMidiControllerMapping with the wizard
- if (m_pJSEngine) {
- callFunctionOnObjects(m_scriptFunctionPrefixes, "shutdown");
+ callShutdownFunction();
+
+#ifdef MIXXX_USE_QML
+ m_engineThreadControl.setCanPause(false);
+ // Wait till the splash off animation has finished rendering.
+ std::chrono::milliseconds maxSplashOffDuration{};
+ for (const auto& pScreen : std::as_const(m_renderingScreens)) {
+ if (!pScreen->isRunning()) {
+ continue;
+ }
+ maxSplashOffDuration = std::max(maxSplashOffDuration, pScreen->info().splash_off);
+ }
+
+ auto splashOffDeadline = Clock::now() + maxSplashOffDuration;
+ while (splashOffDeadline > Clock::now()) {
+ QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents,
+ std::chrono::duration_cast(
+ splashOffDeadline - Clock::now())
+ .count());
+ }
+
+ m_rootItems.clear();
+ for (const auto& pScreen : std::as_const(m_renderingScreens)) {
+ // When stopping, the rendering engine emits an event which triggers the
+ // shutdown in case it was initiated following a rendering issue. We
+ // need to disconnect first before stopping.
+ pScreen->disconnect(this);
+ VERIFY_OR_DEBUG_ASSERT(!pScreen->isValid() ||
+ !pScreen->isRunning() || pScreen->stop()) {
+ qCWarning(m_logger) << "Unable to stop the screen";
+ };
}
+ m_renderingScreens.clear();
+ m_transformScreenFrameFunctions.clear();
+#endif
m_scriptWrappedFunctionCache.clear();
m_incomingDataFunctions.clear();
m_scriptFunctionPrefixes.clear();
@@ -226,10 +817,7 @@ bool ControllerScriptEngineLegacy::evaluateScriptFile(const QFileInfo& scriptFil
// 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.
- if (!m_fileWatcher.addPath(scriptFile.absoluteFilePath())) {
- qCWarning(m_logger) << "Failed to watch script file" << scriptFile.absoluteFilePath();
- };
-
+ watchFilePath(scriptFile.absoluteFilePath());
qCDebug(m_logger) << "Loading"
<< scriptFile.absoluteFilePath();
@@ -278,6 +866,90 @@ bool ControllerScriptEngineLegacy::evaluateScriptFile(const QFileInfo& scriptFil
return true;
}
+#ifdef MIXXX_USE_QML
+std::shared_ptr ControllerScriptEngineLegacy::loadQMLFile(
+ const LegacyControllerMapping::ScriptFileInfo& qmlScript,
+ std::shared_ptr pScreen) {
+ VERIFY_OR_DEBUG_ASSERT(m_pJSEngine ||
+ qmlScript.type !=
+ LegacyControllerMapping::ScriptFileInfo::Type::Qml) {
+ return nullptr;
+ }
+
+ 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()
+ << "does not exist.";
+ return nullptr;
+ }
+
+ QDir dir(m_resourcePath + "/qml/");
+
+ scene.open(QIODevice::ReadOnly);
+ qmlComponent.setData(scene.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();
+
+ while (qmlComponent.isLoading()) {
+ qCDebug(m_logger) << "Waiting for component "
+ << qmlScript.file.absoluteFilePath()
+ << " to be ready: " << qmlComponent.progress();
+ QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents, 500);
+ }
+
+ if (qmlComponent.isError()) {
+ const QList errorList = qmlComponent.errors();
+ for (const QQmlError& error : errorList) {
+ qCWarning(m_logger) << "Unable to load the QML scene:" << error.url()
+ << "at line" << error.line() << ", error: " << error;
+ showQMLExceptionDialog(error, true);
+ }
+ return nullptr;
+ }
+
+ VERIFY_OR_DEBUG_ASSERT(qmlComponent.isReady()) {
+ qCWarning(m_logger) << "QMLComponent isn't ready although synchronous load was requested.";
+ return nullptr;
+ }
+
+ 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;
+ }
+ return nullptr;
+ }
+
+ std::shared_ptr rootItem =
+ std::shared_ptr(qobject_cast(pRootObject));
+ if (!rootItem) {
+ qWarning("run: Not a QQuickItem");
+ delete pRootObject;
+ return nullptr;
+ }
+
+ watchFilePath(qmlScript.file.absoluteFilePath());
+
+ // The root item is ready. Associate it with the window.
+ if (!m_bTesting) {
+ rootItem->setParentItem(pScreen->quickWindow()->contentItem());
+
+ rootItem->setWidth(pScreen->quickWindow()->width());
+ rootItem->setHeight(pScreen->quickWindow()->height());
+ }
+
+ return rootItem;
+}
+#endif
+
QJSValue ControllerScriptEngineLegacy::wrapArrayBufferCallback(const QJSValue& callback) {
return m_makeArrayBufferWrapperFunction.call(QJSValueList{callback});
}
diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h
index 2491ff78a52b..b0605f4b584e 100644
--- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h
+++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h
@@ -4,10 +4,18 @@
#include
#include
#include
+#ifdef MIXXX_USE_QML
+#include
+#endif
#include "controllers/legacycontrollermapping.h"
#include "controllers/scripting/controllerscriptenginebase.h"
+#ifdef MIXXX_USE_QML
+class QQuickItem;
+class ControllerRenderingEngine;
+#endif
+
/// ControllerScriptEngineLegacy loads and executes controller scripts for the legacy
/// JS/XML hybrid controller mapping system.
class ControllerScriptEngineLegacy : public ControllerScriptEngineBase {
@@ -42,6 +50,26 @@ class ControllerScriptEngineLegacy : public ControllerScriptEngineBase {
void setSettings(
const QList>& settings);
+#ifdef MIXXX_USE_QML
+ void setModulePaths(const QList& scripts);
+ void setInfoScreens(const QList& scripts);
+ void setResourcePath(const QString& resourcePath) {
+ m_resourcePath = resourcePath;
+ }
+
+ private slots:
+ void handleScreenFrame(
+ const LegacyControllerMapping::ScreenInfo& screeninfo,
+ const QImage& frame,
+ const QDateTime& timestamp);
+
+ signals:
+ /// Emitted when a screen has been rendered.
+ // TODO (XXX) Move this signal in ControllerScriptEngineBase when ScreenInfo
+ // isn't tight to LegacyControllerMapping anymore.
+ void previewRenderedScreen(const LegacyControllerMapping::ScreenInfo& screen, QImage frame);
+#endif
+
private:
struct Setting {
QString name;
@@ -49,16 +77,50 @@ class ControllerScriptEngineLegacy : public ControllerScriptEngineBase {
};
bool evaluateScriptFile(const QFileInfo& scriptFile);
+#ifdef MIXXX_USE_QML
+ bool bindSceneToScreen(
+ const LegacyControllerMapping::ScriptFileInfo& qmlFile,
+ const QString& screenIdentifier,
+ std::shared_ptr pScreen);
+ void extractTransformFunction(const QMetaObject* metaObject, const QString& screenIdentifier);
+
+ std::shared_ptr loadQMLFile(
+ const LegacyControllerMapping::ScriptFileInfo& qmlScript,
+ std::shared_ptr pScreen);
+
+ struct TransformScreenFrameFunction {
+ QMetaMethod method;
+ bool typed;
+ };
+#endif
+
+ /// @brief Call the shutdown hook on the mapping script.
+ /// @return true if the hook was run successfully, or if there was none.
+ bool callShutdownFunction();
+ /// @brief Call the init hook on the mapping script.
+ /// @return true if the hook was run successfully, or if there was none.
+ bool callInitFunction();
void shutdown() override;
-
QJSValue wrapArrayBufferCallback(const QJSValue& callback);
bool callFunctionOnObjects(const QList& scriptFunctionPrefixes,
const QString&,
const QJSValueList& args = {},
bool bFatalError = false);
+ void watchFilePath(const QString& path);
QJSValue m_makeArrayBufferWrapperFunction;
QList m_scriptFunctionPrefixes;
+#ifdef MIXXX_USE_QML
+ QHash> m_renderingScreens;
+ // Contains all the scenes loaded for this mapping. Key is the scene
+ // identifier (LegacyControllerMapping::ScreenInfo::identifier), value in
+ // the QML root item
+ QHash> m_rootItems;
+ QHash m_transformScreenFrameFunctions;
+ QList m_modules;
+ QList m_infoScreens;
+ QString m_resourcePath{QStringLiteral(".")};
+#endif
QList m_incomingDataFunctions;
QHash m_scriptWrappedFunctionCache;
QList m_scriptFiles;
diff --git a/src/coreservices.cpp b/src/coreservices.cpp
index 82af9c68f6e6..d1f807d1c9d5 100644
--- a/src/coreservices.cpp
+++ b/src/coreservices.cpp
@@ -28,6 +28,20 @@
#include "preferences/dialog/dlgprefmodplug.h"
#endif
#include "skin/skincontrols.h"
+#ifdef MIXXX_USE_QML
+#include
+#include
+
+#include "controllers/scripting/controllerscriptenginebase.h"
+#include "qml/qmlconfigproxy.h"
+#include "qml/qmlcontrolproxy.h"
+#include "qml/qmldlgpreferencesproxy.h"
+#include "qml/qmleffectslotproxy.h"
+#include "qml/qmleffectsmanagerproxy.h"
+#include "qml/qmllibraryproxy.h"
+#include "qml/qmlplayermanagerproxy.h"
+#include "qml/qmlplayerproxy.h"
+#endif
#include "soundio/soundmanager.h"
#include "sources/soundsourceproxy.h"
#include "util/clipboard.h"
@@ -448,6 +462,33 @@ void CoreServices::initialize(QApplication* pApp) {
}
m_isInitialized = true;
+
+#ifdef MIXXX_USE_QML
+ initializeQMLSingletons();
+}
+
+void CoreServices::initializeQMLSingletons() {
+ // Any uncreateable non-singleton types registered here require
+ // arguments that we don't want to expose to QML directly. Instead, they
+ // can be retrieved by member properties or methods from the singleton
+ // types.
+ //
+ // The alternative would be to register their *arguments* in the QML
+ // system, which would improve nothing, or we had to expose them as
+ // singletons to that they can be accessed by components instantiated by
+ // QML, which would also be suboptimal.
+ mixxx::qml::QmlEffectsManagerProxy::registerEffectsManager(getEffectsManager());
+ mixxx::qml::QmlPlayerManagerProxy::registerPlayerManager(getPlayerManager());
+ mixxx::qml::QmlConfigProxy::registerUserSettings(getSettings());
+ mixxx::qml::QmlLibraryProxy::registerLibrary(getLibrary());
+
+ ControllerScriptEngineBase::registerTrackCollectionManager(getTrackCollectionManager());
+
+ // Currently, it is required to enforce QQuickWindow RHI backend to use
+ // OpenGL on all platforms to allow offscreen rendering to function as
+ // expected
+ QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
+#endif
}
void CoreServices::initializeKeyboard() {
@@ -555,6 +596,16 @@ void CoreServices::finalize() {
Timer t("CoreServices::~CoreServices");
t.start();
+#ifdef MIXXX_USE_QML
+ // Delete all the QML singletons in order to prevent controller leaks
+ mixxx::qml::QmlEffectsManagerProxy::registerEffectsManager(nullptr);
+ mixxx::qml::QmlPlayerManagerProxy::registerPlayerManager(nullptr);
+ mixxx::qml::QmlConfigProxy::registerUserSettings(nullptr);
+ mixxx::qml::QmlLibraryProxy::registerLibrary(nullptr);
+
+ ControllerScriptEngineBase::registerTrackCollectionManager(nullptr);
+#endif
+
// Stop all pending library operations
qDebug() << t.elapsed(false).debugMillisWithUnit() << "stopping pending Library tasks";
m_pTrackCollectionManager->stopLibraryScan();
diff --git a/src/coreservices.h b/src/coreservices.h
index b5d5325c4d31..d0c1a2499378 100644
--- a/src/coreservices.h
+++ b/src/coreservices.h
@@ -115,6 +115,9 @@ class CoreServices : public QObject {
void initializeSettings();
void initializeScreensaverManager();
void initializeLogging();
+#ifdef MIXXX_USE_QML
+ void initializeQMLSingletons();
+#endif
/// Tear down CoreServices that were previously initialized by `initialize()`.
void finalize();
diff --git a/src/qml/qmlapplication.cpp b/src/qml/qmlapplication.cpp
index 11ace8aa0554..dbae62d83dea 100644
--- a/src/qml/qmlapplication.cpp
+++ b/src/qml/qmlapplication.cpp
@@ -65,20 +65,6 @@ QmlApplication::QmlApplication(
// Since DlgPreferences is only meant to be used in the main QML engine, it
// follows a strict singleton pattern design
QmlDlgPreferencesProxy::s_pInstance = new QmlDlgPreferencesProxy(pDlgPreferences, this);
-
- // Any uncreateable non-singleton types registered here require arguments
- // that we don't want to expose to QML directly. Instead, they can be
- // retrieved by member properties or methods from the singleton types.
- //
- // The alternative would be to register their *arguments* in the QML
- // system, which would improve nothing, or we had to expose them as
- // singletons to that they can be accessed by components instantiated by
- // QML, which would also be suboptimal.
- QmlEffectsManagerProxy::registerEffectsManager(pCoreServices->getEffectsManager());
- QmlPlayerManagerProxy::registerPlayerManager(pCoreServices->getPlayerManager());
- QmlConfigProxy::registerUserSettings(pCoreServices->getSettings());
- QmlLibraryProxy::registerLibrary(pCoreServices->getLibrary());
-
loadQml(m_mainFilePath);
pCoreServices->getControllerManager()->setUpDevices();
@@ -93,10 +79,6 @@ QmlApplication::QmlApplication(
QmlApplication::~QmlApplication() {
// Delete all the QML singletons in order to prevent leak detection in CoreService
- QmlEffectsManagerProxy::registerEffectsManager(nullptr);
- QmlPlayerManagerProxy::registerPlayerManager(nullptr);
- QmlConfigProxy::registerUserSettings(nullptr);
- QmlLibraryProxy::registerLibrary(nullptr);
QmlDlgPreferencesProxy::s_pInstance->deleteLater();
}
diff --git a/src/test/controller_mapping_file_handler_test.cpp b/src/test/controller_mapping_file_handler_test.cpp
new file mode 100644
index 000000000000..57dd8594f347
--- /dev/null
+++ b/src/test/controller_mapping_file_handler_test.cpp
@@ -0,0 +1,1003 @@
+#include
+#include
+
+#include
+#include
+
+#include "controllers/defs_controllers.h"
+#include "controllers/legacycontrollermapping.h"
+#include "controllers/legacycontrollermappingfilehandler.h"
+#include "helpers/log_test.h"
+#include "test/mixxxtest.h"
+#include "util/time.h"
+
+using ::testing::_;
+using ::testing::FieldsAre;
+
+class LegacyControllerMappingFileHandlerTest
+ : public LegacyControllerMappingFileHandler,
+ public MixxxTest {
+ public:
+ void SetUp() override {
+ mixxx::Time::setTestMode(true);
+ mixxx::Time::setTestElapsedTime(mixxx::Duration::fromMillis(10));
+ SETUP_LOG_CAPTURE();
+ }
+
+ void TearDown() override {
+ mixxx::Time::setTestMode(false);
+ }
+ std::shared_ptr load(const QDomElement&,
+ const QString&,
+ const QDir&) override {
+ throw std::runtime_error("not implemented");
+ }
+ static QFileInfo findLibraryPath(std::shared_ptr,
+ const QString& dirname,
+ const QDir&) {
+ return QFileInfo(QDir("/dummy/path/").absoluteFilePath(dirname));
+ }
+ static QFileInfo findScriptFile(std::shared_ptr,
+ const QString& filename,
+ const QDir&) {
+ return QFileInfo(QDir("/dummy/path/").absoluteFilePath(filename));
+ }
+};
+
+class MockLegacyControllerMapping : public LegacyControllerMapping {
+ public:
+ MOCK_METHOD(void,
+ addScriptFile,
+ (LegacyControllerMapping::ScriptFileInfo info),
+ (override));
+ MOCK_METHOD(void,
+ addScreenInfo,
+ (LegacyControllerMapping::ScreenInfo info),
+ (override));
+ MOCK_METHOD(void, addModule, (const QFileInfo& dirinfo, bool builtin), (override));
+
+ std::shared_ptr clone() const override {
+ throw std::runtime_error("not implemented");
+ }
+ bool saveMapping(const QString&) const override {
+ throw std::runtime_error("not implemented");
+ }
+ bool isMappable() const override {
+ throw std::runtime_error("not implemented");
+ }
+};
+
+TEST_F(LegacyControllerMappingFileHandlerTest, canParseSimpleMapping) {
+ QDomDocument doc;
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ auto mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("DummyDeviceDefaultScreen.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ false)));
+ EXPECT_CALL(*mapping, addScreenInfo(_)).Times(0);
+ EXPECT_CALL(*mapping, addModule(_, _)).Times(0);
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+}
+
+TEST_F(LegacyControllerMappingFileHandlerTest, canParseScreenMapping) {
+ QDomDocument doc;
+ doc.setContent(QByteArray(R"EOF(
+
+
+
+
+
+
+
+
+
+
+
+ )EOF"));
+
+ auto mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("DummyDeviceDefaultScreen.qml"),
+ QString(""),
+ QFileInfo("/dummy/path/DummyDeviceDefaultScreen.qml"),
+ LegacyControllerMapping::ScriptFileInfo::Type::Qml,
+ false)));
+ EXPECT_CALL(*mapping,
+ addScreenInfo(FieldsAre(QString("main"),
+ QSize(480, 360),
+ 20,
+ 1,
+ std::chrono::milliseconds(2000),
+ QImage::Format_RGBA8888,
+ LegacyControllerMapping::ScreenInfo::ColorEndian::Little,
+ false,
+ false)));
+ EXPECT_CALL(*mapping, addModule(QFileInfo("/dummy/path/foobar"), false));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+}
+
+TEST_F(LegacyControllerMappingFileHandlerTest, screenMappingTargetFPS) {
+ QDomDocument doc;
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ auto mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping, addScreenInfo(FieldsAre(_, _, 20, _, _, _, _, _, _)));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ ASSERT_ALL_EXPECTED_MSG();
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping, addScreenInfo(_)).Times(0);
+ EXPECT_LOG_MSG(QtWarningMsg,
+ QString("Invalid target FPS. Target FPS must be between 1 and %0")
+ .arg(kMaxTargetFps));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ ASSERT_ALL_EXPECTED_MSG();
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_LOG_MSG(QtWarningMsg,
+ QString("Unable to parse the field \"targetFps\" as an unsigned "
+ "integer in the screen definition."));
+
+ EXPECT_CALL(*mapping, addScreenInfo(_)).Times(0);
+ EXPECT_LOG_MSG(
+ QtWarningMsg,
+ QString("Invalid target FPS. Target FPS must be between 1 and %0")
+ .arg(kMaxTargetFps));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ ASSERT_ALL_EXPECTED_MSG();
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+
+ EXPECT_CALL(*mapping, addScreenInfo(_)).Times(0);
+ EXPECT_LOG_MSG(
+ QtWarningMsg,
+ QString("Invalid target FPS. Target FPS must be between 1 and %0")
+ .arg(kMaxTargetFps));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ ASSERT_ALL_EXPECTED_MSG();
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_LOG_MSG(QtWarningMsg,
+ QString("Unable to parse the field \"targetFps\" as an unsigned "
+ "integer in the screen definition."));
+
+ EXPECT_CALL(*mapping, addScreenInfo(_)).Times(0);
+ EXPECT_LOG_MSG(
+ QtWarningMsg,
+ QString("Invalid target FPS. Target FPS must be between 1 and %0")
+ .arg(kMaxTargetFps));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ ASSERT_ALL_EXPECTED_MSG();
+}
+
+TEST_F(LegacyControllerMappingFileHandlerTest, screenMappingSize) {
+ QDomDocument doc;
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ auto mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping, addScreenInfo(FieldsAre(_, QSize(10, 10), _, _, _, _, _, _, _)));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ ASSERT_ALL_EXPECTED_MSG();
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping, addScreenInfo(_)).Times(0);
+ EXPECT_LOG_MSG(
+ QtWarningMsg,
+ "Invalid screen size. Screen size must have a width and height above 1 pixel");
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ ASSERT_ALL_EXPECTED_MSG();
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_LOG_MSG(QtWarningMsg,
+ QString("Unable to parse the field \"height\" as an unsigned "
+ "integer in the screen definition."));
+ EXPECT_CALL(*mapping, addScreenInfo(_)).Times(0);
+ EXPECT_LOG_MSG(
+ QtWarningMsg,
+ "Invalid screen size. Screen size must have a width and height above 1 pixel");
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ ASSERT_ALL_EXPECTED_MSG();
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_LOG_MSG(QtWarningMsg,
+ QString("Unable to parse the field \"height\" as an unsigned "
+ "integer in the screen definition."));
+ EXPECT_CALL(*mapping, addScreenInfo(_)).Times(0);
+ EXPECT_LOG_MSG(
+ QtWarningMsg,
+ "Invalid screen size. Screen size must have a width and height above 1 pixel");
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ ASSERT_ALL_EXPECTED_MSG();
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping, addScreenInfo(_)).Times(0);
+ EXPECT_LOG_MSG(
+ QtWarningMsg,
+ "Invalid screen size. Screen size must have a width and height above 1 pixel");
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ ASSERT_ALL_EXPECTED_MSG();
+}
+
+TEST_F(LegacyControllerMappingFileHandlerTest, screenMappingBitFormatDefinition) {
+ // pixelType
+ // endian
+
+ // No pixel type default to RGB 8-bits depth
+ QDomDocument doc;
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ auto mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping, addScreenInfo(FieldsAre(_, _, _, _, _, QImage::Format_RGB888, _, _, _)));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping, addScreenInfo(FieldsAre(_, _, _, _, _, QImage::Format_RGB16, _, _, _)));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping, addScreenInfo(_)).Times(0);
+ EXPECT_LOG_MSG(
+ QtWarningMsg,
+ "Unsupported pixel format \"FOOBAR\"");
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ ASSERT_ALL_EXPECTED_MSG();
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping,
+ addScreenInfo(FieldsAre(_,
+ _,
+ _,
+ _,
+ _,
+ _,
+ LegacyControllerMapping::ScreenInfo::ColorEndian::Little,
+ _,
+ _)));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping,
+ addScreenInfo(FieldsAre(_,
+ _,
+ _,
+ _,
+ _,
+ _,
+ LegacyControllerMapping::ScreenInfo::ColorEndian::Little,
+ _,
+ _)));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping,
+ addScreenInfo(FieldsAre(_,
+ _,
+ _,
+ _,
+ _,
+ _,
+ LegacyControllerMapping::ScreenInfo::ColorEndian::Big,
+ _,
+ _)));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping, addScreenInfo(_)).Times(0);
+ EXPECT_LOG_MSG(
+ QtWarningMsg,
+ "Unknown endian format \"enormous\"");
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ ASSERT_ALL_EXPECTED_MSG();
+}
+
+TEST_F(LegacyControllerMappingFileHandlerTest, screenMappingExtraBoolPropertiesDefinition) {
+ bool kExpectedWarning[] = {false, false, true, true, true, true, true};
+ QStringList kFalseValue = {"false", "FALse", "no", "yes", "1", "nope", "maybe"};
+ QStringList kTrueValue = {"true", "trUe", "TRUE "};
+ QDomDocument doc;
+ std::shared_ptr mapping;
+
+ // reversed
+ bool* expectedWarning = &kExpectedWarning[0];
+ for (const QString& falseValue : std::as_const(kFalseValue)) {
+ doc.setContent(
+ QString(R"EOF(
+
+
+
+
+
+ )EOF")
+ .arg(falseValue)
+ .toUtf8());
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ if (expectedWarning++) {
+ EXPECT_LOG_MSG(QtWarningMsg,
+ QString("Unable to parse the field \"reversed\" as a "
+ "boolean in the screen definition."));
+ }
+ EXPECT_CALL(*mapping, addScreenInfo(FieldsAre(_, _, _, _, _, _, _, false, _)));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ }
+ for (const QString& falseValue : std::as_const(kTrueValue)) {
+ doc.setContent(
+ QString(R"EOF(
+
+
+
+
+
+ )EOF")
+ .arg(falseValue)
+ .toUtf8());
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping, addScreenInfo(FieldsAre(_, _, _, _, _, _, _, true, _)));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ }
+ // raw
+ expectedWarning = &kExpectedWarning[0];
+ for (const QString& falseValue : std::as_const(kFalseValue)) {
+ doc.setContent(
+ QString(R"EOF(
+
+
+
+
+
+ )EOF")
+ .arg(falseValue)
+ .toUtf8());
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping, addScreenInfo(FieldsAre(_, _, _, _, _, _, _, _, false)));
+ if (expectedWarning++) {
+ EXPECT_LOG_MSG(QtWarningMsg,
+ QString("Unable to parse the field \"raw\" as a boolean in "
+ "the screen definition."));
+ }
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ }
+ for (const QString& falseValue : std::as_const(kTrueValue)) {
+ doc.setContent(
+ QString(R"EOF(
+
+
+
+
+
+ )EOF")
+ .arg(falseValue)
+ .toUtf8());
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping, addScreenInfo(FieldsAre(_, _, _, _, _, _, _, _, true)));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ }
+}
+
+TEST_F(LegacyControllerMappingFileHandlerTest, screenMappingExtraIntPropertiesDefinition) {
+ // splashoff
+ QDomDocument doc;
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ auto mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping,
+ addScreenInfo(FieldsAre(
+ _, _, _, _, std::chrono::milliseconds(0), _, _, _, _)));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping,
+ addScreenInfo(FieldsAre(
+ _, _, _, _, std::chrono::milliseconds(500), _, _, _, _)));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_CALL(*mapping,
+ addScreenInfo(FieldsAre(_,
+ _,
+ _,
+ _,
+ std::chrono::milliseconds(kMaxSplashOffDuration),
+ _,
+ _,
+ _,
+ _)));
+ EXPECT_LOG_MSG(
+ QtWarningMsg,
+ QString("Invalid splashoff duration. Splashoff duration must "
+ "be between 0 and %0. Clamping to %1")
+ .arg(kMaxSplashOffDuration)
+ .arg(kMaxSplashOffDuration));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+ ASSERT_ALL_EXPECTED_MSG();
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_LOG_MSG(QtWarningMsg,
+ QString("Unable to parse the field \"splashoff\" as an unsigned "
+ "integer in the screen definition."));
+ EXPECT_CALL(*mapping,
+ addScreenInfo(FieldsAre(
+ _, _, _, _, std::chrono::milliseconds(0), _, _, _, _)));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+
+ doc.setContent(
+ QByteArray(R"EOF(
+
+
+
+
+
+ )EOF"));
+
+ mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+ EXPECT_LOG_MSG(QtWarningMsg,
+ QString("Unable to parse the field \"splashoff\" as an unsigned "
+ "integer in the screen definition."));
+ EXPECT_CALL(*mapping,
+ addScreenInfo(FieldsAre(
+ _, _, _, _, std::chrono::milliseconds(0), _, _, _, _)));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+}
+
+TEST_F(LegacyControllerMappingFileHandlerTest, canParseHybridMapping) {
+ QDomDocument doc;
+
+ doc.setContent(QByteArray(R"EOF(
+
+
+
+
+
+
+
+
+
+
+
+
+ )EOF"));
+
+ auto mapping = std::make_shared();
+ // This file always gets added
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("common-controller-scripts.js"),
+ QString(""),
+ _, // gmock seems unable to assert QFileInfo
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ true)));
+
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("DummyDeviceDefaultScreen.qml"),
+ QString(""),
+ QFileInfo("/dummy/path/DummyDeviceDefaultScreen.qml"),
+ LegacyControllerMapping::ScriptFileInfo::Type::Qml,
+ false)));
+ EXPECT_CALL(*mapping,
+ addScriptFile(FieldsAre(QString("LegacyScript.js"),
+ QString(""),
+ QFileInfo("/dummy/path/LegacyScript.js"),
+ LegacyControllerMapping::ScriptFileInfo::Type::Javascript,
+ false)));
+ EXPECT_CALL(*mapping,
+ addScreenInfo(FieldsAre(QString("main"),
+ QSize(480, 360),
+ 20,
+ 1,
+ std::chrono::milliseconds(2000),
+ QImage::Format_RGBA8888,
+ LegacyControllerMapping::ScreenInfo::ColorEndian::Little,
+ false,
+ false)));
+ EXPECT_CALL(*mapping, addModule(QFileInfo("/dummy/path/foobar"), false));
+
+ addScriptFilesToMapping(
+ doc.documentElement(),
+ mapping,
+ QDir());
+}
diff --git a/src/test/controller_mapping_validation_test.cpp b/src/test/controller_mapping_validation_test.cpp
index 29be36d171c7..1ffbfad36285 100644
--- a/src/test/controller_mapping_validation_test.cpp
+++ b/src/test/controller_mapping_validation_test.cpp
@@ -1,9 +1,23 @@
#include "test/controller_mapping_validation_test.h"
+#include
+#include
+
#include
#include "controllers/defs_controllers.h"
#include "controllers/scripting/legacy/controllerscriptenginelegacy.h"
+#ifdef MIXXX_USE_QML
+#include "effects/effectsmanager.h"
+#include "engine/channelhandle.h"
+#include "engine/enginemixer.h"
+#include "library/coverartcache.h"
+#include "library/library.h"
+#include "mixer/playerinfo.h"
+#include "mixer/playermanager.h"
+#include "qml/qmlplayermanagerproxy.h"
+#include "soundio/soundmanager.h"
+#endif
#include "moc_controller_mapping_validation_test.cpp"
FakeMidiControllerJSProxy::FakeMidiControllerJSProxy()
@@ -107,10 +121,73 @@ bool FakeController::isMappable() const {
return false;
}
+#ifdef MIXXX_USE_QML
+void deleteTrack(Track* pTrack) {
+ // Delete track objects directly in unit tests with
+ // no main event loop
+ delete pTrack;
+};
+#endif
+
void LegacyControllerMappingValidationTest::SetUp() {
m_mappingPath = QDir::current();
m_mappingPath.cd("res/controllers");
m_pEnumerator.reset(new MappingInfoEnumerator(QList{m_mappingPath.absolutePath()}));
+#ifdef MIXXX_USE_QML
+ // This setup mirrors coreservices -- it would be nice if we could use coreservices instead
+ // but it does a lot of local disk / settings setup.
+ auto pChannelHandleFactory = std::make_shared();
+ m_pEffectsManager = std::make_shared(m_pConfig, pChannelHandleFactory);
+ m_pEngine = std::make_shared(
+ m_pConfig,
+ "[Master]",
+ m_pEffectsManager.get(),
+ pChannelHandleFactory,
+ true);
+ m_pSoundManager = std::make_shared(m_pConfig, m_pEngine.get());
+ m_pControlIndicatorTimer = std::make_shared(nullptr);
+ m_pEngine->registerNonEngineChannelSoundIO(m_pSoundManager.get());
+ m_pPlayerManager = std::make_shared(m_pConfig,
+ m_pSoundManager.get(),
+ m_pEffectsManager.get(),
+ m_pEngine.get());
+
+ m_pPlayerManager->addConfiguredDecks();
+ m_pPlayerManager->addSampler();
+ PlayerInfo::create();
+ m_pEffectsManager->setup();
+
+ const auto dbConnection = mixxx::DbConnectionPooled(dbConnectionPooler());
+ if (!MixxxDb::initDatabaseSchema(dbConnection)) {
+ exit(1);
+ }
+ m_pTrackCollectionManager = std::make_shared(
+ nullptr,
+ m_pConfig,
+ dbConnectionPooler(),
+ deleteTrack);
+
+ m_pRecordingManager = std::make_shared(m_pConfig, m_pEngine.get());
+ CoverArtCache::createInstance();
+ m_pLibrary = std::make_shared(
+ nullptr,
+ m_pConfig,
+ dbConnectionPooler(),
+ m_pTrackCollectionManager.get(),
+ m_pPlayerManager.get(),
+ m_pRecordingManager.get());
+
+ m_pPlayerManager->bindToLibrary(m_pLibrary.get());
+ mixxx::qml::QmlPlayerManagerProxy::registerPlayerManager(m_pPlayerManager);
+ ControllerScriptEngineBase::registerTrackCollectionManager(m_pTrackCollectionManager);
+}
+
+void LegacyControllerMappingValidationTest::TearDown() {
+ PlayerInfo::destroy();
+ CoverArtCache::destroy();
+ mixxx::qml::QmlPlayerManagerProxy::registerPlayerManager(nullptr);
+ ControllerScriptEngineBase::registerTrackCollectionManager(nullptr);
+#endif
}
bool LegacyControllerMappingValidationTest::testLoadMapping(const MappingInfo& mapping) {
@@ -123,7 +200,7 @@ bool LegacyControllerMappingValidationTest::testLoadMapping(const MappingInfo& m
FakeController controller;
controller.setMapping(pMapping);
- bool result = controller.applyMapping();
+ bool result = controller.applyMapping("./res");
controller.stopEngine();
return result;
}
diff --git a/src/test/controller_mapping_validation_test.h b/src/test/controller_mapping_validation_test.h
index da868da44cb1..b2596c51051a 100644
--- a/src/test/controller_mapping_validation_test.h
+++ b/src/test/controller_mapping_validation_test.h
@@ -2,11 +2,14 @@
#include
+#include "control/controlindicatortimer.h"
#include "controllers/controller.h"
#include "controllers/controllermappinginfoenumerator.h"
#include "controllers/hid/legacyhidcontrollermapping.h"
#include "controllers/midi/legacymidicontrollermapping.h"
-#include "test/mixxxtest.h"
+#include "library/trackcollectionmanager.h"
+#include "test/mixxxdbtest.h"
+#include "test/soundsourceproviderregistration.h"
class FakeMidiControllerJSProxy : public ControllerJSProxy {
Q_OBJECT
@@ -136,9 +139,41 @@ class FakeController : public Controller {
std::shared_ptr m_pHidMapping;
};
-class LegacyControllerMappingValidationTest : public MixxxTest {
+class EngineMixer;
+class EffectsManager;
+class SoundManager;
+class RecordingManager;
+class Library;
+class PlayerManager;
+
+// We can't inherit from LibraryTest because that creates a key_notation control object that is also
+// created by the Library object itself. The duplicated CO creation causes a debug assert.
+class LegacyControllerMappingValidationTest : public MixxxDbTest, SoundSourceProviderRegistration {
+ public:
+ LegacyControllerMappingValidationTest()
+ : MixxxDbTest(true) {
+ }
+
protected:
void SetUp() override;
+#ifdef MIXXX_USE_QML
+ void TearDown() override;
+
+ TrackPointer getOrAddTrackByLocation(
+ const QString& trackLocation) const {
+ return m_pTrackCollectionManager->getOrAddTrack(
+ TrackRef::fromFilePath(trackLocation));
+ }
+
+ std::shared_ptr m_pEffectsManager;
+ std::shared_ptr m_pControlIndicatorTimer;
+ std::shared_ptr m_pEngine;
+ std::shared_ptr m_pSoundManager;
+ std::shared_ptr m_pPlayerManager;
+ std::shared_ptr m_pTrackCollectionManager;
+ std::shared_ptr m_pRecordingManager;
+ std::shared_ptr m_pLibrary;
+#endif
bool testLoadMapping(const MappingInfo& mapping);
diff --git a/src/test/controllerrenderingengine_test.cpp b/src/test/controllerrenderingengine_test.cpp
new file mode 100644
index 000000000000..864497861f15
--- /dev/null
+++ b/src/test/controllerrenderingengine_test.cpp
@@ -0,0 +1,51 @@
+#include "controllers/rendering/controllerrenderingengine.h"
+
+#include
+#include
+
+#include
+#include
+
+#include "controllers/controllerenginethreadcontrol.h"
+#include "controllers/legacycontrollermappingfilehandler.h"
+#include "helpers/log_test.h"
+#include "test/mixxxtest.h"
+
+using ::testing::_;
+
+class ControllerRenderingEngineTest : public MixxxTest {
+ public:
+ void SetUp() override {
+ mixxx::Time::setTestMode(true);
+ mixxx::Time::setTestElapsedTime(mixxx::Duration::fromMillis(10));
+ SETUP_LOG_CAPTURE();
+ }
+
+ QList supportedPixelFormat() const {
+ return LegacyControllerMappingFileHandler::kSupportedPixelFormat.values();
+ }
+};
+
+class MockRenderingEngine : public ControllerRenderingEngine {
+ public:
+ MockRenderingEngine(const LegacyControllerMapping::ScreenInfo& info)
+ : ControllerRenderingEngine(info, new ControllerEngineThreadControl){};
+};
+
+TEST_F(ControllerRenderingEngineTest, createValidRendererWithSupportedTypes) {
+ for (auto pixelFormat : supportedPixelFormat()) {
+ MockRenderingEngine screenTest(LegacyControllerMapping::ScreenInfo{
+ "", // identifier
+ QSize(0, 0), // size
+ 10, // target_fps
+ 1, // msaa
+ std::chrono::milliseconds(10), // splash_off
+ pixelFormat, // pixelFormat
+ LegacyControllerMapping::ScreenInfo::ColorEndian::Big, // endian
+ false, // reversedColor
+ false // rawData
+ });
+ EXPECT_TRUE(screenTest.isValid());
+ EXPECT_TRUE(screenTest.stop());
+ }
+}
diff --git a/src/test/controllerscriptenginelegacy_test.cpp b/src/test/controllerscriptenginelegacy_test.cpp
index df55b0724129..9fd4c5e34f42 100644
--- a/src/test/controllerscriptenginelegacy_test.cpp
+++ b/src/test/controllerscriptenginelegacy_test.cpp
@@ -1,25 +1,41 @@
#include "controllers/scripting/legacy/controllerscriptenginelegacy.h"
+#include
+#include
+
#include
#include
#include
#include
+#include
#include
#include "control/controlobject.h"
#include "control/controlpotmeter.h"
+#ifdef MIXXX_USE_QML
+#include
+
+#include "controllers/controllerenginethreadcontrol.h"
+#include "controllers/rendering/controllerrenderingengine.h"
+#endif
#include "controllers/softtakeover.h"
+#include "helpers/log_test.h"
#include "preferences/usersettings.h"
#include "test/mixxxtest.h"
#include "util/color/colorpalette.h"
#include "util/time.h"
+using ::testing::_;
+
typedef std::unique_ptr ScopedTemporaryFile;
const RuntimeLoggingCategory logger(QString("test").toLocal8Bit());
-class ControllerScriptEngineLegacyTest : public MixxxTest {
+class ControllerScriptEngineLegacyTest : public ControllerScriptEngineLegacy, public MixxxTest {
protected:
+ ControllerScriptEngineLegacyTest()
+ : ControllerScriptEngineLegacy(nullptr, logger) {
+ }
static ScopedTemporaryFile makeTemporaryFile(const QString& contents) {
QByteArray contentsBa = contents.toLocal8Bit();
ScopedTemporaryFile pFile = std::make_unique();
@@ -33,21 +49,19 @@ class ControllerScriptEngineLegacyTest : public MixxxTest {
mixxx::Time::setTestMode(true);
mixxx::Time::setTestElapsedTime(mixxx::Duration::fromMillis(10));
QThread::currentThread()->setObjectName("Main");
- cEngine = new ControllerScriptEngineLegacy(nullptr, logger);
- cEngine->initialize();
+ initialize();
}
void TearDown() override {
- delete cEngine;
mixxx::Time::setTestMode(false);
}
bool evaluateScriptFile(const QFileInfo& scriptFile) {
- return cEngine->evaluateScriptFile(scriptFile);
+ return ControllerScriptEngineLegacy::evaluateScriptFile(scriptFile);
}
QJSValue evaluate(const QString& code) {
- return cEngine->jsEngine()->evaluate(code);
+ return jsEngine()->evaluate(code);
}
bool evaluateAndAssert(const QString& code) {
@@ -65,7 +79,31 @@ class ControllerScriptEngineLegacyTest : public MixxxTest {
application()->processEvents();
}
- ControllerScriptEngineLegacy* cEngine;
+#ifdef MIXXX_USE_QML
+ QHash& transformScreenFrameFunctions() {
+ return m_transformScreenFrameFunctions;
+ }
+
+ QHash>& renderingScreens() {
+ return m_renderingScreens;
+ }
+
+ QHash>& rootItems() {
+ return m_rootItems;
+ }
+
+ void testHandleScreen(
+ const LegacyControllerMapping::ScreenInfo& screeninfo,
+ const QImage& frame,
+ const QDateTime& timestamp) {
+ handleScreenFrame(screeninfo, frame, timestamp);
+ }
+
+ TransformScreenFrameFunction newTransformScreenFrameFunction(
+ QMetaMethod method, bool typed) const {
+ return TransformScreenFrameFunction{method, typed};
+ }
+#endif
};
TEST_F(ControllerScriptEngineLegacyTest, commonScriptHasNoErrors) {
@@ -620,3 +658,90 @@ TEST_F(ControllerScriptEngineLegacyTest, connectionExecutesWithCorrectThisObject
// The counter should have been incremented exactly once.
EXPECT_DOUBLE_EQ(1.0, pass->get());
}
+
+#ifdef MIXXX_USE_QML
+class MockScreenRender : public ControllerRenderingEngine {
+ public:
+ MockScreenRender(const LegacyControllerMapping::ScreenInfo& info)
+ : ControllerRenderingEngine(info, new ControllerEngineThreadControl){};
+ MOCK_METHOD(void,
+ requestSendingFrameData,
+ (Controller * controller, const QByteArray& frame),
+ (override));
+};
+
+TEST_F(ControllerScriptEngineLegacyTest, screenWontSentRawDataIfNotConfigured) {
+ SETUP_LOG_CAPTURE();
+ LegacyControllerMapping::ScreenInfo dummyScreen{
+ "", // identifier
+ QSize(0, 0), // size
+ 10, // target_fps
+ 1, // msaa
+ std::chrono::milliseconds(10), // splash_off
+ QImage::Format_RGB16, // pixelFormat
+ LegacyControllerMapping::ScreenInfo::ColorEndian::Big, // endian
+ false, // rawData
+ false // reversedColor
+ };
+ QImage dummyFrame;
+ // Allocate screen on the heap as it need to outlive the this function,
+ // since the engine will take ownership of it
+ std::shared_ptr pDummyRender =
+ std::make_shared(dummyScreen);
+ EXPECT_CALL(*pDummyRender, requestSendingFrameData(_, _)).Times(0);
+ EXPECT_LOG_MSG(QtWarningMsg,
+ "Could not find a valid transform function but the screen doesn't "
+ "accept raw data. Aborting screen rendering.");
+
+ transformScreenFrameFunctions().insert(
+ dummyScreen.identifier,
+ newTransformScreenFrameFunction(
+ QMetaMethod(),
+ false));
+ renderingScreens().insert(dummyScreen.identifier, pDummyRender);
+ rootItems().insert(dummyScreen.identifier, std::make_shared());
+
+ testHandleScreen(
+ dummyScreen,
+ dummyFrame,
+ QDateTime::currentDateTime());
+
+ ASSERT_ALL_EXPECTED_MSG();
+}
+
+TEST_F(ControllerScriptEngineLegacyTest, screenWillSentRawDataIfConfigured) {
+ SETUP_LOG_CAPTURE();
+ LegacyControllerMapping::ScreenInfo dummyScreen{
+ "", // identifier
+ QSize(0, 0), // size
+ 10, // target_fps
+ 1, // msaa
+ std::chrono::milliseconds(10), // splash_off
+ QImage::Format_RGB16, // pixelFormat
+ LegacyControllerMapping::ScreenInfo::ColorEndian::Big, // endian
+ false, // reversedColor
+ true // rawData
+ };
+ QImage dummyFrame;
+ // Allocate screen on the heap as it need to outlive the this function,
+ // since the engine will take ownership of it
+ std::shared_ptr