Skip to content

Commit 70558c8

Browse files
committed
Implement Duplication command for undo/redo
1 parent e56d9a9 commit 70558c8

File tree

6 files changed

+306
-24
lines changed

6 files changed

+306
-24
lines changed

docs/classes.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,24 @@ Basic Classes
6161
.. doxygenclass:: QtNodes::NodeConnectionInteraction
6262
:members:
6363

64+
Undo Redo
65+
---------
66+
67+
.. doxygenclass:: QtNodes::DeleteCommand
68+
:members:
69+
70+
.. doxygenclass:: QtNodes::DuplicateCommand
71+
:members:
72+
73+
.. doxygenclass:: QtNodes::DisconnectCommand
74+
:members:
75+
76+
.. doxygenclass:: QtNodes::ConnectCommand
77+
:members:
78+
79+
.. doxygenclass:: QtNodes::MoveNodeCommand
80+
:members:
81+
6482
Dataflow Classes
6583
----------------
6684

include/QtNodes/internal/GraphicsView.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ public Q_SLOTS:
4343
void
4444
onDeleteSelectedObjects();
4545

46+
void
47+
onDuplicateSelectedObjects();
48+
4649
protected:
4750
void
4851
contextMenuEvent(QContextMenuEvent *event) override;
@@ -75,6 +78,7 @@ public Q_SLOTS:
7578
private:
7679
QAction* _clearSelectionAction;
7780
QAction* _deleteSelectionAction;
81+
QAction* _duplicateSelectionAction;
7882

7983
QPointF _clickPos;
8084
};

src/GraphicsView.cpp

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -77,34 +77,50 @@ deleteSelectionAction() const
7777

7878
void
7979
GraphicsView::
80-
setScene(BasicGraphicsScene *scene)
80+
setScene(BasicGraphicsScene * scene)
8181
{
8282
QGraphicsView::setScene(scene);
8383

84-
// setup actions
85-
delete _clearSelectionAction;
86-
_clearSelectionAction = new QAction(QStringLiteral("Clear Selection"), this);
87-
_clearSelectionAction->setShortcut(Qt::Key_Escape);
88-
89-
connect(_clearSelectionAction,
90-
&QAction::triggered,
91-
scene,
92-
&QGraphicsScene::clearSelection);
84+
{
85+
// setup actions
86+
delete _clearSelectionAction;
87+
_clearSelectionAction = new QAction(QStringLiteral("Clear Selection"), this);
88+
_clearSelectionAction->setShortcut(Qt::Key_Escape);
9389

94-
addAction(_clearSelectionAction);
90+
connect(_clearSelectionAction,
91+
&QAction::triggered,
92+
scene,
93+
&QGraphicsScene::clearSelection);
9594

95+
addAction(_clearSelectionAction);
96+
}
9697

9798

98-
delete _deleteSelectionAction;
99-
_deleteSelectionAction = new QAction(QStringLiteral("Delete Selection"), this);
100-
_deleteSelectionAction->setShortcutContext(Qt::ShortcutContext::WidgetShortcut);
101-
_deleteSelectionAction->setShortcut(QKeySequence(QKeySequence::Delete));
102-
connect(_deleteSelectionAction,
103-
&QAction::triggered,
104-
this,
105-
&GraphicsView::onDeleteSelectedObjects);
99+
{
100+
delete _deleteSelectionAction;
101+
_deleteSelectionAction = new QAction(QStringLiteral("Delete Selection"), this);
102+
_deleteSelectionAction->setShortcutContext(Qt::ShortcutContext::WidgetShortcut);
103+
_deleteSelectionAction->setShortcut(QKeySequence(QKeySequence::Delete));
104+
connect(_deleteSelectionAction,
105+
&QAction::triggered,
106+
this,
107+
&GraphicsView::onDeleteSelectedObjects);
108+
109+
addAction(_deleteSelectionAction);
110+
}
106111

107-
addAction(_deleteSelectionAction);
112+
{
113+
delete _duplicateSelectionAction;
114+
_duplicateSelectionAction = new QAction(QStringLiteral("Duplicate Selection"), this);
115+
_duplicateSelectionAction->setShortcutContext(Qt::ShortcutContext::WidgetShortcut);
116+
_duplicateSelectionAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D));
117+
connect(_duplicateSelectionAction,
118+
&QAction::triggered,
119+
this,
120+
&GraphicsView::onDuplicateSelectedObjects);
121+
122+
addAction(_duplicateSelectionAction);
123+
}
108124

109125

110126
auto undoAction = scene->undoStack().createUndoAction(this, tr("&Undo"));
@@ -217,7 +233,23 @@ onDeleteSelectedObjects()
217233

218234
void
219235
GraphicsView::
220-
keyPressEvent(QKeyEvent *event)
236+
onDuplicateSelectedObjects()
237+
{
238+
QPoint origin = mapFromGlobal(QCursor::pos());
239+
240+
QRect const viewRect = rect();
241+
if (!viewRect.contains(origin))
242+
origin = viewRect.center();
243+
244+
QPointF relativeOrigin = mapToScene(origin);
245+
246+
nodeScene()->undoStack().push(new DuplicateCommand(nodeScene(), relativeOrigin));
247+
}
248+
249+
250+
void
251+
GraphicsView::
252+
keyPressEvent(QKeyEvent * event)
221253
{
222254
switch (event->key())
223255
{

src/NodeGraphicsObject.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,8 @@ hoverLeaveEvent(QGraphicsSceneHoverEvent* event)
404404
{
405405
_nodeState.setHovered(false);
406406

407+
setZValue(0.0);
408+
407409
update();
408410

409411
Q_EMIT nodeScene()->nodeHoverLeft(_nodeId);

src/UndoCommands.cpp

Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,24 +91,208 @@ redo()
9191
{
9292
auto & graphModel = _scene->graphModel();
9393

94+
QJsonArray connectionJsonArray = _sceneJson["connections"].toArray();
95+
96+
for (QJsonValueRef connection : connectionJsonArray)
97+
{
98+
QJsonObject connJson = connection.toObject();
99+
100+
ConnectionId connId = fromJson(connJson);
101+
102+
graphModel.deleteConnection(connId);
103+
}
104+
105+
94106
QJsonArray nodesJsonArray = _sceneJson["nodes"].toArray();
95107

96108
for (QJsonValueRef node : nodesJsonArray)
97109
{
98110
QJsonObject nodeJson = node.toObject();
99111
graphModel.deleteNode(static_cast<NodeId>(nodeJson["id"].toInt()));
100112
}
113+
}
114+
115+
116+
//-------------------------------------
117+
118+
119+
DuplicateCommand::
120+
DuplicateCommand(BasicGraphicsScene* scene,
121+
QPointF const & mouseScenePos)
122+
: _scene(scene)
123+
, _mouseScenePos(mouseScenePos)
124+
{
125+
auto & graphModel = _scene->graphModel();
126+
127+
std::unordered_set<NodeId> selectedNodes;
128+
129+
QJsonArray nodesJsonArray;
130+
// Delete the nodes; this will delete many of the connections.
131+
// Selected connections were already deleted prior to this loop,
132+
for (QGraphicsItem * item : _scene->selectedItems())
133+
{
134+
if (auto n = qgraphicsitem_cast<NodeGraphicsObject*>(item))
135+
{
136+
nodesJsonArray.append(graphModel.saveNode(n->nodeId()));
137+
138+
selectedNodes.insert(n->nodeId());
139+
}
140+
}
141+
142+
QJsonArray connJsonArray;
143+
// Delete the selected connections first, ensuring that they won't be
144+
// automatically deleted when selected nodes are deleted (deleting a
145+
// node deletes some connections as well)
146+
for (QGraphicsItem * item : _scene->selectedItems())
147+
{
148+
if (auto c = qgraphicsitem_cast<ConnectionGraphicsObject*>(item))
149+
{
150+
auto const& cid = c->connectionId();
151+
152+
if (selectedNodes.count(cid.outNodeId) > 0 &&
153+
selectedNodes.count(cid.inNodeId) > 0)
154+
{
155+
connJsonArray.append(toJson(cid));
156+
}
157+
}
158+
}
159+
160+
161+
_sceneJson["nodes"] = nodesJsonArray;
162+
_sceneJson["connections"] = connJsonArray;
163+
}
164+
165+
166+
void
167+
DuplicateCommand::
168+
undo()
169+
{
170+
auto & graphModel = _scene->graphModel();
171+
172+
QJsonArray connectionJsonArray = _newSceneJson["connections"].toArray();
173+
174+
for (QJsonValueRef connection : connectionJsonArray)
175+
{
176+
QJsonObject connJson = connection.toObject();
177+
178+
ConnectionId connId = fromJson(connJson);
179+
180+
graphModel.deleteConnection(connId);
181+
}
182+
183+
QJsonArray nodesJsonArray = _newSceneJson["nodes"].toArray();
184+
185+
for (QJsonValueRef node : nodesJsonArray)
186+
{
187+
QJsonObject nodeJson = node.toObject();
188+
graphModel.deleteNode(static_cast<NodeId>(nodeJson["id"].toInt()));
189+
}
190+
}
191+
192+
193+
void
194+
DuplicateCommand::
195+
redo()
196+
{
197+
_scene->clearSelection();
198+
199+
auto & graphModel = _scene->graphModel();
200+
201+
std::unordered_map<NodeId, NodeId> mapNodeIds;
202+
203+
QPointF averagePos;
204+
205+
QJsonArray nodesJsonArray = _sceneJson["nodes"].toArray();
206+
207+
// Cycle below replaces the NodeId with the new generated value
208+
// and computes an average position of the old selected node group
209+
210+
QJsonArray newNodesJsonArray;
211+
for (QJsonValueRef node : nodesJsonArray)
212+
{
213+
QJsonObject nodeJson = node.toObject();
214+
215+
NodeId oldNodeId = nodeJson["id"].toInt();
216+
217+
averagePos +=
218+
QPointF(nodeJson["position"].toObject()["x"].toDouble(),
219+
nodeJson["position"].toObject()["y"].toDouble());
220+
221+
NodeId newNodeId = graphModel.newNodeId();
222+
223+
mapNodeIds[oldNodeId] = newNodeId;
224+
225+
// Replace NodeId in json
226+
nodeJson["id"] = static_cast<qint64>(newNodeId);
227+
228+
newNodesJsonArray.append(nodeJson);
229+
}
230+
231+
averagePos /= static_cast<double>(nodesJsonArray.size());
232+
233+
234+
// The cycle below replaces old NodeIds in connections with the new values
101235

102236
QJsonArray connectionJsonArray = _sceneJson["connections"].toArray();
103237

238+
QJsonArray newConnJsonArray;
104239
for (QJsonValueRef connection : connectionJsonArray)
105240
{
106241
QJsonObject connJson = connection.toObject();
107-
graphModel.deleteConnection(fromJson(connJson));
242+
243+
ConnectionId connId = fromJson(connJson);
244+
245+
ConnectionId newConnId{mapNodeIds[connId.outNodeId],
246+
connId.outPortIndex,
247+
mapNodeIds[connId.inNodeId],
248+
connId.inPortIndex};
249+
250+
251+
newConnJsonArray.append(toJson(newConnId));
252+
}
253+
254+
// Cycle below offsets the new node group to the position of the mouse cursor
255+
QPointF const diff = _mouseScenePos - averagePos;
256+
257+
for (QJsonValueRef node : newNodesJsonArray)
258+
{
259+
QJsonObject obj = node.toObject();
260+
NodeId const id = obj["id"].toInt();
261+
262+
QPointF oldPos(obj["position"].toObject()["x"].toDouble(),
263+
obj["position"].toObject()["y"].toDouble());
264+
265+
oldPos += diff;
266+
267+
QJsonObject posJson;
268+
posJson["x"] = oldPos.x();
269+
posJson["y"] = oldPos.y();
270+
obj["position"] = posJson;
271+
272+
273+
graphModel.loadNode(obj);
274+
275+
_scene->nodeGraphicsObject(id)->setZValue(1.0);
276+
}
277+
278+
for (QJsonValueRef connection : newConnJsonArray)
279+
{
280+
QJsonObject connJson = connection.toObject();
281+
282+
ConnectionId connId = fromJson(connJson);
283+
284+
// Restore the connection
285+
graphModel.addConnection(connId);
108286
}
287+
288+
289+
_newSceneJson["nodes"] = newNodesJsonArray;
290+
_newSceneJson["connections"] = newConnJsonArray;
109291
}
110292

111293

294+
//-------------------------------------
295+
112296

113297

114298
DisconnectCommand::

0 commit comments

Comments
 (0)