GUI milestone 0
This commit is contained in:
parent
286077af69
commit
4fc36822ba
4 changed files with 395 additions and 7 deletions
345
.sisyphus/notepads/warppipe-node-editor/learnings.md
Normal file
345
.sisyphus/notepads/warppipe-node-editor/learnings.md
Normal 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
|
||||||
|
|
||||||
|
|
@ -59,3 +59,31 @@ if(WARPPIPE_BUILD_TESTS)
|
||||||
target_compile_definitions(warppipe_tests PRIVATE WARPPIPE_TESTING)
|
target_compile_definitions(warppipe_tests PRIVATE WARPPIPE_TESTING)
|
||||||
add_test(NAME warppipe_tests COMMAND warppipe_tests)
|
add_test(NAME warppipe_tests COMMAND warppipe_tests)
|
||||||
endif()
|
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()
|
||||||
|
|
|
||||||
14
GUI_PLAN.md
14
GUI_PLAN.md
|
|
@ -7,13 +7,13 @@ A Qt6-based node editor GUI for warppipe using the QtNodes (nodeeditor) library.
|
||||||
|
|
||||||
## Milestones
|
## Milestones
|
||||||
|
|
||||||
- [ ] Milestone 0 - Qt6 Project Setup
|
- [x] Milestone 0 - Qt6 Project Setup
|
||||||
- [ ] Create `gui/` subdirectory in warppipe project
|
- [x] Create `gui/` subdirectory in warppipe project
|
||||||
- [ ] Add Qt6 + QtNodes to CMakeLists.txt (FetchContent for nodeeditor from github.com/paceholder/nodeeditor)
|
- [x] Add Qt6 + QtNodes to CMakeLists.txt (FetchContent for nodeeditor from github.com/paceholder/nodeeditor)
|
||||||
- [ ] Create `warppipe-gui` target with Qt6::Widgets and QtNodes dependencies
|
- [x] Create `warppipe-gui` target with Qt6::Widgets and QtNodes dependencies
|
||||||
- [ ] Enable CMAKE_AUTOMOC, CMAKE_AUTORCC, CMAKE_AUTOUIC
|
- [x] Enable CMAKE_AUTOMOC, CMAKE_AUTORCC, CMAKE_AUTOUIC
|
||||||
- [ ] Create minimal main.cpp with QApplication + QMainWindow
|
- [x] Create minimal main.cpp with QApplication + QMainWindow
|
||||||
- [ ] Verify GUI launches and shows empty window
|
- [x] Verify GUI launches and shows empty window
|
||||||
|
|
||||||
- [ ] Milestone 1 - Core Model Integration
|
- [ ] Milestone 1 - Core Model Integration
|
||||||
- [ ] Create `WarpGraphModel : public QtNodes::AbstractGraphModel`
|
- [ ] Create `WarpGraphModel : public QtNodes::AbstractGraphModel`
|
||||||
|
|
|
||||||
15
gui/main.cpp
Normal file
15
gui/main.cpp
Normal 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();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue