warp-pipe/.sisyphus/notepads/warppipe-node-editor/learnings.md

8.4 KiB

QtNodes (nodeeditor) Library Research - 2026-01-29

Library Overview

Key Architecture Concepts

  1. Model-View Approach: Graph structure defined by classes derived from AbstractGraphModel
  2. Headless Mode: Can create/modify graphs without GUI (useful for testing/processing)
  3. Two Main Workflows:
    • Pure graph visualization (AbstractGraphModel)
    • Data propagation (DataFlowGraphModel)

CMake Integration

# Build options
cmake .. -DUSE_QT6=on   # or off for Qt5
cmake .. -DBUILD_SHARED_LIBS=off  # for static lib

# Dependencies
- Qt >5.15
- CMake 3.8+
- Catch2 (for tests, optional)

Creating Custom Node Types

1. Derive from NodeDelegateModel

class MyCustomNode : public QtNodes::NodeDelegateModel {
    Q_OBJECT
public:
    QString name() const override { return "MyNodeType"; }
    QString caption() const override { return "My Node"; }
    
    // Define ports
    unsigned int nPorts(PortType portType) const override;
    NodeDataType dataType(PortType portType, PortIndex index) const override;
    
    // Data handling
    void setInData(std::shared_ptr<NodeData> data, PortIndex port) override;
    std::shared_ptr<NodeData> outData(PortIndex port) override;
    
    // Optional: Embed Qt widget
    QWidget* embeddedWidget() override { return _myWidget; }
    
    // Optional: Make resizable
    bool resizable() const override { return true; }
};

2. Register with NodeDelegateModelRegistry

auto registry = std::make_shared<NodeDelegateModelRegistry>();
registry->registerModel<MyCustomNode>("Category");

3. Create DataFlowGraphModel

auto model = std::make_shared<DataFlowGraphModel>(registry);

Port Management

Port Types

enum class PortType { In, Out };

Defining Ports

unsigned int MyNode::nPorts(PortType portType) const {
    if (portType == PortType::In) return 2;  // 2 input ports
    return 1;  // 1 output port
}

NodeDataType MyNode::dataType(PortType portType, PortIndex index) const {
    return {"audio", "Audio Stream"};  // {id, name}
}

Dynamic Ports

Use these signals to add/remove ports at runtime:

portsAboutToBeInserted(nodeId, PortType::Out, startIndex, endIndex);
// Modify underlying data
portsInserted();

portsAboutToBeDeleted(nodeId, PortType::In, startIndex, endIndex);
// Modify underlying data
portsDeleted();

Connection Management

Connection Structure

struct ConnectionId {
    NodeId outNodeId;
    PortIndex outPortIndex;
    NodeId inNodeId;
    PortIndex inPortIndex;
};

Creating Connections

// Programmatically
ConnectionId connId{sourceNode, 0, targetNode, 0};
model->addConnection(connId);

// Check if possible
if (model->connectionPossible(connId)) {
    model->addConnection(connId);
}

Connection Policies

enum class ConnectionPolicy {
    One,   // Only one connection per port
    Many   // Multiple connections allowed
};

// Return from nodeData with PortRole::ConnectionPolicyRole

Data Propagation

Data Flow Chain

  1. Source node emits dataUpdated(PortIndex)
  2. DataFlowGraphModel::onOutPortDataUpdated() triggered
  3. Data fetched via outData(portIndex)
  4. Data set to connected nodes via setInData(data, portIndex)
  5. Signal inPortDataWasSet() emitted

Custom Data Types

class MyAudioData : public NodeData {
public:
    NodeDataType type() const override {
        return {"audio", "Audio Stream"};
    }
    
    // Your data members
    std::vector<float> samples;
    int sampleRate;
};

Styling and Customization

Style Classes

  • NodeStyle: Node appearance (colors, borders, shadows)
  • ConnectionStyle: Connection lines (colors, width)
  • GraphicsViewStyle: Background grid colors

Accessing Styles

#include <QtNodes/StyleCollection>

auto& nodeStyle = StyleCollection::nodeStyle();
auto& connStyle = StyleCollection::connectionStyle();

JSON Style Format

{
  "NodeStyle": {
    "NormalBoundaryColor": [255, 255, 255],
    "SelectedBoundaryColor": [255, 165, 0],
    "GradientColor0": "gray",
    "ShadowColor": [20, 20, 20],
    "FontColor": "white",
    "ConnectionPointColor": [169, 169, 169],
    "PenWidth": 1.0,
    "Opacity": 0.8
  }
}

Event Handling

Context Menus

// GraphicsView level
void MyGraphicsView::contextMenuEvent(QContextMenuEvent* event) override;

// Node level - connect to signal
connect(scene, &BasicGraphicsScene::nodeContextMenu,
        [](NodeId nodeId, QPointF scenePos) {
            // Show custom menu
        });

Node Events

Available signals from BasicGraphicsScene:

  • nodeClicked(NodeId)
  • nodeDoubleClicked(NodeId)
  • nodeContextMenu(NodeId, QPointF)
  • nodeHovered(NodeId, QPoint)
  • nodeHoverLeft(NodeId)

Scene Setup

Basic Setup

#include <QtNodes/DataFlowGraphModel>
#include <QtNodes/DataFlowGraphicsScene>
#include <QtNodes/GraphicsView>

auto registry = registerMyNodes();
auto model = std::make_shared<DataFlowGraphModel>(registry);
auto scene = new DataFlowGraphicsScene(*model);
auto view = new GraphicsView(scene);

view->show();

Undo/Redo Support

// Scene has built-in QUndoStack
scene->undoStack().undo();
scene->undoStack().redo();

// Keyboard shortcuts (built-in)
// Ctrl+Z: Undo
// Ctrl+Shift+Z: Redo
// Ctrl+D: Duplicate

Serialization

Save/Load Graph

// Save
QJsonObject json = model->save();
QJsonDocument doc(json);
QFile file("graph.json");
file.open(QIODevice::WriteOnly);
file.write(doc.toJson());

// Load
QFile file("graph.json");
file.open(QIODevice::ReadOnly);
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
model->load(doc.object());

Custom Node Serialization

QJsonObject MyNode::save() const override {
    QJsonObject obj = NodeDelegateModel::save();
    obj["custom-data"] = _myCustomValue;
    return obj;
}

void MyNode::load(QJsonObject const& obj) override {
    _myCustomValue = obj["custom-data"].toInt();
}

Layout Options

Horizontal (Default)

 -------
o       |  Caption
o       |
 -------

Vertical

 -------o-------------o-------
|  PortCaption   PortCaption  |
|        Node Caption         |
|         PortCaption         |
 --------------o--------------

Use DefaultVerticalNodeGeometry instead of DefaultHorizontalNodeGeometry.

Advanced Features

Locked Nodes

NodeFlags MyModel::nodeFlags(NodeId nodeId) const override {
    auto flags = DataFlowGraphModel::nodeFlags(nodeId);
    if (shouldLock) flags |= NodeFlag::Locked;
    return flags;
}

Disable Connection Detaching

bool MyModel::detachPossible(ConnectionId const& id) const override {
    return false;  // Prevent detaching
}

Embedded Widgets

QWidget* MyNode::embeddedWidget() override {
    if (!_widget) {
        _widget = new QSlider(Qt::Horizontal);
        connect(_widget, &QSlider::valueChanged, [this](int val) {
            // Update and propagate data
            Q_EMIT dataUpdated(0);
        });
    }
    return _widget;
}

Example References

  • examples/calculator/: Basic data flow with math operations
  • examples/dynamic_ports/: Adding/removing ports at runtime
  • examples/styles/: Custom styling
  • examples/vertical_layout/: Vertical node layout
  • examples/lock_nodes_and_connections/: Node locking
  • examples/headless_main.cpp: Using without GUI

Key Classes Summary

  • AbstractGraphModel: Base graph model
  • DataFlowGraphModel: Graph with data propagation
  • NodeDelegateModel: Custom node implementation
  • NodeDelegateModelRegistry: Node type registration
  • BasicGraphicsScene: Scene management
  • DataFlowGraphicsScene: Scene with data flow
  • GraphicsView: View widget
  • NodeData: Base class for data types
  • StyleCollection: Centralized styling

Important Notes for Audio Routing

  1. Use ConnectionPolicy::Many for audio ports (multiple connections)
  2. Consider using std::shared_ptr<AudioBuffer> for data sharing
  3. Implement proper sample rate/buffer size handling in data types
  4. Use embedded widgets for gain controls, meters, etc.
  5. Consider headless mode for audio processing thread
  6. Implement proper serialization for saving/loading patches