diff --git a/CMakeLists.txt b/CMakeLists.txt index cf036012..32b4f679 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,7 @@ else() endif() find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets Gui OpenGL) +find_package(Qt${QT_VERSION_MAJOR} QUIET COMPONENTS Quick) message(STATUS "QT_VERSION: ${QT_VERSION}, QT_DIR: ${QT_DIR}") if (${QT_VERSION} VERSION_LESS 5.11.0) @@ -129,6 +130,12 @@ set(HPP_HEADER_FILES include/QtNodes/internal/UndoCommands.hpp ) +if(Qt${QT_VERSION_MAJOR}Quick_FOUND AND ${QT_VERSION} VERSION_GREATER_EQUAL 6.0.0) + message(STATUS "Qt Quick found") + list(APPEND CPP_SOURCE_FILES src/QmlWrapper.cpp) + list(APPEND HPP_HEADER_FILES include/QtNodes/internal/QmlWrapper.hpp) +endif() + # If we want to give the option to build a static library, # set BUILD_SHARED_LIBS option to OFF add_library(QtNodes @@ -155,6 +162,12 @@ target_link_libraries(QtNodes Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::OpenGL ) +if(Qt${QT_VERSION_MAJOR}Quick_FOUND AND ${QT_VERSION} VERSION_GREATER_EQUAL 6.0.0) + target_link_libraries(QtNodes + PUBLIC + Qt${QT_VERSION_MAJOR}::Quick +) +endif() target_compile_definitions(QtNodes PUBLIC diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 49494da2..5c74c574 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -16,3 +16,6 @@ add_subdirectory(dynamic_ports) add_subdirectory(lock_nodes_and_connections) +if(Qt${QT_VERSION_MAJOR}Quick_FOUND AND ${QT_VERSION} VERSION_GREATER_EQUAL 6.0.0) + add_subdirectory(calculator_qml) +endif() diff --git a/examples/calculator_qml/AdditionModel.hpp b/examples/calculator_qml/AdditionModel.hpp new file mode 100644 index 00000000..b172b6ae --- /dev/null +++ b/examples/calculator_qml/AdditionModel.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "DecimalData.hpp" +#include "MathOperationDataModel.hpp" + +#include + +#include +#include + +/// The model dictates the number of inputs and outputs for the Node. +/// In this example it has no logic. +class AdditionModel : public MathOperationDataModel +{ +public: + ~AdditionModel() = default; + +public: + QString caption() const override { return QStringLiteral("Addition"); } + + QString name() const override { return QStringLiteral("Addition"); } + +private: + void compute() override + { + PortIndex const outPortIndex = 0; + + auto n1 = _number1.lock(); + auto n2 = _number2.lock(); + + if (n1 && n2) { + _result = std::make_shared(n1->number() + n2->number()); + } else { + _result.reset(); + } + + Q_EMIT dataUpdated(outPortIndex); + } +}; diff --git a/examples/calculator_qml/CMakeLists.txt b/examples/calculator_qml/CMakeLists.txt new file mode 100644 index 00000000..0cf1c2f6 --- /dev/null +++ b/examples/calculator_qml/CMakeLists.txt @@ -0,0 +1,33 @@ +set(CALC_SOURCE_FILES + main.cpp MathOperationDataModel.cpp MyNodeEditor.cpp NumberDisplayDataModel.cpp + NumberSourceDataModel.cpp StringDataModel.cpp) + +set(CALC_HEADER_FILES + AdditionModel.hpp + DivisionModel.hpp + DecimalData.hpp + MathOperationDataModel.hpp + MultiplicationModel.hpp + MyNodeEditor.hpp + NumberDisplayDataModel.hpp + NumberSourceDataModel.hpp + SubtractionModel.hpp + StringDataModel.hpp + StringData.hpp) + +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +cmake_policy(SET CMP0071 NEW) + +qt_standard_project_setup(REQUIRES 6.5) + +qt_add_resources(QT_RESOURCES "main.qrc") +qt_add_executable(calculator_qml ${CALC_SOURCE_FILES} ${CALC_HEADER_FILES} + ${QT_RESOURCES}) + +qt_add_qml_module( + calculator_qml VERSION 1.0 URI hello QML_FILES main.qml +) + +target_link_libraries(calculator_qml PRIVATE Qt6::Gui QtNodes) diff --git a/examples/calculator_qml/DecimalData.hpp b/examples/calculator_qml/DecimalData.hpp new file mode 100644 index 00000000..bd337d24 --- /dev/null +++ b/examples/calculator_qml/DecimalData.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +using QtNodes::NodeData; +using QtNodes::NodeDataType; + +/// The class can potentially incapsulate any user data which +/// need to be transferred within the Node Editor graph +class DecimalData : public NodeData +{ +public: + DecimalData() + : _number(0.0) + {} + + DecimalData(double const number) + : _number(number) + {} + + NodeDataType type() const override { return NodeDataType{"decimal", "Decimal"}; } + + double number() const { return _number; } + + QString numberAsText() const { return QString::number(_number, 'f'); } + +private: + double _number; +}; diff --git a/examples/calculator_qml/DivisionModel.hpp b/examples/calculator_qml/DivisionModel.hpp new file mode 100644 index 00000000..9018450c --- /dev/null +++ b/examples/calculator_qml/DivisionModel.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include "DecimalData.hpp" +#include "MathOperationDataModel.hpp" + +#include + +#include +#include + +/// The model dictates the number of inputs and outputs for the Node. +/// In this example it has no logic. +class DivisionModel : public MathOperationDataModel +{ +public: + virtual ~DivisionModel() {} + +public: + QString caption() const override { return QStringLiteral("Division"); } + + bool portCaptionVisible(PortType portType, PortIndex portIndex) const override + { + Q_UNUSED(portType); + Q_UNUSED(portIndex); + return true; + } + + QString portCaption(PortType portType, PortIndex portIndex) const override + { + switch (portType) { + case PortType::In: + if (portIndex == 0) + return QStringLiteral("Dividend"); + else if (portIndex == 1) + return QStringLiteral("Divisor"); + + break; + + case PortType::Out: + return QStringLiteral("Result"); + + default: + break; + } + return QString(); + } + + QString name() const override { return QStringLiteral("Division"); } + +private: + void compute() override + { + PortIndex const outPortIndex = 0; + + auto n1 = _number1.lock(); + auto n2 = _number2.lock(); + + if (n2 && (n2->number() == 0.0)) { + //modelValidationState = NodeValidationState::Error; + //modelValidationError = QStringLiteral("Division by zero error"); + _result.reset(); + } else if (n1 && n2) { + //modelValidationState = NodeValidationState::Valid; + //modelValidationError = QString(); + _result = std::make_shared(n1->number() / n2->number()); + } else { + //modelValidationState = NodeValidationState::Warning; + //modelValidationError = QStringLiteral("Missing or incorrect inputs"); + _result.reset(); + } + + Q_EMIT dataUpdated(outPortIndex); + } +}; diff --git a/examples/calculator_qml/MathOperationDataModel.cpp b/examples/calculator_qml/MathOperationDataModel.cpp new file mode 100644 index 00000000..f6a3a26d --- /dev/null +++ b/examples/calculator_qml/MathOperationDataModel.cpp @@ -0,0 +1,42 @@ +#include "MathOperationDataModel.hpp" + +#include "DecimalData.hpp" + +unsigned int MathOperationDataModel::nPorts(PortType portType) const +{ + unsigned int result; + + if (portType == PortType::In) + result = 2; + else + result = 1; + + return result; +} + +NodeDataType MathOperationDataModel::dataType(PortType, PortIndex) const +{ + return DecimalData().type(); +} + +std::shared_ptr MathOperationDataModel::outData(PortIndex) +{ + return std::static_pointer_cast(_result); +} + +void MathOperationDataModel::setInData(std::shared_ptr data, PortIndex portIndex) +{ + auto numberData = std::dynamic_pointer_cast(data); + + if (!data) { + Q_EMIT dataInvalidated(0); + } + + if (portIndex == 0) { + _number1 = numberData; + } else { + _number2 = numberData; + } + + compute(); +} diff --git a/examples/calculator_qml/MathOperationDataModel.hpp b/examples/calculator_qml/MathOperationDataModel.hpp new file mode 100644 index 00000000..6d279642 --- /dev/null +++ b/examples/calculator_qml/MathOperationDataModel.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include +#include +#include + +#include + +class DecimalData; + +using QtNodes::NodeData; +using QtNodes::NodeDataType; +using QtNodes::NodeDelegateModel; +using QtNodes::PortIndex; +using QtNodes::PortType; + +/// The model dictates the number of inputs and outputs for the Node. +/// In this example it has no logic. +class MathOperationDataModel : public NodeDelegateModel +{ + Q_OBJECT + +public: + ~MathOperationDataModel() = default; + +public: + unsigned int nPorts(PortType portType) const override; + + NodeDataType dataType(PortType portType, PortIndex portIndex) const override; + + std::shared_ptr outData(PortIndex port) override; + + void setInData(std::shared_ptr data, PortIndex portIndex) override; + + QWidget *embeddedWidget() override { return nullptr; } + +protected: + virtual void compute() = 0; + +protected: + std::weak_ptr _number1; + std::weak_ptr _number2; + + std::shared_ptr _result; +}; diff --git a/examples/calculator_qml/MultiplicationModel.hpp b/examples/calculator_qml/MultiplicationModel.hpp new file mode 100644 index 00000000..127ae401 --- /dev/null +++ b/examples/calculator_qml/MultiplicationModel.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include + +#include +#include + +#include "MathOperationDataModel.hpp" + +#include "DecimalData.hpp" + +/// The model dictates the number of inputs and outputs for the Node. +/// In this example it has no logic. +class MultiplicationModel : public MathOperationDataModel +{ +public: + virtual ~MultiplicationModel() {} + +public: + QString caption() const override { return QStringLiteral("Multiplication"); } + + QString name() const override { return QStringLiteral("Multiplication"); } + +private: + void compute() override + { + PortIndex const outPortIndex = 0; + + auto n1 = _number1.lock(); + auto n2 = _number2.lock(); + + if (n1 && n2) { + //modelValidationState = NodeValidationState::Valid; + //modelValidationError = QString(); + _result = std::make_shared(n1->number() * n2->number()); + } else { + //modelValidationState = NodeValidationState::Warning; + //modelValidationError = QStringLiteral("Missing or incorrect inputs"); + _result.reset(); + } + + Q_EMIT dataUpdated(outPortIndex); + } +}; diff --git a/examples/calculator_qml/MyNodeEditor.cpp b/examples/calculator_qml/MyNodeEditor.cpp new file mode 100644 index 00000000..37952aaf --- /dev/null +++ b/examples/calculator_qml/MyNodeEditor.cpp @@ -0,0 +1,29 @@ +#include "MyNodeEditor.hpp" + +#include "AdditionModel.hpp" +#include "DivisionModel.hpp" +#include "MultiplicationModel.hpp" +#include "NumberDisplayDataModel.hpp" +#include "NumberSourceDataModel.hpp" +#include "StringDataModel.hpp" +#include "SubtractionModel.hpp" + +std::shared_ptr MyNodeEditor::registerDataModels() const +{ + auto ret = std::make_shared(); + ret->registerModel("Sources"); + + ret->registerModel("Displays"); + + ret->registerModel("Operators"); + + ret->registerModel("Operators"); + + ret->registerModel("Operators"); + + ret->registerModel("Operators"); + + ret->registerModel("Sources"); + + return ret; +} diff --git a/examples/calculator_qml/MyNodeEditor.hpp b/examples/calculator_qml/MyNodeEditor.hpp new file mode 100644 index 00000000..477cadb1 --- /dev/null +++ b/examples/calculator_qml/MyNodeEditor.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +class MyNodeEditor : public QtNodes::QmlWrapper +{ + Q_OBJECT + +public: + explicit MyNodeEditor(QQuickItem *parent = nullptr) + : QtNodes::QmlWrapper(parent) + {} + +protected: + std::shared_ptr registerDataModels() const override; +}; diff --git a/examples/calculator_qml/NumberDisplayDataModel.cpp b/examples/calculator_qml/NumberDisplayDataModel.cpp new file mode 100644 index 00000000..5086050c --- /dev/null +++ b/examples/calculator_qml/NumberDisplayDataModel.cpp @@ -0,0 +1,71 @@ +#include "NumberDisplayDataModel.hpp" + +#include + +NumberDisplayDataModel::NumberDisplayDataModel() + : _label{nullptr} +{} + +unsigned int NumberDisplayDataModel::nPorts(PortType portType) const +{ + unsigned int result = 1; + + switch (portType) { + case PortType::In: + result = 1; + break; + + case PortType::Out: + result = 0; + + default: + break; + } + + return result; +} + +NodeDataType NumberDisplayDataModel::dataType(PortType, PortIndex) const +{ + return DecimalData().type(); +} + +std::shared_ptr NumberDisplayDataModel::outData(PortIndex) +{ + std::shared_ptr ptr; + return ptr; +} + +void NumberDisplayDataModel::setInData(std::shared_ptr data, PortIndex portIndex) +{ + _numberData = std::dynamic_pointer_cast(data); + + if (!_label) + return; + + if (_numberData) { + _label->setText(_numberData->numberAsText()); + } else { + _label->clear(); + } + + _label->adjustSize(); +} + +QWidget *NumberDisplayDataModel::embeddedWidget() +{ + if (!_label) { + _label = new QLabel(); + _label->setMargin(3); + } + + return _label; +} + +double NumberDisplayDataModel::number() const +{ + if (_numberData) + return _numberData->number(); + + return 0.0; +} diff --git a/examples/calculator_qml/NumberDisplayDataModel.hpp b/examples/calculator_qml/NumberDisplayDataModel.hpp new file mode 100644 index 00000000..78f984f5 --- /dev/null +++ b/examples/calculator_qml/NumberDisplayDataModel.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include + +#include + +#include "DecimalData.hpp" + +using QtNodes::NodeData; +using QtNodes::NodeDataType; +using QtNodes::NodeDelegateModel; +using QtNodes::PortIndex; +using QtNodes::PortType; + +class QLabel; + +/// The model dictates the number of inputs and outputs for the Node. +/// In this example it has no logic. +class NumberDisplayDataModel : public NodeDelegateModel +{ + Q_OBJECT + +public: + NumberDisplayDataModel(); + + ~NumberDisplayDataModel() = default; + +public: + QString caption() const override { return QStringLiteral("Result"); } + + bool captionVisible() const override { return false; } + + QString name() const override { return QStringLiteral("Result"); } + +public: + unsigned int nPorts(PortType portType) const override; + + NodeDataType dataType(PortType portType, PortIndex portIndex) const override; + + std::shared_ptr outData(PortIndex port) override; + + void setInData(std::shared_ptr data, PortIndex portIndex) override; + + QWidget *embeddedWidget() override; + + double number() const; + +private: + std::shared_ptr _numberData; + + QLabel *_label; +}; diff --git a/examples/calculator_qml/NumberSourceDataModel.cpp b/examples/calculator_qml/NumberSourceDataModel.cpp new file mode 100644 index 00000000..f6a7ca55 --- /dev/null +++ b/examples/calculator_qml/NumberSourceDataModel.cpp @@ -0,0 +1,110 @@ +#include "NumberSourceDataModel.hpp" + +#include "DecimalData.hpp" + +#include +#include +#include + +NumberSourceDataModel::NumberSourceDataModel() + : _lineEdit{nullptr} + , _number(std::make_shared(0.0)) +{} + +QJsonObject NumberSourceDataModel::save() const +{ + QJsonObject modelJson = NodeDelegateModel::save(); + + modelJson["number"] = QString::number(_number->number()); + + return modelJson; +} + +void NumberSourceDataModel::load(QJsonObject const &p) +{ + QJsonValue v = p["number"]; + + if (!v.isUndefined()) { + QString strNum = v.toString(); + + bool ok; + double d = strNum.toDouble(&ok); + if (ok) { + _number = std::make_shared(d); + + if (_lineEdit) + _lineEdit->setText(strNum); + } + } +} + +unsigned int NumberSourceDataModel::nPorts(PortType portType) const +{ + unsigned int result = 1; + + switch (portType) { + case PortType::In: + result = 0; + break; + + case PortType::Out: + result = 1; + + default: + break; + } + + return result; +} + +void NumberSourceDataModel::onTextEdited(QString const &str) +{ + bool ok = false; + + double number = str.toDouble(&ok); + + if (ok) { + _number = std::make_shared(number); + + Q_EMIT dataUpdated(0); + + } else { + Q_EMIT dataInvalidated(0); + } +} + +NodeDataType NumberSourceDataModel::dataType(PortType, PortIndex) const +{ + return DecimalData().type(); +} + +std::shared_ptr NumberSourceDataModel::outData(PortIndex) +{ + return _number; +} + +QWidget *NumberSourceDataModel::embeddedWidget() +{ + if (!_lineEdit) { + _lineEdit = new QLineEdit(); + + _lineEdit->setValidator(new QDoubleValidator()); + _lineEdit->setMaximumSize(_lineEdit->sizeHint()); + + connect(_lineEdit, &QLineEdit::textChanged, this, &NumberSourceDataModel::onTextEdited); + + _lineEdit->setText(QString::number(_number->number())); + } + + return _lineEdit; +} + +void NumberSourceDataModel::setNumber(double n) +{ + _number = std::make_shared(n); + + Q_EMIT dataUpdated(0); + + if (_lineEdit) + _lineEdit->setText(QString::number(_number->number())); +} diff --git a/examples/calculator_qml/NumberSourceDataModel.hpp b/examples/calculator_qml/NumberSourceDataModel.hpp new file mode 100644 index 00000000..8ae4e324 --- /dev/null +++ b/examples/calculator_qml/NumberSourceDataModel.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include + +#include + +#include + +class DecimalData; + +using QtNodes::NodeData; +using QtNodes::NodeDataType; +using QtNodes::NodeDelegateModel; +using QtNodes::PortIndex; +using QtNodes::PortType; + +class QLineEdit; + +/// The model dictates the number of inputs and outputs for the Node. +/// In this example it has no logic. +class NumberSourceDataModel : public NodeDelegateModel +{ + Q_OBJECT + +public: + NumberSourceDataModel(); + + virtual ~NumberSourceDataModel() {} + +public: + QString caption() const override { return QStringLiteral("Number Source"); } + + bool captionVisible() const override { return false; } + + QString name() const override { return QStringLiteral("NumberSource"); } + +public: + QJsonObject save() const override; + + void load(QJsonObject const &p) override; + +public: + unsigned int nPorts(PortType portType) const override; + + NodeDataType dataType(PortType portType, PortIndex portIndex) const override; + + std::shared_ptr outData(PortIndex port) override; + + void setInData(std::shared_ptr, PortIndex) override {} + + QWidget *embeddedWidget() override; + +public: + void setNumber(double number); + +private Q_SLOTS: + + void onTextEdited(QString const &string); + +private: + std::shared_ptr _number; + + QLineEdit *_lineEdit; +}; diff --git a/examples/calculator_qml/StringData.hpp b/examples/calculator_qml/StringData.hpp new file mode 100644 index 00000000..a4be2f8c --- /dev/null +++ b/examples/calculator_qml/StringData.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +using QtNodes::NodeData; +using QtNodes::NodeDataType; + +/// The class can potentially incapsulate any user data which +/// need to be transferred within the Node Editor graph +class StringData : public NodeData +{ +public: + StringData() + : _text() + {} + + StringData(QString const text) + : _text(text) + {} + + NodeDataType type() const override { return NodeDataType{"string", "String"}; } + + QString number() const { return _text; } + + QString numberAsText() const { return _text; } + +private: + QString _text; +}; diff --git a/examples/calculator_qml/StringDataModel.cpp b/examples/calculator_qml/StringDataModel.cpp new file mode 100644 index 00000000..a891735c --- /dev/null +++ b/examples/calculator_qml/StringDataModel.cpp @@ -0,0 +1 @@ +#include "StringDataModel.hpp" diff --git a/examples/calculator_qml/StringDataModel.hpp b/examples/calculator_qml/StringDataModel.hpp new file mode 100644 index 00000000..1636cff9 --- /dev/null +++ b/examples/calculator_qml/StringDataModel.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include "StringData.hpp" + +using QtNodes::PortType; +using QtNodes::NodeDelegateModel; +using QtNodes::PortIndex; +using QtNodes::NodeDataType; +using QtNodes::NodeData; + +class StringDataModel : public NodeDelegateModel +{ + Q_OBJECT +public: + StringDataModel() = default; + virtual QString caption() const {return _text->numberAsText();} + virtual QString name() const {return "Text";}; + virtual unsigned int nPorts(PortType portType) const { + unsigned int result = 1; + + switch (portType) { + case PortType::In: + result = 0; + break; + + case PortType::Out: + result = 1; + break; + + default: + break; + } + + return result; + } + + virtual NodeDataType dataType(PortType portType, PortIndex portIndex) const { + switch (portType) { + case PortType::In: + return StringData().type(); + + case PortType::Out: + return StringData().type(); + case PortType::None: + default: + return{}; + } + } + virtual void setInData(std::shared_ptr nodeData, PortIndex const portIndex) { + } + + virtual std::shared_ptr outData(PortIndex const port) { + return std::static_pointer_cast(_text); + } + virtual QWidget *embeddedWidget() {return nullptr;} + +private: + std::shared_ptr _text = std::make_shared("Hello"); +}; diff --git a/examples/calculator_qml/SubtractionModel.hpp b/examples/calculator_qml/SubtractionModel.hpp new file mode 100644 index 00000000..ce6e926e --- /dev/null +++ b/examples/calculator_qml/SubtractionModel.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include + +#include + +#include "MathOperationDataModel.hpp" + +#include "DecimalData.hpp" + +/// The model dictates the number of inputs and outputs for the Node. +/// In this example it has no logic. +class SubtractionModel : public MathOperationDataModel +{ +public: + virtual ~SubtractionModel() {} + +public: + QString caption() const override { return QStringLiteral("Subtraction"); } + + virtual bool portCaptionVisible(PortType portType, PortIndex portIndex) const override + { + Q_UNUSED(portType); + Q_UNUSED(portIndex); + return true; + } + + virtual QString portCaption(PortType portType, PortIndex portIndex) const override + { + switch (portType) { + case PortType::In: + if (portIndex == 0) + return QStringLiteral("Minuend"); + else if (portIndex == 1) + return QStringLiteral("Subtrahend"); + + break; + + case PortType::Out: + return QStringLiteral("Result"); + + default: + break; + } + return QString(); + } + + QString name() const override { return QStringLiteral("Subtraction"); } + +private: + void compute() override + { + PortIndex const outPortIndex = 0; + + auto n1 = _number1.lock(); + auto n2 = _number2.lock(); + + if (n1 && n2) { + _result = std::make_shared(n1->number() - n2->number()); + } else { + _result.reset(); + } + + Q_EMIT dataUpdated(outPortIndex); + } +}; diff --git a/examples/calculator_qml/main.cpp b/examples/calculator_qml/main.cpp new file mode 100644 index 00000000..c710bced --- /dev/null +++ b/examples/calculator_qml/main.cpp @@ -0,0 +1,63 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "AdditionModel.hpp" +#include "DivisionModel.hpp" +#include "MultiplicationModel.hpp" +#include "MyNodeEditor.hpp" +#include "NumberDisplayDataModel.hpp" +#include "NumberSourceDataModel.hpp" +#include "StringDataModel.hpp" +#include "SubtractionModel.hpp" + +using QtNodes::ConnectionStyle; + +static void setStyle() +{ + ConnectionStyle::setConnectionStyle( + R"( + { + "ConnectionStyle": { + "ConstructionColor": "gray", + "NormalColor": "black", + "SelectedColor": "gray", + "SelectedHaloColor": "deepskyblue", + "HoveredColor": "deepskyblue", + + "LineWidth": 3.0, + "ConstructionLineWidth": 2.0, + "PointDiameter": 10.0, + + "UseDataDefinedColors": true + } + } + )"); +} + +int main(int argc, char *argv[]) +{ + qputenv("QT_QUICK_BACKEND", "software"); + QApplication app(argc, argv); + + setStyle(); + + qmlRegisterType("bk", 0, 0, "NodeEditor"); + + QQmlApplicationEngine engine; + const QUrl url("qrc:/hello/main.qml"); + engine.load(url); + + return app.exec(); +} diff --git a/examples/calculator_qml/main.qml b/examples/calculator_qml/main.qml new file mode 100644 index 00000000..308df6a7 --- /dev/null +++ b/examples/calculator_qml/main.qml @@ -0,0 +1,23 @@ +import QtQuick 2.0 +import QtQml +import QtQuick.Window +import bk + +Window { + width: 1500 + height: 1000 + visible: true + Text { + id: myLabel + width: 200 + height: 200 + text: "Label to show that zooming with mouse is broken\nif node editor is not a (0, 0) position." + } + NodeEditor { + id: qmlwrap + width: 800 + height: 800 + anchors.top: myLabel.bottom + anchors.left: myLabel.right + } +} diff --git a/examples/calculator_qml/main.qrc b/examples/calculator_qml/main.qrc new file mode 100644 index 00000000..99a0ae43 --- /dev/null +++ b/examples/calculator_qml/main.qrc @@ -0,0 +1,5 @@ + + + main.qml + + diff --git a/include/QtNodes/QmlWrapper b/include/QtNodes/QmlWrapper new file mode 100644 index 00000000..be2d7eeb --- /dev/null +++ b/include/QtNodes/QmlWrapper @@ -0,0 +1 @@ +#include "internal/QmlWrapper.hpp" diff --git a/include/QtNodes/internal/GraphicsView.hpp b/include/QtNodes/internal/GraphicsView.hpp index 52068129..2dd1af2c 100644 --- a/include/QtNodes/internal/GraphicsView.hpp +++ b/include/QtNodes/internal/GraphicsView.hpp @@ -43,6 +43,8 @@ class NODE_EDITOR_PUBLIC GraphicsView : public QGraphicsView double getScale() const; + bool handleEvent(QEvent *e); + public Q_SLOTS: void scaleUp(); @@ -62,6 +64,8 @@ public Q_SLOTS: void scaleChanged(double scale); protected: + bool handleMouseEvent(QMouseEvent *event); + void contextMenuEvent(QContextMenuEvent *event) override; void wheelEvent(QWheelEvent *event) override; @@ -93,5 +97,7 @@ public Q_SLOTS: QPointF _clickPos; ScaleRange _scaleRange; + + QWidget *m_mouseDownWidget = nullptr; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/QmlWrapper.hpp b/include/QtNodes/internal/QmlWrapper.hpp new file mode 100644 index 00000000..2ebb3c8e --- /dev/null +++ b/include/QtNodes/internal/QmlWrapper.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +using QtNodes::GraphicsView; +using QtNodes::DataFlowGraphicsScene; +using QtNodes::DataFlowGraphModel; + +namespace QtNodes { + +class NODE_EDITOR_PUBLIC QuickPaintedView : public QQuickPaintedItem +{ + Q_OBJECT +public: + QuickPaintedView(QQuickItem *parent = nullptr); + +protected: + QSGNode *updatePaintNode(QSGNode *old, UpdatePaintNodeData *d) override; +}; + +class NODE_EDITOR_PUBLIC QmlWrapper : public QuickPaintedView +{ + Q_OBJECT + +public: + explicit QmlWrapper(QQuickItem *parent = nullptr); + +protected: + void componentComplete() override; + virtual std::shared_ptr registerDataModels() const = 0; + +private: + void paint(QPainter *painter) override; + bool event(QEvent *event) override; + bool handleHoverEvent(QHoverEvent *event); + bool handleMouseEvent(QMouseEvent *event); + + QWidget *qWidget() const; + void updateSizeConstraints(); + + void setWidget(); + + GraphicsView *m_widget = nullptr; + DataFlowGraphicsScene *m_widget_scene = nullptr; + DataFlowGraphModel *m_model = nullptr; +}; + +} // namespace QtNodes diff --git a/src/GraphicsView.cpp b/src/GraphicsView.cpp index cbd87cc8..eb0c581b 100644 --- a/src/GraphicsView.cpp +++ b/src/GraphicsView.cpp @@ -164,6 +164,61 @@ void GraphicsView::centerScene() } } +bool GraphicsView::handleMouseEvent(QMouseEvent *event) +{ + if (event->type() == QEvent::MouseButtonPress) { + mousePressEvent(event); + } else if (event->type() == QEvent::MouseMove) { + mouseMoveEvent(event); + } + + QPoint pos = event->pos(); + + QWidget *child = childAt(pos); + + if (event->type() == QEvent::MouseButtonPress) { + m_mouseDownWidget = child; + } else if (event->type() == QEvent::MouseButtonRelease) { + m_mouseDownWidget = nullptr; + } + + QWidget *receiver = m_mouseDownWidget ? m_mouseDownWidget : child; + + if (receiver != nullptr) { + QPoint newPt = receiver->mapFrom(this, pos); + + if (event->button() == Qt::MouseButton::RightButton) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QPoint globalPos = event->globalPosition().toPoint(); +#else + QPoint globalPos = event->globalPos(); +#endif + contextMenuEvent(new QContextMenuEvent(QContextMenuEvent::Mouse, newPt, globalPos)); + } +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QMouseEvent *eventlocal = new QMouseEvent(event->type(), + newPt, + event->scenePosition(), + newPt, + event->button(), + event->buttons(), + event->modifiers(), + event->source(), + event->pointingDevice()); +#else + QMouseEvent *eventlocal = new QMouseEvent(event->type(), + newPt, + event->screenPos(), + event->button(), + event->buttons(), + event->modifiers()); +#endif + return qApp->notify(receiver, eventlocal); + } + + return false; +} + void GraphicsView::contextMenuEvent(QContextMenuEvent *event) { if (itemAt(event->pos())) { @@ -219,6 +274,20 @@ void GraphicsView::setScaleRange(ScaleRange range) setScaleRange(range.minimum, range.maximum); } +bool GraphicsView::handleEvent(QEvent *e) +{ + if (QMouseEvent *me = dynamic_cast(e)) { + return handleMouseEvent(me); + } + + if (QWheelEvent *we = dynamic_cast(e)) { + wheelEvent(we); + return true; + } + + return QWidget::event(e); +} + void GraphicsView::scaleUp() { double const step = 1.2; diff --git a/src/QmlWrapper.cpp b/src/QmlWrapper.cpp new file mode 100644 index 00000000..f7bce99c --- /dev/null +++ b/src/QmlWrapper.cpp @@ -0,0 +1,167 @@ +#include "QmlWrapper.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +using QtNodes::ConnectionStyle; +using QtNodes::DataFlowGraphicsScene; +using QtNodes::DataFlowGraphModel; +using QtNodes::GraphicsView; +using QtNodes::NodeDelegateModelRegistry; + +using QtNodes::NodeDelegateModelRegistry; + +namespace QtNodes { + +QuickPaintedView::QuickPaintedView(QQuickItem *parent) + : QQuickPaintedItem(parent) +{ + //! NOTE It is necessary that when UI scaling is displayed without a blur + setAntialiasing(false); + setSmooth(false); +} + +QSGNode *QuickPaintedView::updatePaintNode(QSGNode *old, UpdatePaintNodeData *data) +{ + //! NOTE It is necessary that when UI scaling is displayed without a blur + setTextureSize(QSize(width(), height())); + QSGNode *n = QQuickPaintedItem::updatePaintNode(old, data); + update(); // Endless loop + return n; +} + +QmlWrapper::QmlWrapper(QQuickItem *parent) + : QuickPaintedView(parent) +{ + setFlag(QQuickItem::ItemAcceptsDrops, true); + setFlag(QQuickItem::ItemHasContents, true); + + setAcceptHoverEvents(true); + setAcceptedMouseButtons(Qt::AllButtons); +} + +void QmlWrapper::paint(QPainter *painter) +{ + if (qWidget()) { + qWidget()->render(painter, + QPoint(), + QRegion(), + QWidget::DrawWindowBackground | QWidget::DrawChildren); + } +} + +bool QmlWrapper::event(QEvent *event) +{ + if (!m_widget) { + return QQuickItem::event(event); + } + + bool ok = true; + + switch (event->type()) { + case QEvent::HoverEnter: + case QEvent::HoverMove: + case QEvent::HoverLeave: + ok = handleHoverEvent(dynamic_cast(event)); + break; + case QEvent::MouseButtonPress: { + setFocus(true); + ok = m_widget->handleEvent(event); + break; + } + default: + ok = m_widget->handleEvent(event); + break; + } + + if (ok) { + update(); + } + + return ok; +} + +bool QmlWrapper::handleHoverEvent(QHoverEvent *event) +{ + auto convertEventType = [](QEvent::Type type) { + static const QMap types{{QEvent::HoverLeave, QEvent::Leave}, + {QEvent::HoverEnter, QEvent::Enter}, + {QEvent::HoverMove, QEvent::MouseMove}}; + + return types[type]; + }; + + QEvent::Type convertedType = convertEventType(event->type()); + + if (convertedType == QEvent::MouseMove) { + QMouseEvent *mouseEvent = new QMouseEvent(convertedType, + event->position(), + event->scenePosition(), + event->globalPosition(), + event->button(), + event->buttons(), + event->modifiers(), + Qt::MouseEventSource::MouseEventNotSynthesized, + event->pointingDevice()); + mouseEvent->setAccepted(event->isAccepted()); + mouseEvent->setTimestamp(event->timestamp()); + bool ok = m_widget->handleEvent(mouseEvent); + setCursor(m_widget->cursor()); + return ok; + } + + QEvent newEvent(convertedType); + newEvent.setAccepted(event->isAccepted()); + + return QCoreApplication::sendEvent(m_widget, &newEvent); +} + +void QmlWrapper::componentComplete() +{ + QQuickItem::componentComplete(); + + setWidget(); + + connect(this, &QQuickItem::widthChanged, [this]() { updateSizeConstraints(); }); + + connect(this, &QQuickItem::heightChanged, [this]() { updateSizeConstraints(); }); +} + +QWidget *QmlWrapper::qWidget() const +{ + return m_widget; +} + +void QmlWrapper::updateSizeConstraints() +{ + if (qWidget()) { + qWidget()->setFixedSize(width(), height()); + } +} + +void QmlWrapper::setWidget() +{ + std::shared_ptr registry = registerDataModels(); + + m_model = new DataFlowGraphModel(registry); + + m_widget_scene = new DataFlowGraphicsScene(*m_model, nullptr); + + m_widget = new GraphicsView(m_widget_scene); + + updateSizeConstraints(); + + QObject::connect(m_widget_scene, + &DataFlowGraphicsScene::sceneLoaded, + m_widget, + &GraphicsView::centerScene); +} + +} // namespace QtNodes