diff --git a/.bak/nodeview/nodeview.cpp b/.bak/nodeview/nodeview.cpp
new file mode 100644
index 000000000..f7bd014c8
--- /dev/null
+++ b/.bak/nodeview/nodeview.cpp
@@ -0,0 +1,1982 @@
+/***
+
+ 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/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/factorymenu.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 = 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 (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(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()
+{
+ 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 = 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
+ 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()));
+}
+
+}
diff --git a/.bak/nodeview/nodeview.h b/.bak/nodeview/nodeview.h
new file mode 100644
index 000000000..9298180d6
--- /dev/null
+++ b/.bak/nodeview/nodeview.h
@@ -0,0 +1,301 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_NODEVIEW_H
+#define OAK_NODEVIEW_H
+
+#include
+#include
+
+#include "core.h"
+#include "node/group/group.h"
+#include "nodeviewedge.h"
+#include "nodeviewcontext.h"
+#include "nodeviewminimap.h"
+#include "nodeviewscene.h"
+#include "widget/handmovableview/handmovableview.h"
+#include "widget/menu/menu.h"
+
+namespace olive
+{
+
+/**
+ * @brief A widget for viewing and editing node graphs
+ *
+ * This widget takes a NodeGraph object and constructs a QGraphicsScene representing its data, viewing and allowing
+ * the user to make modifications to it.
+ */
+class NodeView : public HandMovableView {
+ Q_OBJECT
+public:
+ NodeView(QWidget *parent = nullptr);
+
+ virtual ~NodeView() override;
+
+ void set_contexts(const QVector &nodes);
+
+ const QVector &get_contexts() const
+ {
+ if (overlay_view_) {
+ return overlay_view_->get_contexts();
+ } else {
+ return contexts_;
+ }
+ }
+
+ bool is_group_overlay() const
+ {
+ return overlay_view_;
+ }
+
+ void close_contexts_belonging_to_project(Project *project);
+
+ void clear_graph();
+
+ /**
+ * @brief Delete selected nodes from graph (user-friendly/undoable)
+ */
+ void delete_selected();
+
+ void select_all();
+ void deselect_all();
+
+ void select(const QVector &nodes,
+ bool center_view_on_item);
+
+ void copy_selected(bool cut);
+ void paste();
+
+ void duplicate();
+
+ void set_color_label(int index);
+
+ void zoom_in();
+
+ void zoom_out();
+
+ const QVector &get_current_contexts() const
+ {
+ return contexts_;
+ }
+
+public slots:
+ void set_mini_map_enabled(bool e)
+ {
+ minimap_->setVisible(e);
+ }
+
+ void show_add_menu()
+ {
+ Menu *m = create_add_menu(nullptr);
+ m->exec(QCursor::pos());
+ delete m;
+ }
+
+ void center_on_items_bounding_rect();
+
+ void center_on_node(OakEngineNode *n);
+
+ void label_selected_nodes();
+
+signals:
+ void nodes_selected(const QVector &nodes);
+
+ void nodes_deselected(const QVector &nodes);
+
+ void node_selection_changed(const QVector &nodes);
+ void
+ node_selection_changed_with_contexts(const QVector &nodes);
+
+ void node_group_opened(NodeGroup *group);
+ void node_group_closed();
+
+ void esc_pressed();
+
+protected:
+ virtual void keyPressEvent(QKeyEvent *event) override;
+
+ virtual void mousePressEvent(QMouseEvent *event) override;
+ virtual void mouseMoveEvent(QMouseEvent *event) override;
+ virtual void mouseReleaseEvent(QMouseEvent *event) override;
+ virtual void mouseDoubleClickEvent(QMouseEvent *event) override;
+
+ virtual void dragEnterEvent(QDragEnterEvent *event) override;
+ virtual void dragMoveEvent(QDragMoveEvent *event) override;
+ virtual void dropEvent(QDropEvent *event) override;
+ virtual void dragLeaveEvent(QDragLeaveEvent *event) override;
+
+ virtual void resizeEvent(QResizeEvent *event) override;
+
+ virtual void zoom_into_cursor_position(QWheelEvent *event, double multiplier,
+ const QPointF &cursor_pos) override;
+
+ virtual bool event(QEvent *event) override;
+
+ virtual bool eventFilter(QObject *object, QEvent *event) override;
+
+ virtual void changeEvent(QEvent *e) override;
+
+private:
+ void detach_items_from_cursor(bool delete_nodes_too = true);
+
+ void set_flow_direction(NodeViewCommon::FlowDirection dir);
+
+ void move_attached_nodes_to_cursor(const QPoint &p);
+ void process_moving_attached_nodes(const QPoint &pos);
+ QVector process_dropping_attached_nodes(MultiUndoCommand *command,
+ Node *select_context,
+ const QPoint &pos);
+ Node *get_context_at_mouse_pos(const QPoint &p);
+
+ void connect_selection_changed_signal();
+ void disconnect_selection_changed_signal();
+
+ void zoom_from_keyboard(double multiplier);
+
+ void clear_create_edge_input_if_necessary();
+
+ QPointF get_estimated_position_for_context(NodeViewItem *item,
+ Node *context) const;
+
+ NodeViewItem *get_assumed_item_for_selected_node(Node *node);
+ bool get_assumed_position_for_selected_node(Node *node, Node::Position *pos);
+
+ Menu *create_add_menu(Menu *parent);
+
+ void position_new_edge(const QPoint &pos);
+
+ void add_context(Node *n);
+
+ void remove_context(Node *n);
+
+ bool is_item_attached_to_cursor(NodeViewItem *item) const;
+
+ void expand_item(NodeViewItem *item);
+
+ void collapse_item(NodeViewItem *item);
+
+ void end_edge_drag(bool cancel = false);
+
+ void post_paste(const QVector &new_nodes,
+ const Node::PositionMap &map);
+
+ void resize_overlay();
+
+ NodeViewMiniMap *minimap_;
+
+ NodeViewContext *get_context_item_from_node_item(NodeViewItem *item);
+
+ struct AttachedItem {
+ NodeViewItem *item;
+ Node *node;
+ QPointF original_pos;
+ };
+
+ void set_attached_items(const QVector &items);
+ QVector attached_items_;
+
+ NodeViewEdge *drop_edge_;
+ NodeInput drop_input_;
+
+ NodeViewEdge *create_edge_;
+ NodeViewItem *create_edge_output_item_;
+ NodeViewItem *create_edge_input_item_;
+ NodeInput create_edge_input_;
+ bool create_edge_already_exists_;
+ bool create_edge_from_output_;
+
+ QVector create_edge_expanded_items_;
+
+ NodeViewScene scene_;
+
+ QVector selected_nodes_;
+
+ QVector contexts_;
+ QVector last_set_filter_nodes_;
+ QMap context_offsets_;
+
+ QMap dragging_items_;
+
+ NodeView *overlay_view_;
+
+ double scale_;
+
+ bool dont_emit_selection_signals_;
+
+ QAction *show_in_param_editor_action_;
+
+ static const double k_minimum_scale;
+
+ static const int k_maximum_contexts;
+
+private slots:
+ /**
+ * @brief Receiver for when the scene's selected items change
+ */
+ void update_selection_cache();
+
+ /**
+ * @brief Receiver for when the user right clicks (or otherwise requests a context menu)
+ */
+ void show_context_menu(const QPoint &pos);
+
+ /**
+ * @brief Receiver for when the user requests a new node from the add menu
+ */
+ void create_node_slot(QAction *action);
+
+ /**
+ * @brief Receiver for setting the direction from the context menu
+ */
+ void context_menu_set_direction(QAction *action);
+
+ /**
+ * @brief Opens the selected node in a Viewer
+ */
+ void open_selected_node_in_viewer();
+
+ void update_scene_bounding_rect();
+
+ void reposition_mini_map();
+
+ void update_viewport_on_mini_map();
+
+ void move_to_scene_point(const QPointF &pos);
+
+ void node_removed_from_graph();
+
+ void group_nodes();
+
+ void ungroup_nodes();
+
+ void show_node_properties();
+
+ void show_selected_node_in_param_editor();
+
+ void item_about_to_be_deleted(NodeViewItem *item);
+
+ void close_overlay();
+};
+
+}
+
+#endif // OAK_NODEVIEW_H
diff --git a/.bak/nodeview/nodeviewcommon.h b/.bak/nodeview/nodeviewcommon.h
new file mode 100644
index 000000000..c6562e5c4
--- /dev/null
+++ b/.bak/nodeview/nodeviewcommon.h
@@ -0,0 +1,76 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_NODEVIEWCOMMON_H
+#define OAK_NODEVIEWCOMMON_H
+
+#include
+
+#include "common/define.h"
+
+namespace olive
+{
+
+class NodeViewCommon {
+public:
+ enum FlowDirection {
+ k_invalid_direction = -1,
+ k_top_to_bottom,
+ k_bottom_to_top,
+ k_left_to_right,
+ k_right_to_left
+ };
+
+ static Qt::Orientation get_flow_orientation(FlowDirection dir)
+ {
+ if (dir == k_top_to_bottom || dir == k_bottom_to_top) {
+ return Qt::Vertical;
+ } else {
+ return Qt::Horizontal;
+ }
+ }
+
+ static bool is_flow_vertical(FlowDirection dir)
+ {
+ return dir == k_top_to_bottom || dir == k_bottom_to_top;
+ }
+
+ static bool is_flow_horizontal(FlowDirection dir)
+ {
+ return dir == k_left_to_right || dir == k_right_to_left;
+ }
+
+ static bool directions_are_opposing(FlowDirection a, FlowDirection b)
+ {
+ return ((a == NodeViewCommon::k_left_to_right &&
+ b == NodeViewCommon::k_right_to_left) ||
+ (a == NodeViewCommon::k_right_to_left &&
+ b == NodeViewCommon::k_left_to_right) ||
+ (a == NodeViewCommon::k_top_to_bottom &&
+ b == NodeViewCommon::k_bottom_to_top) ||
+ (a == NodeViewCommon::k_bottom_to_top &&
+ b == NodeViewCommon::k_top_to_bottom));
+ }
+};
+
+}
+
+#endif // OAK_NODEVIEWCOMMON_H
diff --git a/.bak/nodeview/nodeviewcontext.cpp b/.bak/nodeview/nodeviewcontext.cpp
new file mode 100644
index 000000000..4175a3c2a
--- /dev/null
+++ b/.bak/nodeview/nodeviewcontext.cpp
@@ -0,0 +1,412 @@
+/*
+ * Oak Video Editor - Non-Linear Video Editor
+ * Copyright (C) 2025 Olive CE Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "nodeviewcontext.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core.h"
+#include "node/block/block.h"
+#include "node/group/group.h"
+#include "node/output/track/track.h"
+#include "node/project.h"
+#include "node/project/sequence/sequence.h"
+#include "nodeviewitem.h"
+#include "ui/colorcoding.h"
+
+namespace olive
+{
+
+#define super QGraphicsRectItem
+
+NodeViewContext::NodeViewContext(Node *context, QGraphicsItem *item)
+ : super(item)
+ , context_(context)
+{
+ Block *block = dynamic_cast(context_);
+ if (block && block->track() && block->track()->sequence()) {
+ Rational timebase = block->track()
+ ->sequence()
+ ->get_video_params()
+ .frame_rate_as_time_base();
+ lbl_ =
+ QCoreApplication::translate("NodeViewContext", "%1 [%2] :: %3 - %4")
+ .arg(block->get_label_and_name(),
+ Track::Reference::type_to_translated_string(
+ block->track()->type()),
+ QString::fromStdString(Timecode::time_to_timecode(
+ block->in(), timebase,
+ Core::instance()->get_timecode_display())),
+ QString::fromStdString(Timecode::time_to_timecode(
+ block->out(), timebase,
+ Core::instance()->get_timecode_display())));
+ } else {
+ lbl_ = context_->get_label_and_name();
+ }
+
+ const Node::PositionMap &map = context_->get_context_positions();
+ for (auto it = map.cbegin(); it != map.cend(); it++) {
+ add_child(it.key());
+ }
+
+ connect(context_, &Node::node_added_to_context, this,
+ &NodeViewContext::add_child, Qt::DirectConnection);
+ connect(context_, &Node::node_position_in_context_changed, this,
+ &NodeViewContext::set_child_position, Qt::DirectConnection);
+ connect(context_, &Node::node_removed_from_context, this,
+ &NodeViewContext::remove_child, Qt::DirectConnection);
+}
+
+NodeViewContext::~NodeViewContext()
+{
+ // Delete edges before items, because the edge constructor references the items
+ qDeleteAll(edges_);
+ edges_.clear();
+}
+
+void NodeViewContext::add_child(Node *node)
+{
+ if (!context_) {
+ return;
+ }
+
+ NodeViewItem *item = new NodeViewItem(node, context_, this);
+ item->set_flow_direction(flow_dir_);
+
+ add_node_internal(node, item);
+
+ if (NodeGroup *group = dynamic_cast(node)) {
+ for (auto it = group->get_context_positions().cbegin();
+ it != group->get_context_positions().cend(); it++) {
+ // Use this item as the representative for all of these nodes too
+ add_node_internal(it.key(), item);
+ }
+
+ connect(group, &NodeGroup::node_added_to_context, this,
+ &NodeViewContext::group_added_node);
+ connect(group, &NodeGroup::node_removed_from_context, this,
+ &NodeViewContext::group_removed_node);
+ }
+
+ update_rect();
+}
+
+void NodeViewContext::set_child_position(Node *node, const QPointF &pos)
+{
+ item_map_.value(node)->set_node_position(pos);
+}
+
+void NodeViewContext::remove_child(Node *node)
+{
+ disconnect(node, &Node::input_connected, this,
+ &NodeViewContext::child_input_connected);
+ disconnect(node, &Node::input_disconnected, this,
+ &NodeViewContext::child_input_disconnected);
+
+ if (NodeGroup *group = dynamic_cast(node)) {
+ disconnect(group, &NodeGroup::node_added_to_context, this,
+ &NodeViewContext::group_added_node);
+ disconnect(group, &NodeGroup::node_removed_from_context, this,
+ &NodeViewContext::group_removed_node);
+ }
+
+ NodeViewItem *item = item_map_.take(node);
+
+ // Remove from scene before emitting signal so that any drag functions that might be happening
+ // now can be handled before the item is destroyed
+ scene()->removeItem(item);
+
+ emit item_about_to_be_deleted(item);
+
+ // Delete edges first because the edge destructor will try to reference item (maybe that should
+ // be changed...)
+ QVector edges_to_remove = item->get_all_edges_recursively();
+ foreach (NodeViewEdge *edge, edges_to_remove) {
+ if (node == item->get_node() || edge->output() == node ||
+ edge->input().node() == node) {
+ child_input_disconnected(edge->output(), edge->input());
+ }
+ }
+
+ // Check if this item is specifically for this node and the node is a group. If so, remove it for
+ // all other entries in the map.
+ if (item->get_node() == node) {
+ if (dynamic_cast(item->get_node())) {
+ for (auto it = item_map_.begin(); it != item_map_.end();) {
+ if (it.value() == item) {
+ it = item_map_.erase(it);
+ } else {
+ it++;
+ }
+ }
+ }
+
+ delete item;
+ }
+
+ update_rect();
+}
+
+void NodeViewContext::child_input_connected(Node *output, const NodeInput &input)
+{
+ // Add edge
+ if (!input.is_hidden()) {
+ if (NodeViewItem *output_item = item_map_.value(output)) {
+ add_edge_internal(
+ output, input, output_item,
+ item_map_.value(input.node())->get_item_for_input(input));
+ }
+ }
+}
+
+bool NodeViewContext::child_input_disconnected(Node *output,
+ const NodeInput &input)
+{
+ // Remove edge
+ for (int i = 0; i < edges_.size(); i++) {
+ NodeViewEdge *e = edges_.at(i);
+ if (e->output() == output && e->input() == input) {
+ delete e;
+ edges_.removeAt(i);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+qreal get_text_offset(const QFontMetricsF &fm)
+{
+ return fm.height() / 2;
+}
+
+void NodeViewContext::update_rect()
+{
+ QFont f;
+ QFontMetricsF fm(f);
+ qreal lbl_offset = get_text_offset(fm);
+
+ QRectF cbr = childrenBoundingRect();
+ QRectF rect = cbr;
+ int pad = NodeViewItem::default_item_height();
+ rect.adjust(-pad, -lbl_offset * 2 - fm.height() - pad, pad, pad);
+ setRect(rect);
+
+ last_titlebar_height_ = rect.y() + (cbr.y() - rect.y()) - pad;
+}
+
+void NodeViewContext::set_flow_direction(NodeViewCommon::FlowDirection dir)
+{
+ flow_dir_ = dir;
+
+ foreach (NodeViewItem *item, item_map_) {
+ item->set_flow_direction(dir);
+ }
+}
+
+void NodeViewContext::set_curved_edges(bool e)
+{
+ curved_edges_ = e;
+
+ foreach (NodeViewEdge *edge, edges_) {
+ edge->set_curved(e);
+ }
+}
+
+int NodeViewContext::delete_selected(NodeViewDeleteCommand *command)
+{
+ int count = 0;
+
+ // Delete any selected edges
+ foreach (NodeViewEdge *edge, edges_) {
+ if (edge->isSelected()) {
+ command->add_edge(edge->output(), edge->input());
+ }
+ }
+
+ // Delete any selected nodes
+ foreach (NodeViewItem *node, item_map_) {
+ if (node->isSelected()) {
+ command->add_node(node->get_node(), context_);
+ count++;
+ }
+ }
+
+ return count;
+}
+
+void NodeViewContext::select(const QVector &nodes)
+{
+ foreach (Node *n, nodes) {
+ if (NodeViewItem *item = item_map_.value(n)) {
+ item->setSelected(true);
+ }
+ }
+}
+
+QVector NodeViewContext::get_selected_items() const
+{
+ QVector items;
+
+ for (auto it = item_map_.cbegin(); it != item_map_.cend(); it++) {
+ if (it.value()->isSelected()) {
+ if (!items.contains(it.value())) {
+ items.append(it.value());
+ }
+ }
+ }
+
+ return items;
+}
+
+QPointF NodeViewContext::map_scene_pos_to_node_pos_in_context(const QPointF &pos) const
+{
+ for (auto it = item_map_.cbegin(); it != item_map_.cend(); it++) {
+ QPointF pos_inside_parent =
+ it.value()->mapToParent(it.value()->mapFromScene(pos));
+ return NodeViewItem::screen_to_node_point(pos_inside_parent, flow_dir_);
+ }
+ return QPointF(0, 0);
+}
+
+void NodeViewContext::paint(QPainter *painter,
+ const QStyleOptionGraphicsItem *option,
+ QWidget *widget)
+{
+ // Set pen and brush
+ Color color = context_->color();
+ QColor c = QtUtils::to_q_color(color);
+ QPen pen(c, 2);
+ if (option->state & QStyle::State_Selected) {
+ pen.setStyle(Qt::DotLine);
+ }
+ painter->setPen(pen);
+
+ QColor bg = c;
+ bg.setAlpha(128);
+ painter->setBrush(bg);
+
+ // Draw semi-transparent rect for whole item
+ int rounded = painter->fontMetrics().height();
+ painter->drawRoundedRect(rect(), rounded, rounded);
+
+ // Draw solid background for titlebar
+ QRectF titlebar_rect = rect();
+ titlebar_rect.setHeight(last_titlebar_height_ - rect().top());
+ painter->setClipRect(titlebar_rect);
+ painter->setBrush(c);
+ painter->drawRoundedRect(rect(), rounded, rounded);
+ painter->setClipping(false);
+
+ // Draw titlebar text
+ painter->setPen(ColorCoding::get_ui_selector_color(color));
+
+ int offset = get_text_offset(painter->fontMetrics());
+
+ QRectF text_rect = rect();
+ text_rect.adjust(offset, offset, -offset, -offset);
+ painter->drawText(text_rect, lbl_);
+}
+
+QVariant NodeViewContext::itemChange(GraphicsItemChange change,
+ const QVariant &value)
+{
+ return super::itemChange(change, value);
+}
+
+void NodeViewContext::mousePressEvent(QGraphicsSceneMouseEvent *event)
+{
+ bool clicked_inside_titlebar = (event->pos().y() < last_titlebar_height_);
+
+ setFlag(ItemIsMovable, clicked_inside_titlebar);
+ setFlag(ItemIsSelectable, clicked_inside_titlebar);
+
+ super::mousePressEvent(event);
+}
+
+void NodeViewContext::add_node_internal(Node *node, NodeViewItem *item)
+{
+ connect(node, &Node::input_connected, this,
+ &NodeViewContext::child_input_connected);
+ connect(node, &Node::input_disconnected, this,
+ &NodeViewContext::child_input_disconnected);
+
+ item_map_.insert(node, item);
+
+ if (node == context_) {
+ item->set_label_as_output(true);
+ }
+
+ for (auto it = node->output_connections().cbegin();
+ it != node->output_connections().cend(); it++) {
+ if (!it->second.is_hidden()) {
+ if (NodeViewItem *other_item = item_map_.value(it->second.node())) {
+ add_edge_internal(node, it->second, item,
+ other_item->get_item_for_input(it->second));
+ }
+ }
+ }
+
+ for (auto it = node->input_connections().cbegin();
+ it != node->input_connections().cend(); it++) {
+ if (!it->first.is_hidden()) {
+ if (NodeViewItem *other_item = item_map_.value(it->second)) {
+ add_edge_internal(it->second, it->first, other_item,
+ item->get_item_for_input(it->first));
+ }
+ }
+ }
+}
+
+void NodeViewContext::add_edge_internal(Node *output, const NodeInput &input,
+ NodeViewItem *from, NodeViewItem *to)
+{
+ if (from == to) {
+ return;
+ }
+
+ NodeViewEdge *edge_ui = new NodeViewEdge(output, input, from, to, this);
+
+ edge_ui->adjust();
+ edge_ui->set_curved(curved_edges_);
+
+ edges_.append(edge_ui);
+}
+
+void NodeViewContext::group_added_node(Node *node)
+{
+ NodeGroup *group = static_cast(sender());
+
+ add_node_internal(node, item_map_.value(group));
+}
+
+void NodeViewContext::group_removed_node(Node *node)
+{
+ NodeGroup *group = static_cast(sender());
+
+ if (item_map_.value(node) == item_map_.value(group)) {
+ item_map_.remove(node);
+ }
+}
+
+}
diff --git a/.bak/nodeview/nodeviewcontext.h b/.bak/nodeview/nodeviewcontext.h
new file mode 100644
index 000000000..f62ef6ff4
--- /dev/null
+++ b/.bak/nodeview/nodeviewcontext.h
@@ -0,0 +1,116 @@
+/*
+ * Oak Video Editor - Non-Linear Video Editor
+ * Copyright (C) 2025 Olive CE Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef OAK_NODEVIEWCONTEXT_H
+#define OAK_NODEVIEWCONTEXT_H
+
+#include
+#include
+
+#include "node/node.h"
+#include "node/nodeundo.h"
+#include "nodeviewcommon.h"
+#include "nodeviewedge.h"
+
+namespace olive
+{
+
+class NodeViewContext : public QObject, public QGraphicsRectItem {
+ Q_OBJECT
+public:
+ NodeViewContext(Node *context, QGraphicsItem *item = nullptr);
+
+ virtual ~NodeViewContext() override;
+
+ Node *get_context() const
+ {
+ return context_;
+ }
+
+ void update_rect();
+
+ void set_flow_direction(NodeViewCommon::FlowDirection dir);
+
+ void set_curved_edges(bool e);
+
+ int delete_selected(NodeViewDeleteCommand *command);
+
+ void select(const QVector &nodes);
+
+ QVector get_selected_items() const;
+
+ QPointF map_scene_pos_to_node_pos_in_context(const QPointF &pos) const;
+
+ NodeViewItem *get_item_from_map(Node *node) const
+ {
+ return item_map_.value(node);
+ }
+
+ virtual void paint(QPainter *painter,
+ const QStyleOptionGraphicsItem *option,
+ QWidget *widget = nullptr) override;
+
+public slots:
+ void add_child(Node *node);
+
+ void set_child_position(Node *node, const QPointF &pos);
+
+ void remove_child(Node *node);
+
+ void child_input_connected(Node *output, const NodeInput &input);
+
+ bool child_input_disconnected(Node *output, const NodeInput &input);
+
+signals:
+ void item_about_to_be_deleted(NodeViewItem *item);
+
+protected:
+ virtual QVariant itemChange(QGraphicsItem::GraphicsItemChange change,
+ const QVariant &value) override;
+
+ virtual void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
+
+private:
+ void add_node_internal(Node *node, NodeViewItem *item);
+
+ void add_edge_internal(Node *output, const NodeInput &input,
+ NodeViewItem *from, NodeViewItem *to);
+
+ Node *context_;
+
+ QString lbl_;
+
+ NodeViewCommon::FlowDirection flow_dir_;
+
+ bool curved_edges_;
+
+ int last_titlebar_height_;
+
+ QMap item_map_;
+
+ QVector edges_;
+
+private slots:
+ void group_added_node(Node *node);
+
+ void group_removed_node(Node *node);
+};
+
+}
+
+#endif // OAK_NODEVIEWCONTEXT_H
diff --git a/.bak/nodeview/nodeviewedge.cpp b/.bak/nodeview/nodeviewedge.cpp
new file mode 100644
index 000000000..7625ef94a
--- /dev/null
+++ b/.bak/nodeview/nodeviewedge.cpp
@@ -0,0 +1,260 @@
+/***
+
+ 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 "nodeviewedge.h"
+
+#include
+#include
+#include
+#include
+
+#include "common/lerp.h"
+#include "nodeview.h"
+#include "nodeviewitem.h"
+#include "nodeviewscene.h"
+
+namespace olive
+{
+
+#define super QGraphicsPathItem
+
+NodeViewEdge::NodeViewEdge(Node *output, const NodeInput &input,
+ NodeViewItem *from_item, NodeViewItem *to_item,
+ QGraphicsItem *parent)
+ : super(parent)
+ , output_(output)
+ , input_(input)
+ , from_item_(from_item)
+ , to_item_(to_item)
+{
+ init();
+ set_connected(true);
+
+ from_item_->add_edge(this);
+ to_item_->add_edge(this);
+}
+
+NodeViewEdge::NodeViewEdge(QGraphicsItem *parent)
+ : QGraphicsPathItem(parent)
+ , from_item_(nullptr)
+ , to_item_(nullptr)
+{
+ init();
+}
+
+NodeViewEdge::~NodeViewEdge()
+{
+ if (from_item_) {
+ from_item_->remove_edge(this);
+ }
+
+ if (to_item_) {
+ to_item_->remove_edge(this);
+ }
+}
+
+void NodeViewEdge::set_from_item(NodeViewItem *i)
+{
+ if (from_item_) {
+ from_item_->remove_edge(this);
+ }
+
+ from_item_ = i;
+
+ if (from_item_) {
+ from_item_->add_edge(this);
+ }
+
+ adjust();
+}
+
+void NodeViewEdge::set_to_item(NodeViewItem *i)
+{
+ if (to_item_) {
+ to_item_->remove_edge(this);
+ }
+
+ to_item_ = i;
+
+ if (to_item_) {
+ to_item_->add_edge(this);
+ }
+
+ adjust();
+}
+
+void NodeViewEdge::adjust()
+{
+ // Draw a line between the two
+ set_points(from_item()->get_output_point(), to_item()->get_input_point());
+}
+
+void NodeViewEdge::set_connected(bool c)
+{
+ connected_ = c;
+
+ update();
+}
+
+void NodeViewEdge::set_highlighted(bool e)
+{
+ highlighted_ = e;
+
+ update();
+}
+
+void NodeViewEdge::set_points(const QPointF &start, const QPointF &end)
+{
+ cached_start_ = start;
+ cached_end_ = end;
+
+ update_curve();
+}
+
+void NodeViewEdge::set_curved(bool e)
+{
+ curved_ = e;
+
+ update_curve();
+}
+
+void NodeViewEdge::paint(QPainter *painter,
+ const QStyleOptionGraphicsItem *option, QWidget *)
+{
+ QPalette::ColorGroup group;
+ QPalette::ColorRole role;
+
+ if (connected_) {
+ group = QPalette::Active;
+ } else {
+ group = QPalette::Disabled;
+ }
+
+ if (highlighted_ != bool(option->state & QStyle::State_Selected)) {
+ role = QPalette::Highlight;
+ } else {
+ role = QPalette::Text;
+ }
+
+ // Draw main path
+ QColor edge_color = qApp->palette().color(group, role);
+
+ painter->setPen(QPen(edge_color, edge_width_));
+ painter->setBrush(Qt::NoBrush);
+ painter->drawPath(path());
+}
+
+void NodeViewEdge::init()
+{
+ connected_ = false;
+ highlighted_ = false;
+ curved_ = true;
+
+ setFlag(QGraphicsItem::ItemIsSelectable);
+
+ // Ensures this UI object is drawn behind other objects
+ setZValue(-1);
+
+ // Use font metrics to set edge width for basic high DPI support
+ edge_width_ = QFontMetrics(QFont()).height() / 12;
+}
+
+void NodeViewEdge::update_curve()
+{
+ const QPointF &start = cached_start_;
+ const QPointF &end = cached_end_;
+
+ QPainterPath path;
+ path.moveTo(start);
+
+ double angle = std::atan2(end.y() - start.y(), end.x() - start.x());
+
+ if (curved_) {
+ double half_x = lerp(start.x(), end.x(), 0.5);
+ double half_y = lerp(start.y(), end.y(), 0.5);
+
+ QPointF cp1, cp2;
+
+ NodeViewCommon::FlowDirection from_flow =
+ from_item_ ? from_item_->get_flow_direction() :
+ NodeViewCommon::k_invalid_direction;
+ NodeViewCommon::FlowDirection to_flow =
+ to_item_ ? to_item_->get_flow_direction() :
+ NodeViewCommon::k_invalid_direction;
+
+ if (from_flow == NodeViewCommon::k_invalid_direction &&
+ to_flow == NodeViewCommon::k_invalid_direction) {
+ // This is a technically unsupported scenario, but to avoid issues, we'll use a fallback
+ from_flow = NodeViewCommon::k_left_to_right;
+ to_flow = NodeViewCommon::k_left_to_right;
+ } else if (from_flow == NodeViewCommon::k_invalid_direction) {
+ from_flow = to_flow;
+ } else if (to_flow == NodeViewCommon::k_invalid_direction) {
+ to_flow = from_flow;
+ }
+
+ if (NodeViewCommon::get_flow_orientation(from_flow) == Qt::Horizontal) {
+ cp1 = QPointF(half_x, start.y());
+ } else {
+ cp1 = QPointF(start.x(), half_y);
+ }
+
+ if (NodeViewCommon::get_flow_orientation(to_flow) == Qt::Horizontal) {
+ cp2 = QPointF(half_x, end.y());
+ } else {
+ cp2 = QPointF(end.x(), half_y);
+ }
+
+ path.cubicTo(cp1, cp2, end);
+
+ if (!qFuzzyCompare(start.x(), end.x())) {
+ double continue_x = end.x() - std::cos(angle);
+
+ double x1 = start.x();
+ double x2 = cp1.x();
+ double x3 = cp2.x();
+ double x4 = end.x();
+ double y1 = start.y();
+ double y2 = cp1.y();
+ double y3 = cp2.y();
+ double y4 = end.y();
+
+ if (start.x() >= end.x()) {
+ std::swap(x1, x4);
+ std::swap(x2, x3);
+ std::swap(y1, y4);
+ std::swap(y2, y3);
+ }
+
+ double t = Bezier::cubic_xto_t(continue_x, x1, x2, x3, x4);
+ double y = Bezier::cubic_tto_y(y1, y2, y3, y4, t);
+
+ angle = std::atan2(end.y() - y, end.x() - continue_x);
+ }
+
+ } else {
+ path.lineTo(end);
+ }
+
+ setPath(mapFromScene(path));
+}
+
+}
diff --git a/.bak/nodeview/nodeviewedge.h b/.bak/nodeview/nodeviewedge.h
new file mode 100644
index 000000000..db33ab60d
--- /dev/null
+++ b/.bak/nodeview/nodeviewedge.h
@@ -0,0 +1,149 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_NODEEDGEITEM_H
+#define OAK_NODEEDGEITEM_H
+
+#include
+#include
+
+#include "nodeviewcommon.h"
+#include "node/node.h"
+
+namespace olive
+{
+
+class NodeViewItem;
+
+/**
+ * @brief A graphical representation of a NodeEdge to be used in NodeView
+ *
+ * A fairly simple line widget use to visualize a connection between two node parameters (a NodeEdge).
+ */
+class NodeViewEdge : public QGraphicsPathItem {
+public:
+ NodeViewEdge(Node *output, const NodeInput &input, NodeViewItem *from_item,
+ NodeViewItem *to_item, QGraphicsItem *parent = nullptr);
+
+ NodeViewEdge(QGraphicsItem *parent = nullptr);
+
+ virtual ~NodeViewEdge() override;
+
+ Node *output() const
+ {
+ return output_;
+ }
+
+ const NodeInput &input() const
+ {
+ return input_;
+ }
+
+ int element() const
+ {
+ return element_;
+ }
+
+ NodeViewItem *from_item() const
+ {
+ return from_item_;
+ }
+
+ NodeViewItem *to_item() const
+ {
+ return to_item_;
+ }
+
+ void set_from_item(NodeViewItem *i);
+
+ void set_to_item(NodeViewItem *i);
+
+ void adjust();
+
+ /**
+ * @brief Set the connected state of this line
+ *
+ * When the edge is not connected, it visually depicts this by coloring the line grey. When an edge is connected or
+ * a potential connection is valid, the line is colored white. This function sets whether the line should be grey
+ * (false) or white (true).
+ *
+ * Using SetEdge() automatically sets this to true. Under most circumstances this should be left alone, and only
+ * be set when an edge is being created/dragged.
+ */
+ void set_connected(bool c);
+
+ bool is_connected() const
+ {
+ return connected_;
+ }
+
+ /**
+ * @brief Set highlighted state
+ *
+ * Changes color of edge.
+ */
+ void set_highlighted(bool e);
+
+ /**
+ * @brief Set points to create curve from
+ */
+ void set_points(const QPointF &start, const QPointF &end);
+
+ /**
+ * @brief Set whether edges should be drawn as curved or as straight lines
+ */
+ void set_curved(bool e);
+
+protected:
+ virtual void paint(QPainter *painter,
+ const QStyleOptionGraphicsItem *option,
+ QWidget *widget = nullptr) override;
+
+private:
+ void init();
+
+ void update_curve();
+
+ Node *output_;
+
+ NodeInput input_;
+
+ int element_;
+
+ NodeViewItem *from_item_;
+
+ NodeViewItem *to_item_;
+
+ int edge_width_;
+
+ bool connected_;
+
+ bool highlighted_;
+
+ bool curved_;
+
+ QPointF cached_start_;
+ QPointF cached_end_;
+};
+
+}
+
+#endif // OAK_NODEEDGEITEM_H
diff --git a/.bak/nodeview/nodeviewitem.cpp b/.bak/nodeview/nodeviewitem.cpp
new file mode 100644
index 000000000..320e0469b
--- /dev/null
+++ b/.bak/nodeview/nodeviewitem.cpp
@@ -0,0 +1,907 @@
+/***
+
+ 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 "nodeviewitem.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "common/qtutils.h"
+#include "config/config.h"
+#include "core.h"
+#include "node/nodeundo.h"
+#include "node/value.h"
+#include "pluginSupport/oliveplugininstance.h"
+#include "nodeview.h"
+#include "nodeviewscene.h"
+#include "ui/colorcoding.h"
+#include "ui/icons/icons.h"
+#include "window/mainwindow/mainwindow.h"
+
+namespace olive
+{
+
+NodeViewItem::NodeViewItem(Node *node, const QString &input, int element,
+ Node *context, QGraphicsItem *parent)
+ : QGraphicsRectItem(parent)
+ , node_(node)
+ , input_(input)
+ , element_(element)
+ , context_(context)
+ , expanded_(false)
+ , highlighted_(false)
+ , flow_dir_(NodeViewCommon::k_invalid_direction)
+ , arrow_click_(false)
+ , label_as_output_(false)
+{
+ //
+ // We use font metrics to set all the UI measurements for DPI-awareness
+ //
+
+ // Set border width
+ node_border_width_ = default_item_border();
+
+ // Set rect size to default
+ set_rect_size();
+
+ // Create connector
+ input_connector_ = new NodeViewItemConnector(false, this);
+ output_connector_ = new NodeViewItemConnector(true, this);
+
+ connect(node_, &Node::label_changed, this,
+ &NodeViewItem::node_appearance_changed);
+ connect(node_, &Node::color_changed, this,
+ &NodeViewItem::node_appearance_changed);
+ connect(node_, &Node::message_count_changed, this,
+ &NodeViewItem::node_appearance_changed);
+
+ if (is_output_item()) {
+ connect(node_, &Node::input_added, this,
+ &NodeViewItem::repopulate_inputs);
+ connect(node_, &Node::input_removed, this,
+ &NodeViewItem::repopulate_inputs);
+ repopulate_inputs();
+
+ // Set flags for this widget
+ setFlag(QGraphicsItem::ItemSendsGeometryChanges);
+ setFlag(QGraphicsItem::ItemIsMovable);
+ setFlag(QGraphicsItem::ItemIsSelectable);
+
+ if (context_) {
+ set_node_position(context_->get_node_position_data_in_context(node_));
+ }
+ } else {
+ output_connector_->setVisible(false);
+
+ connect(node_, &Node::input_array_size_changed, this,
+ &NodeViewItem::input_array_size_changed);
+ connect(node_, &Node::input_array_size_changed, this,
+ &NodeViewItem::input_array_size_changed);
+ }
+
+ // This should be set during runtime, but just in case here's a default fallback
+ set_flow_direction(NodeViewCommon::k_left_to_right);
+}
+
+NodeViewItem::~NodeViewItem()
+{
+ Q_ASSERT(edges_.isEmpty());
+}
+
+Node::Position NodeViewItem::get_node_position_data() const
+{
+ return Node::Position(get_node_position(), is_expanded());
+}
+
+QPointF NodeViewItem::get_node_position() const
+{
+ return screen_to_node_point(pos(), flow_dir_);
+}
+
+void NodeViewItem::set_node_position(const QPointF &pos)
+{
+ cached_node_pos_ = pos;
+
+ update_node_position();
+}
+
+void NodeViewItem::set_node_position(const Node::Position &pos)
+{
+ set_node_position(pos.position);
+ set_expanded(pos.expanded);
+}
+
+QVector NodeViewItem::get_all_edges_recursively() const
+{
+ QVector list = edges_;
+
+ foreach (NodeViewItem *item, children_) {
+ list.append(item->get_all_edges_recursively());
+ }
+
+ return list;
+}
+
+int NodeViewItem::default_text_padding()
+{
+ return QFontMetrics(QFont()).height() / 4;
+}
+
+int NodeViewItem::default_item_height()
+{
+ return QFontMetrics(QFont()).height() + default_text_padding() * 2;
+}
+
+int NodeViewItem::default_item_width()
+{
+ return QtUtils::q_font_metrics_width(QFontMetrics(QFont()),
+ "HHHHHHHHHHHHHHHH");
+ ;
+}
+
+int NodeViewItem::default_item_border()
+{
+ return QFontMetrics(QFont()).height() / 12;
+}
+
+QPointF NodeViewItem::node_to_screen_point(QPointF p,
+ NodeViewCommon::FlowDirection direction)
+{
+ switch (direction) {
+ case NodeViewCommon::k_left_to_right:
+ // NodeGraphs are always left-to-right internally, no need to translate
+ break;
+ case NodeViewCommon::k_right_to_left:
+ // Invert X value
+ p.setX(-p.x());
+ break;
+ case NodeViewCommon::k_top_to_bottom:
+ // Swap X/Y
+ p = QPointF(p.y(), p.x());
+ break;
+ case NodeViewCommon::k_bottom_to_top:
+ // Swap X/Y and invert Y
+ p = QPointF(p.y(), -p.x());
+ break;
+ case NodeViewCommon::k_invalid_direction:
+ break;
+ }
+
+ // Multiply by item sizes for this direction
+ p.setX(p.x() * default_item_horizontal_padding(direction));
+ p.setY(p.y() * default_item_vertical_padding(direction));
+
+ return p;
+}
+
+QPointF NodeViewItem::screen_to_node_point(QPointF p,
+ NodeViewCommon::FlowDirection direction)
+{
+ // Divide by item sizes for this direction
+ p.setX(p.x() / default_item_horizontal_padding(direction));
+ p.setY(p.y() / default_item_vertical_padding(direction));
+
+ switch (direction) {
+ case NodeViewCommon::k_left_to_right:
+ // NodeGraphs are always left-to-right internally, no need to translate
+ break;
+ case NodeViewCommon::k_right_to_left:
+ // Invert X value
+ p.setX(-p.x());
+ break;
+ case NodeViewCommon::k_top_to_bottom:
+ // Swap X/Y
+ p = QPointF(p.y(), p.x());
+ break;
+ case NodeViewCommon::k_bottom_to_top:
+ // Swap X/Y and invert Y
+ p = QPointF(-p.y(), p.x());
+ break;
+ case NodeViewCommon::k_invalid_direction:
+ break;
+ }
+
+ return p;
+}
+
+qreal NodeViewItem::default_item_horizontal_padding(
+ NodeViewCommon::FlowDirection dir)
+{
+ if (NodeViewCommon::get_flow_orientation(dir) == Qt::Horizontal) {
+ return default_item_width() * 1.5;
+ } else {
+ return default_item_width() * 1.25;
+ }
+}
+
+qreal NodeViewItem::default_item_vertical_padding(NodeViewCommon::FlowDirection dir)
+{
+ if (NodeViewCommon::get_flow_orientation(dir) == Qt::Horizontal) {
+ return default_item_height() * 1.5;
+ } else {
+ return default_item_height() * 2.0;
+ }
+}
+
+qreal NodeViewItem::default_item_horizontal_padding() const
+{
+ return default_item_horizontal_padding(flow_dir_);
+}
+
+qreal NodeViewItem::default_item_vertical_padding() const
+{
+ return default_item_vertical_padding(flow_dir_);
+}
+
+void NodeViewItem::add_edge(NodeViewEdge *edge)
+{
+ edges_.append(edge);
+}
+
+void NodeViewItem::remove_edge(NodeViewEdge *edge)
+{
+ edges_.removeOne(edge);
+}
+
+void NodeViewItem::set_expanded(bool e, bool hide_titlebar)
+{
+ if (!can_be_expanded() || (expanded_ == e)) {
+ return;
+ }
+
+ expanded_ = e;
+
+ if (context_) {
+ context_->set_node_expanded_in_context(node_, e);
+ }
+
+ if (is_output_item()) {
+ // We don't have to check has_connectable_inputs_ here because we did it at the top
+ input_connector_->setVisible(!expanded_);
+ }
+
+ if (expanded_) {
+ node_->retranslate();
+
+ if (is_output_item()) {
+ // Create items for each input of the node
+ foreach (const QString &input, node_->inputs()) {
+ if (is_input_valid(input)) {
+ NodeViewItem *item =
+ new NodeViewItem(node_, input, -1, context_, this);
+ children_.append(item);
+ }
+ }
+
+ QVector edges = edges_;
+ for (auto it = edges.cbegin(); it != edges.cend(); it++) {
+ if ((*it)->to_item() == this) {
+ (*it)->set_to_item(get_item_for_input((*it)->input()));
+ }
+ }
+ } else {
+ // Create items for each element of the input array
+ int arr_sz = node_->input_array_size(input_);
+ children_.resize(arr_sz);
+ for (int i = 0; i < arr_sz; i++) {
+ NodeViewItem *item =
+ new NodeViewItem(node_, input_, i, context_, this);
+ children_[i] = item;
+ }
+
+ QVector edges = edges_;
+ for (auto it = edges.cbegin(); it != edges.cend(); it++) {
+ if ((*it)->to_item() == this) {
+ (*it)->set_to_item(get_item_for_input((*it)->input()));
+ }
+ }
+ }
+ } else {
+ foreach (NodeViewItem *child, children_) {
+ QVector child_edges = child->edges();
+ foreach (NodeViewEdge *edge, child_edges) {
+ edge->set_to_item(this);
+ }
+ delete child;
+ }
+ children_.clear();
+ }
+
+ update_children_positions();
+
+ if (flow_dir_ == NodeViewCommon::k_top_to_bottom) {
+ update_output_connector_position();
+ }
+
+ readjust_all_edges();
+
+ update_context_rect();
+
+ update();
+}
+
+void NodeViewItem::toggle_expanded()
+{
+ set_expanded(!is_expanded());
+}
+
+void NodeViewItem::paint(QPainter *painter,
+ const QStyleOptionGraphicsItem *option, QWidget *)
+{
+ // Use main window palette since the palette passed in `widget` is the NodeView palette which
+ // has been slightly modified
+ QPalette app_pal = Core::instance()->main_window()->palette();
+
+ // We only draw a single unit's worth
+ QRectF single_unit_rect = rect();
+ single_unit_rect.setHeight(default_item_height());
+
+ if (is_output_item()) {
+ // Set output item colors
+ painter->setPen(Qt::black);
+ painter->setBrush(
+ node_->brush(single_unit_rect.top(), single_unit_rect.bottom()));
+ } else {
+ // Set input item colors
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(element_ == -1 ? app_pal.color(QPalette::Window) :
+ app_pal.color(QPalette::Base));
+ }
+
+ painter->drawRect(single_unit_rect);
+
+ // Draw highlight if applicable
+ if (highlighted_) {
+ QColor highlight_col = app_pal.color(QPalette::Text);
+ highlight_col.setAlpha(64);
+ painter->setBrush(highlight_col);
+ painter->drawRect(rect());
+ }
+
+ // Determine what text to draw and whether to draw an arrow
+ QString node_label, node_name;
+
+ if (is_output_item()) {
+ if (label_as_output_) {
+ node_name = QCoreApplication::translate("NodeViewItem", "Output");
+ } else {
+ node_label = node_->get_label();
+ node_name = node_->short_name();
+ }
+ } else {
+ if (element_ == -1) {
+ node_name = node_->get_input_name(input_);
+ } else {
+ node_name = QString::number(
+ element_ +
+ node_->get_input_property(input_, QStringLiteral("arraystart"))
+ .toInt());
+ }
+ }
+
+ // Draw arrow if necessary
+ int arrow_size = can_be_expanded() ? draw_expand_arrow(painter) : 0;
+
+ if (is_output_item()) {
+ // Determine the text color (automatically calculate from node background color)
+ painter->setPen(ColorCoding::get_ui_selector_color(node_->color()));
+ } else {
+ // Just use text item
+ painter->setPen(app_pal.text().color());
+ }
+
+ if (node_label.isEmpty()) {
+ // Draw name only
+ draw_node_title(painter, node_name, single_unit_rect, Qt::AlignVCenter,
+ arrow_size);
+ } else {
+ int text_pad = default_text_padding() / 2;
+ QRectF safe_label_bounds =
+ single_unit_rect.adjusted(text_pad, text_pad, -text_pad, -text_pad);
+ QFont f;
+ qreal font_sz = f.pointSizeF();
+
+ // Draw label as larger/upper text
+ f.setPointSizeF(font_sz * 0.8);
+ painter->setFont(f);
+ draw_node_title(painter, node_label, safe_label_bounds, Qt::AlignTop,
+ arrow_size);
+
+ // Draw node name as smaller/lower text
+ f.setPointSizeF(font_sz * 0.6);
+ painter->setFont(f);
+ draw_node_title(painter, node_name, safe_label_bounds, Qt::AlignBottom,
+ arrow_size);
+ }
+
+ if (is_output_item()) {
+ auto *instance = node_->getPluginInstance();
+ auto *olive_instance =
+ dynamic_cast(instance);
+ int message_count =
+ olive_instance ? olive_instance->persistent_message_count() : 0;
+
+ if (message_count > 0) {
+ QString badge_text = QString::number(message_count);
+ QFont badge_font = painter->font();
+ badge_font.setPointSizeF(badge_font.pointSizeF() * 0.7);
+ painter->setFont(badge_font);
+
+ QFontMetrics badge_metrics(badge_font);
+ int text_width = badge_metrics.horizontalAdvance(badge_text);
+ int text_height = badge_metrics.height();
+ int pad = text_height / 3;
+ int badge_width = qMax(text_width + pad * 2, text_height + pad);
+ int badge_height = text_height + pad;
+
+ QRectF badge_rect(single_unit_rect.right() - badge_width - 4,
+ single_unit_rect.top() + 4, badge_width,
+ badge_height);
+
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(QColor(220, 50, 47));
+ painter->drawRoundedRect(badge_rect, badge_height / 2,
+ badge_height / 2);
+
+ painter->setPen(Qt::white);
+ painter->drawText(badge_rect, Qt::AlignCenter, badge_text);
+ }
+ }
+
+ // Draw final border (output only)
+ if (is_output_item()) {
+ QPen border_pen;
+ border_pen.setWidth(node_border_width_);
+
+ if (option->state & QStyle::State_Selected) {
+ border_pen.setColor(app_pal.color(QPalette::Highlight));
+ } else {
+ border_pen.setColor(Qt::black);
+ }
+
+ painter->setPen(border_pen);
+ painter->setBrush(Qt::NoBrush);
+
+ painter->drawRect(rect());
+ }
+}
+
+void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
+{
+ if (last_arrow_rect_.contains(event->pos().toPoint())) {
+ arrow_click_ = true;
+ toggle_expanded();
+ return;
+ }
+
+ event->setModifiers(
+ QtUtils::flip_control_and_shift_modifiers(event->modifiers()));
+
+ QGraphicsRectItem::mousePressEvent(event);
+}
+
+void NodeViewItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
+{
+ if (arrow_click_) {
+ return;
+ }
+
+ event->setModifiers(
+ QtUtils::flip_control_and_shift_modifiers(event->modifiers()));
+
+ QGraphicsRectItem::mouseMoveEvent(event);
+}
+
+void NodeViewItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
+{
+ if (arrow_click_) {
+ arrow_click_ = false;
+ return;
+ }
+
+ event->setModifiers(
+ QtUtils::flip_control_and_shift_modifiers(event->modifiers()));
+
+ QGraphicsRectItem::mouseReleaseEvent(event);
+}
+
+QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change,
+ const QVariant &value)
+{
+ if (node_) {
+ if (change == ItemPositionHasChanged) {
+ readjust_all_edges();
+
+ update_context_rect();
+ } else if (change == ItemSelectedHasChanged) {
+ if (value.toBool()) {
+ qDebug() << "Selected node:" << node_;
+ }
+ }
+ }
+
+ return QGraphicsItem::itemChange(change, value);
+}
+
+void NodeViewItem::readjust_all_edges()
+{
+ foreach (NodeViewEdge *edge, edges_) {
+ if (NodeViewItem *to_item = edge->to_item()) {
+ static_cast(to_item->parentItem())
+ ->update_flow_direction_of_input_item(to_item);
+ }
+
+ edge->adjust();
+ }
+ foreach (NodeViewItem *child, children_) {
+ child->readjust_all_edges();
+ }
+}
+
+void NodeViewItem::update_context_rect()
+{
+ QGraphicsItem *item = parentItem();
+
+ while (item) {
+ if (NodeViewContext *ctx = dynamic_cast(item)) {
+ ctx->update_rect();
+ break;
+ }
+
+ item = item->parentItem();
+ }
+}
+
+void NodeViewItem::draw_node_title(QPainter *painter, QString text,
+ const QRectF &rect,
+ Qt::Alignment vertical_align,
+ int icon_full_size)
+{
+ QFontMetrics fm = painter->fontMetrics();
+
+ // Calculate how much space we have for text
+ int item_width = this->rect().width();
+ int max_text_width = item_width - default_text_padding() * 2 - icon_full_size;
+ int label_width = QtUtils::q_font_metrics_width(fm, text);
+
+ // Concatenate text if necessary (adds a "..." to the end and removes characters until the
+ // string fits in the bounds)
+ if (label_width > max_text_width) {
+ QString concatenated;
+
+ do {
+ text.chop(1);
+ concatenated =
+ QCoreApplication::translate("NodeViewItem", "%1...").arg(text);
+ } while ((label_width = QtUtils::q_font_metrics_width(fm, concatenated)) >
+ max_text_width);
+
+ text = concatenated;
+ }
+
+ // Determine X position (favors horizontal centering unless it'll overrun the arrow)
+ QRectF text_rect = rect;
+ Qt::Alignment text_align = Qt::AlignHCenter | vertical_align;
+ int likely_x = item_width / 2 - label_width / 2;
+ if (likely_x < icon_full_size) {
+ text_rect.adjust(icon_full_size, 0, 0, 0);
+ text_align = Qt::AlignLeft | vertical_align;
+ }
+
+ // Draw the text in a rect (the rect is sized around text already in the constructor)
+ painter->drawText(text_rect, text_align, text);
+}
+
+int NodeViewItem::draw_expand_arrow(QPainter *painter)
+{
+ // Draw right or down arrow based on expanded state
+ int icon_size = painter->fontMetrics().height() / 2;
+ int icon_padding = default_item_height() / 2 - icon_size / 2;
+ int icon_full_size = icon_size + icon_padding * 2;
+
+ painter->setRenderHint(QPainter::SmoothPixmapTransform);
+
+ const QIcon &expand_icon = is_expanded() ? icon::tri_down : icon::tri_right;
+ int icon_size_scaled = icon_size * painter->transform().m11();
+
+ last_arrow_rect_ = QRect(this->rect().x() + icon_padding,
+ this->rect().y() + icon_padding, icon_size,
+ icon_size);
+
+ painter->drawPixmap(
+ last_arrow_rect_,
+ expand_icon.pixmap(QSize(icon_size_scaled, icon_size_scaled)));
+
+ return icon_full_size;
+}
+
+void NodeViewItem::set_label_as_output(bool e)
+{
+ label_as_output_ = e;
+ output_connector_->setVisible(!e);
+ update();
+}
+
+QPointF NodeViewItem::get_input_point() const
+{
+ return input_connector_->scenePos();
+}
+
+QPointF NodeViewItem::get_output_point() const
+{
+ QPointF p = output_connector_->scenePos();
+ QRectF r = output_connector_->polygon().boundingRect();
+
+ switch (flow_dir_) {
+ case NodeViewCommon::k_left_to_right:
+ default:
+ p.setX(p.x() + r.width());
+ break;
+ case NodeViewCommon::k_right_to_left:
+ p.setX(p.x() - r.width());
+ break;
+ case NodeViewCommon::k_top_to_bottom:
+ p.setY(p.y() + r.height());
+ break;
+ case NodeViewCommon::k_bottom_to_top:
+ p.setY(p.y() - r.height());
+ break;
+ }
+
+ return p;
+}
+
+void NodeViewItem::set_flow_direction(NodeViewCommon::FlowDirection dir)
+{
+ if (flow_dir_ != dir) {
+ flow_dir_ = dir;
+
+ input_connector_->set_flow_direction(dir);
+ output_connector_->set_flow_direction(dir);
+
+ update_input_connector_position();
+ update_output_connector_position();
+
+ if (is_output_item()) {
+ update_node_position();
+ }
+
+ readjust_all_edges();
+ }
+}
+
+void NodeViewItem::update_node_position()
+{
+ setPos(node_to_screen_point(cached_node_pos_, flow_dir_));
+}
+
+void NodeViewItem::update_input_connector_position()
+{
+ QRectF output_rect = input_connector_->polygon().boundingRect();
+
+ NodeViewCommon::FlowDirection using_flow_dir = flow_dir_;
+
+ if (is_expanded() && !NodeViewCommon::is_flow_horizontal(flow_dir_)) {
+ if (edges_.isEmpty() || edges_.first()->from_item()->x() < this->x()) {
+ using_flow_dir = NodeViewCommon::k_left_to_right;
+ } else {
+ using_flow_dir = NodeViewCommon::k_right_to_left;
+ }
+ }
+
+ // Input connector flow directions change conditionally
+ switch (using_flow_dir) {
+ case NodeViewCommon::k_left_to_right:
+ input_connector_->setPos(rect().left() - output_rect.width(), 0);
+ break;
+ case NodeViewCommon::k_right_to_left:
+ input_connector_->setPos(rect().right() + output_rect.width(), 0);
+ break;
+ case NodeViewCommon::k_top_to_bottom:
+ input_connector_->setPos(rect().center().x(),
+ rect().top() - output_rect.height());
+ break;
+ case NodeViewCommon::k_bottom_to_top:
+ input_connector_->setPos(rect().center().x(),
+ rect().bottom() + output_rect.height());
+ break;
+ case NodeViewCommon::k_invalid_direction:
+ break;
+ }
+}
+
+void NodeViewItem::update_output_connector_position()
+{
+ switch (flow_dir_) {
+ case NodeViewCommon::k_left_to_right:
+ output_connector_->setPos(rect().right(), 0);
+ break;
+ case NodeViewCommon::k_right_to_left:
+ output_connector_->setPos(rect().left(), 0);
+ break;
+ case NodeViewCommon::k_top_to_bottom:
+ output_connector_->setPos(rect().center().x(), rect().bottom());
+ break;
+ case NodeViewCommon::k_bottom_to_top:
+ output_connector_->setPos(rect().center().x(), rect().top());
+ break;
+ case NodeViewCommon::k_invalid_direction:
+ break;
+ }
+}
+
+bool NodeViewItem::is_input_valid(const QString &input)
+{
+ if (!node_->is_input_connectable(input) || node_->is_input_hidden(input)) {
+ return false;
+ }
+ // For OFX plugin nodes, only show texture inputs in the node graph
+ // to avoid excessively tall nodes with dozens of scalar parameters.
+ // Scalar parameters are still visible in the parameter panel.
+ if (node_->getPluginInstance() != nullptr &&
+ node_->get_input_data_type(input) != NodeValue::k_texture) {
+ return false;
+ }
+ return true;
+}
+
+void NodeViewItem::set_rect_size(int height_units)
+{
+ // Set rect
+ int widget_width = default_item_width();
+ int widget_height = default_item_height();
+
+ setRect(QRectF(-widget_width / 2, -widget_height / 2, widget_width,
+ widget_height * height_units));
+}
+
+bool NodeViewItem::can_be_expanded() const
+{
+ if (is_output_item()) {
+ return has_connectable_inputs_;
+ } else {
+ return node_->get_input_flags(input_) & k_input_flag_array &&
+ element_ == -1 && !node_->is_input_connected(input_);
+ }
+}
+
+void NodeViewItem::update_children_positions()
+{
+ int y = 1;
+ int h = default_item_height();
+
+ foreach (NodeViewItem *c, children_) {
+ c->setPos(QPointF(0, y * h));
+
+ y += c->get_logical_height_with_children();
+ }
+
+ set_rect_size(y);
+
+ if (NodeViewItem *p = dynamic_cast(parentItem())) {
+ p->update_children_positions();
+ }
+}
+
+int NodeViewItem::get_logical_height_with_children() const
+{
+ int h = 1;
+
+ foreach (NodeViewItem *c, children_) {
+ h += c->get_logical_height_with_children();
+ }
+
+ return h;
+}
+
+void NodeViewItem::update_flow_direction_of_input_item(NodeViewItem *child)
+{
+ if (!child->is_output_item()) {
+ if (NodeViewCommon::is_flow_vertical(flow_dir_)) {
+ if (!child->edges().isEmpty() &&
+ child->edges().first()->from_item()->scenePos().x() >
+ child->scenePos().x()) {
+ child->set_flow_direction(NodeViewCommon::k_right_to_left);
+ } else {
+ child->set_flow_direction(NodeViewCommon::k_left_to_right);
+ }
+ } else {
+ child->set_flow_direction(flow_dir_);
+ }
+ }
+}
+
+void NodeViewItem::repopulate_inputs()
+{
+ if (is_output_item()) {
+ has_connectable_inputs_ = false;
+
+ foreach (const QString &input, node_->inputs()) {
+ if (is_input_valid(input)) {
+ has_connectable_inputs_ = true;
+ break;
+ }
+ }
+
+ input_connector_->setVisible(has_connectable_inputs_);
+ }
+
+ if (is_expanded() && (is_output_item() || element_ == -1)) {
+ // Create or remove inputs when necessary
+ // NOTE: This is not the most efficient thing in the world, but it does work
+ set_expanded(false);
+ set_expanded(true);
+ }
+}
+
+void NodeViewItem::input_array_size_changed(const QString &input)
+{
+ if (input == input_) {
+ repopulate_inputs();
+ }
+}
+
+void NodeViewItem::node_appearance_changed()
+{
+ update();
+}
+
+void NodeViewItem::set_highlighted(bool e)
+{
+ highlighted_ = e;
+ update();
+}
+
+NodeViewItem *NodeViewItem::get_item_for_input(NodeInput input)
+{
+ if (NodeGroup *group = dynamic_cast(node_)) {
+ if (input.node() != group) {
+ // Translate input to group input
+ QString id = group->get_id_of_passthrough(input);
+ input.set_node(group);
+ input.set_input(id);
+ }
+ }
+
+ if (is_expanded()) {
+ if (input_.isEmpty()) {
+ // Look for the input in our children
+ foreach (NodeViewItem *i, children_) {
+ if (i->input_ == input.input()) {
+ return i->get_item_for_input(input);
+ }
+ }
+ } else {
+ // Look for element in our children
+ if (input.element() >= 0 && input.element() < children_.size()) {
+ return children_.at(input.element())->get_item_for_input(input);
+ }
+ }
+ }
+
+ // Fallback to this object
+ return this;
+}
+
+}
diff --git a/.bak/nodeview/nodeviewitem.h b/.bak/nodeview/nodeviewitem.h
new file mode 100644
index 000000000..93d4f13ac
--- /dev/null
+++ b/.bak/nodeview/nodeviewitem.h
@@ -0,0 +1,246 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_NODEVIEWITEM_H
+#define OAK_NODEVIEWITEM_H
+
+#include
+#include
+#include
+#include
+
+#include "node/node.h"
+#include "nodeviewcommon.h"
+#include "nodeviewitemconnector.h"
+
+namespace olive
+{
+
+class NodeViewItem;
+class NodeViewEdge;
+
+/**
+ * @brief A visual widget representation of a Node object to be used in a NodeView
+ *
+ * This widget can be collapsed or expanded to show/hide the node's various parameters.
+ *
+ * To retrieve the NodeViewItem for a certain Node, use NodeView::NodeToUIObject().
+ */
+class NodeViewItem : public QObject, public QGraphicsRectItem {
+ Q_OBJECT
+public:
+ NodeViewItem(Node *node, const QString &input, int element, Node *context,
+ QGraphicsItem *parent = nullptr);
+ NodeViewItem(Node *node, Node *context, QGraphicsItem *parent = nullptr)
+ : NodeViewItem(node, QString(), -1, context, parent)
+ {
+ }
+
+ virtual ~NodeViewItem() override;
+
+ Node::Position get_node_position_data() const;
+ QPointF get_node_position() const;
+ void set_node_position(const QPointF &pos);
+ void set_node_position(const Node::Position &pos);
+
+ QVector get_all_edges_recursively() const;
+
+ /**
+ * @brief Get currently attached node
+ */
+ Node *get_node() const
+ {
+ return node_;
+ }
+
+ NodeInput get_input() const
+ {
+ return NodeInput(node_, input_, element_);
+ }
+
+ Node *get_context() const
+ {
+ return context_;
+ }
+
+ /**
+ * @brief Get expanded state
+ */
+ bool is_expanded() const
+ {
+ return expanded_;
+ }
+
+ const QVector &edges() const
+ {
+ return edges_;
+ }
+
+ /**
+ * @brief Set expanded state
+ */
+ void set_expanded(bool e, bool hide_titlebar = false);
+ void toggle_expanded();
+
+ QPointF get_input_point() const;
+ QPointF get_output_point() const;
+
+ /**
+ * @brief Sets the direction nodes are flowing
+ */
+ void set_flow_direction(NodeViewCommon::FlowDirection dir);
+
+ NodeViewCommon::FlowDirection get_flow_direction() const
+ {
+ return flow_dir_;
+ }
+
+ static int default_text_padding();
+
+ static int default_item_height();
+
+ static int default_item_width();
+
+ static int default_item_border();
+
+ static QPointF node_to_screen_point(QPointF p,
+ NodeViewCommon::FlowDirection direction);
+ static QPointF screen_to_node_point(QPointF p,
+ NodeViewCommon::FlowDirection direction);
+
+ static qreal
+ default_item_horizontal_padding(NodeViewCommon::FlowDirection dir);
+ static qreal default_item_vertical_padding(NodeViewCommon::FlowDirection dir);
+ qreal default_item_horizontal_padding() const;
+ qreal default_item_vertical_padding() const;
+
+ void add_edge(NodeViewEdge *edge);
+ void remove_edge(NodeViewEdge *edge);
+
+ bool is_labelled_as_output_of_context() const
+ {
+ return label_as_output_;
+ }
+
+ void set_label_as_output(bool e);
+
+ void set_highlighted(bool e);
+
+ NodeViewItem *get_item_for_input(NodeInput input);
+
+ bool is_output_item() const
+ {
+ return input_.isEmpty();
+ }
+
+ void readjust_all_edges();
+
+ void update_flow_direction_of_input_item(NodeViewItem *child);
+
+ bool can_be_expanded() const;
+
+protected:
+ virtual void paint(QPainter *painter,
+ const QStyleOptionGraphicsItem *option,
+ QWidget *widget = nullptr) override;
+
+ virtual void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
+ virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
+ virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
+
+ virtual QVariant itemChange(QGraphicsItem::GraphicsItemChange change,
+ const QVariant &value) override;
+
+private:
+ void update_context_rect();
+
+ void draw_node_title(QPainter *painter, QString text, const QRectF &rect,
+ Qt::Alignment vertical_align, int icon_full_size);
+
+ int draw_expand_arrow(QPainter *painter);
+
+ /**
+ * @brief Internal update function when logical position changes
+ */
+ void update_node_position();
+
+ void update_input_connector_position();
+ void update_output_connector_position();
+
+ bool is_input_valid(const QString &input);
+
+ void set_rect_size(int height_units = 1);
+
+ void update_children_positions();
+
+ int get_logical_height_with_children() const;
+
+ /**
+ * @brief Reference to attached Node
+ */
+ Node *node_;
+ QString input_;
+ int element_;
+
+ Node *context_;
+
+ /**
+ * @brief Cached list of node inputs
+ */
+ QVector children_;
+
+ /// Sizing variables to use when drawing
+ int node_border_width_;
+
+ /**
+ * @brief Expanded state
+ */
+ bool expanded_;
+
+ bool highlighted_;
+
+ NodeViewCommon::FlowDirection flow_dir_;
+
+ QVector edges_;
+
+ QPointF cached_node_pos_;
+
+ QRect last_arrow_rect_;
+ bool arrow_click_;
+
+ NodeViewItemConnector *input_connector_;
+ NodeViewItemConnector *output_connector_;
+
+ bool has_connectable_inputs_;
+
+ bool label_as_output_;
+
+private slots:
+ void node_appearance_changed();
+
+ void repopulate_inputs();
+
+ void input_array_size_changed(const QString &input);
+};
+
+}
+
+#endif // OAK_NODEVIEWITEM_H
diff --git a/.bak/nodeview/nodeviewitemconnector.cpp b/.bak/nodeview/nodeviewitemconnector.cpp
new file mode 100644
index 000000000..8e05ade87
--- /dev/null
+++ b/.bak/nodeview/nodeviewitemconnector.cpp
@@ -0,0 +1,102 @@
+/***
+
+ 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 "nodeviewitemconnector.h"
+
+#include
+#include
+#include
+#include
+
+#include "nodeviewitem.h"
+
+namespace olive
+{
+
+NodeViewItemConnector::NodeViewItemConnector(bool is_output,
+ QGraphicsItem *parent)
+ : QGraphicsPolygonItem(parent)
+ , output_(is_output)
+{
+ QColor c = qApp->palette().text().color();
+ setPen(QPen(c, NodeViewItem::default_item_border()));
+ setBrush(c);
+}
+
+void NodeViewItemConnector::set_flow_direction(NodeViewCommon::FlowDirection dir)
+{
+ QFont f;
+ QFontMetricsF fm(f);
+
+ int triangle_sz = fm.height() / 2;
+ int triangle_sz_half = triangle_sz / 2;
+
+ QPolygonF p;
+ p.resize(3);
+
+ switch (dir) {
+ case NodeViewCommon::k_left_to_right:
+ // Triangle pointing right
+ p[0] = QPointF(0, -triangle_sz_half);
+ p[1] = QPointF(triangle_sz_half, 0);
+ p[2] = QPointF(0, triangle_sz_half);
+ break;
+ case NodeViewCommon::k_top_to_bottom:
+ // Triangle pointing down
+ p[0] = QPointF(-triangle_sz_half, 0);
+ p[1] = QPointF(0, triangle_sz_half);
+ p[2] = QPointF(triangle_sz_half, 0);
+ break;
+ case NodeViewCommon::k_bottom_to_top:
+ // Triangle pointing up
+ p[0] = QPointF(-triangle_sz_half, 0);
+ p[1] = QPointF(0, -triangle_sz_half);
+ p[2] = QPointF(triangle_sz_half, 0);
+ break;
+ case NodeViewCommon::k_right_to_left:
+ // Triangle pointing left
+ p[0] = QPointF(0, -triangle_sz_half);
+ p[1] = QPointF(-triangle_sz_half, 0);
+ p[2] = QPointF(0, triangle_sz_half);
+ break;
+ case NodeViewCommon::k_invalid_direction:
+ break;
+ }
+
+ setPolygon(p);
+}
+
+QPainterPath NodeViewItemConnector::shape() const
+{
+ // Yes, we skip QGraphicsPolygonItem because it adds the polygon. QGraphicsItem adds the
+ // boundingRect which we modify below
+ return QGraphicsItem::shape(); // clazy:exclude=skipped-base-method
+}
+
+QRectF NodeViewItemConnector::boundingRect() const
+{
+ QRectF b = this->polygon().boundingRect();
+ const int radius = QFontMetrics(QFont()).height() / 2;
+ b.adjust(-radius, -radius, radius, radius);
+ return b;
+}
+
+}
diff --git a/.bak/nodeview/nodeviewitemconnector.h b/.bak/nodeview/nodeviewitemconnector.h
new file mode 100644
index 000000000..253e15014
--- /dev/null
+++ b/.bak/nodeview/nodeviewitemconnector.h
@@ -0,0 +1,52 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_NODEVIEWITEMCONNECTOR_H
+#define OAK_NODEVIEWITEMCONNECTOR_H
+
+#include
+
+#include "nodeviewcommon.h"
+
+namespace olive
+{
+
+class NodeViewItemConnector : public QGraphicsPolygonItem {
+public:
+ NodeViewItemConnector(bool is_output, QGraphicsItem *parent = nullptr);
+
+ void set_flow_direction(NodeViewCommon::FlowDirection dir);
+
+ bool is_output() const
+ {
+ return output_;
+ }
+
+ virtual QPainterPath shape() const override;
+ virtual QRectF boundingRect() const override;
+
+private:
+ bool output_;
+};
+
+}
+
+#endif // OAK_NODEVIEWITEMCONNECTOR_H
diff --git a/.bak/nodeview/nodeviewminimap.cpp b/.bak/nodeview/nodeviewminimap.cpp
new file mode 100644
index 000000000..3e7baace5
--- /dev/null
+++ b/.bak/nodeview/nodeviewminimap.cpp
@@ -0,0 +1,161 @@
+/***
+
+ 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 "nodeviewminimap.h"
+
+#include
+
+namespace olive
+{
+
+#define super QGraphicsView
+
+NodeViewMiniMap::NodeViewMiniMap(NodeViewScene *scene, QWidget *parent)
+ : super(parent)
+ , resizing_(false)
+{
+ connect(scene, &QGraphicsScene::sceneRectChanged, this,
+ &NodeViewMiniMap::scene_changed);
+ setScene(scene);
+
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ setViewportUpdateMode(FullViewportUpdate);
+ setFrameShape(QFrame::Panel);
+ setFrameShadow(QFrame::Plain);
+ setMouseTracking(true);
+
+ QMetaObject::invokeMethod(this, &NodeViewMiniMap::set_default_size,
+ Qt::QueuedConnection);
+
+ resize_triangle_sz_ = fontMetrics().height() / 2;
+}
+
+void NodeViewMiniMap::set_viewport_rect(const QPolygonF &rect)
+{
+ viewport_rect_ = rect;
+
+ viewport()->update();
+}
+
+void NodeViewMiniMap::drawForeground(QPainter *painter, const QRectF &rect)
+{
+ super::drawForeground(painter, rect);
+
+ QColor viewport_color = palette().text().color();
+
+ // Draw resize triangle
+ painter->save();
+ painter->resetTransform();
+
+ QPointF triangle[3] = { QPointF(0, 0), QPointF(resize_triangle_sz_, 0),
+ QPointF(0, resize_triangle_sz_) };
+ painter->setBrush(viewport_color);
+ painter->setPen(viewport_color);
+ painter->drawPolygon(triangle, 3);
+
+ painter->restore();
+
+ // Draw viewport rectangle
+ viewport_color.setAlphaF(0.25);
+ painter->setBrush(viewport_color);
+
+ painter->drawPolygon(viewport_rect_);
+}
+
+void NodeViewMiniMap::resizeEvent(QResizeEvent *event)
+{
+ super::resizeEvent(event);
+
+ emit resized();
+
+ scene_changed(sceneRect());
+}
+
+void NodeViewMiniMap::mousePressEvent(QMouseEvent *event)
+{
+ if (event->button() == Qt::LeftButton) {
+ if (mouse_inside_resize_triangle(event)) {
+ // Resizing!
+ resizing_ = true;
+ resize_anchor_ = QCursor::pos();
+ } else {
+ emit_move_signal(event);
+ }
+ }
+}
+
+void NodeViewMiniMap::mouseMoveEvent(QMouseEvent *event)
+{
+ if (event->buttons() & Qt::LeftButton) {
+ if (resizing_) {
+ QPointF movement = QCursor::pos() - resize_anchor_;
+ resize(QSize(width() - movement.x(), height() - movement.y()));
+ resize_anchor_ = QCursor::pos();
+ } else {
+ emit_move_signal(event);
+ }
+ } else {
+ if (mouse_inside_resize_triangle(event)) {
+ setCursor(Qt::SizeFDiagCursor);
+ } else {
+ unsetCursor();
+ }
+ }
+}
+
+void NodeViewMiniMap::mouseReleaseEvent(QMouseEvent *event)
+{
+ resizing_ = false;
+}
+
+void NodeViewMiniMap::scene_changed(const QRectF &bounding)
+{
+ double x_scale = double(this->width()) / bounding.width();
+ double y_scale = double(this->height()) / bounding.height();
+
+ double min_scale = qMin(x_scale, y_scale);
+
+ QTransform transform;
+ transform.scale(min_scale, min_scale);
+
+ setTransform(transform);
+}
+
+void NodeViewMiniMap::set_default_size()
+{
+ if (parentWidget()) {
+ resize(parentWidget()->width() / 4, parentWidget()->height() / 4);
+ }
+}
+
+bool NodeViewMiniMap::mouse_inside_resize_triangle(QMouseEvent *event)
+{
+ return event->pos().x() <= resize_triangle_sz_ &&
+ event->pos().y() <= resize_triangle_sz_;
+}
+
+void NodeViewMiniMap::emit_move_signal(QMouseEvent *event)
+{
+ emit move_to_scene_point(mapToScene(event->pos()));
+}
+
+}
diff --git a/.bak/nodeview/nodeviewminimap.h b/.bak/nodeview/nodeviewminimap.h
new file mode 100644
index 000000000..de7176d50
--- /dev/null
+++ b/.bak/nodeview/nodeviewminimap.h
@@ -0,0 +1,78 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_NODEVIEWMINIMAP_H
+#define OAK_NODEVIEWMINIMAP_H
+
+#include
+
+#include "nodeviewscene.h"
+
+namespace olive
+{
+
+class NodeViewMiniMap : public QGraphicsView {
+ Q_OBJECT
+public:
+ NodeViewMiniMap(NodeViewScene *scene, QWidget *parent = nullptr);
+
+public slots:
+ void set_viewport_rect(const QPolygonF &rect);
+
+signals:
+ void resized();
+
+ void move_to_scene_point(const QPointF &pos);
+
+protected:
+ virtual void drawForeground(QPainter *painter, const QRectF &rect) override;
+
+ virtual void resizeEvent(QResizeEvent *event) override;
+
+ virtual void mousePressEvent(QMouseEvent *event) override;
+ virtual void mouseMoveEvent(QMouseEvent *event) override;
+ virtual void mouseReleaseEvent(QMouseEvent *event) override;
+ virtual void mouseDoubleClickEvent(QMouseEvent *event) override
+ {
+ }
+
+private slots:
+ void scene_changed(const QRectF &bounding);
+
+ void set_default_size();
+
+private:
+ bool mouse_inside_resize_triangle(QMouseEvent *event);
+
+ void emit_move_signal(QMouseEvent *event);
+
+ int resize_triangle_sz_;
+
+ QPolygonF viewport_rect_;
+
+ bool resizing_;
+
+ QPoint resize_anchor_;
+};
+
+}
+
+#endif // OAK_NODEVIEWMINIMAP_H
diff --git a/.bak/nodeview/nodeviewscene.cpp b/.bak/nodeview/nodeviewscene.cpp
new file mode 100644
index 000000000..bb302efcc
--- /dev/null
+++ b/.bak/nodeview/nodeviewscene.cpp
@@ -0,0 +1,120 @@
+/***
+
+ 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 "nodeviewscene.h"
+
+#include "core.h"
+#include "node/project/sequence/sequence.h"
+#include "nodeviewedge.h"
+#include "nodeviewitem.h"
+
+namespace olive
+{
+
+NodeViewScene::NodeViewScene(QObject *parent)
+ : QGraphicsScene(parent)
+ , direction_(NodeViewCommon::k_left_to_right)
+ , curved_edges_(true)
+{
+}
+
+void NodeViewScene::set_flow_direction(NodeViewCommon::FlowDirection direction)
+{
+ direction_ = direction;
+
+ foreach (NodeViewContext *ctx, context_map_) {
+ ctx->set_flow_direction(direction_);
+ }
+}
+
+void NodeViewScene::select_all()
+{
+ foreach (QGraphicsItem *i, items()) {
+ i->setSelected(true);
+ }
+}
+
+void NodeViewScene::deselect_all()
+{
+ foreach (QGraphicsItem *i, items()) {
+ i->setSelected(false);
+ }
+}
+
+QVector NodeViewScene::get_selected_items() const
+{
+ QVector items;
+
+ foreach (NodeViewContext *ctx, context_map_) {
+ items.append(ctx->get_selected_items());
+ }
+
+ return items;
+}
+
+NodeViewContext *NodeViewScene::add_context(Node *node)
+{
+ NodeViewContext *context_item = context_map_.value(node);
+
+ if (!context_item) {
+ context_item = new NodeViewContext(node);
+
+ context_item->set_flow_direction(get_flow_direction());
+ context_item->set_curved_edges(get_edges_are_curved());
+
+ QPointF pos(0, 0);
+ QRectF item_rect = context_item->rect();
+ while (!items(item_rect).isEmpty()) {
+ pos.setY(pos.y() + item_rect.height());
+ item_rect = context_item->rect().translated(pos);
+ }
+ context_item->setPos(pos);
+
+ addItem(context_item);
+
+ context_map_.insert(node, context_item);
+ }
+
+ return context_item;
+}
+
+void NodeViewScene::remove_context(Node *node)
+{
+ delete context_map_.take(node);
+}
+
+Qt::Orientation NodeViewScene::get_flow_orientation() const
+{
+ return NodeViewCommon::get_flow_orientation(direction_);
+}
+
+void NodeViewScene::set_edges_are_curved(bool curved)
+{
+ if (curved_edges_ != curved) {
+ curved_edges_ = curved;
+
+ foreach (NodeViewContext *ctx, context_map_) {
+ ctx->set_curved_edges(curved_edges_);
+ }
+ }
+}
+
+}
diff --git a/.bak/nodeview/nodeviewscene.h b/.bak/nodeview/nodeviewscene.h
new file mode 100644
index 000000000..297f16447
--- /dev/null
+++ b/.bak/nodeview/nodeviewscene.h
@@ -0,0 +1,87 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_NODEVIEWSCENE_H
+#define OAK_NODEVIEWSCENE_H
+
+#include
+#include
+
+#include "node/project.h"
+#include "nodeviewcontext.h"
+#include "nodeviewedge.h"
+#include "nodeviewitem.h"
+#include "undo/undostack.h"
+
+namespace olive
+{
+
+class NodeViewScene : public QGraphicsScene {
+ Q_OBJECT
+public:
+ NodeViewScene(QObject *parent = nullptr);
+
+ void select_all();
+ void deselect_all();
+
+ QVector get_selected_items() const;
+
+ const QHash &context_map() const
+ {
+ return context_map_;
+ }
+
+ Qt::Orientation get_flow_orientation() const;
+
+ NodeViewCommon::FlowDirection get_flow_direction() const
+ {
+ return direction_;
+ }
+
+ void set_flow_direction(NodeViewCommon::FlowDirection direction);
+
+ bool get_edges_are_curved() const
+ {
+ return curved_edges_;
+ }
+
+public slots:
+ NodeViewContext *add_context(Node *node);
+ void remove_context(Node *node);
+
+ /**
+ * @brief Set whether edges in this scene should be curved or not
+ */
+ void set_edges_are_curved(bool curved);
+
+private:
+ QHash context_map_;
+
+ Project *graph_;
+
+ NodeViewCommon::FlowDirection direction_;
+
+ bool curved_edges_;
+};
+
+}
+
+#endif // OAK_NODEVIEWSCENE_H
diff --git a/.bak/nodeview/nodeviewtoolbar.cpp b/.bak/nodeview/nodeviewtoolbar.cpp
new file mode 100644
index 000000000..54d377407
--- /dev/null
+++ b/.bak/nodeview/nodeviewtoolbar.cpp
@@ -0,0 +1,76 @@
+/*
+ * Oak Video Editor - Non-Linear Video Editor
+ * Copyright (C) 2025 Olive CE Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "nodeviewtoolbar.h"
+
+#include
+#include
+
+#include "ui/icons/icons.h"
+
+namespace olive
+{
+
+#define super QWidget
+
+NodeViewToolBar::NodeViewToolBar(QWidget *parent)
+ : QWidget(parent)
+{
+ QHBoxLayout *layout = new QHBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+
+ add_node_btn_ = new QPushButton();
+ connect(add_node_btn_, &QPushButton::clicked, this,
+ &NodeViewToolBar::add_node_clicked);
+ layout->addWidget(add_node_btn_);
+
+ minimap_btn_ = new QPushButton();
+ minimap_btn_->setCheckable(true);
+ connect(minimap_btn_, &QPushButton::clicked, this,
+ &NodeViewToolBar::mini_map_enabled_toggled);
+ layout->addWidget(minimap_btn_);
+
+ layout->addStretch();
+
+ retranslate();
+ update_icons();
+}
+
+void NodeViewToolBar::changeEvent(QEvent *e)
+{
+ if (e->type() == QEvent::LanguageChange) {
+ retranslate();
+ } else if (e->type() == QEvent::StyleChange) {
+ update_icons();
+ }
+ super::changeEvent(e);
+}
+
+void NodeViewToolBar::retranslate()
+{
+ add_node_btn_->setToolTip(tr("Add Node"));
+ minimap_btn_->setToolTip(tr("Toggle Mini-Map"));
+}
+
+void NodeViewToolBar::update_icons()
+{
+ add_node_btn_->setIcon(icon::add);
+ minimap_btn_->setIcon(icon::mini_map);
+}
+
+}
diff --git a/.bak/nodeview/nodeviewtoolbar.h b/.bak/nodeview/nodeviewtoolbar.h
new file mode 100644
index 000000000..9dbe49bbf
--- /dev/null
+++ b/.bak/nodeview/nodeviewtoolbar.h
@@ -0,0 +1,59 @@
+/*
+ * Oak Video Editor - Non-Linear Video Editor
+ * Copyright (C) 2025 Olive CE Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef OAK_NODEVIEWTOOLBAR_H
+#define OAK_NODEVIEWTOOLBAR_H
+
+#include
+#include
+
+namespace olive
+{
+
+class NodeViewToolBar : public QWidget {
+ Q_OBJECT
+public:
+ NodeViewToolBar(QWidget *parent = nullptr);
+
+public slots:
+ void set_mini_map_enabled(bool e)
+ {
+ minimap_btn_->setChecked(e);
+ }
+
+signals:
+ void add_node_clicked();
+
+ void mini_map_enabled_toggled(bool e);
+
+protected:
+ virtual void changeEvent(QEvent *e) override;
+
+private:
+ void retranslate();
+
+ void update_icons();
+
+ QPushButton *add_node_btn_;
+
+ QPushButton *minimap_btn_;
+};
+
+}
+
+#endif // OAK_NODEVIEWTOOLBAR_H
diff --git a/.bak/nodeview/nodewidget.cpp b/.bak/nodeview/nodewidget.cpp
new file mode 100644
index 000000000..0f3b62e90
--- /dev/null
+++ b/.bak/nodeview/nodewidget.cpp
@@ -0,0 +1,55 @@
+/***
+
+ 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 "nodewidget.h"
+
+#include
+
+namespace olive
+{
+
+NodeWidget::NodeWidget(QWidget *parent)
+ : QWidget(parent)
+{
+ QVBoxLayout *outer_layout = new QVBoxLayout(this);
+ outer_layout->setContentsMargins(0, 0, 0, 0);
+
+ toolbar_ = new NodeViewToolBar();
+ outer_layout->addWidget(toolbar_);
+
+ // Create NodeView widget
+ node_view_ = new NodeView(this);
+ outer_layout->addWidget(node_view_);
+
+ // Connect toolbar to NodeView
+ connect(toolbar_, &NodeViewToolBar::mini_map_enabled_toggled, node_view_,
+ &NodeView::set_mini_map_enabled);
+ connect(toolbar_, &NodeViewToolBar::add_node_clicked, node_view_,
+ &NodeView::show_add_menu);
+
+ // Set defaults
+ toolbar_->set_mini_map_enabled(true);
+ node_view_->set_mini_map_enabled(true);
+
+ setSizePolicy(node_view_->sizePolicy());
+}
+
+}
diff --git a/.bak/nodeview/nodewidget.h b/.bak/nodeview/nodewidget.h
new file mode 100644
index 000000000..2715a2863
--- /dev/null
+++ b/.bak/nodeview/nodewidget.h
@@ -0,0 +1,57 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_NODEWIDGET_H
+#define OAK_NODEWIDGET_H
+
+#include
+
+#include "nodeview.h"
+#include "nodeviewtoolbar.h"
+
+namespace olive
+{
+
+class NodeWidget : public QWidget {
+ Q_OBJECT
+public:
+ NodeWidget(QWidget *parent = nullptr);
+
+ NodeView *view() const
+ {
+ return node_view_;
+ }
+
+ void set_contexts(const QVector &nodes)
+ {
+ node_view_->set_contexts(nodes);
+ toolbar_->setEnabled(!nodes.isEmpty());
+ }
+
+private:
+ NodeView *node_view_;
+
+ NodeViewToolBar *toolbar_;
+};
+
+}
+
+#endif // OAK_NODEWIDGET_H
diff --git a/.bak/projectexplorer/projectexplorer.cpp b/.bak/projectexplorer/projectexplorer.cpp
new file mode 100644
index 000000000..26db30d42
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorer.cpp
@@ -0,0 +1,947 @@
+/***
+
+ 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 "projectexplorer.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "common/define.h"
+#include "core.h"
+#include "dialog/footageproperties/footageproperties.h"
+#include "dialog/proxy/proxydialog.h"
+#include "dialog/sequence/sequence.h"
+#include "projectexplorerundo.h"
+#include "oakengine/footage.h"
+#include "oakengine/node.h"
+#include "oakengine/task.h"
+#include "widget/menu/menu.h"
+#include "widget/menu/menushared.h"
+#include "node/nodeundo.h"
+#include "window/mainwindow/mainwindow.h"
+#include "window/mainwindow/mainwindowundo.h"
+#include "widget/timelinewidget/timelinewidget.h"
+
+namespace olive
+{
+
+namespace
+{
+QVector get_selected_proxy_footage(const QVector &items)
+{
+ QVector footage;
+ for (Node *node : items) {
+ Footage *candidate = dynamic_cast(node);
+ if (!candidate || !candidate->get_first_enabled_video_stream().is_valid() ||
+ footage.contains(candidate)) {
+ continue;
+ }
+ footage.append(candidate);
+ }
+ return footage;
+}
+
+/**
+ * @brief Proxy generation driven by the liboakengine C ABI facade
+ *
+ * Replaces the direct ProxyManager::get_or_start_proxy() drive: the actual
+ * transcode and its synchronous wait live behind
+ * oakengine_footage_proxy_generate() (which also records the proxy state
+ * on the footage and invalidates it), while the task stays on the
+ * TaskManager queue like before.
+ */
+class FacadeProxyTask : public Task {
+public:
+ FacadeProxyTask(Footage *footage)
+ : footage_(footage)
+ {
+ set_title(tr("Generating proxy for \"%1\"")
+ .arg(footage->get_label_or_name()));
+ }
+
+protected:
+ virtual bool run() override
+ {
+ OakEngineFootage *handle = oakengine_footage_borrow(
+ reinterpret_cast(footage_));
+ const int rc = oakengine_footage_proxy_generate(handle);
+ oakengine_footage_free(handle);
+ if (rc != OAKENGINE_OK) {
+ char err[512];
+ err[0] = '\0';
+ oakengine_footage_last_error(err, sizeof(err));
+ set_error(err[0] ? QString::fromUtf8(err) :
+ tr("Proxy generation failed"));
+ return false;
+ }
+ return true;
+ }
+
+private:
+ Footage *footage_;
+};
+}
+
+ProjectExplorer::ProjectExplorer(QWidget *parent)
+ : QWidget(parent)
+ , model_(this)
+{
+ // Create layout
+ QVBoxLayout *layout = new QVBoxLayout(this);
+ layout->setSpacing(0);
+ layout->setContentsMargins(0, 0, 0, 0);
+
+ // Set up navigation bar
+ nav_bar_ = new ProjectExplorerNavigation(this);
+ connect(nav_bar_, &ProjectExplorerNavigation::size_changed, this,
+ &ProjectExplorer::size_changed_slot);
+ connect(nav_bar_, &ProjectExplorerNavigation::directory_up_clicked, this,
+ &ProjectExplorer::dir_up_slot);
+ layout->addWidget(nav_bar_);
+
+ // Set up stacked widget
+ stacked_widget_ = new QStackedWidget(this);
+ layout->addWidget(stacked_widget_);
+
+ // Set up sort filter proxy model
+ sort_model_.setSourceModel(&model_);
+ sort_model_.setFilterCaseSensitivity(Qt::CaseInsensitive);
+ sort_model_.setSortRole(ProjectViewModel::k_inner_text_role);
+
+ // Add tree view to stacked widget
+ tree_view_ = new ProjectExplorerTreeView(stacked_widget_);
+ tree_view_->setSortingEnabled(true);
+ tree_view_->sortByColumn(0, Qt::AscendingOrder);
+ tree_view_->setContextMenuPolicy(Qt::CustomContextMenu);
+ add_view(tree_view_);
+
+ // Add list view to stacked widget
+ list_view_ = new ProjectExplorerListView(stacked_widget_);
+ list_view_->setContextMenuPolicy(Qt::CustomContextMenu);
+ add_view(list_view_);
+
+ // Add icon view to stacked widget
+ icon_view_ = new ProjectExplorerIconView(stacked_widget_);
+ icon_view_->setContextMenuPolicy(Qt::CustomContextMenu);
+ add_view(icon_view_);
+
+ // Set default view to tree view
+ set_view_type(ProjectToolbar::tree_view);
+
+ // Set default icon size
+ size_changed_slot(k_project_icon_size_default);
+
+ connect(tree_view_, &ProjectExplorerTreeView::customContextMenuRequested,
+ this, &ProjectExplorer::show_context_menu);
+ connect(list_view_, &ProjectExplorerListView::customContextMenuRequested,
+ this, &ProjectExplorer::show_context_menu);
+ connect(icon_view_, &ProjectExplorerIconView::customContextMenuRequested,
+ this, &ProjectExplorer::show_context_menu);
+
+ update_nav_bar_text();
+}
+
+const ProjectToolbar::ViewType &ProjectExplorer::view_type() const
+{
+ return view_type_;
+}
+
+void ProjectExplorer::set_view_type(ProjectToolbar::ViewType type)
+{
+ view_type_ = type;
+
+ // Set widget based on view type
+ switch (view_type_) {
+ case ProjectToolbar::tree_view:
+ stacked_widget_->setCurrentWidget(tree_view_);
+ nav_bar_->setVisible(false);
+ break;
+ case ProjectToolbar::list_view:
+ stacked_widget_->setCurrentWidget(list_view_);
+ nav_bar_->setVisible(true);
+ break;
+ case ProjectToolbar::icon_view:
+ stacked_widget_->setCurrentWidget(icon_view_);
+ nav_bar_->setVisible(true);
+ break;
+ }
+}
+
+void ProjectExplorer::edit(Node *item)
+{
+ current_view()->edit(
+ sort_model_.mapFromSource(model_.create_index_from_item(item)));
+}
+
+void ProjectExplorer::add_view(QAbstractItemView *view)
+{
+ view->setModel(&sort_model_);
+ view->setEditTriggers(QAbstractItemView::SelectedClicked);
+ connect(view, &QAbstractItemView::doubleClicked, this,
+ &ProjectExplorer::item_double_clicked_slot);
+ connect(view->selectionModel(), &QItemSelectionModel::selectionChanged,
+ this, &ProjectExplorer::view_selection_changed);
+ connect(view, SIGNAL(double_clicked_empty_area()), this,
+ SLOT(view_empty_area_double_clicked_slot()));
+ stacked_widget_->addWidget(view);
+}
+
+void ProjectExplorer::browse_to_folder(const QModelIndex &index)
+{
+ // Set appropriate views to this index
+ icon_view_->setRootIndex(index);
+ list_view_->setRootIndex(index);
+
+ // Set navbar text to folder's name
+ update_nav_bar_text();
+
+ // Set directory up enabled button based on whether we're in root or not
+ nav_bar_->set_dir_up_enabled(index.isValid());
+}
+
+int ProjectExplorer::confirm_item_deletion(Node *item)
+{
+ QMessageBox msgbox(this);
+ msgbox.setWindowTitle(tr("Confirm Item Deletion"));
+ msgbox.setIcon(QMessageBox::Warning);
+
+ QStringList connected_nodes_names;
+ foreach (const Node::OutputConnection &connected,
+ item->output_connections()) {
+ if (!dynamic_cast(connected.second.node())) {
+ connected_nodes_names.append(
+ get_human_readable_node_name(connected.second.node()));
+ }
+ }
+
+ msgbox.setText(
+ tr("The item \"%1\" is currently connected to the following nodes:\n\n"
+ "%2\n\n"
+ "Are you sure you wish to delete this footage?")
+ .arg(get_human_readable_node_name(item),
+ connected_nodes_names.join('\n')));
+
+ // Set up buttons
+ msgbox.addButton(QMessageBox::Yes);
+ msgbox.addButton(QMessageBox::YesToAll);
+ msgbox.addButton(QMessageBox::No);
+ msgbox.addButton(QMessageBox::Cancel);
+
+ // Run messagebox
+ return msgbox.exec();
+}
+
+bool ProjectExplorer::delete_items_internal(const QVector &selected,
+ bool &check_if_item_is_in_use,
+ MultiUndoCommand *command)
+{
+ for (int i = 0; i < selected.size(); i++) {
+ // Delete sequences first
+ Node *node = selected.at(i);
+
+ bool can_delete_item = true;
+
+ if (check_if_item_is_in_use) {
+ foreach (const Node::OutputConnection &oc,
+ node->output_connections()) {
+ Folder *folder_test = dynamic_cast(oc.second.node());
+ if (!folder_test) {
+ // This sequence outputs to SOMETHING, confirm the user if they want to delete this
+ int r = confirm_item_deletion(node);
+
+ switch (r) {
+ case QMessageBox::No:
+ can_delete_item = false;
+ break;
+ case QMessageBox::Cancel:
+ return false;
+ case QMessageBox::YesToAll:
+ check_if_item_is_in_use = false;
+ break;
+ }
+ }
+ }
+ }
+
+ if (can_delete_item) {
+ Sequence *sequence = dynamic_cast(node);
+ if (sequence &&
+ Core::instance()->main_window()->is_sequence_open(sequence)) {
+ command->add_child(new CloseSequenceCommand(sequence));
+ }
+
+ if (node->folder()) {
+ command->add_child(
+ new Folder::RemoveElementCommand(node->folder(), node));
+ }
+
+ command->add_child(
+ new NodeRemoveWithExclusiveDependenciesAndDisconnect(node));
+ }
+ }
+
+ return true;
+}
+
+QString ProjectExplorer::get_human_readable_node_name(Node *node)
+{
+ if (node->get_label().isEmpty()) {
+ return node->name();
+ } else {
+ return tr("%1 (%2)").arg(node->get_label(), node->name());
+ }
+}
+
+void ProjectExplorer::update_nav_bar_text()
+{
+ QString absolute;
+
+ Folder *f = static_cast(
+ sort_model_.mapToSource(list_view_->rootIndex()).internalPointer());
+ while (f && f != project()->root()) {
+ absolute.prepend(QStringLiteral("%1 / ").arg(f->get_label()));
+ f = f->folder();
+ }
+
+ absolute.prepend(QStringLiteral("/ "));
+
+ nav_bar_->set_text(absolute);
+}
+
+QAbstractItemView *ProjectExplorer::current_view() const
+{
+ return static_cast(stacked_widget_->currentWidget());
+}
+
+void ProjectExplorer::view_empty_area_double_clicked_slot()
+{
+ emit double_clicked_item(nullptr);
+}
+
+void ProjectExplorer::item_double_clicked_slot(const QModelIndex &index)
+{
+ // Retrieve source item from index
+ Node *i =
+ static_cast(sort_model_.mapToSource(index).internalPointer());
+
+ // If the item is a folder, browse to it
+ if (dynamic_cast(i) &&
+ (view_type() == ProjectToolbar::list_view ||
+ view_type() == ProjectToolbar::icon_view)) {
+ browse_to_folder(index);
+ }
+
+ // Emit a signal
+ emit double_clicked_item(i);
+}
+
+void ProjectExplorer::size_changed_slot(int s)
+{
+ icon_view_->setGridSize(QSize(s, s));
+
+ list_view_->setIconSize(QSize(s, s));
+}
+
+void ProjectExplorer::dir_up_slot()
+{
+ QModelIndex current_root = icon_view_->rootIndex();
+
+ if (current_root.isValid()) {
+ QModelIndex parent = current_root.parent();
+
+ browse_to_folder(parent);
+ }
+}
+
+void ProjectExplorer::rename_selected_item()
+{
+ auto indexes = current_view()->selectionModel()->selectedRows();
+ if (!indexes.empty()) {
+ current_view()->edit(indexes.first());
+ }
+}
+
+void ProjectExplorer::set_search_filter(const QString &s)
+{
+ sort_model_.setFilterFixedString(s);
+}
+
+void ProjectExplorer::show_context_menu()
+{
+ Menu menu;
+ Menu new_menu;
+
+ context_menu_items_ = selected_items();
+
+ if (context_menu_items_.isEmpty()) {
+ // Items to show if no items are selected
+
+ // "New" menu
+ new_menu.setTitle(tr("&New"));
+ MenuShared::instance()->add_items_for_new_menu(&new_menu);
+ menu.addMenu(&new_menu);
+
+ // "Import" action
+ QAction *import_action = menu.addAction(tr("&Import..."));
+ connect(import_action, &QAction::triggered, Core::instance(),
+ &Core::dialog_import_show);
+ } else {
+ // Actions to add when only one item is selected
+ if (context_menu_items_.size() == 1) {
+ Node *context_menu_item = context_menu_items_.first();
+
+ if (dynamic_cast(context_menu_item)) {
+ QAction *open_in_new_tab =
+ menu.addAction(tr("Open in New Tab"));
+ connect(open_in_new_tab, &QAction::triggered, this,
+ &ProjectExplorer::open_context_menu_item_in_new_tab);
+
+ QAction *open_in_new_window =
+ menu.addAction(tr("Open in New Window"));
+ connect(open_in_new_window, &QAction::triggered, this,
+ &ProjectExplorer::open_context_menu_item_in_new_window);
+
+ } else if (dynamic_cast(context_menu_item)) {
+ QString reveal_text;
+
+#if defined(Q_OS_WINDOWS)
+ reveal_text = tr("Reveal in Explorer");
+#elif defined(Q_OS_MAC)
+ reveal_text = tr("Reveal in Finder");
+#else
+ reveal_text = tr("Reveal in File Manager");
+#endif
+
+ QAction *reveal_action = menu.addAction(reveal_text);
+ connect(reveal_action, &QAction::triggered, this,
+ &ProjectExplorer::reveal_selected_footage);
+
+ QAction *replace_action = menu.addAction(tr("Replace Footage"));
+ connect(replace_action, &QAction::triggered, this,
+ &ProjectExplorer::replace_selected_footage);
+ }
+
+ menu.addSeparator();
+ }
+
+ bool all_items_are_footage = true;
+ bool all_items_have_video_streams = true;
+ bool all_items_are_footage_or_sequence = true;
+
+ foreach (Node *i, context_menu_items_) {
+ Footage *footage_cast_test = dynamic_cast(i);
+ Sequence *sequence_cast_test = dynamic_cast(i);
+
+ if (footage_cast_test &&
+ !footage_cast_test->has_enabled_video_streams()) {
+ all_items_have_video_streams = false;
+ }
+
+ if (!footage_cast_test) {
+ all_items_are_footage = false;
+ }
+
+ if (!footage_cast_test && !sequence_cast_test) {
+ all_items_are_footage_or_sequence = false;
+ }
+ }
+
+ if (all_items_are_footage && all_items_have_video_streams) {
+ const QVector proxy_footage =
+ get_selected_proxy_footage(context_menu_items_);
+
+ Menu *proxy_menu = new Menu(tr("Proxy"), &menu);
+ menu.addMenu(proxy_menu);
+
+ QAction *generate_proxy =
+ proxy_menu->addAction(tr("Generate Proxy"));
+ generate_proxy->setEnabled(!proxy_footage.isEmpty());
+ connect(generate_proxy, &QAction::triggered, this,
+ &ProjectExplorer::generate_proxies_for_selected_footage);
+
+ QAction *use_proxy = proxy_menu->addAction(tr("Use Proxy"));
+ use_proxy->setCheckable(true);
+ use_proxy->setEnabled(!proxy_footage.isEmpty());
+ use_proxy->setChecked(
+ !proxy_footage.isEmpty() &&
+ std::all_of(proxy_footage.cbegin(), proxy_footage.cend(),
+ [](const Footage *footage) {
+ return footage->proxy_enabled();
+ }));
+ connect(use_proxy, &QAction::triggered, this,
+ &ProjectExplorer::set_selected_footage_proxy_enabled);
+
+ QAction *reveal_proxy = proxy_menu->addAction(tr("Reveal Proxy"));
+ reveal_proxy->setEnabled(
+ std::any_of(proxy_footage.cbegin(), proxy_footage.cend(),
+ [](const Footage *footage) {
+ return !footage->proxy_path().isEmpty();
+ }));
+ connect(reveal_proxy, &QAction::triggered, this,
+ &ProjectExplorer::reveal_proxy_for_selected_footage);
+
+ QAction *delete_proxy = proxy_menu->addAction(tr("Delete Proxy"));
+ delete_proxy->setEnabled(
+ std::any_of(proxy_footage.cbegin(), proxy_footage.cend(),
+ [](const Footage *footage) {
+ return !footage->proxy_path().isEmpty();
+ }));
+ connect(delete_proxy, &QAction::triggered, this,
+ &ProjectExplorer::delete_proxies_for_selected_footage);
+
+ QAction *proxy_settings =
+ proxy_menu->addAction(tr("Proxy Settings..."));
+ connect(proxy_settings, &QAction::triggered, this,
+ &ProjectExplorer::show_proxy_dialog_for_selected_footage);
+ }
+
+ Q_UNUSED(all_items_are_footage_or_sequence)
+
+ if (context_menu_items_.size() == 1) {
+ menu.addSeparator();
+
+ auto rename_action = menu.addAction(tr("Rename"));
+ connect(rename_action, &QAction::triggered, this,
+ &ProjectExplorer::rename_selected_item);
+ }
+
+ auto delete_action = menu.addAction(tr("Delete"));
+ connect(delete_action, &QAction::triggered, this,
+ &ProjectExplorer::delete_selected);
+
+ if (context_menu_items_.size() == 1) {
+ menu.addSeparator();
+
+ QAction *properties_action = menu.addAction(tr("P&roperties"));
+ connect(properties_action, &QAction::triggered, this,
+ &ProjectExplorer::show_item_properties_dialog);
+ }
+ }
+
+ menu.exec(QCursor::pos());
+}
+
+void ProjectExplorer::show_item_properties_dialog()
+{
+ Node *sel = context_menu_items_.first();
+
+ // FIXME: Support for multiple items
+ if (dynamic_cast(sel)) {
+ FootagePropertiesDialog fpd(this, static_cast(sel));
+ fpd.exec();
+
+ } else if (dynamic_cast(sel)) {
+ Core::instance()->label_nodes(context_menu_items_);
+
+ } else if (dynamic_cast(sel)) {
+ SequenceDialog sd(static_cast(sel),
+ SequenceDialog::k_existing, this);
+ sd.exec();
+ }
+}
+
+void ProjectExplorer::reveal_selected_footage()
+{
+ Footage *footage = static_cast(context_menu_items_.first());
+
+#if defined(Q_OS_WINDOWS)
+ // Explorer
+ QStringList args;
+ args << "/select," << QDir::toNativeSeparators(footage->filename());
+ QProcess::startDetached("explorer", args);
+#elif defined(Q_OS_MAC)
+ QStringList args;
+ args << "-e";
+ args << "tell application \"Finder\"";
+ args << "-e";
+ args << "activate";
+ args << "-e";
+ args << "select POSIX file \"" + footage->filename() + "\"";
+ args << "-e";
+ args << "end tell";
+ QProcess::startDetached("osascript", args);
+#else
+ QDesktopServices::openUrl(QUrl::fromLocalFile(
+ QFileInfo(footage->filename()).dir().absolutePath()));
+#endif
+}
+
+void ProjectExplorer::replace_selected_footage()
+{
+ Footage *footage = static_cast(context_menu_items_.first());
+
+ QString file =
+ QFileDialog::getOpenFileName(this, tr("Replace Footage"), QString(),
+ Core::footage_file_dialog_filter());
+ if (!file.isEmpty()) {
+ if (!Core::is_footage_extension_allowed(file)) {
+ QMessageBox::warning(
+ this, tr("Unsupported media"),
+ tr("This file type is not allowed by the current media type "
+ "filter."));
+ return;
+ }
+
+ // Change the filename through the facade relink (reprobes the new
+ // file and resets proxy/stream state); the label policy stays here.
+ OakEngineFootage *facade_handle = oakengine_footage_borrow(
+ reinterpret_cast(footage));
+ const int relink_rc = oakengine_footage_relink(
+ facade_handle, file.toUtf8().constData());
+ oakengine_footage_free(facade_handle);
+ if (relink_rc != OAKENGINE_OK) {
+ char err[512];
+ err[0] = '\0';
+ oakengine_footage_last_error(err, sizeof(err));
+ QMessageBox::warning(
+ this, tr("Cannot replace footage"),
+ err[0] ? QString::fromUtf8(err) :
+ tr("The file could not be used as media."));
+ return;
+ }
+
+ if (QFileInfo(footage->filename()).fileName() ==
+ footage->get_label()) {
+ // Footage label == filename, change label too
+ oakengine_node_set_label(
+ reinterpret_cast(footage),
+ QFileInfo(file).fileName().toUtf8().constData());
+ }
+ }
+}
+
+void ProjectExplorer::open_context_menu_item_in_new_tab()
+{
+ Core::instance()->main_window()->open_folder(
+ static_cast(context_menu_items_.first()), false);
+}
+
+void ProjectExplorer::open_context_menu_item_in_new_window()
+{
+ Core::instance()->main_window()->open_folder(
+ static_cast(context_menu_items_.first()), true);
+}
+
+void ProjectExplorer::generate_proxies_for_selected_footage()
+{
+ if (!project()) {
+ qWarning() << "GenerateProxiesForSelectedFootage: no project";
+ return;
+ }
+
+ const QVector footage =
+ get_selected_proxy_footage(context_menu_items_);
+ qDebug()
+ << "GenerateProxiesForSelectedFootage: starting proxy generation for"
+ << footage.size() << "footage item(s)";
+ for (Footage *item : footage) {
+ const VideoParams video = item->get_first_enabled_video_stream();
+ if (!video.is_valid()) {
+ qWarning()
+ << "GenerateProxiesForSelectedFootage: skipping item with no valid video stream"
+ << item->filename();
+ continue;
+ }
+
+ // Queue one facade-backed task per footage item (same queueing
+ // semantics as the old per-footage proxy tasks).
+ oakengine_task_manager_add(
+ reinterpret_cast(new FacadeProxyTask(item)));
+ }
+}
+
+void ProjectExplorer::set_selected_footage_proxy_enabled(bool enabled)
+{
+ const QVector footage =
+ get_selected_proxy_footage(context_menu_items_);
+ qDebug() << "ProjectExplorer::SetSelectedFootageProxyEnabled:" << enabled
+ << "footage count=" << footage.size();
+ for (Footage *item : footage) {
+ if (item->proxy_path().isEmpty()) {
+ qDebug()
+ << " skipping item with empty proxy path" << item->filename();
+ continue;
+ }
+
+ OakEngineFootage *handle = oakengine_footage_borrow(
+ reinterpret_cast(item));
+ oakengine_footage_proxy_set_enabled(handle, enabled ? 1 : 0);
+ oakengine_footage_free(handle);
+ // The facade call toggles the flag; cache invalidation for the UI
+ // stays here.
+ item->invalidate_all(Footage::k_filename_input);
+ }
+}
+
+void ProjectExplorer::reveal_proxy_for_selected_footage()
+{
+ const QVector footage =
+ get_selected_proxy_footage(context_menu_items_);
+ for (Footage *item : footage) {
+ char proxy_path[4096];
+ proxy_path[0] = '\0';
+ OakEngineFootage *handle = oakengine_footage_borrow(
+ reinterpret_cast(item));
+ oakengine_footage_proxy_get_path(handle, proxy_path,
+ sizeof(proxy_path));
+ oakengine_footage_free(handle);
+ if (proxy_path[0] == '\0') {
+ continue;
+ }
+ const QString path = QString::fromUtf8(proxy_path);
+
+#if defined(Q_OS_WINDOWS)
+ QStringList args;
+ args << "/select," << QDir::toNativeSeparators(path);
+ QProcess::startDetached(QStringLiteral("explorer"), args);
+#elif defined(Q_OS_MAC)
+ QStringList args;
+ args << "-e";
+ args << "tell application \"Finder\"";
+ args << "-e";
+ args << "activate";
+ args << "-e";
+ args << "select POSIX file \"" + path + "\"";
+ args << "-e";
+ args << "end tell";
+ QProcess::startDetached(QStringLiteral("osascript"), args);
+#else
+ QDesktopServices::openUrl(QUrl::fromLocalFile(
+ QFileInfo(path).dir().absolutePath()));
+#endif
+ }
+}
+
+void ProjectExplorer::delete_proxies_for_selected_footage()
+{
+ const QVector footage =
+ get_selected_proxy_footage(context_menu_items_);
+ for (Footage *item : footage) {
+ if (item->proxy_path().isEmpty()) {
+ continue;
+ }
+
+ // Facade delete: removes the file, clears the proxy state and
+ // invalidates the footage.
+ OakEngineFootage *handle = oakengine_footage_borrow(
+ reinterpret_cast(item));
+ oakengine_footage_proxy_delete(handle);
+ oakengine_footage_free(handle);
+ }
+}
+
+void ProjectExplorer::show_proxy_dialog_for_selected_footage()
+{
+ ProxyDialog d(this, get_selected_proxy_footage(context_menu_items_));
+ d.exec();
+}
+
+void ProjectExplorer::view_selection_changed()
+{
+ QItemSelectionModel *model = static_cast(sender());
+
+ QModelIndexList selection = model->selectedIndexes();
+
+ QVector nodes;
+
+ foreach (const QModelIndex &index, selection) {
+ Node *sel = static_cast(
+ sort_model_.mapToSource(index).internalPointer());
+ if (!nodes.contains(sel)) {
+ nodes.append(sel);
+ }
+ }
+
+ if (nodes.isEmpty()) {
+ nodes.append(get_root());
+ }
+
+ emit selection_changed(nodes);
+}
+
+Project *ProjectExplorer::project() const
+{
+ return model_.project();
+}
+
+void ProjectExplorer::set_project(Project *p)
+{
+ model_.set_project(p);
+}
+
+Folder *ProjectExplorer::get_root() const
+{
+ QModelIndex root_index = sort_model_.mapToSource(tree_view_->rootIndex());
+
+ if (!root_index.isValid()) {
+ return project()->root();
+ }
+
+ return static_cast(root_index.internalPointer());
+}
+
+void ProjectExplorer::set_root(Folder *item)
+{
+ QModelIndex index =
+ sort_model_.mapFromSource(model_.create_index_from_item(item));
+
+ browse_to_folder(index);
+ tree_view_->setRootIndex(index);
+}
+
+QVector ProjectExplorer::selected_items() const
+{
+ // Determine which view is active and get its selected indexes
+ QModelIndexList index_list =
+ current_view()->selectionModel()->selectedRows();
+
+ // Convert indexes to item objects
+ QVector selected_items;
+
+ for (int i = 0; i < index_list.size(); i++) {
+ QModelIndex index = sort_model_.mapToSource(index_list.at(i));
+
+ Node *item = static_cast(index.internalPointer());
+
+ selected_items.append(item);
+ }
+
+ return selected_items;
+}
+
+Folder *ProjectExplorer::get_selected_folder() const
+{
+ if (project() == nullptr) {
+ return nullptr;
+ }
+
+ Folder *folder = nullptr;
+
+ // Get the selected items from the panel
+ QVector selected_nodes = selected_items();
+
+ // Heuristic for finding the selected folder:
+ //
+ // - If `folder` is nullptr, we set the first folder we find. Either the item itself if it's a folder, or the
+ // item's parent.
+ // - Otherwise, if all folders found are the same, we'll use that to import into.
+ // - If more than one folder is found, we play it safe and import into the root folder
+
+ for (int i = 0; i < selected_nodes.size(); i++) {
+ Node *sel_item = selected_nodes.at(i);
+
+ // If this item is not a folder, presumably it's parent is
+ if (!dynamic_cast(sel_item)) {
+ sel_item = sel_item->folder();
+ }
+
+ if (folder == nullptr) {
+ // If the folder is nullptr, cache it as this folder
+ folder = static_cast(sel_item);
+ } else if (folder != sel_item) {
+ // If not, we've already cached a folder so we check if it's the same
+ // If it isn't, we "play it safe" and use the root folder
+ folder = nullptr;
+ break;
+ }
+ }
+
+ // If we didn't pick up a folder from the heuristic above for whatever reason, use root
+ if (folder == nullptr) {
+ folder = project()->root();
+ }
+
+ return folder;
+}
+
+ProjectViewModel *ProjectExplorer::model()
+{
+ return &model_;
+}
+
+void ProjectExplorer::select_all()
+{
+ current_view()->selectAll();
+}
+
+void ProjectExplorer::deselect_all()
+{
+ current_view()->selectionModel()->clearSelection();
+}
+
+void ProjectExplorer::delete_selected()
+{
+ QVector selected = selected_items();
+
+ if (selected.isEmpty()) {
+ return;
+ }
+
+ MultiUndoCommand *command = new MultiUndoCommand();
+
+ bool check_if_item_is_in_use = true;
+
+ if (delete_items_internal(selected, check_if_item_is_in_use, command)) {
+ Core::instance()->undo_stack()->push(
+ command, tr("Deleted %1 Item(s)").arg(selected.size()));
+ } else {
+ delete command;
+ }
+}
+
+bool ProjectExplorer::select_item(Node *n, bool deselect_all_first)
+{
+ if (deselect_all_first) {
+ deselect_all();
+ }
+
+ QModelIndex index = model_.create_index_from_item(n);
+
+ if (index.isValid()) {
+ index = sort_model_.mapFromSource(index);
+
+ QModelIndex parent = index.parent();
+ if (view_type() == ProjectToolbar::tree_view) {
+ // Expand all folders until this index is visible
+ while (parent.isValid()) {
+ tree_view_->expand(parent);
+ parent = parent.parent();
+ }
+ } else {
+ browse_to_folder(parent);
+ }
+
+ current_view()->selectionModel()->select(
+ index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
+
+ return true;
+ }
+
+ return false;
+}
+
+}
diff --git a/.bak/projectexplorer/projectexplorer.h b/.bak/projectexplorer/projectexplorer.h
new file mode 100644
index 000000000..4f79ceaf2
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorer.h
@@ -0,0 +1,208 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_PROJECTEXPLORER_H
+#define OAK_PROJECTEXPLORER_H
+
+#include
+#include
+#include
+#include
+
+#include "node/project.h"
+#include "projectviewmodel.h"
+#include "widget/projectexplorer/projectexplorericonview.h"
+#include "widget/projectexplorer/projectexplorerlistview.h"
+#include "widget/projectexplorer/projectexplorertreeview.h"
+#include "widget/projectexplorer/projectexplorernavigation.h"
+#include "widget/projecttoolbar/projecttoolbar.h"
+
+namespace olive
+{
+
+/**
+ * @brief A widget for browsing through a Project structure.
+ *
+ * ProjectExplorer automatically handles the view<->model system using a ProjectViewModel. Therefore, all that needs to
+ * be provided is the Project structure itself.
+ *
+ * This widget contains three views, tree view, list view, and icon view. These can be switched at any time.
+ */
+class ProjectExplorer : public QWidget {
+ Q_OBJECT
+public:
+ ProjectExplorer(QWidget *parent);
+
+ const ProjectToolbar::ViewType &view_type() const;
+
+ Project *project() const;
+ void set_project(Project *p);
+
+ Folder *get_root() const;
+ void set_root(Folder *item);
+
+ QVector selected_items() const;
+
+ /**
+ * @brief Use a heuristic to determine which (if any) folder is selected
+ *
+ * Generally for some import/adding processes, we assume that if a folder is selected, the user probably wants to
+ * create the new object in it rather than in the root. If, however, more than one folder is selected, we can't
+ * truly determine any folder from this and just return the root instead.
+ *
+ * @return
+ *
+ * A folder that's heuristically been determined as "selected", or the root directory if none, or nullptr if no
+ * project is open.
+ */
+ Folder *get_selected_folder() const;
+
+ /**
+ * @brief Access the ViewModel model of the project
+ */
+ ProjectViewModel *model();
+
+ void select_all();
+
+ void deselect_all();
+
+ void delete_selected();
+
+ bool select_item(Node *n, bool deselect_all_first = true);
+
+public slots:
+ void set_view_type(ProjectToolbar::ViewType type);
+
+ void edit(Node *item);
+
+ void rename_selected_item();
+
+ void set_search_filter(const QString &s);
+
+signals:
+ /**
+ * @brief Emitted when an Item is double clicked
+ *
+ * @param item
+ *
+ * The Item that was double clicked, or nullptr if empty area was double clicked
+ */
+ void double_clicked_item(Node *item);
+
+ void selection_changed(const QVector &selected);
+
+private:
+ /**
+ * @brief Get all the blocks that solely rely on an input node
+ *
+ * Ignores blocks that depend on multiple inputs
+ */
+ QList get_footage_blocks(QList nodes);
+
+ /**
+ * @brief Simple convenience function for adding a view to this stacked widget
+ *
+ * Mainly for use in the constructor. Adds the view, connects its signals/slots, and sets the model.
+ *
+ * @param view
+ *
+ * View to add to the stack
+ */
+ void add_view(QAbstractItemView *view);
+
+ /**
+ * @brief Browse to a specific folder index in the model
+ *
+ * Only affects list_view_ and icon_view_.
+ *
+ * @param index
+ *
+ * Either an invalid index to return to the project root, or an index to a valid Folder object.
+ */
+ void browse_to_folder(const QModelIndex &index);
+
+ int confirm_item_deletion(Node *item);
+
+ bool delete_items_internal(const QVector &selected,
+ bool &check_if_item_is_in_use,
+ MultiUndoCommand *command);
+
+ static QString get_human_readable_node_name(Node *node);
+
+ void update_nav_bar_text();
+
+ /**
+ * @brief Get the currently active QAbstractItemView
+ */
+ QAbstractItemView *current_view() const;
+
+ QStackedWidget *stacked_widget_;
+
+ ProjectExplorerNavigation *nav_bar_;
+
+ ProjectExplorerIconView *icon_view_;
+ ProjectExplorerListView *list_view_;
+ ProjectExplorerTreeView *tree_view_;
+
+ ProjectToolbar::ViewType view_type_;
+
+ QSortFilterProxyModel sort_model_;
+ ProjectViewModel model_;
+
+ QVector context_menu_items_;
+
+private slots:
+ void view_empty_area_double_clicked_slot();
+
+ void item_double_clicked_slot(const QModelIndex &index);
+
+ void size_changed_slot(int s);
+
+ void dir_up_slot();
+
+ void show_context_menu();
+
+ void show_item_properties_dialog();
+
+ void reveal_selected_footage();
+
+ void replace_selected_footage();
+
+ void open_context_menu_item_in_new_tab();
+
+ void open_context_menu_item_in_new_window();
+
+ void generate_proxies_for_selected_footage();
+
+ void set_selected_footage_proxy_enabled(bool enabled);
+
+ void reveal_proxy_for_selected_footage();
+
+ void delete_proxies_for_selected_footage();
+
+ void show_proxy_dialog_for_selected_footage();
+
+ void view_selection_changed();
+};
+
+}
+
+#endif // OAK_PROJECTEXPLORER_H
diff --git a/.bak/projectexplorer/projectexplorericonview.cpp b/.bak/projectexplorer/projectexplorericonview.cpp
new file mode 100644
index 000000000..8bf040870
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorericonview.cpp
@@ -0,0 +1,35 @@
+/***
+
+ 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 "projectexplorericonview.h"
+
+namespace olive
+{
+
+ProjectExplorerIconView::ProjectExplorerIconView(QWidget *parent)
+ : ProjectExplorerListViewBase(parent)
+{
+ setViewMode(QListView::IconMode);
+
+ setItemDelegate(&delegate_);
+}
+
+}
diff --git a/.bak/projectexplorer/projectexplorericonview.h b/.bak/projectexplorer/projectexplorericonview.h
new file mode 100644
index 000000000..ce5871c78
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorericonview.h
@@ -0,0 +1,45 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_PROJECTEXPLORERICONVIEW_H
+#define OAK_PROJECTEXPLORERICONVIEW_H
+
+#include "projectexplorerlistviewbase.h"
+#include "projectexplorericonviewitemdelegate.h"
+
+namespace olive
+{
+
+/**
+ * @brief The view widget used when ProjectExplorer is in Icon View
+ */
+class ProjectExplorerIconView : public ProjectExplorerListViewBase {
+ Q_OBJECT
+public:
+ ProjectExplorerIconView(QWidget *parent);
+
+private:
+ ProjectExplorerIconViewItemDelegate delegate_;
+};
+
+}
+
+#endif // OAK_PROJECTEXPLORERICONVIEW_H
diff --git a/.bak/projectexplorer/projectexplorericonviewitemdelegate.cpp b/.bak/projectexplorer/projectexplorericonviewitemdelegate.cpp
new file mode 100644
index 000000000..692ddf4ce
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorericonviewitemdelegate.cpp
@@ -0,0 +1,110 @@
+/***
+
+ 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 "projectexplorericonviewitemdelegate.h"
+
+#include
+
+#include "common/qtutils.h"
+
+namespace olive
+{
+
+ProjectExplorerIconViewItemDelegate::ProjectExplorerIconViewItemDelegate(
+ QObject *parent)
+ : QStyledItemDelegate(parent)
+{
+}
+
+QSize ProjectExplorerIconViewItemDelegate::sizeHint(
+ const QStyleOptionViewItem &option, const QModelIndex &) const
+{
+ Q_UNUSED(option)
+
+ return QSize(256, 256);
+}
+
+void ProjectExplorerIconViewItemDelegate::paint(
+ QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const
+{
+ QFontMetrics fm = painter->fontMetrics();
+ QRect img_rect = option.rect;
+
+ // Draw Text
+ if (fm.height() < option.rect.height() / 2) {
+ img_rect.setHeight(img_rect.height() - fm.height());
+
+ QRect text_rect = option.rect;
+ text_rect.setTop(text_rect.top() + option.rect.height() - fm.height());
+
+ QColor text_bgcolor;
+ QColor text_fgcolor;
+
+ if (option.state & QStyle::State_Selected) {
+ text_bgcolor = option.palette.highlight().color();
+ text_fgcolor = option.palette.highlightedText().color();
+ } else {
+ text_bgcolor = Qt::white;
+ text_fgcolor = Qt::black;
+ }
+
+ painter->fillRect(text_rect, text_bgcolor);
+ painter->setPen(text_fgcolor);
+
+ QString duration_str = index.data(Qt::UserRole).toString();
+
+ int timecode_width = QtUtils::q_font_metrics_width(fm, duration_str);
+
+ int max_name_width = option.rect.width();
+
+ if (timecode_width < option.rect.width() / 2) {
+ painter->drawText(
+ text_rect, static_cast(Qt::AlignBottom | Qt::AlignRight),
+ index.data(Qt::UserRole).toString());
+ max_name_width -= timecode_width;
+ }
+
+ painter->drawText(text_rect,
+ static_cast(Qt::AlignBottom | Qt::AlignLeft),
+ fm.elidedText(index.data(Qt::DisplayRole).toString(),
+ Qt::ElideRight, max_name_width));
+ }
+
+ // Draw image
+ QIcon ico = index.data(Qt::DecorationRole).value();
+ QSize icon_size = ico.actualSize(img_rect.size());
+ img_rect =
+ QRect(img_rect.x() + (img_rect.width() / 2 - icon_size.width() / 2),
+ img_rect.y() + (img_rect.height() / 2 - icon_size.height() / 2),
+ icon_size.width(), icon_size.height());
+ painter->drawPixmap(img_rect, ico.pixmap(icon_size));
+
+ if (option.state & QStyle::State_Selected) {
+ QColor highlight_color = option.palette.highlight().color();
+ highlight_color.setAlphaF(0.5);
+
+ painter->setCompositionMode(QPainter::CompositionMode_SourceAtop);
+ painter->fillRect(img_rect, highlight_color);
+ }
+}
+
+}
diff --git a/.bak/projectexplorer/projectexplorericonviewitemdelegate.h b/.bak/projectexplorer/projectexplorericonviewitemdelegate.h
new file mode 100644
index 000000000..d7c2d01e3
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorericonviewitemdelegate.h
@@ -0,0 +1,47 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_PROJECTEXPLORERICONVIEWITEMDELEGATE_H
+#define OAK_PROJECTEXPLORERICONVIEWITEMDELEGATE_H
+
+#include
+
+#include "common/define.h"
+
+namespace olive
+{
+
+/**
+ * @brief The delegate that's used to draw items when ProjectExplorer is in Icon view
+ */
+class ProjectExplorerIconViewItemDelegate : public QStyledItemDelegate {
+public:
+ ProjectExplorerIconViewItemDelegate(QObject *parent = nullptr);
+
+ virtual QSize sizeHint(const QStyleOptionViewItem &option,
+ const QModelIndex &index) const override;
+ virtual void paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const override;
+};
+
+}
+
+#endif // OAK_PROJECTEXPLORERICONVIEWITEMDELEGATE_H
diff --git a/.bak/projectexplorer/projectexplorerlistview.cpp b/.bak/projectexplorer/projectexplorerlistview.cpp
new file mode 100644
index 000000000..b8574a1e5
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorerlistview.cpp
@@ -0,0 +1,35 @@
+/***
+
+ 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 "projectexplorerlistview.h"
+
+namespace olive
+{
+
+ProjectExplorerListView::ProjectExplorerListView(QWidget *parent)
+ : ProjectExplorerListViewBase(parent)
+{
+ setViewMode(QListView::ListMode);
+
+ setItemDelegate(&delegate_);
+}
+
+}
diff --git a/.bak/projectexplorer/projectexplorerlistview.h b/.bak/projectexplorer/projectexplorerlistview.h
new file mode 100644
index 000000000..2a34429f8
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorerlistview.h
@@ -0,0 +1,45 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_PROJECTEXPLORERLISTVIEW_H
+#define OAK_PROJECTEXPLORERLISTVIEW_H
+
+#include "projectexplorerlistviewbase.h"
+#include "projectexplorerlistviewitemdelegate.h"
+
+namespace olive
+{
+
+/**
+ * @brief The view widget used when ProjectExplorer is in List View
+ */
+class ProjectExplorerListView : public ProjectExplorerListViewBase {
+ Q_OBJECT
+public:
+ ProjectExplorerListView(QWidget *parent);
+
+private:
+ ProjectExplorerListViewItemDelegate delegate_;
+};
+
+}
+
+#endif // OAK_PROJECTEXPLORERLISTVIEW_H
diff --git a/.bak/projectexplorer/projectexplorerlistviewbase.cpp b/.bak/projectexplorer/projectexplorerlistviewbase.cpp
new file mode 100644
index 000000000..848528713
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorerlistviewbase.cpp
@@ -0,0 +1,59 @@
+/***
+
+ 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 "projectexplorerlistviewbase.h"
+
+#include
+
+namespace olive
+{
+
+ProjectExplorerListViewBase::ProjectExplorerListViewBase(QWidget *parent)
+ : QListView(parent)
+{
+ // FIXME Is this necessary?
+ setMovement(QListView::Free);
+
+ // Set selection mode (allows multiple item selection)
+ setSelectionMode(QAbstractItemView::ExtendedSelection);
+
+ // Set resize mode
+ setResizeMode(QListView::Adjust);
+
+ // Set widget to emit a signal on right click
+ setContextMenuPolicy(Qt::CustomContextMenu);
+}
+
+void ProjectExplorerListViewBase::mouseDoubleClickEvent(QMouseEvent *event)
+{
+ // Cache here so if the index becomes invalid after the base call, we still know the truth
+ bool item_at_location = indexAt(event->pos()).isValid();
+
+ // Perform default double click functions
+ QListView::mouseDoubleClickEvent(event);
+
+ // QAbstractItemView already has a doubleClicked() signal, but we emit another here for double clicking empty space
+ if (!item_at_location) {
+ emit double_clicked_empty_area();
+ }
+}
+
+}
diff --git a/.bak/projectexplorer/projectexplorerlistviewbase.h b/.bak/projectexplorer/projectexplorerlistviewbase.h
new file mode 100644
index 000000000..2169b573a
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorerlistviewbase.h
@@ -0,0 +1,63 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_PROJECTEXPLORERLISTVIEWBASE_H
+#define OAK_PROJECTEXPLORERLISTVIEWBASE_H
+
+#include
+
+#include "common/define.h"
+
+namespace olive
+{
+
+/**
+ * @brief A QListView derivative that contains functionality used by both List view and Icon view (which are both based
+ * on QListView)
+ */
+class ProjectExplorerListViewBase : public QListView {
+ Q_OBJECT
+public:
+ ProjectExplorerListViewBase(QWidget *parent);
+
+protected:
+ /**
+ * @brief Double click event override
+ *
+ * Function that signals DoubleClickedView().
+ *
+ * FIXME: This code is the same as the code in ProjectExplorerTreeView. Is there a way to merge these two through
+ * subclassing?
+ */
+ virtual void mouseDoubleClickEvent(QMouseEvent *event) override;
+
+signals:
+ /**
+ * @brief Unconditional double click signal
+ *
+ * Emits a signal when the view is double clicked but not on any particular item
+ */
+ void double_clicked_empty_area();
+};
+
+}
+
+#endif // OAK_PROJECTEXPLORERLISTVIEWBASE_H
diff --git a/.bak/projectexplorer/projectexplorerlistviewitemdelegate.cpp b/.bak/projectexplorer/projectexplorerlistviewitemdelegate.cpp
new file mode 100644
index 000000000..fa2d243a0
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorerlistviewitemdelegate.cpp
@@ -0,0 +1,91 @@
+/***
+
+ 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 "projectexplorerlistviewitemdelegate.h"
+
+#include
+
+namespace olive
+{
+
+ProjectExplorerListViewItemDelegate::ProjectExplorerListViewItemDelegate(
+ QObject *parent)
+ : QStyledItemDelegate(parent)
+{
+}
+
+QSize ProjectExplorerListViewItemDelegate::sizeHint(
+ const QStyleOptionViewItem &option, const QModelIndex &) const
+{
+ return QSize(option.decorationSize.height(),
+ option.decorationSize.height());
+}
+
+void ProjectExplorerListViewItemDelegate::paint(
+ QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const
+{
+ QFontMetrics fm = painter->fontMetrics();
+ QRect img_rect = option.rect;
+
+ if (option.state & QStyle::State_Selected) {
+ painter->fillRect(option.rect, option.palette.highlight());
+ }
+
+ img_rect.setWidth(qMin(img_rect.width(), img_rect.height()));
+
+ QIcon ico = index.data(Qt::DecorationRole).value();
+ QSize icon_size = ico.actualSize(img_rect.size());
+ img_rect =
+ QRect(img_rect.x() + (img_rect.width() / 2 - icon_size.width() / 2),
+ img_rect.y() + (img_rect.height() / 2 - icon_size.height() / 2),
+ icon_size.width(), icon_size.height());
+ painter->drawPixmap(img_rect, ico.pixmap(icon_size));
+
+ QRect text_rect = option.rect;
+ text_rect.setLeft(text_rect.left() + option.rect.height());
+
+ int maximum_line_count = qMax(1, option.rect.height() / fm.height() - 1);
+ QString text;
+ if (maximum_line_count == 1) {
+ text = index.data(Qt::DisplayRole).toString();
+ } else {
+ text = index.data(Qt::ToolTipRole).toString();
+ if (text.isEmpty()) {
+ text = index.data(Qt::DisplayRole).toString();
+ } else {
+ QStringList strings = text.split("\n");
+ while (strings.size() > maximum_line_count) {
+ strings.removeLast();
+ }
+ text = strings.join("\n");
+ }
+ }
+
+ painter->setPen(option.state & QStyle::State_Selected ?
+ option.palette.highlightedText().color() :
+ option.palette.text().color());
+
+ painter->drawText(text_rect,
+ static_cast(Qt::AlignLeft | Qt::AlignVCenter), text);
+}
+
+}
diff --git a/.bak/projectexplorer/projectexplorerlistviewitemdelegate.h b/.bak/projectexplorer/projectexplorerlistviewitemdelegate.h
new file mode 100644
index 000000000..7297fdf33
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorerlistviewitemdelegate.h
@@ -0,0 +1,47 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_PROJECTEXPLORERLISTVIEWITEMDELEGATE_H
+#define OAK_PROJECTEXPLORERLISTVIEWITEMDELEGATE_H
+
+#include
+
+#include "common/define.h"
+
+namespace olive
+{
+
+/**
+ * @brief The delegate that's used to draw items when ProjectExplorer is in List view
+ */
+class ProjectExplorerListViewItemDelegate : public QStyledItemDelegate {
+public:
+ ProjectExplorerListViewItemDelegate(QObject *parent = nullptr);
+
+ virtual QSize sizeHint(const QStyleOptionViewItem &option,
+ const QModelIndex &index) const override;
+ virtual void paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const override;
+};
+
+}
+
+#endif // OAK_PROJECTEXPLORERLISTVIEWITEMDELEGATE_H
diff --git a/.bak/projectexplorer/projectexplorernavigation.cpp b/.bak/projectexplorer/projectexplorernavigation.cpp
new file mode 100644
index 000000000..ea7b90d7f
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorernavigation.cpp
@@ -0,0 +1,103 @@
+/***
+
+ 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 "projectexplorernavigation.h"
+
+#include
+#include
+
+#include "common/define.h"
+#include "ui/icons/icons.h"
+
+namespace olive
+{
+
+ProjectExplorerNavigation::ProjectExplorerNavigation(QWidget *parent)
+ : QWidget(parent)
+{
+ // Create widget layout
+ QHBoxLayout *layout = new QHBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+
+ // Create "directory up" button
+ dir_up_btn_ = new QPushButton(this);
+ dir_up_btn_->setEnabled(false);
+ dir_up_btn_->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
+ layout->addWidget(dir_up_btn_);
+ connect(dir_up_btn_, SIGNAL(clicked(bool)), this,
+ SIGNAL(directory_up_clicked()));
+
+ // Create directory tree label
+ dir_lbl_ = new QLabel(this);
+ dir_lbl_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
+ layout->addWidget(dir_lbl_);
+
+ // Create size slider
+ size_slider_ = new QSlider(this);
+ size_slider_->setOrientation(Qt::Horizontal);
+ size_slider_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
+ layout->addWidget(size_slider_);
+ connect(size_slider_, SIGNAL(valueChanged(int)), this,
+ SIGNAL(size_changed(int)));
+
+ retranslate();
+ update_icons();
+}
+
+void ProjectExplorerNavigation::set_text(const QString &s)
+{
+ dir_lbl_->setText(s);
+}
+
+void ProjectExplorerNavigation::set_dir_up_enabled(bool e)
+{
+ dir_up_btn_->setEnabled(e);
+}
+
+void ProjectExplorerNavigation::set_size_value(int s)
+{
+ size_slider_->setValue(s);
+}
+
+void ProjectExplorerNavigation::changeEvent(QEvent *e)
+{
+ if (e->type() == QEvent::LanguageChange) {
+ retranslate();
+ } else if (e->type() == QEvent::StyleChange) {
+ update_icons();
+ }
+ QWidget::changeEvent(e);
+}
+
+void ProjectExplorerNavigation::retranslate()
+{
+ dir_up_btn_->setToolTip(tr("Go to parent folder"));
+}
+
+void ProjectExplorerNavigation::update_icons()
+{
+ dir_up_btn_->setIcon(icon::dir_up);
+ size_slider_->setMinimum(k_project_icon_size_minimum);
+ size_slider_->setMaximum(k_project_icon_size_maximum);
+ size_slider_->setValue(k_project_icon_size_default);
+}
+
+}
diff --git a/.bak/projectexplorer/projectexplorernavigation.h b/.bak/projectexplorer/projectexplorernavigation.h
new file mode 100644
index 000000000..4bf12fa30
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorernavigation.h
@@ -0,0 +1,118 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_PROJECTEXPLORERLISTVIEWTOOLBAR_H
+#define OAK_PROJECTEXPLORERLISTVIEWTOOLBAR_H
+
+#include
+#include
+#include
+#include
+
+#include "common/define.h"
+
+namespace olive
+{
+
+/**
+ * @brief A navigation bar widget for ProjectExplorer's Icon and List views
+ *
+ * Unlike the Tree view, Icon and List don't follow a hierarchical view of information. This means there is no direct
+ * way of navigating in and out of folders in those view types. We solve this in two ways:
+ *
+ * * Double clicking a Folder in those views will enter that folder
+ * * This navigation bar offers a "directory up" button for leaving a folder
+ *
+ * This navbar also provides an icon size slider for those views (between kProjectIconSizeMinimum and
+ * kProjectIconSizeMaximum) as well as text that's intended to be set to the current Folder's name (or
+ * empty for the root folder).
+ *
+ * This widget does not actually communicate to Project or ProjectExplorer classes. It is simply UI widgets that are
+ * intended to be connected in ways that do. This is the primarily responsibility of ProjectExplorer.
+ *
+ * By default, the directory up button is disabled (assuming root folder), the text is empty, and the icon size slider
+ * is set to kProjectIconSizeDefault.
+ */
+class ProjectExplorerNavigation : public QWidget {
+ Q_OBJECT
+public:
+ ProjectExplorerNavigation(QWidget *parent);
+
+ /**
+ * @brief Sets the text string
+ *
+ * This text is intended to be set to the current Folder's name
+ *
+ * @param s
+ */
+ void set_text(const QString &s);
+
+ /**
+ * @brief Set whether the "directory up" button is enabled or not
+ *
+ * @param e
+ */
+ void set_dir_up_enabled(bool e);
+
+ /**
+ * @brief Set the current value of the size slider
+ *
+ * NOTE: Does NOT emit SizeChanged().
+ *
+ * @param s
+ *
+ * New size value to set to
+ */
+ void set_size_value(int s);
+
+signals:
+ /**
+ * @brief Signal emitted when the directory up button is clicked
+ */
+ void directory_up_clicked();
+
+ /**
+ * @brief Signal emitted when the icon size slider changes value
+ *
+ * @param size
+ *
+ * New size set in the slider
+ */
+ void size_changed(int size);
+
+protected:
+ virtual void changeEvent(QEvent *) override;
+
+private:
+ void retranslate();
+
+ void update_icons();
+
+ QPushButton *dir_up_btn_;
+
+ QLabel *dir_lbl_;
+
+ QSlider *size_slider_;
+};
+
+}
+
+#endif // OAK_PROJECTEXPLORERLISTVIEWTOOLBAR_H
diff --git a/.bak/projectexplorer/projectexplorertreeview.cpp b/.bak/projectexplorer/projectexplorertreeview.cpp
new file mode 100644
index 000000000..76ae834dc
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorertreeview.cpp
@@ -0,0 +1,59 @@
+/***
+
+ 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 "projectexplorertreeview.h"
+
+#include
+
+namespace olive
+{
+
+ProjectExplorerTreeView::ProjectExplorerTreeView(QWidget *parent)
+ : QTreeView(parent)
+{
+ // Set selection mode (allows multiple item selection)
+ setSelectionMode(QAbstractItemView::ExtendedSelection);
+
+ // Allow dragging and dropping
+ setDragDropMode(QAbstractItemView::DragDrop);
+
+ // Enable dragging
+ setDragEnabled(true);
+
+ // Allow dropping from external sources
+ setAcceptDrops(true);
+
+ // Set context menu to emit a signal
+ setContextMenuPolicy(Qt::CustomContextMenu);
+}
+
+void ProjectExplorerTreeView::mouseDoubleClickEvent(QMouseEvent *event)
+{
+ // Perform default double click functions
+ QTreeView::mouseDoubleClickEvent(event);
+
+ // QAbstractItemView already has a doubleClicked() signal, but we emit another here for double clicking empty space
+ if (!indexAt(event->pos()).isValid()) {
+ emit double_clicked_empty_area();
+ }
+}
+
+}
diff --git a/.bak/projectexplorer/projectexplorertreeview.h b/.bak/projectexplorer/projectexplorertreeview.h
new file mode 100644
index 000000000..a334c36d5
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorertreeview.h
@@ -0,0 +1,65 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_PROJECTEXPLORERTREEVIEW_H
+#define OAK_PROJECTEXPLORERTREEVIEW_H
+
+#include
+
+#include "common/define.h"
+
+namespace olive
+{
+
+/**
+ * @brief The view widget used when ProjectExplorer is in Tree View
+ *
+ * A fairly simple subclass of QTreeView that provides a double clicked signal whether the index is valid or not
+ * (QAbstractItemView has a doubleClicked() signal but it's only emitted with a valid index).
+ */
+class ProjectExplorerTreeView : public QTreeView {
+ Q_OBJECT
+public:
+ ProjectExplorerTreeView(QWidget *parent);
+
+protected:
+ /**
+ * @brief Double click event override
+ *
+ * Function that signals DoubleClickedView().
+ *
+ * FIXME: This code is the same as the code in ProjectExplorerListViewBase. Is there a way to merge these two through
+ *
+ */
+ virtual void mouseDoubleClickEvent(QMouseEvent *event) override;
+
+signals:
+ /**
+ * @brief Unconditional double click signal
+ *
+ * Emits a signal when the view is double clicked but not on any particular item
+ */
+ void double_clicked_empty_area();
+};
+
+}
+
+#endif // OAK_PROJECTEXPLORERTREEVIEW_H
diff --git a/.bak/projectexplorer/projectexplorerundo.h b/.bak/projectexplorer/projectexplorerundo.h
new file mode 100644
index 000000000..0dc337a2d
--- /dev/null
+++ b/.bak/projectexplorer/projectexplorerundo.h
@@ -0,0 +1,32 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_PROJECTEXPLORERUNDO_H
+#define OAK_PROJECTEXPLORERUNDO_H
+
+#include "undo/undocommand.h"
+
+namespace olive
+{
+
+}
+
+#endif // OAK_PROJECTEXPLORERUNDO_H
diff --git a/.bak/projectexplorer/projectviewmodel.cpp b/.bak/projectexplorer/projectviewmodel.cpp
new file mode 100644
index 000000000..3b6e6c19e
--- /dev/null
+++ b/.bak/projectexplorer/projectviewmodel.cpp
@@ -0,0 +1,573 @@
+/***
+
+ 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 "projectviewmodel.h"
+#include "ui/icons/icons.h"
+
+#include
+#include
+#include
+
+#include "common/qtutils.h"
+#include "core.h"
+#include "node/nodeundo.h"
+
+namespace olive
+{
+
+ProjectViewModel::ProjectViewModel(QObject *parent)
+ : QAbstractItemModel(parent)
+ , project_(nullptr)
+{
+}
+
+Project *ProjectViewModel::project() const
+{
+ return project_;
+}
+
+void ProjectViewModel::set_project(Project *p)
+{
+ beginResetModel();
+
+ if (project_) {
+ disconnect_item(project_->root());
+ }
+
+ project_ = p;
+
+ if (project_) {
+ connect_item(project_->root());
+ }
+
+ endResetModel();
+}
+
+QModelIndex ProjectViewModel::index(int row, int column,
+ const QModelIndex &parent) const
+{
+ // I'm actually not 100% sure what this does, but it seems logical and was in the earlier code
+ if (!hasIndex(row, column, parent)) {
+ return QModelIndex();
+ }
+
+ // Get the parent object, we assume it's a folder since only folders can have children
+ Folder *item_parent = static_cast(get_item_object_from_index(parent));
+
+ // Return an index to this object
+ return createIndex(row, column, item_parent->item_child(row));
+}
+
+QModelIndex ProjectViewModel::parent(const QModelIndex &child) const
+{
+ // Get the Item object from the index
+ Node *item = get_item_object_from_index(child);
+
+ // Get Item's parent object
+ Folder *par = item->folder();
+
+ // If the parent is the root, return an empty index
+ if (par == project_->root()) {
+ return QModelIndex();
+ }
+
+ // Otherwise return a true index to its parent
+ int parent_index = index_of_child(par);
+
+ // Make sure the index is valid (there's no reason it shouldn't be)
+ Q_ASSERT(parent_index > -1);
+
+ // Return an index to the parent
+ return createIndex(parent_index, 0, par);
+}
+
+int ProjectViewModel::rowCount(const QModelIndex &parent) const
+{
+ // If there's no project, there are obviously no items to show
+ if (project_ == nullptr) {
+ return 0;
+ }
+
+ // If the index is the root, return the root child count
+ if (parent == QModelIndex()) {
+ return project_->root()->item_child_count();
+ }
+
+ // Otherwise, the index must contain a valid pointer, so we just return its child count
+ return static_cast(get_item_object_from_index(parent))
+ ->item_child_count();
+}
+
+int ProjectViewModel::columnCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent)
+
+ // Not strictly necessary, but a decent visual cue that there's no project currently active
+ if (project_ == nullptr) {
+ return 0;
+ }
+
+ return k_column_count;
+}
+
+QVariant ProjectViewModel::data(const QModelIndex &index, int role) const
+{
+ Node *internal_item = get_item_object_from_index(index);
+
+ ColumnType column_type = static_cast(index.column());
+
+ switch (role) {
+ case Qt::DisplayRole:
+ case k_inner_text_role: {
+ // Standard text role
+
+ switch (column_type) {
+ case k_name:
+ return internal_item->get_label();
+ case k_duration:
+ return internal_item->data(Node::duration);
+ case k_rate:
+ return internal_item->data(Node::frequency_rate);
+ case k_last_modified:
+ case k_created_time: {
+ qint64 using_time =
+ (column_type == k_last_modified) ?
+ internal_item->data(Node::modified_time).toLongLong() :
+ internal_item->data(Node::created_time).toLongLong();
+
+ if (using_time == 0) {
+ // 0 is the null value, return nothing
+ break;
+ }
+
+ QVariant ret;
+
+ if (role == k_inner_text_role) {
+ // Use time value directly for correct sorting
+ ret = using_time;
+ } else {
+ // Display role, format to a human readable string
+ ret = QtUtils::get_formatted_date_time(
+ QDateTime::fromSecsSinceEpoch(using_time));
+ }
+
+ return ret;
+ }
+ case k_column_count:
+ break;
+ }
+ } break;
+ case Qt::EditRole:
+ if (column_type == k_name) {
+ return internal_item->get_label();
+ }
+ break;
+ case Qt::DecorationRole:
+ // If this is the first column, return the Item's icon
+ if (column_type == k_name) {
+ return icon::from_name(internal_item->data(Node::icon).toString());
+ }
+ break;
+ case Qt::ToolTipRole:
+ return internal_item->data(Node::tooltip);
+ }
+
+ return QVariant();
+}
+
+QVariant ProjectViewModel::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ // Check if we need text data (DisplayRole) and orientation is horizontal
+ // FIXME I'm not 100% sure what happens if the orientation is vertical/if that check is necessary
+ if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
+ ColumnType column_type = static_cast(section);
+
+ // Return the name based on the column's current type
+ switch (column_type) {
+ case k_name:
+ return tr("Name");
+ case k_duration:
+ return tr("Duration");
+ case k_rate:
+ return tr("Rate");
+ case k_last_modified:
+ return tr("Modified");
+ case k_created_time:
+ return tr("Created");
+ case k_column_count:
+ break;
+ }
+ }
+
+ return QAbstractItemModel::headerData(section, orientation, role);
+}
+
+bool ProjectViewModel::hasChildren(const QModelIndex &parent) const
+{
+ // If it's a folder, we always return TRUE in order to always show the "expand triangle" icon,
+ // even when there are no "physical" children
+ Node *item = get_item_object_from_index(parent);
+
+ return dynamic_cast