Skip to content

Commit 80a9077

Browse files
author
Tato Levicz
committed
feat: undo redo + readme qml updated
1 parent c7065a9 commit 80a9077

5 files changed

Lines changed: 333 additions & 32 deletions

File tree

README_QML.md

Lines changed: 117 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,95 @@ This document describes the implementation of QML support for the **QtNodes** li
77
The implementation follows a Model-View-ViewModel (MVVM) pattern adapted for Qt/QML:
88

99
### 1. C++ Integration Layer (`src/qml/`)
10-
* **`QuickGraphModel`**: The main controller class. It wraps the internal `DataFlowGraphModel` and exposes high-level operations (add/remove nodes, create connections) to QML.
10+
* **`QuickGraphModel`**: The main controller class. It wraps the internal `DataFlowGraphModel` and exposes high-level operations (add/remove nodes, create connections) to QML. It also manages an **UndoStack** for undo/redo operations.
1111
* **`NodesListModel`**: A `QAbstractListModel` that exposes the nodes in the graph. It provides roles for properties like position, caption, and input/output port counts. Crucially, it exposes the underlying `NodeDelegateModel` as a `QObject*`, allowing QML to bind directly to custom node data (e.g., numbers, text).
1212
* **`ConnectionsListModel`**: A `QAbstractListModel` that tracks active connections, providing source/destination node IDs and port indices.
1313

1414
### 2. QML Components (`resources/qml/`)
1515
* **`NodeGraph.qml`**: The main canvas component.
16-
* Handles **Infinite Panning & Zooming** using a background `MouseArea` and transform/scale logic.
16+
* Handles **Infinite Panning & Zooming** (mouse-centered) using a background `MouseArea` and transform/scale logic.
1717
* Renders a dynamic **Infinite Grid** using a `Canvas` item (avoiding shader compatibility issues).
1818
* Manages the lifecycle of Nodes and Connections using `Repeater`s linked to the C++ models.
1919
* Handles **Connection Drafting**: Implements geometry-based hit-testing to reliably find target nodes/ports under the mouse cursor, ignoring z-order overlays.
20+
* Supports **Marquee Selection** for selecting multiple nodes and connections.
21+
* Handles **Keyboard Shortcuts**: Delete/Backspace/X for deletion, Ctrl+Z for undo, Ctrl+Shift+Z/Ctrl+Y for redo.
2022
* **`Node.qml`**: A generic node shell.
2123
* Displays the node caption and background.
22-
* Generates input/output ports dynamically.
24+
* Generates input/output ports dynamically with **type-based coloring**.
2325
* Uses a `Loader` with a `nodeContentDelegate` to allow users to inject **custom QML content** inside the node (e.g., text fields, images) with full property binding propagation.
24-
* Handles node dragging and position updates, with feedback loops prevented by threshold checks.
26+
* Handles node dragging and position updates, including **group dragging** for selected nodes.
27+
* Shows visual feedback for selected state.
2528
* **`Connection.qml`**:
2629
* Renders connections as smooth cubic Bezier curves using `QtQuick.Shapes`.
27-
* Updates geometry in real-time when linked nodes are moved by monitoring specific `xChanged`/`yChanged` signals.
30+
* Updates geometry in real-time when linked nodes are moved.
31+
* Supports **selection** (click or Ctrl+click) and **hover highlighting**.
32+
* Uses **port type colors** for visual consistency.
33+
* **`NodeGraphStyle.qml`**: A centralized styling component for theming.
34+
* Defines colors, sizes, and appearance for canvas, nodes, ports, connections, and selection.
35+
* Supports **custom themes** (e.g., dark/light mode) by instantiating with different property values.
36+
* Includes port type color mapping for type safety visualization.
2837

2938
## Features Implemented
3039

40+
### Core Functionality
3141
***Hybrid C++/QML Architecture**: Full separation of graph logic (C++) and UI (QML).
3242
***Dynamic Graph Rendering**: Nodes and connections appear and update automatically based on the C++ model.
33-
***Interactive Workspace**: Smooth zooming and panning of the graph canvas.
43+
***Interactive Workspace**: Smooth zooming (mouse-centered) and panning of the graph canvas.
3444
***Node Manipulation**: Drag-and-drop nodes to move them.
3545
***Connection Creation**: Drag from any port to a compatible target port to create a connection.
36-
***Customizable Nodes**: Users can define the look and behavior of specific node types (e.g., "NumberSource") completely in QML.
37-
***Example Application**: `qml_calculator` demonstrates a working calculator where C++ handles the math and QML handles the UI.
46+
***Customizable Nodes**: Users can define the look and behavior of specific node types completely in QML.
47+
48+
### Selection & Editing
49+
***Node Selection**: Click to select, Ctrl+click for additive selection.
50+
***Marquee Selection**: Click and drag on canvas to select multiple nodes and connections.
51+
***Group Dragging**: Drag any selected node to move all selected nodes together.
52+
***Connection Selection**: Click on connections to select them, with hover highlighting.
53+
***Node Deletion**: Delete selected nodes via Delete/Backspace/X keys.
54+
***Connection Deletion**: Delete selected connections via Delete/Backspace/X keys.
55+
***Disconnect by Dragging**: Drag from an input port to disconnect and re-route an existing connection.
56+
57+
### Type Safety & Visual Feedback
58+
***Port Type Colors**: Ports are colored based on their data type (decimal=green, integer=blue, string=orange, boolean=purple).
59+
***Compatibility Highlighting**: During connection dragging, compatible ports are highlighted while incompatible ports are dimmed.
60+
***Connection Type Colors**: Connections inherit the color of their source port type.
61+
62+
### Theming & Styling
63+
***NodeGraphStyle.qml**: Centralized styling with customizable properties for:
64+
* Canvas background and grid colors
65+
* Node background, border, caption, and selection colors
66+
* Port sizes, colors, and hover/active states
67+
* Connection width, hover effects, and selection outline
68+
* Marquee selection appearance
69+
***Theme Switching**: Support for runtime theme changes (e.g., dark/light mode toggle).
70+
***Reactive Styling**: All components respond to style property changes in real-time.
71+
72+
### Undo/Redo
73+
***Full Undo/Redo Support**: All graph operations are undoable:
74+
* Add/Remove nodes
75+
* Add/Remove connections
76+
***Keyboard Shortcuts**: Ctrl+Z (undo), Ctrl+Shift+Z or Ctrl+Y (redo).
77+
***QML API**: `canUndo`/`canRedo` properties and `undo()`/`redo()` methods exposed to QML.
78+
79+
### Focus Management
80+
***Correct Input Focus**: Text fields inside nodes properly receive and release focus.
81+
***Canvas Focus**: Clicking on canvas or nodes removes focus from inputs for keyboard shortcuts to work.
82+
83+
## Example Application
84+
85+
The `qml_calculator` example demonstrates all features:
86+
* Multiple node types: NumberSource, Addition, Subtract, Multiply, Divide, FormatNumber, StringDisplay, IntegerSource, ToInteger, GreaterThan, NumberDisplay, IntegerDisplay, BooleanDisplay
87+
* **Theme Toggle Button**: Switch between dark and light themes at runtime
88+
* **Undo/Redo Buttons**: Visual buttons in toolbar with enabled/disabled state
89+
* **Custom Node Content**: Each node type has its own QML UI (text fields, labels, symbols)
90+
* **Type-Safe Connections**: Connections enforce type compatibility with visual feedback
3891

3992
## Technical Notes
93+
4094
* **Grid Implementation**: The grid is drawn using an HTML5-style `Canvas` API rather than GLSL shaders. This ensures compatibility with Qt 6's RHI (which removed inline OpenGL shaders) while maintaining performance for infinite grid rendering.
4195
* **Z-Ordering & Hit Testing**: Custom geometry-based hit testing is used for connection drafting because the temporary connection line (a `Shape` item) overlays the nodes, blocking standard `childAt` calls.
4296
* **Coordinate Mapping**: All drag operations use `mapToItem`/`mapFromItem` relative to the main `canvas` item to ensure correct positioning regardless of the current pan/zoom state.
97+
* **Reactive Bindings**: Style properties use direct `graph.style` access for proper reactivity when themes change.
98+
* **Undo Commands**: Custom `QUndoCommand` subclasses handle node state serialization for proper undo/redo of node additions and deletions.
4399

44100
## How to Build
45101

@@ -57,22 +113,56 @@ make
57113
./bin/qml_calculator
58114
```
59115

60-
## Next Steps (Roadmap)
61-
62-
To achieve full feature parity with the Widgets-based version, the following features need to be implemented:
63-
64-
1. **Connection Interaction**:
65-
* Ability to select/highlight existing connections.
66-
* Ability to delete connections (e.g., via right-click menu or keyboard shortcut).
67-
2. **Node Deletion**:
68-
* UI mechanism to delete selected nodes.
69-
3. **Selection Model**:
70-
* Support for selecting multiple nodes (marquee selection).
71-
* Visual feedback for selected states.
72-
4. **Undo/Redo Stack**:
73-
* Expose the C++ `UndoStack` to QML to trigger undo/redo actions.
74-
5. **Styling**:
75-
* Expose more style properties (colors, line thickness) to QML for easy theming.
76-
6. **Port Data & Type Safety**:
77-
* Visualize port data types (colors based on type).
78-
* Add visual feedback during connection dragging (highlight compatible ports, dim incompatible ones).
116+
## API Reference
117+
118+
### QuickGraphModel (C++ → QML)
119+
120+
| Property/Method | Type | Description |
121+
|-----------------|------|-------------|
122+
| `nodes` | NodesListModel* | List model of all nodes |
123+
| `connections` | ConnectionsListModel* | List model of all connections |
124+
| `canUndo` | bool | Whether undo is available |
125+
| `canRedo` | bool | Whether redo is available |
126+
| `addNode(nodeType)` | int | Add a node, returns node ID |
127+
| `removeNode(nodeId)` | bool | Remove a node |
128+
| `addConnection(...)` | void | Create a connection |
129+
| `removeConnection(...)` | void | Remove a connection |
130+
| `undo()` | void | Undo last operation |
131+
| `redo()` | void | Redo last undone operation |
132+
133+
### NodeGraph.qml
134+
135+
| Property | Type | Description |
136+
|----------|------|-------------|
137+
| `graphModel` | QuickGraphModel | The C++ model to visualize |
138+
| `style` | NodeGraphStyle | Styling configuration |
139+
| `nodeContentDelegate` | Component | Custom content for nodes |
140+
141+
### NodeGraphStyle.qml
142+
143+
| Category | Properties |
144+
|----------|------------|
145+
| Canvas | `canvasBackground`, `gridMinorLine`, `gridMajorLine`, `gridMinorSpacing`, `gridMajorSpacing` |
146+
| Node | `nodeBackground`, `nodeBorder`, `nodeSelectedBorder`, `nodeBorderWidth`, `nodeSelectedBorderWidth`, `nodeRadius`, `nodeCaptionColor`, `nodeCaptionFontSize`, `nodeCaptionBold`, `nodeMinWidth`, `nodePortSpacing`, `nodeHeaderHeight`, `nodeContentColor` |
147+
| Ports | `portSize`, `portBorderWidth`, `portBorderColor`, `portHighlightBorder`, `portHighlightBorderWidth`, `portHoverScale`, `portActiveScale`, `portDimmedOpacity`, `portTypeColors` |
148+
| Connection | `connectionWidth`, `connectionHoverWidth`, `connectionSelectedWidth`, `connectionSelectionOutline`, `connectionSelectionOutlineWidth`, `draftConnectionWidth`, `draftConnectionColor` |
149+
| Selection | `selectionRectFill`, `selectionRectBorder`, `selectionRectBorderWidth` |
150+
151+
## Keyboard Shortcuts
152+
153+
| Shortcut | Action |
154+
|----------|--------|
155+
| Delete / Backspace / X | Delete selected nodes and connections |
156+
| Ctrl+Z | Undo |
157+
| Ctrl+Shift+Z / Ctrl+Y | Redo |
158+
| Ctrl+Click | Additive selection |
159+
160+
## Future Enhancements
161+
162+
The QML implementation now has feature parity with the Widgets version. Potential future enhancements:
163+
164+
* **Copy/Paste**: Clipboard support for nodes and connections
165+
* **Node Groups**: Collapsible node groups for complex graphs
166+
* **Minimap**: Overview navigation for large graphs
167+
* **Animation**: Smooth transitions for node/connection state changes
168+
* **Touch Support**: Multi-touch gestures for mobile/tablet devices

examples/qml_calculator/main.qml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,20 @@ Window {
6464
onClicked: darkTheme = !darkTheme
6565
}
6666

67-
Rectangle { width: 1; height: 30; color: "#555" }
67+
Rectangle { width: 1; height: 30; color: darkTheme ? "#555" : "#bbb" }
68+
69+
Button {
70+
text: "↶ Undo"
71+
enabled: model.canUndo
72+
onClicked: model.undo()
73+
}
74+
Button {
75+
text: "↷ Redo"
76+
enabled: model.canRedo
77+
onClicked: model.redo()
78+
}
79+
80+
Rectangle { width: 1; height: 30; color: darkTheme ? "#555" : "#bbb" }
6881

6982
Label {
7083
text: "Numbers:"

include/QtNodes/qml/QuickGraphModel.hpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#include "NodesListModel.hpp"
77
#include "ConnectionsListModel.hpp"
88

9+
class QUndoStack;
10+
911
namespace QtNodes {
1012

1113
class DataFlowGraphModel;
@@ -16,6 +18,8 @@ class QuickGraphModel : public QObject
1618
Q_OBJECT
1719
Q_PROPERTY(QtNodes::NodesListModel* nodes READ nodes CONSTANT)
1820
Q_PROPERTY(QtNodes::ConnectionsListModel* connections READ connections CONSTANT)
21+
Q_PROPERTY(bool canUndo READ canUndo NOTIFY undoStateChanged)
22+
Q_PROPERTY(bool canRedo READ canRedo NOTIFY undoStateChanged)
1923

2024
public:
2125
explicit QuickGraphModel(QObject *parent = nullptr);
@@ -38,11 +42,21 @@ class QuickGraphModel : public QObject
3842
Q_INVOKABLE QVariantMap getConnectionAtInput(int nodeId, int portIndex);
3943
Q_INVOKABLE QString getPortDataTypeId(int nodeId, int portType, int portIndex);
4044
Q_INVOKABLE bool connectionPossible(int outNodeId, int outPortIndex, int inNodeId, int inPortIndex);
45+
46+
// Undo/Redo
47+
bool canUndo() const;
48+
bool canRedo() const;
49+
Q_INVOKABLE void undo();
50+
Q_INVOKABLE void redo();
51+
52+
Q_SIGNALS:
53+
void undoStateChanged();
4154

4255
private:
4356
std::shared_ptr<DataFlowGraphModel> _model;
4457
NodesListModel* _nodesList;
4558
ConnectionsListModel* _connectionsList;
59+
QUndoStack* _undoStack;
4660
};
4761

4862
} // namespace QtNodes

resources/qml/NodeGraph.qml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,16 @@ Item {
228228
if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace || event.key === Qt.Key_X) {
229229
deleteSelected()
230230
event.accepted = true
231+
} else if (event.key === Qt.Key_Z && (event.modifiers & Qt.ControlModifier)) {
232+
if (event.modifiers & Qt.ShiftModifier) {
233+
if (graphModel) graphModel.redo()
234+
} else {
235+
if (graphModel) graphModel.undo()
236+
}
237+
event.accepted = true
238+
} else if (event.key === Qt.Key_Y && (event.modifiers & Qt.ControlModifier)) {
239+
if (graphModel) graphModel.redo()
240+
event.accepted = true
231241
}
232242
}
233243

0 commit comments

Comments
 (0)