/*** Olive - Non-Linear Video Editor Copyright (C) 2021 Olive Team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***/ #include "nodeview.h" #include #include #include #include #include "core.h" #include "nodeviewundo.h" #include "node/factory.h" #include "node/traverser.h" #include "widget/menu/menushared.h" #include "widget/timebased/timebasedview.h" #define super HandMovableView namespace olive { const double NodeView::kMinimumScale = 0.1; NodeView::NodeView(QWidget *parent) : HandMovableView(parent), graph_(nullptr), drop_edge_(nullptr), create_edge_(nullptr), create_edge_dst_(nullptr), create_edge_dst_temp_expanded_(false), paste_command_(nullptr), filter_mode_(kFilterShowSelective), scale_(1.0) { setScene(&scene_); SetDefaultDragMode(RubberBandDrag); setContextMenuPolicy(Qt::CustomContextMenu); setMouseTracking(true); setRenderHint(QPainter::Antialiasing); setViewportUpdateMode(FullViewportUpdate); connect(this, &NodeView::customContextMenuRequested, this, &NodeView::ShowContextMenu); ConnectSelectionChangedSignal(); SetFlowDirection(NodeViewCommon::kTopToBottom); UpdateSceneBoundingRect(); connect(&scene_, &QGraphicsScene::changed, this, &NodeView::UpdateSceneBoundingRect); minimap_ = new NodeViewMiniMap(&scene_, this); minimap_->show(); connect(minimap_, &NodeViewMiniMap::Resized, this, &NodeView::RepositionMiniMap); connect(minimap_, &NodeViewMiniMap::MoveToScenePoint, this, &NodeView::MoveToScenePoint); connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &NodeView::UpdateViewportOnMiniMap); connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &NodeView::UpdateViewportOnMiniMap); } NodeView::~NodeView() { // Unset the current graph ClearGraph(); } void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) { bool graph_changed = graph_ != graph; bool context_changed = filter_nodes_ != nodes; if (graph_changed || context_changed) { // Clear nodes if necessary bool refresh_required = (graph_changed && filter_mode_ == kFilterShowAll) || (context_changed && filter_mode_ == kFilterShowSelective); bool nodes_visible = (graph && filter_mode_ == kFilterShowAll) || (!nodes.isEmpty() && filter_mode_ == kFilterShowSelective); if (refresh_required) { DeselectAll(); positions_.clear(); scene_.clear(); } // Handle graph change if (graph_changed) { if (graph_) { // Disconnect from current graph disconnect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); disconnect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); disconnect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); disconnect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); disconnect(graph_, &NodeGraph::NodePositionAdded, this, &NodeView::AddNodePosition); disconnect(graph_, &NodeGraph::NodePositionRemoved, this, &NodeView::RemoveNodePosition); } graph_ = graph; if (graph_) { // Connect to new graph connect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); connect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); connect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); connect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); connect(graph_, &NodeGraph::NodePositionAdded, this, &NodeView::AddNodePosition); connect(graph_, &NodeGraph::NodePositionRemoved, this, &NodeView::RemoveNodePosition); } } if (context_changed) { filter_nodes_ = nodes; } if (refresh_required && nodes_visible) { if (filter_mode_ == kFilterShowAll) { /* = WIP = for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { Node *context = it.key(); const NodeGraph::PositionMap &map = it.value(); for (auto jt=map.cbegin(); jt!=map.cend(); jt++) { } }*/ } else { // Reserve an arbitrary number to reduce the amount of reallocations qreal last_offset = 0; int additional_spacing = 0; foreach (Node *n, filter_nodes_) { const NodeGraph::PositionMap &map = graph_->GetNodesForContext(n); // First determine the total "height" of this graph and how much we need to offset it qreal top = 0; qreal bottom = 0; for (auto it=map.cbegin(); it!=map.cend(); it++) { const QPointF &node_pos_in_context = it.value(); top = qMin(node_pos_in_context.y(), top); bottom = qMax(node_pos_in_context.y(), bottom); } last_offset += (additional_spacing + (bottom - top)); additional_spacing = 1; context_offsets_.insert(n, QPointF(0, last_offset)); // Finally add all nodes for (auto it=map.cbegin(); it!=map.cend(); it++) { AddNodePosition(it.key(), n); } } } // Center on something QMetaObject::invokeMethod(this, &NodeView::CenterOnItemsBoundingRect, Qt::QueuedConnection); } } } void NodeView::ClearGraph() { SetGraph(nullptr, QVector()); } void NodeView::DeleteSelected() { if (!graph_) { return; } MultiUndoCommand* command = new MultiUndoCommand(); { // First remove any selected edges QVector selected_edges = scene_.GetSelectedEdges(); 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" for (int i=0;iCanBeDeleted()) { selected_nodes.removeAt(i); i--; } } if (!selected_nodes.isEmpty()) { foreach (Node* node, selected_nodes) { command->add_child(new NodeRemoveAndDisconnectCommand(node)); } } } Core::instance()->undo_stack()->pushIfHasChildren(command); } void NodeView::SelectAll() { if (!graph_) { return; } // Optimization: rather than respond to every single item being selected, ignore the signal and // then handle them all at the end. DisconnectSelectionChangedSignal(); scene_.SelectAll(); ConnectSelectionChangedSignal(); if (selected_nodes_.isEmpty()) { // No nodes were selected before so we can just emit them all emit NodesSelected(graph_->nodes()); } else { // We have to determine the difference QVector new_selection; foreach (Node* n, graph_->nodes()) { if (!selected_nodes_.contains(n)) { new_selection.append(n); } } emit NodesSelected(new_selection); } // Just add everything to the selected nodes list selected_nodes_ = graph_->nodes(); } void NodeView::DeselectAll() { if (!graph_ || selected_nodes_.isEmpty()) { return; } // Optimization: rather than respond to every single item being selected, ignore the signal and // then handle them all at the end. DisconnectSelectionChangedSignal(); scene_.DeselectAll(); ConnectSelectionChangedSignal(); // Just emit all the nodes that are currently selected as no longer selected emit NodesDeselected(selected_nodes_); selected_nodes_.clear(); } void NodeView::Select(QVector nodes, bool center_view_on_item) { if (!graph_) { return; } // Optimization: rather than respond to every single item being selected, ignore the signal and // then handle them all at the end. DisconnectSelectionChangedSignal(); QVector deselections = selected_nodes_; QVector new_selections; scene_.DeselectAll(); // Remove any duplicates QVector processed; NodeViewItem *first_item = nullptr; foreach (Node* n, nodes) { if (processed.contains(n)) { continue; } processed.append(n); NodeViewItem* item = scene_.NodeToUIObject(n); if (item) { item->setSelected(true); if (!first_item) { first_item = item; } if (deselections.contains(n)) { deselections.removeOne(n); } else { new_selections.append(n); } } } // Center on something if (center_view_on_item && first_item) { centerOn(first_item); } ConnectSelectionChangedSignal(); // Emit deselect signal for any nodes that weren't in the list if (!deselections.isEmpty()) { emit NodesDeselected(deselections); } // Emit select signal for any nodes that weren't in the list if (!new_selections.isEmpty()) { emit NodesSelected(new_selections); } // Update selected list to the list we received selected_nodes_ = nodes; } void NodeView::SelectWithDependencies(QVector nodes, bool center_view_on_item) { if (!graph_) { return; } int original_length = nodes.size(); for (int i=0;iGetDependencies()); } Select(nodes, center_view_on_item); } void NodeView::CopySelected(bool cut) { if (!graph_) { return; } QVector selected = scene_.GetSelectedNodes(); if (selected.isEmpty()) { return; } CopyNodesToClipboard(selected); if (cut) { DeleteSelected(); } } void NodeView::Paste() { if (!graph_) { return; } paste_command_ = new MultiUndoCommand(); QVector pasted_nodes = PasteNodesFromClipboard(graph_, paste_command_); if (!pasted_nodes.isEmpty()) { paste_command_->add_child(new NodeViewAttachNodesToCursor(this, pasted_nodes)); } paste_command_->redo(); } void NodeView::Duplicate() { if (!graph_) { return; } QVector selected = scene_.GetSelectedNodes(); if (selected.isEmpty()) { return; } paste_command_ = new MultiUndoCommand(); QVector duplicated_nodes = Node::CopyDependencyGraph(selected, paste_command_); if (!duplicated_nodes.isEmpty()) { paste_command_->add_child(new NodeViewAttachNodesToCursor(this, duplicated_nodes)); } paste_command_->redo(); } void NodeView::SetColorLabel(int index) { foreach (Node* node, selected_nodes_) { node->SetOverrideColor(index); } } void NodeView::ZoomIn() { ZoomFromKeyboard(1.25); } void NodeView::ZoomOut() { ZoomFromKeyboard(0.8); } void NodeView::keyPressEvent(QKeyEvent *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); // Determine one pixel in scene units double movement_amt = 1.0 / scale_; // 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; } } void NodeView::mousePressEvent(QMouseEvent *event) { if (HandPress(event)) return; if (event->button() == Qt::LeftButton) { 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 if (!(event->modifiers() & Qt::ShiftModifier)) { scene_.clearSelection(); } // If there's an item here, select it if (item) { item->setSelected(true); } } } if (event->modifiers() & Qt::ControlModifier) { NodeViewItem* node_item = dynamic_cast(item); if (node_item) { create_edge_ = new NodeViewEdge(); create_edge_src_ = node_item; create_edge_src_output_ = Node::kDefaultOutput; create_edge_already_exists_ = false; create_edge_->SetCurved(scene_.GetEdgesAreCurved()); create_edge_->SetFlowDirection(scene_.GetFlowDirection()); scene_.addItem(create_edge_); return; } } super::mousePressEvent(event); } void NodeView::mouseMoveEvent(QMouseEvent *event) { if (HandMove(event)) return; if (create_edge_) { // Determine scene coordinate QPointF scene_pt = mapToScene(event->pos()); // Find if the cursor is currently inside an item NodeViewItem* item_at_cursor = dynamic_cast(itemAt(event->pos())); // Filter out connecting to self if (item_at_cursor == create_edge_src_) { item_at_cursor = nullptr; } // Filter out connecting to a node that connects to us if (item_at_cursor && item_at_cursor->GetNode()->OutputsTo(create_edge_src_->GetNode(), true)) { item_at_cursor = nullptr; } // If the item has changed if (item_at_cursor != create_edge_dst_) { // If we had a destination active, disconnect from it since the item has changed if (create_edge_dst_) { create_edge_dst_->SetHighlightedIndex(-1); if (create_edge_dst_temp_expanded_) { // We expanded this item, so we can un-expand it create_edge_dst_->SetExpanded(false); create_edge_dst_->setZValue(0); } } // Set destination create_edge_dst_ = item_at_cursor; // If our destination is an item, ensure it's expanded if (create_edge_dst_) { if ((create_edge_dst_temp_expanded_ = (!create_edge_dst_->IsExpanded()))) { create_edge_dst_->SetExpanded(true, true); create_edge_dst_->setZValue(100); // Ensure item is in front } } } // If we have a destination, highlight the appropriate input int highlight_index = -1; if (create_edge_dst_) { highlight_index = create_edge_dst_->GetIndexAt(scene_pt); create_edge_dst_->SetHighlightedIndex(highlight_index); } if (highlight_index >= 0) { create_edge_dst_input_ = create_edge_dst_->GetInputAtIndex(highlight_index); create_edge_->SetPoints(create_edge_src_->GetOutputPoint(Node::kDefaultOutput), create_edge_dst_->GetInputPoint(create_edge_dst_input_.input(), create_edge_dst_input_.element(), create_edge_src_->pos()), true); } else { create_edge_dst_input_.Reset(); create_edge_->SetPoints(create_edge_src_->GetOutputPoint(Node::kDefaultOutput), scene_pt, false); } // Set connected to whether we have a valid input destination create_edge_->SetConnected(create_edge_dst_input_.IsValid()); return; } super::mouseMoveEvent(event); // See if there are any items attached if (!attached_items_.isEmpty()) { // Move those items to the cursor MoveAttachedNodesToCursor(event->pos()); // See if the user clicked on an edge (only when dropping single nodes) if (attached_items_.size() == 1) { Node* attached_node = attached_items_.first().item->GetNode(); QRect edge_detect_rect(event->pos(), event->pos()); int edge_detect_radius = fontMetrics().height(); edge_detect_rect.adjust(-edge_detect_radius, -edge_detect_radius, edge_detect_radius, edge_detect_radius); QList items = this->items(edge_detect_rect); NodeViewEdge* new_drop_edge = nullptr; // See if there is an edge here foreach (QGraphicsItem* item, items) { new_drop_edge = dynamic_cast(item); if (new_drop_edge) { drop_input_.Reset(); NodeValue::Type drop_edge_data_type = NodeValue::kNone; // Run the Node and guess what type it's actually returning NodeTraverser traverser; NodeValueTable table = traverser.GenerateTable(new_drop_edge->output(), TimeRange(0, 0)); if (table.Count() > 0) { drop_edge_data_type = table.at(table.Count() - 1).type(); } // Iterate through the inputs of our dragging node and see if our node has any acceptable // inputs to connect to for this type foreach (const QString& input, attached_node->inputs()) { NodeInput i(attached_node, input); if (attached_node->IsInputConnectable(input)) { if (attached_node->GetInputDataType(input) == drop_edge_data_type) { // Found exactly the type we're looking for, set and break this loop drop_input_ = i; break; } else if (!drop_input_.IsValid()) { // Default to first connectable input drop_input_ = i; } } } if (drop_input_.IsValid()) { break; } else { new_drop_edge = nullptr; } } } if (drop_edge_ != new_drop_edge) { if (drop_edge_) { drop_edge_->SetHighlighted(false); } drop_edge_ = new_drop_edge; if (drop_edge_) { drop_edge_->SetHighlighted(true); } } } } } 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_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_; } create_edge_ = nullptr; if (create_edge_dst_) { // Clear highlight create_edge_dst_->SetHighlightedIndex(-1); // Collapse if we expanded it if (create_edge_dst_temp_expanded_) { create_edge_dst_->SetExpanded(false); create_edge_dst_->setZValue(0); } NodeInput &creating_input = create_edge_dst_input_; if (creating_input.IsValid()) { // Make connection 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; } MultiUndoCommand* command = new MultiUndoCommand(); if (!attached_items_.isEmpty()) { if (paste_command_) { // We've already "done" this command, but MultiUndoCommand prevents "redoing" twice, so we // add it to this command (which may have extra commands added too) so that it all gets undone // in the same action command->add_child(paste_command_); paste_command_ = nullptr; } } { // 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; 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; } } 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); super::mouseReleaseEvent(event); } void NodeView::resizeEvent(QResizeEvent *event) { super::resizeEvent(event); RepositionMiniMap(); } void NodeView::UpdateSelectionCache() { QVector current_selection = scene_.GetSelectedNodes(); QVector selected; QVector deselected; // Determine which nodes are newly selected if (selected_nodes_.isEmpty()) { // All nodes in the current selection have just been selected selected = current_selection; } else { foreach (Node* n, current_selection) { if (!selected_nodes_.contains(n)) { selected.append(n); } } } // Determine which nodes are newly deselected if (current_selection.isEmpty()) { // All nodes that were selected have been deselected deselected = selected_nodes_; } else { foreach (Node* n, selected_nodes_) { if (!current_selection.contains(n)) { deselected.append(n); } } } selected_nodes_ = current_selection; if (!selected.isEmpty()) { emit NodesSelected(selected); } if (!deselected.isEmpty()) { emit NodesDeselected(deselected); } } void NodeView::ShowContextMenu(const QPoint &pos) { if (!graph_) { return; } Menu m; MenuShared::instance()->AddItemsForEditMenu(&m, false); m.addSeparator(); QVector selected = scene_.GetSelectedItems(); if (itemAt(pos) && !selected.isEmpty()) { // Label node action QAction* label_action = m.addAction(tr("Label")); connect(label_action, &QAction::triggered, this, [this](){ Core::instance()->LabelNodes(scene_.GetSelectedNodes()); }); // Color menu MenuShared::instance()->AddColorCodingMenu(&m); m.addSeparator(); // Auto-position action QAction* autopos = m.addAction(tr("Auto-Position")); connect(autopos, &QAction::triggered, this, &NodeView::AutoPositionDescendents); ViewerOutput* viewer = dynamic_cast(selected.first()->GetNode()); if (viewer) { m.addSeparator(); QAction* open_in_viewer_action = m.addAction(tr("Open in Viewer")); connect(open_in_viewer_action, &QAction::triggered, this, &NodeView::OpenSelectedNodeInViewer); } } else { QAction* curved_action = m.addAction(tr("Smooth Edges")); curved_action->setCheckable(true); curved_action->setChecked(scene_.GetEdgesAreCurved()); connect(curved_action, &QAction::triggered, &scene_, &NodeViewScene::SetEdgesAreCurved); m.addSeparator(); AddSetScrollZoomsByDefaultActionToMenu(&m); m.addSeparator(); Menu* filter_menu = new Menu(tr("Filter"), &m); m.addMenu(filter_menu); filter_menu->AddActionWithData(tr("Show All Nodes"), kFilterShowAll, filter_mode_); filter_menu->AddActionWithData(tr("Show Selected"), kFilterShowSelective, filter_mode_); connect(filter_menu, &Menu::triggered, this, &NodeView::ContextMenuFilterChanged); Menu* direction_menu = new Menu(tr("Direction"), &m); m.addMenu(direction_menu); direction_menu->AddActionWithData(tr("Top to Bottom"), NodeViewCommon::kTopToBottom, scene_.GetFlowDirection()); direction_menu->AddActionWithData(tr("Bottom to Top"), NodeViewCommon::kBottomToTop, scene_.GetFlowDirection()); direction_menu->AddActionWithData(tr("Left to Right"), NodeViewCommon::kLeftToRight, scene_.GetFlowDirection()); direction_menu->AddActionWithData(tr("Right to Left"), NodeViewCommon::kRightToLeft, scene_.GetFlowDirection()); connect(direction_menu, &Menu::triggered, this, &NodeView::ContextMenuSetDirection); m.addSeparator(); Menu* add_menu = CreateAddMenu(&m); m.addMenu(add_menu); } m.exec(mapToGlobal(pos)); } void NodeView::CreateNodeSlot(QAction *action) { Node* new_node = NodeFactory::CreateFromMenuAction(action); if (new_node) { paste_command_ = new MultiUndoCommand(); paste_command_->add_child(new NodeAddCommand(graph_, new_node)); foreach (Node *context, filter_nodes_) { paste_command_->add_child(new NodeSetPositionCommand(new_node, context, QPointF(0, 0), false)); } paste_command_->add_child(new NodeViewAttachNodesToCursor(this, {new_node})); paste_command_->redo(); } } void NodeView::ContextMenuSetDirection(QAction *action) { SetFlowDirection(static_cast(action->data().toInt())); } void NodeView::AutoPositionDescendents() { QVector selected = scene_.GetSelectedNodes(); foreach (Node* n, selected) { scene_.ReorganizeFrom(n); } } void NodeView::ContextMenuFilterChanged(QAction *action) { FilterMode mode = static_cast(action->data().toInt()); if (filter_mode_ != mode) { // Store temporary graph variables NodeGraph *graph = graph_; QVector nodes = filter_nodes_; // Unset graph with current filter mode ClearGraph(); // Change filter mode filter_mode_ = mode; // Re-set graph with new filter mode SetGraph(graph, nodes); } } void NodeView::OpenSelectedNodeInViewer() { QVector selected = scene_.GetSelectedNodes(); ViewerOutput* viewer = selected.isEmpty() ? nullptr : dynamic_cast(selected.first()); if (viewer) { Core::instance()->OpenNodeInViewer(viewer); } } void NodeView::AddNode(Node *node) { if (filter_mode_ == kFilterShowAll) { scene_.AddNode(node); } } void NodeView::RemoveNode(Node *node) { scene_.RemoveNode(node); } void NodeView::AddEdge(const NodeOutput &output, const NodeInput &input) { if (filter_mode_ == kFilterShowAll) { scene_.AddEdge(output, input); } else if (filter_mode_ == kFilterShowSelective) { Node *output_node = output.node(); Node *input_node = input.node(); if (scene_.item_map().contains(output_node) && scene_.item_map().contains(input_node)) { scene_.AddEdge(output, input); } } } void NodeView::RemoveEdge(const NodeOutput &output, const NodeInput &input) { scene_.RemoveEdge(output, input); } void NodeView::AddNodePosition(Node *node, Node *relative) { if (filter_mode_ == kFilterShowSelective) { if (filter_nodes_.contains(relative)) { // Get UI item or create if it doesn't exist NodeViewItem *item = scene_.item_map().value(node); if (!item) { item = scene_.AddNode(node); for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { if (scene_.item_map().contains(it->second.node())) { scene_.AddEdge(it->second, it->first); } } for (auto it=node->output_connections().cbegin(); it!=node->output_connections().cend(); it++) { if (scene_.item_map().contains(it->second.node())) { scene_.AddEdge(it->first, it->second); } } } // Determine "view" position by averaging the Y value and "min"ing the X value of all contexts QPointF item_pos(DBL_MAX, 0.0); int average_count = 0; foreach (Node *context, filter_nodes_) { if (graph_->GetNodesForContext(context).contains(node)) { QPointF this_context_pos = graph_->GetNodePosition(node, context); this_context_pos += context_offsets_.value(context); item_pos.setX(qMin(item_pos.x(), this_context_pos.x())); item_pos.setY(item_pos.y() + this_context_pos.y()); average_count++; } } item_pos.setY(item_pos.y() / average_count); // Set position item->SetNodePosition(item_pos); positions_.insert(item, {node, item_pos}); } } } void NodeView::RemoveNodePosition(Node *node, Node *relative) { if (filter_mode_ == kFilterShowSelective) { if (filter_nodes_.contains(relative)) { NodeViewItem *item = scene_.item_map().value(node); 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) { 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); } } } } } void NodeView::UpdateSceneBoundingRect() { // Get current items bounding rect QRectF r = scene_.itemsBoundingRect(); // Adjust so that it fills the view r.adjust(-width(), -height(), width(), height()); // Set it scene_.setSceneRect(r); } void NodeView::CenterOnItemsBoundingRect() { centerOn(scene_.itemsBoundingRect().center()); } void NodeView::RepositionMiniMap() { if (minimap_->isVisible()) { int margin = fontMetrics().height(); int w = width() - minimap_->width() - margin; int h = height() - minimap_->height() - margin; if (verticalScrollBar()->isVisible()) { w -= verticalScrollBar()->width(); } if (horizontalScrollBar()->isVisible()) { h -= horizontalScrollBar()->height(); } minimap_->move(w, h); UpdateViewportOnMiniMap(); } } void NodeView::UpdateViewportOnMiniMap() { if (minimap_->isVisible()) { minimap_->SetViewportRect(mapToScene(viewport()->rect())); } } void NodeView::MoveToScenePoint(const QPointF &pos) { centerOn(pos); } void NodeView::AttachNodesToCursor(const QVector &nodes) { QVector items(nodes.size()); for (int i=0; i& items) { DetachItemsFromCursor(); if (!items.isEmpty()) { foreach (NodeViewItem* i, items) { attached_items_.append({i, i->pos() - items.first()->pos()}); } MoveAttachedNodesToCursor(mapFromGlobal(QCursor::pos())); } } void NodeView::DetachItemsFromCursor() { attached_items_.clear(); } void NodeView::SetFlowDirection(NodeViewCommon::FlowDirection dir) { scene_.SetFlowDirection(dir); } void NodeView::MoveAttachedNodesToCursor(const QPoint& p) { QPointF item_pos = mapToScene(p); foreach (const AttachedItem& i, attached_items_) { i.item->setPos(item_pos + i.original_pos); } } void NodeView::ConnectSelectionChangedSignal() { connect(&scene_, &QGraphicsScene::selectionChanged, this, &NodeView::UpdateSelectionCache); } void NodeView::DisconnectSelectionChangedSignal() { disconnect(&scene_, &QGraphicsScene::selectionChanged, this, &NodeView::UpdateSelectionCache); } void NodeView::ZoomIntoCursorPosition(QWheelEvent *event, double multiplier, const QPointF& cursor_pos) { Q_UNUSED(event) double test_scale = scale_ * multiplier; if (test_scale > kMinimumScale) { int anchor_x = qRound(double(cursor_pos.x() + horizontalScrollBar()->value()) / scale_ * test_scale - cursor_pos.x()); int anchor_y = qRound(double(cursor_pos.y() + verticalScrollBar()->value()) / scale_ * test_scale - cursor_pos.y()); scale(multiplier, multiplier); this->horizontalScrollBar()->setValue(anchor_x); this->verticalScrollBar()->setValue(anchor_y); scale_ = test_scale; } } 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()); // If the cursor is not currently within the widget, zoom into the center if (!rect().contains(cursor_pos)) { cursor_pos = QPoint(width()/2, height()/2); } 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); } Menu *NodeView::CreateAddMenu(Menu *parent) { Menu* add_menu = NodeFactory::CreateMenu(parent); add_menu->setTitle(tr("Add")); connect(add_menu, &Menu::triggered, this, &NodeView::CreateNodeSlot); return add_menu; } NodeView::NodeViewAttachNodesToCursor::NodeViewAttachNodesToCursor(NodeView *view, const QVector &nodes) : view_(view), nodes_(nodes) { } void NodeView::NodeViewAttachNodesToCursor::redo() { view_->AttachNodesToCursor(nodes_); } void NodeView::NodeViewAttachNodesToCursor::undo() { view_->DetachItemsFromCursor(); } Project *NodeView::NodeViewAttachNodesToCursor::GetRelevantProject() const { // Will either return a project or a nullptr which is also acceptable 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_); } } }