/*** Olive - Non-Linear Video Editor Copyright (C) 2022 Olive Team Modifications Copyright (C) 2025 mikesolar 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 #include #include #include "node/audio/volume/volume.h" #include "node/distort/transform/transformdistortnode.h" #include "node/factory.h" #include "node/group/group.h" #include "node/nodeundo.h" #include "node/project/serializer/serializer.h" #include "panel/panelmanager.h" #include "node/traverser.h" #include "ui/icons/icons.h" #include "widget/menu/menushared.h" #include "widget/timebased/timebasedview.h" #define super HandMovableView namespace olive { const double NodeView::k_minimum_scale = 0.1; const int NodeView::k_maximum_contexts = 10; NodeView::NodeView(QWidget *parent) : HandMovableView(parent) , drop_edge_(nullptr) , create_edge_(nullptr) , create_edge_output_item_(nullptr) , create_edge_input_item_(nullptr) , overlay_view_(nullptr) , scale_(1.0) , dont_emit_selection_signals_(false) , show_in_param_editor_action_(nullptr) { setScene(&scene_); set_default_drag_mode(RubberBandDrag); setContextMenuPolicy(Qt::CustomContextMenu); setMouseTracking(true); setRenderHint(QPainter::Antialiasing); setViewportUpdateMode(FullViewportUpdate); connect(this, &NodeView::customContextMenuRequested, this, &NodeView::show_context_menu); connect_selection_changed_signal(); set_flow_direction(NodeViewCommon::k_left_to_right); show_in_param_editor_action_ = new QAction(tr("Show in Parameter Editor"), this); Menu::conform_item(show_in_param_editor_action_, QStringLiteral("shownodeparams"), QKeySequence(tr("Shift+P"))); show_in_param_editor_action_->setShortcutContext(Qt::WindowShortcut); addAction(show_in_param_editor_action_); connect(show_in_param_editor_action_, &QAction::triggered, this, &NodeView::show_selected_node_in_param_editor); update_scene_bounding_rect(); connect(&scene_, &QGraphicsScene::changed, this, &NodeView::update_scene_bounding_rect); minimap_ = new NodeViewMiniMap(&scene_, this); minimap_->show(); connect(minimap_, &NodeViewMiniMap::resized, this, &NodeView::reposition_mini_map); connect(minimap_, &NodeViewMiniMap::move_to_scene_point, this, &NodeView::move_to_scene_point); connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &NodeView::update_viewport_on_mini_map); connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &NodeView::update_viewport_on_mini_map); viewport()->installEventFilter(this); } NodeView::~NodeView() { // Unset the current graph clear_graph(); } void NodeView::set_contexts(const QVector &nodes) { if (overlay_view_) { close_overlay(); } // Remove contexts that are no longer in the list foreach (Node *n, contexts_) { if (!nodes.contains(n)) { remove_context(n); } } // Add contexts that are now in the list foreach (Node *n, nodes) { if (scene_.context_map().size() >= k_maximum_contexts) { break; } if (!contexts_.contains(n)) { add_context(n); } } contexts_ = nodes; center_on_items_bounding_rect(); } void NodeView::close_contexts_belonging_to_project(Project *project) { QVector new_contexts = contexts_; for (auto it = new_contexts.begin(); it != new_contexts.end();) { if ((*it)->project() == project) { it = new_contexts.erase(it); } else { it++; } } set_contexts(new_contexts); } void NodeView::clear_graph() { set_contexts(QVector()); } void NodeView::delete_selected() { NodeViewDeleteCommand *command = new NodeViewDeleteCommand(); int count = 0; foreach (NodeViewContext *ctx, scene_.context_map()) { count += ctx->delete_selected(command); } Core::instance()->undo_stack()->push(command, tr("Deleted %1 Node(s)").arg(count)); } void NodeView::select_all() { // Optimization: rather than respond to every single item being selected, ignore the signal and // then handle them all at the end. disconnect_selection_changed_signal(); scene_.select_all(); connect_selection_changed_signal(); update_selection_cache(); } void NodeView::deselect_all() { if (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. disconnect_selection_changed_signal(); scene_.deselect_all(); connect_selection_changed_signal(); // Just emit all the nodes that are currently selected as no longer selected emit nodes_deselected(selected_nodes_); selected_nodes_.clear(); emit node_selection_changed(selected_nodes_); emit node_selection_changed_with_contexts(QVector()); } void NodeView::select(const QVector &nodes, bool center_view_on_item) { // Optimization: rather than respond to every single item being selected, ignore the signal and // then handle them all at the end. disconnect_selection_changed_signal(); QVector deselections = selected_nodes_; QVector new_selections; scene_.deselect_all(); foreach (const Node::ContextPair &p, nodes) { NodeViewContext *ctx = scene_.context_map().value(p.context); if (ctx) { NodeViewItem *item = ctx->get_item_from_map(p.node); if (item) { item->setSelected(true); } } } // Center on something if (center_view_on_item && !nodes.isEmpty()) { QMetaObject::invokeMethod(this, "center_on_node", Qt::QueuedConnection, OLIVE_NS_ARG(Node *, nodes.first().node)); } connect_selection_changed_signal(); // Don't signal when this function was likely triggered from another widget's signal anyway dont_emit_selection_signals_ = true; update_selection_cache(); dont_emit_selection_signals_ = false; } void NodeView::copy_selected(bool cut) { if (selected_nodes_.isEmpty()) { return; } QString copy_str; QXmlStreamWriter writer(©_str); ProjectSerializer::SaveData sdata(ProjectSerializer::k_only_nodes); sdata.set_only_serialize_nodes_and_resolve_groups(selected_nodes_); ProjectSerializer::SerializedProperties properties; for (Node *n : selected_nodes_) { NodeViewItem *item = get_assumed_item_for_selected_node(n); if (item) { Node::Position pos = item->get_node_position_data(); properties[n][QStringLiteral("x")] = QString::number(pos.position.x()); properties[n][QStringLiteral("y")] = QString::number(pos.position.y()); properties[n][QStringLiteral("expanded")] = QString::number(pos.expanded); } } sdata.set_properties(properties); ProjectSerializer::save(&writer, sdata); Core::copy_string_to_clipboard(copy_str); if (cut) { delete_selected(); } } void NodeView::paste() { if (contexts_.isEmpty()) { return; } ProjectSerializer::Result res = ProjectSerializer::paste(ProjectSerializer::k_only_nodes); if (res.get_load_data().nodes.isEmpty()) { return; } Node::PositionMap map; for (auto it = res.get_load_data().properties.cbegin(); it != res.get_load_data().properties.cend(); it++) { Node::Position pos; const QMap &node_props = it.value(); pos.position.setX(node_props.value(QStringLiteral("x")).toDouble()); pos.position.setY(node_props.value(QStringLiteral("y")).toDouble()); pos.expanded = node_props.value(QStringLiteral("expanded")).toDouble(); map.insert(it.key(), pos); } post_paste(res.get_load_data().nodes, map); } void NodeView::duplicate() { if (!selected_nodes_.isEmpty()) { QVector selected = selected_nodes_; QVector new_nodes; Node::PositionMap map; new_nodes.resize(selected.size()); // Create copies of each selected node, checking for groups and adding children if necessary for (int i = 0; i < selected.size(); i++) { new_nodes[i] = selected.at(i)->copy(); if (NodeGroup *g = dynamic_cast(selected.at(i))) { for (auto it = g->get_context_positions().cbegin(); it != g->get_context_positions().cend(); it++) { if (!selected.contains(it.key())) { // This should automatically recurse if this is a group inside a group selected.append(it.key()); } } new_nodes.resize(selected.size()); } } // Get positions in contexts, add input passthroughs, and copy input values/keyframes for (int i = 0; i < new_nodes.size(); i++) { Node *og = selected.at(i); Node *copy = new_nodes.at(i); Node::Position pos; if (get_assumed_position_for_selected_node(og, &pos)) { map.insert(copy, pos); } for (auto it = og->get_context_positions().cbegin(); it != og->get_context_positions().cend(); it++) { Node *child_og = it.key(); int child_index = selected.indexOf(child_og); if (child_index != -1) { Node *child_copy = new_nodes.at(child_index); copy->set_node_position_in_context(child_copy, it.value()); } } if (NodeGroup *src_group = dynamic_cast(og)) { NodeGroup *dst_group = static_cast(copy); for (auto it = src_group->get_input_passthroughs().cbegin(); it != src_group->get_input_passthroughs().cend(); it++) { NodeInput input = it->second; input.set_node( new_nodes.at(selected.indexOf(input.node()))); dst_group->add_input_passthrough(input, it->first); } dst_group->set_output_passthrough(new_nodes.at( selected.indexOf(src_group->get_output_passthrough()))); } Node::copy_inputs(selected.at(i), new_nodes.at(i), false); } // Copy connections Node::copy_dependency_graph(selected, new_nodes, nullptr); // Set root level context positions and attach to post_paste(new_nodes, map); } } void NodeView::set_color_label(int index) { MultiUndoCommand *command = new MultiUndoCommand(); for (Node *node : qAsConst(selected_nodes_)) { command->add_child(new NodeOverrideColorCommand(node, index)); } Core::instance()->undo_stack()->push( command, tr("Set Color of %1 Node(s)").arg(selected_nodes_.size())); } void NodeView::zoom_in() { zoom_from_keyboard(1.25); } void NodeView::zoom_out() { zoom_from_keyboard(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: { MultiUndoCommand *pos_command = new MultiUndoCommand(); for (Node *n : qAsConst(selected_nodes_)) { for (Node *context : qAsConst(contexts_)) { if (context->context_contains_node(n)) { Node::Position old_pos = context->get_node_position_in_context(n); // 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::screen_to_node_point( node_movement, scene_.get_flow_direction()); // Move command pos_command->add_child(new NodeSetPositionCommand( n, context, old_pos + node_movement)); } } } Core::instance()->undo_stack()->push( pos_command, tr("Moved %1 Node(s)").arg(selected_nodes_.size())); break; } case Qt::Key_Escape: if (!attached_items_.isEmpty()) { detach_items_from_cursor(); break; } emit esc_pressed(); /* fall through */ default: super::keyPressEvent(event); break; } } void NodeView::mousePressEvent(QMouseEvent *event) { // Handle mouse press event if (hand_press(event)) return; // Get the item that the user clicked on, if any QGraphicsItem *item = itemAt(event->pos()); if (event->button() == Qt::LeftButton) { // Sane defaults create_edge_already_exists_ = false; create_edge_from_output_ = true; create_edge_input_.reset(); if (event->modifiers() & Qt::ControlModifier) { NodeViewItem *mouse_item = dynamic_cast(item); if (mouse_item) { if (mouse_item->is_output_item()) { create_edge_output_item_ = mouse_item; } else { create_edge_input_item_ = mouse_item; create_edge_input_ = mouse_item->get_input(); create_edge_from_output_ = false; } // Highlight start item for better user experience mouse_item->set_highlighted(true); } } if (!create_edge_output_item_ && !create_edge_input_item_) { // Determine if user clicked on a connector if (NodeViewItemConnector *connector = dynamic_cast(item)) { NodeViewItem *attached = static_cast(connector->parentItem()); if (connector->is_output()) { create_edge_output_item_ = attached; } else { create_edge_input_item_ = attached; if (!create_edge_input_item_->edges().isEmpty()) { // Drag existing edge instead create_edge_ = create_edge_input_item_->edges().first(); create_edge_input_item_ = nullptr; create_edge_output_item_ = create_edge_->from_item(); create_edge_already_exists_ = true; } else { create_edge_from_output_ = false; create_edge_input_ = create_edge_input_item_->get_input(); } } } } if ((create_edge_output_item_ || create_edge_input_item_) && !create_edge_already_exists_) { // Create a new edge from this output create_edge_ = new NodeViewEdge(); create_edge_->set_curved(scene_.get_edges_are_curved()); // Add edge to scene scene_.addItem(create_edge_); // Position edge to mouse cursor position_new_edge(event->pos()); return; } } // Handle selections with the right mouse button 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 (attached_items_.isEmpty()) { // Default QGraphicsView functionality (selecting, dragging, etc.) super::mousePressEvent(event); } // For any selected item, store its position in case the user is dragging it somewhere else auto selected_items = scene_.get_selected_items(); foreach (NodeViewItem *i, selected_items) { // Ignore items attached to the cursor if (!is_item_attached_to_cursor(i)) { dragging_items_.insert(i, i->get_node_position()); } } } void NodeView::mouseMoveEvent(QMouseEvent *event) { if (hand_move(event)) return; if (create_edge_) { position_new_edge(event->pos()); return; } if (attached_items_.isEmpty()) { super::mouseMoveEvent(event); } // See if there are any items attached if (!attached_items_.isEmpty()) { process_moving_attached_nodes(event->pos()); } } void NodeView::mouseReleaseEvent(QMouseEvent *event) { if (hand_release(event)) return; if (create_edge_) { end_edge_drag(); } MultiUndoCommand *command = new MultiUndoCommand(); Node *select_context = nullptr; QVector select_nodes; bool had_attached_items = !attached_items_.isEmpty(); if (!attached_items_.isEmpty()) { select_context = get_context_at_mouse_pos(event->pos()); if (select_context) { select_nodes = process_dropping_attached_nodes(command, select_context, event->pos()); } else { QToolTip::showText(QCursor::pos(), tr("Nodes must be placed inside a context.")); } } QList> dragged_items; for (auto it = dragging_items_.cbegin(); it != dragging_items_.cend(); it++) { dragged_items.append(it.key()); } foreach (NodeViewItem *i, dragged_items) { if (!i) { continue; } QPointF current_pos = i->get_node_position(); if (dragging_items_.value(i) != current_pos) { command->add_child(new NodeSetPositionCommand( i->get_node(), i->get_context(), current_pos)); } } Core::instance()->undo_stack()->push( command, tr("Moved %1 Node(s)").arg(dragging_items_.size())); dragging_items_.clear(); if (!had_attached_items) { super::mouseReleaseEvent(event); } if (select_context) { deselect_all(); scene_.context_map().value(select_context)->select(select_nodes); } } void NodeView::mouseDoubleClickEvent(QMouseEvent *event) { super::mouseDoubleClickEvent(event); if (!(event->modifiers() & Qt::ControlModifier)) { NodeViewItem *item_at_cursor = dynamic_cast(itemAt(event->pos())); if (item_at_cursor) { item_at_cursor->toggle_expanded(); } } } void NodeView::dragEnterEvent(QDragEnterEvent *event) { if (contexts_.empty()) { event->ignore(); return; } QStringList mime_fmts = event->mimeData()->formats(); if (mime_fmts.contains(Project::k_item_mime_type)) { QByteArray model_data = event->mimeData()->data(Project::k_item_mime_type); QDataStream stream(&model_data, QIODevice::ReadOnly); // Variables to deserialize into quintptr item_ptr; QVector enabled_streams; QVector new_attached; int y = 0; while (!stream.atEnd()) { stream >> enabled_streams >> item_ptr; // Get Item object Node *item = reinterpret_cast(item_ptr); if (ViewerOutput *f = dynamic_cast(item)) { NodeViewItem *new_item; new_item = new NodeViewItem(f, nullptr); new_item->set_flow_direction(scene_.get_flow_direction()); new_item->set_node_position(QPointF(0, y)); y++; scene_.addItem(new_item); new_attached.append({ new_item, f, new_item->pos() }); } } if (new_attached.empty()) { event->ignore(); } else { set_attached_items(new_attached); event->accept(); } } } void NodeView::dragMoveEvent(QDragMoveEvent *event) { if (attached_items_.empty()) { event->ignore(); } else { process_moving_attached_nodes(event->pos()); if (get_context_at_mouse_pos(event->pos())) { event->accept(); } else { event->ignore(); } } } void NodeView::dropEvent(QDropEvent *event) { if (Node *drop_ctx = get_context_at_mouse_pos(event->pos())) { MultiUndoCommand *command = new MultiUndoCommand(); QVector select_nodes = process_dropping_attached_nodes(command, drop_ctx, event->pos()); Core::instance()->undo_stack()->push( command, tr("Dropped %1 Node(s)").arg(select_nodes.size())); deselect_all(); scene_.context_map().value(drop_ctx)->select(select_nodes); event->accept(); } else { detach_items_from_cursor(false); event->ignore(); } } void NodeView::dragLeaveEvent(QDragLeaveEvent *event) { if (attached_items_.empty()) { event->ignore(); } else { detach_items_from_cursor(false); event->accept(); } } void NodeView::resizeEvent(QResizeEvent *event) { super::resizeEvent(event); reposition_mini_map(); if (overlay_view_) { resize_overlay(); } } void NodeView::update_selection_cache() { QVector current_selection = scene_.get_selected_items(); QVector selected; QVector deselected; QVector sel_with_ctx(current_selection.size()); // Determine which nodes are newly selected for (int j = 0; j < current_selection.size(); j++) { NodeViewItem *i = current_selection.at(j); Node *n = i->get_node(); if (!selected_nodes_.contains(n)) { selected.append(n); selected_nodes_.append(n); } sel_with_ctx[j] = { n, i->get_context() }; } // Determine which nodes are newly deselected if (current_selection.isEmpty()) { // All nodes that were selected have been deselected, so we'll just set them all to `deselected` deselected = selected_nodes_; selected_nodes_.clear(); } else { foreach (Node *n, selected_nodes_) { bool still_selected = false; foreach (NodeViewItem *i, current_selection) { if (i->get_node() == n) { still_selected = true; break; } } if (!still_selected) { deselected.append(n); selected_nodes_.removeOne(n); } } } if (!deselected.isEmpty()) { emit nodes_deselected(deselected); } if (!selected.isEmpty()) { emit nodes_selected(selected); } if (!dont_emit_selection_signals_) { emit node_selection_changed(selected_nodes_); emit node_selection_changed_with_contexts(sel_with_ctx); } } void NodeView::show_context_menu(const QPoint &pos) { if (contexts_.isEmpty()) { return; } Menu m; MenuShared::instance()->add_items_for_edit_menu(&m, false); m.addSeparator(); QVector selected = scene_.get_selected_items(); NodeViewItem *item_under_cursor = dynamic_cast(itemAt(pos)); if (item_under_cursor && !selected.contains(item_under_cursor)) { // Right-clicked a node that isn't part of the current selection, // make the clicked node the sole selection so context-menu actions // operate on it. scene_.clearSelection(); item_under_cursor->setSelected(true); selected = scene_.get_selected_items(); } if (item_under_cursor && !selected.isEmpty()) { // Grouping if (selected.size() == 1 && dynamic_cast(selected.first()->get_node())) { QAction *ungroup_action = m.addAction(tr("Ungroup")); connect(ungroup_action, &QAction::triggered, this, &NodeView::ungroup_nodes); } else { QAction *group_action = m.addAction(tr("Group")); connect(group_action, &QAction::triggered, this, &NodeView::group_nodes); } // Color menu MenuShared::instance()->add_color_coding_menu(&m); // Show in Viewer option for nodes based on Viewer if (ViewerOutput *viewer = dynamic_cast(selected.first()->get_node())) { Q_UNUSED(viewer) m.addSeparator(); QAction *open_in_viewer_action = m.addAction(tr("Open in Viewer")); connect(open_in_viewer_action, &QAction::triggered, this, &NodeView::open_selected_node_in_viewer); } m.addSeparator(); // Show in Parameter Editor QAction *show_in_param_editor_action = m.addAction(tr("Show in Parameter Editor")); show_in_param_editor_action->setShortcut( show_in_param_editor_action_->shortcut()); connect(show_in_param_editor_action, &QAction::triggered, this, &NodeView::show_selected_node_in_param_editor); // Properties QAction *properties_action = m.addAction(tr("P&roperties")); connect(properties_action, &QAction::triggered, this, &NodeView::show_node_properties); } else { QAction *curved_action = m.addAction(tr("Smooth Edges")); curved_action->setCheckable(true); curved_action->setChecked(scene_.get_edges_are_curved()); connect(curved_action, &QAction::triggered, &scene_, &NodeViewScene::set_edges_are_curved); m.addSeparator(); Menu *direction_menu = new Menu(tr("Direction"), &m); m.addMenu(direction_menu); direction_menu->add_action_with_data(tr("Top to Bottom"), NodeViewCommon::k_top_to_bottom, scene_.get_flow_direction()); direction_menu->add_action_with_data(tr("Bottom to Top"), NodeViewCommon::k_bottom_to_top, scene_.get_flow_direction()); direction_menu->add_action_with_data(tr("Left to Right"), NodeViewCommon::k_left_to_right, scene_.get_flow_direction()); direction_menu->add_action_with_data(tr("Right to Left"), NodeViewCommon::k_right_to_left, scene_.get_flow_direction()); connect(direction_menu, &Menu::triggered, this, &NodeView::context_menu_set_direction); m.addSeparator(); Menu *add_menu = create_add_menu(&m); m.addMenu(add_menu); } m.exec(mapToGlobal(pos)); } void NodeView::create_node_slot(QAction *action) { Node *new_node = NodeFactory::CreateFromMenuAction(action); if (new_node) { NodeViewItem *new_item = new NodeViewItem(new_node, nullptr); new_item->set_flow_direction(scene_.get_flow_direction()); scene_.addItem(new_item); QVector new_attached; new_attached.append({ new_item, new_node, QPointF(0, 0) }); if (NodeGroup *new_group = dynamic_cast(new_node)) { for (auto it = new_group->get_context_positions().cbegin(); it != new_group->get_context_positions().cend(); it++) { new_attached.append({ nullptr, it.key(), QPointF(0, 0) }); } } set_attached_items(new_attached); } } void NodeView::context_menu_set_direction(QAction *action) { set_flow_direction( static_cast(action->data().toInt())); } void NodeView::open_selected_node_in_viewer() { // Find first viewer in list of selected nodes and open it foreach (Node *n, selected_nodes_) { if (ViewerOutput *viewer = dynamic_cast(n)) { Core::instance()->open_node_in_viewer(viewer); break; } } } void NodeView::update_scene_bounding_rect() { // 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::center_on_items_bounding_rect() { centerOn(scene_.itemsBoundingRect().center()); } void NodeView::center_on_node(Node *n) { foreach (NodeViewContext *ctx, scene_.context_map()) { if (NodeViewItem *item = ctx->get_item_from_map(n)) { centerOn(item); break; } } } void NodeView::reposition_mini_map() { 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); update_viewport_on_mini_map(); } } void NodeView::update_viewport_on_mini_map() { if (minimap_->isVisible()) { minimap_->set_viewport_rect(mapToScene(viewport()->rect())); } } void NodeView::move_to_scene_point(const QPointF &pos) { centerOn(pos); } void NodeView::node_removed_from_graph() { Node *context = static_cast(sender()); remove_context(context); contexts_.removeOne(context); } void NodeView::detach_items_from_cursor(bool delete_nodes_too) { foreach (const AttachedItem &ai, attached_items_) { delete ai.item; if (delete_nodes_too) { qDebug() << "deleting" << ai.node; delete ai.node; } } attached_items_.clear(); } void NodeView::set_flow_direction(NodeViewCommon::FlowDirection dir) { scene_.set_flow_direction(dir); } void NodeView::move_attached_nodes_to_cursor(const QPoint &p) { QPointF item_pos = mapToScene(p); for (const AttachedItem &i : qAsConst(attached_items_)) { if (i.item) { i.item->setPos(item_pos + i.original_pos); } } } void NodeView::process_moving_attached_nodes(const QPoint &pos) { // Move those items to the cursor move_attached_nodes_to_cursor(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->get_node(); QRect edge_detect_rect(pos, 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 for (QGraphicsItem *item : qAsConst(items)) { new_drop_edge = dynamic_cast(item); if (new_drop_edge) { drop_input_.reset(); NodeValue::Type drop_edge_data_type = new_drop_edge->input().get_data_type(); // Determine best input to connect to our new node if (attached_node->get_effect_input().is_valid()) { // If node specifies an effect input, use that immediately drop_input_ = attached_node->get_effect_input(); } else { // Otherwise, we may have to iterate to find a valid one for (const QString &input : attached_node->inputs()) { if (input == Node::k_enabled_input) { // Ignore enabled input continue; } NodeInput i(attached_node, input); if (attached_node->is_input_connectable(input)) { if (attached_node->get_input_data_type(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_.is_valid()) { // Default to first connectable input drop_input_ = i; } } } } if (attached_node->inputs_from(new_drop_edge->input().node(), true)) { drop_input_.reset(); } if (drop_input_.is_valid()) { break; } else { new_drop_edge = nullptr; } } } if (drop_edge_ != new_drop_edge) { if (drop_edge_) { drop_edge_->set_highlighted(false); } drop_edge_ = new_drop_edge; if (drop_edge_) { drop_edge_->set_highlighted(true); } } } } QVector NodeView::process_dropping_attached_nodes(MultiUndoCommand *command, Node *select_context, const QPoint &pos) { QVector select_nodes; // Make a copy QVector attached = attached_items_; for (int i = 0; i < attached.size(); i++) { const AttachedItem &ai = attached.at(i); if (ai.node->inputs_from(select_context, true)) { attached.removeAt(i); } else if (select_context->context_contains_node(ai.node)) { select_nodes.append(ai.node); attached.removeAt(i); } } { MultiUndoCommand *add_command = new MultiUndoCommand(); foreach (const AttachedItem &ai, attached) { // Add node to the same graph that the context is in if (ai.node->parent() != select_context->parent()) { add_command->add_child( new NodeAddCommand(select_context->parent(), ai.node)); if (ai.node->is_item() && !ai.node->folder()) { add_command->add_child(new FolderAddChild( select_context->parent()->root(), ai.node)); } } // Add node to the context if (ai.item) { select_nodes.append(ai.node); add_command->add_child(new NodeSetPositionCommand( ai.node, select_context, scene_.context_map() .value(select_context) ->map_scene_pos_to_node_pos_in_context(ai.item->pos()))); } } if (add_command->child_count()) { add_command->redo_now(); command->add_child(add_command); } else { delete add_command; } } { // Dropped attached item onto an edge, connect it between them MultiUndoCommand *drop_edge_command = new MultiUndoCommand(); if (attached.size() == 1) { Node *dropping_node = nullptr; foreach (const AttachedItem &ai, attached) { if (ai.item && !ai.node->inputs_from(select_context, true)) { dropping_node = ai.node; break; } } if (dropping_node && 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_now(); command->add_child(drop_edge_command); } else { delete drop_edge_command; } } detach_items_from_cursor(false); return select_nodes; } Node *NodeView::get_context_at_mouse_pos(const QPoint &p) { QList items_at_cursor = this->items(p); foreach (QGraphicsItem *i, items_at_cursor) { if (NodeViewContext *context_item = dynamic_cast(i)) { return context_item->get_context(); } } return nullptr; } void NodeView::connect_selection_changed_signal() { connect(&scene_, &QGraphicsScene::selectionChanged, this, &NodeView::update_selection_cache); } void NodeView::disconnect_selection_changed_signal() { disconnect(&scene_, &QGraphicsScene::selectionChanged, this, &NodeView::update_selection_cache); } void NodeView::zoom_into_cursor_position(QWheelEvent *event, double multiplier, const QPointF &cursor_pos) { Q_UNUSED(event) double test_scale = scale_ * multiplier; if (test_scale > k_minimum_scale) { 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); } bool NodeView::eventFilter(QObject *object, QEvent *event) { return super::eventFilter(object, event); } void NodeView::changeEvent(QEvent *e) { // Add translation code super::changeEvent(e); } void NodeView::zoom_from_keyboard(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); } zoom_into_cursor_position(nullptr, multiplier, cursor_pos); } void NodeView::clear_create_edge_input_if_necessary() { if (create_edge_from_output_ && create_edge_input_.is_valid()) { create_edge_input_.reset(); } } QPointF NodeView::get_estimated_position_for_context(NodeViewItem *item, Node *context) const { return item->get_node_position() - context_offsets_.value(context); } NodeViewItem *NodeView::get_assumed_item_for_selected_node(Node *node) { // Try to find corresponding selected item foreach (NodeViewContext *ctx, scene_.context_map()) { NodeViewItem *item = ctx->get_item_from_map(node); if (item && item->get_node() == node && item->isSelected()) { // Good enough return item; } } return nullptr; } bool NodeView::get_assumed_position_for_selected_node(Node *node, Node::Position *pos) { if (NodeViewItem *item = get_assumed_item_for_selected_node(node)) { *pos = item->get_node_position_data(); return true; } else { return false; } } Menu *NodeView::create_add_menu(Menu *parent) { Menu *add_menu = NodeFactory::create_menu(parent); add_menu->setTitle(tr("Add")); connect(add_menu, &Menu::triggered, this, &NodeView::create_node_slot); return add_menu; } void NodeView::position_new_edge(const QPoint &pos) { // Determine scene coordinate QPointF scene_pt = mapToScene(pos); // Find if the cursor is currently inside an item NodeViewItem *item_at_cursor = dynamic_cast(itemAt(pos)); NodeViewItem *source_item = create_edge_from_output_ ? create_edge_output_item_ : create_edge_input_item_; NodeViewItem *&opposing_item = create_edge_from_output_ ? create_edge_input_item_ : create_edge_output_item_; // Filter out connecting to self if (item_at_cursor && item_at_cursor->get_node() == source_item->get_node()) { item_at_cursor = nullptr; } // Collapse any items that the cursor is no longer inside int i = create_edge_expanded_items_.size() - 1; for (; i >= 0; i--) { NodeViewItem *nvi = create_edge_expanded_items_.at(i); QPointF local_pt = nvi->mapFromScene(scene_pt); if (nvi->scene() == &scene_ && (nvi->contains(local_pt) || (!nvi->is_output_item() && nvi->parentItem()->contains( nvi->parentItem()->mapFromScene(scene_pt)) && local_pt.y() > nvi->rect().bottom()))) { break; } else { // Collapsing an item will destroy its children, so if the cursor item happens to be a child // of the item we're about to collapse, set it to null if (item_at_cursor && item_at_cursor->parentItem() == nvi) { item_at_cursor = nullptr; } if (opposing_item && opposing_item->parentItem() == nvi) { opposing_item = nullptr; clear_create_edge_input_if_necessary(); } collapse_item(nvi); } } create_edge_expanded_items_.resize(i + 1); // Expand item if possible if (item_at_cursor && item_at_cursor->can_be_expanded() && !item_at_cursor->is_expanded() && create_edge_from_output_) { expand_item(item_at_cursor); create_edge_expanded_items_.append(item_at_cursor); } // Filter out connecting to a node that connects to us or an item of the same type if (item_at_cursor && ((create_edge_from_output_ && source_item->get_node()->inputs_from( item_at_cursor->get_node(), true)) || (!create_edge_from_output_ && item_at_cursor->get_node()->inputs_from( source_item->get_node(), true)) || (create_edge_from_output_ == item_at_cursor->is_output_item()))) { item_at_cursor = nullptr; } // Filter out "output node" of the context, we assume users won't want to fetch the output of this if (item_at_cursor && !create_edge_from_output_ && item_at_cursor->is_labelled_as_output_of_context()) { item_at_cursor = nullptr; } // If the item has changed if (item_at_cursor != opposing_item) { // If we had a destination active, disconnect from it since the item has changed if (opposing_item) { opposing_item->set_highlighted(false); opposing_item = nullptr; } // Clear cached input clear_create_edge_input_if_necessary(); // If this is an input and we're opposing_item = item_at_cursor; if (opposing_item) { opposing_item->set_highlighted(true); if (!opposing_item->is_output_item()) { create_edge_input_ = opposing_item->get_input(); } } } QPointF output_point = create_edge_output_item_ ? create_edge_output_item_->get_output_point() : scene_pt; QPointF input_point = create_edge_input_.is_valid() ? create_edge_input_item_->get_input_point() : scene_pt; create_edge_->set_points(output_point, input_point); create_edge_->set_connected(create_edge_output_item_ && create_edge_input_.is_valid()); } void NodeView::group_nodes() { // Get items QVector items = scene_.get_selected_items(); if (items.isEmpty()) { return; } // Get node context Node *context = items.first()->get_context(); QPointF avg_pos = items.first()->get_node_position(); for (int i = 1; i < items.size(); i++) { if (items.at(i)->get_context() != context) { QMessageBox::critical( this, tr("Failed to group nodes"), tr("Nodes can only be grouped if they're in the same context.")); return; } avg_pos += items.at(i)->get_node_position(); } avg_pos /= items.size(); // Create group NodeGroup *group = new NodeGroup(); // Add group to graph and context MultiUndoCommand *command = new MultiUndoCommand(); // Add nodes to group Node *output_passthrough = nullptr; QVector nodes_to_group = selected_nodes_; deselect_all(); foreach (Node *n, nodes_to_group) { command->add_child( new NodeRemovePositionFromContextCommand(n, context)); command->add_child(new NodeSetPositionCommand( n, group, context->get_node_position_data_in_context(n))); for (auto it = n->inputs().cbegin(); it != n->inputs().cend(); it++) { NodeInput input(n, *it, -1); if (!input.is_connected() || !nodes_to_group.contains(input.get_connected_output())) { command->add_child( new NodeGroupAddInputPassthrough(group, input)); } } if (!output_passthrough) { // Default to the first node we find that doesn't output to a node inside the group output_passthrough = nodes_to_group.first(); foreach (Node *potential_in, nodes_to_group) { if (potential_in != n && !potential_in->inputs_from(n, false)) { output_passthrough = n; break; } } } } // Set output passthrough command->add_child( new NodeGroupSetOutputPassthrough(group, output_passthrough)); // Add group to graph command->add_child(new NodeAddCommand(context->parent(), group)); command->add_child(new NodeSetPositionCommand(group, context, avg_pos)); // Do command Core::instance()->label_nodes({ group }, command); Core::instance()->undo_stack()->push(command, tr("Grouped Nodes")); } void NodeView::ungroup_nodes() { NodeViewItem *group_item = nullptr; QVector items = scene_.get_selected_items(); if (items.isEmpty()) { return; } NodeGroup *group = nullptr; foreach (NodeViewItem *i, items) { if ((group = dynamic_cast(i->get_node()))) { group_item = i; break; } } if (!group_item) { return; } MultiUndoCommand *command = new MultiUndoCommand(); Node *context = group_item->get_context(); command->add_child( new NodeRemovePositionFromContextCommand(group, context)); command->add_child(new NodeRemoveAndDisconnectCommand(group)); for (auto it = group->get_context_positions().cbegin(); it != group->get_context_positions().cend(); it++) { command->add_child( new NodeRemovePositionFromContextCommand(it.key(), group)); command->add_child(new NodeSetPositionCommand( it.key(), context, group->get_node_position_data_in_context(it.key()))); } Core::instance()->undo_stack()->push(command, tr("Ungrouped Nodes")); } void NodeView::show_node_properties() { Node *first_node = selected_nodes_.first(); if (NodeGroup *group = dynamic_cast(first_node)) { if (!overlay_view_) { overlay_view_ = new NodeView(this); overlay_view_->show(); QPushButton *overlay_close_btn = new QPushButton(overlay_view_); overlay_close_btn->setIcon(icon::error); int offset = overlay_close_btn->sizeHint().width() / 2; overlay_close_btn->move(offset, offset); overlay_close_btn->show(); connect(overlay_view_, &NodeView::nodes_selected, this, &NodeView::nodes_selected); connect(overlay_view_, &NodeView::nodes_deselected, this, &NodeView::nodes_deselected); connect(overlay_view_, &NodeView::node_group_opened, this, &NodeView::node_group_opened); connect(overlay_view_, &NodeView::node_group_closed, this, &NodeView::node_group_closed); connect(overlay_view_, &NodeView::esc_pressed, this, &NodeView::close_overlay); connect(overlay_close_btn, &QPushButton::clicked, this, &NodeView::close_overlay); const QColor &bgcol = overlay_view_->palette().base().color(); overlay_view_->setStyleSheet( QStringLiteral( "QGraphicsView { background: rgba(%1, %2, %3, 0.8); }") .arg(QString::number(bgcol.red()), QString::number(bgcol.green()), QString::number(bgcol.blue()))); overlay_close_btn->setStyleSheet( QStringLiteral("background: transparent; border: none;")); } overlay_view_->set_contexts({ group }); resize_overlay(); QMetaObject::invokeMethod(overlay_view_, &NodeView::center_on_items_bounding_rect, Qt::QueuedConnection); overlay_view_->setFocus(); emit nodes_deselected(selected_nodes_); emit node_selection_changed(QVector()); emit node_selection_changed_with_contexts(QVector()); overlay_view_->select_all(); emit node_group_opened(group); } else { label_selected_nodes(); } } void NodeView::show_selected_node_in_param_editor() { QVector selected = scene_.get_selected_items(); if (selected.isEmpty()) { return; } QVector selection_with_contexts; selection_with_contexts.reserve(selected.size()); foreach (NodeViewItem *item, selected) { if (item && item->get_node()) { selection_with_contexts.append( Node::ContextPair{ item->get_node(), item->get_context() }); } } if (selection_with_contexts.isEmpty()) { return; } if (PanelManager::instance()) { if (PanelWidget *panel = PanelManager::instance()->get_panel_with_name( QStringLiteral("ParamPanel"))) { panel->show(); QMetaObject::invokeMethod(panel, &PanelWidget::raise, Qt::QueuedConnection); QMetaObject::invokeMethod( panel, [panel]() { panel->activateWindow(); panel->setFocus(Qt::OtherFocusReason); }, Qt::QueuedConnection); } } emit node_selection_changed_with_contexts(selection_with_contexts); } void NodeView::label_selected_nodes() { Core::instance()->label_nodes(selected_nodes_); } void NodeView::item_about_to_be_deleted(NodeViewItem *item) { dragging_items_.remove(item); if (create_edge_) { // Item should be removed from scene, but not yet deleted, allowing a safe PositionNewEdge call // to disconnect position_new_edge(mapFromGlobal(QCursor::pos())); QGraphicsItem *test = item; do { if (test == item) { break; } test = test->parentItem(); } while (test); if (test == item) { // Cancel edge function end_edge_drag(true); } } } void NodeView::close_overlay() { if (overlay_view_->overlay_view_) { overlay_view_->close_overlay(); } overlay_view_->deleteLater(); overlay_view_ = nullptr; emit node_group_closed(); } void NodeView::add_context(Node *n) { NodeViewContext *ctx = scene_.add_context(n); connect(ctx, &NodeViewContext::item_about_to_be_deleted, this, &NodeView::item_about_to_be_deleted); connect(n, &Node::removed_from_graph, this, &NodeView::node_removed_from_graph); } void NodeView::remove_context(Node *n) { scene_.remove_context(n); disconnect(n, &Node::removed_from_graph, this, &NodeView::node_removed_from_graph); } bool NodeView::is_item_attached_to_cursor(NodeViewItem *item) const { foreach (const AttachedItem &ai, attached_items_) { if (ai.item == item) { return true; } } return false; } void NodeView::expand_item(NodeViewItem *item) { item->set_expanded(true); item->setZValue(100); } void NodeView::collapse_item(NodeViewItem *item) { item->set_expanded(false); item->setZValue(0); } void NodeView::end_edge_drag(bool cancel) { // Check if the edge was reconnected to the same place as before MultiUndoCommand *command = new MultiUndoCommand(); bool reconnected_to_itself = false; if (create_edge_already_exists_) { if (!cancel) { if (create_edge_output_item_ == create_edge_->from_item() && create_edge_->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())); } } } else { // We're creating a new edge, which means this UI object is only temporary delete create_edge_; } create_edge_ = nullptr; // Clear highlight if we set one if (create_edge_output_item_) { create_edge_output_item_->set_highlighted(false); } if (create_edge_input_item_) { create_edge_input_item_->set_highlighted(false); } QString command_name; NodeInput &creating_input = create_edge_input_; if (create_edge_output_item_ && create_edge_input_item_ && !cancel) { if (creating_input.is_valid()) { // Make connection if (!reconnected_to_itself) { Node *creating_output = create_edge_output_item_->get_node(); while (NodeGroup *output_group = dynamic_cast(creating_output)) { creating_output = output_group->get_output_passthrough(); } while (NodeGroup *input_group = dynamic_cast(creating_input.node())) { creating_input = input_group->get_input_from_id(creating_input.input()); } if (creating_input.is_connected()) { Node::OutputConnection existing_edge_to_remove = { creating_input.get_connected_output(), creating_input }; Node *already_connected_output = creating_input.get_connected_output(); NodeViewContext *ctx = get_context_item_from_node_item(create_edge_input_item_); if (ctx && !ctx->get_item_from_map(already_connected_output)) { if (QMessageBox::warning( this, QString(), tr("Input \"%1\" is currently connected to node \"%2\", which is not visible in this context. " "By connecting this, that connection will be removed. Do you wish to continue?") .arg(creating_input.name(), already_connected_output ->get_label_and_name()), QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { cancel = true; } } if (!cancel) { command->add_child(new NodeEdgeRemoveCommand( existing_edge_to_remove.first, existing_edge_to_remove.second)); } } if (!cancel) { command->add_child(new NodeEdgeAddCommand(creating_output, creating_input)); command_name = Node::get_connect_command_string( creating_output, creating_input); // If the output is not in the input's context, add it now. We check the item rather than // the node itself, because sometimes a node may not be in the context but another node // representing it will be (e.g. groups) if (!scene_.context_map() .value(create_edge_input_item_->get_context()) ->get_item_from_map(creating_output)) { command->add_child(new NodeSetPositionCommand( creating_output, create_edge_input_item_->get_context(), scene_.context_map() .value(create_edge_input_item_->get_context()) ->map_scene_pos_to_node_pos_in_context( create_edge_output_item_->scenePos()))); } } } } } creating_input.reset(); create_edge_output_item_ = nullptr; create_edge_input_item_ = nullptr; // Collapse any items we expanded for (auto it = create_edge_expanded_items_.crbegin(); it != create_edge_expanded_items_.crend(); it++) { collapse_item(*it); } create_edge_expanded_items_.clear(); Core::instance()->undo_stack()->push(command, command_name); } void NodeView::post_paste(const QVector &new_nodes, const Node::PositionMap &map) { QVector new_attached; NodeViewItem *first_item = nullptr; for (int i = 0; i < new_nodes.size(); i++) { Node *node = new_nodes.at(i); // Determine if item had a position, if not don't create an item for it NodeViewItem *new_item; if (map.contains(node)) { new_item = new NodeViewItem(node, nullptr); new_item->set_flow_direction(scene_.get_flow_direction()); new_item->set_node_position(map.value(node)); scene_.addItem(new_item); if (!first_item) { first_item = new_item; } } else { new_item = nullptr; } new_attached.append({ new_item, node, QPointF(0, 0) }); } // Correct positions if (first_item) { for (int i = 0; i < new_attached.size(); i++) { AttachedItem &ai = new_attached[i]; if (ai.item) { ai.original_pos = ai.item->pos() - first_item->pos(); } } } set_attached_items(new_attached); } void NodeView::resize_overlay() { overlay_view_->resize(this->size()); } NodeViewContext *NodeView::get_context_item_from_node_item(NodeViewItem *item) { QGraphicsItem *i = item; while ((i = i->parentItem())) { if (NodeViewContext *nvc = dynamic_cast(i)) { return nvc; } } return nullptr; } void NodeView::set_attached_items(const QVector &items) { // Detach anything currently attached detach_items_from_cursor(); attached_items_ = items; // Move to cursor move_attached_nodes_to_cursor(mapFromGlobal(QCursor::pos())); } }