Skip to content

Commit ff23d00

Browse files
authored
Adds validation state feedback to nodes (#475)
* wip: adds NodeValidationState info to NodeDelegateModel * makes the nodeObject red in case of an invalid state and adds a tooltip for the error msg * adds warning state and adapts calculator example * adds validation icon and adapts calculation example * improves NodeValidationState struct * qt6 for linux-gcc * reverts qt_version changes in cmake build * changes validation coloring scheme to border instead of whole node * replaces info-tooltip with a better contrast * improves node ui for invalid state * clang-format improvements
1 parent 8b054bc commit ff23d00

13 files changed

Lines changed: 215 additions & 64 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ CMakeLists.txt.user
44
build*/
55
.vscode/
66

7+
qt-build
8+
79
tags

examples/calculator/DivisionModel.hpp

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,27 @@ class DivisionModel : public MathOperationDataModel
5555
auto n1 = _number1.lock();
5656
auto n2 = _number2.lock();
5757

58+
QtNodes::NodeValidationState state;
5859
if (n2 && (n2->number() == 0.0)) {
59-
//modelValidationState = NodeValidationState::Error;
60-
//modelValidationError = QStringLiteral("Division by zero error");
60+
state._state = QtNodes::NodeValidationState::State::Error;
61+
state._stateMessage = QStringLiteral("Division by zero error");
62+
setValidatonState(state);
6163
_result.reset();
64+
} else if ( n2 && (n2->number() < 1e-5)) {
65+
state._state = QtNodes::NodeValidationState::State::Warning;
66+
state._stateMessage = QStringLiteral("Very small divident. Result might overflow");
67+
setValidatonState(state);
68+
if (n1) {
69+
_result = std::make_shared<DecimalData>(n1->number() / n2->number());
70+
} else {
71+
_result.reset();
72+
}
6273
} else if (n1 && n2) {
63-
//modelValidationState = NodeValidationState::Valid;
64-
//modelValidationError = QString();
74+
setValidatonState(state);
6575
_result = std::make_shared<DecimalData>(n1->number() / n2->number());
6676
} else {
67-
//modelValidationState = NodeValidationState::Warning;
68-
//modelValidationError = QStringLiteral("Missing or incorrect inputs");
77+
QtNodes::NodeValidationState state;
78+
setValidatonState(state);
6979
_result.reset();
7080
}
7181

include/QtNodes/internal/DefaultNodePainter.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include <QIcon>
34
#include <QtGui/QPainter>
45

56
#include "AbstractNodePainter.hpp"
@@ -30,5 +31,10 @@ class NODE_EDITOR_PUBLIC DefaultNodePainter : public AbstractNodePainter
3031
void drawEntryLabels(QPainter *painter, NodeGraphicsObject &ngo) const;
3132

3233
void drawResizeRect(QPainter *painter, NodeGraphicsObject &ngo) const;
34+
35+
void drawValidationIcon(QPainter *painter, NodeGraphicsObject &ngo) const;
36+
37+
private:
38+
QIcon _toolTipIcon{"://info-tooltip.svg"};
3339
};
3440
} // namespace QtNodes

include/QtNodes/internal/Definitions.hpp

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,22 @@ NODE_EDITOR_PUBLIC Q_NAMESPACE
1818
Q_NAMESPACE_EXPORT(NODE_EDITOR_PUBLIC)
1919
#endif
2020

21-
/**
21+
/**
2222
* Constants used for fetching QVariant data from GraphModel.
2323
*/
24-
enum class NodeRole {
25-
Type = 0, ///< Type of the current node, usually a string.
26-
Position = 1, ///< `QPointF` positon of the node on the scene.
27-
Size = 2, ///< `QSize` for resizable nodes.
28-
CaptionVisible = 3, ///< `bool` for caption visibility.
29-
Caption = 4, ///< `QString` for node caption.
30-
Style = 5, ///< Custom NodeStyle as QJsonDocument
31-
InternalData = 6, ///< Node-stecific user data as QJsonObject
32-
InPortCount = 7, ///< `unsigned int`
33-
OutPortCount = 9, ///< `unsigned int`
34-
Widget = 10, ///< Optional `QWidget*` or `nullptr`
35-
};
24+
enum class NodeRole {
25+
Type = 0, ///< Type of the current node, usually a string.
26+
Position = 1, ///< `QPointF` positon of the node on the scene.
27+
Size = 2, ///< `QSize` for resizable nodes.
28+
CaptionVisible = 3, ///< `bool` for caption visibility.
29+
Caption = 4, ///< `QString` for node caption.
30+
Style = 5, ///< Custom NodeStyle as QJsonDocument
31+
InternalData = 6, ///< Node-stecific user data as QJsonObject
32+
InPortCount = 7, ///< `unsigned int`
33+
OutPortCount = 9, ///< `unsigned int`
34+
Widget = 10, ///< Optional `QWidget*` or `nullptr`
35+
ValidationState = 11, ///< Enum NodeValidationState of the node
36+
};
3637
Q_ENUM_NS(NodeRole)
3738

3839
/**

include/QtNodes/internal/NodeDelegateModel.hpp

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,36 @@
11
#pragma once
22

3+
#include <memory>
4+
5+
#include <QMetaType>
6+
#include <QtWidgets/QWidget>
7+
38
#include "Definitions.hpp"
49
#include "Export.hpp"
510
#include "NodeData.hpp"
611
#include "NodeStyle.hpp"
712
#include "Serializable.hpp"
813

9-
#include <QtWidgets/QWidget>
10-
11-
#include <memory>
12-
13-
1414
namespace QtNodes {
1515

16+
/**
17+
* Describes whether a node configuration is usable and defines a description message
18+
*/
19+
struct NodeValidationState
20+
{
21+
enum class State : int {
22+
Valid = 0, ///< All required inputs are present and correct.
23+
Warning = 1, ///< Some inputs are missing or questionable, processing may be unreliable.
24+
Error = 2, ///< Inputs or settings are invalid, preventing successful computation.
25+
};
26+
bool isValid() { return _state == State::Valid; };
27+
QString const message() { return _stateMessage; }
28+
State state() { return _state; }
29+
30+
State _state{State::Valid};
31+
QString _stateMessage{""};
32+
};
33+
1634
class StyleCollection;
1735

1836
/**
@@ -45,9 +63,16 @@ class NODE_EDITOR_PUBLIC NodeDelegateModel : public QObject, public Serializable
4563
/// It is possible to hide port caption in GUI
4664
virtual bool portCaptionVisible(PortType, PortIndex) const { return false; }
4765

66+
/// Validation State will default to Valid, but you can manipulate it by overriding in an inherited class
67+
virtual NodeValidationState validationState() const { return _nodeValidationState; }
68+
69+
public:
4870
QJsonObject save() const override;
71+
4972
void load(QJsonObject const &) override;
5073

74+
void setValidatonState(const NodeValidationState &validationState);
75+
5176
virtual unsigned int nPorts(PortType portType) const = 0;
5277

5378
virtual NodeDataType dataType(PortType portType, PortIndex portIndex) const = 0;
@@ -117,6 +142,10 @@ public Q_SLOTS:
117142

118143
private:
119144
NodeStyle _nodeStyle;
145+
146+
NodeValidationState _nodeValidationState;
120147
};
121148

122149
} // namespace QtNodes
150+
151+
Q_DECLARE_METATYPE(QtNodes::NodeValidationState)

include/QtNodes/internal/NodeStyle.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class NODE_EDITOR_PUBLIC NodeStyle : public Style
4343

4444
QColor WarningColor;
4545
QColor ErrorColor;
46+
QColor ToolTipIconColor;
4647

4748
float PenWidth;
4849
float HoveredPenWidth;

resources/DefaultStyle.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
"FontColorFaded" : "gray",
1818
"ConnectionPointColor": [169, 169, 169],
1919
"FilledConnectionPointColor": "cyan",
20-
"ErrorColor": "red",
21-
"WarningColor": [128, 128, 0],
20+
"ErrorColor": [211, 47, 47],
21+
"WarningColor": [255, 179, 0],
22+
"ToolTipIconColor": "white",
2223

2324
"PenWidth": 1.0,
2425
"HoveredPenWidth": 1.5,

resources/info-tooltip.svg

Lines changed: 4 additions & 0 deletions
Loading

resources/resources.qrc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
<RCC version="1.0">
2-
<qresource>
1+
<RCC>
2+
<qresource prefix="/">
33
<file>DefaultStyle.json</file>
4+
<file>info-tooltip.svg</file>
45
</qresource>
56
</RCC>

src/DataFlowGraphModel.cpp

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
#include <stack>
99
#include <stdexcept>
1010

11-
1211
namespace QtNodes {
1312

1413
DataFlowGraphModel::DataFlowGraphModel(std::shared_ptr<NodeDelegateModelRegistry> registry)
@@ -117,12 +116,10 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con
117116
// Check port bounds, i.e. that we do not connect non-existing port numbers
118117
auto checkPortBounds = [&](PortType const portType) {
119118
NodeId const nodeId = getNodeId(portType, connectionId);
120-
auto portCountRole = (portType == PortType::Out) ?
121-
NodeRole::OutPortCount :
122-
NodeRole::InPortCount;
119+
auto portCountRole = (portType == PortType::Out) ? NodeRole::OutPortCount
120+
: NodeRole::InPortCount;
123121

124-
std::size_t const portCount =
125-
nodeData(nodeId, portCountRole).toUInt();
122+
std::size_t const portCount = nodeData(nodeId, portCountRole).toUInt();
126123

127124
return getPortIndex(portType, connectionId) < portCount;
128125
};
@@ -146,12 +143,9 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con
146143
return connected.empty() || (policy == ConnectionPolicy::Many);
147144
};
148145

149-
bool const basicChecks =
150-
getDataType(PortType::Out).id == getDataType(PortType::In).id
151-
&& portVacant(PortType::Out)
152-
&& portVacant(PortType::In)
153-
&& checkPortBounds(PortType::Out)
154-
&& checkPortBounds(PortType::In);
146+
bool const basicChecks = getDataType(PortType::Out).id == getDataType(PortType::In).id
147+
&& portVacant(PortType::Out) && portVacant(PortType::In)
148+
&& checkPortBounds(PortType::Out) && checkPortBounds(PortType::In);
155149

156150
// In data-flow mode (this class) it's important to forbid graph loops.
157151
// We perform depth-first graph traversal starting from the "Input" port of
@@ -161,17 +155,16 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con
161155
std::stack<NodeId> filo;
162156
filo.push(connectionId.inNodeId);
163157

164-
while (!filo.empty())
165-
{
166-
auto id = filo.top(); filo.pop();
158+
while (!filo.empty()) {
159+
auto id = filo.top();
160+
filo.pop();
167161

168162
if (id == connectionId.outNodeId) { // LOOP!
169-
return true;
163+
return true;
170164
}
171165

172166
// Add out-connections to continue interations
173-
std::size_t const nOutPorts =
174-
nodeData(id, NodeRole::OutPortCount).toUInt();
167+
std::size_t const nOutPorts = nodeData(id, NodeRole::OutPortCount).toUInt();
175168

176169
for (PortIndex index = 0; index < nOutPorts; ++index) {
177170
auto const &outConnectionIds = connections(id, PortType::Out, index);
@@ -188,7 +181,6 @@ bool DataFlowGraphModel::connectionPossible(ConnectionId const connectionId) con
188181
return basicChecks && (loopsEnabled() || !hasLoops());
189182
}
190183

191-
192184
void DataFlowGraphModel::addConnection(ConnectionId const connectionId)
193185
{
194186
_connectivity.insert(connectionId);
@@ -294,9 +286,14 @@ QVariant DataFlowGraphModel::nodeData(NodeId nodeId, NodeRole role) const
294286
break;
295287

296288
case NodeRole::Widget: {
297-
auto w = model->embeddedWidget();
289+
auto *w = model->embeddedWidget();
298290
result = QVariant::fromValue(w);
299291
} break;
292+
293+
case NodeRole::ValidationState: {
294+
auto validationState = model->validationState();
295+
result = QVariant::fromValue(validationState);
296+
} break;
300297
}
301298

302299
return result;
@@ -356,6 +353,16 @@ bool DataFlowGraphModel::setNodeData(NodeId nodeId, NodeRole role, QVariant valu
356353

357354
case NodeRole::Widget:
358355
break;
356+
357+
case NodeRole::ValidationState: {
358+
if (value.canConvert<NodeValidationState>()) {
359+
auto state = value.value<NodeValidationState>();
360+
if (auto node = delegateModel<NodeDelegateModel>(nodeId); node != nullptr) {
361+
node->setValidatonState(state);
362+
}
363+
}
364+
Q_EMIT nodeUpdated(nodeId);
365+
} break;
359366
}
360367

361368
return result;
@@ -538,7 +545,8 @@ void DataFlowGraphModel::loadNode(QJsonObject const &nodeJson)
538545
connect(model.get(),
539546
&NodeDelegateModel::portsAboutToBeDeleted,
540547
this,
541-
[restoredNodeId, this](PortType const portType, PortIndex const first, PortIndex const last) {
548+
[restoredNodeId,
549+
this](PortType const portType, PortIndex const first, PortIndex const last) {
542550
portsAboutToBeDeleted(restoredNodeId, portType, first, last);
543551
});
544552

@@ -550,7 +558,8 @@ void DataFlowGraphModel::loadNode(QJsonObject const &nodeJson)
550558
connect(model.get(),
551559
&NodeDelegateModel::portsAboutToBeInserted,
552560
this,
553-
[restoredNodeId, this](PortType const portType, PortIndex const first, PortIndex const last) {
561+
[restoredNodeId,
562+
this](PortType const portType, PortIndex const first, PortIndex const last) {
554563
portsAboutToBeInserted(restoredNodeId, portType, first, last);
555564
});
556565

0 commit comments

Comments
 (0)