diff --git a/README.md b/README.md index ddaf28e..1c5522e 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ qputenv("QSG_RENDER_LOOP", "basic"); - [ ] Touching sprite block - [ ] Touching color blocks - [ ] Pen blocks -- [ ] Monitors +- [x] Monitors - [ ] Graphics effects (maybe using shaders) - [ ] Speech and thought bubbles - [ ] Question text box ("ask and wait" block) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 01f19d5..5cd33b5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,10 @@ qt_add_qml_module(scratchcpp-render OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/ScratchCPP/Render QML_FILES ProjectPlayer.qml + RESOURCES + internal/ValueMonitor.qml + internal/MonitorSlider.qml + internal/ListMonitor.qml SOURCES global.h projectloader.cpp @@ -18,6 +22,14 @@ qt_add_qml_module(scratchcpp-render stagemodel.h spritemodel.cpp spritemodel.h + monitormodel.cpp + monitormodel.h + valuemonitormodel.cpp + valuemonitormodel.h + listmonitormodel.cpp + listmonitormodel.h + listmonitorlistmodel.cpp + listmonitorlistmodel.h irenderedtarget.h renderedtarget.cpp renderedtarget.h diff --git a/src/ProjectPlayer.qml b/src/ProjectPlayer.qml index 9828345..908dbf6 100644 --- a/src/ProjectPlayer.qml +++ b/src/ProjectPlayer.qml @@ -5,6 +5,8 @@ import QtQuick.Layouts import QtQuick.Controls import ScratchCPP.Render +import "internal" + ProjectScene { property string fileName property int stageWidth: 480 @@ -64,6 +66,21 @@ ProjectScene { else clones.model.remove(i); } + + onMonitorAdded: (monitorModel)=> monitors.model.append({"monitorModel": monitorModel}) + + onMonitorRemoved: (monitorModel)=> { + // TODO: Removing the monitor from C++ would probably be faster + let i; + + for(i = 0; i < monitors.model.count; i++) { + if(monitors.model.get(i).monitorModel === monitorModel) + break; + } + + if(i !== monitors.model.count) + monitors.model.remove(i); + } } function start() { @@ -120,6 +137,57 @@ ProjectScene { delegate: renderedSprite } + SceneMouseArea { + id: sceneMouseArea + anchors.fill: parent + stage: stageTarget + projectLoader: loader + onMouseMoved: (x, y)=> root.handleMouseMove(x, y) + onMousePressed: root.handleMousePress() + onMouseReleased: root.handleMouseRelease() + } + + Component { + id: renderedValueMonitor + + ValueMonitor { + model: parent.model + scale: root.stageScale + transformOrigin: Item.TopLeft + x: model.x * scale + y: model.y * scale + } + } + + Component { + id: renderedListMonitor + + ListMonitor { + model: parent.model + scale: root.stageScale + transformOrigin: Item.TopLeft + x: model.x * scale + y: model.y * scale + } + } + + Component { + id: renderedMonitor + + Loader { + readonly property MonitorModel model: monitorModel + sourceComponent: monitorModel ? (monitorModel.type === MonitorModel.Value ? renderedValueMonitor : renderedListMonitor) : null + active: sourceComponent != null + z: loader.sprites.length + loader.clones.length + 1 // above all sprites + } + } + + Repeater { + id: monitors + model: ListModel {} + delegate: renderedMonitor + } + Loader { anchors.fill: parent active: showLoadingProgress && loading @@ -159,15 +227,5 @@ ProjectScene { Item { Layout.fillHeight: true } } } - - SceneMouseArea { - id: sceneMouseArea - anchors.fill: parent - stage: stageTarget - projectLoader: loader - onMouseMoved: (x, y)=> root.handleMouseMove(x, y) - onMousePressed: root.handleMousePress() - onMouseReleased: root.handleMouseRelease() - } } } diff --git a/src/internal/ListMonitor.qml b/src/internal/ListMonitor.qml new file mode 100644 index 0000000..36e02d4 --- /dev/null +++ b/src/internal/ListMonitor.qml @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import ScratchCPP.Render + +// NOTE: All the values here make list monitors look +// like on Scratch, so be careful when doing any changes. +Rectangle { + id: root + property ListMonitorModel model: null + + width: model ? (model.width > 0 ? model.width : priv.defaultWidth) : priv.defaultWidth + height: model ? (model.height > 0 ? model.height : priv.defaultHeight) : priv.defaultHeight + color: Qt.rgba(0.9, 0.94, 1, 1) + border.color: Qt.rgba(0.765, 0.8, 0.85, 1) + radius: 5 + visible: model ? model.visible : true + + QtObject { + id: priv + readonly property int defaultWidth: 102 + readonly property int defaultHeight: 203 + readonly property color textColor: Qt.rgba(0.34, 0.37, 0.46, 1) + } + + Text { + id: header + anchors.left: parent.left + anchors.top: parent.top + width: parent.width + color: priv.textColor + text: model ? model.name : "" + textFormat: Text.PlainText + font.pointSize: 9 + font.bold: true + font.family: "Helvetica" + horizontalAlignment: Qt.AlignHCenter + wrapMode: Text.WordWrap + clip: true + padding: 3 + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: 1 + anchors.topMargin: 1 + anchors.rightMargin: 1 + height: root.height + color: "white" + radius: root.radius + z: -1 + } + } + + Text { + id: emptyText + anchors.left: parent.left + anchors.right: parent.right + anchors.top: header.bottom + visible: listView.count <= 0 + color: priv.textColor + text: qsTr("(empty)") + textFormat: Text.PlainText + font.pointSize: 9 + font.family: "Helvetica" + horizontalAlignment: Qt.AlignHCenter + wrapMode: Text.WordWrap + clip: true + padding: 3 + } + + ListView { + property real oldContentY + readonly property int scrollBarWidth: 15 + + id: listView + anchors.left: parent.left + anchors.right: parent.right + anchors.top: header.bottom + anchors.bottom: footer.top + anchors.topMargin: 1 + clip: true + model: root.model ? root.model.listModel : null + boundsBehavior: Flickable.StopAtBounds + + ScrollBar.vertical: ScrollBar { + id: scrollBar + anchors.right: listView.right + anchors.rightMargin: 2 + width: 13 + visible: scrollBar.size < 1 + policy: ScrollBar.AlwaysOn + + contentItem: Rectangle { + color: scrollBar.pressed ? Qt.rgba(0.47, 0.47, 0.47, 1) : (hoverHandler.hovered ? Qt.rgba(0.66, 0.66, 0.66, 1) : Qt.rgba(0.76, 0.76, 0.76, 1)) + + HoverHandler { + id: hoverHandler + } + } + + background: null // background is a separate component because contentItem width can't be changed + } + + Rectangle { + // Scroll bar background + id: scrollBarBg + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.rightMargin: 1 + visible: scrollBar.visible + width: listView.scrollBarWidth + color: Qt.rgba(0.95, 0.95, 0.95, 1) + } + + delegate: RowLayout { + width: scrollBar.visible ? listView.width - listView.scrollBarWidth - 6 : listView.width - 6 + height: implicitHeight + 2 + spacing: 6 + + Text { + color: priv.textColor + text: index + 1 + font.pointSize: 9 + font.bold: true + font.family: "Helvetica" + Layout.leftMargin: 6 + } + + Item { + height: 22 + Layout.fillWidth: true + + TextEdit { + // TextEdit instead of Text for mouse selection + id: itemText + anchors.left: parent.left + anchors.leftMargin: 3 + color: "white" + text: value + textFormat: TextEdit.PlainText + font.pointSize: 9 + font.family: "Helvetica" + selectByMouse: true + padding: 2 + Layout.rightMargin: 6 + } + + Rectangle { + anchors.fill: parent + color: root.model ? root.model.color : "green" + border.color: color.darker(1.2) + radius: root.radius + z: -1 + } + } + } + } + + Text { + id: footer + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + color: priv.textColor + text: qsTr("length %1").arg(listView.count) + textFormat: Text.PlainText + font.pointSize: 9 + font.bold: true + font.family: "Helvetica" + horizontalAlignment: Qt.AlignHCenter + wrapMode: Text.WordWrap + clip: true + padding: 3 + + Rectangle { + anchors.fill: parent + anchors.leftMargin: 1 + anchors.bottomMargin: 1 + anchors.rightMargin: 1 + anchors.topMargin: -5 + color: "white" + radius: root.radius + z: -1 + } + } + + Rectangle { + // for header and footer borders + anchors.left: parent.left + anchors.right: parent.right + anchors.top: header.bottom + anchors.bottom: footer.top + color: "transparent" + border.color: root.border.color + } +} diff --git a/src/internal/MonitorSlider.qml b/src/internal/MonitorSlider.qml new file mode 100644 index 0000000..d9a02f8 --- /dev/null +++ b/src/internal/MonitorSlider.qml @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material + +// NOTE: All the values here make monitor sliders look exactly +// like on Scratch, so be careful when doing any changes. +Slider { + id: control + property bool discrete: false + + QtObject { + id: priv + readonly property color bgColor: Qt.rgba(0.94, 0.94, 0.94, 1) + readonly property color currentBgColor: control.pressed ? Qt.rgba(0.96, 0.96, 0.96, 1) : (hoverHandler.hovered ? Qt.darker(bgColor, 1.05) : bgColor) + + readonly property color bgBorderColor: Qt.rgba(0.7, 0.7, 0.7, 1) + readonly property color currentBgBorderColor: control.pressed ? Qt.rgba(0.77, 0.77, 0.77, 1) : (hoverHandler.hovered ? Qt.darker(bgBorderColor, 1.1) : bgBorderColor) + + readonly property color sliderColor: Qt.rgba(0, 0.46, 1, 1) + readonly property color currentSliderColor: control.pressed ? Qt.rgba(0.22, 0.58, 1, 1) : (hoverHandler.hovered ? Qt.darker(sliderColor, 1.28) : sliderColor) + + readonly property color positionBorderColor: Qt.rgba(0.21, 0.46, 0.75, 1) + readonly property color currentPositionBorderColor: control.pressed ? Qt.rgba(0.37, 0.56, 0.79, 1) : (hoverHandler.hovered ? Qt.darker(positionBorderColor, 1.28) : positionBorderColor) + } + + stepSize: discrete ? 1 : 0.1 + snapMode: Slider.SnapAlways + implicitWidth: 119 + implicitHeight: 16 + leftPadding: -4 + rightPadding: -4 + topPadding: -1 + bottomPadding: 0 + activeFocusOnTab: false + hoverEnabled: true + + background: Rectangle { + x: control.leftPadding + y: control.topPadding + control.availableHeight / 2 - height / 2 + implicitWidth: 200 + implicitHeight: 7.5 + width: control.availableWidth + height: implicitHeight + radius: 6 + color: priv.currentBgColor + border.color: priv.currentBgBorderColor + + Rectangle { + width: control.visualPosition * (parent.width - handle.width) + handle.width + height: parent.height + color: priv.currentSliderColor + border.color: priv.currentPositionBorderColor + radius: 6 + } + } + + handle: Rectangle { + x: control.leftPadding + control.visualPosition * (control.availableWidth - width) + y: control.topPadding + control.availableHeight / 2 - height / 2 + implicitWidth: 15 + implicitHeight: 15 + radius: 7 + color: control.pressed ? priv.currentSliderColor : (handleHoverHandler.hovered ? priv.currentSliderColor : priv.sliderColor) + + HoverHandler { + id: handleHoverHandler + } + } + + HoverHandler { + id: hoverHandler + } +} diff --git a/src/internal/ValueMonitor.qml b/src/internal/ValueMonitor.qml new file mode 100644 index 0000000..74dd34f --- /dev/null +++ b/src/internal/ValueMonitor.qml @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick +import QtQuick.Layouts +import ScratchCPP.Render + +// NOTE: All the values here make monitors look exactly +// like on Scratch, so be careful when doing any changes. +Rectangle { + property ValueMonitorModel model: null + + color: model ? (model.mode === ValueMonitorModel.Large ? model.color : priv.bgColor) : priv.bgColor + border.color: Qt.rgba(0.765, 0.8, 0.85, 1) + radius: 5 + width: layout.implicitWidth + priv.horizontalMargins * 2 + height: layout.implicitHeight + priv.verticalMargins * 2 + visible: model ? model.visible : true + + QtObject { + id: priv + readonly property int horizontalMargins: 9 + readonly property double verticalMargins: 2 + readonly property color bgColor: Qt.rgba(0.9, 0.94, 1, 1) + } + + ColumnLayout { + id: layout + anchors.leftMargin: priv.horizontalMargins + anchors.rightMargin: priv.horizontalMargins + anchors.topMargin: priv.verticalMargins + anchors.bottomMargin: priv.verticalMargins + anchors.centerIn: parent + spacing: 0 + + Loader { + active: model ? model.mode !== ValueMonitorModel.Large : true + sourceComponent: RowLayout { + spacing: 9 + + Text { + color: Qt.rgba(0.34, 0.37, 0.46, 1) + text: model ? model.name : "" + textFormat: Text.PlainText + font.pointSize: 9 + font.bold: true + font.family: "Helvetica" + } + + Text { + id: valueText + color: "white" + text: model ? model.value : "" + textFormat: Text.PlainText + font.pointSize: 9 + font.family: "Helvetica" + leftPadding: 2 + rightPadding: 2 + topPadding: -2 + bottomPadding: -1 + horizontalAlignment: Qt.AlignHCenter + Layout.minimumWidth: 40 + + Rectangle { + color: model ? model.color : "green" + radius: 5 + anchors.fill: parent + anchors.margins: 0 + z: -1 + } + } + } + } + + Loader { + active: model ? model.mode === ValueMonitorModel.Slider : false + sourceComponent: MonitorSlider { + from: model ? model.sliderMin : 0 + to: model ? model.sliderMax : 0 + discrete: model ? model.discrete : true + value: model ? model.value : 0 + Layout.fillWidth: true + onMoved: { + if(model) + model.value = value; + } + } + } + + Loader { + active: model ? model.mode === ValueMonitorModel.Large : false + sourceComponent: Text { + color: "white" + text: model ? model.value : "" + textFormat: Text.PlainText + font.pointSize: 12 + font.family: "Helvetica" + leftPadding: 2 + rightPadding: 2 + topPadding: -3 + bottomPadding: 0 + horizontalAlignment: Qt.AlignHCenter + width: Math.max(31, implicitWidth) + height: Math.max(14.46, implicitHeight) + } + } + } +} diff --git a/src/listmonitorlistmodel.cpp b/src/listmonitorlistmodel.cpp new file mode 100644 index 0000000..3cd7fd7 --- /dev/null +++ b/src/listmonitorlistmodel.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include + +#include "listmonitorlistmodel.h" + +using namespace scratchcpprender; + +ListMonitorListModel::ListMonitorListModel(QObject *parent) : + QAbstractListModel(parent) +{ +} + +void ListMonitorListModel::setList(libscratchcpp::List *list) +{ + if (!list) + return; + + // Initial load + if (m_list != list) { + beginResetModel(); + m_list = list; + m_oldRowCount = m_list->size(); + endResetModel(); + return; + } + + // Notify about changed items + int count = std::min(m_oldRowCount, static_cast(m_list->size())); + + for (int i = 0; i < count; i++) + emit dataChanged(index(i), index(i)); + + // Notify about new items (at the end of the list) + if (m_list->size() > m_oldRowCount) { + beginInsertRows(QModelIndex(), m_oldRowCount, m_list->size() - 1); + endInsertRows(); + } else if (m_list->size() < m_oldRowCount) { + // Notify about removed items (at the end of the list) + beginRemoveRows(QModelIndex(), m_list->size(), m_oldRowCount - 1); + endRemoveRows(); + } + + m_oldRowCount = m_list->size(); +} + +int ListMonitorListModel::rowCount(const QModelIndex &parent) const +{ + if (m_list) + return m_list->size(); + else + return 0; +} + +QVariant ListMonitorListModel::data(const QModelIndex &index, int role) const +{ + if (!m_list || index.row() < 0 || index.row() >= m_list->size()) + return QVariant(); + + return QString::fromStdString((*m_list)[index.row()].toString()); +} + +QHash ListMonitorListModel::roleNames() const +{ + return { { 0, "value" } }; +} diff --git a/src/listmonitorlistmodel.h b/src/listmonitorlistmodel.h new file mode 100644 index 0000000..cb0a8ce --- /dev/null +++ b/src/listmonitorlistmodel.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +namespace libscratchcpp +{ + +class List; + +} + +namespace scratchcpprender +{ + +class ListMonitorListModel : public QAbstractListModel +{ + public: + explicit ListMonitorListModel(QObject *parent = nullptr); + + void setList(libscratchcpp::List *list); + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + private: + libscratchcpp::List *m_list = nullptr; + int m_oldRowCount = 0; +}; + +} // namespace scratchcpprender diff --git a/src/listmonitormodel.cpp b/src/listmonitormodel.cpp new file mode 100644 index 0000000..67f920a --- /dev/null +++ b/src/listmonitormodel.cpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include +#include + +#include "listmonitormodel.h" +#include "listmonitorlistmodel.h" + +using namespace scratchcpprender; + +ListMonitorModel::ListMonitorModel(QObject *parent) : + MonitorModel(parent) +{ + m_listModel = new ListMonitorListModel(this); +} + +ListMonitorModel::ListMonitorModel(libscratchcpp::IBlockSection *section, QObject *parent) : + ListMonitorModel(parent) +{ + if (!section) + return; + + // TODO: Get the color from the block section + std::string name = section->name(); + if (name == "Motion") + m_color = QColor::fromString("#4C97FF"); + else if (name == "Looks") + m_color = QColor::fromString("#9966FF"); + else if (name == "Sound") + m_color = QColor::fromString("#CF63CF"); + else if (name == "Sensing") + m_color = QColor::fromString("#5CB1D6"); + else if (name == "Variables") + m_color = QColor::fromString("#FF8C1A"); + else if (name == "Lists") + m_color = QColor::fromString("#FF661A"); +} + +void ListMonitorModel::onValueChanged(const libscratchcpp::VirtualMachine *vm) +{ + if (vm->registerCount() == 1) { + long index = vm->getInput(0, 1)->toLong(); + libscratchcpp::List *list = vm->lists()[index]; + m_listModel->setList(list); + } +} + +MonitorModel::Type ListMonitorModel::type() const +{ + return Type::List; +} + +const QColor &ListMonitorModel::color() const +{ + return m_color; +} + +ListMonitorListModel *ListMonitorModel::listModel() const +{ + return m_listModel; +} diff --git a/src/listmonitormodel.h b/src/listmonitormodel.h new file mode 100644 index 0000000..2afe1d1 --- /dev/null +++ b/src/listmonitormodel.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include +#include + +#include "monitormodel.h" + +Q_MOC_INCLUDE("listmonitorlistmodel.h") + +namespace libscratchcpp +{ + +class IBlockSection; + +} + +namespace scratchcpprender +{ + +class ListMonitorListModel; + +class ListMonitorModel : public MonitorModel +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(QColor color READ color NOTIFY colorChanged) + Q_PROPERTY(ListMonitorListModel *listModel READ listModel NOTIFY listModelChanged) + + public: + ListMonitorModel(QObject *parent = nullptr); + ListMonitorModel(libscratchcpp::IBlockSection *section, QObject *parent = nullptr); + + void onValueChanged(const libscratchcpp::VirtualMachine *vm) override; + + Type type() const override; + + const QColor &color() const; + + ListMonitorListModel *listModel() const; + + signals: + void colorChanged(); + void listModelChanged(); + + private: + QColor m_color = Qt::green; + ListMonitorListModel *m_listModel = nullptr; +}; + +} // namespace scratchcpprender diff --git a/src/monitormodel.cpp b/src/monitormodel.cpp new file mode 100644 index 0000000..5ff9d8c --- /dev/null +++ b/src/monitormodel.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include +#include + +#include "monitormodel.h" + +using namespace scratchcpprender; + +MonitorModel::MonitorModel(QObject *parent) : + QObject(parent) +{ +} + +QString MonitorModel::name() const +{ + if (m_monitor) { + if (libscratchcpp::Sprite *sprite = m_monitor->sprite()) + return QString::fromStdString(sprite->name() + ": " + m_monitor->name()); + else + return QString::fromStdString(m_monitor->name()); + } else + return ""; +} + +bool MonitorModel::visible() const +{ + if (m_monitor) + return m_monitor->visible(); + else + return false; +} + +void MonitorModel::init(libscratchcpp::Monitor *monitor) +{ + m_monitor = monitor; +} + +void MonitorModel::onVisibleChanged(bool visible) +{ + emit visibleChanged(); +} + +libscratchcpp::Monitor *MonitorModel::monitor() const +{ + return m_monitor; +} + +int MonitorModel::x() const +{ + if (m_monitor) + return m_monitor->x(); + else + return 0; +} + +int MonitorModel::y() const +{ + if (m_monitor) + return m_monitor->y(); + else + return 0; +} + +unsigned int MonitorModel::width() const +{ + if (m_monitor) + return m_monitor->width(); + else + return 0; +} + +void MonitorModel::setWidth(unsigned int newWidth) +{ + if (m_monitor) { + m_monitor->setWidth(newWidth); + emit widthChanged(); + } +} + +unsigned int MonitorModel::height() const +{ + if (m_monitor) + return m_monitor->height(); + else + return 0; +} + +void MonitorModel::setHeight(unsigned int newHeight) +{ + if (m_monitor) { + m_monitor->setHeight(newHeight); + emit heightChanged(); + } +} diff --git a/src/monitormodel.h b/src/monitormodel.h new file mode 100644 index 0000000..29c7ba1 --- /dev/null +++ b/src/monitormodel.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include +#include +#include + +namespace scratchcpprender +{ + +class MonitorModel + : public QObject + , public libscratchcpp::IMonitorHandler +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(bool visible READ visible NOTIFY visibleChanged) + Q_PROPERTY(Type type READ type NOTIFY typeChanged) + Q_PROPERTY(int x READ x NOTIFY xChanged) + Q_PROPERTY(int y READ y NOTIFY yChanged) + Q_PROPERTY(unsigned int width READ width WRITE setWidth NOTIFY widthChanged) + Q_PROPERTY(unsigned int height READ height WRITE setHeight NOTIFY heightChanged) + + public: + enum class Type + { + Invalid, + Value, + List + }; + + Q_ENUM(Type) + + MonitorModel(QObject *parent = nullptr); + + void init(libscratchcpp::Monitor *monitor) override final; + + virtual void onValueChanged(const libscratchcpp::VirtualMachine *vm) override { } + void onVisibleChanged(bool visible) override final; + + libscratchcpp::Monitor *monitor() const; + + QString name() const; + + bool visible() const; + + virtual Type type() const { return Type::Invalid; } + + int x() const; + + int y() const; + + unsigned int width() const; + void setWidth(unsigned int newWidth); + + unsigned int height() const; + void setHeight(unsigned int newHeight); + + signals: + void visibleChanged(); + void typeChanged(); + void xChanged(); + void yChanged(); + void widthChanged(); + void heightChanged(); + void nameChanged(); + + private: + libscratchcpp::Monitor *m_monitor = nullptr; +}; + +} // namespace scratchcpprender diff --git a/src/projectloader.cpp b/src/projectloader.cpp index 2e56c13..fe700ec 100644 --- a/src/projectloader.cpp +++ b/src/projectloader.cpp @@ -2,11 +2,14 @@ #include #include +#include #include #include #include "projectloader.h" #include "spritemodel.h" +#include "valuemonitormodel.h" +#include "listmonitormodel.h" #include "renderedtarget.h" using namespace scratchcpprender; @@ -126,6 +129,16 @@ const QList &ProjectLoader::cloneList() const return m_clones; } +QQmlListProperty ProjectLoader::monitors() +{ + return QQmlListProperty(this, &m_monitors); +} + +const QList &ProjectLoader::monitorList() const +{ + return m_monitors; +} + void ProjectLoader::start() { if (m_loadThread.isRunning()) @@ -193,8 +206,14 @@ void ProjectLoader::load() m_engine->setCloneLimit(m_cloneLimit); m_engine->setSpriteFencingEnabled(m_spriteFencing); - auto handler = std::bind(&ProjectLoader::redraw, this); - m_engine->setRedrawHandler(std::function(handler)); + auto redrawHandler = std::bind(&ProjectLoader::redraw, this); + m_engine->setRedrawHandler(std::function(redrawHandler)); + + auto addMonitorHandler = std::bind(&ProjectLoader::addMonitor, this, std::placeholders::_1); + m_engine->setAddMonitorHandler(std::function(addMonitorHandler)); + + auto removeMonitorHandler = std::bind(&ProjectLoader::removeMonitor, this, std::placeholders::_1, std::placeholders::_2); + m_engine->setRemoveMonitorHandler(std::function(removeMonitorHandler)); // Load targets const auto &targets = m_engine->targets(); @@ -211,6 +230,12 @@ void ProjectLoader::load() } } + // Load monitors + const auto &monitors = m_engine->monitors(); + + for (auto monitor : monitors) + addMonitor(monitor.get()); + if (m_stopLoading) { m_engineMutex.unlock(); emit fileNameChanged(); @@ -258,6 +283,8 @@ void ProjectLoader::redraw() if (renderedTarget) renderedTarget->beforeRedraw(); } + + m_engine->updateMonitors(); } void ProjectLoader::addClone(SpriteModel *model) @@ -278,6 +305,48 @@ void ProjectLoader::deleteClone(SpriteModel *model) emit clonesChanged(); } +void ProjectLoader::addMonitor(Monitor *monitor) +{ + auto section = monitor->blockSection(); + + if (!section) + return; + + MonitorModel *model = nullptr; + + switch (monitor->mode()) { + case Monitor::Mode::List: + model = new ListMonitorModel(section.get()); + break; + + default: + model = new ValueMonitorModel(section.get()); + break; + } + + if (!model) + return; + + model->moveToThread(qApp->thread()); + model->setParent(this); + monitor->setInterface(model); + m_monitors.push_back(model); + emit monitorAdded(model); + emit monitorsChanged(); +} + +void ProjectLoader::removeMonitor(Monitor *monitor, IMonitorHandler *iface) +{ + MonitorModel *model = dynamic_cast(iface); + + if (!model) + return; + + m_monitors.removeAll(model); + emit monitorRemoved(model); + emit monitorsChanged(); +} + double ProjectLoader::fps() const { return m_fps; diff --git a/src/projectloader.h b/src/projectloader.h index 9c13736..06ac070 100644 --- a/src/projectloader.h +++ b/src/projectloader.h @@ -10,11 +10,13 @@ #include "stagemodel.h" Q_MOC_INCLUDE("spritemodel.h"); +Q_MOC_INCLUDE("monitormodel.h"); namespace scratchcpprender { class SpriteModel; +class MonitorModel; class ProjectLoader : public QObject { @@ -26,6 +28,7 @@ class ProjectLoader : public QObject Q_PROPERTY(StageModel *stage READ stage NOTIFY stageChanged) Q_PROPERTY(QQmlListProperty sprites READ sprites NOTIFY spritesChanged) Q_PROPERTY(QQmlListProperty clones READ clones NOTIFY clonesChanged) + Q_PROPERTY(QQmlListProperty monitors READ monitors NOTIFY monitorsChanged) Q_PROPERTY(double fps READ fps WRITE setFps NOTIFY fpsChanged) Q_PROPERTY(bool turboMode READ turboMode WRITE setTurboMode NOTIFY turboModeChanged) Q_PROPERTY(unsigned int stageWidth READ stageWidth WRITE setStageWidth NOTIFY stageWidthChanged) @@ -55,6 +58,9 @@ class ProjectLoader : public QObject QQmlListProperty clones(); const QList &cloneList() const; + QQmlListProperty monitors(); + const QList &monitorList() const; + Q_INVOKABLE void start(); Q_INVOKABLE void stop(); @@ -88,6 +94,7 @@ class ProjectLoader : public QObject void stageChanged(); void spritesChanged(); void clonesChanged(); + void monitorsChanged(); void fpsChanged(); void turboModeChanged(); void stageWidthChanged(); @@ -98,6 +105,8 @@ class ProjectLoader : public QObject void assetCountChanged(); void cloneCreated(SpriteModel *model); void cloneDeleted(SpriteModel *model); + void monitorAdded(MonitorModel *model); + void monitorRemoved(MonitorModel *model); protected: void timerEvent(QTimerEvent *event) override; @@ -109,6 +118,8 @@ class ProjectLoader : public QObject void redraw(); void addClone(SpriteModel *model); void deleteClone(SpriteModel *model); + void addMonitor(libscratchcpp::Monitor *monitor); + void removeMonitor(libscratchcpp::Monitor *monitor, libscratchcpp::IMonitorHandler *iface); int m_timerId = -1; QString m_fileName; @@ -120,6 +131,7 @@ class ProjectLoader : public QObject StageModel m_stage; QList m_sprites; QList m_clones; + QList m_monitors; double m_fps = 30; bool m_turboMode = false; unsigned int m_stageWidth = 480; diff --git a/src/valuemonitormodel.cpp b/src/valuemonitormodel.cpp new file mode 100644 index 0000000..d716730 --- /dev/null +++ b/src/valuemonitormodel.cpp @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include +#include +#include + +#include "valuemonitormodel.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +static const std::unordered_map + MODE_MAP = { { Monitor::Mode::Default, ValueMonitorModel::Mode::Default }, { Monitor::Mode::Large, ValueMonitorModel::Mode::Large }, { Monitor::Mode::Slider, ValueMonitorModel::Mode::Slider } }; + +ValueMonitorModel::ValueMonitorModel(QObject *parent) : + MonitorModel(parent) +{ +} + +ValueMonitorModel::ValueMonitorModel(IBlockSection *section, QObject *parent) : + MonitorModel(parent) +{ + if (!section) + return; + + // TODO: Get the color from the block section + std::string name = section->name(); + if (name == "Motion") + m_color = QColor::fromString("#4C97FF"); + else if (name == "Looks") + m_color = QColor::fromString("#9966FF"); + else if (name == "Sound") + m_color = QColor::fromString("#CF63CF"); + else if (name == "Sensing") + m_color = QColor::fromString("#5CB1D6"); + else if (name == "Variables") + m_color = QColor::fromString("#FF8C1A"); + else if (name == "Lists") + m_color = QColor::fromString("#FF661A"); +} + +void ValueMonitorModel::onValueChanged(const VirtualMachine *vm) +{ + if (vm->registerCount() == 1) { + m_value = QString::fromStdString(vm->getInput(0, 1)->toString()); + emit valueChanged(); + } +} + +MonitorModel::Type ValueMonitorModel::type() const +{ + return Type::Value; +} + +const QString &ValueMonitorModel::value() const +{ + return m_value; +} + +void ValueMonitorModel::setValue(const QString &newValue) +{ + if (newValue == m_value) + return; + + if (monitor()) + monitor()->changeValue(newValue.toStdString()); + else { + m_value = newValue; + emit valueChanged(); + } +} + +const QColor &ValueMonitorModel::color() const +{ + return m_color; +} + +ValueMonitorModel::Mode ValueMonitorModel::mode() const +{ + if (monitor()) + return MODE_MAP.at(monitor()->mode()); + else + return Mode::Default; +} + +double ValueMonitorModel::sliderMin() const +{ + if (monitor()) + return monitor()->sliderMin(); + else + return 0; +} + +double ValueMonitorModel::sliderMax() const +{ + if (monitor()) + return monitor()->sliderMax(); + else + return 0; +} + +bool ValueMonitorModel::discrete() const +{ + if (monitor()) + return monitor()->discrete(); + else + return true; +} diff --git a/src/valuemonitormodel.h b/src/valuemonitormodel.h new file mode 100644 index 0000000..aa7f2cc --- /dev/null +++ b/src/valuemonitormodel.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +#include "monitormodel.h" + +namespace libscratchcpp +{ + +class IBlockSection; + +} + +namespace scratchcpprender +{ + +class ValueMonitorModel : public MonitorModel +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(QString value READ value WRITE setValue NOTIFY valueChanged) + Q_PROPERTY(QColor color READ color NOTIFY colorChanged) + Q_PROPERTY(Mode mode READ mode NOTIFY modeChanged) + Q_PROPERTY(double sliderMin READ sliderMin NOTIFY sliderMinChanged) + Q_PROPERTY(double sliderMax READ sliderMax NOTIFY sliderMaxChanged) + Q_PROPERTY(bool discrete READ discrete NOTIFY discreteChanged) + + public: + enum class Mode + { + Default, + Large, + Slider + }; + + Q_ENUM(Mode) + + ValueMonitorModel(QObject *parent = nullptr); + ValueMonitorModel(libscratchcpp::IBlockSection *section, QObject *parent = nullptr); + + void onValueChanged(const libscratchcpp::VirtualMachine *vm) override; + + Type type() const override; + + const QString &value() const; + void setValue(const QString &newValue); + + const QColor &color() const; + Mode mode() const; + double sliderMin() const; + double sliderMax() const; + bool discrete() const; + + signals: + void valueChanged(); + void colorChanged(); + void modeChanged(); + void sliderMinChanged(); + void sliderMaxChanged(); + void discreteChanged(); + + private: + QString m_value; + QColor m_color = Qt::green; +}; + +} // namespace scratchcpprender diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 95312da..20be59f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -28,3 +28,4 @@ add_subdirectory(projectscene) add_subdirectory(keyeventhandler) add_subdirectory(mouseeventhandler) add_subdirectory(scenemousearea) +add_subdirectory(monitor_models) diff --git a/test/load_test.sb3 b/test/load_test.sb3 index cd7c8eb..d1553e8 100644 Binary files a/test/load_test.sb3 and b/test/load_test.sb3 differ diff --git a/test/mocks/blocksectionmock.h b/test/mocks/blocksectionmock.h new file mode 100644 index 0000000..1963f11 --- /dev/null +++ b/test/mocks/blocksectionmock.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +using namespace libscratchcpp; + +namespace scratchcpprender +{ + +class BlockSectionMock : public IBlockSection +{ + public: + MOCK_METHOD(std::string, name, (), (const, override)); + MOCK_METHOD(bool, categoryVisible, (), (const, override)); + MOCK_METHOD(void, registerBlocks, (IEngine * engine), (override)); +}; + +} // namespace scratchcpprender diff --git a/test/monitor_models/CMakeLists.txt b/test/monitor_models/CMakeLists.txt new file mode 100644 index 0000000..ebd6d03 --- /dev/null +++ b/test/monitor_models/CMakeLists.txt @@ -0,0 +1,71 @@ +# monitormodel +add_executable( + monitormodel_test + monitormodel_test.cpp +) + +target_link_libraries( + monitormodel_test + GTest::gtest_main + scratchcpp-render + ${QT_LIBS} + Qt6::Test +) + +add_test(monitormodel_test) +gtest_discover_tests(monitormodel_test) + +# valuemonitormodel +add_executable( + valuemonitormodel_test + valuemonitormodel_test.cpp +) + +target_link_libraries( + valuemonitormodel_test + GTest::gtest_main + GTest::gmock_main + scratchcpp-render + scratchcpprender_mocks + ${QT_LIBS} + Qt6::Test +) + +add_test(valuemonitormodel_test) +gtest_discover_tests(valuemonitormodel_test) + +# listmonitorlistmodel +add_executable( + listmonitorlistmodel_test + listmonitorlistmodel_test.cpp +) + +target_link_libraries( + listmonitorlistmodel_test + GTest::gtest_main + scratchcpp-render + ${QT_LIBS} + Qt6::Test +) + +add_test(listmonitorlistmodel_test) +gtest_discover_tests(listmonitorlistmodel_test) + +# listmonitormodel +add_executable( + listmonitormodel_test + listmonitormodel_test.cpp +) + +target_link_libraries( + listmonitormodel_test + GTest::gtest_main + GTest::gmock_main + scratchcpp-render + scratchcpprender_mocks + ${QT_LIBS} + Qt6::Test +) + +add_test(listmonitormodel_test) +gtest_discover_tests(listmonitormodel_test) diff --git a/test/monitor_models/listmonitorlistmodel_test.cpp b/test/monitor_models/listmonitorlistmodel_test.cpp new file mode 100644 index 0000000..91584b3 --- /dev/null +++ b/test/monitor_models/listmonitorlistmodel_test.cpp @@ -0,0 +1,218 @@ +#include +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +TEST(ListMonitorListModelTest, Constructors) +{ + ListMonitorListModel model1; + ListMonitorListModel model2(&model1); + ASSERT_EQ(model2.parent(), &model1); +} + +TEST(ListMonitorListModelTest, LoadData) +{ + ListMonitorListModel model; + QSignalSpy dataChangedSpy(&model, &ListMonitorListModel::dataChanged); + QSignalSpy aboutToResetSpy(&model, &ListMonitorListModel::modelAboutToBeReset); + QSignalSpy resetSpy(&model, &ListMonitorListModel::modelReset); + QSignalSpy aboutToInsertSpy(&model, &ListMonitorListModel::rowsAboutToBeInserted); + QSignalSpy insertSpy(&model, &ListMonitorListModel::rowsInserted); + QSignalSpy aboutToRemoveSpy(&model, &ListMonitorListModel::rowsAboutToBeRemoved); + QSignalSpy removeSpy(&model, &ListMonitorListModel::rowsRemoved); + + List list1("", ""); + list1.push_back(1); + list1.push_back(2); + model.setList(&list1); + ASSERT_TRUE(dataChangedSpy.empty()); + ASSERT_EQ(aboutToResetSpy.count(), 1); + ASSERT_EQ(resetSpy.count(), 1); + ASSERT_TRUE(aboutToInsertSpy.empty()); + ASSERT_TRUE(insertSpy.empty()); + ASSERT_TRUE(aboutToRemoveSpy.empty()); + ASSERT_TRUE(removeSpy.empty()); + + model.setList(&list1); + ASSERT_EQ(dataChangedSpy.count(), 2); + ASSERT_EQ(aboutToResetSpy.count(), 1); + ASSERT_EQ(resetSpy.count(), 1); + ASSERT_TRUE(aboutToInsertSpy.empty()); + ASSERT_TRUE(insertSpy.empty()); + ASSERT_TRUE(aboutToRemoveSpy.empty()); + ASSERT_TRUE(removeSpy.empty()); + + auto args = dataChangedSpy.at(0); + QModelIndex arg1 = args.at(0).value(); + QModelIndex arg2 = args.at(1).value(); + ASSERT_EQ(arg1.row(), 0); + ASSERT_EQ(arg1.column(), 0); + ASSERT_EQ(arg2.row(), 0); + ASSERT_EQ(arg2.column(), 0); + ASSERT_TRUE(args.at(2).toList().isEmpty()); + + args = dataChangedSpy.at(1); + arg1 = args.at(0).value(); + arg2 = args.at(1).value(); + ASSERT_EQ(arg1.row(), 1); + ASSERT_EQ(arg1.column(), 0); + ASSERT_EQ(arg2.row(), 1); + ASSERT_EQ(arg2.column(), 0); + ASSERT_TRUE(args.at(2).toList().isEmpty()); + + List list2("", ""); + model.setList(&list2); + ASSERT_EQ(dataChangedSpy.count(), 2); + ASSERT_EQ(aboutToResetSpy.count(), 2); + ASSERT_EQ(resetSpy.count(), 2); + ASSERT_TRUE(aboutToInsertSpy.empty()); + ASSERT_TRUE(insertSpy.empty()); + ASSERT_TRUE(aboutToRemoveSpy.empty()); + ASSERT_TRUE(removeSpy.empty()); +} + +TEST(ListMonitorListModelTest, AddRows) +{ + ListMonitorListModel model; + QSignalSpy dataChangedSpy(&model, &ListMonitorListModel::dataChanged); + QSignalSpy aboutToResetSpy(&model, &ListMonitorListModel::modelAboutToBeReset); + QSignalSpy resetSpy(&model, &ListMonitorListModel::modelReset); + QSignalSpy aboutToInsertSpy(&model, &ListMonitorListModel::rowsAboutToBeInserted); + QSignalSpy insertSpy(&model, &ListMonitorListModel::rowsInserted); + QSignalSpy aboutToRemoveSpy(&model, &ListMonitorListModel::rowsAboutToBeRemoved); + QSignalSpy removeSpy(&model, &ListMonitorListModel::rowsRemoved); + + List list1("", ""); + list1.push_back(1); + list1.push_back(2); + model.setList(&list1); + ASSERT_TRUE(dataChangedSpy.empty()); + ASSERT_EQ(aboutToResetSpy.count(), 1); + ASSERT_EQ(resetSpy.count(), 1); + ASSERT_TRUE(aboutToInsertSpy.empty()); + ASSERT_TRUE(insertSpy.empty()); + ASSERT_TRUE(aboutToRemoveSpy.empty()); + ASSERT_TRUE(removeSpy.empty()); + + list1.push_back(9); + list1.push_back(8); + list1.push_back(7); + model.setList(&list1); + ASSERT_EQ(dataChangedSpy.count(), 2); + ASSERT_EQ(aboutToResetSpy.count(), 1); + ASSERT_EQ(resetSpy.count(), 1); + ASSERT_EQ(aboutToInsertSpy.count(), 1); + ASSERT_EQ(insertSpy.count(), 1); + ASSERT_TRUE(aboutToRemoveSpy.empty()); + ASSERT_TRUE(removeSpy.empty()); + + auto args = dataChangedSpy.at(0); + QModelIndex arg1 = args.at(0).value(); + QModelIndex arg2 = args.at(1).value(); + ASSERT_EQ(arg1.row(), 0); + ASSERT_EQ(arg1.column(), 0); + ASSERT_EQ(arg2.row(), 0); + ASSERT_EQ(arg2.column(), 0); + ASSERT_TRUE(args.at(2).toList().isEmpty()); + + args = dataChangedSpy.at(1); + arg1 = args.at(0).value(); + arg2 = args.at(1).value(); + ASSERT_EQ(arg1.row(), 1); + ASSERT_EQ(arg1.column(), 0); + ASSERT_EQ(arg2.row(), 1); + ASSERT_EQ(arg2.column(), 0); + ASSERT_TRUE(args.at(2).toList().isEmpty()); + + args = aboutToInsertSpy.at(0); + arg1 = args.at(0).value(); + ASSERT_EQ(arg1, QModelIndex()); + ASSERT_EQ(args.at(1).toInt(), 2); + ASSERT_EQ(args.at(2).toInt(), 4); +} + +TEST(ListMonitorListModelTest, RemoveRows) +{ + ListMonitorListModel model; + QSignalSpy dataChangedSpy(&model, &ListMonitorListModel::dataChanged); + QSignalSpy aboutToResetSpy(&model, &ListMonitorListModel::modelAboutToBeReset); + QSignalSpy resetSpy(&model, &ListMonitorListModel::modelReset); + QSignalSpy aboutToInsertSpy(&model, &ListMonitorListModel::rowsAboutToBeInserted); + QSignalSpy insertSpy(&model, &ListMonitorListModel::rowsInserted); + QSignalSpy aboutToRemoveSpy(&model, &ListMonitorListModel::rowsAboutToBeRemoved); + QSignalSpy removeSpy(&model, &ListMonitorListModel::rowsRemoved); + + List list1("", ""); + list1.push_back(1); + list1.push_back(2); + list1.push_back(3); + model.setList(&list1); + ASSERT_TRUE(dataChangedSpy.empty()); + ASSERT_EQ(aboutToResetSpy.count(), 1); + ASSERT_EQ(resetSpy.count(), 1); + ASSERT_TRUE(aboutToInsertSpy.empty()); + ASSERT_TRUE(insertSpy.empty()); + ASSERT_TRUE(aboutToRemoveSpy.empty()); + ASSERT_TRUE(removeSpy.empty()); + + list1.removeAt(2); + list1.removeAt(0); + model.setList(&list1); + ASSERT_EQ(dataChangedSpy.count(), 1); + ASSERT_EQ(aboutToResetSpy.count(), 1); + ASSERT_EQ(resetSpy.count(), 1); + ASSERT_TRUE(aboutToInsertSpy.empty()); + ASSERT_TRUE(insertSpy.empty()); + ASSERT_EQ(aboutToRemoveSpy.count(), 1); + ASSERT_EQ(removeSpy.count(), 1); + + auto args = dataChangedSpy.at(0); + QModelIndex arg1 = args.at(0).value(); + QModelIndex arg2 = args.at(1).value(); + ASSERT_EQ(arg1.row(), 0); + ASSERT_EQ(arg1.column(), 0); + ASSERT_EQ(arg2.row(), 0); + ASSERT_EQ(arg2.column(), 0); + ASSERT_TRUE(args.at(2).toList().isEmpty()); + + args = aboutToRemoveSpy.at(0); + arg1 = args.at(0).value(); + ASSERT_EQ(arg1, QModelIndex()); + ASSERT_EQ(args.at(1).toInt(), 1); + ASSERT_EQ(args.at(2).toInt(), 2); +} + +TEST(ListMonitorListModelTest, RowCount) +{ + ListMonitorListModel model; + List list("", ""); + list.push_back(1); + list.push_back(2); + list.push_back(3); + model.setList(&list); + ASSERT_EQ(model.rowCount(QModelIndex()), list.size()); +} + +TEST(ListMonitorListModelTest, Data) +{ + ListMonitorListModel model; + List list("", ""); + list.push_back(1); + list.push_back(2); + list.push_back(3); + model.setList(&list); + ASSERT_EQ(model.data(model.index(0), 0).toString(), "1"); + ASSERT_EQ(model.data(model.index(1), 0).toString(), "2"); + ASSERT_EQ(model.data(model.index(2), 0).toString(), "3"); +} + +TEST(ListMonitorListModelTest, RoleNames) +{ + ListMonitorListModel model; + QHash names({ { 0, "value" } }); + ASSERT_EQ(model.roleNames(), names); +} diff --git a/test/monitor_models/listmonitormodel_test.cpp b/test/monitor_models/listmonitormodel_test.cpp new file mode 100644 index 0000000..84bc740 --- /dev/null +++ b/test/monitor_models/listmonitormodel_test.cpp @@ -0,0 +1,130 @@ +#include +#include +#include +#include +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +using ::testing::Return; + +TEST(ListMonitorModelTest, Constructors) +{ + { + ListMonitorModel model1; + ListMonitorModel model2(&model1); + ASSERT_EQ(model2.parent(), &model1); + } + + { + ListMonitorModel model1; + ListMonitorModel model2(nullptr, &model1); + ASSERT_EQ(model2.parent(), &model1); + } +} + +TEST(ListMonitorModelTest, OnValueChanged) +{ + ListMonitorModel model; + ListMonitorListModel *listModel = model.listModel(); + VirtualMachine vm; + + List list1("", ""); + list1.push_back(1); + list1.push_back(2); + + List list2("", ""); + list2.push_back(1); + list2.push_back(2); + list2.push_back(3); + list2.push_back(4); + + List list3("", ""); + list3.push_back(1); + list3.push_back(2); + list3.push_back(3); + + List *lists[] = { &list1, &list2, &list3 }; + vm.setLists(lists); + + vm.addReturnValue(1); + model.onValueChanged(&vm); + ASSERT_EQ(listModel->rowCount(QModelIndex()), 4); + + vm.reset(); + vm.addReturnValue(2); + model.onValueChanged(&vm); + ASSERT_EQ(listModel->rowCount(QModelIndex()), 3); + + vm.reset(); + vm.addReturnValue(0); + model.onValueChanged(&vm); + ASSERT_EQ(listModel->rowCount(QModelIndex()), 2); +} + +TEST(ListMonitorModelTest, Type) +{ + ListMonitorModel model; + ASSERT_EQ(model.type(), MonitorModel::Type::List); +} + +TEST(ListMonitorModelTest, Color) +{ + { + ListMonitorModel model; + ASSERT_EQ(model.color(), Qt::green); + } + + { + ListMonitorModel model(nullptr, nullptr); + ASSERT_EQ(model.color(), Qt::green); + } + + BlockSectionMock section; + + { + // Invalid + EXPECT_CALL(section, name()).WillOnce(Return("")); + ListMonitorModel model(§ion); + ASSERT_EQ(model.color(), Qt::green); + } + + { + // Motion + EXPECT_CALL(section, name()).WillOnce(Return("Motion")); + ListMonitorModel model(§ion); + ASSERT_EQ(model.color(), QColor::fromString("#4C97FF")); + } + + { + // Looks + EXPECT_CALL(section, name()).WillOnce(Return("Looks")); + ListMonitorModel model(§ion); + ASSERT_EQ(model.color(), QColor::fromString("#9966FF")); + } + + { + // Sound + EXPECT_CALL(section, name()).WillOnce(Return("Sound")); + ListMonitorModel model(§ion); + ASSERT_EQ(model.color(), QColor::fromString("#CF63CF")); + } + + { + // Variables + EXPECT_CALL(section, name()).WillOnce(Return("Variables")); + ListMonitorModel model(§ion); + ASSERT_EQ(model.color(), QColor::fromString("#FF8C1A")); + } + + { + // Lists + EXPECT_CALL(section, name()).WillOnce(Return("Lists")); + ListMonitorModel model(§ion); + ASSERT_EQ(model.color(), QColor::fromString("#FF661A")); + } +} diff --git a/test/monitor_models/monitormodel_test.cpp b/test/monitor_models/monitormodel_test.cpp new file mode 100644 index 0000000..f02373a --- /dev/null +++ b/test/monitor_models/monitormodel_test.cpp @@ -0,0 +1,136 @@ +#include +#include +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +TEST(MonitorModelTest, Constructors) +{ + MonitorModel model1; + MonitorModel model2(&model1); + ASSERT_EQ(model2.parent(), &model1); +} + +TEST(MonitorModelTest, Init) +{ + MonitorModel model; + ASSERT_EQ(model.monitor(), nullptr); + + Monitor monitor("", ""); + model.init(&monitor); + ASSERT_EQ(model.monitor(), &monitor); +} + +TEST(MonitorModelTest, OnVisibleChanged) +{ + MonitorModel model; + QSignalSpy spy(&model, &MonitorModel::visibleChanged); + + model.onVisibleChanged(false); + ASSERT_EQ(spy.count(), 1); + + model.onVisibleChanged(true); + ASSERT_EQ(spy.count(), 2); +} + +TEST(MonitorModelTest, Name) +{ + MonitorModel model; + Monitor monitor("", ""); + // TODO: Use monitor.setName() + const_cast(&monitor.name())->assign("days since 2000"); + model.init(&monitor); + ASSERT_EQ(model.name().toStdString(), monitor.name()); + + Sprite sprite; + sprite.setName("Sprite2"); + monitor.setSprite(&sprite); + // TODO: Use monitor.setName() + const_cast(&monitor.name())->assign("x position"); + ASSERT_EQ(model.name().toStdString(), sprite.name() + ": " + monitor.name()); +} + +TEST(MonitorModelTest, Visible) +{ + MonitorModel model; + Monitor monitor("", ""); + monitor.setVisible(true); + model.init(&monitor); + ASSERT_TRUE(model.visible()); + + monitor.setVisible(false); + ASSERT_FALSE(model.visible()); + + monitor.setVisible(false); + ASSERT_FALSE(model.visible()); +} + +TEST(MonitorModelTest, Type) +{ + MonitorModel model; + ASSERT_EQ(model.type(), MonitorModel::Type::Invalid); +} + +TEST(MonitorModelTest, X) +{ + MonitorModel model; + Monitor monitor("", ""); + monitor.setX(65); + model.init(&monitor); + ASSERT_EQ(model.x(), 65); + + monitor.setX(-2); + ASSERT_EQ(model.x(), -2); +} + +TEST(MonitorModelTest, Y) +{ + MonitorModel model; + Monitor monitor("", ""); + monitor.setY(15); + model.init(&monitor); + ASSERT_EQ(model.y(), 15); + + monitor.setY(-8); + ASSERT_EQ(model.y(), -8); +} + +TEST(MonitorModelTest, Width) +{ + MonitorModel model; + Monitor monitor("", ""); + QSignalSpy spy(&model, &MonitorModel::widthChanged); + monitor.setWidth(20); + model.init(&monitor); + ASSERT_EQ(model.width(), 20); + + monitor.setWidth(150); + ASSERT_EQ(model.width(), 150); + + model.setWidth(87); + ASSERT_EQ(model.width(), 87); + ASSERT_EQ(monitor.width(), 87); + ASSERT_EQ(spy.count(), 1); +} + +TEST(MonitorModelTest, Height) +{ + MonitorModel model; + Monitor monitor("", ""); + QSignalSpy spy(&model, &MonitorModel::heightChanged); + monitor.setHeight(20); + model.init(&monitor); + ASSERT_EQ(model.height(), 20); + + monitor.setHeight(150); + ASSERT_EQ(model.height(), 150); + + model.setHeight(87); + ASSERT_EQ(model.height(), 87); + ASSERT_EQ(monitor.height(), 87); + ASSERT_EQ(spy.count(), 1); +} diff --git a/test/monitor_models/valuemonitormodel_test.cpp b/test/monitor_models/valuemonitormodel_test.cpp new file mode 100644 index 0000000..a1000e7 --- /dev/null +++ b/test/monitor_models/valuemonitormodel_test.cpp @@ -0,0 +1,193 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +using ::testing::Return; + +TEST(ValueMonitorModelTest, Constructors) +{ + { + ValueMonitorModel model1; + ValueMonitorModel model2(&model1); + ASSERT_EQ(model2.parent(), &model1); + } + + { + ValueMonitorModel model1; + ValueMonitorModel model2(nullptr, &model1); + ASSERT_EQ(model2.parent(), &model1); + } +} + +TEST(ValueMonitorModelTest, OnValueChanged) +{ + ValueMonitorModel model; + QSignalSpy spy(&model, &ValueMonitorModel::valueChanged); + VirtualMachine vm; + + vm.addReturnValue(5.4); + model.onValueChanged(&vm); + ASSERT_EQ(model.value(), "5.4"); + ASSERT_EQ(spy.count(), 1); + + vm.reset(); + vm.addReturnValue("test"); + model.onValueChanged(&vm); + ASSERT_EQ(model.value(), "test"); + ASSERT_EQ(spy.count(), 2); +} + +TEST(ValueMonitorModelTest, Value) +{ + ValueMonitorModel model; + QSignalSpy spy(&model, &ValueMonitorModel::valueChanged); + ASSERT_TRUE(model.value().isEmpty()); + + model.setValue("hello"); + ASSERT_EQ(model.value(), "hello"); + ASSERT_EQ(spy.count(), 1); + + model.setValue("world"); + ASSERT_EQ(model.value(), "world"); + ASSERT_EQ(spy.count(), 2); + + Monitor monitor("", ""); + monitor.setValueChangeFunction([](Block *block, const Value &newValue) { + auto comment = std::make_shared(""); + comment->setText("it works"); + block->setComment(comment); + }); + model.init(&monitor); + + model.setValue("test"); + ASSERT_EQ(model.value(), "world"); + ASSERT_TRUE(monitor.block()->comment()); + ASSERT_EQ(monitor.block()->comment()->text(), "it works"); + ASSERT_EQ(spy.count(), 2); +} + +TEST(ValueMonitorModelTest, Type) +{ + ValueMonitorModel model; + ASSERT_EQ(model.type(), MonitorModel::Type::Value); +} + +TEST(ValueMonitorModelTest, Color) +{ + { + ValueMonitorModel model; + ASSERT_EQ(model.color(), Qt::green); + } + + { + ValueMonitorModel model(nullptr, nullptr); + ASSERT_EQ(model.color(), Qt::green); + } + + BlockSectionMock section; + + { + // Invalid + EXPECT_CALL(section, name()).WillOnce(Return("")); + ValueMonitorModel model(§ion); + ASSERT_EQ(model.color(), Qt::green); + } + + { + // Motion + EXPECT_CALL(section, name()).WillOnce(Return("Motion")); + ValueMonitorModel model(§ion); + ASSERT_EQ(model.color(), QColor::fromString("#4C97FF")); + } + + { + // Looks + EXPECT_CALL(section, name()).WillOnce(Return("Looks")); + ValueMonitorModel model(§ion); + ASSERT_EQ(model.color(), QColor::fromString("#9966FF")); + } + + { + // Sound + EXPECT_CALL(section, name()).WillOnce(Return("Sound")); + ValueMonitorModel model(§ion); + ASSERT_EQ(model.color(), QColor::fromString("#CF63CF")); + } + + { + // Variables + EXPECT_CALL(section, name()).WillOnce(Return("Variables")); + ValueMonitorModel model(§ion); + ASSERT_EQ(model.color(), QColor::fromString("#FF8C1A")); + } + + { + // Lists + EXPECT_CALL(section, name()).WillOnce(Return("Lists")); + ValueMonitorModel model(§ion); + ASSERT_EQ(model.color(), QColor::fromString("#FF661A")); + } +} + +TEST(ValueMonitorModelTest, Mode) +{ + ValueMonitorModel model; + Monitor monitor("", ""); + monitor.setMode(Monitor::Mode::Default); + model.init(&monitor); + ASSERT_EQ(model.mode(), ValueMonitorModel::Mode::Default); + + monitor.setMode(Monitor::Mode::Large); + ASSERT_EQ(model.mode(), ValueMonitorModel::Mode::Large); + + monitor.setMode(Monitor::Mode::Slider); + ASSERT_EQ(model.mode(), ValueMonitorModel::Mode::Slider); +} + +TEST(ValueMonitorModelTest, SliderMin) +{ + ValueMonitorModel model; + Monitor monitor("", ""); + monitor.setSliderMin(-0.5); + model.init(&monitor); + ASSERT_EQ(model.sliderMin(), -0.5); + + monitor.setSliderMin(2.65); + ASSERT_EQ(model.sliderMin(), 2.65); +} + +TEST(ValueMonitorModelTest, SliderMax) +{ + ValueMonitorModel model; + Monitor monitor("", ""); + monitor.setSliderMax(-0.5); + model.init(&monitor); + ASSERT_EQ(model.sliderMax(), -0.5); + + monitor.setSliderMax(2.65); + ASSERT_EQ(model.sliderMax(), 2.65); +} + +TEST(ValueMonitorModelTest, Discrete) +{ + ValueMonitorModel model; + Monitor monitor("", ""); + monitor.setDiscrete(true); + model.init(&monitor); + ASSERT_TRUE(model.discrete()); + + monitor.setDiscrete(false); + ASSERT_FALSE(model.discrete()); + + monitor.setDiscrete(false); + ASSERT_FALSE(model.discrete()); +} diff --git a/test/projectloader/projectloader_test.cpp b/test/projectloader/projectloader_test.cpp index 842e17a..84adb5c 100644 --- a/test/projectloader/projectloader_test.cpp +++ b/test/projectloader/projectloader_test.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include @@ -24,6 +26,8 @@ class ProjectLoaderTest : public testing::Test QSignalSpy engineSpy(loader, &ProjectLoader::engineChanged); QSignalSpy stageSpy(loader, &ProjectLoader::stageChanged); QSignalSpy spritesSpy(loader, &ProjectLoader::spritesChanged); + QSignalSpy monitorsSpy(loader, &ProjectLoader::monitorsChanged); + QSignalSpy monitorAddedSpy(loader, &ProjectLoader::monitorAdded); loader->setFileName(fileName); @@ -33,6 +37,8 @@ class ProjectLoaderTest : public testing::Test ASSERT_TRUE(engineSpy.empty()); ASSERT_TRUE(stageSpy.empty()); ASSERT_TRUE(spritesSpy.empty()); + ASSERT_TRUE(monitorsSpy.empty()); + ASSERT_TRUE(monitorAddedSpy.empty()); ASSERT_EQ(loader->fileName(), fileName); ASSERT_FALSE(loader->loadStatus()); @@ -46,6 +52,8 @@ class ProjectLoaderTest : public testing::Test ASSERT_EQ(engineSpy.count(), 1); ASSERT_EQ(stageSpy.count(), 1); ASSERT_EQ(spritesSpy.count(), 1); + ASSERT_EQ(monitorsSpy.count(), loader->monitorList().size()); + ASSERT_EQ(monitorAddedSpy.count(), loader->monitorList().size()); } }; @@ -72,6 +80,17 @@ TEST_F(ProjectLoaderTest, Load) ASSERT_EQ(sprites.size(), 2); ASSERT_EQ(sprites[0]->sprite(), engine->targetAt(1)); ASSERT_EQ(sprites[1]->sprite(), engine->targetAt(2)); + + const auto &monitors = loader.monitorList(); + ASSERT_EQ(monitors.size(), 10); + + ListMonitorModel *listMonitorModel = dynamic_cast(monitors[0]); + ASSERT_EQ(listMonitorModel->monitor(), engine->monitors().at(0).get()); + ASSERT_EQ(listMonitorModel->color(), QColor::fromString("#FF661A")); + + ValueMonitorModel *valueMonitorModel = dynamic_cast(monitors[3]); + ASSERT_EQ(valueMonitorModel->monitor(), engine->monitors().at(3).get()); + ASSERT_EQ(valueMonitorModel->color(), QColor::fromString("#FF8C1A")); } TEST_F(ProjectLoaderTest, Clones)