From eff07d87c2266f5c2d00ca9a8abac91e6e0ff192 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Fri, 20 Oct 2023 21:34:11 +0100 Subject: [PATCH] feat: controller screen rendering --- CMakeLists.txt | 7 +- res/controllers/Dummy Device Screen.hid.xml | 21 + res/controllers/DummyDeviceDefaultScreen.qml | 221 ++++++++ src/controllers/bulk/bulkcontroller.cpp | 2 +- src/controllers/controller.cpp | 9 +- src/controllers/controller.h | 16 +- src/controllers/controllermappinginfo.h | 10 +- src/controllers/dlgprefcontroller.cpp | 105 ++++ src/controllers/dlgprefcontroller.h | 27 + src/controllers/dlgprefcontrollerdlg.ui | 267 ++++----- src/controllers/hid/hidiooutputreport.cpp | 27 +- src/controllers/legacycontrollermapping.h | 120 ++++- .../legacycontrollermappingfilehandler.cpp | 134 ++++- .../legacycontrollermappingfilehandler.h | 24 +- .../rendering/controllerrenderingengine.cpp | 311 +++++++++++ .../rendering/controllerrenderingengine.h | 93 ++++ .../scripting/controllerscriptenginebase.cpp | 37 +- .../scripting/controllerscriptenginebase.h | 22 + .../legacy/controllerscriptenginelegacy.cpp | 467 +++++++++++++++- .../legacy/controllerscriptenginelegacy.h | 42 +- src/coreservices.cpp | 43 ++ src/coreservices.h | 3 + src/preferences/dialog/dlgpreferences.cpp | 5 +- src/qml/qmlapplication.cpp | 18 - .../controller_mapping_file_handler_test.cpp | 507 ++++++++++++++++++ .../controller_mapping_validation_test.cpp | 3 + src/test/controller_mapping_validation_test.h | 8 + src/util/cmdlineargs.cpp | 7 + src/util/cmdlineargs.h | 4 + tools/README | 20 + tools/dummy_hid_device.cpp | 253 +++++++++ 31 files changed, 2639 insertions(+), 194 deletions(-) create mode 100644 res/controllers/Dummy Device Screen.hid.xml create mode 100755 res/controllers/DummyDeviceDefaultScreen.qml create mode 100644 src/controllers/rendering/controllerrenderingengine.cpp create mode 100644 src/controllers/rendering/controllerrenderingengine.h create mode 100644 src/test/controller_mapping_file_handler_test.cpp create mode 100644 tools/dummy_hid_device.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 077b18e42a12..1a80fd44ee12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1406,7 +1406,12 @@ target_sources(mixxx-lib PRIVATE src/widget/wvumeterlegacy.cpp src/widget/wwaveformviewer.cpp ) -if (NOT QML) +if (QML) + target_sources(mixxx-lib PRIVATE + # The following source depends of QML but aren't part of the new QML UI + src/controllers/rendering/controllerrenderingengine.cpp + ) +else() target_sources(mixxx-lib PRIVATE src/control/controlmodel.cpp src/control/controlsortfiltermodel.cpp diff --git a/res/controllers/Dummy Device Screen.hid.xml b/res/controllers/Dummy Device Screen.hid.xml new file mode 100644 index 000000000000..4011131b0136 --- /dev/null +++ b/res/controllers/Dummy Device Screen.hid.xml @@ -0,0 +1,21 @@ + + + + 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..6ea0ea24497d --- /dev/null +++ b/res/controllers/DummyDeviceDefaultScreen.qml @@ -0,0 +1,221 @@ +import QtQuick 2.15 +import QtQuick.Window 2.3 +import QtQuick.Scene3D 2.14 + +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 + +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`) + loader.sourceComponent = live + } + + function shutdown() { + console.log(`Screen ${root.screenId} is stopping`) + loader.sourceComponent = splashoff + } + + function transformFrame(input, timestamp) { + return new ArrayBuffer(0); + } + + Timer { + id: channelchange + + interval: 2000 + repeat: true + running: true + + onTriggered: { + root.group = root.group === "[Channel1]" ? "[Channel2]" : "[Channel1]" + } + } + + Component { + id: splashoff + Rectangle { + color: "black" + anchors.fill: parent + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectFit + source: "../images/templates/logo_mixxx.png" + } + } + } + + Component { + id: live + + Rectangle { + 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 + } + } + } + + Repeater { + id: debugValue + + 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: live + } +} diff --git a/src/controllers/bulk/bulkcontroller.cpp b/src/controllers/bulk/bulkcontroller.cpp index 4dd814b7e521..6bd18585d6ea 100644 --- a/src/controllers/bulk/bulkcontroller.cpp +++ b/src/controllers/bulk/bulkcontroller.cpp @@ -241,7 +241,7 @@ void BulkController::sendBytes(const QByteArray& data) { qCWarning(m_logOutput) << "Unable to send data to" << getName() << "serial #" << m_sUID; } else { - qCDebug(m_logOutput) << ret << "bytes sent to" << getName() + qCDebug(m_logOutput) << transferred << "bytes sent to" << getName() << "serial #" << m_sUID; } } diff --git a/src/controllers/controller.cpp b/src/controllers/controller.cpp index db07cea1aabb..d46c429ab8f6 100644 --- a/src/controllers/controller.cpp +++ b/src/controllers/controller.cpp @@ -5,8 +5,10 @@ #include #include +#include "controllers/controllermanager.h" #include "controllers/defs_controllers.h" #include "moc_controller.cpp" +#include "util/cmdlineargs.h" #include "util/screensaver.h" namespace { @@ -78,6 +80,10 @@ bool Controller::applyMapping() { } m_pScriptEngineLegacy->setScriptFiles(scriptFiles); +#ifdef MIXXX_USE_QML + m_pScriptEngineLegacy->setLibraryDirectories(pMapping->getLibraryDirectories()); + m_pScriptEngineLegacy->setInfoScrens(pMapping->getInfoScreens()); +#endif return m_pScriptEngineLegacy->initialize(); } @@ -124,7 +130,8 @@ void Controller::receive(const QByteArray& data, mixxx::Duration timestamp) { triggerActivity(); int length = data.size(); - if (m_logInput().isDebugEnabled()) { + if (CmdlineArgs::Instance() + .getControllerDebug()) { // Formatted packet display QString message = QString("t:%2, %3 bytes:\n") .arg(timestamp.formatMillisWithUnit(), diff --git a/src/controllers/controller.h b/src/controllers/controller.h index 8838450119de..86c396e92b5d 100644 --- a/src/controllers/controller.h +++ b/src/controllers/controller.h @@ -57,6 +57,10 @@ class Controller : public QObject { return m_bLearning; } + inline ControllerScriptEngineLegacy* getScriptEngine() const { + return m_pScriptEngineLegacy; + } + virtual bool matchMapping(const MappingInfo& mapping) = 0; signals: @@ -102,10 +106,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(); @@ -117,9 +117,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; } @@ -139,6 +136,11 @@ class Controller : public QObject { const RuntimeLoggingCategory m_logInput; const RuntimeLoggingCategory m_logOutput; + public slots: + // 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; diff --git a/src/controllers/controllermappinginfo.h b/src/controllers/controllermappinginfo.h index 2e80d67a67b4..dec67fa831c6 100644 --- a/src/controllers/controllermappinginfo.h +++ b/src/controllers/controllermappinginfo.h @@ -14,15 +14,13 @@ struct ProductInfo { QString protocol; QString vendor_id; QString product_id; - - // HID-specific - QString in_epaddr; - QString out_epaddr; - - // Bulk-specific QString usage_page; QString usage; QString interface_number; + + // Bulk-specific + QString in_epaddr; + QString out_epaddr; }; /// Base class handling enumeration and parsing of mapping info headers diff --git a/src/controllers/dlgprefcontroller.cpp b/src/controllers/dlgprefcontroller.cpp index e753f156e640..83e39b093ac3 100644 --- a/src/controllers/dlgprefcontroller.cpp +++ b/src/controllers/dlgprefcontroller.cpp @@ -17,6 +17,8 @@ #include "defs_urls.h" #include "moc_dlgprefcontroller.cpp" #include "preferences/usersettings.h" +#include "util/cmdlineargs.h" +#include "util/time.h" #include "util/versionstore.h" namespace { @@ -104,6 +106,10 @@ DlgPrefController::DlgPrefController( &ControllerManager::mappingApplied, this, &DlgPrefController::enableWizardAndIOTabs); + connect(m_pController, + &Controller::openChanged, + this, + [this](bool) { slotShowMapping(m_pMapping); }); // Open script file links connect(m_ui.labelLoadedMappingScriptFileLinks, @@ -397,6 +403,24 @@ QString DlgPrefController::mappingFileLinks( linkList << scriptFileLink; } + +#ifdef MIXXX_USE_QML + for (const auto& qmlLibrary : pMapping->getLibraryDirectories()) { + QString scriptFileLink = coloredLinkString( + m_pLinkColor, + qmlLibrary.dirinfo.fileName(), + qmlLibrary.dirinfo.absoluteFilePath()); + if (!qmlLibrary.dirinfo.exists()) { + scriptFileLink += + QStringLiteral(" (") + tr("missing") + QStringLiteral(")"); + } else if (qmlLibrary.dirinfo.absoluteFilePath().startsWith( + systemMappingPath)) { + scriptFileLink += builtinFileSuffix; + } + + linkList << scriptFileLink; + } +#endif return linkList.join("
"); } @@ -606,6 +630,7 @@ void DlgPrefController::slotMappingSelected(int chosenIndex) { } enableWizardAndIOTabs(false); } + m_ui.groupBoxScreens->setVisible(false); } else { // User picked a mapping m_ui.chkEnabledDevice->setEnabled(true); @@ -808,6 +833,54 @@ void DlgPrefController::initTableView(QTableView* pTable) { pTable->setAlternatingRowColors(true); } +#ifdef MIXXX_USE_QML +ControllerScreenPreview::ControllerScreenPreview( + QWidget* parent, const LegacyControllerMapping::ScreenInfo& screen) + : QWidget(parent), + m_screenInfo(screen), + m_pFrame(new QLabel(this)), + m_pStat(new QLabel("- FPS", this)), + m_frameDurationHistoryIdx(0), + m_lastFrameTimespamp(mixxx::Time::elapsed()) { + size_t frameDurationHistoryLenght = sizeof(m_frameDurationHistory) / sizeof(uint); + memset(m_frameDurationHistory, 0, frameDurationHistoryLenght); + m_pFrame->setFixedSize(screen.size); + m_pStat->setAlignment(Qt::AlignRight); + auto aLayout = make_parented(this); + auto aBottomLayout = new QHBoxLayout(); + aLayout->addWidget(m_pFrame); + aBottomLayout->addWidget(make_parented( + QString("Screen \"%0\"").arg(m_screenInfo.identifier), this)); + aBottomLayout->addWidget(m_pStat); + aLayout->addItem(aBottomLayout); +} +void ControllerScreenPreview::updateFrame( + const LegacyControllerMapping::ScreenInfo& screen, const QImage& frame) { + if (m_screenInfo.identifier != screen.identifier) { + return; + } + size_t frameDurationHistoryLenght = sizeof(m_frameDurationHistory) / sizeof(uint); + auto currentTimestamp = mixxx::Time::elapsed(); + m_frameDurationHistory[m_frameDurationHistoryIdx++] = + (currentTimestamp - m_lastFrameTimespamp).toIntegerMillis(); + m_frameDurationHistoryIdx %= frameDurationHistoryLenght; + + double durationSinceLastFrame = 0.0; + for (uint8_t i = 0; i < frameDurationHistoryLenght; i++) { + durationSinceLastFrame += (double)m_frameDurationHistory[i]; + } + durationSinceLastFrame /= (double)frameDurationHistoryLenght; + + if (durationSinceLastFrame > 0.0) { + m_pStat->setText(QString("%0 FPS (requested %1)") + .arg((int)(1000.0 / durationSinceLastFrame)) + .arg(m_screenInfo.target_fps)); + } + m_pFrame->setPixmap(QPixmap::fromImage(frame)); + m_lastFrameTimespamp = currentTimestamp; +} +#endif + void DlgPrefController::slotShowMapping(std::shared_ptr pMapping) { m_ui.labelLoadedMapping->setText(mappingName(pMapping)); m_ui.labelLoadedMappingDescription->setText(mappingDescription(pMapping)); @@ -815,6 +888,38 @@ void DlgPrefController::slotShowMapping(std::shared_ptr m_ui.labelLoadedMappingSupportLinks->setText(mappingSupportLinks(pMapping)); m_ui.labelLoadedMappingScriptFileLinks->setText(mappingFileLinks(pMapping)); +#ifdef MIXXX_USE_QML + if (m_pController->getScriptEngine()) { + disconnect(m_pController->getScriptEngine(), nullptr, this, nullptr); + } + qDeleteAll(m_ui.groupBoxScreens->findChildren("", Qt::FindDirectChildrenOnly)); + + if (pMapping && + CmdlineArgs::Instance() + .getControllerPreviewScreens() && // TODO (ac) use currently + // active screen instead + // of mapping one + m_pController->getScriptEngine()) { + auto screens = pMapping->getInfoScreens(); + + for (const LegacyControllerMapping::ScreenInfo& screen : qAsConst(screens)) { + ControllerScreenPreview* pPreviewScreen = + new ControllerScreenPreview(m_ui.groupBoxScreens, screen); + m_ui.groupBoxScreens->layout()->addWidget(pPreviewScreen); + + connect(m_pController->getScriptEngine(), + &ControllerScriptEngineLegacy::previewRenderedScreen, + pPreviewScreen, + &ControllerScreenPreview::updateFrame); + } + + m_ui.groupBoxScreens->setVisible(!screens.isEmpty()); + } else +#endif + { + m_ui.groupBoxScreens->setVisible(false); + } + // We mutate this mapping so keep a reference to it while we are using it. // TODO(rryan): Clone it? Technically a waste since nothing else uses this // copy but if someone did they might not expect it to change. diff --git a/src/controllers/dlgprefcontroller.h b/src/controllers/dlgprefcontroller.h index ae425df9d62d..e705c6070859 100644 --- a/src/controllers/dlgprefcontroller.h +++ b/src/controllers/dlgprefcontroller.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "controllers/controllerinputmappingtablemodel.h" @@ -12,11 +13,37 @@ #include "preferences/dialog/dlgpreferencepage.h" #include "preferences/usersettings.h" +#ifdef MIXXX_USE_QML +#define CONTROLLER_SCREEN_PREVIEW_FRAME_HISTORY_SIZE 5 +#endif + // Forward declarations class Controller; class ControllerManager; class MappingInfoEnumerator; +#ifdef MIXXX_USE_QML +/// Widget to preview controller screen +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; + + QLabel* m_pFrame; + QLabel* m_pStat; + uint8_t m_frameDurationHistoryIdx; + uint m_frameDurationHistory[CONTROLLER_SCREEN_PREVIEW_FRAME_HISTORY_SIZE]; + + mixxx::Duration m_lastFrameTimespamp; +}; +#endif + /// Configuration dialog for a single DJ controller class DlgPrefController : public DlgPreferencePage { Q_OBJECT diff --git a/src/controllers/dlgprefcontrollerdlg.ui b/src/controllers/dlgprefcontrollerdlg.ui index 1d391eb06aad..5b68bcbf857c 100644 --- a/src/controllers/dlgprefcontrollerdlg.ui +++ b/src/controllers/dlgprefcontrollerdlg.ui @@ -6,8 +6,8 @@ 0 0 - 507 - 437 + 1045 + 717 @@ -26,87 +26,16 @@ 0 - - Controller Setup - 0 0 + + Controller Setup + - - - - true - - - - 0 - 0 - - - - - 14 - 75 - true - - - - Controller Name - - - - - - - true - - - - 0 - 0 - - - - - - - (device category goes here) - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Enabled - - - - - - - Click to start the Controller Learning wizard. - - - - - - Learning Wizard (MIDI Only) - - - false - - - false - - - @@ -131,64 +60,28 @@ comboBoxMapping - - - + + + + + true + - + 0 0 - - + + + 14 + 75 + true + + + + Controller Name - - - - - - 0 - 0 - - - - - 50 - 50 - - - - - 50 - 50 - - - - (icon) - - - - - - - - - - (Warning message goes here) - - - true - - - true - - - Qt::TextBrowserInteraction - - - - @@ -421,7 +314,114 @@ - + + + + + 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 + + + + + + + Enabled + + + + + + + true + + + + 0 + 0 + + + + + + + (device category goes here) + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + Qt::Vertical @@ -434,6 +434,14 @@ + + + + Screens preview + + + + @@ -525,7 +533,6 @@ - Output Mappings diff --git a/src/controllers/hid/hidiooutputreport.cpp b/src/controllers/hid/hidiooutputreport.cpp index c5fdca463869..baf27b7bfef9 100644 --- a/src/controllers/hid/hidiooutputreport.cpp +++ b/src/controllers/hid/hidiooutputreport.cpp @@ -36,7 +36,9 @@ void HidIoOutputReport::updateCachedData(const QByteArray& data, m_lastCachedDataSize = data.size(); } else { - if (m_possiblyUnsentDataCached && !useNonSkippingFIFO) { + if (CmdlineArgs::Instance() + .getControllerDebug() && + m_possiblyUnsentDataCached && !useNonSkippingFIFO) { qCDebug(logOutput) << "t:" << mixxx::Time::elapsed().formatMillisWithUnit() << "skipped superseded OutputReport data for ReportID" << m_reportId; @@ -97,9 +99,13 @@ bool HidIoOutputReport::sendCachedData(QMutex* pHidDeviceAndPollMutex, cacheLock.unlock(); - qCDebug(logOutput) << "t:" << startOfHidWrite.formatMillisWithUnit() - << "Skipped sending identical OutputReport data from cache for ReportID" - << m_reportId; + if (CmdlineArgs::Instance() + .getControllerDebug()) { + qCDebug(logOutput) << "t:" << startOfHidWrite.formatMillisWithUnit() + << "Skipped sending identical OutputReport data " + "from cache for ReportID" + << m_reportId; + } // Return with false, to signal the caller, that no time consuming IO operation was necessary return false; @@ -146,11 +152,14 @@ bool HidIoOutputReport::sendCachedData(QMutex* pHidDeviceAndPollMutex, return true; } - qCDebug(logOutput) << "t:" << startOfHidWrite.formatMillisWithUnit() << " " - << result << "bytes ( including ReportID of" - << static_cast(m_reportId) - << ") sent from skipping cache - Needed:" - << (mixxx::Time::elapsed() - startOfHidWrite).formatMicrosWithUnit(); + if (CmdlineArgs::Instance() + .getControllerDebug()) { + qCDebug(logOutput) << "t:" << startOfHidWrite.formatMillisWithUnit() << " " + << result << "bytes ( including ReportID of" + << static_cast(m_reportId) + << ") sent from skipping cache - Needed:" + << (mixxx::Time::elapsed() - startOfHidWrite).formatMicrosWithUnit(); + } // Return with true, to signal the caller, that the time consuming hid_write operation was executed return true; diff --git a/src/controllers/legacycontrollermapping.h b/src/controllers/legacycontrollermapping.h index ca0e369b96c4..635e88f6e1cb 100644 --- a/src/controllers/legacycontrollermapping.h +++ b/src/controllers/legacycontrollermapping.h @@ -3,12 +3,17 @@ #include #include #include +#include #include +#include #include +#include #include +#include #include #include "defs_urls.h" +#include "util/assert.h" /// This class represents a controller mapping, containing the data elements that /// make it up. @@ -22,29 +27,83 @@ class LegacyControllerMapping { virtual std::shared_ptr clone() const = 0; struct ScriptFileInfo { + enum Type { + JAVASCRIPT, +#ifdef MIXXX_USE_QML + QML, +#endif + }; + ScriptFileInfo() : builtin(false) { } QString name; - QString functionPrefix; + QString identifier; QFileInfo file; + Type type; + bool builtin; + }; + +#ifdef MIXXX_USE_QML + struct QMLModuleInfo { + QMLModuleInfo(const QFileInfo& aDirinfo, + bool isBuiltin) + : dirinfo(aDirinfo), + builtin(isBuiltin) { + } + + QFileInfo dirinfo; bool builtin; }; + struct ScreenInfo { + ScreenInfo(const QString& aIdentifier, + const QSize& aSize, + uint aTargetFps, + uint aSplashOff, + QImage::Format aPixelFormat, + std::endian anEndian, + bool isReversedColor, + bool isRawData) + : identifier(aIdentifier), + size(aSize), + target_fps(aTargetFps), + splash_off(aSplashOff), + pixelFormat(aPixelFormat), + endian(anEndian), + reversedColor(isReversedColor), + rawData(isRawData) { + } + + QString identifier; + QSize size; + uint target_fps; + uint splash_off; + QImage::Format pixelFormat; + std::endian endian; + bool reversedColor; + bool rawData; + }; +#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 identifier The script's function prefix with Javascript OR the + /// screen identifier this QML should be run for (or empty string) /// @param file A FileInfo object pointing to the script file + /// @param type A ScriptFileInfo::Type the specify script file type /// @param builtin If this is true, the script won't be written to the XML void addScriptFile(const QString& name, - const QString& functionprefix, + const QString& identifier, const QFileInfo& file, + ScriptFileInfo::Type type = ScriptFileInfo::Type::JAVASCRIPT, bool builtin = false) { ScriptFileInfo info; info.name = name; - info.functionPrefix = functionprefix; + info.identifier = identifier; info.file = file; + info.type = type; info.builtin = builtin; m_scripts.append(info); setDirty(true); @@ -54,6 +113,55 @@ class LegacyControllerMapping { return m_scripts; } +#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 + void addLibraryDirectory(const QFileInfo& dirinfo, + bool builtin = false) { + m_modules.append(QMLModuleInfo( + dirinfo, + builtin)); + setDirty(true); + } + + const QList& getLibraryDirectories() 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 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 + void addScreenInfo(const QString& identifier, + const QSize& size, + uint targetFps = 30, + uint splashoff = 50, + QImage::Format pixelFormat = QImage::Format_RGB32, + std::endian endian = std::endian::big, + bool reversedColor = false, + bool rawData = false) { + m_screens.append(ScreenInfo(identifier, + size, + targetFps, + splashoff, + pixelFormat, + endian, + reversedColor, + rawData)); + setDirty(true); + } + + const QList& getInfoScreens() const { + return m_screens; + } +#endif + inline void setDirty(bool bDirty) { m_bDirty = bDirty; } @@ -192,4 +300,8 @@ class LegacyControllerMapping { QString m_mixxxVersion; QList m_scripts; +#ifdef MIXXX_USE_QML + QList m_modules; + QList m_screens; +#endif }; diff --git a/src/controllers/legacycontrollermappingfilehandler.cpp b/src/controllers/legacycontrollermappingfilehandler.cpp index 20986456c8c4..682d42bdab13 100644 --- a/src/controllers/legacycontrollermappingfilehandler.cpp +++ b/src/controllers/legacycontrollermappingfilehandler.cpp @@ -28,8 +28,44 @@ QFileInfo findScriptFile(std::shared_ptr mapping, return file; } +#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(std::shared_ptr 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; +} +#endif + } // namespace +#ifdef MIXXX_USE_QML +QMap LegacyControllerMappingFileHandler::kSupportedPixelFormat = { + {"RBG", QImage::Format_RGB888}, + {"RBGA", QImage::Format_RGBA8888}, + {"RGB565", QImage::Format_RGB16}, +}; + +QMap LegacyControllerMappingFileHandler::kEndianFormat = { + {"big", std::endian::big}, + {"little", std::endian::little}, +}; +#endif + // static std::shared_ptr LegacyControllerMappingFileHandler::loadMapping( const QFileInfo& mappingFile, const QDir& systemMappingsPath) { @@ -131,17 +167,107 @@ void LegacyControllerMappingFileHandler::addScriptFilesToMapping( mapping->addScriptFile(REQUIRED_SCRIPT_FILE, "", findScriptFile(mapping, REQUIRED_SCRIPT_FILE, 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(filename, + identifier, + file, + LegacyControllerMapping::ScriptFileInfo::Type::QML); +#else + qWarning() << "Unsupported render scene. Mixxx isn't built with QML support"; + return; +#endif + } else { + QString functionPrefix = scriptFile.attribute("functionprefix", ""); + mapping->addScriptFile(filename, + functionPrefix, + file, + LegacyControllerMapping::ScriptFileInfo::Type::JAVASCRIPT); + } 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()) { + QString identifier = screen.attribute("identifier", ""); + uint targetFps = screen.attribute("targetFps", "30").toUInt(); + QString pixelFormatName = screen.attribute("pixelType", "RBG888"); + QString endianName = screen.attribute("endian", "little"); + QString reversedColor = screen.attribute("reversed", "false").toLower(); + QString rawData = screen.attribute("raw", "false").toLower(); + uint splashoff = screen.attribute("splashoff", "0").toUInt(); + + if (!targetFps || targetFps > MAX_TARGET_FPS) { + qWarning() << "Invalid target FPS. Target FPS must be between 1 and " << MAX_TARGET_FPS; + return; + } + + if (splashoff > MAX_SPLASHOFF_DURATION) { + qWarning() << "Invalid splashoff duration. Splashoff duration must " + "be between 0 and " + << MAX_SPLASHOFF_DURATION; + splashoff = MAX_SPLASHOFF_DURATION; + } + + if (!kSupportedPixelFormat.contains(pixelFormatName)) { + qWarning() << "Unsupported pixel format" << pixelFormatName; + return; + } + + if (!kEndianFormat.contains(endianName)) { + qWarning() << "Unknown endiant format" << endianName; + return; + } + + QImage::Format pixelFormat = kSupportedPixelFormat.value(pixelFormatName); + std::endian endian = kEndianFormat.value(endianName); + + uint width = screen.attribute("width", "0").toUInt(); + uint height = screen.attribute("height", "0").toUInt(); + + qDebug() << "Adding screen " << identifier; + mapping->addScreenInfo(identifier, + QSize(width, height), + targetFps, + splashoff, + pixelFormat, + endian, + reversedColor == "yes" || reversedColor == "true" || reversedColor == "1", + rawData == "yes" || rawData == "true" || rawData == "1"); + 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); + if (path.isDir()) { + qDebug() << "Adding QML directory " << libFilename; + mapping->addLibraryDirectory(path); + } else { + qWarning() << "Unable to add controller QML library path." + << path.absolutePath() + << "is not a directory or is missing"; + } + qmlLibrary = qmlLibrary.nextSiblingElement("library"); + } +#endif } bool LegacyControllerMappingFileHandler::writeDocument( @@ -225,7 +351,7 @@ QDomDocument LegacyControllerMappingFileHandler::buildRootWithScripts( continue; } qDebug() << "writing script block for" << filename; - QString functionPrefix = script.functionPrefix; + 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 eb4d6035eee6..701f09f7d303 100644 --- a/src/controllers/legacycontrollermappingfilehandler.h +++ b/src/controllers/legacycontrollermappingfilehandler.h @@ -1,5 +1,12 @@ #pragma once +#ifdef MIXXX_USE_QML +#include +#include +#include +#include +#endif + #include "controllers/legacycontrollermapping.h" #include "util/xml.h" @@ -36,10 +43,10 @@ class LegacyControllerMappingFileHandler { void parseMappingInfo(const QDomElement& root, std::shared_ptr 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. @@ -60,4 +67,17 @@ class LegacyControllerMappingFileHandler { virtual std::shared_ptr load(const QDomElement& root, const QString& filePath, const QDir& systemMappingPath) = 0; + +#ifdef MIXXX_USE_QML + static QMap kSupportedPixelFormat; + static QMap kEndianFormat; +#endif }; + +#ifdef MIXXX_USE_QML +// Maximum target frame per request for a a screen controller +#define MAX_TARGET_FPS 240 + +// Maximum time allowed for a screen to run a splash off animation +#define MAX_SPLASHOFF_DURATION 3000 +#endif diff --git a/src/controllers/rendering/controllerrenderingengine.cpp b/src/controllers/rendering/controllerrenderingengine.cpp new file mode 100644 index 000000000000..68e6239f7349 --- /dev/null +++ b/src/controllers/rendering/controllerrenderingengine.cpp @@ -0,0 +1,311 @@ +#include "controllers/rendering/controllerrenderingengine.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "controllers/controller.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/time.h" + +ControllerRenderingEngine::ControllerRenderingEngine( + const LegacyControllerMapping::ScreenInfo& info) + : QObject(), + m_screenInfo(info), + m_GLDataFormat(GL_RGBA), + m_GLDataType(GL_UNSIGNED_BYTE), + m_isValid(true) { + 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; + + m_pThread = std::make_unique(); + m_pThread->setObjectName("ControllerScreenRenderer"); + + moveToThread(m_pThread.get()); + connect(this, + &ControllerRenderingEngine::setupRequested, + this, + &ControllerRenderingEngine::setup); + connect(this, + &ControllerRenderingEngine::sendRequested, + this, + &ControllerRenderingEngine::send); + connect(this, + &ControllerRenderingEngine::stopRequested, + this, + &ControllerRenderingEngine::finish); + + m_pThread->start(QThread::NormalPriority); +} + +ControllerRenderingEngine::~ControllerRenderingEngine() { + VERIFY_OR_DEBUG_ASSERT(!m_fbo) { + qWarning() << "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()) { + qWarning() << "Render thread has or ir about to terminate. Cannot " + "start this render anymore."; + return; + } + QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); +} + +void ControllerRenderingEngine::requestSetup(std::shared_ptr qmlEngine) { + VERIFY_OR_DEBUG_ASSERT(QThread::currentThread() != thread()) { + qWarning() << "Unable to setup OpenGL rendering context from the same " + "thread as the render object"; + return; + } + emit setupRequested(qmlEngine); + + const auto lock = lockMutex(&m_mutex); + if (!m_quickWindow) { + m_waitCondition.wait(&m_mutex); + } +} + +void ControllerRenderingEngine::requestSend(Controller* controller, const QByteArray& frame) { + emit sendRequested(controller, frame); +} + +void ControllerRenderingEngine::setup(std::shared_ptr qmlEngine) { + QSurfaceFormat format; + format.setDepthBufferSize(16); + format.setStencilBufferSize(8); + + m_context = std::make_unique(); + m_context->setFormat(format); + VERIFY_OR_DEBUG_ASSERT(m_context->create()) { + qWarning() << "Unable to intiliaze controller screen rendering. Giving up"; + m_isValid = false; + m_waitCondition.wakeAll(); + finish(); + return; + } + connect(m_context.get(), + &QOpenGLContext::aboutToBeDestroyed, + this, + &ControllerRenderingEngine::finish, + Qt::BlockingQueuedConnection); + + m_offscreenSurface = std::make_unique(); + m_offscreenSurface->setFormat(m_context->format()); + + QMetaObject::invokeMethod( + qApp, + [this] { + m_offscreenSurface->create(); + }, + // This invocation will block the current thread! + Qt::BlockingQueuedConnection); + + 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()); + + m_waitCondition.wakeAll(); +} + +void ControllerRenderingEngine::finish() { + disconnect(this); + + if (m_context && m_context->isValid()) { + m_context->makeCurrent(m_offscreenSurface.get()); + m_renderControl->deleteLater(); + m_offscreenSurface->deleteLater(); + m_quickWindow->deleteLater(); + + // Free the engine and FBO + m_fbo.reset(); + + m_context->doneCurrent(); + } + m_pThread->quit(); +} + +void ControllerRenderingEngine::renderFrame() { + VERIFY_OR_DEBUG_ASSERT(m_offscreenSurface->isValid()) { + qWarning() << "OffscrenSurface isn't valid anymore."; + finish(); + return; + }; + VERIFY_OR_DEBUG_ASSERT(m_context->isValid()) { + qWarning() << "GLContext isn't valid anymore."; + finish(); + return; + }; + VERIFY_OR_DEBUG_ASSERT(m_context->makeCurrent(m_offscreenSurface.get())) { + qWarning() << "Couldn't make the GLContext current to the OffscrenSurface."; + finish(); + return; + }; + + if (!m_fbo) { + m_fbo = std::make_unique( + m_screenInfo.size, QOpenGLFramebufferObject::CombinedDepthStencil); + + m_quickWindow->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(m_context.get())); + + VERIFY_OR_DEBUG_ASSERT(m_renderControl->initialize()) { + qWarning() << "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 = mixxx::Time::elapsed(); + m_renderControl->polishItems(); + + m_renderControl->beginFrame(); + VERIFY_OR_DEBUG_ASSERT(m_renderControl->sync()) { + qWarning() << "Couldn't sync the render control."; + }; + QImage fboImage(m_screenInfo.size, m_screenInfo.pixelFormat); + + VERIFY_OR_DEBUG_ASSERT(m_fbo->bind()) { + qWarning() << "Couldn't bind the FBO."; + } + GLenum glError; + m_context->functions()->glFlush(); + glError = m_context->functions()->glGetError(); + VERIFY_OR_DEBUG_ASSERT(glError == GL_NO_ERROR) { + qWarning() << "GLError: " << glError; + finish(); + } + if (m_screenInfo.endian != std::endian::native) { + m_context->functions()->glPixelStorei(GL_PACK_SWAP_BYTES, GL_TRUE); + } + glError = m_context->functions()->glGetError(); + VERIFY_OR_DEBUG_ASSERT(glError == GL_NO_ERROR) { + qWarning() << "GLError: " << glError; + finish(); + } + + QDateTime timestamp = QDateTime::currentDateTime(); + m_renderControl->render(); + m_renderControl->endFrame(); + + while (m_context->functions()->glGetError()) + ; + 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_DEBUG_ASSERT(glError == GL_NO_ERROR) { + qWarning() << "GLError: " << glError; + finish(); + } + VERIFY_OR_DEBUG_ASSERT(!fboImage.isNull()) { + qWarning() << "Screen frame is null!"; + } + VERIFY_OR_DEBUG_ASSERT(m_fbo->release()) { + qDebug() << "Couldn't release the FBO."; + } + + fboImage.mirror(false, true); + + emit frameRendered(m_screenInfo, fboImage, timestamp); + + m_context->doneCurrent(); +} + +bool ControllerRenderingEngine::stop() { + emit stopRequested(); + return m_pThread->wait(); +} + +void ControllerRenderingEngine::send(Controller* controller, const QByteArray& frame) { + if (!frame.isEmpty()) { + controller->sendBytes(frame); + } + + if (CmdlineArgs::Instance() + .getControllerDebug()) { + auto endOfRender = mixxx::Time::elapsed(); + qDebug() << "Fame took " + << (endOfRender - m_nextFrameStart).formatMillisWithUnit() + << " and frame has" << frame.size() << "bytes"; + } + + m_nextFrameStart += mixxx::Duration::fromSeconds(1.0 / (double)m_screenInfo.target_fps); + + auto durationToWaitBeforeFrame = (m_nextFrameStart - mixxx::Time::elapsed()); + auto msecToWaitBeforeFrame = durationToWaitBeforeFrame.toIntegerMillis(); + + if (msecToWaitBeforeFrame > 0) { + if (CmdlineArgs::Instance() + .getControllerDebug()) { + qDebug() << "Waiting for " + << durationToWaitBeforeFrame.formatMillisWithUnit() + << " before rendering next frame"; + } + QTimer::singleShot(msecToWaitBeforeFrame, + Qt::PreciseTimer, + this, + &ControllerRenderingEngine::renderFrame); + } else { + QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); + } +} + +bool ControllerRenderingEngine::event(QEvent* event) { + 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..2ca5517b9700 --- /dev/null +++ b/src/controllers/rendering/controllerrenderingengine.h @@ -0,0 +1,93 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "controllers/legacycontrollermapping.h" +#include "controllers/scripting/controllerscriptenginebase.h" +#include "preferences/configobject.h" +#include "util/parented_ptr.h" +#include "util/runtimeloggingcategory.h" +#include "util/time.h" + +class Controller; + +class ControllerRenderingEngine : public QObject { + Q_OBJECT + public: + ControllerRenderingEngine(const LegacyControllerMapping::ScreenInfo& info); + ~ControllerRenderingEngine(); + + bool event(QEvent* event) override; + + const QSize& size() const { + return m_screenInfo.size; + } + + bool isValid() const { + return m_isValid; + } + + QQuickWindow* quickWindow() const { + return m_quickWindow.get(); + } + + const LegacyControllerMapping::ScreenInfo& info() const { + return m_screenInfo; + } + + public slots: + void requestSetup(std::shared_ptr qmlEngine); + void requestSend(Controller* controller, const QByteArray& frame); + void start(); + 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 setupRequested(std::shared_ptr engine); + void stopRequested(); + void sendRequested(Controller* controller, const QByteArray& frame); + + private: + mixxx::Duration 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; + + QWaitCondition m_waitCondition; + QMutex m_mutex; +}; diff --git a/src/controllers/scripting/controllerscriptenginebase.cpp b/src/controllers/scripting/controllerscriptenginebase.cpp index ba244373f169..2a294e4f2d98 100644 --- a/src/controllers/scripting/controllerscriptenginebase.cpp +++ b/src/controllers/scripting/controllerscriptenginebase.cpp @@ -1,11 +1,15 @@ #include "controllers/scripting/controllerscriptenginebase.h" +#include + #include "control/controlobject.h" #include "controllers/controller.h" #include "controllers/scripting/colormapperjsproxy.h" #include "errordialoghandler.h" +#include "library/trackcollectionmanager.h" #include "mixer/playermanager.h" #include "moc_controllerscriptenginebase.cpp" +#include "qml/asyncimageprovider.h" #include "util/cmdlineargs.h" ControllerScriptEngineBase::ControllerScriptEngineBase( @@ -15,11 +19,21 @@ 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); +} +#endif + bool ControllerScriptEngineBase::initialize() { VERIFY_OR_DEBUG_ASSERT(!m_pJSEngine) { return false; @@ -28,9 +42,28 @@ 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"); + qWarning() << "TrackCollectionManager hasn't been registered yet"; + } + m_pJSEngine = std::move(pQmlEngine); + } +#endif QJSValue engineGlobalObject = m_pJSEngine->globalObject(); diff --git a/src/controllers/scripting/controllerscriptenginebase.h b/src/controllers/scripting/controllerscriptenginebase.h index c736c947c2a4..f7689398973e 100644 --- a/src/controllers/scripting/controllerscriptenginebase.h +++ b/src/controllers/scripting/controllerscriptenginebase.h @@ -12,6 +12,9 @@ class Controller; class EvaluationException; +#ifdef MIXXX_USE_QML +class TrackCollectionManager; +#endif /// ControllerScriptEngineBase manages the JavaScript engine for controller scripts. /// ControllerScriptModuleEngine implements the current system using JS modules. @@ -40,10 +43,21 @@ class ControllerScriptEngineBase : public QObject { m_bTesting = testing; }; +#ifdef MIXXX_USE_QML + inline void setQMLMode(bool qmlFlag) { + m_bQmlMode = qmlFlag; + }; +#endif + bool isTesting() const { return m_bTesting; } +#ifdef MIXXX_USE_QML + static void registerTrackCollectionManager( + std::shared_ptr pTrackCollectionManager); +#endif + protected: virtual void shutdown(); @@ -58,8 +72,16 @@ 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; +#endif + protected slots: void reload(); diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp index a5886204ec61..f44f1eee6574 100644 --- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp @@ -1,12 +1,25 @@ #include "controllers/scripting/legacy/controllerscriptenginelegacy.h" +#ifdef MIXXX_USE_QML +#include +#include +#include +#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" +#endif ControllerScriptEngineLegacy::ControllerScriptEngineLegacy( Controller* controller, const RuntimeLoggingCategory& logger) @@ -54,6 +67,89 @@ bool ControllerScriptEngineLegacy::callFunctionOnObjects( success = false; } } +#ifdef MIXXX_USE_QML + if (m_bQmlMode) { + QHashIterator> i(m_rootItems); + while (i.hasNext()) { + i.next(); + const QMetaObject* metaObject = i.value()->metaObject(); + QStringList argList; + for (int i = 0; i < args.size(); i++) + argList << "QVariant"; + int methodIdx = + metaObject->indexOfMethod(QString("%1(%2)") + .arg(function, argList.join(',')) + .toUtf8()); + if (methodIdx == -1) { + qCWarning(m_logger) << "QML Scene " << i.key() << "has no" + << function << " method"; + continue; + } + QMetaMethod method = metaObject->method(methodIdx); + qCDebug(m_logger) << "Executing" + << function << "on QML Scene " << i.key(); + QVariant returnedValue; + bool isSuccessfull; + + switch (args.size()) { + case 0: + isSuccessfull = method.invoke(i.value().get(), + Qt::DirectConnection, + Q_RETURN_ARG(QVariant, returnedValue)); + break; + case 1: + isSuccessfull = method.invoke(i.value().get(), + Qt::DirectConnection, + Q_RETURN_ARG(QVariant, returnedValue), + Q_ARG(QVariant, args[0].toVariant())); + break; + case 2: + isSuccessfull = method.invoke(i.value().get(), + Qt::DirectConnection, + Q_RETURN_ARG(QVariant, returnedValue), + Q_ARG(QVariant, args[0].toVariant()), + Q_ARG(QVariant, args[1].toVariant())); + break; + case 3: + isSuccessfull = method.invoke(i.value().get(), + Qt::DirectConnection, + Q_RETURN_ARG(QVariant, returnedValue), + Q_ARG(QVariant, args[0].toVariant()), + Q_ARG(QVariant, args[1].toVariant()), + Q_ARG(QVariant, args[2].toVariant())); + break; + case 4: + isSuccessfull = method.invoke(i.value().get(), + Qt::DirectConnection, + Q_RETURN_ARG(QVariant, returnedValue), + Q_ARG(QVariant, args[0].toVariant()), + Q_ARG(QVariant, args[1].toVariant()), + Q_ARG(QVariant, args[2].toVariant()), + Q_ARG(QVariant, args[3].toVariant())); + break; + default: + qDebug() << "Trying to call a controller lifecycle method with " + "more than 5 args. Ignoring extra args"; + case 5: + isSuccessfull = method.invoke(i.value().get(), + Qt::DirectConnection, + Q_RETURN_ARG(QVariant, returnedValue), + Q_ARG(QVariant, args[0].toVariant()), + Q_ARG(QVariant, args[1].toVariant()), + Q_ARG(QVariant, args[2].toVariant()), + Q_ARG(QVariant, args[3].toVariant()), + Q_ARG(QVariant, args[4].toVariant())); + break; + } + + if (!isSuccessfull) { + // TODO how do we show the error? + // showScriptExceptionDialog(QJSValue{}, bFatalError); + success = false; + } + } + } +#endif return success; } @@ -90,6 +186,25 @@ QJSValue ControllerScriptEngineLegacy::wrapFunctionCode( return wrappedFunction; } +#ifdef MIXXX_USE_QML +void ControllerScriptEngineLegacy::setLibraryDirectories( + const QList& directories) { + const QStringList paths = m_fileWatcher.files(); + if (!paths.isEmpty()) { + m_fileWatcher.removePaths(paths); + } + + m_libraryDirectories = directories; +} +void ControllerScriptEngineLegacy::setInfoScrens( + 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 +212,17 @@ void ControllerScriptEngineLegacy::setScriptFiles( m_fileWatcher.removePaths(paths); } m_scriptFiles = scripts; + +#ifdef MIXXX_USE_QML + for (const LegacyControllerMapping::ScriptFileInfo& script : std::as_const(m_scriptFiles)) { + if (script.type == LegacyControllerMapping::ScriptFileInfo::Type::QML) { + setQMLMode(true); + return; + } + } + + setQMLMode(false); +#endif } bool ControllerScriptEngineLegacy::initialize() { @@ -104,6 +230,47 @@ bool ControllerScriptEngineLegacy::initialize() { return false; } +#ifdef MIXXX_USE_QML + QMap availableScreens; + + if (m_bQmlMode) { + for (const LegacyControllerMapping::ScreenInfo& screen : std::as_const(m_infoScreens)) { + VERIFY_OR_DEBUG_ASSERT(!availableScreens.contains(screen.identifier)) { + qWarning() << "A controller screen already contains the " + "identifier " + << screen.identifier; + return false; + } + availableScreens.insert(screen.identifier, + new ControllerRenderingEngine(screen)); + + if (!availableScreens.value(screen.identifier)->isValid()) { + qWarning() << QString( + "Unable to start the screen render for %1.") + .arg(screen.identifier); + return false; + } + + if (m_bTesting) + continue; + + availableScreens.value(screen.identifier) + ->requestSetup( + std::dynamic_pointer_cast(m_pJSEngine)); + + if (!availableScreens.value(screen.identifier)->isValid()) { + qWarning() << QString( + "Unable to setupr the screen render for %1.") + .arg(screen.identifier); + return false; + } + } + } else if (!m_infoScreens.isEmpty()) { + qWarning() << "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 @@ -126,14 +293,76 @@ 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_libraryDirectories)) { + auto path = module.dirinfo.absoluteFilePath(); + QDirIterator it(path, + QStringList() << "*.qml", + QDir::Files, + QDirIterator::Subdirectories); + while (it.hasNext()) { + if (!m_fileWatcher.addPath(it.next())) { + qCWarning(m_logger) << "Failed to watch QML module file" + << it.next() << "in QML module" << path; + } else { + qDebug() << "Watching file" << it.next() << "for controller auto-reload"; + } + } + std::dynamic_pointer_cast(m_pJSEngine)->addImportPath(path); + } + } else if (!m_libraryDirectories.isEmpty()) { + qWarning() << "Controller mapping has QML library definitions but no " + "QML files to use it. Ignoring."; + } + +#endif for (const LegacyControllerMapping::ScriptFileInfo& script : std::as_const(m_scriptFiles)) { - if (!evaluateScriptFile(script.file)) { - shutdown(); - return false; +#ifdef MIXXX_USE_QML + switch (script.type) { + case 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 + break; + case LegacyControllerMapping::ScriptFileInfo::Type::QML: + if (script.identifier.isEmpty()) { + while (!availableScreens.isEmpty()) { + QString screenIdentifier(availableScreens.firstKey()); + if (!bindSceneToScreen(script, + screenIdentifier, + availableScreens.take(screenIdentifier))) { + shutdown(); + return false; + } + } + } else if (!bindSceneToScreen(script, + script.identifier, + availableScreens.take(script.identifier))) { + shutdown(); + return false; + } + break; } - if (!script.functionPrefix.isEmpty()) { - m_scriptFunctionPrefixes.append(script.functionPrefix); + } + + if (!availableScreens.isEmpty()) { + qWarning() + << "Found screen with no QML scene able to run on it. Ignoring" + << availableScreens.size() << "screens"; + + while (!availableScreens.isEmpty()) { + auto orphanScreen = availableScreens.take(availableScreens.firstKey()); + std::move(orphanScreen)->deleteLater(); } +#endif } // For testing, do not actually initialize the scripts, just check for @@ -158,7 +387,18 @@ bool ControllerScriptEngineLegacy::initialize() { controllerName, m_logger().isDebugEnabled(), }; - if (!callFunctionOnObjects(m_scriptFunctionPrefixes, "init", args, true)) { + +#ifdef MIXXX_USE_QML + for (ControllerRenderingEngine* pScreen : qAsConst(m_renderingScreens)) { + pScreen->start(); + } +#endif + + if ( +#ifdef MIXXX_USE_QML + !m_bQmlMode && +#endif + !callFunctionOnObjects(m_scriptFunctionPrefixes, "init", args, true)) { shutdown(); return false; } @@ -166,12 +406,157 @@ bool ControllerScriptEngineLegacy::initialize() { return true; } +#ifdef MIXXX_USE_QML +bool ControllerScriptEngineLegacy::bindSceneToScreen( + const LegacyControllerMapping::ScriptFileInfo& qmlFile, + const QString& screenIdentifier, + ControllerRenderingEngine* pScreen) { + auto pScene = loadQMLFile(qmlFile, pScreen); + if (!pScene) { + return false; + } + const QMetaObject* metaObject = pScene->metaObject(); + + // TODO support typed QML with (ArrayBuffer, Date) + int methodIdx = metaObject->indexOfMethod("transformFrame(QVariant,QVariant)"); + if (methodIdx == -1 || !metaObject->method(methodIdx).isValid()) { + qDebug() << "QML Scene for screen" << screenIdentifier + << "has no transformFrame method. The frame data will be sent " + "untransformed"; + m_transformScreenFrameFunctions.insert(screenIdentifier, QMetaMethod()); + } else { + m_transformScreenFrameFunctions.insert(screenIdentifier, metaObject->method(methodIdx)); + } + connect(pScreen, + &ControllerRenderingEngine::frameRendered, + this, + &ControllerScriptEngineLegacy::handleScreenFrame); + m_renderingScreens.insert(screenIdentifier, pScreen); + m_rootItems.insert(screenIdentifier, pScene); + 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)) { + qWarning() << "Unable to find transform function info for the given screen"; + return; + }; + VERIFY_OR_DEBUG_ASSERT(m_rootItems.contains(screeninfo.identifier)) { + qWarning() << "Unable to find a root item for the given screen"; + return; + }; + + if (CmdlineArgs::Instance().getControllerPreviewScreens()) { + QImage screenDebug(frame); + + if (screeninfo.endian != std::endian::native) { + switch (screeninfo.endian) { + case std::endian::big: + qFromBigEndian(frame.constBits(), + frame.sizeInBytes() / 2, + screenDebug.bits()); + break; + case std::endian::little: + qFromLittleEndian(frame.constBits(), + frame.sizeInBytes() / 2, + screenDebug.bits()); + break; + default: + break; + } + } + if (screeninfo.reversedColor) { + screenDebug.rgbSwap(); + } + + emit previewRenderedScreen(screeninfo, screenDebug); + } + + QByteArray input((const char*)frame.constBits(), frame.sizeInBytes()); + // qDebug() << "About to transform: "<sendBytes(input); + m_renderingScreens[screeninfo.identifier]->requestSend(m_pController, input); + return; + } + + QVariant returnedValue; + + bool isSuccessful = tranformMethod.invoke(m_rootItems.value(screeninfo.identifier).get(), + Qt::DirectConnection, + Q_RETURN_ARG(QVariant, returnedValue), + Q_ARG(QVariant, input), + Q_ARG(QVariant, timestamp)); + + if (!isSuccessful || !returnedValue.isValid()) { + qWarning() << "Could not transform rendering buffer. Error should be " + "visible in previous log messages"; + return; + } + + if (returnedValue.canView()) { + // QByteArray transformedFrame(returnedValue.view()); + // qDebug() << "About to send: "<sendBytes(returnedValue.view()); + m_renderingScreens[screeninfo.identifier]->requestSend( + m_pController, returnedValue.view()); + } else if (returnedValue.canConvert()) { + // qDebug() << "About to send: "<sendBytes(returnedValue.toByteArray()); + m_renderingScreens[screeninfo.identifier]->requestSend( + m_pController, returnedValue.toByteArray()); + } else { + qWarning() << "Unable to interpret the returned data " << returnedValue; + } +} +#endif + void ControllerScriptEngineLegacy::shutdown() { +#ifdef MIXXX_USE_QML + const QStringList paths = m_fileWatcher.files(); + if (!paths.isEmpty()) { + m_fileWatcher.removePaths(paths); + } +#endif + // 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"); } + +#ifdef MIXXX_USE_QML + // Wait for up to 4 frames to allow screens to display a shutdown + // splash/idle screen or simply clear themselves + uint maxSplashOffDuration = 0; + for (const ControllerRenderingEngine* pScreen : qAsConst(m_renderingScreens)) { + maxSplashOffDuration = qMax(maxSplashOffDuration, pScreen->info().splash_off); + } + + auto splashOffDeadline = mixxx::Duration::fromMillis(maxSplashOffDuration) + + mixxx::Time::elapsed(); + while (splashOffDeadline > mixxx::Time::elapsed()) { + QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents, + (splashOffDeadline - mixxx::Time::elapsed()).toIntegerMillis()); + } + + m_rootItems.clear(); + if (!m_bTesting) { + for (ControllerRenderingEngine* pScreen : qAsConst(m_renderingScreens)) { + VERIFY_OR_DEBUG_ASSERT(!pScreen->isValid() || pScreen->stop()){}; + } + } + m_renderingScreens.clear(); + m_transformScreenFrameFunctions.clear(); +#endif m_scriptWrappedFunctionCache.clear(); m_incomingDataFunctions.clear(); m_scriptFunctionPrefixes.clear(); @@ -215,7 +600,7 @@ bool ControllerScriptEngineLegacy::evaluateScriptFile(const QFileInfo& scriptFil // evaluating it. if (!m_fileWatcher.addPath(scriptFile.absoluteFilePath())) { qCWarning(m_logger) << "Failed to watch script file" << scriptFile.absoluteFilePath(); - }; + } qCDebug(m_logger) << "Loading" << scriptFile.absoluteFilePath(); @@ -265,6 +650,74 @@ bool ControllerScriptEngineLegacy::evaluateScriptFile(const QFileInfo& scriptFil return true; } +#ifdef MIXXX_USE_QML +std::shared_ptr ControllerScriptEngineLegacy::loadQMLFile( + const LegacyControllerMapping::ScriptFileInfo& qmlScript, + const ControllerRenderingEngine* pScreen) { + VERIFY_OR_DEBUG_ASSERT(m_pJSEngine || + qmlScript.type != + LegacyControllerMapping::ScriptFileInfo::Type::QML) { + return std::shared_ptr(nullptr); + } + + std::unique_ptr qmlComponent = + std::make_unique( + std::dynamic_pointer_cast(m_pJSEngine).get(), + QUrl::fromLocalFile(qmlScript.file.absoluteFilePath()), + QQmlComponent::PreferSynchronous); + while (qmlComponent->isLoading()) { + qDebug() << "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) + qWarning() << "Unable to load the QML scene:" << error.url() + << "at line" << error.line() << ", error: " << error; + return std::shared_ptr(nullptr); + } + + VERIFY_OR_DEBUG_ASSERT(qmlComponent->isReady()) { + qWarning() << "QMLComponent isn't ready although synchronous load was requested."; + return std::shared_ptr(nullptr); + } + + QObject* pRootObject = qmlComponent->createWithInitialProperties( + QVariantMap{{"screenId", pScreen->info().identifier}}); + if (qmlComponent->isError()) { + const QList errorList = qmlComponent->errors(); + for (const QQmlError& error : errorList) + qWarning() << error.url() << error.line() << error; + return std::shared_ptr(nullptr); + } + + std::shared_ptr rootItem = + std::shared_ptr(qobject_cast(pRootObject)); + if (!rootItem) { + qWarning("run: Not a QQuickItem"); + delete pRootObject; + return std::shared_ptr(nullptr); + } + + if (!m_fileWatcher.addPath(qmlScript.file.absoluteFilePath())) { + qCWarning(m_logger) << "Failed to watch script file" << 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 70dea6a392e0..694b67838286 100644 --- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h @@ -4,10 +4,17 @@ #include #include #include +#ifdef MIXXX_USE_QML +#include +#endif #include "controllers/legacycontrollermapping.h" #include "controllers/scripting/controllerscriptenginebase.h" +#ifdef MIXXX_USE_QML +class ControllerRenderingEngine; +#endif + /// ControllerScriptEngineLegacy loads and executes controller scripts for the legacy /// JS/XML hybrid controller mapping system. class ControllerScriptEngineLegacy : public ControllerScriptEngineBase { @@ -28,11 +35,36 @@ class ControllerScriptEngineLegacy : public ControllerScriptEngineBase { public slots: void setScriptFiles(const QList& scripts); +#ifdef MIXXX_USE_QML + void setLibraryDirectories(const QList& scripts); + void setInfoScrens(const QList& scripts); + + private slots: + void handleScreenFrame( + const LegacyControllerMapping::ScreenInfo& screeninfo, + const QImage& frame, + const QDateTime& timestamp); + + signals: + /// Emitted when a screen has been rendered + void previewRenderedScreen(const LegacyControllerMapping::ScreenInfo& screen, QImage frame); +#endif private: bool evaluateScriptFile(const QFileInfo& scriptFile); - void shutdown() override; +#ifdef MIXXX_USE_QML + bool bindSceneToScreen( + const LegacyControllerMapping::ScriptFileInfo& qmlFile, + const QString& screenIdentifier, + ControllerRenderingEngine* pScreen); + bool bindSceneToScreen(); + + std::shared_ptr loadQMLFile( + const LegacyControllerMapping::ScriptFileInfo& qmlScript, + const ControllerRenderingEngine* pScreen); +#endif + void shutdown() override; QJSValue wrapArrayBufferCallback(const QJSValue& callback); bool callFunctionOnObjects(const QList& scriptFunctionPrefixes, const QString&, @@ -41,6 +73,14 @@ class ControllerScriptEngineLegacy : public ControllerScriptEngineBase { QJSValue m_makeArrayBufferWrapperFunction; QList m_scriptFunctionPrefixes; +#ifdef MIXXX_USE_QML + QHash m_renderingScreens; + QHash m_isScreenSending; + QHash> m_rootItems; + QHash m_transformScreenFrameFunctions; + QList m_libraryDirectories; + QList m_infoScreens; +#endif QList m_incomingDataFunctions; QHash m_scriptWrappedFunctionCache; QList m_scriptFiles; diff --git a/src/coreservices.cpp b/src/coreservices.cpp index a63599fe6d82..9bff17262373 100644 --- a/src/coreservices.cpp +++ b/src/coreservices.cpp @@ -27,6 +27,17 @@ #ifdef __MODPLUG__ #include "preferences/dialog/dlgprefmodplug.h" #endif +#ifdef MIXXX_USE_QML +#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/db/dbconnectionpooled.h" @@ -445,6 +456,28 @@ void CoreServices::initialize(QApplication* pApp) { } m_isInitialized = true; + +#ifdef MIXXX_USE_QML + initializeQMLSignletons(); +} + +void CoreServices::initializeQMLSignletons() { + // 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()); +#endif } void CoreServices::initializeKeyboard() { @@ -550,6 +583,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 71f84cfce701..da6b978f2bae 100644 --- a/src/coreservices.h +++ b/src/coreservices.h @@ -119,6 +119,9 @@ class CoreServices : public QObject { void initializeSettings(); void initializeScreensaverManager(); void initializeLogging(); +#ifdef MIXXX_USE_QML + void initializeQMLSignletons(); +#endif /// Tear down CoreServices that were previously initialized by `initialize()`. void finalize(); diff --git a/src/preferences/dialog/dlgpreferences.cpp b/src/preferences/dialog/dlgpreferences.cpp index b35f3a138c67..3833ad262e69 100644 --- a/src/preferences/dialog/dlgpreferences.cpp +++ b/src/preferences/dialog/dlgpreferences.cpp @@ -279,7 +279,10 @@ DlgPreferences::~DlgPreferences() { // &DlgPreferences::changePage iterates on the PreferencesPage instances in m_allPages, // but the pDlg objects of the controller items are already destroyed by DlgPrefControllers, // which causes a crash when accessed. - disconnect(contentsTreeWidget, &QTreeWidget::currentItemChanged, this, &DlgPreferences::changePage); + disconnect(contentsTreeWidget, + &QTreeWidget::currentItemChanged, + this, + &DlgPreferences::changePage); // Need to explicitly delete rather than relying on child auto-deletion // because otherwise the QStackedWidget will delete the controller // preference pages (and DlgPrefControllers dynamically generates and diff --git a/src/qml/qmlapplication.cpp b/src/qml/qmlapplication.cpp index 82cd4501ddc6..fb8b9e9857af 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(); @@ -91,10 +77,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..a7e0f4ba1c48 --- /dev/null +++ b/src/test/controller_mapping_file_handler_test.cpp @@ -0,0 +1,507 @@ +#include +#include + +#include "controllers/legacycontrollermappingfilehandler.h" +#include "test/mixxxtest.h" + +// TODO (ac) uinit test supported fortmat at +// LegacyControllerMappingFileHandler::kSupportedPixelFormat + +class LegacyControllerMappingFileHandlerTest : public MixxxTest { +}; + +// const char* const kValidBoolean = +// ""; + +// const char* const kValidInteger = +// ""; + +// // This setting has purposfully no custom "label" and description +// const char* const kValidDouble = +// ""; +// const char* const kValidEnumOption = "%2"; + +TEST_F(LegacyControllerMappingFileHandlerTest, canParseSimpleMapping) { + EXPECT_TRUE(false) << "TODO"; +} + +TEST_F(LegacyControllerMappingFileHandlerTest, canParseScreenMapping) { + EXPECT_TRUE(false) << "TODO"; +} + +TEST_F(LegacyControllerMappingFileHandlerTest, screenMappingScreenBaseDefinition) { + // identifier + // targetFps + // width + // height + EXPECT_TRUE(false) << "TODO"; +} + +TEST_F(LegacyControllerMappingFileHandlerTest, screenMappingBitFormatDefinition) { + // pixelType + // endian + EXPECT_TRUE(false) << "TODO"; +} + +TEST_F(LegacyControllerMappingFileHandlerTest, screenMappingExtraPropertiesDefinition) { + // reversed + // raw + // splashoff + EXPECT_TRUE(false) << "TODO"; +} + +TEST_F(LegacyControllerMappingFileHandlerTest, canParseHybridMapping) { + EXPECT_TRUE(false) << "TODO"; +} + +// TEST_F(LegacyControllerMappingFileHandlerTest, booleanSettingEditing) { +// QDomDocument doc; +// doc.setContent( +// QByteArray("