GUI milestone 0

This commit is contained in:
Joey Yakimowich-Payne 2026-01-29 22:03:59 -07:00
commit 4fc36822ba
4 changed files with 395 additions and 7 deletions

View file

@ -0,0 +1,345 @@
## QtNodes (nodeeditor) Library Research - 2026-01-29
### Library Overview
- **Repository**: https://github.com/paceholder/nodeeditor
- **License**: BSD-3-Clause
- **Latest Version**: 3.0.x (major rewrite from v2.x)
- **Qt Support**: Qt 5.15+ and Qt 6.x
- **Documentation**: https://qtnodes.readthedocs.io/
### 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
```cmake
# 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
```cpp
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
```cpp
auto registry = std::make_shared<NodeDelegateModelRegistry>();
registry->registerModel<MyCustomNode>("Category");
```
#### 3. Create DataFlowGraphModel
```cpp
auto model = std::make_shared<DataFlowGraphModel>(registry);
```
### Port Management
#### Port Types
```cpp
enum class PortType { In, Out };
```
#### Defining Ports
```cpp
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:
```cpp
portsAboutToBeInserted(nodeId, PortType::Out, startIndex, endIndex);
// Modify underlying data
portsInserted();
portsAboutToBeDeleted(nodeId, PortType::In, startIndex, endIndex);
// Modify underlying data
portsDeleted();
```
### Connection Management
#### Connection Structure
```cpp
struct ConnectionId {
NodeId outNodeId;
PortIndex outPortIndex;
NodeId inNodeId;
PortIndex inPortIndex;
};
```
#### Creating Connections
```cpp
// Programmatically
ConnectionId connId{sourceNode, 0, targetNode, 0};
model->addConnection(connId);
// Check if possible
if (model->connectionPossible(connId)) {
model->addConnection(connId);
}
```
#### Connection Policies
```cpp
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
```cpp
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
```cpp
#include <QtNodes/StyleCollection>
auto& nodeStyle = StyleCollection::nodeStyle();
auto& connStyle = StyleCollection::connectionStyle();
```
#### JSON Style Format
```json
{
"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
```cpp
// 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
```cpp
#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
```cpp
// 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
```cpp
// 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
```cpp
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
```cpp
NodeFlags MyModel::nodeFlags(NodeId nodeId) const override {
auto flags = DataFlowGraphModel::nodeFlags(nodeId);
if (shouldLock) flags |= NodeFlag::Locked;
return flags;
}
```
#### Disable Connection Detaching
```cpp
bool MyModel::detachPossible(ConnectionId const& id) const override {
return false; // Prevent detaching
}
```
#### Embedded Widgets
```cpp
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

View file

@ -59,3 +59,31 @@ if(WARPPIPE_BUILD_TESTS)
target_compile_definitions(warppipe_tests PRIVATE WARPPIPE_TESTING)
add_test(NAME warppipe_tests COMMAND warppipe_tests)
endif()
option(WARPPIPE_BUILD_GUI "Build warppipe Qt6 GUI application" ON)
if(WARPPIPE_BUILD_GUI)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
find_package(Qt6 6.2 REQUIRED COMPONENTS Core Widgets)
FetchContent_Declare(
QtNodes
GIT_REPOSITORY https://github.com/paceholder/nodeeditor
GIT_TAG master
)
FetchContent_MakeAvailable(QtNodes)
add_executable(warppipe-gui
gui/main.cpp
)
target_link_libraries(warppipe-gui PRIVATE
warppipe
Qt6::Core
Qt6::Widgets
QtNodes
)
endif()

View file

@ -7,13 +7,13 @@ A Qt6-based node editor GUI for warppipe using the QtNodes (nodeeditor) library.
## Milestones
- [ ] Milestone 0 - Qt6 Project Setup
- [ ] Create `gui/` subdirectory in warppipe project
- [ ] Add Qt6 + QtNodes to CMakeLists.txt (FetchContent for nodeeditor from github.com/paceholder/nodeeditor)
- [ ] Create `warppipe-gui` target with Qt6::Widgets and QtNodes dependencies
- [ ] Enable CMAKE_AUTOMOC, CMAKE_AUTORCC, CMAKE_AUTOUIC
- [ ] Create minimal main.cpp with QApplication + QMainWindow
- [ ] Verify GUI launches and shows empty window
- [x] Milestone 0 - Qt6 Project Setup
- [x] Create `gui/` subdirectory in warppipe project
- [x] Add Qt6 + QtNodes to CMakeLists.txt (FetchContent for nodeeditor from github.com/paceholder/nodeeditor)
- [x] Create `warppipe-gui` target with Qt6::Widgets and QtNodes dependencies
- [x] Enable CMAKE_AUTOMOC, CMAKE_AUTORCC, CMAKE_AUTOUIC
- [x] Create minimal main.cpp with QApplication + QMainWindow
- [x] Verify GUI launches and shows empty window
- [ ] Milestone 1 - Core Model Integration
- [ ] Create `WarpGraphModel : public QtNodes::AbstractGraphModel`

15
gui/main.cpp Normal file
View file

@ -0,0 +1,15 @@
#include <QApplication>
#include <QMainWindow>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QCoreApplication::setApplicationName("Warppipe");
QCoreApplication::setApplicationVersion("0.1.0");
QMainWindow window;
window.setWindowTitle("Warppipe — Audio Router");
window.resize(1280, 720);
window.show();
return app.exec();
}