open-story-teller/story-editor/nodeeditor/src/UndoCommands.cpp
2023-05-28 14:50:48 +02:00

465 lines
12 KiB
C++

#include "UndoCommands.hpp"
#include "BasicGraphicsScene.hpp"
#include "ConnectionGraphicsObject.hpp"
#include "ConnectionIdUtils.hpp"
#include "Definitions.hpp"
#include "NodeGraphicsObject.hpp"
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QMimeData>
#include <QtGui/QClipboard>
#include <QtWidgets/QApplication>
#include <QtWidgets/QGraphicsObject>
#include <typeinfo>
namespace QtNodes {
static QJsonObject serializeSelectedItems(BasicGraphicsScene *scene)
{
QJsonObject serializedScene;
std::unordered_set<NodeId> selectedNodes;
QJsonArray nodesJsonArray;
for (QGraphicsItem *item : scene->selectedItems()) {
if (auto n = qgraphicsitem_cast<NodeGraphicsObject *>(item)) {
QJsonObject obj;
obj["id"] = static_cast<int>(n->nodeId());
nodesJsonArray.append(obj);
selectedNodes.insert(n->nodeId());
}
}
QJsonArray connJsonArray;
for (QGraphicsItem *item : scene->selectedItems()) {
if (auto c = qgraphicsitem_cast<ConnectionGraphicsObject *>(item)) {
auto const &cid = c->connectionId();
if (selectedNodes.count(cid.outNodeId) > 0 && selectedNodes.count(cid.inNodeId) > 0) {
connJsonArray.append(toJson(cid));
}
}
}
serializedScene["nodes"] = nodesJsonArray;
serializedScene["connections"] = connJsonArray;
return serializedScene;
}
static void insertSerializedItems(QJsonObject const &json, BasicGraphicsScene *scene)
{
AbstractGraphModel &graphModel = scene->graphModel();
QJsonArray const &nodesJsonArray = json["nodes"].toArray();
for (QJsonValue node : nodesJsonArray) {
QJsonObject obj = node.toObject();
graphModel.loadNode(obj);
auto id = obj["id"].toInt();
scene->nodeGraphicsObject(id)->setZValue(1.0);
scene->nodeGraphicsObject(id)->setSelected(true);
}
QJsonArray const &connJsonArray = json["connections"].toArray();
for (QJsonValue connection : connJsonArray) {
QJsonObject connJson = connection.toObject();
ConnectionId connId = fromJson(connJson);
// Restore the connection
graphModel.addConnection(connId);
scene->connectionGraphicsObject(connId)->setSelected(true);
}
}
static void deleteSerializedItems(QJsonObject &sceneJson, AbstractGraphModel &graphModel)
{
QJsonArray connectionJsonArray = sceneJson["connections"].toArray();
for (QJsonValueRef connection : connectionJsonArray) {
QJsonObject connJson = connection.toObject();
ConnectionId connId = fromJson(connJson);
graphModel.deleteConnection(connId);
}
QJsonArray nodesJsonArray = sceneJson["nodes"].toArray();
for (QJsonValueRef node : nodesJsonArray) {
QJsonObject nodeJson = node.toObject();
graphModel.deleteNode(nodeJson["id"].toInt());
}
}
static QPointF computeAverageNodePosition(QJsonObject const &sceneJson)
{
QPointF averagePos(0, 0);
QJsonArray nodesJsonArray = sceneJson["nodes"].toArray();
for (QJsonValueRef node : nodesJsonArray) {
QJsonObject nodeJson = node.toObject();
averagePos += QPointF(nodeJson["position"].toObject()["x"].toDouble(),
nodeJson["position"].toObject()["y"].toDouble());
}
averagePos /= static_cast<double>(nodesJsonArray.size());
return averagePos;
}
//-------------------------------------
CreateCommand::CreateCommand(BasicGraphicsScene *scene,
QString const name,
QPointF const &mouseScenePos)
: _scene(scene)
, _sceneJson(QJsonObject())
{
_nodeId = _scene->graphModel().addNode(name);
if (_nodeId != InvalidNodeId) {
_scene->graphModel().setNodeData(_nodeId, NodeRole::Position, mouseScenePos);
} else {
setObsolete(true);
}
}
void CreateCommand::undo()
{
QJsonArray nodesJsonArray;
QJsonObject obj;
obj["id"] = static_cast<int>(_scene->graphModel().nodeData(_nodeId, NodeRole::Id).toInt());
nodesJsonArray.append(obj);
_sceneJson["nodes"] = nodesJsonArray;
_scene->graphModel().deleteNode(_nodeId);
}
void CreateCommand::redo()
{
if (_sceneJson.empty() || _sceneJson["nodes"].toArray().empty())
return;
insertSerializedItems(_sceneJson, _scene);
}
//-------------------------------------
DeleteCommand::DeleteCommand(BasicGraphicsScene *scene)
: _scene(scene)
{
auto &graphModel = _scene->graphModel();
QJsonArray connJsonArray;
// Delete the selected connections first, ensuring that they won't be
// automatically deleted when selected nodes are deleted (deleting a
// node deletes some connections as well)
for (QGraphicsItem *item : _scene->selectedItems()) {
if (auto c = qgraphicsitem_cast<ConnectionGraphicsObject *>(item)) {
auto const &cid = c->connectionId();
connJsonArray.append(toJson(cid));
}
}
QJsonArray nodesJsonArray;
// Delete the nodes; this will delete many of the connections.
// Selected connections were already deleted prior to this loop,
for (QGraphicsItem *item : _scene->selectedItems()) {
if (auto n = qgraphicsitem_cast<NodeGraphicsObject *>(item)) {
// saving connections attached to the selected nodes
for (auto const &cid : graphModel.allConnectionIds(n->nodeId())) {
connJsonArray.append(toJson(cid));
}
QJsonObject obj;
obj["id"] = static_cast<int>(n->nodeId());
nodesJsonArray.append(obj);
}
}
// If nothing is deleted, cancel this operation
if (connJsonArray.isEmpty() && nodesJsonArray.isEmpty())
setObsolete(true);
_sceneJson["nodes"] = nodesJsonArray;
_sceneJson["connections"] = connJsonArray;
}
void DeleteCommand::undo()
{
insertSerializedItems(_sceneJson, _scene);
}
void DeleteCommand::redo()
{
deleteSerializedItems(_sceneJson, _scene->graphModel());
}
//-------------------------------------
void offsetNodeGroup(QJsonObject &sceneJson, QPointF const &diff)
{
QJsonArray nodesJsonArray = sceneJson["nodes"].toArray();
QJsonArray newNodesJsonArray;
for (QJsonValueRef node : nodesJsonArray) {
QJsonObject obj = node.toObject();
QPointF oldPos(obj["position"].toObject()["x"].toDouble(),
obj["position"].toObject()["y"].toDouble());
oldPos += diff;
QJsonObject posJson;
posJson["x"] = oldPos.x();
posJson["y"] = oldPos.y();
obj["position"] = posJson;
newNodesJsonArray.append(obj);
}
sceneJson["nodes"] = newNodesJsonArray;
}
//-------------------------------------
CopyCommand::CopyCommand(BasicGraphicsScene *scene)
{
QJsonObject sceneJson = serializeSelectedItems(scene);
if (sceneJson.empty() || sceneJson["nodes"].toArray().empty()) {
setObsolete(true);
return;
}
QClipboard *clipboard = QApplication::clipboard();
QByteArray const data = QJsonDocument(sceneJson).toJson();
QMimeData *mimeData = new QMimeData();
mimeData->setData("application/qt-nodes-graph", data);
mimeData->setText(data);
clipboard->setMimeData(mimeData);
// Copy command does not have any effective redo/undo operations.
// It copies the data to the clipboard and could be immediately removed
// from the stack.
setObsolete(true);
}
//-------------------------------------
PasteCommand::PasteCommand(BasicGraphicsScene *scene, QPointF const &mouseScenePos)
: _scene(scene)
, _mouseScenePos(mouseScenePos)
{
_newSceneJson = takeSceneJsonFromClipboard();
if (_newSceneJson.empty() || _newSceneJson["nodes"].toArray().empty()) {
setObsolete(true);
return;
}
_newSceneJson = makeNewNodeIdsInScene(_newSceneJson);
QPointF averagePos = computeAverageNodePosition(_newSceneJson);
offsetNodeGroup(_newSceneJson, _mouseScenePos - averagePos);
}
void PasteCommand::undo()
{
deleteSerializedItems(_newSceneJson, _scene->graphModel());
}
void PasteCommand::redo()
{
_scene->clearSelection();
// Ignore if pasted in content does not generate nodes.
try {
insertSerializedItems(_newSceneJson, _scene);
} catch (...) {
// If the paste does not work, delete all selected nodes and connections
// `deleteNode(...)` implicitly removed connections
auto &graphModel = _scene->graphModel();
QJsonArray nodesJsonArray;
for (QGraphicsItem *item : _scene->selectedItems()) {
if (auto n = qgraphicsitem_cast<NodeGraphicsObject *>(item)) {
graphModel.deleteNode(n->nodeId());
}
}
setObsolete(true);
}
}
QJsonObject PasteCommand::takeSceneJsonFromClipboard()
{
QClipboard const *clipboard = QApplication::clipboard();
QMimeData const *mimeData = clipboard->mimeData();
QJsonDocument json;
if (mimeData->hasFormat("application/qt-nodes-graph")) {
json = QJsonDocument::fromJson(mimeData->data("application/qt-nodes-graph"));
} else if (mimeData->hasText()) {
json = QJsonDocument::fromJson(mimeData->text().toUtf8());
}
return json.object();
}
QJsonObject PasteCommand::makeNewNodeIdsInScene(QJsonObject const &sceneJson)
{
AbstractGraphModel &graphModel = _scene->graphModel();
std::unordered_map<NodeId, NodeId> mapNodeIds;
QJsonArray nodesJsonArray = sceneJson["nodes"].toArray();
QJsonArray newNodesJsonArray;
for (QJsonValueRef node : nodesJsonArray) {
QJsonObject nodeJson = node.toObject();
NodeId oldNodeId = nodeJson["id"].toInt();
NodeId newNodeId = graphModel.newNodeId();
mapNodeIds[oldNodeId] = newNodeId;
// Replace NodeId in json
nodeJson["id"] = static_cast<qint64>(newNodeId);
newNodesJsonArray.append(nodeJson);
}
QJsonArray connectionJsonArray = sceneJson["connections"].toArray();
QJsonArray newConnJsonArray;
for (QJsonValueRef connection : connectionJsonArray) {
QJsonObject connJson = connection.toObject();
ConnectionId connId = fromJson(connJson);
ConnectionId newConnId{mapNodeIds[connId.outNodeId],
connId.outPortIndex,
mapNodeIds[connId.inNodeId],
connId.inPortIndex};
newConnJsonArray.append(toJson(newConnId));
}
QJsonObject newSceneJson;
newSceneJson["nodes"] = newNodesJsonArray;
newSceneJson["connections"] = newConnJsonArray;
return newSceneJson;
}
//-------------------------------------
DisconnectCommand::DisconnectCommand(BasicGraphicsScene *scene, ConnectionId const connId)
: _scene(scene)
, _connId(connId)
{
//
}
void DisconnectCommand::undo()
{
_scene->graphModel().addConnection(_connId);
}
void DisconnectCommand::redo()
{
_scene->graphModel().deleteConnection(_connId);
}
//------
ConnectCommand::ConnectCommand(BasicGraphicsScene *scene, ConnectionId const connId)
: _scene(scene)
, _connId(connId)
{
//
}
void ConnectCommand::undo()
{
_scene->graphModel().deleteConnection(_connId);
}
void ConnectCommand::redo()
{
_scene->graphModel().addConnection(_connId);
}
//------
MoveNodeCommand::MoveNodeCommand(BasicGraphicsScene *scene, QPointF const &diff)
: _scene(scene)
, _diff(diff)
{
_selectedNodes.clear();
for (QGraphicsItem *item : _scene->selectedItems()) {
if (auto n = qgraphicsitem_cast<NodeGraphicsObject *>(item)) {
_selectedNodes.insert(n->nodeId());
}
}
}
void MoveNodeCommand::undo()
{
for (auto nodeId : _selectedNodes) {
auto oldPos = _scene->graphModel().nodeData(nodeId, NodeRole::Position).value<QPointF>();
oldPos -= _diff;
_scene->graphModel().setNodeData(nodeId, NodeRole::Position, oldPos);
}
}
void MoveNodeCommand::redo()
{
for (auto nodeId : _selectedNodes) {
auto oldPos = _scene->graphModel().nodeData(nodeId, NodeRole::Position).value<QPointF>();
oldPos += _diff;
_scene->graphModel().setNodeData(nodeId, NodeRole::Position, oldPos);
}
}
int MoveNodeCommand::id() const
{
return static_cast<int>(typeid(MoveNodeCommand).hash_code());
}
bool MoveNodeCommand::mergeWith(QUndoCommand const *c)
{
auto mc = static_cast<MoveNodeCommand const *>(c);
if (_selectedNodes == mc->_selectedNodes) {
_diff += mc->_diff;
return true;
}
return false;
}
} // namespace QtNodes