From 96defacacb560044913db4511222cba67c465fee Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Sun, 26 Jul 2026 22:43:42 +0800 Subject: [PATCH] chore: recovery backups, temp files and misc campaign artifacts .bak LocalHistory recovery backups, .tmp0/.tmp1, .gitignore update, reasonix.toml, convert_videoparams.py. Kept as-is from the campaign branch; cleanup can happen separately on main. --- .bak/nodeview/nodeview.cpp | 1982 +++++++++++++++++ .bak/nodeview/nodeview.h | 301 +++ .bak/nodeview/nodeviewcommon.h | 76 + .bak/nodeview/nodeviewcontext.cpp | 412 ++++ .bak/nodeview/nodeviewcontext.h | 116 + .bak/nodeview/nodeviewedge.cpp | 260 +++ .bak/nodeview/nodeviewedge.h | 149 ++ .bak/nodeview/nodeviewitem.cpp | 907 ++++++++ .bak/nodeview/nodeviewitem.h | 246 ++ .bak/nodeview/nodeviewitemconnector.cpp | 102 + .bak/nodeview/nodeviewitemconnector.h | 52 + .bak/nodeview/nodeviewminimap.cpp | 161 ++ .bak/nodeview/nodeviewminimap.h | 78 + .bak/nodeview/nodeviewscene.cpp | 120 + .bak/nodeview/nodeviewscene.h | 87 + .bak/nodeview/nodeviewtoolbar.cpp | 76 + .bak/nodeview/nodeviewtoolbar.h | 59 + .bak/nodeview/nodewidget.cpp | 55 + .bak/nodeview/nodewidget.h | 57 + .bak/projectexplorer/projectexplorer.cpp | 947 ++++++++ .bak/projectexplorer/projectexplorer.h | 208 ++ .../projectexplorericonview.cpp | 35 + .../projectexplorer/projectexplorericonview.h | 45 + .../projectexplorericonviewitemdelegate.cpp | 110 + .../projectexplorericonviewitemdelegate.h | 47 + .../projectexplorerlistview.cpp | 35 + .../projectexplorer/projectexplorerlistview.h | 45 + .../projectexplorerlistviewbase.cpp | 59 + .../projectexplorerlistviewbase.h | 63 + .../projectexplorerlistviewitemdelegate.cpp | 91 + .../projectexplorerlistviewitemdelegate.h | 47 + .../projectexplorernavigation.cpp | 103 + .../projectexplorernavigation.h | 118 + .../projectexplorertreeview.cpp | 59 + .../projectexplorer/projectexplorertreeview.h | 65 + .bak/projectexplorer/projectexplorerundo.h | 32 + .bak/projectexplorer/projectviewmodel.cpp | 573 +++++ .bak/projectexplorer/projectviewmodel.h | 176 ++ .gitignore | 1 + .tmp0 | Bin 0 -> 608 bytes .tmp1 | Bin 0 -> 607 bytes convert_videoparams.py | 66 + reasonix.toml | 2 + 43 files changed, 8223 insertions(+) create mode 100644 .bak/nodeview/nodeview.cpp create mode 100644 .bak/nodeview/nodeview.h create mode 100644 .bak/nodeview/nodeviewcommon.h create mode 100644 .bak/nodeview/nodeviewcontext.cpp create mode 100644 .bak/nodeview/nodeviewcontext.h create mode 100644 .bak/nodeview/nodeviewedge.cpp create mode 100644 .bak/nodeview/nodeviewedge.h create mode 100644 .bak/nodeview/nodeviewitem.cpp create mode 100644 .bak/nodeview/nodeviewitem.h create mode 100644 .bak/nodeview/nodeviewitemconnector.cpp create mode 100644 .bak/nodeview/nodeviewitemconnector.h create mode 100644 .bak/nodeview/nodeviewminimap.cpp create mode 100644 .bak/nodeview/nodeviewminimap.h create mode 100644 .bak/nodeview/nodeviewscene.cpp create mode 100644 .bak/nodeview/nodeviewscene.h create mode 100644 .bak/nodeview/nodeviewtoolbar.cpp create mode 100644 .bak/nodeview/nodeviewtoolbar.h create mode 100644 .bak/nodeview/nodewidget.cpp create mode 100644 .bak/nodeview/nodewidget.h create mode 100644 .bak/projectexplorer/projectexplorer.cpp create mode 100644 .bak/projectexplorer/projectexplorer.h create mode 100644 .bak/projectexplorer/projectexplorericonview.cpp create mode 100644 .bak/projectexplorer/projectexplorericonview.h create mode 100644 .bak/projectexplorer/projectexplorericonviewitemdelegate.cpp create mode 100644 .bak/projectexplorer/projectexplorericonviewitemdelegate.h create mode 100644 .bak/projectexplorer/projectexplorerlistview.cpp create mode 100644 .bak/projectexplorer/projectexplorerlistview.h create mode 100644 .bak/projectexplorer/projectexplorerlistviewbase.cpp create mode 100644 .bak/projectexplorer/projectexplorerlistviewbase.h create mode 100644 .bak/projectexplorer/projectexplorerlistviewitemdelegate.cpp create mode 100644 .bak/projectexplorer/projectexplorerlistviewitemdelegate.h create mode 100644 .bak/projectexplorer/projectexplorernavigation.cpp create mode 100644 .bak/projectexplorer/projectexplorernavigation.h create mode 100644 .bak/projectexplorer/projectexplorertreeview.cpp create mode 100644 .bak/projectexplorer/projectexplorertreeview.h create mode 100644 .bak/projectexplorer/projectexplorerundo.h create mode 100644 .bak/projectexplorer/projectviewmodel.cpp create mode 100644 .bak/projectexplorer/projectviewmodel.h create mode 100644 .tmp0 create mode 100644 .tmp1 create mode 100644 convert_videoparams.py create mode 100644 reasonix.toml 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(item); +} + +bool ProjectViewModel::setData(const QModelIndex &index, const QVariant &value, + int role) +{ + // The name is editable + if (index.isValid() && index.column() == k_name && role == Qt::EditRole) { + Node *item = get_item_object_from_index(index); + + QString new_name = value.toString(); + + if (!new_name.isEmpty()) { + NodeRenameCommand *nrc = new NodeRenameCommand(); + + nrc->add_node(item, value.toString()); + + Core::instance()->undo_stack()->push( + nrc, tr("Renamed Item \"%1\" to \"%2\"") + .arg(item->get_label(), new_name)); + + return true; + } + } + + return false; +} + +bool ProjectViewModel::canFetchMore(const QModelIndex &parent) const +{ + // Use the same hack that always returns true with folders so the expand triangle is always visible + return hasChildren(parent); +} + +Qt::ItemFlags ProjectViewModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) { + // Allow dropping files from external sources + return Qt::ItemIsDropEnabled; + } + + Qt::ItemFlags f = Qt::ItemIsDragEnabled | QAbstractItemModel::flags(index); + + if (dynamic_cast(get_item_object_from_index(index))) { + f |= Qt::ItemIsDropEnabled; + } + + // If the column is the kName column, that means it's editable + if (index.column() == k_name) { + f |= Qt::ItemIsEditable; + } + + return f; +} + +QStringList ProjectViewModel::mimeTypes() const +{ + // Allow data from this model and a file list from external sources + return { Project::k_item_mime_type, QStringLiteral("text/uri-list") }; +} + +QMimeData *ProjectViewModel::mimeData(const QModelIndexList &indexes) const +{ + // Compliance with Qt standard + if (indexes.isEmpty()) { + return nullptr; + } + + // Encode mime data for the rows/items that were dragged + QMimeData *data = new QMimeData(); + + // Use QDataStream to stream the item data into a byte array + QByteArray encoded_data; + QDataStream stream(&encoded_data, QIODevice::WriteOnly); + + // The indexes list includes indexes for each column which we don't use. To make sure each row only gets sent *once*, + // we keep a list of dragged items + QVector dragged_items; + + foreach (QModelIndex index, indexes) { + if (index.isValid()) { + // Check if we've dragged this item before + if (!dragged_items.contains(index.internalPointer())) { + // If not, add it to the stream (and also keep track of it in the vector) + Node *item = static_cast(index.internalPointer()); + QVector streams; + + if (ViewerOutput *footage = + dynamic_cast(item)) { + streams = footage->get_enabled_streams_as_references(); + } + + stream << streams << reinterpret_cast(item); + + dragged_items.append(item); + } + } + } + + // Set byte array as the mime data and return the mime data + data->setData(Project::k_item_mime_type, encoded_data); + + return data; +} + +bool ProjectViewModel::dropMimeData(const QMimeData *data, + Qt::DropAction action, int row, int column, + const QModelIndex &drop) +{ + // Default recommended checks from https://doc.qt.io/qt-5/model-view-programming.html#using-drag-and-drop-with-item-views + if (!canDropMimeData(data, action, row, column, drop)) { + return false; + } + + if (action == Qt::IgnoreAction) { + return true; + } + + // Probe mime data for its format + QStringList mime_formats = data->formats(); + + if (mime_formats.contains(Project::k_item_mime_type)) { + // Data is drag/drop data from this model + QByteArray model_data = data->data(Project::k_item_mime_type); + + // Use QDataStream to deserialize the data + QDataStream stream(&model_data, QIODevice::ReadOnly); + + // Get the Item object that the items were dropped on + Folder *drop_location = + dynamic_cast(get_item_object_from_index(drop)); + + // If this is not a folder, we cannot drop these items here + if (!drop_location) { + return false; + } + + // Variables to deserialize into + quintptr item_ptr; + QList streams; + + // Loop through all data + MultiUndoCommand *move_command = new MultiUndoCommand(); + + int count = 0; + + while (!stream.atEnd()) { + stream >> streams >> item_ptr; + + Node *item = reinterpret_cast(item_ptr); + + // Check if Item is already the drop location or if its parent is the drop location, in which case this is a + // no-op + + if (item != drop_location && item->folder() != drop_location && + (!dynamic_cast(item) || + !item_is_parent_of_child(static_cast(item), + drop_location))) { + move_command->add_child(new NodeEdgeRemoveCommand( + item, + NodeInput(item->folder(), Folder::k_child_input, + item->folder()->index_of_child_in_array(item)))); + move_command->add_child( + new FolderAddChild(drop_location, item)); + count++; + } + } + + Core::instance()->undo_stack()->push(move_command, + tr("Move %1 Item(s)").arg(count)); + + return true; + + } else if (mime_formats.contains(QStringLiteral("text/uri-list"))) { + // We received a list of files + QByteArray file_data = data->data(QStringLiteral("text/uri-list")); + + // Use text stream to parse (just an easy way of sifting through line breaks + QTextStream stream(&file_data); + + // Convert QByteArray to QStringList (which Core takes for importing) + QStringList urls; + while (!stream.atEnd()) { + QUrl url = stream.readLine(); + + if (!url.isEmpty()) { + urls.append(url.toLocalFile()); + } + } + + // Get folder dropped onto + Node *drop_item = get_item_object_from_index(drop); + + // If we didn't drop onto an item, find the nearest parent folder (should eventually terminate at root either way) + if (!dynamic_cast(drop_item)) { + drop_item = drop_item->folder(); + + if (!drop_item) { + // Failed to find folder to place this in + return false; + } + } + + // Trigger an import + Core::instance()->import_files(urls, static_cast(drop_item)); + + return true; + } + + return false; +} + +int ProjectViewModel::index_of_child(Node *item) const +{ + // Find parent's index within its own parent + Folder *parent = item->folder(); + + if (parent) { + return parent->index_of_child(item); + } + + return -1; +} + +Node *ProjectViewModel::get_item_object_from_index(const QModelIndex &index) const +{ + if (index.isValid()) { + return static_cast(index.internalPointer()); + } + + return project_ ? project_->root() : nullptr; +} + +bool ProjectViewModel::item_is_parent_of_child(Folder *parent, Node *child) const +{ + // Loop through parent hierarchy checking if `parent` is one of its parents + do { + child = child->folder(); + + if (parent == child) { + return true; + } + } while (child != nullptr); + + return false; +} + +void ProjectViewModel::connect_item(Node *n) +{ + connect(n, &Node::label_changed, this, &ProjectViewModel::item_renamed); + + Folder *f = dynamic_cast(n); + if (f) { + connect(f, &Folder::begin_insert_item, this, + &ProjectViewModel::folder_begin_insert_item); + connect(f, &Folder::end_insert_item, this, + &ProjectViewModel::folder_end_insert_item); + connect(f, &Folder::begin_remove_item, this, + &ProjectViewModel::folder_begin_remove_item); + connect(f, &Folder::end_remove_item, this, + &ProjectViewModel::folder_end_remove_item); + + foreach (Node *c, f->children()) { + connect_item(c); + } + } +} + +void ProjectViewModel::disconnect_item(Node *n) +{ + disconnect(n, &Node::label_changed, this, &ProjectViewModel::item_renamed); + + Folder *f = dynamic_cast(n); + if (f) { + disconnect(f, &Folder::begin_insert_item, this, + &ProjectViewModel::folder_begin_insert_item); + disconnect(f, &Folder::end_insert_item, this, + &ProjectViewModel::folder_end_insert_item); + disconnect(f, &Folder::begin_remove_item, this, + &ProjectViewModel::folder_begin_remove_item); + disconnect(f, &Folder::end_remove_item, this, + &ProjectViewModel::folder_end_remove_item); + + foreach (Node *c, f->children()) { + disconnect_item(c); + } + } +} + +void ProjectViewModel::folder_begin_insert_item(Node *n, int insert_index) +{ + Folder *folder = static_cast(sender()); + + connect_item(n); + + QModelIndex index; + + if (folder != project_->root()) { + index = create_index_from_item(folder); + } + + beginInsertRows(index, insert_index, insert_index); +} + +void ProjectViewModel::folder_end_insert_item() +{ + endInsertRows(); +} + +void ProjectViewModel::folder_begin_remove_item(Node *n, int child_index) +{ + Folder *folder = static_cast(sender()); + + disconnect_item(n); + + QModelIndex index; + + if (folder != project_->root()) { + index = create_index_from_item(folder); + } + + beginRemoveRows(index, child_index, child_index); +} + +void ProjectViewModel::folder_end_remove_item() +{ + endRemoveRows(); +} + +void ProjectViewModel::item_renamed() +{ + Node *item = static_cast(sender()); + + QModelIndex index = create_index_from_item(item); + + emit dataChanged(index, index, { Qt::DisplayRole, Qt::EditRole }); +} + +QModelIndex ProjectViewModel::create_index_from_item(Node *item, int column) +{ + return createIndex(index_of_child(item), column, item); +} + +} diff --git a/.bak/projectexplorer/projectviewmodel.h b/.bak/projectexplorer/projectviewmodel.h new file mode 100644 index 000000000..85cddb825 --- /dev/null +++ b/.bak/projectexplorer/projectviewmodel.h @@ -0,0 +1,176 @@ +/*** + + 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_VIEWMODEL_H +#define OAK_VIEWMODEL_H + +#include + +#include "node/block/block.h" +#include "node/project.h" +#include "undo/undocommand.h" + +namespace olive +{ + +/** + * @brief An adapter that interprets the data in a Project into a Qt item model for usage in ViewModel Views. + * + * Assuming a Project is currently "open" (i.e. the Project is connected to a ProjectExplorer/ProjectPanel through + * a ProjectViewModel), it may be better to make modifications (e.g. additions/removals/renames) through the + * ProjectViewModel so that the views can be efficiently and correctly updated. ProjectViewModel contains several + * "wrapper" functions for Project and Item functions that also signal any connected views to update accordingly. + */ +class ProjectViewModel : public QAbstractItemModel { + Q_OBJECT +public: + enum ColumnType { + /// Media name + k_name, + + /// Media duration + k_duration, + + /// Media rate (frame rate for video, sample rate for audio) + k_rate, + + /// Last modified time (for footage/files) + k_last_modified, + + /// Creation time (for footage/files) + k_created_time, + + /// Count + k_column_count + }; + + static const int k_inner_text_role = Qt::UserRole + 1; + + /** + * @brief ProjectViewModel Constructor + * + * @param parent + * Parent object for memory handling + */ + ProjectViewModel(QObject *parent); + + /** + * @brief Get currently active project + * + * @return + * + * Currently active project or nullptr if there is none + */ + Project *project() const; + + /** + * @brief Set the project to adapt + * + * Any views attached to this model will get updated by this function. + * + * @param p + * + * Project to adapt, can be set to nullptr to "close" the project (will show an empty model that cannot be modified) + */ + void set_project(Project *p); + + /** Compulsory Qt QAbstractItemModel overrides */ + virtual QModelIndex + index(int row, int column, + const QModelIndex &parent = QModelIndex()) const override; + virtual QModelIndex parent(const QModelIndex &child) const override; + virtual int + rowCount(const QModelIndex &parent = QModelIndex()) const override; + virtual int + columnCount(const QModelIndex &parent = QModelIndex()) const override; + virtual QVariant data(const QModelIndex &index, + int role = Qt::DisplayRole) const override; + + /** Optional Qt QAbstractItemModel overrides */ + virtual QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + virtual bool + hasChildren(const QModelIndex &parent = QModelIndex()) const override; + virtual bool setData(const QModelIndex &index, const QVariant &value, + int role = Qt::EditRole) override; + virtual bool canFetchMore(const QModelIndex &parent) const override; + + /** Drag and drop support */ + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + virtual QStringList mimeTypes() const override; + virtual QMimeData *mimeData(const QModelIndexList &indexes) const override; + virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, + int row, int column, + const QModelIndex &parent) override; + + /** + * @brief Convenience function for creating QModelIndexes from an Item object + */ + QModelIndex create_index_from_item(Node *item, int column = 0); + +private: + /** + * @brief Retrieve the index of `item` in its parent + * + * This function will return the index of a specified item in its parent according to whichever sorting algorithm + * is currently active. + * + * @return + * + * Index of the specified item, or -1 if the item is root (in which case it has no parent). + */ + int index_of_child(Node *item) const; + + /** + * @brief Retrieves the Item object from a given index + * + * A convenience function for retrieving Item objects. If the index is not valid, this returns the root Item. + */ + Node *get_item_object_from_index(const QModelIndex &index) const; + + /** + * @brief Check if an Item is a parent of a Child + * + * Checks entire "parent hierarchy" of `child` to see if `parent` is one of its parents. + */ + bool item_is_parent_of_child(Folder *parent, Node *child) const; + + void connect_item(Node *n); + + void disconnect_item(Node *n); + + Project *project_; + +private slots: + void folder_begin_insert_item(Node *n, int insert_index); + + void folder_end_insert_item(); + + void folder_begin_remove_item(Node *n, int child_index); + + void folder_end_remove_item(); + + void item_renamed(); +}; + +} + +#endif // OAK_VIEWMODEL_H diff --git a/.gitignore b/.gitignore index 9b0d97838..fbddaefdf 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,4 @@ AGENTS.md operations-log.md verification.md act +.tmp/ diff --git a/.tmp0 b/.tmp0 new file mode 100644 index 0000000000000000000000000000000000000000..9ad899edd77bfa0ab0f1a551741cbd4b23d14c67 GIT binary patch literal 608 zcmV-m0-ya)Rz*Vq00$p@n0&0-6_x2{_!*%BuU=#^**X?854G;{4+>RPsEflWqQuir<#w z|Byln^>psqzXQaet*dt=lPg9li3m`Is1O)cmDC6u1v#!NqD(dR)r6jBi|tbH7z0f3 zilP-Z22mpzstn1HW}1mCC%Ta3$Tp$p*}~J%%CYA&CJB{*L{2M22^2_bQJ|vIMW$$x zmyC{V6MCNQMQp)1-_|<-Ayt`4RG4C4tj@;t@Cqp*GhwM_Lt{G-eoXER;*>{&5knK= z4T?J^;2o%Cfvd&skNX>*&VWwY{OEA?XG`R13?5Z`TG{@RAJ2qFyt?7iNSLg+KN6V- u+rnOk3{^ms zT;*z&mFXXf1Nbz*GZOmK*k}BB+l7|v$k^v)=wPcQ7?9c-yS(2AmdZ2|Y_!-oyN<|; zq^aMNlto!((ItJ@qG24G`oS1S@wlGG>25Q)us?K+Hz1#MH^ccMnr@kWpGO4LE4(Yq zTK|9QnBt?i>uJ4Xon%&ChXts!OuW|44nXm^zD|w5=BD^+a})=yzSL2?=;q(3_-#4< z4=Dr_FH^_<6Tk+kO}!IkUMapxkf^wyE1@{5;)-B}$+yt5hM=qDnY;!q0>m-LGj&#n3y` z;t9jSd&fC5Cp_U!voKBgRFpaSm6j<-Q&~KQ4x);#FQ=>az))zN{xa5(b*wDCe zf0dWWk|Es^37JnQzdv;84IzY`kvZiZ`D!f3_)naI@X t7WOin$F~~ZCg74eb{_Ln>7WkN>EO7^v8In}>`*hs;kKx6{sLs9vqpTWGbsQ7 literal 0 HcmV?d00001 diff --git a/convert_videoparams.py b/convert_videoparams.py new file mode 100644 index 000000000..8db908d04 --- /dev/null +++ b/convert_videoparams.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Batch convert simple VideoParams constant and static method references to facade equivalents.""" + +import os +import re + +# Files that use VideoParams (non-standardcombos - those are already converted) +target_files = [] +for root, dirs, fnames in os.walk('app'): + for f in fnames: + if f.endswith(('.cpp', '.h')) and 'autogen' not in root: + path = os.path.join(root, f) + try: + content = open(path, 'r', errors='replace').read() + if 'VideoParams' in content: + target_files.append(path) + except: + pass + +print(f"Files with VideoParams: {len(target_files)}") + +# Simple constant replacements (these are pure enum/integer replacements) +simple_replacements = [ + # (old, new) - EXACT string matches + ('VideoParams::k_interlace_none', '0'), + ('VideoParams::k_color_range_default', '0'), + ('VideoParams::k_color_range_limited', '0'), + ('VideoParams::k_color_range_full', '1'), + ('VideoParams::k_rgba_channel_count', '4'), + ('VideoParams::k_video_type_still', '1'), + ('VideoParams::k_video_type_image_sequence', '2'), + ('VideoParams::k_video_type_video', '0'), # This might be wrong - check + ('VideoParams::k_interlaced_top_first', '1'), + ('VideoParams::k_interlaced_bottom_first', '2'), +] + +# Track what was replaced +total_replacements = {} +for old, new in simple_replacements: + total_replacements[old] = 0 + +# For each file, do the replacements +for filepath in target_files: + with open(filepath, 'r', errors='replace') as f: + content = f.read() + + original = content + + for old, new in simple_replacements: + # Use word boundary to avoid partial matches + count = content.count(old) + if count > 0: + content = content.replace(old, new) + total_replacements[old] += count + + if content != original: + with open(filepath, 'w') as f: + f.write(content) + print(f" Modified: {filepath}") + +print("\nReplacement summary:") +for old, count in total_replacements.items(): + if count > 0: + print(f" {old} -> replaced {count} times") + +print("\nDone!") diff --git a/reasonix.toml b/reasonix.toml new file mode 100644 index 000000000..07b75400e --- /dev/null +++ b/reasonix.toml @@ -0,0 +1,2 @@ +[permissions] +allow = ["Bash(cd /home/mikesolar/Projects/oak && cmake --build cmake-build-debug -j$(nproc) 2>&1 | tail -30)"]