diff --git a/CMakeLists.txt b/CMakeLists.txt index 2eb4f543283..0e701555537 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -771,9 +771,13 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/skin/qml/asyncimageprovider.cpp src/skin/qml/qmlcontrolproxy.cpp src/skin/qml/qmlconfigproxy.cpp + src/skin/qml/qmleffectmanifestparametersmodel.cpp + src/skin/qml/qmleffectsmanagerproxy.cpp + src/skin/qml/qmleffectslotproxy.cpp src/skin/qml/qmlplayermanagerproxy.cpp src/skin/qml/qmlplayerproxy.cpp src/skin/qml/qmlskin.cpp + src/skin/qml/qmlvisibleeffectsmodel.cpp src/skin/qml/qmlwaveformoverview.cpp src/skin/legacy/skincontext.cpp src/skin/legacy/tooltips.cpp diff --git a/res/skins/QMLDemo/ComboBox.qml b/res/skins/QMLDemo/ComboBox.qml index db2e97e0dc6..d5d5e82ae5e 100644 --- a/res/skins/QMLDemo/ComboBox.qml +++ b/res/skins/QMLDemo/ComboBox.qml @@ -12,9 +12,10 @@ ComboBox { delegate: ItemDelegate { width: parent.width highlighted: root.highlightedIndex === index + text: root.textAt(index) contentItem: Text { - text: modelData + text: parent.text color: Theme.deckTextColor elide: Text.ElideRight verticalAlignment: Text.AlignVCenter diff --git a/res/skins/QMLDemo/EffectRow.qml b/res/skins/QMLDemo/EffectRow.qml index b57f926d28f..16c8d55f0ca 100644 --- a/res/skins/QMLDemo/EffectRow.qml +++ b/res/skins/QMLDemo/EffectRow.qml @@ -2,25 +2,43 @@ import "." as Skin import QtQuick 2.12 import QtQuick.Controls 2.12 -Row { +Item { id: root - height: 60 + width: positioner.width + height: positioner.height - Skin.EffectUnit { - id: effectUnit1 + Row { + id: positioner + + Skin.EffectUnit { + id: effectUnit1 + + width: root.width / 2 + unitNumber: 1 + } + + Skin.EffectUnit { + id: effectUnit2 + + width: root.width / 2 + unitNumber: 2 + } - width: root.width / 2 - height: root.height - unitNumber: 1 } - Skin.EffectUnit { - id: effectUnit2 + Skin.SectionBackground { + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.right: parent.horizontalCenter + } - width: root.width / 2 - height: root.height - unitNumber: 2 + Skin.SectionBackground { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.horizontalCenter + anchors.right: parent.right } } diff --git a/res/skins/QMLDemo/EffectSlot.qml b/res/skins/QMLDemo/EffectSlot.qml new file mode 100644 index 00000000000..f3004d376ba --- /dev/null +++ b/res/skins/QMLDemo/EffectSlot.qml @@ -0,0 +1,199 @@ +import "." as Skin +import Mixxx 0.1 as Mixxx +import QtQuick 2.12 +import "Theme" + +Item { + id: root + + property Mixxx.EffectSlotProxy slot: Mixxx.EffectsManager.getEffectSlot(1, unitNumber, effectNumber) + property int unitNumber // required + property int effectNumber // required + property bool expanded: false + readonly property string group: slot.group + property real maxSelectorWidth: 300 + + height: 50 + + Item { + id: selector + + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: Math.min(root.width, root.maxSelectorWidth) + + Skin.ControlButton { + id: effectEnableButton + + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.margins: 5 + width: 40 + group: root.group + key: "enabled" + toggleable: true + text: "ON" + activeColor: Theme.effectColor + } + + Skin.ComboBox { + id: effectSelector + + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: effectEnableButton.right + anchors.right: effectMetaKnob.left + anchors.margins: 5 + textRole: "display" + model: Mixxx.EffectsManager.visibleEffectsModel + onActivated: { + const effectId = model.get(index).effectId; + if (root.slot.effectId != effectId) + root.slot.effectId = effectId; + + } + Component.onCompleted: root.slot.onEffectIdChanged() + + Connections { + function onEffectIdChanged() { + const rowCount = effectSelector.model.rowCount(); + // TODO: Consider using an additional QHash in the + // model and provide a more efficient lookup method + for (let i = 0; i < rowCount; i++) { + if (effectSelector.model.get(i).effectId === target.effectId) { + effectSelector.currentIndex = i; + break; + } + } + } + + target: root.slot + } + + } + + Skin.ControlMiniKnob { + id: effectMetaKnob + + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.margins: 5 + arcStart: Knob.ArcStart.Minimum + width: 40 + group: root.group + key: "meta" + color: Theme.effectColor + } + + } + + ListView { + id: parametersView + + visible: root.expanded + anchors.leftMargin: 10 + anchors.top: parent.top + anchors.left: selector.right + anchors.right: parent.right + anchors.bottom: parent.bottom + clip: true + spacing: 5 + model: root.slot.parametersModel + orientation: ListView.Horizontal + + delegate: Item { + id: parameter + + property int number: index + 1 + // TODO: Use null coalescing when we switch to Qt >= 5.15 + property string label: shortName ? shortName : name + property string key: controlKey + property bool isButton: controlHint > 0 && controlHint == 6 + property bool isKnob: controlHint > 0 && controlHint < 6 + + width: 50 + height: 50 + + EmbeddedText { + anchors.fill: parent + verticalAlignment: Text.AlignBottom + text: parameter.label + font.bold: false + } + + Skin.ControlMiniKnob { + id: parameterKnob + + width: 30 + height: 30 + anchors.centerIn: parent + arcStart: 0 + group: root.group + key: parameter.key + color: Theme.effectColor + visible: parameter.isKnob + + Mixxx.ControlProxy { + id: parameterLoadedControl + + property bool loaded: value != 0 + + group: root.group + key: parameter.key + "_loaded" + } + + } + + Skin.ControlButton { + id: buttonParameterButton + + height: 22 + width: parent.width + anchors.centerIn: parent + group: root.group + key: parameter.key + activeColor: Theme.effectColor + visible: parameter.isButton + toggleable: true + text: "ON" + + Mixxx.ControlProxy { + id: buttonParameterLoadedControl + + property bool loaded: value != 0 + + group: root.group + key: parameter.key + "_loaded" + } + + } + + } + + populate: Transition { + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: 200 + } + + NumberAnimation { + property: "scale" + from: 0 + to: 1 + duration: 200 + } + + } + + Skin.FadeBehavior on opacity { + fadeTarget: parametersView + } + + } + +} diff --git a/res/skins/QMLDemo/EffectUnit.qml b/res/skins/QMLDemo/EffectUnit.qml index 2bb5206eddf..4cab128f49c 100644 --- a/res/skins/QMLDemo/EffectUnit.qml +++ b/res/skins/QMLDemo/EffectUnit.qml @@ -1,75 +1,119 @@ import "." as Skin +import Mixxx 0.1 as Mixxx import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import "Theme" Item { + id: root + property int unitNumber // required - Skin.SectionBackground { - anchors.fill: parent - } + implicitHeight: effectContainer.height - RowLayout { + Item { + id: effectContainer + + anchors.margins: 5 anchors.left: parent.left anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: effectSuperKnobFrame.left - anchors.rightMargin: 5 + anchors.right: effectUnitControlsFrame.left + height: 60 + + EffectSlot { + id: effect1 + + anchors.top: parent.top + anchors.left: parent.left + width: parent.width / 3 + unitNumber: root.unitNumber + effectNumber: 1 + expanded: false + } - Repeater { - model: 3 + EffectSlot { + id: effect2 - Item { - id: effect - - property string group: "[EffectRack1_EffectUnit" + unitNumber + "_Effect" + (index + 1) + "]" - - height: 50 - Layout.fillWidth: true - - Skin.ControlButton { - id: effectEnableButton - - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: 5 - width: 40 - group: effect.group - key: "enabled" - toggleable: true - text: "ON" - activeColor: Theme.effectColor - } + anchors.top: parent.top + anchors.left: effect1.right + width: parent.width / 3 + unitNumber: root.unitNumber + effectNumber: 2 + expanded: false + } - Skin.ComboBox { - id: effectSelector + EffectSlot { + id: effect3 - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: effectEnableButton.right - anchors.right: effectMetaKnob.left - anchors.margins: 5 - // TODO: Add a way to retrieve effect names here - model: ["---", "Effect 1", "Effect 2", "Effect 3", "Effect 4"] - } + anchors.top: parent.top + anchors.left: effect2.right + width: parent.width / 3 + unitNumber: root.unitNumber + effectNumber: 3 + expanded: false + } - Skin.ControlMiniKnob { - id: effectMetaKnob - - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: 5 - arcStart: Knob.ArcStart.Minimum - width: 40 - group: effect.group - key: "meta" - color: Theme.effectColor - } + states: State { + when: expandButton.checked + name: "expanded" + + AnchorChanges { + target: effect1 + anchors.left: effectContainer.left + } + + AnchorChanges { + target: effect2 + anchors.left: effectContainer.left + anchors.top: effect1.bottom + } + + AnchorChanges { + target: effect3 + anchors.left: effectContainer.left + anchors.top: effect2.bottom + } + + PropertyChanges { + target: effect1 + width: parent.width + expanded: true + } + + PropertyChanges { + target: effect2 + width: parent.width + expanded: true + } + + PropertyChanges { + target: effect3 + width: parent.width + expanded: true + } + + PropertyChanges { + target: effectContainer + height: 160 + } + + PropertyChanges { + target: superKnob + visible: true + } + + PropertyChanges { + target: dryWetKnob + visible: true + } + + } + transitions: Transition { + AnchorAnimation { + targets: [effect1, effect2, effect3] + duration: 150 } } @@ -77,26 +121,90 @@ Item { } Rectangle { - id: effectSuperKnobFrame + id: effectUnitControlsFrame anchors.margins: 5 anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom - width: height + width: effectUnitControls.width color: Theme.knobBackgroundColor radius: 5 - Skin.ControlKnob { - id: effectSuperKnob + Column { + id: effectUnitControls + + anchors.top: parent.top + anchors.right: parent.right + padding: 5 + spacing: 10 + + Item { + width: 40 + height: width + + Skin.Button { + id: expandButton + + anchors.fill: parent + activeColor: Theme.effectUnitColor + text: "▼" + checkable: true + } + + } + + Skin.ControlKnob { + id: superKnob + + height: 40 + width: height + arcStart: Knob.ArcStart.Minimum + group: "[EffectRack1_EffectUnit" + unitNumber + "]" + key: "super1" + color: Theme.effectUnitColor + visible: false + + Skin.FadeBehavior on visible { + fadeTarget: superKnob + } + + } + + Skin.ControlKnob { + id: dryWetKnob + + height: 40 + width: height + arcStart: Knob.ArcStart.Minimum + group: "[EffectRack1_EffectUnit" + unitNumber + "]" + key: "mix" + color: Theme.effectUnitColor + visible: false + + Skin.FadeBehavior on visible { + fadeTarget: dryWetKnob + } + + } + + add: Transition { + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: 150 + } + + NumberAnimation { + property: "scale" + from: 0 + to: 1 + duration: 150 + } + + } - anchors.centerIn: parent - width: 48 - height: 48 - arcStart: Knob.ArcStart.Minimum - group: "[EffectRack1_EffectUnit" + unitNumber + "]" - key: "super1" - color: Theme.effectUnitColor } } diff --git a/res/skins/QMLDemo/Knob.qml b/res/skins/QMLDemo/Knob.qml index e139dfd2848..8405716bb08 100644 --- a/res/skins/QMLDemo/Knob.qml +++ b/res/skins/QMLDemo/Knob.qml @@ -6,6 +6,8 @@ MixxxControls.Knob { id: root property color color // required + property url shadowSource: Theme.imgKnobShadow + property url backgroundSource: Theme.imgKnob implicitWidth: background.width implicitHeight: implicitWidth @@ -24,7 +26,7 @@ MixxxControls.Knob { anchors.right: parent.right height: width * 7 / 6 fillMode: Image.PreserveAspectFit - source: Theme.imgKnobShadow + source: root.shadowSource } background: Image { @@ -34,12 +36,10 @@ MixxxControls.Knob { anchors.left: parent.left anchors.right: parent.right height: width - source: Theme.imgKnob + source: root.backgroundSource } foreground: Item { - id: inidicator - anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -47,8 +47,8 @@ MixxxControls.Knob { Rectangle { anchors.horizontalCenter: parent.horizontalCenter - width: root.width / 30 - height: 10 + width: 2 + height: root.width / 5 y: height color: root.color } diff --git a/res/skins/QMLDemo/MiniKnob.qml b/res/skins/QMLDemo/MiniKnob.qml index b55bbc411bf..7fce622df66 100644 --- a/res/skins/QMLDemo/MiniKnob.qml +++ b/res/skins/QMLDemo/MiniKnob.qml @@ -1,56 +1,9 @@ -import Mixxx.Controls 0.1 as MixxxControls -import QtQuick 2.12 +import "." as Skin import "Theme" -MixxxControls.Knob { +Skin.Knob { id: root - property color color // required - - implicitWidth: background.width - implicitHeight: implicitWidth - arc: true - arcRadius: width * 0.45 - arcOffsetY: width * 0.01 - arcColor: root.color - arcWidth: 2 - angle: 116 - - Image { - id: shadow - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - height: width * 7 / 6 - fillMode: Image.PreserveAspectFit - source: Theme.imgKnobMiniShadow - } - - background: Image { - id: background - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - height: width - source: Theme.imgKnobMini - } - - foreground: Item { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - height: width - - Rectangle { - anchors.horizontalCenter: parent.horizontalCenter - width: root.width / 30 - height: 10 - y: height - color: root.color - } - - } - + shadowSource: Theme.imgKnobMiniShadow + backgroundSource: Theme.imgKnobMini } diff --git a/src/skin/qml/qmleffectmanifestparametersmodel.cpp b/src/skin/qml/qmleffectmanifestparametersmodel.cpp new file mode 100644 index 00000000000..69980ccc7b2 --- /dev/null +++ b/src/skin/qml/qmleffectmanifestparametersmodel.cpp @@ -0,0 +1,126 @@ +#include "skin/qml/qmleffectmanifestparametersmodel.h" + +#include + +#include "effects/effectmanifest.h" + +namespace mixxx { +namespace skin { +namespace qml { +namespace { +const QHash kRoleNames = { + {QmlEffectManifestParametersModel::IdRole, "id"}, + {QmlEffectManifestParametersModel::NameRole, "name"}, + {QmlEffectManifestParametersModel::ShortNameRole, "shortName"}, + {QmlEffectManifestParametersModel::DescriptionRole, "description"}, + {QmlEffectManifestParametersModel::ControlHintRole, "controlHint"}, + {QmlEffectManifestParametersModel::ControlKeyRole, "controlKey"}, +}; +} + +QmlEffectManifestParametersModel::QmlEffectManifestParametersModel( + EffectManifestPointer pEffectManifest, + QObject* parent) + : QAbstractListModel(parent), m_pEffectManifest(pEffectManifest) { +} + +QVariant QmlEffectManifestParametersModel::data(const QModelIndex& index, int role) const { + const QList& parameters = m_pEffectManifest->parameters(); + if (index.row() >= parameters.size()) { + return QVariant(); + } + + const EffectManifestParameterPointer pParameter = parameters.at(index.row()); + switch (role) { + case QmlEffectManifestParametersModel::IdRole: + return pParameter->id(); + case QmlEffectManifestParametersModel::NameRole: + return pParameter->name(); + case QmlEffectManifestParametersModel::ShortNameRole: + return pParameter->shortName(); + case QmlEffectManifestParametersModel::DescriptionRole: + return pParameter->description(); + case QmlEffectManifestParametersModel::ControlHintRole: + // TODO: Remove this cast, instead expose the enum directly using + // Q_ENUM after #2618 has been merged. + return static_cast(pParameter->controlHint()); + case QmlEffectManifestParametersModel::ControlKeyRole: { + // FIXME: Unfortunately our effect parameter controls are messed up. + // Even though we only have a single, ordered list of parameters, our + // COs splits up this list into two distinct list (`parameter_N` and + // `button_parameter_M`), and their indices don't match up with the + // original list. + // + // For example, if you have 4 parameters (A: Knob, B: Button, C: Knob, + // D: Knob), one would expect the following control keys: + // parameter1 -> A + // button_parameter2 -> B + // parameter3 -> C + // parameter4 -> D + // + // But in reality, this will lead to the following control keys: + // parameter1 -> A + // button_parameter1 -> B + // parameter2 -> C + // parameter3 -> D + // + // This makes it extremely hard to show the parameters in the correct + // order, because you also need to know how many parameters of the same + // type are in that list. + // + // Due to backwards compatibility, we cannot fix this. This attempts to + // solve this problem by letting the user fetch the appropriate key + // from the model. + if (pParameter->controlHint() == EffectManifestParameter::ControlHint::UNKNOWN) { + return QString(); + } + const bool isButton = pParameter->controlHint() == + EffectManifestParameter::ControlHint::TOGGLE_STEPPING; + int keyNumber = 1; + for (int i = 0; i < index.row(); i++) { + const EffectManifestParameterPointer pPrevParameter = parameters.at(i); + if (pPrevParameter->controlHint() == EffectManifestParameter::ControlHint::UNKNOWN) { + continue; + } + if (isButton == + (pPrevParameter->controlHint() == + EffectManifestParameter::ControlHint:: + TOGGLE_STEPPING)) { + keyNumber++; + } + } + + return (isButton ? QStringLiteral("button_parameter%1") + : QStringLiteral("parameter%1")) + .arg(QString::number(keyNumber)); + } + default: + return QVariant(); + } +} + +int QmlEffectManifestParametersModel::rowCount(const QModelIndex& parent) const { + if (parent.isValid()) { + return 0; + } + + // Add +1 because we also include "no effect" in the model + return m_pEffectManifest->parameters().size(); +} + +QHash QmlEffectManifestParametersModel::roleNames() const { + return kRoleNames; +} + +QVariant QmlEffectManifestParametersModel::get(int row) const { + QModelIndex idx = index(row, 0); + QVariantMap dataMap; + for (auto it = kRoleNames.constBegin(); it != kRoleNames.constEnd(); it++) { + dataMap.insert(it.value(), data(idx, it.key())); + } + return dataMap; +} + +} // namespace qml +} // namespace skin +} // namespace mixxx diff --git a/src/skin/qml/qmleffectmanifestparametersmodel.h b/src/skin/qml/qmleffectmanifestparametersmodel.h new file mode 100644 index 00000000000..bd913fb2219 --- /dev/null +++ b/src/skin/qml/qmleffectmanifestparametersmodel.h @@ -0,0 +1,39 @@ +#pragma once +#include +#include + +#include "effects/effectsmanager.h" + +namespace mixxx { +namespace skin { +namespace qml { + +class QmlEffectManifestParametersModel : public QAbstractListModel { + Q_OBJECT + public: + enum Roles { + IdRole = Qt::UserRole + 1, + NameRole, + ShortNameRole, + DescriptionRole, + ControlHintRole, + ControlKeyRole, + }; + Q_ENUM(Roles) + + explicit QmlEffectManifestParametersModel( + EffectManifestPointer pManifest, + QObject* parent = nullptr); + + QVariant data(const QModelIndex& index, int role) const override; + int rowCount(const QModelIndex& parent) const override; + QHash roleNames() const override; + Q_INVOKABLE QVariant get(int row) const; + + private: + const EffectManifestPointer m_pEffectManifest; +}; + +} // namespace qml +} // namespace skin +} // namespace mixxx diff --git a/src/skin/qml/qmleffectslotproxy.cpp b/src/skin/qml/qmleffectslotproxy.cpp new file mode 100644 index 00000000000..ef1880ff6cf --- /dev/null +++ b/src/skin/qml/qmleffectslotproxy.cpp @@ -0,0 +1,94 @@ +#include "skin/qml/qmleffectslotproxy.h" + +#include +#include + +#include "effects/effectrack.h" +#include "effects/effectslot.h" +#include "skin/qml/qmleffectmanifestparametersmodel.h" + +namespace mixxx { +namespace skin { +namespace qml { + +QmlEffectSlotProxy::QmlEffectSlotProxy(EffectRackPointer pRack, + EffectChainSlotPointer pChainSlot, + EffectSlotPointer pEffectSlot, + QObject* parent) + : QObject(parent), + m_pRack(pRack), + m_pChainSlot(pChainSlot), + m_pEffectSlot(pEffectSlot) { + DEBUG_ASSERT(m_pRack); + DEBUG_ASSERT(m_pChainSlot); + DEBUG_ASSERT(m_pEffectSlot); + connect(m_pEffectSlot.get(), + &EffectSlot::updated, + this, + &QmlEffectSlotProxy::effectIdChanged); + connect(m_pEffectSlot.get(), + &EffectSlot::updated, + this, + &QmlEffectSlotProxy::parametersModelChanged); +} + +int QmlEffectSlotProxy::getRackNumber() const { + return m_pRack->getRackNumber(); +} + +QString QmlEffectSlotProxy::getRackGroup() const { + return m_pRack->getGroup(); +} + +int QmlEffectSlotProxy::getChainSlotNumber() const { + return m_pChainSlot->getChainSlotNumber(); +} + +QString QmlEffectSlotProxy::getChainSlotGroup() const { + return m_pChainSlot->getGroup(); +} + +int QmlEffectSlotProxy::getNumber() const { + return m_pEffectSlot->getEffectSlotNumber(); +} + +QString QmlEffectSlotProxy::getGroup() const { + return m_pEffectSlot->getGroup(); +} + +QString QmlEffectSlotProxy::getEffectId() const { + const EffectPointer pEffect = m_pEffectSlot->getEffect(); + if (!pEffect) { + return QString(); + } + + const EffectManifestPointer pManifest = pEffect->getManifest(); + return pManifest->id(); +} + +void QmlEffectSlotProxy::setEffectId(const QString& effectId) { + m_pRack->maybeLoadEffect( + m_pChainSlot->getChainSlotNumber(), + m_pEffectSlot->getEffectSlotNumber(), + effectId); +} + +QmlEffectManifestParametersModel* QmlEffectSlotProxy::getParametersModel() const { + const EffectPointer pEffect = m_pEffectSlot->getEffect(); + if (!pEffect) { + return nullptr; + } + + const EffectManifestPointer pManifest = pEffect->getManifest(); + VERIFY_OR_DEBUG_ASSERT(pManifest) { + return nullptr; + } + + QmlEffectManifestParametersModel* pModel = new QmlEffectManifestParametersModel(pManifest); + QQmlEngine::setObjectOwnership(pModel, QQmlEngine::JavaScriptOwnership); + return pModel; +} + +} // namespace qml +} // namespace skin +} // namespace mixxx diff --git a/src/skin/qml/qmleffectslotproxy.h b/src/skin/qml/qmleffectslotproxy.h new file mode 100644 index 00000000000..29044657e1c --- /dev/null +++ b/src/skin/qml/qmleffectslotproxy.h @@ -0,0 +1,54 @@ +#pragma once +#include + +#include "effects/effectsmanager.h" + +namespace mixxx { +namespace skin { +namespace qml { + +class QmlEffectManifestParametersModel; + +class QmlEffectSlotProxy : public QObject { + Q_OBJECT + Q_PROPERTY(int rackNumber READ getRackNumber CONSTANT) + Q_PROPERTY(QString rackGroup READ getRackGroup CONSTANT) + Q_PROPERTY(int chainSlotNumber READ getChainSlotNumber CONSTANT) + Q_PROPERTY(QString chainSlotGroup READ getChainSlotGroup CONSTANT) + Q_PROPERTY(int number READ getNumber CONSTANT) + Q_PROPERTY(QString group READ getGroup CONSTANT) + Q_PROPERTY(QString effectId READ getEffectId WRITE setEffectId NOTIFY effectIdChanged) + Q_PROPERTY(mixxx::skin::qml::QmlEffectManifestParametersModel* parametersModel + READ getParametersModel NOTIFY parametersModelChanged) + + public: + explicit QmlEffectSlotProxy(EffectRackPointer pEffectRack, + EffectChainSlotPointer pChainSlot, + EffectSlotPointer pEffectSlot, + QObject* parent = nullptr); + + int getRackNumber() const; + QString getRackGroup() const; + int getChainSlotNumber() const; + QString getChainSlotGroup() const; + int getNumber() const; + QString getGroup() const; + QString getEffectId() const; + QmlEffectManifestParametersModel* getParametersModel() const; + + public slots: + void setEffectId(const QString& effectId); + + signals: + void effectIdChanged(); + void parametersModelChanged(); + + private: + const EffectRackPointer m_pRack; + const EffectChainSlotPointer m_pChainSlot; + const EffectSlotPointer m_pEffectSlot; +}; + +} // namespace qml +} // namespace skin +} // namespace mixxx diff --git a/src/skin/qml/qmleffectsmanagerproxy.cpp b/src/skin/qml/qmleffectsmanagerproxy.cpp new file mode 100644 index 00000000000..17d91b39ecc --- /dev/null +++ b/src/skin/qml/qmleffectsmanagerproxy.cpp @@ -0,0 +1,60 @@ +#include "skin/qml/qmleffectsmanagerproxy.h" + +#include +#include + +#include "effects/effectchainslot.h" +#include "effects/effectrack.h" +#include "skin/qml/qmleffectslotproxy.h" +#include "skin/qml/qmlvisibleeffectsmodel.h" + +namespace mixxx { +namespace skin { +namespace qml { + +QmlEffectsManagerProxy::QmlEffectsManagerProxy( + std::shared_ptr pEffectsManager, QObject* parent) + : QObject(parent), + m_pEffectsManager(pEffectsManager), + m_pVisibleEffectsModel( + new QmlVisibleEffectsModel(pEffectsManager, this)) { +} + +QmlEffectSlotProxy* QmlEffectsManagerProxy::getEffectSlot( + int rackNumber, int unitNumber, int effectNumber) const { + // Subtract 1 from all numbers, because internally our indices are + // zero-based + const int rackIndex = rackNumber - 1; + const auto pRack = m_pEffectsManager->getStandardEffectRack(rackIndex); + if (!pRack) { + qWarning() << "QmlEffectsManagerProxy: Effect Rack" << rackNumber << "not found!"; + return nullptr; + } + + const int unitIndex = unitNumber - 1; + const auto pEffectUnit = pRack->getEffectChainSlot(unitIndex); + if (!pEffectUnit) { + qWarning() << "QmlEffectsManagerProxy: Effect Unit" << unitNumber + << "in Rack" << rackNumber << "not found!"; + return nullptr; + } + + const int effectIndex = effectNumber - 1; + const auto pEffectSlot = pEffectUnit->getEffectSlot(effectIndex); + if (!pEffectSlot) { + qWarning() << "QmlEffectsManagerProxy: Effect Slot" << effectNumber + << "in Unit" << unitNumber << "of Rack" << rackNumber + << "not found!"; + return nullptr; + } + + // Don't set a parent here, so that the QML engine deletes the object when + // the corresponding JS object is garbage collected. + QmlEffectSlotProxy* pEffectSlotProxy = new QmlEffectSlotProxy(pRack, pEffectUnit, pEffectSlot); + QQmlEngine::setObjectOwnership(pEffectSlotProxy, QQmlEngine::JavaScriptOwnership); + return pEffectSlotProxy; +} + +} // namespace qml +} // namespace skin +} // namespace mixxx diff --git a/src/skin/qml/qmleffectsmanagerproxy.h b/src/skin/qml/qmleffectsmanagerproxy.h new file mode 100644 index 00000000000..9b1af880723 --- /dev/null +++ b/src/skin/qml/qmleffectsmanagerproxy.h @@ -0,0 +1,33 @@ +#pragma once +#include + +#include "effects/effectsmanager.h" + +namespace mixxx { +namespace skin { +namespace qml { + +class QmlVisibleEffectsModel; +class QmlEffectSlotProxy; + +class QmlEffectsManagerProxy : public QObject { + Q_OBJECT + Q_PROPERTY(mixxx::skin::qml::QmlVisibleEffectsModel* visibleEffectsModel + MEMBER m_pVisibleEffectsModel CONSTANT); + + public: + explicit QmlEffectsManagerProxy( + std::shared_ptr pEffectsManager, + QObject* parent = nullptr); + + Q_INVOKABLE mixxx::skin::qml::QmlEffectSlotProxy* getEffectSlot( + int rackNumber, int unitNumber, int effectNumber) const; + + private: + const std::shared_ptr m_pEffectsManager; + QmlVisibleEffectsModel* m_pVisibleEffectsModel; +}; + +} // namespace qml +} // namespace skin +} // namespace mixxx diff --git a/src/skin/qml/qmlskin.cpp b/src/skin/qml/qmlskin.cpp index edf35eb34e0..0e0ed36631f 100644 --- a/src/skin/qml/qmlskin.cpp +++ b/src/skin/qml/qmlskin.cpp @@ -8,8 +8,12 @@ #include "skin/qml/asyncimageprovider.h" #include "skin/qml/qmlconfigproxy.h" #include "skin/qml/qmlcontrolproxy.h" +#include "skin/qml/qmleffectmanifestparametersmodel.h" +#include "skin/qml/qmleffectslotproxy.h" +#include "skin/qml/qmleffectsmanagerproxy.h" #include "skin/qml/qmlplayermanagerproxy.h" #include "skin/qml/qmlplayerproxy.h" +#include "skin/qml/qmlvisibleeffectsmodel.h" #include "skin/qml/qmlwaveformoverview.h" #include "util/assert.h" @@ -128,6 +132,40 @@ QWidget* QmlSkin::loadSkin(QWidget* pParent, qmlRegisterType("Mixxx", 0, 1, "ControlProxy"); qmlRegisterType("Mixxx", 0, 1, "WaveformOverview"); + qmlRegisterSingletonType("Mixxx", + 0, + 1, + "EffectsManager", + lambda_to_singleton_type_factory_ptr( + [pCoreServices](QQmlEngine* pEngine, + QJSEngine* pScriptEngine) -> QObject* { + Q_UNUSED(pScriptEngine); + + QmlEffectsManagerProxy* pEffectsManagerProxy = + new QmlEffectsManagerProxy( + pCoreServices->getEffectsManager(), + pEngine); + return pEffectsManagerProxy; + })); + qmlRegisterUncreatableType("Mixxx", + 0, + 1, + "VisibleEffectsModel", + "VisibleEffectsModel objects can't be created directly, please use " + "Mixxx.EffectsManager.visibleEffectsModel"); + qmlRegisterUncreatableType("Mixxx", + 0, + 1, + "EffectManifestParametersModel", + "EffectManifestParametersModel objects can't be created directly, " + "please use Mixxx.EffectsSlot.parametersModel"); + qmlRegisterUncreatableType("Mixxx", + 0, + 1, + "EffectSlotProxy", + "EffectSlotProxy objects can't be created directly, please use " + "Mixxx.EffectsManager.getEffectSlot(rackNumber, unitNumber, effectNumber)"); + qmlRegisterSingletonType("Mixxx", 0, 1, @@ -150,7 +188,7 @@ QWidget* QmlSkin::loadSkin(QWidget* pParent, "Player objects can't be created directly, please use " "Mixxx.PlayerManager.getPlayer(group)"); - qmlRegisterSingletonType("Mixxx", + qmlRegisterSingletonType("Mixxx", 0, 1, "Config", diff --git a/src/skin/qml/qmlvisibleeffectsmodel.cpp b/src/skin/qml/qmlvisibleeffectsmodel.cpp new file mode 100644 index 00000000000..ca49643a5e2 --- /dev/null +++ b/src/skin/qml/qmlvisibleeffectsmodel.cpp @@ -0,0 +1,89 @@ +#include "skin/qml/qmlvisibleeffectsmodel.h" + +#include + +#include "effects/effectmanifest.h" +#include "effects/effectsmanager.h" + +namespace mixxx { +namespace skin { +namespace qml { +namespace { +const QHash kRoleNames = { + {Qt::DisplayRole, "display"}, + {Qt::ToolTipRole, "tooltip"}, + {QmlVisibleEffectsModel::EffectIdRole, "effectId"}, +}; +} + +QmlVisibleEffectsModel::QmlVisibleEffectsModel( + std::shared_ptr pEffectsManager, + QObject* parent) + : QAbstractListModel(parent), m_pEffectsManager(pEffectsManager) { + slotVisibleEffectsUpdated(); + connect(m_pEffectsManager.get(), + &EffectsManager::visibleEffectsUpdated, + this, + &QmlVisibleEffectsModel::slotVisibleEffectsUpdated); +} + +void QmlVisibleEffectsModel::slotVisibleEffectsUpdated() { + beginResetModel(); + m_visibleEffectManifests = m_pEffectsManager->getVisibleEffectManifests(); + endResetModel(); +} + +QVariant QmlVisibleEffectsModel::data(const QModelIndex& index, int role) const { + if (index.row() == 0) { + switch (role) { + case Qt::DisplayRole: + return EffectsManager::kNoEffectString; + case Qt::ToolTipRole: + return tr("No effect loaded."); + default: + return QVariant(); + } + } + + if (index.row() > m_visibleEffectManifests.size()) { + return QVariant(); + } + + const EffectManifestPointer pManifest = m_visibleEffectManifests.at(index.row() - 1); + switch (role) { + case Qt::DisplayRole: + return pManifest->displayName(); + case Qt::ToolTipRole: + return pManifest->description(); + case QmlVisibleEffectsModel::EffectIdRole: + return pManifest->id(); + default: + return QVariant(); + } +} + +int QmlVisibleEffectsModel::rowCount(const QModelIndex& parent) const { + if (parent.isValid()) { + return 0; + } + + // Add +1 because we also include "no effect" in the model + return m_visibleEffectManifests.size() + 1; +} + +QHash QmlVisibleEffectsModel::roleNames() const { + return kRoleNames; +} + +QVariant QmlVisibleEffectsModel::get(int row) const { + QModelIndex idx = index(row, 0); + QVariantMap dataMap; + for (auto it = kRoleNames.constBegin(); it != kRoleNames.constEnd(); it++) { + dataMap.insert(it.value(), data(idx, it.key())); + } + return dataMap; +} + +} // namespace qml +} // namespace skin +} // namespace mixxx diff --git a/src/skin/qml/qmlvisibleeffectsmodel.h b/src/skin/qml/qmlvisibleeffectsmodel.h new file mode 100644 index 00000000000..b061471a359 --- /dev/null +++ b/src/skin/qml/qmlvisibleeffectsmodel.h @@ -0,0 +1,38 @@ +#pragma once +#include +#include + +#include "effects/effectsmanager.h" + +namespace mixxx { +namespace skin { +namespace qml { + +class QmlVisibleEffectsModel : public QAbstractListModel { + Q_OBJECT + public: + enum Roles { + EffectIdRole = Qt::UserRole + 1, + }; + Q_ENUM(Roles) + + explicit QmlVisibleEffectsModel( + std::shared_ptr pEffectsManager, + QObject* parent = nullptr); + + QVariant data(const QModelIndex& index, int role) const override; + int rowCount(const QModelIndex& parent) const override; + QHash roleNames() const override; + Q_INVOKABLE QVariant get(int row) const; + + private slots: + void slotVisibleEffectsUpdated(); + + private: + const std::shared_ptr m_pEffectsManager; + QList m_visibleEffectManifests; +}; + +} // namespace qml +} // namespace skin +} // namespace mixxx