/*** 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 "core.h" #include "nodeviewundo.h" #include "node/factory.h" NodeView::NodeView(QWidget *parent) : QGraphicsView(parent), graph_(nullptr), attached_item_(nullptr), drop_edge_(nullptr) { setScene(&scene_); setDragMode(RubberBandDrag); setContextMenuPolicy(Qt::CustomContextMenu); connect(&scene_, &QGraphicsScene::changed, this, &NodeView::ItemsChanged); connect(&scene_, &QGraphicsScene::selectionChanged, this, &NodeView::SceneSelectionChangedSlot); connect(this, &NodeView::customContextMenuRequested, this, &NodeView::ShowContextMenu); connect(&reorganize_timer_, &QTimer::timeout, &reorganize_timer_, &QTimer::stop); connect(&reorganize_timer_, &QTimer::timeout, this, &NodeView::Reorganize); setMouseTracking(true); } NodeView::~NodeView() { // Unset the current graph SetGraph(nullptr); } void NodeView::SetGraph(NodeGraph *graph) { if (graph_ == graph) { return; } if (graph_ != nullptr) { disconnect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); disconnect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); disconnect(graph_, &NodeGraph::EdgeAdded, this, &NodeView::AddEdge); disconnect(graph_, &NodeGraph::EdgeRemoved, this, &NodeView::RemoveEdge); } // Clear the scene of all UI objects scene_.clear(); // Set reference to the graph graph_ = graph; // If the graph is valid, add UI objects for each of its Nodes if (graph_ != nullptr) { connect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); connect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); connect(graph_, &NodeGraph::EdgeAdded, this, &NodeView::AddEdge); connect(graph_, &NodeGraph::EdgeRemoved, this, &NodeView::RemoveEdge); QList graph_nodes = graph_->nodes(); foreach (Node* node, graph_nodes) { AddNode(node); } } } NodeViewItem *NodeView::NodeToUIObject(QGraphicsScene *scene, Node *n) { QList graphics_items = scene->items(); for (int i=0;i(graphics_items.at(i)); if (item != nullptr) { if (item->node() == n) { return item; } } } return nullptr; } NodeViewEdge *NodeView::EdgeToUIObject(QGraphicsScene *scene, NodeEdgePtr n) { QList graphics_items = scene->items(); for (int i=0;i(graphics_items.at(i)); if (edge != nullptr) { if (edge->edge() == n) { return edge; } } } return nullptr; } NodeViewItem *NodeView::NodeToUIObject(Node *n) { return NodeToUIObject(&scene_, n); } NodeViewEdge *NodeView::EdgeToUIObject(NodeEdgePtr n) { return EdgeToUIObject(&scene_, n); } void NodeView::DeleteSelected() { if (!graph_) { return; } QList selected = scene_.selectedItems(); QList selected_nodes; foreach (QGraphicsItem* item, selected) { NodeViewItem* node_item = dynamic_cast(item); if (node_item) { selected_nodes.append(node_item->node()); } } if (selected_nodes.isEmpty()) { return; } Core::instance()->undo_stack()->push(new NodeRemoveCommand(graph_, selected_nodes)); } void NodeView::SelectAll() { QList all_items = this->items(); foreach (QGraphicsItem* i, all_items) { i->setSelected(true); } } void NodeView::DeselectAll() { QList all_items = this->items(); foreach (QGraphicsItem* i, all_items) { i->setSelected(false); } } void NodeView::Select(const QList &nodes) { DeselectAll(); foreach (Node* n, nodes) { NodeViewItem* item = NodeToUIObject(n); if (item) { item->setSelected(true); } } } void NodeView::SelectWithDependencies(const QList &nodes) { DeselectAll(); QList nodes_with_deps; foreach (Node* n, nodes) { NodeViewItem* item = NodeToUIObject(n); if (item) { item->setSelected(true); } QList deps = n->GetDependencies(); foreach (Node* d, deps) { item = NodeToUIObject(d); if (item) { item->setSelected(true); } } } } void NodeView::AddNode(Node* node) { NodeViewItem* item = new NodeViewItem(); item->SetNode(node); scene_.addItem(item); // Add a NodeViewEdge for each connection foreach (NodeParam* param, node->parameters()) { // We only bother working with outputs since eventually this will cover all inputs too // (covering both would lead to duplicates since every edge connects to one input and one output) if (param->type() == NodeParam::kOutput) { const QVector& edges = param->edges(); foreach(NodeEdgePtr edge, edges) { AddEdge(edge); } } } QueueReorganize(); } void NodeView::RemoveNode(Node *node) { NodeViewItem* item = NodeToUIObject(scene(), node); delete item; } void NodeView::AddEdge(NodeEdgePtr edge) { NodeViewEdge* edge_ui = new NodeViewEdge(); edge_ui->SetEdge(edge); scene_.addItem(edge_ui); QueueReorganize(); } void NodeView::RemoveEdge(NodeEdgePtr edge) { NodeViewEdge* edge_ui = EdgeToUIObject(scene(), edge); delete edge_ui; } void NodeView::ItemsChanged() { QList items = scene_.items(); foreach (QGraphicsItem* item, items) { NodeViewEdge* edge = dynamic_cast(item); if (edge != nullptr) { edge->Adjust(); } } } void NodeView::keyPressEvent(QKeyEvent *event) { QGraphicsView::keyPressEvent(event); if (event->key() == Qt::Key_Escape && attached_item_) { DetachItemFromCursor(); // 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 (attached_item_) { Node* dropping_node = attached_item_->node(); DetachItemFromCursor(); 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_compatible_input_, command); new NodeEdgeAddCommand(dropping_node->output(), old_edge->input(), command); Core::instance()->undo_stack()->push(command); } drop_edge_ = nullptr; } QGraphicsView::mousePressEvent(event); } void NodeView::mouseMoveEvent(QMouseEvent *event) { QGraphicsView::mouseMoveEvent(event); if (attached_item_) { attached_item_->setPos(mapToScene(event->pos())); // See if the user clicked on an edge 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; foreach (QGraphicsItem* item, items) { NodeViewEdge* edge = dynamic_cast(item); if (edge) { // Try to place this node inside this edge // See if the node we're dropping has an input of a compatible data type NodeInput* edges_input = edge->edge()->input(); NodeParam::DataType input_type = edges_input->data_type(); NodeInput* compatible_input = nullptr; foreach (NodeParam* drop_node_param, attached_item_->node()->parameters()) { if (drop_node_param->type() == NodeParam::kInput && static_cast(drop_node_param)->data_type() & input_type) { compatible_input = static_cast(drop_node_param); break; } } if (compatible_input) { new_drop_edge = edge; drop_compatible_input_ = compatible_input; break; } } } 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::SceneSelectionChangedSlot() { // Get the scene's selected items and convert it into a list of selected nodes QList selected_items = scene_.selectedItems(); QList selected_nodes; for (int i=0;i(selected_items.at(i)); if (item != nullptr) { selected_nodes.append(item->node()); } } emit SelectionChanged(selected_nodes); } void NodeView::ShowContextMenu(const QPoint &pos) { if (!graph_) { return; } Menu* m = new Menu(); Menu* add_menu = NodeFactory::CreateMenu(); 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) { Core::instance()->undo_stack()->push(new NodeAddCommand(graph_, new_node)); NodeViewItem* item = NodeToUIObject(new_node); AttachItemToCursor(item); } } 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->node() << "for" << n->node(); 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->node(); Node* theirs = node_item->node(); 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); } } QList NodeView::GetNodeDirectDescendants(Node* n, const QList connected_nodes, QList& processed_nodes) { QList direct_descendants = connected_nodes; processed_nodes.append(n); // Remove any nodes that aren't necessarily attached directly for (int i=0;ioutput()->edges().size();j++) { Node* this_output_connection = connected->output()->edges().at(j)->input()->parentNode(); if (!processed_nodes.contains(this_output_connection)) { direct_descendants.removeAt(i); i--; break; } } } return direct_descendants; } int NodeView::FindWeightsInternal(Node *node, QHash &weights, QList& weighted_nodes) { QList connected_nodes = node->GetImmediateDependencies(); int weight = 0; if (!connected_nodes.isEmpty()) { QList direct_descendants = GetNodeDirectDescendants(node, connected_nodes, weighted_nodes); foreach (Node* dep, direct_descendants) { weight += FindWeightsInternal(dep, weights, weighted_nodes); } } weight = qMax(weight, 1); weights.insert(node, weight); return weight; } void NodeView::ReorganizeInternal(NodeViewItem* src_item, QHash& weights, QList& positioned_nodes) { Node* n = src_item->node(); QList connected_nodes = n->GetImmediateDependencies(); if (connected_nodes.isEmpty()) { return; } QList direct_descendants = GetNodeDirectDescendants(n, connected_nodes, positioned_nodes); int descendant_weight = 0; foreach (Node* dep, direct_descendants) { descendant_weight += weights.value(dep); } qreal center_y = src_item->y(); qreal total_height = descendant_weight * src_item->rect().height() + (direct_descendants.size()-1) * src_item->rect().height()/2; double item_top = center_y - (total_height/2) + src_item->rect().height()/2; // Set each node's position int weight_index = 0; for (int i=0;irect().height() * 1.5; QPointF item_pos(src_item->pos().x() - item->rect().width() * 3 / 2, item_y); item->setPos(item_pos); weight_index += weights.value(connected); } // Recursively work on each node foreach (Node* connected, connected_nodes) { NodeViewItem* item = NodeToUIObject(connected); if (!item) { continue; } ReorganizeInternal(item, weights, positioned_nodes); } } void NodeView::QueueReorganize() { // Avoids the fairly complex Reorganize() function every single time a connection or node is added reorganize_timer_.stop(); reorganize_timer_.start(20); } void NodeView::AttachItemToCursor(NodeViewItem *item) { attached_item_ = item; setMouseTracking(attached_item_); } void NodeView::DetachItemFromCursor() { AttachItemToCursor(nullptr); } void NodeView::Reorganize() { if (!graph_) { return; } QList end_nodes; // Calculate the nodes that don't output to anything, they'll be our anchors foreach (Node* node, graph_->nodes()) { if (!node->HasConnectedOutputs()) { end_nodes.append(node); } } QList processed_nodes; QHash node_weights; foreach (Node* end_node, end_nodes) { FindWeightsInternal(end_node, node_weights, processed_nodes); } processed_nodes.clear(); foreach (Node* end_node, end_nodes) { ReorganizeInternal(NodeToUIObject(end_node), node_weights, processed_nodes); } }