diff --git a/.gitignore b/.gitignore index fc8ef420c..552a9b2f0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ CMakeLists.txt.user build*/ .vscode/ +qt-build + tags diff --git a/examples/calculator/DivisionModel.hpp b/examples/calculator/DivisionModel.hpp index 9018450c8..ea5e6b09a 100644 --- a/examples/calculator/DivisionModel.hpp +++ b/examples/calculator/DivisionModel.hpp @@ -55,17 +55,27 @@ class DivisionModel : public MathOperationDataModel auto n1 = _number1.lock(); auto n2 = _number2.lock(); + QtNodes::NodeValidationState state; if (n2 && (n2->number() == 0.0)) { - //modelValidationState = NodeValidationState::Error; - //modelValidationError = QStringLiteral("Division by zero error"); + state._state = QtNodes::NodeValidationState::State::Error; + state._stateMessage = QStringLiteral("Division by zero error"); + setValidatonState(state); _result.reset(); + } else if ( n2 && (n2->number() < 1e-5)) { + state._state = QtNodes::NodeValidationState::State::Warning; + state._stateMessage = QStringLiteral("Very small divident. Result might overflow"); + setValidatonState(state); + if (n1) { + _result = std::make_shared(n1->number() / n2->number()); + } else { + _result.reset(); + } } else if (n1 && n2) { - //modelValidationState = NodeValidationState::Valid; - //modelValidationError = QString(); + setValidatonState(state); _result = std::make_shared(n1->number() / n2->number()); } else { - //modelValidationState = NodeValidationState::Warning; - //modelValidationError = QStringLiteral("Missing or incorrect inputs"); + QtNodes::NodeValidationState state; + setValidatonState(state); _result.reset(); } diff --git a/include/QtNodes/internal/DefaultNodePainter.hpp b/include/QtNodes/internal/DefaultNodePainter.hpp index 484969f9a..589800ddd 100644 --- a/include/QtNodes/internal/DefaultNodePainter.hpp +++ b/include/QtNodes/internal/DefaultNodePainter.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include "AbstractNodePainter.hpp" @@ -30,5 +31,10 @@ class NODE_EDITOR_PUBLIC DefaultNodePainter : public AbstractNodePainter void drawEntryLabels(QPainter *painter, NodeGraphicsObject &ngo) const; void drawResizeRect(QPainter *painter, NodeGraphicsObject &ngo) const; + + void drawValidationIcon(QPainter *painter, NodeGraphicsObject &ngo) const; + +private: + QIcon _toolTipIcon{"://info-tooltip.svg"}; }; } // namespace QtNodes diff --git a/include/QtNodes/internal/Definitions.hpp b/include/QtNodes/internal/Definitions.hpp index 863fa40b4..efd9bbd3e 100644 --- a/include/QtNodes/internal/Definitions.hpp +++ b/include/QtNodes/internal/Definitions.hpp @@ -18,21 +18,22 @@ NODE_EDITOR_PUBLIC Q_NAMESPACE Q_NAMESPACE_EXPORT(NODE_EDITOR_PUBLIC) #endif - /** +/** * Constants used for fetching QVariant data from GraphModel. */ - enum class NodeRole { - Type = 0, ///< Type of the current node, usually a string. - Position = 1, ///< `QPointF` positon of the node on the scene. - Size = 2, ///< `QSize` for resizable nodes. - CaptionVisible = 3, ///< `bool` for caption visibility. - Caption = 4, ///< `QString` for node caption. - Style = 5, ///< Custom NodeStyle as QJsonDocument - InternalData = 6, ///< Node-stecific user data as QJsonObject - InPortCount = 7, ///< `unsigned int` - OutPortCount = 9, ///< `unsigned int` - Widget = 10, ///< Optional `QWidget*` or `nullptr` - }; +enum class NodeRole { + Type = 0, ///< Type of the current node, usually a string. + Position = 1, ///< `QPointF` positon of the node on the scene. + Size = 2, ///< `QSize` for resizable nodes. + CaptionVisible = 3, ///< `bool` for caption visibility. + Caption = 4, ///< `QString` for node caption. + Style = 5, ///< Custom NodeStyle as QJsonDocument + InternalData = 6, ///< Node-stecific user data as QJsonObject + InPortCount = 7, ///< `unsigned int` + OutPortCount = 9, ///< `unsigned int` + Widget = 10, ///< Optional `QWidget*` or `nullptr` + ValidationState = 11, ///< Enum NodeValidationState of the node +}; Q_ENUM_NS(NodeRole) /** diff --git a/include/QtNodes/internal/NodeDelegateModel.hpp b/include/QtNodes/internal/NodeDelegateModel.hpp index a7ae23bd5..b47596f3a 100644 --- a/include/QtNodes/internal/NodeDelegateModel.hpp +++ b/include/QtNodes/internal/NodeDelegateModel.hpp @@ -1,18 +1,36 @@ #pragma once +#include + +#include +#include + #include "Definitions.hpp" #include "Export.hpp" #include "NodeData.hpp" #include "NodeStyle.hpp" #include "Serializable.hpp" -#include - -#include - - namespace QtNodes { +/** + * Describes whether a node configuration is usable and defines a description message + */ +struct NodeValidationState +{ + enum class State : int { + Valid = 0, ///< All required inputs are present and correct. + Warning = 1, ///< Some inputs are missing or questionable, processing may be unreliable. + Error = 2, ///< Inputs or settings are invalid, preventing successful computation. + }; + bool isValid() { return _state == State::Valid; }; + QString const message() { return _stateMessage; } + State state() { return _state; } + + State _state{State::Valid}; + QString _stateMessage{""}; +}; + class StyleCollection; /** @@ -45,9 +63,16 @@ class NODE_EDITOR_PUBLIC NodeDelegateModel : public QObject, public Serializable /// It is possible to hide port caption in GUI virtual bool portCaptionVisible(PortType, PortIndex) const { return false; } + /// Validation State will default to Valid, but you can manipulate it by overriding in an inherited class + virtual NodeValidationState validationState() const { return _nodeValidationState; } + +public: QJsonObject save() const override; + void load(QJsonObject const &) override; + void setValidatonState(const NodeValidationState &validationState); + virtual unsigned int nPorts(PortType portType) const = 0; virtual NodeDataType dataType(PortType portType, PortIndex portIndex) const = 0; @@ -117,6 +142,10 @@ public Q_SLOTS: private: NodeStyle _nodeStyle; + + NodeValidationState _nodeValidationState; }; } // namespace QtNodes + +Q_DECLARE_METATYPE(QtNodes::NodeValidationState) diff --git a/include/QtNodes/internal/NodeStyle.hpp b/include/QtNodes/internal/NodeStyle.hpp index 85abc5612..9cb4a250c 100644 --- a/include/QtNodes/internal/NodeStyle.hpp +++ b/include/QtNodes/internal/NodeStyle.hpp @@ -43,6 +43,7 @@ class NODE_EDITOR_PUBLIC NodeStyle : public Style QColor WarningColor; QColor ErrorColor; + QColor ToolTipIconColor; float PenWidth; float HoveredPenWidth; diff --git a/resources/DefaultStyle.json b/resources/DefaultStyle.json index 4fe334063..2df69bfd2 100644 --- a/resources/DefaultStyle.json +++ b/resources/DefaultStyle.json @@ -17,8 +17,9 @@ "FontColorFaded" : "gray", "ConnectionPointColor": [169, 169, 169], "FilledConnectionPointColor": "cyan", - "ErrorColor": "red", - "WarningColor": [128, 128, 0], + "ErrorColor": [211, 47, 47], + "WarningColor": [255, 179, 0], + "ToolTipIconColor": "white", "PenWidth": 1.0, "HoveredPenWidth": 1.5, diff --git a/resources/info-tooltip.svg b/resources/info-tooltip.svg new file mode 100644 index 000000000..bab5f333e --- /dev/null +++ b/resources/info-tooltip.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/resources.qrc b/resources/resources.qrc index a0b5ef8ba..08aec37e6 100644 --- a/resources/resources.qrc +++ b/resources/resources.qrc @@ -1,5 +1,6 @@ - - + + DefaultStyle.json + info-tooltip.svg diff --git a/src/DataFlowGraphModel.cpp b/src/DataFlowGraphModel.cpp index f7892a02a..396437241 100644 --- a/src/DataFlowGraphModel.cpp +++ b/src/DataFlowGraphModel.cpp @@ -8,7 +8,6 @@ #include #include - namespace QtNodes { DataFlowGraphModel::DataFlowGraphModel(std::shared_ptr registry) @@ -117,12 +116,10 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con // Check port bounds, i.e. that we do not connect non-existing port numbers auto checkPortBounds = [&](PortType const portType) { NodeId const nodeId = getNodeId(portType, connectionId); - auto portCountRole = (portType == PortType::Out) ? - NodeRole::OutPortCount : - NodeRole::InPortCount; + auto portCountRole = (portType == PortType::Out) ? NodeRole::OutPortCount + : NodeRole::InPortCount; - std::size_t const portCount = - nodeData(nodeId, portCountRole).toUInt(); + std::size_t const portCount = nodeData(nodeId, portCountRole).toUInt(); return getPortIndex(portType, connectionId) < portCount; }; @@ -146,12 +143,9 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con return connected.empty() || (policy == ConnectionPolicy::Many); }; - bool const basicChecks = - getDataType(PortType::Out).id == getDataType(PortType::In).id - && portVacant(PortType::Out) - && portVacant(PortType::In) - && checkPortBounds(PortType::Out) - && checkPortBounds(PortType::In); + bool const basicChecks = getDataType(PortType::Out).id == getDataType(PortType::In).id + && portVacant(PortType::Out) && portVacant(PortType::In) + && checkPortBounds(PortType::Out) && checkPortBounds(PortType::In); // In data-flow mode (this class) it's important to forbid graph loops. // We perform depth-first graph traversal starting from the "Input" port of @@ -161,17 +155,16 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con std::stack filo; filo.push(connectionId.inNodeId); - while (!filo.empty()) - { - auto id = filo.top(); filo.pop(); + while (!filo.empty()) { + auto id = filo.top(); + filo.pop(); if (id == connectionId.outNodeId) { // LOOP! - return true; + return true; } // Add out-connections to continue interations - std::size_t const nOutPorts = - nodeData(id, NodeRole::OutPortCount).toUInt(); + std::size_t const nOutPorts = nodeData(id, NodeRole::OutPortCount).toUInt(); for (PortIndex index = 0; index < nOutPorts; ++index) { auto const &outConnectionIds = connections(id, PortType::Out, index); @@ -188,7 +181,6 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con return basicChecks && (loopsEnabled() || !hasLoops()); } - void DataFlowGraphModel::addConnection(ConnectionId const connectionId) { _connectivity.insert(connectionId); @@ -294,9 +286,14 @@ QVariant DataFlowGraphModel::nodeData(NodeId nodeId, NodeRole role) const break; case NodeRole::Widget: { - auto w = model->embeddedWidget(); + auto *w = model->embeddedWidget(); result = QVariant::fromValue(w); } break; + + case NodeRole::ValidationState: { + auto validationState = model->validationState(); + result = QVariant::fromValue(validationState); + } break; } return result; @@ -356,6 +353,16 @@ bool DataFlowGraphModel::setNodeData(NodeId nodeId, NodeRole role, QVariant valu case NodeRole::Widget: break; + + case NodeRole::ValidationState: { + if (value.canConvert()) { + auto state = value.value(); + if (auto node = delegateModel(nodeId); node != nullptr) { + node->setValidatonState(state); + } + } + Q_EMIT nodeUpdated(nodeId); + } break; } return result; @@ -538,7 +545,8 @@ void DataFlowGraphModel::loadNode(QJsonObject const &nodeJson) connect(model.get(), &NodeDelegateModel::portsAboutToBeDeleted, this, - [restoredNodeId, this](PortType const portType, PortIndex const first, PortIndex const last) { + [restoredNodeId, + this](PortType const portType, PortIndex const first, PortIndex const last) { portsAboutToBeDeleted(restoredNodeId, portType, first, last); }); @@ -550,7 +558,8 @@ void DataFlowGraphModel::loadNode(QJsonObject const &nodeJson) connect(model.get(), &NodeDelegateModel::portsAboutToBeInserted, this, - [restoredNodeId, this](PortType const portType, PortIndex const first, PortIndex const last) { + [restoredNodeId, + this](PortType const portType, PortIndex const first, PortIndex const last) { portsAboutToBeInserted(restoredNodeId, portType, first, last); }); diff --git a/src/DefaultNodePainter.cpp b/src/DefaultNodePainter.cpp index 30bc309d0..1bd2bc5f5 100644 --- a/src/DefaultNodePainter.cpp +++ b/src/DefaultNodePainter.cpp @@ -5,6 +5,7 @@ #include "BasicGraphicsScene.hpp" #include "ConnectionGraphicsObject.hpp" #include "ConnectionIdUtils.hpp" +#include "NodeDelegateModel.hpp" #include "NodeGraphicsObject.hpp" #include "NodeState.hpp" #include "StyleCollection.hpp" @@ -33,6 +34,8 @@ void DefaultNodePainter::paint(QPainter *painter, NodeGraphicsObject &ngo) const drawEntryLabels(painter, ngo); drawResizeRect(painter, ngo); + + drawValidationIcon(painter, ngo); } void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo) const @@ -49,18 +52,38 @@ void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo NodeStyle nodeStyle(json.object()); - auto color = ngo.isSelected() ? nodeStyle.SelectedBoundaryColor : nodeStyle.NormalBoundaryColor; + QVariant var = model.nodeData(nodeId, NodeRole::ValidationState); + + QColor color = ngo.isSelected() ? nodeStyle.SelectedBoundaryColor + : nodeStyle.NormalBoundaryColor; + + auto validationState = NodeValidationState::State::Valid; + if (var.canConvert()) { + auto state = var.value(); + validationState = state._state; + switch (validationState) { + case NodeValidationState::State::Error: + color = nodeStyle.ErrorColor; + break; + case NodeValidationState::State::Warning: + color = nodeStyle.WarningColor; + break; + default: + break; + } + } - if (ngo.nodeState().hovered()) { - QPen p(color, nodeStyle.HoveredPenWidth); - painter->setPen(p); - } else { - QPen p(color, nodeStyle.PenWidth); - painter->setPen(p); + float penWidth = ngo.nodeState().hovered() ? nodeStyle.HoveredPenWidth : nodeStyle.PenWidth; + if (validationState != NodeValidationState::State::Valid) { + float factor = (validationState == NodeValidationState::State::Error) ? 3.0f : 2.0f; + penWidth *= factor; } - QLinearGradient gradient(QPointF(0.0, 0.0), QPointF(2.0, size.height())); + QPen p(color, penWidth); + painter->setPen(p); + + QLinearGradient gradient(QPointF(0.0, 0.0), QPointF(2.0, size.height())); gradient.setColorAt(0.0, nodeStyle.GradientColor0); gradient.setColorAt(0.10, nodeStyle.GradientColor1); gradient.setColorAt(0.90, nodeStyle.GradientColor2); @@ -270,4 +293,53 @@ void DefaultNodePainter::drawResizeRect(QPainter *painter, NodeGraphicsObject &n } } +void DefaultNodePainter::drawValidationIcon(QPainter *painter, NodeGraphicsObject &ngo) const +{ + AbstractGraphModel &model = ngo.graphModel(); + NodeId const nodeId = ngo.nodeId(); + AbstractNodeGeometry &geometry = ngo.nodeScene()->nodeGeometry(); + + QVariant var = model.nodeData(nodeId, NodeRole::ValidationState); + if (!var.canConvert()) + return; + + auto state = var.value(); + if (state._state == NodeValidationState::State::Valid) + return; + + QJsonDocument json = QJsonDocument::fromVariant(model.nodeData(nodeId, NodeRole::Style)); + NodeStyle nodeStyle(json.object()); + + QSize size = geometry.size(nodeId); + + QIcon icon(":/info-tooltip.svg"); + QSize iconSize(16, 16); + QPixmap pixmap = icon.pixmap(iconSize); + + QColor color = (state._state == NodeValidationState::State::Error) ? nodeStyle.ErrorColor + : nodeStyle.WarningColor; + + QPointF center(size.width(), 0.0); + center += QPointF(iconSize.width() / 2.0, -iconSize.height() / 2.0); + + painter->save(); + + // Draw a colored circle behind the icon to highlight validation issues + painter->setPen(Qt::NoPen); + painter->setBrush(color); + painter->drawEllipse(center, iconSize.width() / 2.0 + 2.0, iconSize.height() / 2.0 + 2.0); + + + QPainter imgPainter(&pixmap); + imgPainter.setCompositionMode(QPainter::CompositionMode_SourceIn); + imgPainter.fillRect(pixmap.rect(), nodeStyle.FontColor); + imgPainter.end(); + + + painter->drawPixmap(center.toPoint() - QPoint(iconSize.width() / 2, iconSize.height() / 2), + pixmap); + + painter->restore(); +} + } // namespace QtNodes diff --git a/src/NodeDelegateModel.cpp b/src/NodeDelegateModel.cpp index 94e47ad68..34cf2cb22 100644 --- a/src/NodeDelegateModel.cpp +++ b/src/NodeDelegateModel.cpp @@ -24,6 +24,11 @@ void NodeDelegateModel::load(QJsonObject const &) // } +void NodeDelegateModel::setValidatonState(const NodeValidationState &validationState) +{ + _nodeValidationState = validationState; +} + ConnectionPolicy NodeDelegateModel::portConnectionPolicy(PortType portType, PortIndex) const { auto result = ConnectionPolicy::One; diff --git a/src/NodeGraphicsObject.cpp b/src/NodeGraphicsObject.cpp index d3ca03b40..a355c0bf3 100644 --- a/src/NodeGraphicsObject.cpp +++ b/src/NodeGraphicsObject.cpp @@ -7,6 +7,7 @@ #include "ConnectionGraphicsObject.hpp" #include "ConnectionIdUtils.hpp" #include "NodeConnectionInteraction.hpp" +#include "NodeDelegateModel.hpp" #include "StyleCollection.hpp" #include "UndoCommands.hpp" @@ -37,8 +38,7 @@ NodeGraphicsObject::NodeGraphicsObject(BasicGraphicsScene &scene, NodeId nodeId) NodeStyle nodeStyle(nodeStyleJson); - if(nodeStyle.ShadowEnabled) - { + if (nodeStyle.ShadowEnabled) { auto effect = new QGraphicsDropShadowEffect; effect->setOffset(4, 4); effect->setBlurRadius(20); @@ -79,10 +79,10 @@ BasicGraphicsScene *NodeGraphicsObject::nodeScene() const void NodeGraphicsObject::updateQWidgetEmbedPos() { - if (_proxyWidget) { - AbstractNodeGeometry &geometry = nodeScene()->nodeGeometry(); - _proxyWidget->setPos(geometry.widgetPosition(_nodeId)); - } + if (_proxyWidget) { + AbstractNodeGeometry &geometry = nodeScene()->nodeGeometry(); + _proxyWidget->setPos(geometry.widgetPosition(_nodeId)); + } } void NodeGraphicsObject::embedQWidget() @@ -161,6 +161,16 @@ void NodeGraphicsObject::reactToConnection(ConnectionGraphicsObject const *cgo) void NodeGraphicsObject::paint(QPainter *painter, QStyleOptionGraphicsItem const *option, QWidget *) { + QString tooltip; + QVariant var = _graphModel.nodeData(_nodeId, NodeRole::ValidationState); + if (var.canConvert()) { + auto state = var.value(); + if (state._state != NodeValidationState::State::Valid) { + tooltip = state._stateMessage; + } + } + setToolTip(tooltip); + painter->setClipRect(option->exposedRect); nodeScene()->nodePainter().paint(painter, *this);