From 57de3c0dee21621712d9d746e5b3f94749e6ed20 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:05:20 +0100 Subject: [PATCH 01/16] Add MonitorModel class --- src/CMakeLists.txt | 2 + src/monitormodel.cpp | 95 +++++++++++++++ src/monitormodel.h | 74 ++++++++++++ test/CMakeLists.txt | 1 + test/monitor_models/CMakeLists.txt | 15 +++ test/monitor_models/monitormodel_test.cpp | 136 ++++++++++++++++++++++ 6 files changed, 323 insertions(+) create mode 100644 src/monitormodel.cpp create mode 100644 src/monitormodel.h create mode 100644 test/monitor_models/CMakeLists.txt create mode 100644 test/monitor_models/monitormodel_test.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 01f19d5..e98839c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,8 @@ qt_add_qml_module(scratchcpp-render stagemodel.h spritemodel.cpp spritemodel.h + monitormodel.cpp + monitormodel.h irenderedtarget.h renderedtarget.cpp renderedtarget.h 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/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/monitor_models/CMakeLists.txt b/test/monitor_models/CMakeLists.txt new file mode 100644 index 0000000..b692174 --- /dev/null +++ b/test/monitor_models/CMakeLists.txt @@ -0,0 +1,15 @@ +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) 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); +} From 125c9c5bc4980663a064680fb4e15831fea05ffb Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:06:12 +0100 Subject: [PATCH 02/16] Add ValueMonitorModel class --- src/CMakeLists.txt | 2 + src/valuemonitormodel.cpp | 108 ++++++++++ src/valuemonitormodel.h | 69 +++++++ test/mocks/blocksectionmock.h | 19 ++ test/monitor_models/CMakeLists.txt | 18 ++ .../monitor_models/valuemonitormodel_test.cpp | 193 ++++++++++++++++++ 6 files changed, 409 insertions(+) create mode 100644 src/valuemonitormodel.cpp create mode 100644 src/valuemonitormodel.h create mode 100644 test/mocks/blocksectionmock.h create mode 100644 test/monitor_models/valuemonitormodel_test.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e98839c..86dd5ef 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,8 @@ qt_add_qml_module(scratchcpp-render spritemodel.h monitormodel.cpp monitormodel.h + valuemonitormodel.cpp + valuemonitormodel.h irenderedtarget.h renderedtarget.cpp renderedtarget.h 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/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 index b692174..88fde95 100644 --- a/test/monitor_models/CMakeLists.txt +++ b/test/monitor_models/CMakeLists.txt @@ -13,3 +13,21 @@ target_link_libraries( add_test(monitormodel_test) gtest_discover_tests(monitormodel_test) + +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) 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()); +} From 6e48ef808e586679864fef1abdb29330f12acaed Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:58:32 +0100 Subject: [PATCH 03/16] ProjectPlayer: Move sceneMouseArea to content rect --- src/ProjectPlayer.qml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ProjectPlayer.qml b/src/ProjectPlayer.qml index 9828345..ec699bc 100644 --- a/src/ProjectPlayer.qml +++ b/src/ProjectPlayer.qml @@ -120,6 +120,16 @@ 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() + } + Loader { anchors.fill: parent active: showLoadingProgress && loading @@ -159,15 +169,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() - } } } From e40a0e864e8a2d8d8a49b0c62d1faf783709aadd Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Tue, 16 Jan 2024 16:33:05 +0100 Subject: [PATCH 04/16] Load monitors --- src/projectloader.cpp | 72 +++++++++++++++++++++- src/projectloader.h | 12 ++++ test/load_test.sb3 | Bin 48765 -> 48599 bytes test/projectloader/projectloader_test.cpp | 18 ++++++ 4 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/projectloader.cpp b/src/projectloader.cpp index 2e56c13..f46d828 100644 --- a/src/projectloader.cpp +++ b/src/projectloader.cpp @@ -2,11 +2,13 @@ #include #include +#include #include #include #include "projectloader.h" #include "spritemodel.h" +#include "valuemonitormodel.h" #include "renderedtarget.h" using namespace scratchcpprender; @@ -126,6 +128,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 +205,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 +229,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 +282,8 @@ void ProjectLoader::redraw() if (renderedTarget) renderedTarget->beforeRedraw(); } + + m_engine->updateMonitors(); } void ProjectLoader::addClone(SpriteModel *model) @@ -278,6 +304,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: + // TODO: Add support for list monitors + 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/test/load_test.sb3 b/test/load_test.sb3 index cd7c8eb45057ed72ff0f996f5828bad212de63f3..d1553e8e271ee74c1c26c944f535ca58e3104ac1 100644 GIT binary patch delta 3019 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 QuAx7zo>Qm&`d{6@0W@!iTmS$7 delta 3190 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 #include #include +#include #include #include @@ -24,6 +25,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 +36,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 +51,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 +79,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(), 7); + + ValueMonitorModel *monitorModel = dynamic_cast(monitors[0]); + ASSERT_EQ(monitorModel->monitor(), engine->monitors().at(3).get()); + ASSERT_EQ(monitorModel->color(), QColor::fromString("#FF8C1A")); + + monitorModel = dynamic_cast(monitors[1]); + ASSERT_EQ(monitorModel->monitor(), engine->monitors().at(4).get()); + ASSERT_EQ(monitorModel->color(), QColor::fromString("#FF8C1A")); } TEST_F(ProjectLoaderTest, Clones) From d855a78ef58a36c58f1b298528cce08f9229833a Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Tue, 16 Jan 2024 16:33:42 +0100 Subject: [PATCH 05/16] Add ValueMonitor component --- src/CMakeLists.txt | 3 + src/internal/MonitorSlider.qml | 75 +++++++++++++++++++++++ src/internal/ValueMonitor.qml | 107 +++++++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 src/internal/MonitorSlider.qml create mode 100644 src/internal/ValueMonitor.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 86dd5ef..c9f7dd9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,9 @@ 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 SOURCES global.h projectloader.cpp diff --git a/src/internal/MonitorSlider.qml b/src/internal/MonitorSlider.qml new file mode 100644 index 0000000..22893db --- /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.28) : 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.28) : 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 + 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..f90523d --- /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.5 + 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.color + 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: -5 + bottomPadding: 0 + horizontalAlignment: Qt.AlignHCenter + width: Math.max(31, implicitWidth) + height: Math.max(14.46, implicitHeight) + } + } + } +} From 0b9c94f89958b1eaf689e2d1b71767714f095ddd Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Tue, 16 Jan 2024 16:34:29 +0100 Subject: [PATCH 06/16] ProjectPlayer: Implement monitors --- src/ProjectPlayer.qml | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/ProjectPlayer.qml b/src/ProjectPlayer.qml index ec699bc..8a78297 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() { @@ -130,6 +147,35 @@ ProjectScene { 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: renderedMonitor + + Loader { + readonly property MonitorModel model: monitorModel + sourceComponent: monitorModel ? (monitorModel.type === MonitorModel.Value ? renderedValueMonitor : null) : 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 From 79421101131ab8c6d05d4b3ddf48c669510f8da3 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Tue, 16 Jan 2024 23:24:18 +0100 Subject: [PATCH 07/16] ValueMonitor: Adjust margins and padding --- src/internal/ValueMonitor.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/internal/ValueMonitor.qml b/src/internal/ValueMonitor.qml index f90523d..466e054 100644 --- a/src/internal/ValueMonitor.qml +++ b/src/internal/ValueMonitor.qml @@ -19,7 +19,7 @@ Rectangle { QtObject { id: priv readonly property int horizontalMargins: 9 - readonly property double verticalMargins: 2.5 + readonly property double verticalMargins: 2 readonly property color bgColor: Qt.rgba(0.9, 0.94, 1, 1) } @@ -96,7 +96,7 @@ Rectangle { font.family: "Helvetica" leftPadding: 2 rightPadding: 2 - topPadding: -5 + topPadding: -3 bottomPadding: 0 horizontalAlignment: Qt.AlignHCenter width: Math.max(31, implicitWidth) From 69909fff33bcfaefe1671cc8228c128e99ef89b6 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 17 Jan 2024 00:06:10 +0100 Subject: [PATCH 08/16] ValueMonitor: Fix value color when model is null --- src/internal/ValueMonitor.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/ValueMonitor.qml b/src/internal/ValueMonitor.qml index 466e054..74dd34f 100644 --- a/src/internal/ValueMonitor.qml +++ b/src/internal/ValueMonitor.qml @@ -61,7 +61,7 @@ Rectangle { Layout.minimumWidth: 40 Rectangle { - color: model.color + color: model ? model.color : "green" radius: 5 anchors.fill: parent anchors.margins: 0 From aafefb3e8460a04a3df868db1016c081c8e8436a Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 17 Jan 2024 22:43:33 +0100 Subject: [PATCH 09/16] MonitorSlider: Set float step size to 0.1 --- src/internal/MonitorSlider.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/MonitorSlider.qml b/src/internal/MonitorSlider.qml index 22893db..37b84b3 100644 --- a/src/internal/MonitorSlider.qml +++ b/src/internal/MonitorSlider.qml @@ -25,7 +25,7 @@ Slider { 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 + stepSize: discrete ? 1 : 0.1 snapMode: Slider.SnapAlways implicitWidth: 119 implicitHeight: 16 From 79066df6b755669805c66603c042222b0bce63fc Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 17 Jan 2024 23:10:39 +0100 Subject: [PATCH 10/16] MonitorSlider: Improve background colors --- src/internal/MonitorSlider.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/internal/MonitorSlider.qml b/src/internal/MonitorSlider.qml index 37b84b3..d9a02f8 100644 --- a/src/internal/MonitorSlider.qml +++ b/src/internal/MonitorSlider.qml @@ -13,10 +13,10 @@ Slider { 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.28) : bgColor) + 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.28) : bgBorderColor) + 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) From bed46ced5d7fcb32fa2cb01c7dc0a80df69abc97 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 18 Jan 2024 00:18:34 +0100 Subject: [PATCH 11/16] Add ListMonitorListModel class --- src/CMakeLists.txt | 2 + src/listmonitorlistmodel.cpp | 66 ++++++ src/listmonitorlistmodel.h | 33 +++ test/monitor_models/CMakeLists.txt | 19 ++ .../listmonitorlistmodel_test.cpp | 218 ++++++++++++++++++ 5 files changed, 338 insertions(+) create mode 100644 src/listmonitorlistmodel.cpp create mode 100644 src/listmonitorlistmodel.h create mode 100644 test/monitor_models/listmonitorlistmodel_test.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c9f7dd9..7162623 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -25,6 +25,8 @@ qt_add_qml_module(scratchcpp-render monitormodel.h valuemonitormodel.cpp valuemonitormodel.h + listmonitorlistmodel.cpp + listmonitorlistmodel.h irenderedtarget.h renderedtarget.cpp renderedtarget.h 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/test/monitor_models/CMakeLists.txt b/test/monitor_models/CMakeLists.txt index 88fde95..f6c40a1 100644 --- a/test/monitor_models/CMakeLists.txt +++ b/test/monitor_models/CMakeLists.txt @@ -1,3 +1,4 @@ +# monitormodel add_executable( monitormodel_test monitormodel_test.cpp @@ -14,6 +15,7 @@ target_link_libraries( add_test(monitormodel_test) gtest_discover_tests(monitormodel_test) +# valuemonitormodel add_executable( valuemonitormodel_test valuemonitormodel_test.cpp @@ -31,3 +33,20 @@ target_link_libraries( 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) 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); +} From 98e99edc8caa1e31c1d58d03c271e9d9fac1c787 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:25:38 +0100 Subject: [PATCH 12/16] Add ListMonitorModel class --- src/CMakeLists.txt | 2 + src/listmonitormodel.cpp | 61 ++++++++ src/listmonitormodel.h | 52 +++++++ test/monitor_models/CMakeLists.txt | 19 +++ test/monitor_models/listmonitormodel_test.cpp | 130 ++++++++++++++++++ 5 files changed, 264 insertions(+) create mode 100644 src/listmonitormodel.cpp create mode 100644 src/listmonitormodel.h create mode 100644 test/monitor_models/listmonitormodel_test.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7162623..5a3f04e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -25,6 +25,8 @@ qt_add_qml_module(scratchcpp-render monitormodel.h valuemonitormodel.cpp valuemonitormodel.h + listmonitormodel.cpp + listmonitormodel.h listmonitorlistmodel.cpp listmonitorlistmodel.h irenderedtarget.h 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/test/monitor_models/CMakeLists.txt b/test/monitor_models/CMakeLists.txt index f6c40a1..ebd6d03 100644 --- a/test/monitor_models/CMakeLists.txt +++ b/test/monitor_models/CMakeLists.txt @@ -50,3 +50,22 @@ target_link_libraries( 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/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")); + } +} From f3c6092ac79f00536f7c2498a960db8b5c9a1696 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:06:52 +0100 Subject: [PATCH 13/16] Load list monitors --- src/projectloader.cpp | 3 ++- test/projectloader/projectloader_test.cpp | 15 ++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/projectloader.cpp b/src/projectloader.cpp index f46d828..fe700ec 100644 --- a/src/projectloader.cpp +++ b/src/projectloader.cpp @@ -9,6 +9,7 @@ #include "projectloader.h" #include "spritemodel.h" #include "valuemonitormodel.h" +#include "listmonitormodel.h" #include "renderedtarget.h" using namespace scratchcpprender; @@ -315,7 +316,7 @@ void ProjectLoader::addMonitor(Monitor *monitor) switch (monitor->mode()) { case Monitor::Mode::List: - // TODO: Add support for list monitors + model = new ListMonitorModel(section.get()); break; default: diff --git a/test/projectloader/projectloader_test.cpp b/test/projectloader/projectloader_test.cpp index 4b08249..84adb5c 100644 --- a/test/projectloader/projectloader_test.cpp +++ b/test/projectloader/projectloader_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -81,15 +82,15 @@ TEST_F(ProjectLoaderTest, Load) ASSERT_EQ(sprites[1]->sprite(), engine->targetAt(2)); const auto &monitors = loader.monitorList(); - ASSERT_EQ(monitors.size(), 7); + ASSERT_EQ(monitors.size(), 10); - ValueMonitorModel *monitorModel = dynamic_cast(monitors[0]); - ASSERT_EQ(monitorModel->monitor(), engine->monitors().at(3).get()); - ASSERT_EQ(monitorModel->color(), QColor::fromString("#FF8C1A")); + ListMonitorModel *listMonitorModel = dynamic_cast(monitors[0]); + ASSERT_EQ(listMonitorModel->monitor(), engine->monitors().at(0).get()); + ASSERT_EQ(listMonitorModel->color(), QColor::fromString("#FF661A")); - monitorModel = dynamic_cast(monitors[1]); - ASSERT_EQ(monitorModel->monitor(), engine->monitors().at(4).get()); - ASSERT_EQ(monitorModel->color(), QColor::fromString("#FF8C1A")); + 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) From 8e70aeb23c1b68a709ae903bbc8ae567255066f4 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:08:02 +0100 Subject: [PATCH 14/16] Add ListMonitor component --- src/CMakeLists.txt | 1 + src/internal/ListMonitor.qml | 201 +++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 src/internal/ListMonitor.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5a3f04e..5cd33b5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ qt_add_qml_module(scratchcpp-render RESOURCES internal/ValueMonitor.qml internal/MonitorSlider.qml + internal/ListMonitor.qml SOURCES global.h projectloader.cpp 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 + } +} From 3a37f9c155e52a0355384c6e7f3c352f835e0598 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:08:25 +0100 Subject: [PATCH 15/16] ProjectPlayer: Implement list monitors --- src/ProjectPlayer.qml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/ProjectPlayer.qml b/src/ProjectPlayer.qml index 8a78297..908dbf6 100644 --- a/src/ProjectPlayer.qml +++ b/src/ProjectPlayer.qml @@ -159,12 +159,24 @@ ProjectScene { } } + 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 : null) : null + sourceComponent: monitorModel ? (monitorModel.type === MonitorModel.Value ? renderedValueMonitor : renderedListMonitor) : null active: sourceComponent != null z: loader.sprites.length + loader.clones.length + 1 // above all sprites } From 13c9d7d468c32e3d7a8434a957872c8587dad475 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:14:52 +0100 Subject: [PATCH 16/16] README: Mark monitors as finished --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)