/*** 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 "oakengine/node.h" #include "oakengine/project.h" #include "oakengine/serializer.h" #include "oakengine/undo.h" #include "panel/panelmanager.h" #include "node/traverser.h" #include "ui/icons/icons.h" #include "widget/menu/factorymenu.h" #include "widget/menu/menushared.h" #include "widget/timebased/timebasedview.h" #define super HandMovableView namespace olive { namespace { QVector node_vector_to_engine(const QVector &nodes) { QVector result; result.reserve(nodes.size()); foreach (Node *n, nodes) { result.append(reinterpret_cast(n)); } return result; } QVector> context_pair_vector_to_engine(const QVector &pairs) { QVector> result; result.reserve(pairs.size()); foreach (const Node::ContextPair &p, pairs) { result.append(qMakePair(reinterpret_cast(p.node), reinterpret_cast(p.context))); } return result; } } // namespace 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) , bridge_(new EngineEventBridge(this)) { 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(bridge_, &EngineEventBridge::node_removed_from_graph, this, &NodeView::node_removed_from_graph); 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() { QVector nodes; QVector contexts; QVector edges; foreach (NodeViewContext *ctx, scene_.context_map()) { ctx->get_selected_for_deletion(nodes, contexts, edges); } if (nodes.isEmpty() && edges.isEmpty()) { return; } // Build C arrays for the facade QVector oak_nodes(nodes.size()); QVector oak_contexts(nodes.size()); for (int i = 0; i < nodes.size(); i++) { oak_nodes[i] = reinterpret_cast(nodes[i]); oak_contexts[i] = reinterpret_cast(contexts[i]); } QVector edge_outputs(edges.size()); QVector edge_input_nodes(edges.size()); QVector edge_input_ids_storage(edges.size()); QVector edge_input_ids(edges.size()); QVector edge_input_elements(edges.size()); for (int i = 0; i < edges.size(); i++) { edge_outputs[i] = reinterpret_cast(edges[i]->output()); edge_input_nodes[i] = reinterpret_cast(edges[i]->input().node()); edge_input_ids_storage[i] = edges[i]->input().input().toUtf8(); edge_input_ids[i] = edge_input_ids_storage[i].constData(); edge_input_elements[i] = edges[i]->input().element(); } // ONE undoable command whether this deletes nodes, edges, or both oakengine_nodes_delete_many( oak_nodes.isEmpty() ? nullptr : oak_nodes.constData(), oak_contexts.isEmpty() ? nullptr : oak_contexts.constData(), nodes.size(), edge_outputs.isEmpty() ? nullptr : edge_outputs.constData(), edge_input_nodes.isEmpty() ? nullptr : edge_input_nodes.constData(), edge_input_ids.isEmpty() ? nullptr : edge_input_ids.constData(), edge_input_elements.isEmpty() ? nullptr : edge_input_elements.constData(), edges.size()); } 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(node_vector_to_engine(selected_nodes_)); selected_nodes_.clear(); emit node_selection_changed(node_vector_to_engine(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 auto &p, nodes) { Node *node = reinterpret_cast(p.first); Node *context = reinterpret_cast(p.second); NodeViewContext *ctx = scene_.context_map().value(context); if (ctx) { NodeViewItem *item = ctx->get_item_from_map(node); if (item) { item->setSelected(true); } } } // Center on something if (center_view_on_item && !nodes.isEmpty()) { QMetaObject::invokeMethod(this, "center_on_node", Qt::QueuedConnection, Q_ARG(OakEngineNode *, nodes.first().first)); } 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; } OakEngineClipboard *cb = oakengine_clipboard_create( OAKENGINE_CLIPBOARD_NODES, nullptr, nullptr); oakengine_clipboard_set_nodes( cb, reinterpret_cast( selected_nodes_.constData()), selected_nodes_.size()); for (Node *n : selected_nodes_) { NodeViewItem *item = get_assumed_item_for_selected_node(n); if (item) { Node::Position pos = item->get_node_position_data(); oakengine_clipboard_set_property( cb, reinterpret_cast(n), "x", QByteArray::number(pos.position.x()).constData()); oakengine_clipboard_set_property( cb, reinterpret_cast(n), "y", QByteArray::number(pos.position.y()).constData()); oakengine_clipboard_set_property( cb, reinterpret_cast(n), "expanded", QByteArray::number(pos.expanded).constData()); } } // Two-phase buf/size: query the needed length first — serialized node // graphs have no size bound, a fixed buffer would truncate large copies. const int needed = oakengine_clipboard_save_to_xml(cb, nullptr, 0); if (needed >= 0) { QByteArray buf(needed + 1, '\0'); oakengine_clipboard_save_to_xml(cb, buf.data(), buf.size()); Core::copy_string_to_clipboard(QString::fromUtf8(buf.constData())); } oakengine_clipboard_free(cb); if (cut) { delete_selected(); } } void NodeView::paste() { if (contexts_.isEmpty()) { return; } OakEngineClipboard *cb = oakengine_clipboard_create( OAKENGINE_CLIPBOARD_NODES, nullptr, nullptr); int result_code = OAKENGINE_SERIALIZER_NO_DATA; oakengine_clipboard_paste(cb, OAKENGINE_CLIPBOARD_NODES, nullptr, &result_code, nullptr, 0); if (result_code != OAKENGINE_SERIALIZER_OK) { oakengine_clipboard_free(cb); return; } QVector nodes; const int node_count = oakengine_clipboard_get_loaded_node_count(cb); nodes.reserve(node_count); for (int i = 0; i < node_count; i++) { nodes.append(reinterpret_cast( oakengine_clipboard_get_loaded_node_at(cb, i))); } Node::PositionMap map; oakengine_clipboard_foreach_property( cb, [](OakEngineNode *node, const char *key, const char *value, void *userdata) -> int { auto *m = static_cast(userdata); Node *n = reinterpret_cast(node); Node::Position &pos = (*m)[n]; if (std::strcmp(key, "x") == 0) { pos.position.setX(QString::fromUtf8(value).toDouble()); } else if (std::strcmp(key, "y") == 0) { pos.position.setY(QString::fromUtf8(value).toDouble()); } else if (std::strcmp(key, "expanded") == 0) { pos.expanded = QString::fromUtf8(value).toDouble(); } return 0; }, &map); oakengine_clipboard_free(cb); post_paste(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 (oakengine_node_is_group( reinterpret_cast(selected.at(i)))) { for (auto it = selected.at(i)->get_context_positions().cbegin(); it != selected.at(i)->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); oakengine_node_set_context_position( reinterpret_cast(copy), reinterpret_cast(child_copy), it.value().position.x(), it.value().position.y()); } } if (oakengine_node_is_group(reinterpret_cast(og))) { const int pt_count = oakengine_group_input_passthrough_count( reinterpret_cast(og)); for (int pt = 0; pt < pt_count; pt++) { OakEngineNode *inner_node = nullptr; char inner_input[256]; int inner_element = 0; char id[256]; if (oakengine_group_input_passthrough_at( reinterpret_cast(og), pt, id, sizeof(id), &inner_node, inner_input, sizeof(inner_input), &inner_element) != OAKENGINE_OK) { continue; } Node *inner = reinterpret_cast(inner_node); int src_index = selected.indexOf(inner); if (src_index == -1) { continue; } Node *copy_inner = new_nodes.at(src_index); char out_id[256]; oakengine_group_add_input_passthrough( reinterpret_cast(copy), reinterpret_cast(copy_inner), inner_input, inner_element, id, out_id, sizeof(out_id)); } if (OakEngineNode *output = oakengine_group_get_output_passthrough( reinterpret_cast(og))) { int idx = selected.indexOf(reinterpret_cast(output)); if (idx >= 0 && idx < new_nodes.size()) { oakengine_group_set_output_passthrough( reinterpret_cast(copy), reinterpret_cast( new_nodes.at(idx))); } } } oakengine_node_copy_inputs( reinterpret_cast(new_nodes.at(i)), reinterpret_cast(selected.at(i))); } // Copy connections { QVector sel_arr, new_arr; for (Node *n : selected) sel_arr.append(reinterpret_cast(n)); for (Node *n : new_nodes) new_arr.append(reinterpret_cast(n)); oakengine_node_copy_dependency_graph( sel_arr.data(), new_arr.data(), sel_arr.size(), nullptr); } // Set root level context positions and attach to post_paste(new_nodes, map); } } void NodeView::set_color_label(int index) { void *command = oakengine_undo_command_create_multi(); for (Node *node : qAsConst(selected_nodes_)) { oakengine_undo_command_multi_add_child( command, oakengine_node_set_color_label_command( reinterpret_cast(node), index)); } oakengine_undo_push( command, tr("Set Color of %1 Node(s)").arg(selected_nodes_.size()).toUtf8().constData()); } 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: { void *pos_command = oakengine_undo_command_create_multi(); 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 Node::Position new_pos = old_pos; new_pos.position += node_movement; oakengine_undo_command_multi_add_child( pos_command, oakengine_node_set_position_command( reinterpret_cast(n), reinterpret_cast(context), new_pos.position.x(), new_pos.position.y(), new_pos.expanded ? 1 : 0)); } } } oakengine_undo_push( pos_command, tr("Moved %1 Node(s)").arg(selected_nodes_.size()).toUtf8().constData()); 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(); } void *command = oakengine_undo_command_create_multi(); 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) { oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command( reinterpret_cast(i->get_node()), reinterpret_cast(i->get_context()), current_pos.x(), current_pos.y(), 0)); } } oakengine_undo_push( command, tr("Moved %1 Node(s)").arg(dragging_items_.size()).toUtf8().constData()); 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(QString::fromUtf8(oakengine_project_item_mime_type()))) { QByteArray model_data = event->mimeData()->data(QString::fromUtf8(oakengine_project_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())) { void *command = oakengine_undo_command_create_multi(); QVector select_nodes = process_dropping_attached_nodes(command, drop_ctx, event->pos()); oakengine_undo_push( command, tr("Dropped %1 Node(s)").arg(select_nodes.size()).toUtf8().constData()); 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(node_vector_to_engine(deselected)); } if (!selected.isEmpty()) { emit nodes_selected(node_vector_to_engine(selected)); } if (!dont_emit_selection_signals_) { emit node_selection_changed(node_vector_to_engine(selected_nodes_)); emit node_selection_changed_with_contexts( context_pair_vector_to_engine(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 && oakengine_node_is_group(reinterpret_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 = create_node_from_menu_action(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 (oakengine_node_is_group( reinterpret_cast(new_node))) { for (auto it = new_node->get_context_positions().cbegin(); it != new_node->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( reinterpret_cast(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(OakEngineNode *n) { foreach (NodeViewContext *ctx, scene_.context_map()) { if (NodeViewItem *item = ctx->get_item_from_map( reinterpret_cast(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(OakEngineNode *source) { Node *context = reinterpret_cast(source); 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 == QLatin1String(oakengine_node_enabled_input_id())) { // 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(void *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); } } { void *add_command = oakengine_undo_command_create_multi(); foreach (const AttachedItem &ai, attached) { // Add node to the same graph that the context is in if (ai.node->parent() != select_context->parent()) { oakengine_undo_command_multi_add_child(add_command, oakengine_node_add_to_project_command( reinterpret_cast(select_context->parent()), reinterpret_cast(ai.node))); if (ai.node->is_item() && !ai.node->folder()) { oakengine_folder_add_child( reinterpret_cast(select_context->parent()->root()), reinterpret_cast(ai.node)); } } // Add node to the context if (ai.item) { select_nodes.append(ai.node); oakengine_undo_command_multi_add_child(add_command, oakengine_node_set_position_command(reinterpret_cast(ai.node), reinterpret_cast(select_context), scene_.context_map() .value(select_context) ->map_scene_pos_to_node_pos_in_context(ai.item->pos()).x(), scene_.context_map() .value(select_context) ->map_scene_pos_to_node_pos_in_context(ai.item->pos()).y(), 0)); } } if (oakengine_undo_command_multi_child_count(add_command) > 0) { oakengine_undo_command_redo_now(add_command); oakengine_undo_command_multi_add_child(command, add_command); } else { oakengine_undo_command_free(add_command); } } { // Dropped attached item onto an edge, connect it between them as one // undoable child of the parent command. 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_) { void *edge_command = oakengine_undo_command_create_multi(); oakengine_undo_command_multi_add_child( edge_command, oakengine_node_disconnect_command( reinterpret_cast(drop_edge_->input().node()), drop_edge_->input().input().toUtf8().constData(), drop_edge_->input().element())); oakengine_undo_command_multi_add_child( edge_command, oakengine_node_connect_command( reinterpret_cast(drop_edge_->output()), reinterpret_cast(drop_input_.node()), drop_input_.input().toUtf8().constData(), drop_input_.element())); oakengine_undo_command_multi_add_child( edge_command, oakengine_node_connect_command( reinterpret_cast(dropping_node), reinterpret_cast(drop_edge_->input().node()), drop_edge_->input().input().toUtf8().constData(), drop_edge_->input().element())); oakengine_undo_command_redo_now(edge_command); oakengine_undo_command_multi_add_child(command, edge_command); } drop_edge_ = nullptr; } } 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 = create_node_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 Node *group = reinterpret_cast(oakengine_node_group_create()); // Add group to graph and context void *command = oakengine_undo_command_create_multi(); // Add nodes to group Node *output_passthrough = nullptr; QVector nodes_to_group = selected_nodes_; deselect_all(); foreach (Node *n, nodes_to_group) { oakengine_undo_command_multi_add_child(command, oakengine_node_remove_position_command(reinterpret_cast(n), reinterpret_cast(context))); oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(n), reinterpret_cast(group), context->get_node_position_data_in_context(n).position.x(), context->get_node_position_data_in_context(n).position.y(), context->get_node_position_data_in_context(n).expanded ? 1 : 0)); 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())) { oakengine_undo_command_multi_add_child(command, (void *)(oakengine_group_add_input_passthrough_command( reinterpret_cast(group), reinterpret_cast(input.node()), input.input().toUtf8().constData(), input.element(), nullptr))); } } 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 oakengine_undo_command_multi_add_child(command, (void *)(oakengine_group_set_output_passthrough_command( reinterpret_cast(group), reinterpret_cast(output_passthrough)))); // Add group to graph oakengine_undo_command_multi_add_child(command, oakengine_node_add_to_project_command( reinterpret_cast(context->parent()), reinterpret_cast(group))); oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(group), reinterpret_cast(context), avg_pos.x(), avg_pos.y(), 0)); // Do command Core::instance()->label_nodes( QVector{ reinterpret_cast(group) }, command); oakengine_undo_push(command, tr("Grouped Nodes").toUtf8().constData()); } void NodeView::ungroup_nodes() { NodeViewItem *group_item = nullptr; QVector items = scene_.get_selected_items(); if (items.isEmpty()) { return; } Node *group = nullptr; foreach (NodeViewItem *i, items) { if (oakengine_node_is_group(reinterpret_cast( i->get_node()))) { group = i->get_node(); group_item = i; break; } } if (!group_item) { return; } void *command = oakengine_undo_command_create_multi(); Node *context = group_item->get_context(); oakengine_undo_command_multi_add_child(command, oakengine_node_remove_position_command(reinterpret_cast(group), reinterpret_cast(context))); oakengine_undo_command_multi_add_child(command, oakengine_node_remove_and_disconnect_command(reinterpret_cast(group))); for (auto it = group->get_context_positions().cbegin(); it != group->get_context_positions().cend(); it++) { oakengine_undo_command_multi_add_child(command, oakengine_node_remove_position_command(reinterpret_cast(it.key()), reinterpret_cast(group))); oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(it.key()), reinterpret_cast(context), group->get_node_position_data_in_context(it.key()).position.x(), group->get_node_position_data_in_context(it.key()).position.y(), group->get_node_position_data_in_context(it.key()).expanded ? 1 : 0)); } oakengine_undo_push(command, tr("Ungrouped Nodes").toUtf8().constData()); } void NodeView::show_node_properties() { Node *first_node = selected_nodes_.first(); if (oakengine_node_is_group(reinterpret_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({ first_node }); resize_overlay(); QMetaObject::invokeMethod(overlay_view_, &NodeView::center_on_items_bounding_rect, Qt::QueuedConnection); overlay_view_->setFocus(); emit nodes_deselected(node_vector_to_engine(selected_nodes_)); emit node_selection_changed(QVector()); emit node_selection_changed_with_contexts( QVector>()); overlay_view_->select_all(); emit node_group_opened(reinterpret_cast(first_node)); } 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( context_pair_vector_to_engine(selection_with_contexts)); } void NodeView::label_selected_nodes() { Core::instance()->label_nodes( reinterpret_cast &>(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); removed_from_graph_subs_[n] = bridge_->subscribe( reinterpret_cast(n), OAKENGINE_EVENT_NODE_REMOVED_FROM_GRAPH); } void NodeView::remove_context(Node *n) { scene_.remove_context(n); bridge_->unsubscribe(removed_from_graph_subs_.take(n)); } 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 void *command = oakengine_undo_command_create_multi(); 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 oakengine_undo_command_multi_add_child( command, oakengine_node_disconnect_command( reinterpret_cast(create_edge_->input().node()), create_edge_->input().input().toUtf8().constData(), create_edge_->input().element())); } } } 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 (oakengine_node_is_group( reinterpret_cast(creating_output))) { OakEngineNode *out = oakengine_group_get_output_passthrough( reinterpret_cast(creating_output)); if (!out) { break; } creating_output = reinterpret_cast(out); } while (oakengine_node_is_group(reinterpret_cast( creating_input.node()))) { OakEngineNode *inner_node = nullptr; char inner_input[256]; int inner_element = 0; if (oakengine_group_get_passthrough_from_id( reinterpret_cast( creating_input.node()), creating_input.input().toUtf8().constData(), &inner_node, inner_input, sizeof(inner_input), &inner_element) != OAKENGINE_OK) { break; } creating_input = NodeInput( reinterpret_cast(inner_node), QString::fromUtf8(inner_input), inner_element); } 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) { oakengine_undo_command_multi_add_child( command, oakengine_node_disconnect_command( reinterpret_cast(existing_edge_to_remove.second.node()), existing_edge_to_remove.second.input().toUtf8().constData(), existing_edge_to_remove.second.element())); } } if (!cancel) { oakengine_undo_command_multi_add_child( command, oakengine_node_connect_command( reinterpret_cast(creating_output), reinterpret_cast(creating_input.node()), creating_input.input().toUtf8().constData(), creating_input.element())); { char cmd_buf[256]; oakengine_node_connect_command_string( reinterpret_cast(creating_output), reinterpret_cast(creating_input.node()), creating_input.input().toUtf8().constData(), creating_input.element(), cmd_buf, sizeof(cmd_buf)); command_name = QString::fromUtf8(cmd_buf); } // 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)) { QPointF new_pos = scene_.context_map() .value(create_edge_input_item_->get_context()) ->map_scene_pos_to_node_pos_in_context( create_edge_output_item_->scenePos()); oakengine_undo_command_multi_add_child( command, oakengine_node_set_position_command( reinterpret_cast(creating_output), reinterpret_cast( create_edge_input_item_->get_context()), new_pos.x(), new_pos.y(), 0)); } } } } } 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(); oakengine_undo_push(command, command_name.toUtf8().constData()); } 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())); } }