/*** Olive - Non-Linear Video Editor Copyright (C) 2019 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 "core.h" #include "nodeviewundo.h" #include "node/factory.h" #include "widget/menu/menushared.h" #define super HandMovableView OLIVE_NAMESPACE_ENTER NodeView::NodeView(QWidget *parent) : HandMovableView(parent), graph_(nullptr), drop_edge_(nullptr), filter_mode_(kFilterShowSelectedBlocks), scale_(1.0) { setScene(&scene_); SetDefaultDragMode(RubberBandDrag); setContextMenuPolicy(Qt::CustomContextMenu); setMouseTracking(true); setRenderHint(QPainter::Antialiasing); setViewportUpdateMode(FullViewportUpdate); connect(&scene_, &QGraphicsScene::changed, this, &NodeView::ItemsChanged); connect(this, &NodeView::customContextMenuRequested, this, &NodeView::ShowContextMenu); ConnectSelectionChangedSignal(); SetFlowDirection(NodeViewCommon::kTopToBottom); scene_.setSceneRect(-100, -100, 200, 200); } NodeView::~NodeView() { // Unset the current graph SetGraph(nullptr); } void NodeView::SetGraph(NodeGraph *graph) { if (graph_ == graph) { return; } if (graph_ != nullptr) { disconnect(graph_, &NodeGraph::NodeAdded, &scene_, &NodeViewScene::AddNode); disconnect(graph_, &NodeGraph::NodeRemoved, &scene_, &NodeViewScene::RemoveNode); disconnect(graph_, &NodeGraph::EdgeAdded, this, &NodeView::GraphEdgeAdded); disconnect(graph_, &NodeGraph::EdgeRemoved, this, &NodeView::GraphEdgeRemoved); } // Clear the scene of all UI objects scene_.clear(); // Set reference to the graph graph_ = graph; scene_.SetGraph(graph_); // If the graph is valid, add UI objects for each of its Nodes if (graph_ != nullptr) { connect(graph_, &NodeGraph::NodeAdded, &scene_, &NodeViewScene::AddNode); connect(graph_, &NodeGraph::NodeRemoved, &scene_, &NodeViewScene::RemoveNode); connect(graph_, &NodeGraph::EdgeAdded, this, &NodeView::GraphEdgeAdded); connect(graph_, &NodeGraph::EdgeRemoved, this, &NodeView::GraphEdgeRemoved); foreach (Node* n, graph_->nodes()) { scene_.AddNode(n); } ValidateFilter(); } } void NodeView::DeleteSelected() { if (!graph_) { return; } QUndoCommand* command = new QUndoCommand(); { QList selected_edges = scene_.GetSelectedEdges(); foreach (NodeEdge* edge, selected_edges) { new NodeEdgeRemoveCommand(edge->output(), edge->input(), command); } } { QList selected_nodes = scene_.GetSelectedNodes(); // Ensure no nodes are "undeletable" for (int i=0;iCanBeDeleted()) { selected_nodes.removeAt(i); i--; } } if (!selected_nodes.isEmpty()) { new NodeRemoveCommand(graph_, selected_nodes, command); } } Core::instance()->undo_stack()->pushIfHasChildren(command); } void NodeView::SelectAll() { // Optimization: rather than respond to every single item being selected, ignore the signal and // then handle them all at the end. DisconnectSelectionChangedSignal(); scene_.SelectAll(); ReconnectSelectionChangedSignal(); } void NodeView::DeselectAll() { // Optimization: rather than respond to every single item being selected, ignore the signal and // then handle them all at the end. DisconnectSelectionChangedSignal(); scene_.DeselectAll(); ReconnectSelectionChangedSignal(); } void NodeView::Select(const QList &nodes) { 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_.DeselectAll(); foreach (Node* n, nodes) { NodeViewItem* item = scene_.NodeToUIObject(n); item->setSelected(true); } ReconnectSelectionChangedSignal(); } void NodeView::SelectWithDependencies(QList nodes) { if (!graph_) { return; } int original_length = nodes.size(); for (int i=0;iGetDependencies()); } Select(nodes); } void NodeView::SelectBlocks(const QList &blocks) { if (selected_blocks_ == blocks) { return; } // Remove temporary associations foreach (Block* b, selected_blocks_) { if (!blocks.contains(b)) { temporary_association_map_.remove(b); } } selected_blocks_ = blocks; // Block scene signals while our selection is changing a lot scene_.blockSignals(true); if (filter_mode_ == kFilterShowSelectedBlocks) { UpdateBlockFilter(); } QList nodes; nodes.reserve(blocks.size()); foreach (Block* b, blocks) { nodes.append(b); nodes.append(b->GetDependencies()); } SelectWithDependencies(nodes); // Stop blocking signals and send a change signal now that all of our processing is done scene_.blockSignals(false); SceneSelectionChangedSlot(); if (!blocks.isEmpty()) { NodeViewItem* item = scene_.NodeToUIObject(blocks.first()); if (item) { centerOn(item); } } } void NodeView::CopySelected(bool cut) { if (!graph_) { return; } QList selected = scene_.GetSelectedNodes(); if (selected.isEmpty()) { return; } CopyNodesToClipboard(selected); if (cut) { DeleteSelected(); } } void NodeView::Paste() { if (!graph_) { return; } QUndoCommand* command = new QUndoCommand(); QList pasted_nodes = PasteNodesFromClipboard(static_cast(graph_), command); Core::instance()->undo_stack()->pushIfHasChildren(command); if (!pasted_nodes.isEmpty()) { AttachNodesToCursor(pasted_nodes); } } void NodeView::Duplicate() { if (!graph_) { return; } QList selected = scene_.GetSelectedNodes(); if (selected.isEmpty()) { return; } QUndoCommand* command = new QUndoCommand(); QList duplicated_nodes; foreach (Node* n, selected) { Node* copy = n->copy(); Node::CopyInputs(n, copy, false); duplicated_nodes.append(copy); new NodeAddCommand(graph_, copy, command); } for (int i=0;ioutput()->edges()) { if (edge->input()->parentNode() == dst) { new NodeEdgeAddCommand(duplicated_nodes.at(i)->output(), duplicated_nodes.at(j)->GetInputWithID(edge->input()->id()), command); } } } } Core::instance()->undo_stack()->pushIfHasChildren(command); if (!duplicated_nodes.isEmpty()) { AttachNodesToCursor(duplicated_nodes); } } void NodeView::ItemsChanged() { QHash::const_iterator i; for (i=scene_.edge_map().constBegin(); i!=scene_.edge_map().constEnd(); i++) { i.value()->Adjust(); } } void NodeView::keyPressEvent(QKeyEvent *event) { super::keyPressEvent(event); if (event->key() == Qt::Key_Escape && !attached_items_.isEmpty()) { DetachItemsFromCursor(); // We undo the last action which SHOULD be adding the node // FIXME: Possible danger of this not being the case? Core::instance()->undo_stack()->undo(); } } void NodeView::mousePressEvent(QMouseEvent *event) { if (HandPress(event)) return; if (!attached_items_.isEmpty()) { if (attached_items_.size() == 1) { Node* dropping_node = attached_items_.first().item->GetNode(); if (drop_edge_) { NodeEdgePtr old_edge = drop_edge_->edge(); // We have everything we need to place the node in between QUndoCommand* command = new QUndoCommand(); // Remove old edge new NodeEdgeRemoveCommand(old_edge, command); // Place new edges new NodeEdgeAddCommand(old_edge->output(), drop_input_, command); new NodeEdgeAddCommand(dropping_node->output(), old_edge->input(), command); Core::instance()->undo_stack()->push(command); } drop_edge_ = nullptr; } DetachItemsFromCursor(); } super::mousePressEvent(event); } void NodeView::mouseMoveEvent(QMouseEvent *event) { if (HandMove(event)) return; super::mouseMoveEvent(event); if (!attached_items_.isEmpty()) { 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()); // FIXME: Hardcoded numbers edge_detect_rect.adjust(-20, -20, 20, 20); 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_ = nullptr; foreach (NodeParam* param, attached_node->parameters()) { if (param->type() == NodeParam::kInput) { NodeInput* input = static_cast(param); if (input->is_connectable()) { if (input->data_type() & new_drop_edge->edge()->input()->data_type()) { drop_input_ = input; break; } else if (!drop_input_) { drop_input_ = input; } } } } if (drop_input_) { 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; super::mouseReleaseEvent(event); } void NodeView::wheelEvent(QWheelEvent *event) { if (event->modifiers() & Qt::ControlModifier) { // FIXME: Hardcoded divider (0.001) qreal multiplier = 1.0 + (static_cast(event->angleDelta().x() + event->angleDelta().y()) * 0.001); double test_scale = scale_ * multiplier; if (test_scale > 0.1) { scale(multiplier, multiplier); scale_ = test_scale; } } else { QWidget::wheelEvent(event); } } void NodeView::SceneSelectionChangedSlot() { emit SelectionChanged(scene_.GetSelectedNodes()); } void NodeView::ShowContextMenu(const QPoint &pos) { if (!graph_) { return; } Menu m; MenuShared::instance()->AddItemsForEditMenu(&m, false); m.addSeparator(); QList 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()); }); m.addSeparator(); // Auto-position action QAction* autopos = m.addAction(tr("Auto-Position")); connect(autopos, &QAction::triggered, this, &NodeView::AutoPositionDescendents); } 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(); Menu* filter_menu = new Menu(tr("Filter"), &m); m.addMenu(filter_menu); filter_menu->AddActionWithData(tr("Show All"), kFilterShowAll, filter_mode_); filter_menu->AddActionWithData(tr("Show Selected Blocks Only"), kFilterShowSelectedBlocks, 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 = NodeFactory::CreateMenu(&m); add_menu->setTitle(tr("Add")); connect(add_menu, &Menu::triggered, this, &NodeView::CreateNodeSlot); m.addMenu(add_menu); } m.exec(mapToGlobal(pos)); } void NodeView::CreateNodeSlot(QAction *action) { Node* new_node = NodeFactory::CreateFromMenuAction(action); if (new_node) { // Associate this new node with these blocks (allows it to be visible with them even while it // isn't connected to anything) AssociateNodeWithSelectedBlocks(new_node); Core::instance()->undo_stack()->push(new NodeAddCommand(graph_, new_node)); NodeViewItem* item = scene_.NodeToUIObject(new_node); AttachItemsToCursor({item}); } } void NodeView::ContextMenuSetDirection(QAction *action) { SetFlowDirection(static_cast(action->data().toInt())); } void NodeView::AutoPositionDescendents() { QList selected = scene_.GetSelectedNodes(); foreach (Node* n, selected) { scene_.ReorganizeFrom(n); } } void NodeView::ContextMenuFilterChanged(QAction *action) { FilterMode filter = static_cast(action->data().toInt()); if (filter_mode_ != filter) { filter_mode_ = filter; if (filter == kFilterShowAll) { // Un-hide all blocks foreach (NodeViewItem* item, scene_.item_map()) { item->setVisible(true); } foreach (NodeViewEdge* edge, scene_.edge_map()) { edge->setVisible(true); } } ValidateFilter(); } } void NodeView::PlaceNode(NodeViewItem *n, const QPointF &pos) { QRectF destination_rect = n->rect(); destination_rect.translate(n->pos()); double x_movement = destination_rect.width() * 1.5; double y_movement = destination_rect.height() * 1.5; QList items = scene()->items(destination_rect); n->setPos(pos); foreach (QGraphicsItem* item, items) { if (item == n) { continue; } NodeViewItem* node_item = dynamic_cast(item); if (!node_item) { continue; } qDebug() << "Moving" << node_item->GetNode() << "for" << n->GetNode(); QPointF new_pos; if (item->pos() == pos) { qDebug() << "Same pos, need more info"; // Item positions are exact, we'll need more information to determine where this item should go Node* ours = n->GetNode(); Node* theirs = node_item->GetNode(); bool moved = false; new_pos = item->pos(); // Heuristic to determine whether to move the other item above or below foreach (NodeEdgePtr our_edge, ours->output()->edges()) { foreach (NodeEdgePtr their_edge, theirs->output()->edges()) { if (our_edge->output()->parentNode() == their_edge->output()->parentNode()) { qDebug() << " They share a node that they output to"; if (our_edge->input()->index() > their_edge->input()->index()) { // Their edge should go above ours qDebug() << " Our edge goes BELOW theirs"; new_pos.setY(new_pos.y() - y_movement); } else { // Our edge should go below ours qDebug() << " Our edge goes ABOVE theirs"; new_pos.setY(new_pos.y() + y_movement); } moved = true; break; } } } // If we find anything, just move at random if (!moved) { new_pos.setY(new_pos.y() - y_movement); } } else if (item->pos().x() == pos.x()) { qDebug() << "Same X, moving vertically"; // Move strictly up or down new_pos = item->pos(); if (item->pos().y() < pos.y()) { // Move further up new_pos.setY(pos.y() - y_movement); } else { // Move further down new_pos.setY(pos.y() + y_movement); } } else if (item->pos().y() == pos.y()) { qDebug() << "Same Y, moving horizontally"; // Move strictly left or right new_pos = item->pos(); if (item->pos().x() < pos.x()) { // Move further up new_pos.setX(pos.x() - x_movement); } else { // Move further down new_pos.setX(pos.x() + x_movement); } } else { qDebug() << "Diff pos, pushing in angle"; // The item does not have equal X or Y, attempt to push it away from `pos` in the direction it's in double x_diff = item->pos().x() - pos.x(); double y_diff = item->pos().y() - pos.y(); double slope = y_diff / x_diff; double y_int = item->pos().y() - slope * item->pos().x(); if (qAbs(slope) > 1.0) { // Vertical difference is greater than horizontal difference, prioritize vertical movement double desired_y = pos.y(); if (item->pos().y() > pos.y()) { desired_y += y_movement; } else { desired_y -= y_movement; } double x = (desired_y - y_int) / slope; new_pos = QPointF(x, desired_y); } else { // Horizontal difference is greater than vertical difference, prioritize horizontal movement double desired_x = pos.x(); if (item->pos().x() > pos.x()) { desired_x += x_movement; } else { desired_x -= x_movement; } double y = slope * desired_x + y_int; new_pos = QPointF(desired_x, y); } } PlaceNode(node_item, new_pos); } } void NodeView::AttachNodesToCursor(const QList &nodes) { QList items; foreach (Node* p, nodes) { items.append(scene_.NodeToUIObject(p)); } AttachItemsToCursor(items); } void NodeView::AttachItemsToCursor(const QList& items) { DetachItemsFromCursor(); if (!items.isEmpty()) { foreach (NodeViewItem* i, items) { attached_items_.append({i, i->pos() - items.first()->pos()}); } setMouseTracking(true); MoveAttachedNodesToCursor(mapFromGlobal(QCursor::pos())); } } void NodeView::DetachItemsFromCursor() { attached_items_.clear(); setMouseTracking(false); } 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::SceneSelectionChangedSlot); } void NodeView::ReconnectSelectionChangedSignal() { ConnectSelectionChangedSignal(); SceneSelectionChangedSlot(); } void NodeView::DisconnectSelectionChangedSignal() { disconnect(&scene_, &QGraphicsScene::selectionChanged, this, &NodeView::SceneSelectionChangedSlot); } void NodeView::UpdateBlockFilter() { // Hide all nodes foreach (NodeViewItem* item, scene_.item_map()) { item->setVisible(false); } bool first = true; QRectF last_rect; QList currently_visible; foreach (Block* b, selected_blocks_) { // Auto-position this node's dependencies scene_.ReorganizeFrom(b); // Start calculating the bounding rect of this node's deps QPointF node_pos = b->GetPosition(); QRectF anchor(node_pos, node_pos); QList deps = b->GetDependencies(); foreach (Node* d, deps) { QPointF dep_pos = d->GetPosition(); anchor.setLeft(qMin(anchor.left(), dep_pos.x())); anchor.setRight(qMax(anchor.right(), dep_pos.x())); anchor.setTop(qMin(anchor.top(), dep_pos.y())); anchor.setBottom(qMax(anchor.bottom(), dep_pos.y())); NodeViewItem* item = scene_.NodeToUIObject(d); if (item) { item->setVisible(true); } } // Shift the bounding rect in relation to the other bounding rects if (first) { first = false; } else { QPointF desired_anchor_pos = last_rect.bottomRight() + QPointF(0, 2); QPointF necessary_movement = anchor.topRight() - desired_anchor_pos; b->SetPosition(b->GetPosition() - necessary_movement); foreach (Node* d, deps) { d->SetPosition(d->GetPosition() - necessary_movement); } } // Now that we're done calculating the bounding rect, add this block... deps.append(b); // ...then add its associations deps.append(temporary_association_map_[b]); QHash >::const_iterator i; for (i=association_map_.begin(); i!=association_map_.end(); i++) { if (i.value().contains(b)) { deps.append(i.key()); } } // And make sure all nodes are shown foreach (Node* n, deps) { NodeViewItem* item = scene_.NodeToUIObject(n); if (item) { item->setVisible(true); } } // And lastly, add them all to our currently visible list currently_visible.append(deps); // Cache this rect so we can calculate other rects last_rect = anchor; } // Show only edges between those dependencies foreach (NodeViewEdge* edge, scene_.edge_map()) { edge->setVisible((currently_visible.contains(edge->edge()->input()->parentNode()) && currently_visible.contains(edge->edge()->output()->parentNode()))); } } void NodeView::AssociateNodeWithSelectedBlocks(Node *n) { association_map_.insert(n, selected_blocks_); connect(n, &Node::destroyed, this, &NodeView::AssociatedNodeDestroyed, Qt::DirectConnection); } void NodeView::DisassociateNode(Node *n, bool remove_from_map) { if (remove_from_map) { association_map_.remove(n); } disconnect(n, &Node::destroyed, this, &NodeView::AssociatedNodeDestroyed); } void NodeView::ValidateFilter() { // Force auto-positioning switch (filter_mode_) { case kFilterShowAll: // NOTE: Assumes Sequence scene_.ReorganizeFrom(static_cast(graph_)->viewer_output()); break; case kFilterShowSelectedBlocks: UpdateBlockFilter(); break; } } void NodeView::AssociatedNodeDestroyed() { DisassociateNode(static_cast(sender()), true); } void NodeView::GraphEdgeAdded(NodeEdgePtr edge) { Node* input_node = edge->input()->parentNode(); if (input_node->OutputsTo(static_cast(graph_)->viewer_output(), true)) { QHash >::const_iterator i = association_map_.begin(); while (i != association_map_.end()) { if (input_node->InputsFrom(i.key(), true)) { i = association_map_.erase(i); } else { i++; } } } scene_.AddEdge(edge); ValidateFilter(); } void NodeView::GraphEdgeRemoved(NodeEdgePtr edge) { scene_.RemoveEdge(edge); Node* output_node = edge->output()->parentNode(); // Check if this disconnected node still connects to a selected block, in which case do nothing foreach (Block* b, selected_blocks_) { if (output_node->OutputsTo(b, true)) { return; } } QList disconnected_nodes; disconnected_nodes.append(output_node); disconnected_nodes.append(output_node->GetDependencies()); if (output_node->OutputsTo(static_cast(graph_)->viewer_output(), true)) { // Check if this disconnected node still has a path to the viewer somewhere else foreach (Block* b, selected_blocks_) { QList& temp_assocs = temporary_association_map_[b]; temp_assocs.append(disconnected_nodes); } } else { // Otherwise, we must associate these nodes foreach (Node* n, disconnected_nodes) { AssociateNodeWithSelectedBlocks(n); } } } OLIVE_NAMESPACE_EXIT