From f14a301ffaae5ede329199d4a3d2ea3587b4e9d8 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 9 Jul 2021 18:00:01 -0700 Subject: [PATCH] nodeview: updates to complete new context paradigm --- app/node/node.cpp | 92 +++-- app/node/node.h | 53 ++- app/widget/nodeview/nodeview.cpp | 480 ++++++++++++++++++++++---- app/widget/nodeview/nodeview.h | 36 ++ app/widget/nodeview/nodeviewitem.cpp | 122 ++++--- app/widget/nodeview/nodeviewitem.h | 18 +- app/widget/nodeview/nodeviewscene.cpp | 2 +- app/window/mainwindow/mainwindow.cpp | 12 +- 8 files changed, 666 insertions(+), 149 deletions(-) diff --git a/app/node/node.cpp b/app/node/node.cpp index 803b6d43c..51821e820 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -1622,15 +1622,28 @@ void Node::GenerateFrame(FramePtr frame, const GenerateJob &job) const Q_UNUSED(job) } -bool Node::OutputsTo(Node *n, bool recursively) const +bool Node::OutputsTo(Node *n, bool recursively, const OutputConnections &ignore_edges, const OutputConnection &added_edge) const { for (const OutputConnection& conn : output_connections_) { + if (std::find(ignore_edges.cbegin(), ignore_edges.cend(), conn) != ignore_edges.cend()) { + // If this edge is in the "ignore edges" list, skip it + continue; + } + Node* connected = conn.second.node(); if (connected == n) { return true; - } else if (recursively && connected->OutputsTo(n, recursively)) { + } else if (recursively && connected->OutputsTo(n, recursively, ignore_edges, added_edge)) { return true; + } else if (added_edge.first.node() == this) { + Node *proposed_connected = added_edge.second.node(); + + if (proposed_connected == n) { + return true; + } else if (recursively && proposed_connected->OutputsTo(n, recursively, ignore_edges, added_edge)) { + return true; + } } } @@ -1697,7 +1710,7 @@ bool Node::InputsFrom(const QString &id, bool recursively) const return false; } -int Node::GetRoutesTo(Node *n) const +int Node::GetNumberOfRoutesTo(Node *n) const { bool outputs_directly = false; int routes = 0; @@ -1708,7 +1721,7 @@ int Node::GetRoutesTo(Node *n) const if (connected_node == n) { outputs_directly = true; } else { - routes += connected_node->GetRoutesTo(n); + routes += connected_node->GetNumberOfRoutesTo(n); } } @@ -2264,25 +2277,29 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo() commands_.append(set_pos_command); // Get bounding rect - QRectF bounding_rect(position_.x() - 0.5, position_.y() - 0.5, 1, 1); + qreal bounding_rect_sz = 1.0; + qreal bounding_rect_half_sz = bounding_rect_sz * 0.5; + QRectF bounding_rect(position_.x() - bounding_rect_half_sz, position_.y() - bounding_rect_half_sz, bounding_rect_sz, bounding_rect_sz); // Start moving other nodes foreach (Node* surrounding, node_->parent()->nodes()) { - QPointF surrounding_position = node_->parent()->GetNodePosition(surrounding, relative_); - if (bounding_rect.contains(surrounding_position) && surrounding != node_) { - QPointF new_pos = surrounding_position; + if (surrounding != node_) { + QPointF surrounding_position = node_->parent()->GetNodePosition(surrounding, relative_); + if (bounding_rect.contains(surrounding_position)) { + QPointF new_pos = surrounding_position; - qreal move_rate = 0.50; + qreal move_rate = 0.50; - if (surrounding_position.y() < position_.y()) { - move_rate = -move_rate; + if (surrounding_position.y() < position_.y()) { + move_rate = -move_rate; + } + + new_pos.setY(new_pos.y() + move_rate); + + auto sur_command = new NodeSetPositionAndShiftSurroundingsCommand(surrounding, relative_, new_pos, true); + sur_command->redo(); + commands_.append(sur_command); } - - new_pos.setY(new_pos.y() + move_rate); - - auto sur_command = new NodeSetPositionAndShiftSurroundingsCommand(surrounding, relative_, new_pos, true); - sur_command->redo(); - commands_.append(sur_command); } } } else { @@ -2294,20 +2311,19 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo() void NodeSetPositionCommand::redo() { - NodeGraph* graph = node_->parent(); - if (!(added_ = !graph->NodeMapContainsNode(node_, relevant_))) { - old_pos_ = graph->GetNodePosition(node_, relevant_); + graph_ = node_->parent(); + if (!(added_ = !graph_->NodeMapContainsNode(node_, relevant_))) { + old_pos_ = graph_->GetNodePosition(node_, relevant_); } - graph->SetNodePosition(node_, relevant_, pos_); + graph_->SetNodePosition(node_, relevant_, pos_); } void NodeSetPositionCommand::undo() { - NodeGraph* graph = node_->parent(); if (added_) { - graph->RemoveNodePosition(node_, relevant_); + graph_->RemoveNodePosition(node_, relevant_); } else { - graph->SetNodePosition(node_, relevant_, old_pos_); + graph_->SetNodePosition(node_, relevant_, old_pos_); } } @@ -2368,4 +2384,32 @@ void NodeRemovePositionFromContextCommand::undo() } } +void NodeRemovePositionFromAllContextsCommand::redo() +{ + NodeGraph *graph = node_->parent(); + + if (points_.empty()) { + // No points yet, let's see what points we should remove + auto map = graph->GetPositionMap(); + for (auto it=map.cbegin(); it!=map.cend(); it++) { + if (it.value().contains(node_)) { + points_.insert({it.key(), it.value().value(node_)}); + } + } + } + + for (auto it=points_.cbegin(); it!=points_.cend(); it++) { + graph->RemoveNodePosition(node_, it->first); + } +} + +void NodeRemovePositionFromAllContextsCommand::undo() +{ + NodeGraph *graph = node_->parent(); + + for (auto it=points_.crbegin(); it!=points_.crend(); it++) { + graph->SetNodePosition(node_, it->first, it->second); + } +} + } diff --git a/app/node/node.h b/app/node/node.h index e0e6fd986..b730d8024 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -557,7 +557,8 @@ public: * Whether to keep traversing down outputs to find this node (TRUE) or stick to immediate outputs * (FALSE). */ - bool OutputsTo(Node* n, bool recursively) const; + bool OutputsTo(Node* n, bool recursively, const OutputConnections &ignore_edges = OutputConnections(), const OutputConnection &added_edge = OutputConnection()) const; + /** * @brief Same as OutputsTo(Node*), but for a node ID rather than a specific instance. */ @@ -581,7 +582,7 @@ public: /** * @brief Determines how many paths go from this node out to another node */ - int GetRoutesTo(Node* n) const; + int GetNumberOfRoutesTo(Node* n) const; /** * @brief Severs all input and output connections @@ -1327,6 +1328,7 @@ private: QPointF old_pos_; bool added_; bool move_deps_; + NodeGraph *graph_; }; @@ -1375,7 +1377,7 @@ private: class NodeSetPositionAsChildCommand : public UndoCommand { public: - NodeSetPositionAsChildCommand(Node* node, Node* parent, Node *relative, int this_index, int child_count, bool shift_surroundings) : + NodeSetPositionAsChildCommand(Node* node, Node* parent, Node *relative, double this_index, int child_count, bool shift_surroundings) : node_(node), parent_(parent), relative_(relative), @@ -1408,7 +1410,7 @@ private: Node* parent_; Node *relative_; - int this_index_; + double this_index_; int child_count_; bool shift_surroundings_; @@ -1417,6 +1419,25 @@ private: }; +class NodePositionCloseChildGapCommand : public UndoCommand +{ +public: + NodePositionCloseChildGapCommand(Node *parent, void *relative, int remove_index, int child_count, bool shift_surroundings); + + virtual Project * GetRelevantProject() const override + { + return parent_->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + Node *parent_; + +}; + class NodeSetPositionToOffsetOfAnotherNodeCommand : public UndoCommand { public: @@ -1474,6 +1495,30 @@ private: }; +class NodeRemovePositionFromAllContextsCommand : public UndoCommand +{ +public: + NodeRemovePositionFromAllContextsCommand(Node *node) : + node_(node) + { + } + + virtual Project * GetRelevantProject() const override + { + return node_->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + Node *node_; + + std::map points_; + +}; + } #endif // NODE_H diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 6416b0a53..bc943140b 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -149,10 +149,10 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) AddNodePosition(it.key(), n); } } - - // Center on something - CenterOnItemsBoundingRect(); } + + // Center on something + QMetaObject::invokeMethod(this, &NodeView::CenterOnItemsBoundingRect, Qt::QueuedConnection); } } } @@ -171,14 +171,25 @@ void NodeView::DeleteSelected() MultiUndoCommand* command = new MultiUndoCommand(); { + // First remove any selected edges QVector selected_edges = scene_.GetSelectedEdges(); - foreach (NodeViewEdge* edge, selected_edges) { - command->add_child(new NodeEdgeRemoveCommand(edge->output(), edge->input())); + if (!selected_edges.isEmpty()) { + Node::OutputConnections removed_connections(selected_edges.size()); + + for (int i=0; iadd_child(new NodeEdgeRemoveCommand(edge->output(), edge->input())); + removed_connections[i] = {edge->output(), edge->input()}; + } + + // Update contexts + UpdateContextsFromEdgeRemove(command, removed_connections); } } { + // Secondly remove any nodes QVector selected_nodes = scene_.GetSelectedNodes(); // Ensure no nodes are "undeletable" @@ -408,17 +419,69 @@ void NodeView::ZoomOut() void NodeView::keyPressEvent(QKeyEvent *event) { - super::keyPressEvent(event); + switch (event->key()) { + case Qt::Key_Left: + case Qt::Key_Right: + case Qt::Key_Up: + case Qt::Key_Down: + { + if (graph_) { + MultiUndoCommand *pos_command = new MultiUndoCommand(); + foreach (Node *n, selected_nodes_) { + foreach (Node *context, filter_nodes_) { + if (graph_->GetNodesForContext(context).contains(n)) { + QPointF old_pos = graph_->GetNodePosition(n, context); - if (event->key() == Qt::Key_Escape && !attached_items_.isEmpty()) { - DetachItemsFromCursor(); + // Determine one pixel in scene units + double movement_amt = 1.0 / scale_; - // We undo the last action which SHOULD be adding the node - if (paste_command_) { - paste_command_->undo(); - delete paste_command_; - paste_command_ = nullptr; + // Translate to 2D movement + QPointF node_movement; + switch (event->key()) { + case Qt::Key_Left: + node_movement.setX(-movement_amt); + break; + case Qt::Key_Right: + node_movement.setX(movement_amt); + break; + case Qt::Key_Up: + node_movement.setY(-movement_amt); + break; + case Qt::Key_Down: + node_movement.setY(movement_amt); + break; + } + + // Translate from screen units into node units + node_movement = NodeViewItem::ScreenToNodePoint(node_movement, scene_.GetFlowDirection()); + + // Move command + pos_command->add_child(new NodeSetPositionCommand(n, context, old_pos + node_movement, false)); + } + } + } + Core::instance()->undo_stack()->pushIfHasChildren(pos_command); } + break; + } + case Qt::Key_Escape: + if (!attached_items_.isEmpty()) { + DetachItemsFromCursor(); + + // We undo the last action which SHOULD be adding the node + if (paste_command_) { + paste_command_->undo(); + delete paste_command_; + paste_command_ = nullptr; + } + + break; + } + + /* fall through */ + default: + super::keyPressEvent(event); + break; } } @@ -426,19 +489,20 @@ void NodeView::mousePressEvent(QMouseEvent *event) { if (HandPress(event)) return; - QGraphicsItem* item = itemAt(event->pos()); - if (event->button() == Qt::LeftButton) { - NodeViewEdge* edge_item = dynamic_cast(item); - if (edge_item && edge_item->arrow_bounding_rect().contains(mapToScene(event->pos()))) { - create_edge_src_ = scene_.NodeToUIObject(edge_item->output().node()); - create_edge_src_output_ = edge_item->output().output(); - create_edge_ = edge_item; - create_edge_already_exists_ = true; - return; + foreach (NodeViewEdge *edge_item, scene_.edges()) { + if (edge_item->arrow_bounding_rect().contains(mapToScene(event->pos()))) { + create_edge_src_ = scene_.NodeToUIObject(edge_item->output().node()); + create_edge_src_output_ = edge_item->output().output(); + create_edge_ = edge_item; + create_edge_already_exists_ = true; + return; + } } } + QGraphicsItem* item = itemAt(event->pos()); + if (event->button() == Qt::RightButton) { if (!item || !item->isSelected()) { // Qt doesn't do this by default for some reason @@ -623,13 +687,26 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) if (HandRelease(event)) return; if (create_edge_) { + // We are creating a new edge or moving an existing one MultiUndoCommand* command = new MultiUndoCommand(); + Node::OutputConnections removed_edges; + Node::OutputConnection added_edge; + + bool reconnected_to_itself = false; + if (create_edge_already_exists_) { - if (!create_edge_->IsConnected()) { + if (create_edge_dst_input_ == create_edge_->input()) { + reconnected_to_itself = true; + } else { + // We are moving (or removing) an existing edge command->add_child(new NodeEdgeRemoveCommand(create_edge_->output(), create_edge_->input())); + + // Update contexts for edge removal + removed_edges.push_back({create_edge_->output(), create_edge_->input()}); } } else { + // We're creating a new edge, which means this UI object is only temporary delete create_edge_; } @@ -645,15 +722,37 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) create_edge_dst_->setZValue(0); } - if (create_edge_dst_input_.IsValid()) { + NodeInput &creating_input = create_edge_dst_input_; + if (creating_input.IsValid()) { // Make connection - command->add_child(new NodeEdgeAddCommand(NodeOutput(create_edge_src_->GetNode(), create_edge_src_output_), create_edge_dst_input_)); - create_edge_dst_input_.Reset(); + if (!reconnected_to_itself) { + NodeOutput creating_output(create_edge_src_->GetNode(), create_edge_src_output_); + + if (creating_input.IsConnected()) { + Node::OutputConnection existing_edge_to_remove = {creating_input.GetConnectedOutput(), creating_input}; + command->add_child(new NodeEdgeRemoveCommand(existing_edge_to_remove.first, existing_edge_to_remove.second)); + removed_edges.push_back(existing_edge_to_remove); + } + + command->add_child(new NodeEdgeAddCommand(creating_output, creating_input)); + added_edge = {creating_output, creating_input}; + } + + creating_input.Reset(); } create_edge_dst_ = nullptr; } + // Update contexts + if (!removed_edges.empty()) { + UpdateContextsFromEdgeRemove(command, removed_edges); + } + + if (added_edge.first.IsValid()) { + UpdateContextsFromEdgeAdd(command, added_edge, removed_edges); + } + Core::instance()->undo_stack()->pushIfHasChildren(command); return; } @@ -668,44 +767,105 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) command->add_child(paste_command_); paste_command_ = nullptr; } - - if (attached_items_.size() == 1) { - Node* dropping_node = attached_items_.first().item->GetNode(); - - if (drop_edge_) { - // Remove old edge - command->add_child(new NodeEdgeRemoveCommand(drop_edge_->output(), drop_edge_->input())); - - // Place new edges - command->add_child(new NodeEdgeAddCommand(drop_edge_->output(), drop_input_)); - command->add_child(new NodeEdgeAddCommand(dropping_node, drop_edge_->input())); - } - - drop_edge_ = nullptr; - } - - DetachItemsFromCursor(); } - for (auto it=positions_.begin(); it!=positions_.end(); it++) { - NodeViewItem *item = it.key(); - Position &pos_data = it.value(); - QPointF current_item_pos = item->GetNodePosition(); - Node *node = pos_data.node; + { + // If any node positions changed, set them in their contexts now + MultiUndoCommand *set_pos_command = new MultiUndoCommand(); + for (auto it=positions_.begin(); it!=positions_.end(); it++) { + NodeViewItem *item = it.key(); + Position &pos_data = it.value(); + QPointF current_item_pos = item->GetNodePosition(); + Node *node = pos_data.node; - if (pos_data.original_item_pos != current_item_pos) { - QPointF diff = current_item_pos - pos_data.original_item_pos; + if (pos_data.original_item_pos != current_item_pos) { + QPointF diff = current_item_pos - pos_data.original_item_pos; - foreach (Node *context, filter_nodes_) { - if (graph_->ContextContainsNode(node, context)) { - QPointF current_node_pos_in_context = graph_->GetNodePosition(node, context); - current_node_pos_in_context += diff; - command->add_child(new NodeSetPositionCommand(node, context, current_node_pos_in_context, false)); + foreach (Node *context, filter_nodes_) { + if (graph_->ContextContainsNode(node, context)) { + QPointF current_node_pos_in_context = graph_->GetNodePosition(node, context); + current_node_pos_in_context += diff; + set_pos_command->add_child(new NodeSetPositionCommand(node, context, current_node_pos_in_context, false)); + } + } + + pos_data.original_item_pos = current_item_pos; + } + } + if (set_pos_command->child_count()) { + set_pos_command->redo(); + command->add_child(set_pos_command); + } else { + delete set_pos_command; + } + } + + + if (!attached_items_.isEmpty()) { + { + // Dropped attached item onto an edge, connect it between them + MultiUndoCommand *drop_edge_command = new MultiUndoCommand(); + if (attached_items_.size() == 1) { + Node* dropping_node = attached_items_.first().item->GetNode(); + + if (drop_edge_) { + // Remove old edge + drop_edge_command->add_child(new NodeEdgeRemoveCommand(drop_edge_->output(), drop_edge_->input())); + + // Place new edges + drop_edge_command->add_child(new NodeEdgeAddCommand(drop_edge_->output(), drop_input_)); + drop_edge_command->add_child(new NodeEdgeAddCommand(dropping_node, drop_edge_->input())); + } + + drop_edge_ = nullptr; + } + if (drop_edge_command->child_count()) { + drop_edge_command->redo(); + command->add_child(drop_edge_command); + } else { + delete drop_edge_command; + } + } + + { + // Remove from context any nodes that don't specifically output to said context + MultiUndoCommand *remove_pos_command = new MultiUndoCommand(); + + foreach (const AttachedItem &attached, attached_items_) { + MultiUndoCommand *remove_pos_subcommand = new MultiUndoCommand(); + Node *attached_node = scene_.item_map().key(attached.item); + + bool removed = false; + QVector relevant_contexts; + foreach (Node *context, filter_nodes_) { + if (attached_node->OutputsTo(context, true)) { + relevant_contexts.append(context); + } else { + remove_pos_subcommand->add_child(new NodeRemovePositionFromContextCommand(attached_node, context)); + removed = true; + } + } + + if (removed && !relevant_contexts.isEmpty()) { + foreach (Node *relevant, relevant_contexts) { + remove_pos_subcommand->add_child(new NodeSetPositionCommand(attached_node, relevant, GetEstimatedPositionForContext(attached.item, relevant), false)); + } + + remove_pos_command->add_child(remove_pos_subcommand); + } else { + delete remove_pos_subcommand; } } - pos_data.original_item_pos = current_item_pos; + if (remove_pos_command->child_count()) { + remove_pos_command->redo(); + command->add_child(remove_pos_command); + } else { + delete remove_pos_command; + } } + + DetachItemsFromCursor(); } Core::instance()->undo_stack()->pushIfHasChildren(command); @@ -991,19 +1151,29 @@ void NodeView::RemoveNodePosition(Node *node, Node *relative) { if (filter_mode_ == kFilterShowSelective) { if (filter_nodes_.contains(relative)) { - // Determine if any other contexts have this node - bool found = false; + NodeViewItem *item = scene_.item_map().value(node); - foreach (Node *context, filter_nodes_) { - if (graph_->ContextContainsNode(node, context)) { - found = true; - break; + if (item && !item->GetPreventRemoving()) { + // Determine if any other contexts have this node + bool found = false; + + foreach (Node *context, filter_nodes_) { + if (graph_->ContextContainsNode(node, context)) { + found = true; + break; + } } - } - if (!found) { - positions_.remove(scene_.item_map().value(node)); - scene_.RemoveNode(node); + if (!found) { + foreach (const Node::OutputConnection &oc, node->output_connections()) { + scene_.RemoveEdge(oc.first, oc.second); + } + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + scene_.RemoveEdge(it->second, it->first); + } + positions_.remove(item); + scene_.RemoveNode(node); + } } } } @@ -1098,6 +1268,22 @@ void NodeView::ZoomIntoCursorPosition(QWheelEvent *event, double multiplier, con } } +bool NodeView::event(QEvent *event) +{ + if (event->type() == QEvent::ShortcutOverride) { + QKeyEvent *se = static_cast(event); + if (se->key() == Qt::Key_Left + || se->key() == Qt::Key_Right + || se->key() == Qt::Key_Up + || se->key() == Qt::Key_Down) { + se->accept(); + return true; + } + } + + return super::event(event); +} + void NodeView::ZoomFromKeyboard(double multiplier) { QPoint cursor_pos = mapFromGlobal(QCursor::pos()); @@ -1110,6 +1296,149 @@ void NodeView::ZoomFromKeyboard(double multiplier) ZoomIntoCursorPosition(nullptr, multiplier, cursor_pos); } +bool NodeView::DetermineIfNodeIsFloatingInContext(Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge) +{ + // Determines whether `node` outputs to another node in `context` besides `source` + foreach (const Node::OutputConnection &conn, node->output_connections()) { + Node *output_candidate = conn.second.node(); + + if (output_candidate == source) { + continue; + } + + if (graph_->ContextContainsNode(output_candidate, context)) { + if (!output_candidate->OutputsTo(source, true, removed_edges, added_edge)) { + return true; + } + } + } + + return false; +} + +void NodeView::UpdateContextsFromEdgeRemove(MultiUndoCommand *command, const Node::OutputConnections &remove_edges) +{ + // For each edge we remove, determine if we should remove the node from a context as well + foreach (const Node::OutputConnection &edge, remove_edges) { + Node *output_node = edge.first.node(); + QVector contexts_to_remove_from; + int contexts_containing = 0; + + for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { + Node *context = it.key(); + + if (it.value().contains(output_node)) { + bool currently_outputs = output_node->OutputsTo(context, true); + bool will_output_after_operation = output_node->OutputsTo(context, true, remove_edges); + + if (currently_outputs && !will_output_after_operation) { + // Will remove + contexts_to_remove_from.append(context); + } + + contexts_containing++; + } + } + + // Removing from all current contexts, convert to a floating node (i.e. don't remove from the context) + if (contexts_to_remove_from.size() != contexts_containing) { + // Not removing from all contexts, can remove + bool removing_from_all_current_contexts = true; + + foreach (Node *context, filter_nodes_) { + if (graph_->ContextContainsNode(output_node, context)) { + if (!contexts_to_remove_from.contains(context)) { + removing_from_all_current_contexts = false; + break; + } + } + } + + foreach (Node *context, contexts_to_remove_from) { + RecursivelyRemoveFloatingNodeFromContext(command, output_node, context, output_node, remove_edges, Node::OutputConnection(), removing_from_all_current_contexts); + } + } + } +} + +void NodeView::RecursivelyRemoveFloatingNodeFromContext(MultiUndoCommand *command, Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge, bool prevent_removing) +{ + if (prevent_removing) { + command->add_child(new NodeViewItemPreventRemovingCommand(this, node, true)); + } + + command->add_child(new NodeRemovePositionFromContextCommand(node, context)); + + // Remove any dependency from the context that's also floating + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + Node *dependency = it->second.node(); + + // Determine if this node happens to output to anything else in the context (which may be + // another floating node that won't be removed by this operation) + if (!DetermineIfNodeIsFloatingInContext(dependency, context, source, removed_edges, added_edge)) { + RecursivelyRemoveFloatingNodeFromContext(command, dependency, context, source, removed_edges, added_edge, prevent_removing); + } + } +} + +void NodeView::RecursivelyAddNodeToContext(MultiUndoCommand *command, Node *node, Node *context) +{ + command->add_child(new NodeSetPositionCommand(node, context, GetEstimatedPositionForContext(scene_.item_map().value(node), context), false)); + + // Add dependency + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + Node *dependency = it->second.node(); + RecursivelyAddNodeToContext(command, dependency, context); + } +} + +void NodeView::UpdateContextsFromEdgeAdd(MultiUndoCommand *command, const Node::OutputConnection &added_edge, const Node::OutputConnections &removed_edges) +{ + // Determine if node currently does NOT output to a context that it WILL after this operation + QVector contexts_to_add_to; + Node *connecting_node = added_edge.first.node(); + Node *input_node = added_edge.second.node(); + for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { + if (it.value().contains(input_node)) { + contexts_to_add_to.append(it.key()); + } + } + + if (!contexts_to_add_to.isEmpty()) { + // Determine whether the node is currently "floating", i.e. it outputs to none of the contexts + // that it currently belongs to. If so, we will take ownership of it with this node. + bool node_is_floating = true; + QVector current_contexts; + for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { + if (it.value().contains(connecting_node)) { + if (connecting_node->OutputsTo(it.key(), true, removed_edges)) { + node_is_floating = false; + break; + } else { + current_contexts.append(it.key()); + } + } + } + + if (node_is_floating) { + // This action will unfloat this node, so remove it from all current contexts + foreach (Node *context, current_contexts) { + RecursivelyRemoveFloatingNodeFromContext(command, connecting_node, context, connecting_node, removed_edges, added_edge, false); + } + } + + // Add nodes to contexts + foreach (Node *context, contexts_to_add_to) { + RecursivelyAddNodeToContext(command, connecting_node, context); + } + } +} + +QPointF NodeView::GetEstimatedPositionForContext(NodeViewItem *item, Node *context) const +{ + return item->GetNodePosition() - context_offsets_.value(context); +} + NodeView::NodeViewAttachNodesToCursor::NodeViewAttachNodesToCursor(NodeView *view, const QVector &nodes) : view_(view), nodes_(nodes) @@ -1132,4 +1461,23 @@ Project *NodeView::NodeViewAttachNodesToCursor::GetRelevantProject() const return dynamic_cast(view_->graph_); } +void NodeView::NodeViewItemPreventRemovingCommand::redo() +{ + NodeViewItem *item = view_->scene_.item_map().value(node_); + + if (item) { + old_prevent_removing_ = item->GetPreventRemoving(); + item->SetPreventRemoving(new_prevent_removing_); + } +} + +void NodeView::NodeViewItemPreventRemovingCommand::undo() +{ + NodeViewItem *item = view_->scene_.item_map().value(node_); + + if (item) { + item->SetPreventRemoving(old_prevent_removing_); + } +} + } diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 05b4f8ab4..1ba86998c 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -91,6 +91,8 @@ protected: virtual void ZoomIntoCursorPosition(QWheelEvent *event, double multiplier, const QPointF &cursor_pos) override; + virtual bool event(QEvent *event) override; + private: void AttachNodesToCursor(const QVector &nodes); @@ -107,6 +109,14 @@ private: void ZoomFromKeyboard(double multiplier); + bool DetermineIfNodeIsFloatingInContext(Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge); + void UpdateContextsFromEdgeRemove(MultiUndoCommand *command, const Node::OutputConnections &remove_edges); + void UpdateContextsFromEdgeAdd(MultiUndoCommand *command, const Node::OutputConnection &added_edge, const Node::OutputConnections &removed_edges = Node::OutputConnections()); + void RecursivelyAddNodeToContext(MultiUndoCommand *command, Node *node, Node *context); + void RecursivelyRemoveFloatingNodeFromContext(MultiUndoCommand *command, Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge, bool prevent_removing); + + QPointF GetEstimatedPositionForContext(NodeViewItem *item, Node *context) const; + class NodeViewAttachNodesToCursor : public UndoCommand { public: @@ -132,6 +142,32 @@ private: QPointF original_pos; }; + class NodeViewItemPreventRemovingCommand : public UndoCommand + { + public: + NodeViewItemPreventRemovingCommand(NodeView *view, Node *node, bool prevent_removing) : + view_(view), + node_(node), + new_prevent_removing_(prevent_removing) + {} + + virtual void redo() override; + + virtual void undo() override; + + virtual Project * GetRelevantProject() const override + { + return node_->project(); + } + + private: + NodeView *view_; + Node *node_; + bool new_prevent_removing_; + bool old_prevent_removing_; + + }; + QList attached_items_; NodeViewEdge* drop_edge_; diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index 7224edc4d..7978d6c32 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -45,7 +45,8 @@ NodeViewItem::NodeViewItem(QGraphicsItem *parent) : expanded_(false), hide_titlebar_(false), highlighted_index_(-1), - flow_dir_(NodeViewCommon::kLeftToRight) + flow_dir_(NodeViewCommon::kLeftToRight), + prevent_removing_(false) { // Set flags for this widget setFlag(QGraphicsItem::ItemIsMovable); @@ -68,31 +69,7 @@ NodeViewItem::NodeViewItem(QGraphicsItem *parent) : QPointF NodeViewItem::GetNodePosition() const { - QPointF node_pos; - - qreal adjusted_x = pos().x() / DefaultItemHorizontalPadding(); - qreal adjusted_y = pos().y() / DefaultItemVerticalPadding(); - - switch (flow_dir_) { - case NodeViewCommon::kLeftToRight: - node_pos.setX(adjusted_x); - node_pos.setY(adjusted_y); - break; - case NodeViewCommon::kRightToLeft: - node_pos.setX(-adjusted_x); - node_pos.setY(adjusted_y); - break; - case NodeViewCommon::kTopToBottom: - node_pos.setX(adjusted_y); - node_pos.setY(adjusted_x); - break; - case NodeViewCommon::kBottomToTop: - node_pos.setX(-adjusted_y); - node_pos.setY(adjusted_x); - break; - } - - return node_pos; + return ScreenToNodePoint(pos(), flow_dir_); } void NodeViewItem::SetNodePosition(const QPointF &pos) @@ -122,24 +99,88 @@ int NodeViewItem::DefaultItemBorder() return QFontMetrics(QFont()).height() / 12; } -qreal NodeViewItem::DefaultItemHorizontalPadding() const +QPointF NodeViewItem::NodeToScreenPoint(QPointF p, NodeViewCommon::FlowDirection direction) { - if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) { + switch (direction) { + case NodeViewCommon::kLeftToRight: + // NodeGraphs are always left-to-right internally, no need to translate + break; + case NodeViewCommon::kRightToLeft: + // Invert X value + p.setX(-p.x()); + break; + case NodeViewCommon::kTopToBottom: + // Swap X/Y + p = QPointF(p.y(), p.x()); + break; + case NodeViewCommon::kBottomToTop: + // Swap X/Y and invert Y + p = QPointF(p.y(), -p.x()); + break; + } + + // Multiply by item sizes for this direction + p.setX(p.x() * DefaultItemHorizontalPadding(direction)); + p.setY(p.y() * DefaultItemVerticalPadding(direction)); + + return p; +} + +QPointF NodeViewItem::ScreenToNodePoint(QPointF p, NodeViewCommon::FlowDirection direction) +{ + // Divide by item sizes for this direction + p.setX(p.x() / DefaultItemHorizontalPadding(direction)); + p.setY(p.y() / DefaultItemVerticalPadding(direction)); + + switch (direction) { + case NodeViewCommon::kLeftToRight: + // NodeGraphs are always left-to-right internally, no need to translate + break; + case NodeViewCommon::kRightToLeft: + // Invert X value + p.setX(-p.x()); + break; + case NodeViewCommon::kTopToBottom: + // Swap X/Y + p = QPointF(p.y(), p.x()); + break; + case NodeViewCommon::kBottomToTop: + // Swap X/Y and invert Y + p = QPointF(-p.y(), p.x()); + break; + } + + return p; +} + +qreal NodeViewItem::DefaultItemHorizontalPadding(NodeViewCommon::FlowDirection dir) +{ + if (NodeViewCommon::GetFlowOrientation(dir) == Qt::Horizontal) { return DefaultItemWidth() * 1.5; } else { return DefaultItemWidth() * 1.25; } } -qreal NodeViewItem::DefaultItemVerticalPadding() const +qreal NodeViewItem::DefaultItemVerticalPadding(NodeViewCommon::FlowDirection dir) { - if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) { + if (NodeViewCommon::GetFlowOrientation(dir) == Qt::Horizontal) { return DefaultItemHeight() * 1.5; } else { return DefaultItemHeight() * 2.0; } } +qreal NodeViewItem::DefaultItemHorizontalPadding() const +{ + return DefaultItemHorizontalPadding(flow_dir_); +} + +qreal NodeViewItem::DefaultItemVerticalPadding() const +{ + return DefaultItemVerticalPadding(flow_dir_); +} + void NodeViewItem::AddEdge(NodeViewEdge *edge) { edges_.append(edge); @@ -479,26 +520,7 @@ QPointF NodeViewItem::GetInputPointInternal(int index, const QPointF& source_pos void NodeViewItem::UpdateNodePosition() { - const QPointF &pos = cached_node_pos_; - - switch (flow_dir_) { - case NodeViewCommon::kLeftToRight: - setPos(pos.x() * DefaultItemHorizontalPadding(), - pos.y() * DefaultItemVerticalPadding()); - break; - case NodeViewCommon::kRightToLeft: - setPos(-pos.x() * DefaultItemHorizontalPadding(), - pos.y() * DefaultItemVerticalPadding()); - break; - case NodeViewCommon::kTopToBottom: - setPos(pos.y() * DefaultItemHorizontalPadding(), - pos.x() * DefaultItemVerticalPadding()); - break; - case NodeViewCommon::kBottomToTop: - setPos(pos.y() * DefaultItemHorizontalPadding(), - -pos.x() * DefaultItemVerticalPadding()); - break; - } + setPos(NodeToScreenPoint(cached_node_pos_, flow_dir_)); } } diff --git a/app/widget/nodeview/nodeviewitem.h b/app/widget/nodeview/nodeviewitem.h index 1edab3aa5..ffe3e0859 100644 --- a/app/widget/nodeview/nodeviewitem.h +++ b/app/widget/nodeview/nodeviewitem.h @@ -95,8 +95,12 @@ public: static int DefaultItemBorder(); - qreal DefaultItemHorizontalPadding() const; + static QPointF NodeToScreenPoint(QPointF p, NodeViewCommon::FlowDirection direction); + static QPointF ScreenToNodePoint(QPointF p, NodeViewCommon::FlowDirection direction); + static qreal DefaultItemHorizontalPadding(NodeViewCommon::FlowDirection dir); + static qreal DefaultItemVerticalPadding(NodeViewCommon::FlowDirection dir); + qreal DefaultItemHorizontalPadding() const; qreal DefaultItemVerticalPadding() const; void AddEdge(NodeViewEdge* edge); @@ -111,6 +115,16 @@ public: void SetHighlightedIndex(int index); + void SetPreventRemoving(bool e) + { + prevent_removing_ = e; + } + + bool GetPreventRemoving() const + { + return prevent_removing_; + } + protected: virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; @@ -174,6 +188,8 @@ private: QPointF cached_node_pos_; + bool prevent_removing_; + }; } diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index 267a3b164..4e1ca2207 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -200,7 +200,7 @@ int NodeViewScene::DetermineWeight(Node *n) int weight = 0; foreach (Node* i, inputs) { - if (i->GetRoutesTo(n) == 1) { + if (i->GetNumberOfRoutesTo(n) == 1) { weight += DetermineWeight(i); } } diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 011fee537..7db76d1ba 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -754,9 +754,15 @@ void MainWindow::FocusedPanelChanged(PanelWidget *panel) // Signal project panel focus UpdateTitle(); if (project->project()) { - QVector context = {project->project()->root()}; - node_panel_->SetGraph(project->project(), context); - node_panel_->SelectWithDependencies(context, false); + node_panel_->SetGraph(project->project(), {project->project()->root()}); + + bool center = true; + auto selected = project->SelectedItems(); + if (selected.isEmpty()) { + selected.append(project->project()->root()); + center = false; + } + node_panel_->Select(selected, center); } } }