From 8e9859b2b36c09c709f1d562030edaf15ceb4140 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 16 May 2021 09:58:57 +1000 Subject: [PATCH 01/72] initial commit --- app/node/graph.cpp | 12 + app/node/graph.h | 41 ++++ app/node/node.cpp | 116 +++++---- app/node/node.h | 85 ++----- app/node/project/folder/folder.cpp | 3 +- app/node/project/folder/folder.h | 2 - app/node/project/project.cpp | 5 +- app/panel/node/node.h | 23 +- app/panel/timeline/timeline.h | 5 + app/undo/undocommand.cpp | 19 +- app/undo/undocommand.h | 4 +- app/widget/nodeview/nodeview.cpp | 267 ++++++++++++++++----- app/widget/nodeview/nodeview.h | 23 +- app/widget/nodeview/nodeviewitem.cpp | 6 - app/widget/nodeview/nodeviewscene.cpp | 17 +- app/widget/nodeview/nodeviewscene.h | 7 +- app/widget/timelinewidget/timelineundo.cpp | 2 +- app/widget/timelinewidget/timelineundo.h | 10 +- app/widget/timelinewidget/tool/add.cpp | 4 +- app/widget/timelinewidget/tool/import.cpp | 8 +- app/window/mainwindow/mainwindow.cpp | 28 +-- 21 files changed, 439 insertions(+), 248 deletions(-) diff --git a/app/node/graph.cpp b/app/node/graph.cpp index 972310906..3710c5241 100644 --- a/app/node/graph.cpp +++ b/app/node/graph.cpp @@ -75,6 +75,18 @@ void NodeGraph::childEvent(QChildEvent *event) emit NodeRemoved(node); emit node->RemovedFromGraph(this); + for (auto it=position_map_.begin(); it!=position_map_.end(); it++) { + PositionMap &map = it.value(); + for (auto jt=map.begin(); jt!=map.end(); ) { + if (jt.key() == node) { + jt = map.erase(jt); + emit NodePositionRemoved(node, it.key()); + } else { + jt++; + } + } + } + } } } diff --git a/app/node/graph.h b/app/node/graph.h index ecb1073a9..74f300622 100644 --- a/app/node/graph.h +++ b/app/node/graph.h @@ -63,6 +63,39 @@ public: return default_nodes_; } + bool NodeMapContainsNode(Node* node, void* relative) const + { + return position_map_.value(relative).contains(node); + } + + QPointF GetNodePosition(Node* node, void* relative) + { + return position_map_.value(relative).value(node); + } + + void SetNodePosition(Node* node, void* relative, const QPointF& pos) + { + position_map_[relative].insert(node, pos); + emit NodePositionAdded(node, relative, pos); + } + + void RemoveNodePosition(Node* node, void* relative) + { + PositionMap& map = position_map_[relative]; + map.remove(node); + if (map.isEmpty()) { + position_map_.remove(relative); + } + emit NodePositionRemoved(node, relative);; + } + + using PositionMap = QMap; + + const PositionMap &GetNodesForRelative(void *relative) + { + return position_map_[relative]; + } + signals: /** * @brief Signal emitted when a Node is added to the graph @@ -80,6 +113,10 @@ signals: void ValueChanged(const NodeInput& input); + void NodePositionAdded(Node *node, void *relative, const QPointF &position); + + void NodePositionRemoved(Node *node, void *relative); + protected: void AddDefaultNode(Node* n) { @@ -93,6 +130,10 @@ private: QVector default_nodes_; + QMap position_map_; + + PositionMap root_position_map_; + }; } diff --git a/app/node/node.cpp b/app/node/node.cpp index a33ae58bd..14988edf2 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -91,20 +91,6 @@ void Node::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, uint versi LoadInput(reader, xml_node_data, cancelled); } else if (reader->name() == QStringLiteral("ptr")) { xml_node_data.node_ptrs.insert(reader->readElementText().toULongLong(), this); - } else if (reader->name() == QStringLiteral("pos")) { - QPointF p; - - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("x")) { - p.setX(reader->readElementText().toDouble()); - } else if (reader->name() == QStringLiteral("y")) { - p.setY(reader->readElementText().toDouble()); - } else { - reader->skipCurrentElement(); - } - } - - SetPosition(p); } else if (reader->name() == QStringLiteral("label")) { SetLabel(reader->readElementText()); } else if (reader->name() == QStringLiteral("color")) { @@ -166,11 +152,6 @@ void Node::Save(QXmlStreamWriter *writer) const { writer->writeTextElement(QStringLiteral("ptr"), QString::number(reinterpret_cast(this))); - writer->writeStartElement(QStringLiteral("pos")); - writer->writeTextElement(QStringLiteral("x"), QString::number(GetPosition().x())); - writer->writeTextElement(QStringLiteral("y"), QString::number(GetPosition().y())); - writer->writeEndElement(); // pos - writer->writeTextElement(QStringLiteral("label"), GetLabel()); writer->writeTextElement(QStringLiteral("color"), QString::number(override_color_)); @@ -1483,7 +1464,6 @@ void Node::CopyInputs(const Node *source, Node *destination, bool include_connec CopyInput(source, destination, input, include_connections, true); } - destination->SetPosition(source->GetPosition()); destination->SetLabel(source->GetLabel()); destination->SetOverrideColor(source->GetOverrideColor()); } @@ -1832,29 +1812,6 @@ QVariant Node::PtrToValue(void *ptr) return reinterpret_cast(ptr); } -const QPointF &Node::GetPosition() const -{ - return position_; -} - -void Node::SetPosition(const QPointF &pos, bool move_dependencies_relatively_too) -{ - QPointF old_pos = position_; - - position_ = pos; - - emit PositionChanged(position_); - - if (move_dependencies_relatively_too) { - QPointF difference = pos - old_pos; - - for (auto it=input_connections_.cbegin(); it!=input_connections_.cend(); it++) { - Node* c = it->second.node(); - c->SetPosition(c->GetPosition() + difference, true); - } - } -} - void Node::ParameterValueChanged(const QString& input, int element, const TimeRange& range) { UpdateLastChangedTime(); @@ -2303,7 +2260,7 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo() { if (commands_.isEmpty()) { // Move first node - NodeSetPositionCommand* set_pos_command = new NodeSetPositionCommand(node_, position_, move_dependencies_); + NodeSetPositionCommand* set_pos_command = new NodeSetPositionCommand(node_, relative_, position_, move_dependencies_); set_pos_command->redo(); commands_.append(set_pos_command); @@ -2312,18 +2269,19 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo() // Start moving other nodes foreach (Node* surrounding, node_->parent()->nodes()) { - if (bounding_rect.contains(surrounding->GetPosition()) && surrounding != node_) { - QPointF new_pos = surrounding->GetPosition(); + QPointF surrounding_position = node_->parent()->GetNodePosition(surrounding, relative_); + if (bounding_rect.contains(surrounding_position) && surrounding != node_) { + QPointF new_pos = surrounding_position; qreal move_rate = 0.50; - if (surrounding->GetPosition().y() < position_.y()) { + if (surrounding_position.y() < position_.y()) { move_rate = -move_rate; } new_pos.setY(new_pos.y() + move_rate); - auto sur_command = new NodeSetPositionAndShiftSurroundingsCommand(surrounding, new_pos, true); + auto sur_command = new NodeSetPositionAndShiftSurroundingsCommand(surrounding, relative_, new_pos, true); sur_command->redo(); commands_.append(sur_command); } @@ -2335,4 +2293,66 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo() } } +void NodeSetPositionCommand::redo() +{ + NodeGraph* graph = node_->parent(); + if (!(added_ = !graph->NodeMapContainsNode(node_, relevant_))) { + old_pos_ = graph->GetNodePosition(node_, relevant_); + } + graph->SetNodePosition(node_, relevant_, pos_); +} + +void NodeSetPositionCommand::undo() +{ + NodeGraph* graph = node_->parent(); + if (added_) { + graph->RemoveNodePosition(node_, relevant_); + } else { + graph->SetNodePosition(node_, relevant_, old_pos_); + } +} + +void NodeSetPositionAsChildCommand::redo() +{ + if (!sub_command_) { + // Calculate position of node + NodeGraph *graph = parent_->parent(); + QPointF pos = graph->GetNodePosition(parent_, relative_); + + // This is a dependency, so we'll place it one X before + pos.setX(pos.x() - 1); + + // The Y will be calculated using the index and child count + pos.setY(pos.y() - (double(child_count_)*0.5) + this_index_ + 0.5); + + sub_command_ = new MultiUndoCommand(); + if (shift_surroundings_) { + if (relative_) { + sub_command_->add_child(new NodeSetPositionAndShiftSurroundingsCommand(node_, relative_, pos, true)); + } + sub_command_->add_child(new NodeSetPositionAndShiftSurroundingsCommand(node_, nullptr, pos, true)); + } else { + if (relative_) { + sub_command_->add_child(new NodeSetPositionCommand(node_, relative_, pos, true)); + } + sub_command_->add_child(new NodeSetPositionCommand(node_, nullptr, pos, true)); + } + } + + sub_command_->redo(); +} + +void NodeSetPositionToOffsetOfAnotherNodeCommand::redo() +{ + NodeGraph *graph = node_->parent(); + old_pos_ = graph->GetNodePosition(node_, relative_); + graph->SetNodePosition(node_, relative_, graph->GetNodePosition(other_node_, relative_) + offset_); +} + +void NodeSetPositionToOffsetOfAnotherNodeCommand::undo() +{ + NodeGraph *graph = node_->parent(); + graph->SetNodePosition(node_, relative_, old_pos_); +} + } diff --git a/app/node/node.h b/app/node/node.h index 71e6e78cd..9778c688e 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -718,10 +718,6 @@ public: */ virtual NodeValueTable Value(const QString &output, NodeValueDatabase& value) const; - const QPointF& GetPosition() const; - - void SetPosition(const QPointF& pos, bool move_dependencies_relatively_too = false); - virtual bool HasGizmos() const; virtual void DrawGizmos(NodeValueDatabase& db, QPainter* p); @@ -1180,11 +1176,6 @@ private: */ bool can_be_deleted_; - /** - * @brief UI position for NodeViews - */ - QPointF position_; - /** * @brief Custom user label for node */ @@ -1312,35 +1303,29 @@ using NodePtr = std::shared_ptr; class NodeSetPositionCommand : public UndoCommand { public: - NodeSetPositionCommand(Node* node, const QPointF& position, bool move_dependencies_relatively) : - node_(node), - new_pos_(position), - move_deps_(move_dependencies_relatively) + NodeSetPositionCommand(Node* node, void* relevant, const QPointF& pos, bool move_dependencies_relatively) { + node_ = node; + relevant_ = relevant; + pos_ = pos; + move_deps_ = move_dependencies_relatively; } - virtual Project * GetRelevantProject() const override + virtual Project* GetRelevantProject() const override { return node_->project(); } - virtual void redo() override - { - old_pos_ = node_->GetPosition(); - node_->SetPosition(new_pos_, move_deps_); - } + virtual void redo() override; - virtual void undo() override - { - node_->SetPosition(old_pos_, move_deps_); - } + virtual void undo() override; private: Node* node_; - - QPointF new_pos_; + void* relevant_; + QPointF pos_; QPointF old_pos_; - + bool added_; bool move_deps_; }; @@ -1348,8 +1333,9 @@ private: class NodeSetPositionAndShiftSurroundingsCommand : public UndoCommand { public: - NodeSetPositionAndShiftSurroundingsCommand(Node* node, const QPointF& pos, bool move_dependencies_relatively) : + NodeSetPositionAndShiftSurroundingsCommand(Node* node, void *relative, const QPointF& pos, bool move_dependencies_relatively) : node_(node), + relative_(relative), position_(pos), move_dependencies_(move_dependencies_relatively) {} @@ -1376,6 +1362,8 @@ public: private: Node* node_; + void *relative_; + QPointF position_; bool move_dependencies_; @@ -1387,9 +1375,10 @@ private: class NodeSetPositionAsChildCommand : public UndoCommand { public: - NodeSetPositionAsChildCommand(Node* node, Node* parent, int this_index, int child_count, bool shift_surroundings) : + NodeSetPositionAsChildCommand(Node* node, Node* parent, void *relative, int this_index, int child_count, bool shift_surroundings) : node_(node), parent_(parent), + relative_(relative), this_index_(this_index), child_count_(child_count), shift_surroundings_(shift_surroundings), @@ -1407,27 +1396,7 @@ public: return node_->project(); } - virtual void redo() override - { - if (!sub_command_) { - // Calculate position of node - QPointF pos = parent_->GetPosition(); - - // This is a dependency, so we'll place it one X before - pos.setX(pos.x() - 1); - - // The Y will be calculated using the index and child count - pos.setY(pos.y() - (double(child_count_)*0.5) + this_index_ + 0.5); - - if (shift_surroundings_) { - sub_command_ = new NodeSetPositionAndShiftSurroundingsCommand(node_, pos, true); - } else { - sub_command_ = new NodeSetPositionCommand(node_, pos, true); - } - } - - sub_command_->redo(); - } + virtual void redo() override; virtual void undo() override { @@ -1437,22 +1406,24 @@ public: private: Node* node_; Node* parent_; + void *relative_; int this_index_; int child_count_; bool shift_surroundings_; - UndoCommand* sub_command_; + MultiUndoCommand* sub_command_; }; class NodeSetPositionToOffsetOfAnotherNodeCommand : public UndoCommand { public: - NodeSetPositionToOffsetOfAnotherNodeCommand(Node* node, Node* other_node, const QPointF& offset) : + NodeSetPositionToOffsetOfAnotherNodeCommand(Node* node, Node* other_node, void *relative, const QPointF& offset) : node_(node), other_node_(other_node), + relative_(relative), offset_(offset) {} @@ -1461,20 +1432,16 @@ public: return node_->project(); } - virtual void redo() override - { - node_->SetPosition(other_node_->GetPosition() + offset_); - } + virtual void redo() override; - virtual void undo() override - { - node_->SetPosition(other_node_->GetPosition() - offset_); - } + virtual void undo() override; private: Node* node_; Node* other_node_; + void *relative_; QPointF offset_; + QPointF old_pos_; }; diff --git a/app/node/project/folder/folder.cpp b/app/node/project/folder/folder.cpp index dc07f574e..784d4e376 100644 --- a/app/node/project/folder/folder.cpp +++ b/app/node/project/folder/folder.cpp @@ -137,9 +137,8 @@ void FolderAddChild::redo() Node::ConnectEdge(child_, NodeInput(folder_, Folder::kChildInput, array_index)); if (autoposition_) { - old_position_ = child_->GetPosition(); if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(child_, folder_, array_index, array_index+1, true); + position_command_ = new NodeSetPositionAsChildCommand(child_, folder_, folder_->project(), array_index, array_index+1, true); } position_command_->redo(); } diff --git a/app/node/project/folder/folder.h b/app/node/project/folder/folder.h index 3e96cb293..fa21bc4c7 100644 --- a/app/node/project/folder/folder.h +++ b/app/node/project/folder/folder.h @@ -180,8 +180,6 @@ private: bool autoposition_; - QPointF old_position_; - NodeSetPositionAsChildCommand* position_command_; }; diff --git a/app/node/project/project.cpp b/app/node/project/project.cpp index ab4717e7c..35a8b485b 100644 --- a/app/node/project/project.cpp +++ b/app/node/project/project.cpp @@ -42,14 +42,14 @@ Project::Project() : // Adds a color manager "node" to this project so that it synchronizes color_manager_ = new ColorManager(); color_manager_->setParent(this); - color_manager_->SetPosition(QPointF(1, 0)); + SetNodePosition(color_manager_, this, QPointF(1, 0)); color_manager_->SetCanBeDeleted(false); AddDefaultNode(color_manager_); // Same with project settings settings_ = new ProjectSettingsNode(); settings_->setParent(this); - settings_->SetPosition(QPointF(2, 0)); + SetNodePosition(settings_, this, QPointF(2, 0)); settings_->SetCanBeDeleted(false); AddDefaultNode(settings_); @@ -58,6 +58,7 @@ Project::Project() : root_->setParent(this); root_->SetLabel(tr("Root")); root_->SetCanBeDeleted(false); + SetNodePosition(root_, this, QPointF(0, 0)); connect(color_manager(), &ColorManager::ValueChanged, this, &Project::ColorManagerValueChanged); diff --git a/app/panel/node/node.h b/app/panel/node/node.h index 999626b7c..02600ec65 100644 --- a/app/panel/node/node.h +++ b/app/panel/node/node.h @@ -40,9 +40,14 @@ public: return node_view_->GetGraph(); } - void SetGraph(NodeGraph *graph) + void SetGraph(NodeGraph *graph, const QVector &nodes) { - node_view_->SetGraph(graph); + node_view_->SetGraph(graph, nodes); + } + + void ClearGraph() + { + node_view_->ClearGraph(); } virtual void SelectAll() override @@ -106,20 +111,6 @@ public slots: node_view_->SelectWithDependencies(nodes); } - void SelectBlocks(const QVector& blocks) - { - QVector nodes(blocks.size()); - memcpy(nodes.data(), blocks.constData(), blocks.size() * sizeof(Block*)); - node_view_->SelectWithDependencies(nodes); - } - - void DeselectBlocks(const QVector& nodes) - { - Q_UNUSED(nodes) - qDebug() << "Stub"; - //node_view_->DeselectBlocks(nodes); - } - signals: void NodesSelected(const QVector& nodes); diff --git a/app/panel/timeline/timeline.h b/app/panel/timeline/timeline.h index 5a195b818..1b33ac9fb 100644 --- a/app/panel/timeline/timeline.h +++ b/app/panel/timeline/timeline.h @@ -89,6 +89,11 @@ public: void OverwriteFootageAtPlayhead(const QVector &footage); + const QVector& GetSelectedBlocks() const + { + return static_cast(GetTimeBasedWidget())->GetSelectedBlocks(); + } + protected: virtual void Retranslate() override; diff --git a/app/undo/undocommand.cpp b/app/undo/undocommand.cpp index d00a327ec..668617b65 100644 --- a/app/undo/undocommand.cpp +++ b/app/undo/undocommand.cpp @@ -24,17 +24,28 @@ namespace olive { +MultiUndoCommand::MultiUndoCommand() : + done_(false) +{ +} + void MultiUndoCommand::redo() { - for (auto it=children_.cbegin(); it!=children_.cend(); it++) { - (*it)->redo_and_set_modified(); + if (!done_) { + for (auto it=children_.cbegin(); it!=children_.cend(); it++) { + (*it)->redo_and_set_modified(); + } + done_ = true; } } void MultiUndoCommand::undo() { - for (auto it=children_.crbegin(); it!=children_.crend(); it++) { - (*it)->undo_and_set_modified(); + if (done_) { + for (auto it=children_.crbegin(); it!=children_.crend(); it++) { + (*it)->undo_and_set_modified(); + } + done_ = false; } } diff --git a/app/undo/undocommand.h b/app/undo/undocommand.h index 54f663d64..c310bf131 100644 --- a/app/undo/undocommand.h +++ b/app/undo/undocommand.h @@ -71,7 +71,7 @@ private: class MultiUndoCommand : public UndoCommand { public: - MultiUndoCommand() = default; + MultiUndoCommand(); virtual void redo() override; virtual void undo() override; @@ -99,6 +99,8 @@ public: private: std::vector children_; + bool done_; + }; } diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 118583f0f..fcfe6736e 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -44,7 +44,8 @@ NodeView::NodeView(QWidget *parent) : create_edge_(nullptr), create_edge_dst_(nullptr), create_edge_dst_temp_expanded_(false), - filter_mode_(kFilterShowSelectedBlocks), + paste_command_(nullptr), + filter_mode_(kFilterShowSelective), scale_(1.0) { setScene(&scene_); @@ -59,57 +60,78 @@ NodeView::NodeView(QWidget *parent) : ConnectSelectionChangedSignal(); SetFlowDirection(NodeViewCommon::kTopToBottom); - - // Set massive scene rect and hide the scrollbars to create an "infinite space" effect - scene_.setSceneRect(-1000000, -1000000, 2000000, 2000000); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } NodeView::~NodeView() { // Unset the current graph - SetGraph(nullptr); + ClearGraph(); } -void NodeView::SetGraph(NodeGraph *graph) +void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) { - if (graph_ == graph) { - return; - } + // Handle potentially changing graph + if (graph_ != graph) { + if (graph_) { + disconnect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); + disconnect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); + disconnect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); + disconnect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); + disconnect(graph_, &NodeGraph::NodePositionAdded, this, &NodeView::AddNodePosition); + disconnect(graph_, &NodeGraph::NodePositionRemoved, this, &NodeView::RemoveNodePosition); - if (graph_) { - disconnect(graph_, &NodeGraph::NodeAdded, &scene_, &NodeViewScene::AddNode); - disconnect(graph_, &NodeGraph::NodeRemoved, &scene_, &NodeViewScene::RemoveNode); - disconnect(graph_, &NodeGraph::InputConnected, &scene_, &NodeViewScene::AddEdge); - disconnect(graph_, &NodeGraph::InputDisconnected, &scene_, &NodeViewScene::RemoveEdge); - - DeselectAll(); - - // Clear the scene of all UI objects - scene_.clear(); - } - - // Set reference to the graph - graph_ = graph; - - // If the graph is valid, add UI objects for each of its Nodes - if (graph_) { - connect(graph_, &NodeGraph::NodeAdded, &scene_, &NodeViewScene::AddNode); - connect(graph_, &NodeGraph::NodeRemoved, &scene_, &NodeViewScene::RemoveNode); - connect(graph_, &NodeGraph::InputConnected, &scene_, &NodeViewScene::AddEdge); - connect(graph_, &NodeGraph::InputDisconnected, &scene_, &NodeViewScene::RemoveEdge); - - foreach (Node* n, graph_->nodes()) { - scene_.AddNode(n); + if (filter_mode_ == kFilterShowAll) { + // Switching graphs, close all nodes + DeselectAll(); + scene_.clear(); + } } - foreach (Node* n, graph_->nodes()) { - for (auto it=n->input_connections().cbegin(); it!=n->input_connections().cend(); it++) { - scene_.AddEdge(it->second, it->first); + graph_ = graph; + + if (graph_) { + connect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); + connect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); + connect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); + connect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); + connect(graph_, &NodeGraph::NodePositionAdded, this, &NodeView::AddNodePosition); + connect(graph_, &NodeGraph::NodePositionRemoved, this, &NodeView::RemoveNodePosition); + + if (filter_mode_ == kFilterShowAll) { + foreach (Node* n, graph_->nodes()) { + scene_.AddNode(n); + } + + foreach (Node* n, graph_->nodes()) { + for (auto it=n->input_connections().cbegin(); it!=n->input_connections().cend(); it++) { + scene_.AddEdge(it->second, it->first); + } + } } } } + + // Handle changing nodes + if (filter_nodes_ != nodes) { + DeselectAll(); + scene_.clear(); + + filter_nodes_ = nodes; + + foreach (void *n, filter_nodes_) { + const NodeGraph::PositionMap &map = graph_->GetNodesForRelative(n); + + for (auto it=map.cbegin(); it!=map.cend(); it++) { + NodeViewItem *item = scene_.AddNode(it.key()); + item->SetNodePosition(it.value()); + } + } + } +} + +void NodeView::ClearGraph() +{ + SetGraph(nullptr, QVector()); } void NodeView::DeleteSelected() @@ -295,15 +317,15 @@ void NodeView::Paste() return; } - MultiUndoCommand* command = new MultiUndoCommand(); + paste_command_ = new MultiUndoCommand(); - QVector pasted_nodes = PasteNodesFromClipboard(graph_, command); + QVector pasted_nodes = PasteNodesFromClipboard(graph_, paste_command_); if (!pasted_nodes.isEmpty()) { - command->add_child(new NodeViewAttachNodesToCursor(this, pasted_nodes)); + paste_command_->add_child(new NodeViewAttachNodesToCursor(this, pasted_nodes)); } - Core::instance()->undo_stack()->pushIfHasChildren(command); + paste_command_->redo(); } void NodeView::Duplicate() @@ -318,15 +340,15 @@ void NodeView::Duplicate() return; } - MultiUndoCommand* command = new MultiUndoCommand(); + paste_command_ = new MultiUndoCommand(); - QVector duplicated_nodes = Node::CopyDependencyGraph(selected, command); + QVector duplicated_nodes = Node::CopyDependencyGraph(selected, paste_command_); if (!duplicated_nodes.isEmpty()) { - command->add_child(new NodeViewAttachNodesToCursor(this, duplicated_nodes)); + paste_command_->add_child(new NodeViewAttachNodesToCursor(this, duplicated_nodes)); } - Core::instance()->undo_stack()->pushIfHasChildren(command); + paste_command_->redo(); } void NodeView::SetColorLabel(int index) @@ -346,6 +368,59 @@ void NodeView::ZoomOut() ZoomFromKeyboard(0.8); } +/*void NodeView::AddNodesToFilter(const QVector &nodes) +{ + // Determine new nodes + QVector multiple_sources; + + foreach (Node* node, nodes) { + // Node is new and being added + scene_.AddNode(node); + + QList visible = graph_->GetNodesForRelative(node); + foreach (Node* v, visible) { + if (scene_.NodeToUIObject(v)) { + multiple_sources.append(v); + } else { + NodeViewItem* item = scene_.AddNode(v); + item->SetNodePosition(graph_->GetNodePosition(v, node)); + } + } + } + + filter_nodes_.append(nodes); +} + +void NodeView::RemoveNodesFromFilter(const QVector &nodes) +{ + // Determine old nodes + foreach (Node* node, nodes) { + // Node is old and being removed + QList visible = graph_->GetNodesForRelative(node); + foreach (Node* v, visible) { + bool found = false; + + foreach (Node* n, filter_nodes_) { + if (node != n) { + QList other_deps = graph_->GetNodesForRelative(n); + + if (other_deps.contains(v)) { + found = true; + break; + } + } + } + + if (!found) { + scene_.RemoveNode(v); + } + } + + scene_.RemoveNode(node); + filter_nodes_.removeOne(node); + } +}*/ + void NodeView::keyPressEvent(QKeyEvent *event) { super::keyPressEvent(event); @@ -354,8 +429,11 @@ void NodeView::keyPressEvent(QKeyEvent *event) DetachItemsFromCursor(); // We undo the last action which SHOULD be adding the node - // FIXME: Possible danger of this not being the case? - Core::instance()->undo_stack()->undo(); + if (paste_command_) { + paste_command_->undo(); + delete paste_command_; + paste_command_ = nullptr; + } } } @@ -596,27 +674,34 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) } if (!attached_items_.isEmpty()) { + MultiUndoCommand* command = new MultiUndoCommand(); + + if (paste_command_) { + // We've already "done" this command, but MultiUndoCommand prevents "redoing" twice, so we + // add it to this command (which may have extra commands added too) so that it all gets undone + // in the same action + command->add_child(paste_command_); + paste_command_ = nullptr; + } + if (attached_items_.size() == 1) { Node* dropping_node = attached_items_.first().item->GetNode(); if (drop_edge_) { - // We have everything we need to place the node in between - MultiUndoCommand* command = new MultiUndoCommand(); - // Remove old edge command->add_child(new NodeEdgeRemoveCommand(drop_edge_->output(), drop_edge_->input())); // Place new edges command->add_child(new NodeEdgeAddCommand(drop_edge_->output(), drop_input_)); command->add_child(new NodeEdgeAddCommand(dropping_node, drop_edge_->input())); - - Core::instance()->undo_stack()->push(command); } drop_edge_ = nullptr; } DetachItemsFromCursor(); + + Core::instance()->undo_stack()->push(command); } super::mouseReleaseEvent(event); @@ -718,12 +803,12 @@ void NodeView::ShowContextMenu(const QPoint &pos) Menu* filter_menu = new Menu(tr("Filter"), &m); m.addMenu(filter_menu); - filter_menu->AddActionWithData(tr("Show All"), + filter_menu->AddActionWithData(tr("Show All Nodes"), kFilterShowAll, filter_mode_); - filter_menu->AddActionWithData(tr("Show Selected Blocks Only"), - kFilterShowSelectedBlocks, + filter_menu->AddActionWithData(tr("Show Selected"), + kFilterShowSelective, filter_mode_); connect(filter_menu, &Menu::triggered, this, &NodeView::ContextMenuFilterChanged); @@ -791,7 +876,22 @@ void NodeView::AutoPositionDescendents() void NodeView::ContextMenuFilterChanged(QAction *action) { - Q_UNUSED(action) + FilterMode mode = static_cast(action->data().toInt()); + + if (filter_mode_ != mode) { + // Store temporary graph variables + NodeGraph *graph = graph_; + QVector nodes = filter_nodes_; + + // Unset graph with current filter mode + ClearGraph(); + + // Change filter mode + filter_mode_ = mode; + + // Re-set graph with new filter mode + SetGraph(graph, nodes); + } } void NodeView::OpenSelectedNodeInViewer() @@ -804,6 +904,59 @@ void NodeView::OpenSelectedNodeInViewer() } } +void NodeView::AddNode(Node *node) +{ + if (filter_mode_ == kFilterShowAll) { + scene_.AddNode(node); + } +} + +void NodeView::RemoveNode(Node *node) +{ + if (filter_mode_ == kFilterShowAll) { + scene_.RemoveNode(node); + } +} + +void NodeView::AddEdge(const NodeOutput &output, const NodeInput &input) +{ + if (filter_mode_ == kFilterShowAll) { + scene_.AddEdge(output, input); + } +} + +void NodeView::RemoveEdge(const NodeOutput &output, const NodeInput &input) +{ + if (filter_mode_ == kFilterShowAll) { + scene_.RemoveEdge(output, input); + } +} + +void NodeView::AddNodePosition(Node *node, void *relative, const QPointF &pos) +{ + if (filter_mode_ == kFilterShowSelective) { + if (filter_nodes_.contains(relative)) { + NodeViewItem *item = scene_.item_map().value(node); + + if (!item) { + item = scene_.AddNode(node); + } + + item->SetNodePosition(pos); + } + } +} + +void NodeView::RemoveNodePosition(Node *node, void *relative) +{ + if (filter_mode_ == kFilterShowSelective) { + if (filter_nodes_.contains(relative)) { + NodeViewItem *item = scene_.item_map().value(node); + delete item; + } + } +} + void NodeView::AttachNodesToCursor(const QVector &nodes) { QVector items(nodes.size()); diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 28fb3c2d9..05b0ca8a4 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -51,10 +51,9 @@ public: return graph_; } - /** - * @brief Sets the graph to view - */ - void SetGraph(NodeGraph* graph); + void SetGraph(NodeGraph *graph, const QVector &nodes); + + void ClearGraph(); /** * @brief Delete selected nodes from graph (user-friendly/undoable) @@ -147,17 +146,19 @@ private: NodeViewScene scene_; - QVector selected_nodes_; + MultiUndoCommand* paste_command_; - QVector selected_blocks_; + QVector selected_nodes_; enum FilterMode { kFilterShowAll, - kFilterShowSelectedBlocks + kFilterShowSelective }; FilterMode filter_mode_; + QVector filter_nodes_; + double scale_; bool create_edge_already_exists_; @@ -200,6 +201,14 @@ private slots: */ void OpenSelectedNodeInViewer(); + void AddNode(Node *node); + void RemoveNode(Node *node); + void AddEdge(const NodeOutput& output, const NodeInput& input); + void RemoveEdge(const NodeOutput& output, const NodeInput& input); + + void AddNodePosition(Node *node, void *relative, const QPointF &pos); + void RemoveNodePosition(Node *node, void *relative); + }; } diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index ef600bb61..d07fbaa9d 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -192,8 +192,6 @@ void NodeViewItem::SetNode(Node *n) node_inputs_.append(input); } } - - SetNodePosition(node_->GetPosition()); } update(); @@ -352,10 +350,6 @@ void NodeViewItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionHasChanged && node_) { - node_->blockSignals(true); - node_->SetPosition(GetNodePosition()); - node_->blockSignals(false); - ReadjustAllEdges(); } diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index d1447239a..e389b0ce4 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -43,9 +43,6 @@ void NodeViewScene::SetFlowDirection(NodeViewCommon::FlowDirection direction) QHash::const_iterator i; for (i=item_map_.constBegin(); i!=item_map_.constEnd(); i++) { i.value()->SetFlowDirection(direction_); - - // Update position too - i.value()->SetNodePosition(i.key()->GetPosition()); } } @@ -150,7 +147,7 @@ QVector NodeViewScene::GetSelectedEdges() const return edges; } -void NodeViewScene::AddNode(Node* node) +NodeViewItem* NodeViewScene::AddNode(Node* node) { NodeViewItem* item = new NodeViewItem(); @@ -160,16 +157,16 @@ void NodeViewScene::AddNode(Node* node) addItem(item); item_map_.insert(node, item); - connect(node, &Node::PositionChanged, this, &NodeViewScene::NodePositionChanged); connect(node, &Node::LabelChanged, this, &NodeViewScene::NodeAppearanceChanged); connect(node, &Node::ColorChanged, this, &NodeViewScene::NodeAppearanceChanged); + + return item; } void NodeViewScene::RemoveNode(Node *node) { disconnect(node, &Node::ColorChanged, this, &NodeViewScene::NodeAppearanceChanged); disconnect(node, &Node::LabelChanged, this, &NodeViewScene::NodeAppearanceChanged); - disconnect(node, &Node::PositionChanged, this, &NodeViewScene::NodePositionChanged); delete item_map_.take(node); } @@ -229,6 +226,7 @@ NodeViewCommon::FlowDirection NodeViewScene::GetFlowDirection() const void NodeViewScene::ReorganizeFrom(Node* n) { + /* QVector immediates = n->GetImmediateDependencies(); if (immediates.isEmpty()) { @@ -258,6 +256,7 @@ void NodeViewScene::ReorganizeFrom(Node* n) ReorganizeFrom(i); } } + */ } void NodeViewScene::SetEdgesAreCurved(bool curved) @@ -271,12 +270,6 @@ void NodeViewScene::SetEdgesAreCurved(bool curved) } } -void NodeViewScene::NodePositionChanged(const QPointF &pos) -{ - // Update node's internal position - item_map_.value(static_cast(sender()))->SetNodePosition(pos); -} - void NodeViewScene::NodeAppearanceChanged() { // Force item to update diff --git a/app/widget/nodeview/nodeviewscene.h b/app/widget/nodeview/nodeviewscene.h index 501e691f4..272c592ce 100644 --- a/app/widget/nodeview/nodeviewscene.h +++ b/app/widget/nodeview/nodeviewscene.h @@ -88,7 +88,7 @@ public slots: * This should NEVER be called directly, only connected to a NodeGraph. To add a Node to the NodeGraph * use NodeGraph::AddNode(). */ - void AddNode(Node* node); + NodeViewItem *AddNode(Node* node); /** * @brief Slot when a Node is removed from a graph (SetGraph() connects this) @@ -122,11 +122,6 @@ private: bool curved_edges_; private slots: - /** - * @brief Receiver for whenever a node position changes - */ - void NodePositionChanged(const QPointF& pos); - /** * @brief Receiver for when a node's label has changed */ diff --git a/app/widget/timelinewidget/timelineundo.cpp b/app/widget/timelinewidget/timelineundo.cpp index b48cdd452..51625ead2 100644 --- a/app/widget/timelinewidget/timelineundo.cpp +++ b/app/widget/timelinewidget/timelineundo.cpp @@ -260,7 +260,7 @@ void TrackReplaceBlockWithGapCommand::redo() track_->ReplaceBlock(block_, our_gap_); if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(our_gap_, track_, our_gap_->index(), track_->Blocks().size(), true); + position_command_ = new NodeSetPositionAsChildCommand(our_gap_, track_, track_, our_gap_->index(), track_->Blocks().size(), true); } position_command_->redo(); } diff --git a/app/widget/timelinewidget/timelineundo.h b/app/widget/timelinewidget/timelineundo.h index de868f2d6..3a1a25041 100644 --- a/app/widget/timelinewidget/timelineundo.h +++ b/app/widget/timelinewidget/timelineundo.h @@ -372,7 +372,7 @@ public: // Position the block if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(new_block(), track, new_block()->index(), track->Blocks().size(), true); + position_command_ = new NodeSetPositionAsChildCommand(new_block(), track, nullptr, new_block()->index(), track->Blocks().size(), true); } position_command_->redo(); @@ -1198,7 +1198,7 @@ public: timeline_->ArrayAppend(); int track_total_index = timeline_->parent()->GetTracks().size(); if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(track_, timeline_->parent(), track_total_index, track_total_index + 1, true); + position_command_ = new NodeSetPositionAsChildCommand(track_, timeline_->parent(), timeline_->parent(), track_total_index, track_total_index + 1, true); } position_command_->redo(); Node::ConnectEdge(track_, timeline_->track_input(timeline_->ArraySize() - 1)); @@ -1385,9 +1385,9 @@ public: if (position_commands_.isEmpty()) { // Create position commands for insert and gap if necessary if (gap_) { - position_commands_.append(new NodeSetPositionAsChildCommand(gap_, track, gap_->index(), track->Blocks().size(), true)); + position_commands_.append(new NodeSetPositionAsChildCommand(gap_, track, track, gap_->index(), track->Blocks().size(), true)); } - position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, insert_->index(), track->Blocks().size(), true)); + position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, track, insert_->index(), track->Blocks().size(), true)); } } else { // Place the Block at this point @@ -1401,7 +1401,7 @@ public: track->InsertBlockAfter(insert_, ripple_remove_command_->GetInsertionIndex()); if (position_commands_.isEmpty()) { - position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, insert_->index(), track->Blocks().size(), true)); + position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, track, insert_->index(), track->Blocks().size(), true)); } } diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index 06412b6c8..930eec4e3 100644 --- a/app/widget/timelinewidget/tool/add.cpp +++ b/app/widget/timelinewidget/tool/add.cpp @@ -121,7 +121,7 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event) solid)); command->add_child(new NodeEdgeAddCommand(solid, NodeInput(clip, ClipBlock::kBufferIn))); - command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(solid, clip, extra_node_offset)); + command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(solid, clip, clip, extra_node_offset)); break; } case olive::Tool::kAddableTitle: @@ -132,7 +132,7 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event) text)); command->add_child(new NodeEdgeAddCommand(text, NodeInput(clip, ClipBlock::kBufferIn))); - command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(text, clip, extra_node_offset)); + command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(text, clip, clip, extra_node_offset)); break; } case olive::Tool::kAddableBars: diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 205d0f00c..251e21e14 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -387,7 +387,9 @@ void ImportTool::DropGhosts(bool insert) clip->set_length_and_media_out(ghost->GetLength()); clip->SetLabel(footage_stream.footage->GetLabel()); command->add_child(new NodeAddCommand(dst_graph, clip)); - command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(clip, footage_stream.footage, QPointF(2, 0))); + command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(clip, footage_stream.footage, clip, QPointF(2, 0))); + + command->add_child(new NodeSetPositionCommand(footage_stream.footage, clip, QPointF(-2, 0), false)); switch (Track::Reference::TypeFromString(footage_stream.output)) { case Track::kVideo: @@ -397,7 +399,7 @@ void ImportTool::DropGhosts(bool insert) command->add_child(new NodeEdgeAddCommand(corresponding_output, NodeInput(transform, TransformDistortNode::kTextureInput))); command->add_child(new NodeEdgeAddCommand(transform, NodeInput(clip, ClipBlock::kBufferIn))); - command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(transform, clip, QPointF(-1, 0))); + command->add_child(new NodeSetPositionCommand(transform, clip, QPointF(-1, 0), false)); break; } case Track::kAudio: @@ -407,7 +409,7 @@ void ImportTool::DropGhosts(bool insert) command->add_child(new NodeEdgeAddCommand(corresponding_output, NodeInput(volume_node, VolumeNode::kSamplesInput))); command->add_child(new NodeEdgeAddCommand(volume_node, NodeInput(clip, ClipBlock::kBufferIn))); - command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(volume_node, clip, QPointF(-1, 0))); + command->add_child(new NodeSetPositionCommand(volume_node, clip, QPointF(-1, 0), false)); break; } default: diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 8fb4a2bc7..0329bd741 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -385,7 +385,7 @@ void MainWindow::ProjectClose(Project *p) // Close project from NodeView if (node_panel_->GetGraph() == p) { - node_panel_->SetGraph(nullptr); + node_panel_->ClearGraph(); } } @@ -539,8 +539,6 @@ TimelinePanel* MainWindow::AppendTimelinePanel() connect(panel, &TimelinePanel::TimeChanged, param_panel_, &ParamPanel::SetTimestamp); connect(panel, &TimelinePanel::TimeChanged, table_panel_, &NodeTablePanel::SetTimestamp); connect(panel, &TimelinePanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTimestamp); - connect(panel, &TimelinePanel::BlocksSelected, node_panel_, &NodePanel::SelectBlocks); - connect(panel, &TimelinePanel::BlocksDeselected, node_panel_, &NodePanel::DeselectBlocks); connect(param_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp); connect(curve_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp); connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp); @@ -698,24 +696,24 @@ void MainWindow::UpdateAudioMonitorParams(ViewerOutput *viewer) void MainWindow::FocusedPanelChanged(PanelWidget *panel) { // Update audio monitor panel - TimeBasedPanel* tbp = dynamic_cast(panel); - if (tbp) { + if (TimeBasedPanel* tbp = dynamic_cast(panel)) { UpdateAudioMonitorParams(tbp->GetConnectedViewer()); } - // Signal timeline focus - TimelinePanel* timeline = dynamic_cast(panel); - if (timeline) { + if (TimelinePanel* timeline = dynamic_cast(panel)) { + // Signal timeline focus TimelineFocused(timeline->GetConnectedViewer()); - return; - } - // Signal project panel focus - ProjectPanel* project = dynamic_cast(panel); - if (project) { + NodeGraph *graph = timeline->GetConnectedViewer() ? timeline->GetConnectedViewer()->parent() : nullptr; + QVector n(timeline->GetSelectedBlocks().size()); + for (int j=0; jGetSelectedBlocks().at(j); + } + node_panel_->SetGraph(graph, n); + } else if (ProjectPanel* project = dynamic_cast(panel)) { + // Signal project panel focus UpdateTitle(); - node_panel_->SetGraph(project->project()); - return; + node_panel_->SetGraph(project->project(), {project->project()}); } } From f2d64728f99deef492904e2236b71c65c7455b26 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 17 May 2021 11:14:56 +1000 Subject: [PATCH 02/72] work towards timeline/nodeview interactivity --- app/panel/panelmanager.cpp | 14 +- app/panel/panelmanager.h | 19 ++- app/panel/timeline/timeline.cpp | 8 +- app/panel/timeline/timeline.h | 6 +- app/widget/nodeview/nodeview.cpp | 36 ++++- app/widget/nodeview/nodeviewedge.cpp | 139 ++++++++++--------- app/widget/nodeview/nodeviewedge.h | 6 + app/widget/nodeview/nodeviewitem.cpp | 47 ++++--- app/widget/nodeview/nodeviewitem.h | 7 + app/widget/nodeview/nodeviewscene.cpp | 10 +- app/widget/timelinewidget/timelinewidget.cpp | 13 +- app/widget/timelinewidget/timelinewidget.h | 6 +- app/widget/timelinewidget/tool/import.cpp | 6 +- app/window/mainwindow/mainwindow.cpp | 17 +++ app/window/mainwindow/mainwindow.h | 2 + 15 files changed, 204 insertions(+), 132 deletions(-) diff --git a/app/panel/panelmanager.cpp b/app/panel/panelmanager.cpp index ce06e7f9c..779c2be35 100644 --- a/app/panel/panelmanager.cpp +++ b/app/panel/panelmanager.cpp @@ -28,8 +28,7 @@ PanelManager* PanelManager::instance_ = nullptr; PanelManager::PanelManager(QObject *parent) : QObject(parent), - locked_(false), - last_focused_panel_(nullptr) + locked_(false) { } @@ -46,11 +45,11 @@ const QList &PanelManager::panels() return focus_history_; } -PanelWidget *PanelManager::CurrentlyFocused() const +PanelWidget *PanelManager::CurrentlyFocused(bool enable_hover) const { // If hover focus is enabled, find the currently hovered panel and return it (if no panel is hovered, resort to // default behavior) - if (Config::Current()["HoverFocus"].toBool()) { + if (enable_hover && Config::Current()[QStringLiteral("HoverFocus")].toBool()) { PanelWidget* hovered = CurrentlyHovered(); if (hovered != nullptr) { @@ -111,7 +110,7 @@ void PanelManager::FocusChanged(QWidget *old, QWidget *now) if (panel_cast_test) { - if (last_focused_panel_ != panel_cast_test) { + if (focus_history_.first() != panel_cast_test) { // If so, bump this to the top of the focus history int panel_index = focus_history_.indexOf(panel_cast_test); @@ -130,7 +129,6 @@ void PanelManager::FocusChanged(QWidget *old, QWidget *now) focus_history_.move(panel_index, 0); } - last_focused_panel_ = panel_cast_test; emit FocusedPanelChanged(panel_cast_test); } @@ -158,10 +156,6 @@ void PanelManager::PanelDestroyed() PanelWidget* panel = static_cast(sender()); focus_history_.removeOne(panel); - - if (last_focused_panel_ == panel) { - last_focused_panel_ = focus_history_.isEmpty() ? nullptr : focus_history_.first(); - } } } diff --git a/app/panel/panelmanager.h b/app/panel/panelmanager.h index 1f9c68dee..50dfefce6 100644 --- a/app/panel/panelmanager.h +++ b/app/panel/panelmanager.h @@ -65,14 +65,12 @@ public: /** * @brief Return the currently focused widget, or nullptr if nothing is focused * - * This result == CurrentlyFocused() if HoverFocus is true + * This result == CurrentlyFocused() if HoverFocus is true and panel is hovered */ - PanelWidget* CurrentlyFocused() const; + PanelWidget* CurrentlyFocused(bool enable_hover = true) const; /** * @brief Return the widget that the mouse is currently hovering over, or nullptr if nothing is hovered over - * - * This result == CurrentlyFocused() if HoverFocus is true */ PanelWidget* CurrentlyHovered() const; @@ -155,13 +153,6 @@ private: */ static PanelManager* instance_; - /** - * @brief The last panel that was focused - * - * Stored to prevent emitting FocusedPanelChanged() multiple times for the same panel - */ - PanelWidget* last_focused_panel_; - private slots: /** * @brief Processing if a panel gets deleted @@ -195,6 +186,12 @@ T *PanelManager::CreatePanel(QWidget *parent) // Connect destroy signal so we can remove it from focus history connect(panel, &PanelWidget::destroyed, this, &PanelManager::PanelDestroyed, Qt::DirectConnection); + if (focus_history_.size() == 1) { + // This is the first panel, focus it + panel->SetBorderVisible(true); + emit FocusedPanelChanged(panel); + } + return panel; } diff --git a/app/panel/timeline/timeline.cpp b/app/panel/timeline/timeline.cpp index 44b1f3fba..390de75cd 100644 --- a/app/panel/timeline/timeline.cpp +++ b/app/panel/timeline/timeline.cpp @@ -33,13 +33,7 @@ TimelinePanel::TimelinePanel(QWidget *parent) : Retranslate(); - connect(tw, &TimelineWidget::BlocksSelected, this, &TimelinePanel::BlocksSelected); - connect(tw, &TimelineWidget::BlocksDeselected, this, &TimelinePanel::BlocksDeselected); -} - -void TimelinePanel::Clear() -{ - static_cast(GetTimeBasedWidget())->Clear(); + connect(tw, &TimelineWidget::BlockSelectionChanged, this, &TimelinePanel::BlockSelectionChanged); } void TimelinePanel::SplitAtPlayhead() diff --git a/app/panel/timeline/timeline.h b/app/panel/timeline/timeline.h index 1b33ac9fb..5b853dce3 100644 --- a/app/panel/timeline/timeline.h +++ b/app/panel/timeline/timeline.h @@ -35,8 +35,6 @@ class TimelinePanel : public TimeBasedPanel public: TimelinePanel(QWidget* parent); - void Clear(); - void SplitAtPlayhead(); QByteArray SaveSplitterState() const; @@ -98,9 +96,7 @@ protected: virtual void Retranslate() override; signals: - void BlocksSelected(const QVector& selected_blocks); - - void BlocksDeselected(const QVector& deselected_blocks); + void BlockSelectionChanged(const QVector& selected_blocks); }; diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index fcfe6736e..dc241b79b 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -126,6 +126,17 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) item->SetNodePosition(it.value()); } } + + for (auto it=scene_.item_map().cbegin(); it!=scene_.item_map().cend(); it++) { + Node *node = it.key(); + for (auto jt=node->input_connections().cbegin(); jt!=node->input_connections().cend(); jt++) { + const NodeOutput &output = jt->second; + if (scene_.item_map().contains(output.node())) { + // Create edge since both input and output exist + scene_.AddEdge(output, jt->first); + } + } + } } } @@ -913,23 +924,26 @@ void NodeView::AddNode(Node *node) void NodeView::RemoveNode(Node *node) { - if (filter_mode_ == kFilterShowAll) { - scene_.RemoveNode(node); - } + scene_.RemoveNode(node); } void NodeView::AddEdge(const NodeOutput &output, const NodeInput &input) { if (filter_mode_ == kFilterShowAll) { scene_.AddEdge(output, input); + } else if (filter_mode_ == kFilterShowSelective) { + Node *output_node = output.node(); + Node *input_node = input.node(); + + if (scene_.item_map().contains(output_node) && scene_.item_map().contains(input_node)) { + scene_.AddEdge(output, input); + } } } void NodeView::RemoveEdge(const NodeOutput &output, const NodeInput &input) { - if (filter_mode_ == kFilterShowAll) { - scene_.RemoveEdge(output, input); - } + scene_.RemoveEdge(output, input); } void NodeView::AddNodePosition(Node *node, void *relative, const QPointF &pos) @@ -940,6 +954,16 @@ void NodeView::AddNodePosition(Node *node, void *relative, const QPointF &pos) if (!item) { item = scene_.AddNode(node); + + // Add input edges + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + AddEdge(it->second, it->first); + } + + // Add output edges + for (auto it=node->output_connections().cbegin(); it!=node->output_connections().cend(); it++) { + AddEdge(it->first, it->second); + } } item->SetNodePosition(pos); diff --git a/app/widget/nodeview/nodeviewedge.cpp b/app/widget/nodeview/nodeviewedge.cpp index 46357aa4e..e15f6125e 100644 --- a/app/widget/nodeview/nodeviewedge.cpp +++ b/app/widget/nodeview/nodeviewedge.cpp @@ -80,6 +80,82 @@ void NodeViewEdge::SetHighlighted(bool e) void NodeViewEdge::SetPoints(const QPointF &start, const QPointF &end, bool input_is_expanded) { + cached_start_ = start; + cached_end_ = end; + cached_input_is_expanded_ = input_is_expanded; + + UpdateCurve(); +} + +void NodeViewEdge::SetFlowDirection(NodeViewCommon::FlowDirection dir) +{ + flow_dir_ = dir; + + if (from_item_ && to_item_) { + Adjust(); + } +} + +void NodeViewEdge::SetCurved(bool e) +{ + curved_ = e; + + UpdateCurve(); +} + +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()); + + // Draw arrow + painter->setPen(Qt::NoPen); + painter->setBrush(edge_color); + painter->drawPolygon(arrow_); +} + +void NodeViewEdge::Init() +{ + connected_ = false; + highlighted_ = false; + flow_dir_ = NodeViewCommon::kLeftToRight; + 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; + arrow_size_ = QFontMetrics(QFont()).height() / 2; +} + +void NodeViewEdge::UpdateCurve() +{ + const QPointF &start = cached_start_; + const QPointF &end = cached_end_; + const bool input_is_expanded = cached_input_is_expanded_; + QPainterPath path; path.moveTo(start); @@ -156,67 +232,4 @@ void NodeViewEdge::SetPoints(const QPointF &start, const QPointF &end, bool inpu arrow_bounding_rect_.adjust(-arrow_size_, -arrow_size_, arrow_size_, arrow_size_); } -void NodeViewEdge::SetFlowDirection(NodeViewCommon::FlowDirection dir) -{ - flow_dir_ = dir; - - if (from_item_ && to_item_) { - Adjust(); - } -} - -void NodeViewEdge::SetCurved(bool e) -{ - curved_ = e; - - update(); -} - -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()); - - // Draw arrow - painter->setPen(Qt::NoPen); - painter->setBrush(edge_color); - painter->drawPolygon(arrow_); -} - -void NodeViewEdge::Init() -{ - connected_ = false; - highlighted_ = false; - flow_dir_ = NodeViewCommon::kLeftToRight; - 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; - arrow_size_ = QFontMetrics(QFont()).height() / 2; -} - } diff --git a/app/widget/nodeview/nodeviewedge.h b/app/widget/nodeview/nodeviewedge.h index cecdf6ccd..0f90108dc 100644 --- a/app/widget/nodeview/nodeviewedge.h +++ b/app/widget/nodeview/nodeviewedge.h @@ -122,6 +122,8 @@ protected: private: void Init(); + void UpdateCurve(); + NodeOutput output_; NodeInput input_; @@ -148,6 +150,10 @@ private: QRectF arrow_bounding_rect_; + QPointF cached_start_; + QPointF cached_end_; + bool cached_input_is_expanded_; + }; } diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index d07fbaa9d..7e43e1f6b 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -97,24 +97,9 @@ QPointF NodeViewItem::GetNodePosition() const void NodeViewItem::SetNodePosition(const QPointF &pos) { - switch (flow_dir_) { - case NodeViewCommon::kLeftToRight: - setPos(pos.x() * DefaultItemHorizontalPadding(), - pos.y() * DefaultItemVerticalPadding()); - break; - case NodeViewCommon::kRightToLeft: - setPos(-pos.x() * DefaultItemHorizontalPadding(), - pos.y() * DefaultItemVerticalPadding()); - break; - case NodeViewCommon::kTopToBottom: - setPos(pos.y() * DefaultItemHorizontalPadding(), - pos.x() * DefaultItemVerticalPadding()); - break; - case NodeViewCommon::kBottomToTop: - setPos(pos.y() * DefaultItemHorizontalPadding(), - -pos.x() * DefaultItemVerticalPadding()); - break; - } + cached_node_pos_ = pos; + + UpdateNodePosition(); } int NodeViewItem::DefaultTextPadding() @@ -466,6 +451,8 @@ QPointF NodeViewItem::GetOutputPoint(const QString& output) const void NodeViewItem::SetFlowDirection(NodeViewCommon::FlowDirection dir) { flow_dir_ = dir; + + UpdateNodePosition(); } QPointF NodeViewItem::GetInputPointInternal(int index, const QPointF& source_pos) const @@ -490,4 +477,28 @@ QPointF NodeViewItem::GetInputPointInternal(int index, const QPointF& source_pos } } +void NodeViewItem::UpdateNodePosition() +{ + const QPointF &pos = cached_node_pos_; + + switch (flow_dir_) { + case NodeViewCommon::kLeftToRight: + setPos(pos.x() * DefaultItemHorizontalPadding(), + pos.y() * DefaultItemVerticalPadding()); + break; + case NodeViewCommon::kRightToLeft: + setPos(-pos.x() * DefaultItemHorizontalPadding(), + pos.y() * DefaultItemVerticalPadding()); + break; + case NodeViewCommon::kTopToBottom: + setPos(pos.y() * DefaultItemHorizontalPadding(), + pos.x() * DefaultItemVerticalPadding()); + break; + case NodeViewCommon::kBottomToTop: + setPos(pos.y() * DefaultItemHorizontalPadding(), + -pos.x() * DefaultItemVerticalPadding()); + break; + } +} + } diff --git a/app/widget/nodeview/nodeviewitem.h b/app/widget/nodeview/nodeviewitem.h index 07703760f..1edab3aa5 100644 --- a/app/widget/nodeview/nodeviewitem.h +++ b/app/widget/nodeview/nodeviewitem.h @@ -136,6 +136,11 @@ private: */ QPointF GetInputPointInternal(int index, const QPointF &source_pos) const; + /** + * @brief Internal update function when logical position changes + */ + void UpdateNodePosition(); + /** * @brief Reference to attached Node */ @@ -167,6 +172,8 @@ private: QVector edges_; + QPointF cached_node_pos_; + }; } diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index e389b0ce4..4853c0e99 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -179,10 +179,12 @@ void NodeViewScene::AddEdge(const NodeOutput &output, const NodeInput &input) void NodeViewScene::RemoveEdge(const NodeOutput &output, const NodeInput &input) { NodeViewEdge* edge = EdgeToUIObject(output, input); - edge->from_item()->RemoveEdge(edge); - edge->to_item()->RemoveEdge(edge); - edges_.removeOne(edge); - delete edge; + if (edge) { + edge->from_item()->RemoveEdge(edge); + edge->to_item()->RemoveEdge(edge); + edges_.removeOne(edge); + delete edge; + } } int NodeViewScene::DetermineWeight(Node *n) diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index ddbed335b..c426f6893 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -871,7 +871,7 @@ void TimelineWidget::RemoveBlock(Block *block) selected_blocks_.removeAt(select_index); RemoveSelection(block); - emit BlocksDeselected({block}); + SignalBlockSelectionChange(); } } @@ -1087,6 +1087,11 @@ void TimelineWidget::SetScrollZoomsByDefaultOnAllViews(bool e) } } +void TimelineWidget::SignalBlockSelectionChange() +{ + emit BlockSelectionChanged(selected_blocks_); +} + void TimelineWidget::AddGhost(TimelineViewGhostItem *ghost) { ghost_items_.append(ghost); @@ -1168,7 +1173,7 @@ void TimelineWidget::SignalSelectedBlocks(QVector input, bool filter) selected_blocks_.append(input); - emit BlocksSelected(input); + emit SignalBlockSelectionChange(); } void TimelineWidget::SignalDeselectedBlocks(const QVector &deselected_blocks) @@ -1181,14 +1186,14 @@ void TimelineWidget::SignalDeselectedBlocks(const QVector &deselected_b selected_blocks_.removeOne(b); } - emit BlocksDeselected(deselected_blocks); + emit SignalBlockSelectionChange(); } void TimelineWidget::SignalDeselectedAllBlocks() { if (!selected_blocks_.isEmpty()) { - emit BlocksDeselected(selected_blocks_); selected_blocks_.clear(); + SignalBlockSelectionChange(); } } diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 55cf634eb..6b5ddc7d8 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -234,9 +234,7 @@ public: }; signals: - void BlocksSelected(const QVector& selected_blocks); - - void BlocksDeselected(const QVector& deselected_blocks); + void BlockSelectionChanged(const QVector& selected_blocks); protected: virtual void resizeEvent(QResizeEvent *event) override; @@ -355,6 +353,8 @@ private slots: void SetScrollZoomsByDefaultOnAllViews(bool e); + void SignalBlockSelectionChange(); + }; } diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 251e21e14..5f086fb74 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -387,8 +387,12 @@ void ImportTool::DropGhosts(bool insert) clip->set_length_and_media_out(ghost->GetLength()); clip->SetLabel(footage_stream.footage->GetLabel()); command->add_child(new NodeAddCommand(dst_graph, clip)); - command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(clip, footage_stream.footage, clip, QPointF(2, 0))); + command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(clip, footage_stream.footage, nullptr, QPointF(2, 0))); + // Position clip in its own context + command->add_child(new NodeSetPositionCommand(clip, clip, QPointF(0, 0), false)); + + // Position footage in its context command->add_child(new NodeSetPositionCommand(footage_stream.footage, clip, QPointF(-2, 0), false)); switch (Track::Reference::TypeFromString(footage_stream.output)) { diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 0329bd741..f5df542b7 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -466,6 +466,22 @@ void MainWindow::StatusBarDoubleClicked() task_man_panel_->raise(); } +void MainWindow::TimelinePanelSelectionChanged(const QVector &blocks) +{ + TimelinePanel *panel = static_cast(sender()); + + if (PanelManager::instance()->CurrentlyFocused(false) == panel) { + QVector context(blocks.size()); + for (int i=0; iGetConnectedViewer(); + + node_panel_->SetGraph(viewer ? viewer->parent() : nullptr, context); + } +} + #ifdef Q_OS_LINUX void MainWindow::ShowNouveauWarning() { @@ -539,6 +555,7 @@ TimelinePanel* MainWindow::AppendTimelinePanel() connect(panel, &TimelinePanel::TimeChanged, param_panel_, &ParamPanel::SetTimestamp); connect(panel, &TimelinePanel::TimeChanged, table_panel_, &NodeTablePanel::SetTimestamp); connect(panel, &TimelinePanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTimestamp); + connect(panel, &TimelinePanel::BlockSelectionChanged, this, &MainWindow::TimelinePanelSelectionChanged); connect(param_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp); connect(curve_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp); connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp); diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index 18c54f401..c9cf8e017 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -190,6 +190,8 @@ private slots: void ShowNouveauWarning(); #endif + void TimelinePanelSelectionChanged(const QVector &blocks); + }; } From 3c17ecc94e1252d684636cdd5bca1b2b1aa0ff92 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 21 May 2021 12:28:52 +1000 Subject: [PATCH 03/72] more work --- app/node/graph.cpp | 14 ++++++ app/node/graph.h | 2 + app/widget/nodeview/nodeview.cpp | 69 +++++++++++++++++++++------- app/window/mainwindow/mainwindow.cpp | 34 ++++++++------ app/window/mainwindow/mainwindow.h | 2 + 5 files changed, 90 insertions(+), 31 deletions(-) diff --git a/app/node/graph.cpp b/app/node/graph.cpp index 3710c5241..6029b7063 100644 --- a/app/node/graph.cpp +++ b/app/node/graph.cpp @@ -44,6 +44,20 @@ void NodeGraph::Clear() } } +qreal NodeGraph::GetNodeContextHeight(void *context) +{ + const PositionMap &map = position_map_.value(context); + + qreal top = 0, bottom = 0; + + foreach (const QPointF &pt, map) { + top = qMin(pt.y(), top); + bottom = qMax(pt.y(), bottom); + } + + return bottom - top; +} + void NodeGraph::childEvent(QChildEvent *event) { super::childEvent(event); diff --git a/app/node/graph.h b/app/node/graph.h index 74f300622..1751d49a5 100644 --- a/app/node/graph.h +++ b/app/node/graph.h @@ -89,6 +89,8 @@ public: emit NodePositionRemoved(node, relative);; } + qreal GetNodeContextHeight(void *context); + using PositionMap = QMap; const PositionMap &GetNodesForRelative(void *relative) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index dc241b79b..68ade78de 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -113,28 +113,63 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) // Handle changing nodes if (filter_nodes_ != nodes) { - DeselectAll(); - scene_.clear(); - filter_nodes_ = nodes; - foreach (void *n, filter_nodes_) { - const NodeGraph::PositionMap &map = graph_->GetNodesForRelative(n); + if (filter_mode_ == kFilterShowSelective) { + DeselectAll(); + scene_.clear(); - for (auto it=map.cbegin(); it!=map.cend(); it++) { - NodeViewItem *item = scene_.AddNode(it.key()); - item->SetNodePosition(it.value()); - } - } + QMap > averaged_positions; - for (auto it=scene_.item_map().cbegin(); it!=scene_.item_map().cend(); it++) { - Node *node = it.key(); - for (auto jt=node->input_connections().cbegin(); jt!=node->input_connections().cend(); jt++) { - const NodeOutput &output = jt->second; - if (scene_.item_map().contains(output.node())) { - // Create edge since both input and output exist - scene_.AddEdge(output, jt->first); + QPointF origin(0, 0); + + foreach (void *n, filter_nodes_) { + const NodeGraph::PositionMap &map = graph_->GetNodesForRelative(n); + + qreal top = 0, bottom = 0; + + for (auto it=map.cbegin(); it!=map.cend(); it++) { + NodeViewItem *item = scene_.item_map().value(it.key()); + + if (item) { + QVector &averages = averaged_positions[item]; + if (averages.isEmpty()) { + averages.append(item->GetNodePosition()); + } + averages.append(origin + it.value()); + } else { + item = scene_.AddNode(it.key()); + } + + const QPointF &pos = it.value(); + top = qMin(top, pos.y()); + bottom = qMax(bottom, pos.y()); + + item->SetNodePosition(origin + pos); } + + origin.setY(origin.y() + 1 + (bottom - top)); + } + + for (auto it=scene_.item_map().cbegin(); it!=scene_.item_map().cend(); it++) { + Node *node = it.key(); + for (auto jt=node->input_connections().cbegin(); jt!=node->input_connections().cend(); jt++) { + const NodeOutput &output = jt->second; + if (scene_.item_map().contains(output.node())) { + // Create edge since both input and output exist + scene_.AddEdge(output, jt->first); + } + } + } + + for (auto it=averaged_positions.cbegin(); it!=averaged_positions.cend(); it++) { + const QVector &positions = it.value(); + QPointF p; + foreach (const QPointF &pos, positions) { + p += pos; + } + p /= positions.size(); + it.key()->SetNodePosition(p); } } } diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index f5df542b7..8de1ae101 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -471,14 +471,7 @@ void MainWindow::TimelinePanelSelectionChanged(const QVector &blocks) TimelinePanel *panel = static_cast(sender()); if (PanelManager::instance()->CurrentlyFocused(false) == panel) { - QVector context(blocks.size()); - for (int i=0; iGetConnectedViewer(); - - node_panel_->SetGraph(viewer ? viewer->parent() : nullptr, context); + UpdateNodePanelContextFromTimelinePanel(panel); } } @@ -710,6 +703,24 @@ void MainWindow::UpdateAudioMonitorParams(ViewerOutput *viewer) audio_monitor_panel_->SetParams(viewer ? viewer->GetAudioParams() : AudioParams()); } +void MainWindow::UpdateNodePanelContextFromTimelinePanel(TimelinePanel *panel) +{ + // Add selected blocks (if any) + const QVector &blocks = panel->GetSelectedBlocks(); + QVector context(blocks.size()); + for (int i=0; iGetConnectedViewer(); + if (viewer && context.isEmpty()) { + context.append(viewer); + } + + node_panel_->SetGraph(viewer ? viewer->parent() : nullptr, context); +} + void MainWindow::FocusedPanelChanged(PanelWidget *panel) { // Update audio monitor panel @@ -721,12 +732,7 @@ void MainWindow::FocusedPanelChanged(PanelWidget *panel) // Signal timeline focus TimelineFocused(timeline->GetConnectedViewer()); - NodeGraph *graph = timeline->GetConnectedViewer() ? timeline->GetConnectedViewer()->parent() : nullptr; - QVector n(timeline->GetSelectedBlocks().size()); - for (int j=0; jGetSelectedBlocks().at(j); - } - node_panel_->SetGraph(graph, n); + UpdateNodePanelContextFromTimelinePanel(timeline); } else if (ProjectPanel* project = dynamic_cast(panel)) { // Signal project panel focus UpdateTitle(); diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index c9cf8e017..fdead62a9 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -142,6 +142,8 @@ private: void UpdateAudioMonitorParams(ViewerOutput* viewer); + void UpdateNodePanelContextFromTimelinePanel(TimelinePanel *panel); + QByteArray premaximized_state_; // Standard panels From f9309da4b375f7d6cfce6ddd548bef63d7127d0f Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 24 May 2021 10:12:53 +1000 Subject: [PATCH 04/72] nodeview: update positions correctly --- app/widget/nodeview/nodeview.cpp | 192 +++++++++++++++----------- app/widget/nodeview/nodeview.h | 9 ++ app/widget/nodeview/nodeviewitem.cpp | 11 +- app/widget/nodeview/nodeviewitem.h | 8 +- app/widget/nodeview/nodeviewscene.cpp | 7 + app/widget/nodeview/nodeviewscene.h | 5 + 6 files changed, 149 insertions(+), 83 deletions(-) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 68ade78de..9f72a4246 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -20,6 +20,7 @@ #include "nodeview.h" +#include #include #include #include @@ -56,6 +57,7 @@ NodeView::NodeView(QWidget *parent) : setViewportUpdateMode(FullViewportUpdate); connect(this, &NodeView::customContextMenuRequested, this, &NodeView::ShowContextMenu); + connect(&scene_, &NodeViewScene::NodePositionChanged, this, &NodeView::NodePositionChanged); ConnectSelectionChangedSignal(); @@ -70,106 +72,120 @@ NodeView::~NodeView() void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) { - // Handle potentially changing graph - if (graph_ != graph) { - if (graph_) { - disconnect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); - disconnect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); - disconnect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); - disconnect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); - disconnect(graph_, &NodeGraph::NodePositionAdded, this, &NodeView::AddNodePosition); - disconnect(graph_, &NodeGraph::NodePositionRemoved, this, &NodeView::RemoveNodePosition); + bool graph_changed = graph_ != graph; + bool context_changed = filter_nodes_ != nodes; - if (filter_mode_ == kFilterShowAll) { - // Switching graphs, close all nodes - DeselectAll(); - scene_.clear(); - } - } + if (graph_changed || context_changed) { + // Clear nodes if necessary + bool refresh_required = (graph_changed && filter_mode_ == kFilterShowAll) + || (context_changed && filter_mode_ == kFilterShowSelective); + bool nodes_visible = (graph && filter_mode_ == kFilterShowAll) + || (!nodes.isEmpty() && filter_mode_ == kFilterShowSelective); - graph_ = graph; - - if (graph_) { - connect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); - connect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); - connect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); - connect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); - connect(graph_, &NodeGraph::NodePositionAdded, this, &NodeView::AddNodePosition); - connect(graph_, &NodeGraph::NodePositionRemoved, this, &NodeView::RemoveNodePosition); - - if (filter_mode_ == kFilterShowAll) { - foreach (Node* n, graph_->nodes()) { - scene_.AddNode(n); - } - - foreach (Node* n, graph_->nodes()) { - for (auto it=n->input_connections().cbegin(); it!=n->input_connections().cend(); it++) { - scene_.AddEdge(it->second, it->first); - } - } - } - } - } - - // Handle changing nodes - if (filter_nodes_ != nodes) { - filter_nodes_ = nodes; - - if (filter_mode_ == kFilterShowSelective) { + if (refresh_required) { DeselectAll(); + positions_.clear(); scene_.clear(); + } + + // Handle graph change + if (graph_changed) { + if (graph_) { + // Disconnect from current graph + disconnect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); + disconnect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); + disconnect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); + disconnect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); + disconnect(graph_, &NodeGraph::NodePositionAdded, this, &NodeView::AddNodePosition); + disconnect(graph_, &NodeGraph::NodePositionRemoved, this, &NodeView::RemoveNodePosition); + } + + graph_ = graph; + + if (graph_) { + // Connect to new graph + connect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); + connect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); + connect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); + connect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); + connect(graph_, &NodeGraph::NodePositionAdded, this, &NodeView::AddNodePosition); + connect(graph_, &NodeGraph::NodePositionRemoved, this, &NodeView::RemoveNodePosition); + } + } + + if (context_changed) { + filter_nodes_ = nodes; + } + + if (refresh_required && nodes_visible) { QMap > averaged_positions; QPointF origin(0, 0); - foreach (void *n, filter_nodes_) { - const NodeGraph::PositionMap &map = graph_->GetNodesForRelative(n); + if (filter_mode_ == kFilterShowAll) { + // FIXME: Implement + } else { + // Reserve an arbitrary number to reduce the amount of reallocations + foreach (void *n, filter_nodes_) { + const NodeGraph::PositionMap &map = graph_->GetNodesForRelative(n); - qreal top = 0, bottom = 0; + qreal top = 0, bottom = 0; - for (auto it=map.cbegin(); it!=map.cend(); it++) { - NodeViewItem *item = scene_.item_map().value(it.key()); + for (auto it=map.cbegin(); it!=map.cend(); it++) { + // Determine position + NodeViewItem *item = scene_.item_map().value(it.key()); - if (item) { - QVector &averages = averaged_positions[item]; - if (averages.isEmpty()) { - averages.append(item->GetNodePosition()); + if (item) { + QVector &averages = averaged_positions[item]; + if (averages.isEmpty()) { + averages.append(item->GetNodePosition()); + } + averages.append(origin + it.value()); + } else { + item = scene_.AddNode(it.key()); } - averages.append(origin + it.value()); - } else { - item = scene_.AddNode(it.key()); + + const QPointF &pos = it.value(); + top = qMin(top, pos.y()); + bottom = qMax(bottom, pos.y()); + + item->SetNodePosition(origin + pos); } - const QPointF &pos = it.value(); - top = qMin(top, pos.y()); - bottom = qMax(bottom, pos.y()); - - item->SetNodePosition(origin + pos); + origin.setY(origin.y() + 1 + (bottom - top)); } - origin.setY(origin.y() + 1 + (bottom - top)); - } + for (auto it=averaged_positions.cbegin(); it!=averaged_positions.cend(); it++) { + const QVector &positions = it.value(); + double x = DBL_MAX; + double y = 0.0; - for (auto it=scene_.item_map().cbegin(); it!=scene_.item_map().cend(); it++) { - Node *node = it.key(); - for (auto jt=node->input_connections().cbegin(); jt!=node->input_connections().cend(); jt++) { - const NodeOutput &output = jt->second; - if (scene_.item_map().contains(output.node())) { - // Create edge since both input and output exist - scene_.AddEdge(output, jt->first); + // Min the X value and average the Y values + foreach (const QPointF &pos, positions) { + x = qMin(x, pos.x()); + y += pos.y(); } - } - } - for (auto it=averaged_positions.cbegin(); it!=averaged_positions.cend(); it++) { - const QVector &positions = it.value(); - QPointF p; - foreach (const QPointF &pos, positions) { - p += pos; + y /= positions.size(); + + it.key()->SetNodePosition(QPointF(x, y)); + } + + for (auto it=scene_.item_map().cbegin(); it!=scene_.item_map().cend(); it++) { + // Add edge objects + Node *node = it.key(); + for (auto jt=node->input_connections().cbegin(); jt!=node->input_connections().cend(); jt++) { + const NodeOutput &output = jt->second; + if (scene_.item_map().contains(output.node())) { + // Create edge since both input and output exist + scene_.AddEdge(output, jt->first); + } + } + + // Store view position + positions_.insert(it.value(), {it.key(), it.value()->GetNodePosition()}); } - p /= positions.size(); - it.key()->SetNodePosition(p); } } } @@ -983,7 +999,7 @@ void NodeView::RemoveEdge(const NodeOutput &output, const NodeInput &input) void NodeView::AddNodePosition(Node *node, void *relative, const QPointF &pos) { - if (filter_mode_ == kFilterShowSelective) { + /*if (filter_mode_ == kFilterShowSelective) { if (filter_nodes_.contains(relative)) { NodeViewItem *item = scene_.item_map().value(node); @@ -1003,7 +1019,7 @@ void NodeView::AddNodePosition(Node *node, void *relative, const QPointF &pos) item->SetNodePosition(pos); } - } + }*/ } void NodeView::RemoveNodePosition(Node *node, void *relative) @@ -1016,6 +1032,20 @@ void NodeView::RemoveNodePosition(Node *node, void *relative) } } +void NodeView::NodePositionChanged(NodeViewItem *item, const QPointF &pos) +{ + Position &original_pos = positions_[item]; + Node *node = original_pos.node; + QPointF diff = pos - original_pos.original_item_pos; + original_pos.original_item_pos = pos; + + foreach (void *context, filter_nodes_) { + QPointF p = graph_->GetNodePosition(node, context); + p += diff; + graph_->SetNodePosition(node, context, p); + } +} + void NodeView::AttachNodesToCursor(const QVector &nodes) { QVector items(nodes.size()); diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 05b0ca8a4..b832095c7 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -155,6 +155,13 @@ private: kFilterShowSelective }; + struct Position { + Node *node; + QPointF original_item_pos; + }; + + QMap positions_; + FilterMode filter_mode_; QVector filter_nodes_; @@ -209,6 +216,8 @@ private slots: void AddNodePosition(Node *node, void *relative, const QPointF &pos); void RemoveNodePosition(Node *node, void *relative); + void NodePositionChanged(NodeViewItem *item, const QPointF &pos); + }; } diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index 7e43e1f6b..31ad78cf7 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -45,7 +45,8 @@ NodeViewItem::NodeViewItem(QGraphicsItem *parent) : expanded_(false), hide_titlebar_(false), highlighted_index_(-1), - flow_dir_(NodeViewCommon::kLeftToRight) + flow_dir_(NodeViewCommon::kLeftToRight), + dont_signal_(false) { // Set flags for this widget setFlag(QGraphicsItem::ItemIsMovable); @@ -336,6 +337,10 @@ QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, cons { if (change == ItemPositionHasChanged && node_) { ReadjustAllEdges(); + + if (!dont_signal_) { + emit NodePositionChanged(GetNodePosition()); + } } return QGraphicsItem::itemChange(change, value); @@ -479,6 +484,8 @@ QPointF NodeViewItem::GetInputPointInternal(int index, const QPointF& source_pos void NodeViewItem::UpdateNodePosition() { + dont_signal_ = true; + const QPointF &pos = cached_node_pos_; switch (flow_dir_) { @@ -499,6 +506,8 @@ void NodeViewItem::UpdateNodePosition() -pos.x() * DefaultItemVerticalPadding()); break; } + + dont_signal_ = false; } } diff --git a/app/widget/nodeview/nodeviewitem.h b/app/widget/nodeview/nodeviewitem.h index 1edab3aa5..854de5e6c 100644 --- a/app/widget/nodeview/nodeviewitem.h +++ b/app/widget/nodeview/nodeviewitem.h @@ -40,8 +40,9 @@ class NodeViewEdge; * * To retrieve the NodeViewItem for a certain Node, use NodeView::NodeToUIObject(). */ -class NodeViewItem : public QGraphicsRectItem +class NodeViewItem : public QObject, public QGraphicsRectItem { + Q_OBJECT public: NodeViewItem(QGraphicsItem* parent = nullptr); @@ -111,6 +112,9 @@ public: void SetHighlightedIndex(int index); +signals: + void NodePositionChanged(const QPointF &pos); + protected: virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; @@ -174,6 +178,8 @@ private: QPointF cached_node_pos_; + bool dont_signal_; + }; } diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index 4853c0e99..057a9c01b 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -159,6 +159,7 @@ NodeViewItem* NodeViewScene::AddNode(Node* node) connect(node, &Node::LabelChanged, this, &NodeViewScene::NodeAppearanceChanged); connect(node, &Node::ColorChanged, this, &NodeViewScene::NodeAppearanceChanged); + connect(item, &NodeViewItem::NodePositionChanged, this, &NodeViewScene::NodeItemPositionChanged); return item; } @@ -278,4 +279,10 @@ void NodeViewScene::NodeAppearanceChanged() item_map_.value(static_cast(sender()))->update(); } +void NodeViewScene::NodeItemPositionChanged(const QPointF &pos) +{ + NodeViewItem *item = static_cast(sender()); + emit NodePositionChanged(item, pos); +} + } diff --git a/app/widget/nodeview/nodeviewscene.h b/app/widget/nodeview/nodeviewscene.h index 272c592ce..267807812 100644 --- a/app/widget/nodeview/nodeviewscene.h +++ b/app/widget/nodeview/nodeviewscene.h @@ -106,6 +106,9 @@ public slots: */ void SetEdgesAreCurved(bool curved); +signals: + void NodePositionChanged(NodeViewItem *node, const QPointF &pos); + private: static int DetermineWeight(Node* n); @@ -127,6 +130,8 @@ private slots: */ void NodeAppearanceChanged(); + void NodeItemPositionChanged(const QPointF &pos); + }; } From edd52dd811dfe277bee54c17bfe0e648a17f5dff Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 26 May 2021 14:22:10 +1000 Subject: [PATCH 05/72] projectpanel: select nodes when items are selected in the project explorer --- app/panel/project/project.cpp | 1 + app/panel/project/project.h | 2 ++ .../projectexplorer/projectexplorer.cpp | 23 +++++++++++++++++++ app/widget/projectexplorer/projectexplorer.h | 4 ++++ app/window/mainwindow/mainwindow.cpp | 10 ++++++++ app/window/mainwindow/mainwindow.h | 2 ++ 6 files changed, 42 insertions(+) diff --git a/app/panel/project/project.cpp b/app/panel/project/project.cpp index e0f6fcfa3..dd0b2ced9 100644 --- a/app/panel/project/project.cpp +++ b/app/panel/project/project.cpp @@ -57,6 +57,7 @@ ProjectPanel::ProjectPanel(QWidget *parent) : explorer_ = new ProjectExplorer(this); layout->addWidget(explorer_); connect(explorer_, &ProjectExplorer::DoubleClickedItem, this, &ProjectPanel::ItemDoubleClickSlot); + connect(explorer_, &ProjectExplorer::SelectionChanged, this, &ProjectPanel::SelectionChanged); // Set toolbar's view to the explorer's view toolbar->SetView(explorer_->view_type()); diff --git a/app/panel/project/project.h b/app/panel/project/project.h index 2fb36ce8c..bfa23c455 100644 --- a/app/panel/project/project.h +++ b/app/panel/project/project.h @@ -66,6 +66,8 @@ public slots: signals: void ProjectNameChanged(); + void SelectionChanged(const QVector &selected); + private: virtual void Retranslate() override; diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 49720bb0a..d653384f5 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -134,6 +134,7 @@ void ProjectExplorer::AddView(QAbstractItemView *view) view->setEditTriggers(QAbstractItemView::NoEditTriggers); connect(view, &QAbstractItemView::clicked, this, &ProjectExplorer::ItemClickedSlot); connect(view, &QAbstractItemView::doubleClicked, this, &ProjectExplorer::ItemDoubleClickedSlot); + connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ProjectExplorer::ViewSelectionChanged); connect(view, SIGNAL(DoubleClickedEmptyArea()), this, SLOT(ViewEmptyAreaDoubleClickedSlot())); stacked_widget_->addWidget(view); } @@ -509,6 +510,28 @@ void ProjectExplorer::ContextMenuStartProxy(QAction *a) } } +void ProjectExplorer::ViewSelectionChanged() +{ + 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 SelectionChanged(nodes); +} + Project *ProjectExplorer::project() const { return model_.project(); diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index c9e7caf61..83010ea94 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -100,6 +100,8 @@ signals: */ void DoubleClickedItem(Node* item); + void SelectionChanged(const QVector &selected); + private: /** * @brief Get all the blocks that solely rely on an input node @@ -185,6 +187,8 @@ private slots: void ContextMenuStartProxy(QAction* a); + void ViewSelectionChanged(); + }; } diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 8de1ae101..49f109fc0 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -475,6 +475,15 @@ void MainWindow::TimelinePanelSelectionChanged(const QVector &blocks) } } +void MainWindow::ProjectPanelSelectionChanged(const QVector &nodes) +{ + ProjectPanel *panel = static_cast(sender()); + + if (PanelManager::instance()->CurrentlyFocused(false) == panel) { + node_panel_->Select(nodes); + } +} + #ifdef Q_OS_LINUX void MainWindow::ShowNouveauWarning() { @@ -564,6 +573,7 @@ ProjectPanel *MainWindow::AppendProjectPanel() connect(panel, &PanelWidget::CloseRequested, this, &MainWindow::ProjectCloseRequested); connect(panel, &ProjectPanel::ProjectNameChanged, this, &MainWindow::UpdateTitle); + connect(panel, &ProjectPanel::SelectionChanged, this, &MainWindow::ProjectPanelSelectionChanged); return panel; } diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index fdead62a9..0e292571c 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -194,6 +194,8 @@ private slots: void TimelinePanelSelectionChanged(const QVector &blocks); + void ProjectPanelSelectionChanged(const QVector &nodes); + }; } From c9305b93686cecb2d2bde7bcd0f7583b1a04b04a Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 26 May 2021 14:25:05 +1000 Subject: [PATCH 06/72] nodeview: improved average position algorithm --- app/widget/nodeview/nodeview.cpp | 166 ++++++++----------------------- app/widget/nodeview/nodeview.h | 3 +- 2 files changed, 43 insertions(+), 126 deletions(-) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 9f72a4246..ac037f2b2 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -119,72 +119,33 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) if (refresh_required && nodes_visible) { - QMap > averaged_positions; - - QPointF origin(0, 0); - if (filter_mode_ == kFilterShowAll) { // FIXME: Implement } else { // Reserve an arbitrary number to reduce the amount of reallocations + qreal last_offset = 0; + int additional_spacing = 0; + foreach (void *n, filter_nodes_) { const NodeGraph::PositionMap &map = graph_->GetNodesForRelative(n); - qreal top = 0, bottom = 0; - + // First determine the total "height" of this graph and how much we need to offset it + qreal top = 0; + qreal bottom = 0; for (auto it=map.cbegin(); it!=map.cend(); it++) { - // Determine position - NodeViewItem *item = scene_.item_map().value(it.key()); - - if (item) { - QVector &averages = averaged_positions[item]; - if (averages.isEmpty()) { - averages.append(item->GetNodePosition()); - } - averages.append(origin + it.value()); - } else { - item = scene_.AddNode(it.key()); - } - - const QPointF &pos = it.value(); - top = qMin(top, pos.y()); - bottom = qMax(bottom, pos.y()); - - item->SetNodePosition(origin + pos); + const QPointF &node_pos_in_context = it.value(); + top = qMin(node_pos_in_context.y(), top); + bottom = qMax(node_pos_in_context.y(), bottom); } - origin.setY(origin.y() + 1 + (bottom - top)); - } + last_offset += (additional_spacing + (bottom - top)); + additional_spacing = 1; + context_offsets_.insert(n, QPointF(0, last_offset)); - for (auto it=averaged_positions.cbegin(); it!=averaged_positions.cend(); it++) { - const QVector &positions = it.value(); - double x = DBL_MAX; - double y = 0.0; - - // Min the X value and average the Y values - foreach (const QPointF &pos, positions) { - x = qMin(x, pos.x()); - y += pos.y(); + // Finally add all nodes + for (auto it=map.cbegin(); it!=map.cend(); it++) { + AddNodePosition(it.key(), n); } - - y /= positions.size(); - - it.key()->SetNodePosition(QPointF(x, y)); - } - - for (auto it=scene_.item_map().cbegin(); it!=scene_.item_map().cend(); it++) { - // Add edge objects - Node *node = it.key(); - for (auto jt=node->input_connections().cbegin(); jt!=node->input_connections().cend(); jt++) { - const NodeOutput &output = jt->second; - if (scene_.item_map().contains(output.node())) { - // Create edge since both input and output exist - scene_.AddEdge(output, jt->first); - } - } - - // Store view position - positions_.insert(it.value(), {it.key(), it.value()->GetNodePosition()}); } } } @@ -430,59 +391,6 @@ void NodeView::ZoomOut() ZoomFromKeyboard(0.8); } -/*void NodeView::AddNodesToFilter(const QVector &nodes) -{ - // Determine new nodes - QVector multiple_sources; - - foreach (Node* node, nodes) { - // Node is new and being added - scene_.AddNode(node); - - QList visible = graph_->GetNodesForRelative(node); - foreach (Node* v, visible) { - if (scene_.NodeToUIObject(v)) { - multiple_sources.append(v); - } else { - NodeViewItem* item = scene_.AddNode(v); - item->SetNodePosition(graph_->GetNodePosition(v, node)); - } - } - } - - filter_nodes_.append(nodes); -} - -void NodeView::RemoveNodesFromFilter(const QVector &nodes) -{ - // Determine old nodes - foreach (Node* node, nodes) { - // Node is old and being removed - QList visible = graph_->GetNodesForRelative(node); - foreach (Node* v, visible) { - bool found = false; - - foreach (Node* n, filter_nodes_) { - if (node != n) { - QList other_deps = graph_->GetNodesForRelative(n); - - if (other_deps.contains(v)) { - found = true; - break; - } - } - } - - if (!found) { - scene_.RemoveNode(v); - } - } - - scene_.RemoveNode(node); - filter_nodes_.removeOne(node); - } -}*/ - void NodeView::keyPressEvent(QKeyEvent *event) { super::keyPressEvent(event); @@ -997,29 +905,35 @@ void NodeView::RemoveEdge(const NodeOutput &output, const NodeInput &input) scene_.RemoveEdge(output, input); } -void NodeView::AddNodePosition(Node *node, void *relative, const QPointF &pos) +void NodeView::AddNodePosition(Node *node, void *relative) { - /*if (filter_mode_ == kFilterShowSelective) { + if (filter_mode_ == kFilterShowSelective) { if (filter_nodes_.contains(relative)) { + // Get UI item or create if it doesn't exist NodeViewItem *item = scene_.item_map().value(node); - if (!item) { item = scene_.AddNode(node); - - // Add input edges - for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { - AddEdge(it->second, it->first); - } - - // Add output edges - for (auto it=node->output_connections().cbegin(); it!=node->output_connections().cend(); it++) { - AddEdge(it->first, it->second); - } } - item->SetNodePosition(pos); + // Determine "view" position by averaging the Y value and "min"ing the X value of all contexts + QPointF item_pos(DBL_MAX, 0.0); + int average_count = 0; + foreach (void *context, filter_nodes_) { + if (graph_->GetNodesForRelative(context).contains(node)) { + QPointF this_context_pos = graph_->GetNodePosition(node, context); + this_context_pos += context_offsets_.value(context); + item_pos.setX(qMin(item_pos.x(), this_context_pos.x())); + item_pos.setY(item_pos.y() + this_context_pos.y()); + average_count++; + } + } + item_pos.setY(item_pos.y() / average_count); + + // Set position + item->SetNodePosition(item_pos); + positions_.insert(item, {node, item_pos}); } - }*/ + } } void NodeView::RemoveNodePosition(Node *node, void *relative) @@ -1040,9 +954,11 @@ void NodeView::NodePositionChanged(NodeViewItem *item, const QPointF &pos) original_pos.original_item_pos = pos; foreach (void *context, filter_nodes_) { - QPointF p = graph_->GetNodePosition(node, context); - p += diff; - graph_->SetNodePosition(node, context, p); + if (graph_->GetNodesForRelative(context).contains(node)) { + QPointF p = graph_->GetNodePosition(node, context); + p += diff; + graph_->SetNodePosition(node, context, p); + } } } diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index b832095c7..5cf9f8502 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -165,6 +165,7 @@ private: FilterMode filter_mode_; QVector filter_nodes_; + QMap context_offsets_; double scale_; @@ -213,7 +214,7 @@ private slots: void AddEdge(const NodeOutput& output, const NodeInput& input); void RemoveEdge(const NodeOutput& output, const NodeInput& input); - void AddNodePosition(Node *node, void *relative, const QPointF &pos); + void AddNodePosition(Node *node, void *relative); void RemoveNodePosition(Node *node, void *relative); void NodePositionChanged(NodeViewItem *item, const QPointF &pos); From f87624044340a56b20bef34b50bb52fc719772e8 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 27 May 2021 13:32:32 +1000 Subject: [PATCH 07/72] use nodes as context rather than void --- app/node/graph.cpp | 2 +- app/node/graph.h | 34 ++++++++++++++-------------- app/node/node.h | 16 ++++++------- app/node/project/folder/folder.cpp | 2 +- app/node/project/project.cpp | 18 +++++++-------- app/panel/node/node.h | 2 +- app/widget/nodeview/nodeview.cpp | 22 +++++++++--------- app/widget/nodeview/nodeview.h | 10 ++++---- app/window/mainwindow/mainwindow.cpp | 6 +++-- 9 files changed, 57 insertions(+), 55 deletions(-) diff --git a/app/node/graph.cpp b/app/node/graph.cpp index 6029b7063..0527b4d16 100644 --- a/app/node/graph.cpp +++ b/app/node/graph.cpp @@ -44,7 +44,7 @@ void NodeGraph::Clear() } } -qreal NodeGraph::GetNodeContextHeight(void *context) +qreal NodeGraph::GetNodeContextHeight(Node *context) { const PositionMap &map = position_map_.value(context); diff --git a/app/node/graph.h b/app/node/graph.h index 1751d49a5..5ebe8d5fa 100644 --- a/app/node/graph.h +++ b/app/node/graph.h @@ -63,39 +63,39 @@ public: return default_nodes_; } - bool NodeMapContainsNode(Node* node, void* relative) const + bool NodeMapContainsNode(Node* node, Node* context) const { - return position_map_.value(relative).contains(node); + return position_map_.value(context).contains(node); } - QPointF GetNodePosition(Node* node, void* relative) + QPointF GetNodePosition(Node* node, Node* context) { - return position_map_.value(relative).value(node); + return position_map_.value(context).value(node); } - void SetNodePosition(Node* node, void* relative, const QPointF& pos) + void SetNodePosition(Node* node, Node* context, const QPointF& pos) { - position_map_[relative].insert(node, pos); - emit NodePositionAdded(node, relative, pos); + position_map_[context].insert(node, pos); + emit NodePositionAdded(node, context, pos); } - void RemoveNodePosition(Node* node, void* relative) + void RemoveNodePosition(Node* node, Node* context) { - PositionMap& map = position_map_[relative]; + PositionMap& map = position_map_[context]; map.remove(node); if (map.isEmpty()) { - position_map_.remove(relative); + position_map_.remove(context); } - emit NodePositionRemoved(node, relative);; + emit NodePositionRemoved(node, context); } - qreal GetNodeContextHeight(void *context); + qreal GetNodeContextHeight(Node *context); using PositionMap = QMap; - const PositionMap &GetNodesForRelative(void *relative) + const PositionMap &GetNodesForContext(Node *context) { - return position_map_[relative]; + return position_map_[context]; } signals: @@ -115,9 +115,9 @@ signals: void ValueChanged(const NodeInput& input); - void NodePositionAdded(Node *node, void *relative, const QPointF &position); + void NodePositionAdded(Node *node, Node *relative, const QPointF &position); - void NodePositionRemoved(Node *node, void *relative); + void NodePositionRemoved(Node *node, Node *relative); protected: void AddDefaultNode(Node* n) @@ -132,7 +132,7 @@ private: QVector default_nodes_; - QMap position_map_; + QMap position_map_; PositionMap root_position_map_; diff --git a/app/node/node.h b/app/node/node.h index 914761420..44c384fb6 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -1303,7 +1303,7 @@ using NodePtr = std::shared_ptr; class NodeSetPositionCommand : public UndoCommand { public: - NodeSetPositionCommand(Node* node, void* relevant, const QPointF& pos, bool move_dependencies_relatively) + NodeSetPositionCommand(Node* node, Node* relevant, const QPointF& pos, bool move_dependencies_relatively) { node_ = node; relevant_ = relevant; @@ -1322,7 +1322,7 @@ public: private: Node* node_; - void* relevant_; + Node* relevant_; QPointF pos_; QPointF old_pos_; bool added_; @@ -1333,7 +1333,7 @@ private: class NodeSetPositionAndShiftSurroundingsCommand : public UndoCommand { public: - NodeSetPositionAndShiftSurroundingsCommand(Node* node, void *relative, const QPointF& pos, bool move_dependencies_relatively) : + NodeSetPositionAndShiftSurroundingsCommand(Node* node, Node *relative, const QPointF& pos, bool move_dependencies_relatively) : node_(node), relative_(relative), position_(pos), @@ -1362,7 +1362,7 @@ public: private: Node* node_; - void *relative_; + Node *relative_; QPointF position_; @@ -1375,7 +1375,7 @@ private: class NodeSetPositionAsChildCommand : public UndoCommand { public: - NodeSetPositionAsChildCommand(Node* node, Node* parent, void *relative, int this_index, int child_count, bool shift_surroundings) : + NodeSetPositionAsChildCommand(Node* node, Node* parent, Node *relative, int this_index, int child_count, bool shift_surroundings) : node_(node), parent_(parent), relative_(relative), @@ -1406,7 +1406,7 @@ public: private: Node* node_; Node* parent_; - void *relative_; + Node *relative_; int this_index_; int child_count_; @@ -1420,7 +1420,7 @@ private: class NodeSetPositionToOffsetOfAnotherNodeCommand : public UndoCommand { public: - NodeSetPositionToOffsetOfAnotherNodeCommand(Node* node, Node* other_node, void *relative, const QPointF& offset) : + NodeSetPositionToOffsetOfAnotherNodeCommand(Node* node, Node* other_node, Node *relative, const QPointF& offset) : node_(node), other_node_(other_node), relative_(relative), @@ -1439,7 +1439,7 @@ public: private: Node* node_; Node* other_node_; - void *relative_; + Node *relative_; QPointF offset_; QPointF old_pos_; diff --git a/app/node/project/folder/folder.cpp b/app/node/project/folder/folder.cpp index 576d8b829..336feb6bc 100644 --- a/app/node/project/folder/folder.cpp +++ b/app/node/project/folder/folder.cpp @@ -139,7 +139,7 @@ void FolderAddChild::redo() if (autoposition_) { if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(child_, folder_, folder_->project(), array_index, array_index+1, true); + position_command_ = new NodeSetPositionAsChildCommand(child_, folder_, folder_->project()->root(), array_index, array_index+1, true); } position_command_->redo(); } diff --git a/app/node/project/project.cpp b/app/node/project/project.cpp index 35a8b485b..a3f4aca9e 100644 --- a/app/node/project/project.cpp +++ b/app/node/project/project.cpp @@ -39,27 +39,27 @@ Project::Project() : // Generate UUID for this project RegenerateUuid(); + // Folder root for project + root_ = new Folder(); + root_->setParent(this); + root_->SetLabel(tr("Root")); + root_->SetCanBeDeleted(false); + SetNodePosition(root_, root_, QPointF(0, 0)); + // Adds a color manager "node" to this project so that it synchronizes color_manager_ = new ColorManager(); color_manager_->setParent(this); - SetNodePosition(color_manager_, this, QPointF(1, 0)); + SetNodePosition(color_manager_, root_, QPointF(1, 0)); color_manager_->SetCanBeDeleted(false); AddDefaultNode(color_manager_); // Same with project settings settings_ = new ProjectSettingsNode(); settings_->setParent(this); - SetNodePosition(settings_, this, QPointF(2, 0)); + SetNodePosition(settings_, root_, QPointF(2, 0)); settings_->SetCanBeDeleted(false); AddDefaultNode(settings_); - // Folder root for project - root_ = new Folder(); - root_->setParent(this); - root_->SetLabel(tr("Root")); - root_->SetCanBeDeleted(false); - SetNodePosition(root_, this, QPointF(0, 0)); - connect(color_manager(), &ColorManager::ValueChanged, this, &Project::ColorManagerValueChanged); } diff --git a/app/panel/node/node.h b/app/panel/node/node.h index 02600ec65..c465bf725 100644 --- a/app/panel/node/node.h +++ b/app/panel/node/node.h @@ -40,7 +40,7 @@ public: return node_view_->GetGraph(); } - void SetGraph(NodeGraph *graph, const QVector &nodes) + void SetGraph(NodeGraph *graph, const QVector &nodes) { node_view_->SetGraph(graph, nodes); } diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index ac037f2b2..e8e07eac4 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -70,7 +70,7 @@ NodeView::~NodeView() ClearGraph(); } -void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) +void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) { bool graph_changed = graph_ != graph; bool context_changed = filter_nodes_ != nodes; @@ -126,8 +126,8 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) qreal last_offset = 0; int additional_spacing = 0; - foreach (void *n, filter_nodes_) { - const NodeGraph::PositionMap &map = graph_->GetNodesForRelative(n); + foreach (Node *n, filter_nodes_) { + const NodeGraph::PositionMap &map = graph_->GetNodesForContext(n); // First determine the total "height" of this graph and how much we need to offset it qreal top = 0; @@ -154,7 +154,7 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) void NodeView::ClearGraph() { - SetGraph(nullptr, QVector()); + SetGraph(nullptr, QVector()); } void NodeView::DeleteSelected() @@ -851,7 +851,7 @@ void NodeView::ContextMenuFilterChanged(QAction *action) if (filter_mode_ != mode) { // Store temporary graph variables NodeGraph *graph = graph_; - QVector nodes = filter_nodes_; + QVector nodes = filter_nodes_; // Unset graph with current filter mode ClearGraph(); @@ -905,7 +905,7 @@ void NodeView::RemoveEdge(const NodeOutput &output, const NodeInput &input) scene_.RemoveEdge(output, input); } -void NodeView::AddNodePosition(Node *node, void *relative) +void NodeView::AddNodePosition(Node *node, Node *relative) { if (filter_mode_ == kFilterShowSelective) { if (filter_nodes_.contains(relative)) { @@ -918,8 +918,8 @@ void NodeView::AddNodePosition(Node *node, void *relative) // Determine "view" position by averaging the Y value and "min"ing the X value of all contexts QPointF item_pos(DBL_MAX, 0.0); int average_count = 0; - foreach (void *context, filter_nodes_) { - if (graph_->GetNodesForRelative(context).contains(node)) { + foreach (Node *context, filter_nodes_) { + if (graph_->GetNodesForContext(context).contains(node)) { QPointF this_context_pos = graph_->GetNodePosition(node, context); this_context_pos += context_offsets_.value(context); item_pos.setX(qMin(item_pos.x(), this_context_pos.x())); @@ -936,7 +936,7 @@ void NodeView::AddNodePosition(Node *node, void *relative) } } -void NodeView::RemoveNodePosition(Node *node, void *relative) +void NodeView::RemoveNodePosition(Node *node, Node *relative) { if (filter_mode_ == kFilterShowSelective) { if (filter_nodes_.contains(relative)) { @@ -953,8 +953,8 @@ void NodeView::NodePositionChanged(NodeViewItem *item, const QPointF &pos) QPointF diff = pos - original_pos.original_item_pos; original_pos.original_item_pos = pos; - foreach (void *context, filter_nodes_) { - if (graph_->GetNodesForRelative(context).contains(node)) { + foreach (Node *context, filter_nodes_) { + if (graph_->GetNodesForContext(context).contains(node)) { QPointF p = graph_->GetNodePosition(node, context); p += diff; graph_->SetNodePosition(node, context, p); diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 5cf9f8502..c07536f2c 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -51,7 +51,7 @@ public: return graph_; } - void SetGraph(NodeGraph *graph, const QVector &nodes); + void SetGraph(NodeGraph *graph, const QVector &nodes); void ClearGraph(); @@ -164,8 +164,8 @@ private: FilterMode filter_mode_; - QVector filter_nodes_; - QMap context_offsets_; + QVector filter_nodes_; + QMap context_offsets_; double scale_; @@ -214,8 +214,8 @@ private slots: void AddEdge(const NodeOutput& output, const NodeInput& input); void RemoveEdge(const NodeOutput& output, const NodeInput& input); - void AddNodePosition(Node *node, void *relative); - void RemoveNodePosition(Node *node, void *relative); + void AddNodePosition(Node *node, Node *relative); + void RemoveNodePosition(Node *node, Node *relative); void NodePositionChanged(NodeViewItem *item, const QPointF &pos); diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 7f3d00d56..4acd6a562 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -719,7 +719,7 @@ void MainWindow::UpdateNodePanelContextFromTimelinePanel(TimelinePanel *panel) { // Add selected blocks (if any) const QVector &blocks = panel->GetSelectedBlocks(); - QVector context(blocks.size()); + QVector context(blocks.size()); for (int i=0; i(panel)) { // Signal project panel focus UpdateTitle(); - node_panel_->SetGraph(project->project(), {project->project()}); + if (project->project()) { + node_panel_->SetGraph(project->project(), {project->project()->root()}); + } } } From 4b14352bcb60ff906f617c6cd94f3ad29a948a67 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 27 May 2021 15:14:03 +1000 Subject: [PATCH 08/72] save new node contexts to project file --- app/node/graph.h | 5 ++ app/node/project/project.cpp | 91 ++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/app/node/graph.h b/app/node/graph.h index 5ebe8d5fa..198fac466 100644 --- a/app/node/graph.h +++ b/app/node/graph.h @@ -98,6 +98,11 @@ public: return position_map_[context]; } + const QMap &GetPositionMap() const + { + return position_map_; + } + signals: /** * @brief Signal emitted when a Node is added to the graph diff --git a/app/node/project/project.cpp b/app/node/project/project.cpp index a3f4aca9e..5cc50d274 100644 --- a/app/node/project/project.cpp +++ b/app/node/project/project.cpp @@ -136,6 +136,71 @@ void Project::Load(QXmlStreamReader *reader, MainWindowLayoutInfo* layout, uint } } + } else if (reader->name() == QStringLiteral("positions")) { + + while (XMLReadNextStartElement(reader)) { + + if (reader->name() == QStringLiteral("context")) { + + quintptr context_ptr = 0; + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("ptr")) { + context_ptr = attr.value().toULongLong(); + break; + } + } + + Node *context = xml_node_data.node_ptrs.value(context_ptr); + + if (!context) { + qWarning() << "Failed to find pointer for context"; + reader->skipCurrentElement(); + } else { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("node")) { + quintptr node_ptr = 0; + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("ptr")) { + node_ptr = attr.value().toULongLong(); + break; + } + } + + Node *node = xml_node_data.node_ptrs.value(node_ptr); + + if (!node) { + qWarning() << "Failed to find pointer for node position"; + reader->skipCurrentElement(); + } else { + QPointF pos; + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("x")) { + pos.setX(reader->readElementText().toDouble()); + } else if (reader->name() == QStringLiteral("y")) { + pos.setY(reader->readElementText().toDouble()); + } else { + reader->skipCurrentElement(); + } + } + + SetNodePosition(node, context, pos); + } + + } else { + reader->skipCurrentElement(); + } + } + } + + } else { + + reader->skipCurrentElement(); + + } + + } + } else { // Skip this @@ -177,6 +242,32 @@ void Project::Save(QXmlStreamWriter *writer) const writer->writeEndElement(); // nodes + writer->writeStartElement(QStringLiteral("positions")); + + for (auto it=GetPositionMap().cbegin(); it!=GetPositionMap().cend(); it++) { + writer->writeStartElement(QStringLiteral("context")); + + writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(it.key()))); + + const PositionMap &map = it.value(); + + for (auto jt=map.cbegin(); jt!=map.cend(); jt++) { + writer->writeStartElement(QStringLiteral("node")); + + writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(jt.key()))); + + const QPointF &pos = jt.value(); + writer->writeTextElement(QStringLiteral("x"), QString::number(pos.x())); + writer->writeTextElement(QStringLiteral("y"), QString::number(pos.y())); + + writer->writeEndElement(); // node + } + + writer->writeEndElement(); // context + } + + writer->writeEndElement(); // positions + // Save main window project layout MainWindowLayoutInfo main_window_info = Core::instance()->main_window()->SaveLayout(); main_window_info.toXml(writer); From 24ce8f84464c3d759f4daa2540ed7b4c11ab03df Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 27 May 2021 15:14:28 +1000 Subject: [PATCH 09/72] remove null context commands --- app/node/node.cpp | 10 ++-------- app/widget/timelinewidget/tool/import.cpp | 1 - 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/app/node/node.cpp b/app/node/node.cpp index 164104247..25702b157 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -2326,15 +2326,9 @@ void NodeSetPositionAsChildCommand::redo() sub_command_ = new MultiUndoCommand(); if (shift_surroundings_) { - if (relative_) { - sub_command_->add_child(new NodeSetPositionAndShiftSurroundingsCommand(node_, relative_, pos, true)); - } - sub_command_->add_child(new NodeSetPositionAndShiftSurroundingsCommand(node_, nullptr, pos, true)); + sub_command_->add_child(new NodeSetPositionAndShiftSurroundingsCommand(node_, relative_, pos, true)); } else { - if (relative_) { - sub_command_->add_child(new NodeSetPositionCommand(node_, relative_, pos, true)); - } - sub_command_->add_child(new NodeSetPositionCommand(node_, nullptr, pos, true)); + sub_command_->add_child(new NodeSetPositionCommand(node_, relative_, pos, true)); } } diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 5f086fb74..7a6b762e7 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -387,7 +387,6 @@ void ImportTool::DropGhosts(bool insert) clip->set_length_and_media_out(ghost->GetLength()); clip->SetLabel(footage_stream.footage->GetLabel()); command->add_child(new NodeAddCommand(dst_graph, clip)); - command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(clip, footage_stream.footage, nullptr, QPointF(2, 0))); // Position clip in its own context command->add_child(new NodeSetPositionCommand(clip, clip, QPointF(0, 0), false)); From 2162e9d8f86acd029026da83332092c6f53772de Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 28 May 2021 13:29:51 +1000 Subject: [PATCH 10/72] nodeview: improved user manual positioning --- app/node/graph.h | 5 ++ app/widget/nodeview/nodeview.cpp | 68 +++++++++++++++++---------- app/widget/nodeview/nodeview.h | 2 - app/widget/nodeview/nodeviewitem.cpp | 11 +---- app/widget/nodeview/nodeviewitem.h | 8 +--- app/widget/nodeview/nodeviewscene.cpp | 7 --- app/widget/nodeview/nodeviewscene.h | 5 -- 7 files changed, 50 insertions(+), 56 deletions(-) diff --git a/app/node/graph.h b/app/node/graph.h index 198fac466..9c67ed4bb 100644 --- a/app/node/graph.h +++ b/app/node/graph.h @@ -89,6 +89,11 @@ public: emit NodePositionRemoved(node, context); } + bool ContextContainsNode(Node *node, Node *context) + { + return position_map_[context].contains(node); + } + qreal GetNodeContextHeight(Node *context); using PositionMap = QMap; diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index e8e07eac4..5d51f69c2 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -57,7 +57,6 @@ NodeView::NodeView(QWidget *parent) : setViewportUpdateMode(FullViewportUpdate); connect(this, &NodeView::customContextMenuRequested, this, &NodeView::ShowContextMenu); - connect(&scene_, &NodeViewScene::NodePositionChanged, this, &NodeView::NodePositionChanged); ConnectSelectionChangedSignal(); @@ -643,9 +642,9 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) return; } - if (!attached_items_.isEmpty()) { - MultiUndoCommand* command = new MultiUndoCommand(); + MultiUndoCommand* command = new MultiUndoCommand(); + if (!attached_items_.isEmpty()) { if (paste_command_) { // We've already "done" this command, but MultiUndoCommand prevents "redoing" twice, so we // add it to this command (which may have extra commands added too) so that it all gets undone @@ -670,10 +669,31 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) } DetachItemsFromCursor(); - - Core::instance()->undo_stack()->push(command); } + for (auto it=positions_.begin(); it!=positions_.end(); it++) { + NodeViewItem *item = it.key(); + Position &pos_data = it.value(); + QPointF current_item_pos = item->GetNodePosition(); + Node *node = pos_data.node; + + if (pos_data.original_item_pos != current_item_pos) { + QPointF diff = current_item_pos - pos_data.original_item_pos; + + foreach (Node *context, filter_nodes_) { + if (graph_->ContextContainsNode(node, context)) { + QPointF current_node_pos_in_context = graph_->GetNodePosition(node, context); + current_node_pos_in_context += diff; + command->add_child(new NodeSetPositionCommand(node, context, current_node_pos_in_context, false)); + } + } + + pos_data.original_item_pos = current_item_pos; + } + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); + super::mouseReleaseEvent(event); } @@ -823,10 +843,13 @@ void NodeView::CreateNodeSlot(QAction *action) Node* new_node = NodeFactory::CreateFromMenuAction(action); if (new_node) { - Core::instance()->undo_stack()->push(new NodeAddCommand(graph_, new_node)); - - NodeViewItem* item = scene_.NodeToUIObject(new_node); - AttachItemsToCursor({item}); + paste_command_ = new MultiUndoCommand(); + paste_command_->add_child(new NodeAddCommand(graph_, new_node)); + foreach (Node *context, filter_nodes_) { + paste_command_->add_child(new NodeSetPositionCommand(new_node, context, QPointF(0, 0), false)); + } + paste_command_->add_child(new NodeViewAttachNodesToCursor(this, {new_node})); + paste_command_->redo(); } } @@ -940,24 +963,19 @@ void NodeView::RemoveNodePosition(Node *node, Node *relative) { if (filter_mode_ == kFilterShowSelective) { if (filter_nodes_.contains(relative)) { - NodeViewItem *item = scene_.item_map().value(node); - delete item; - } - } -} + // Determine if any other contexts have this node + bool found = false; -void NodeView::NodePositionChanged(NodeViewItem *item, const QPointF &pos) -{ - Position &original_pos = positions_[item]; - Node *node = original_pos.node; - QPointF diff = pos - original_pos.original_item_pos; - original_pos.original_item_pos = pos; + foreach (Node *context, filter_nodes_) { + if (graph_->ContextContainsNode(node, context)) { + found = true; + break; + } + } - foreach (Node *context, filter_nodes_) { - if (graph_->GetNodesForContext(context).contains(node)) { - QPointF p = graph_->GetNodePosition(node, context); - p += diff; - graph_->SetNodePosition(node, context, p); + if (!found) { + scene_.RemoveNode(node); + } } } } diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index c07536f2c..0332b7cf1 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -217,8 +217,6 @@ private slots: void AddNodePosition(Node *node, Node *relative); void RemoveNodePosition(Node *node, Node *relative); - void NodePositionChanged(NodeViewItem *item, const QPointF &pos); - }; } diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index 31ad78cf7..7e43e1f6b 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -45,8 +45,7 @@ NodeViewItem::NodeViewItem(QGraphicsItem *parent) : expanded_(false), hide_titlebar_(false), highlighted_index_(-1), - flow_dir_(NodeViewCommon::kLeftToRight), - dont_signal_(false) + flow_dir_(NodeViewCommon::kLeftToRight) { // Set flags for this widget setFlag(QGraphicsItem::ItemIsMovable); @@ -337,10 +336,6 @@ QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, cons { if (change == ItemPositionHasChanged && node_) { ReadjustAllEdges(); - - if (!dont_signal_) { - emit NodePositionChanged(GetNodePosition()); - } } return QGraphicsItem::itemChange(change, value); @@ -484,8 +479,6 @@ QPointF NodeViewItem::GetInputPointInternal(int index, const QPointF& source_pos void NodeViewItem::UpdateNodePosition() { - dont_signal_ = true; - const QPointF &pos = cached_node_pos_; switch (flow_dir_) { @@ -506,8 +499,6 @@ void NodeViewItem::UpdateNodePosition() -pos.x() * DefaultItemVerticalPadding()); break; } - - dont_signal_ = false; } } diff --git a/app/widget/nodeview/nodeviewitem.h b/app/widget/nodeview/nodeviewitem.h index 854de5e6c..1edab3aa5 100644 --- a/app/widget/nodeview/nodeviewitem.h +++ b/app/widget/nodeview/nodeviewitem.h @@ -40,9 +40,8 @@ class NodeViewEdge; * * To retrieve the NodeViewItem for a certain Node, use NodeView::NodeToUIObject(). */ -class NodeViewItem : public QObject, public QGraphicsRectItem +class NodeViewItem : public QGraphicsRectItem { - Q_OBJECT public: NodeViewItem(QGraphicsItem* parent = nullptr); @@ -112,9 +111,6 @@ public: void SetHighlightedIndex(int index); -signals: - void NodePositionChanged(const QPointF &pos); - protected: virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; @@ -178,8 +174,6 @@ private: QPointF cached_node_pos_; - bool dont_signal_; - }; } diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index 057a9c01b..4853c0e99 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -159,7 +159,6 @@ NodeViewItem* NodeViewScene::AddNode(Node* node) connect(node, &Node::LabelChanged, this, &NodeViewScene::NodeAppearanceChanged); connect(node, &Node::ColorChanged, this, &NodeViewScene::NodeAppearanceChanged); - connect(item, &NodeViewItem::NodePositionChanged, this, &NodeViewScene::NodeItemPositionChanged); return item; } @@ -279,10 +278,4 @@ void NodeViewScene::NodeAppearanceChanged() item_map_.value(static_cast(sender()))->update(); } -void NodeViewScene::NodeItemPositionChanged(const QPointF &pos) -{ - NodeViewItem *item = static_cast(sender()); - emit NodePositionChanged(item, pos); -} - } diff --git a/app/widget/nodeview/nodeviewscene.h b/app/widget/nodeview/nodeviewscene.h index 267807812..272c592ce 100644 --- a/app/widget/nodeview/nodeviewscene.h +++ b/app/widget/nodeview/nodeviewscene.h @@ -106,9 +106,6 @@ public slots: */ void SetEdgesAreCurved(bool curved); -signals: - void NodePositionChanged(NodeViewItem *node, const QPointF &pos); - private: static int DetermineWeight(Node* n); @@ -130,8 +127,6 @@ private slots: */ void NodeAppearanceChanged(); - void NodeItemPositionChanged(const QPointF &pos); - }; } From d9e45a68181c22cc9ac284f41ffe59ee691a49b7 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 28 May 2021 17:53:15 +1000 Subject: [PATCH 11/72] nodeviewscene: skip add duplicate edges --- app/widget/nodeview/nodeviewscene.cpp | 14 +++++++++++--- app/widget/nodeview/nodeviewscene.h | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index 4853c0e99..267a3b164 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -171,9 +171,15 @@ void NodeViewScene::RemoveNode(Node *node) delete item_map_.take(node); } -void NodeViewScene::AddEdge(const NodeOutput &output, const NodeInput &input) +NodeViewEdge* NodeViewScene::AddEdge(const NodeOutput &output, const NodeInput &input) { - AddEdgeInternal(output, input, NodeToUIObject(output.node()), NodeToUIObject(input.node())); + NodeViewEdge *edge = EdgeToUIObject(output, input); + + if (!edge) { + edge = AddEdgeInternal(output, input, NodeToUIObject(output.node()), NodeToUIObject(input.node())); + } + + return edge; } void NodeViewScene::RemoveEdge(const NodeOutput &output, const NodeInput &input) @@ -202,7 +208,7 @@ int NodeViewScene::DetermineWeight(Node *n) return qMax(1, weight); } -void NodeViewScene::AddEdgeInternal(const NodeOutput& output, const NodeInput& input, NodeViewItem *from, NodeViewItem *to) +NodeViewEdge* NodeViewScene::AddEdgeInternal(const NodeOutput& output, const NodeInput& input, NodeViewItem *from, NodeViewItem *to) { NodeViewEdge* edge_ui = new NodeViewEdge(output, input, from, to); @@ -214,6 +220,8 @@ void NodeViewScene::AddEdgeInternal(const NodeOutput& output, const NodeInput& i addItem(edge_ui); edges_.append(edge_ui); + + return edge_ui; } Qt::Orientation NodeViewScene::GetFlowOrientation() const diff --git a/app/widget/nodeview/nodeviewscene.h b/app/widget/nodeview/nodeviewscene.h index 272c592ce..94cadd463 100644 --- a/app/widget/nodeview/nodeviewscene.h +++ b/app/widget/nodeview/nodeviewscene.h @@ -98,7 +98,7 @@ public slots: */ void RemoveNode(Node* node); - void AddEdge(const NodeOutput& output, const NodeInput& input); + NodeViewEdge *AddEdge(const NodeOutput& output, const NodeInput& input); void RemoveEdge(const NodeOutput& output, const NodeInput& input); /** @@ -109,7 +109,7 @@ public slots: private: static int DetermineWeight(Node* n); - void AddEdgeInternal(const NodeOutput &output, const NodeInput &input, NodeViewItem* from, NodeViewItem* to); + NodeViewEdge* AddEdgeInternal(const NodeOutput &output, const NodeInput &input, NodeViewItem* from, NodeViewItem* to); QHash item_map_; From f0068e8c3741c8034322ca47a2786b0530172325 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 28 May 2021 17:53:40 +1000 Subject: [PATCH 12/72] node: added command to remove position from context --- app/node/node.cpp | 20 ++++++++++++++++++++ app/node/node.h | 29 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/app/node/node.cpp b/app/node/node.cpp index 25702b157..803b6d43c 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -2348,4 +2348,24 @@ void NodeSetPositionToOffsetOfAnotherNodeCommand::undo() graph->SetNodePosition(node_, relative_, old_pos_); } +void NodeRemovePositionFromContextCommand::redo() +{ + NodeGraph *graph = node_->parent(); + + contained_ = graph->ContextContainsNode(node_, context_); + + if (contained_) { + old_pos_ = graph->GetNodePosition(node_, context_); + graph->RemoveNodePosition(node_, context_); + } +} + +void NodeRemovePositionFromContextCommand::undo() +{ + if (contained_) { + NodeGraph *graph = node_->parent(); + graph->SetNodePosition(node_, context_, old_pos_); + } +} + } diff --git a/app/node/node.h b/app/node/node.h index 44c384fb6..e0e6fd986 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -1445,6 +1445,35 @@ private: }; +class NodeRemovePositionFromContextCommand : public UndoCommand +{ +public: + NodeRemovePositionFromContextCommand(Node *node, Node *context) : + node_(node), + context_(context) + { + } + + virtual Project * GetRelevantProject() const override + { + return node_->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + Node *node_; + + Node *context_; + + QPointF old_pos_; + + bool contained_; + +}; + } #endif // NODE_H From 517fcf6cfe4996b6f959e38af646646ddc09629f Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 28 May 2021 17:54:09 +1000 Subject: [PATCH 13/72] nodeparamview: use super macro --- app/widget/nodeparamview/nodeparamview.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index 605964f84..b85fbc69d 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -30,8 +30,10 @@ namespace olive { +#define super TimeBasedWidget + NodeParamView::NodeParamView(QWidget *parent) : - TimeBasedWidget(true, false, parent), + super(true, false, parent), last_scroll_val_(0), focused_node_(nullptr) { @@ -194,7 +196,7 @@ void NodeParamView::DeselectNodes(const QVector &nodes) void NodeParamView::resizeEvent(QResizeEvent *event) { - QWidget::resizeEvent(event); + super::resizeEvent(event); vertical_scrollbar_->setPageStep(vertical_scrollbar_->height()); @@ -203,14 +205,14 @@ void NodeParamView::resizeEvent(QResizeEvent *event) void NodeParamView::ScaleChangedEvent(const double &scale) { - TimeBasedWidget::ScaleChangedEvent(scale); + super::ScaleChangedEvent(scale); keyframe_view_->SetScale(scale); } void NodeParamView::TimebaseChangedEvent(const rational &timebase) { - TimeBasedWidget::TimebaseChangedEvent(timebase); + super::TimebaseChangedEvent(timebase); keyframe_view_->SetTimebase(timebase); @@ -223,7 +225,7 @@ void NodeParamView::TimebaseChangedEvent(const rational &timebase) void NodeParamView::TimeChangedEvent(const int64_t ×tamp) { - TimeBasedWidget::TimeChangedEvent(timestamp); + super::TimeChangedEvent(timestamp); keyframe_view_->SetTime(timestamp); From e3e5f836b24b57c09685c9c03817df2803e55963 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 28 May 2021 17:55:07 +1000 Subject: [PATCH 14/72] nodeparamview: use hidden body to preserve width instead of size policy Fixes various layout issues in NodeParamView. --- app/widget/nodeparamview/nodeparamviewitem.cpp | 14 ++++++++------ app/widget/nodeparamview/nodeparamviewitem.h | 2 ++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index 0c4f05a70..ad01f6ba0 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -62,13 +62,15 @@ NodeParamViewItem::NodeParamViewItem(Node *node, QWidget *parent) : this->setWidget(body_); + // Use dummy QWidget to retain width when not expanded (QDockWidget seems to ignore the titlebar + // size hints and will shrink as small as possible if the body is hidden) + hidden_body_ = new QWidget(this); + connect(node_, &Node::LabelChanged, this, &NodeParamViewItem::Retranslate); setBackgroundRole(QPalette::Base); setAutoFillBackground(true); - setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); - setFocusPolicy(Qt::ClickFocus); Retranslate(); @@ -88,7 +90,7 @@ void NodeParamViewItem::SetTime(const rational &time) void NodeParamViewItem::SetTimebase(const rational& timebase) { - body_->SetTimebase(timebase); + body_->SetTimebase(timebase); } Node *NodeParamViewItem::GetNode() const @@ -140,7 +142,7 @@ void NodeParamViewItem::Retranslate() void NodeParamViewItem::SetExpanded(bool e) { - body_->setVisible(e); + setWidget(e ? body_ : hidden_body_); title_bar_->SetExpanded(e); emit ExpandedChanged(e); @@ -537,10 +539,10 @@ void NodeParamViewItemBody::ToggleArrayExpanded() } } -void NodeParamViewItemBody::SetTimebase(const rational& timebase) +void NodeParamViewItemBody::SetTimebase(const rational& timebase) { foreach (const InputUI& ui_obj, input_ui_map_) { - ui_obj.widget_bridge->SetTimebase(timebase); + ui_obj.widget_bridge->SetTimebase(timebase); } } diff --git a/app/widget/nodeparamview/nodeparamviewitem.h b/app/widget/nodeparamview/nodeparamviewitem.h index 30ae562e9..b000b6282 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.h +++ b/app/widget/nodeparamview/nodeparamviewitem.h @@ -216,6 +216,8 @@ private: NodeParamViewItemBody* body_; + QWidget *hidden_body_; + Node* node_; rational time_; From 05320902594bcb909327ed0ecedd5f3a4a07e887 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 28 May 2021 17:55:16 +1000 Subject: [PATCH 15/72] nodeview: make items wider --- app/widget/nodeview/nodeviewitem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index 7e43e1f6b..7224edc4d 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -114,7 +114,7 @@ int NodeViewItem::DefaultItemHeight() int NodeViewItem::DefaultItemWidth() { - return QtUtils::QFontMetricsWidth(QFontMetrics(QFont()), "HHHHHHHHHHHH");; + return QtUtils::QFontMetricsWidth(QFontMetrics(QFont()), "HHHHHHHHHHHHHHHH");; } int NodeViewItem::DefaultItemBorder() From e2bbff5078f9a85532f6fc23e17069064656331e Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 28 May 2021 17:57:38 +1000 Subject: [PATCH 16/72] bump project version --- app/core.cpp | 2 +- app/task/project/load/load.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/core.cpp b/app/core.cpp index 9dcb7f96c..4b9f7159d 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -75,7 +75,7 @@ namespace olive { Core* Core::instance_ = nullptr; -const uint Core::kProjectVersion = 210122; +const uint Core::kProjectVersion = 210528; Core::Core(const CoreParams& params) : main_window_(nullptr), diff --git a/app/task/project/load/load.cpp b/app/task/project/load/load.cpp index 229141921..e747aec33 100644 --- a/app/task/project/load/load.cpp +++ b/app/task/project/load/load.cpp @@ -58,7 +58,7 @@ bool ProjectLoadTask::Run() // Project is newer than we support SetError(tr("This project is newer than this version of Olive and cannot be opened.")); return false; - } else if (project_version < 210122) { // Change this if we drop support for a project version + } else if (project_version < 210528) { // Change this if we drop support for a project version // Project is older than we support SetError(tr("This project is from a version of Olive that is no longer supported in this version.")); return false; From 8ef140dcd0fe4313a6329697e67510d3ce0d1d2d Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 28 May 2021 17:58:42 +1000 Subject: [PATCH 17/72] nodeview: use null check In a perfect world, this would be unnecessary, but it will just save us so many crashes to do it. --- app/widget/nodeview/nodeview.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 5d51f69c2..6791fc98a 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -263,6 +263,8 @@ void NodeView::Select(QVector nodes) // Remove any duplicates QVector processed; + NodeViewItem *first_item = nullptr; + foreach (Node* n, nodes) { if (processed.contains(n)) { continue; @@ -272,17 +274,25 @@ void NodeView::Select(QVector nodes) NodeViewItem* item = scene_.NodeToUIObject(n); - item->setSelected(true); + if (item) { + item->setSelected(true); - if (deselections.contains(n)) { - deselections.removeOne(n); - } else { - new_selections.append(n); + if (!first_item) { + first_item = item; + } + + if (deselections.contains(n)) { + deselections.removeOne(n); + } else { + new_selections.append(n); + } } } // Center on something - centerOn(scene_.NodeToUIObject(nodes.first())); + if (first_item) { + centerOn(first_item); + } ConnectSelectionChangedSignal(); From 6204e780369d13779916370a6b61ebf6085bb6a8 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 28 May 2021 17:59:02 +1000 Subject: [PATCH 18/72] nodeview: reimplement adding edges --- app/widget/nodeview/nodeview.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 6791fc98a..31e8b0bb8 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -946,6 +946,18 @@ void NodeView::AddNodePosition(Node *node, Node *relative) NodeViewItem *item = scene_.item_map().value(node); if (!item) { item = scene_.AddNode(node); + + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + if (scene_.item_map().contains(it->second.node())) { + scene_.AddEdge(it->second, it->first); + } + } + + for (auto it=node->output_connections().cbegin(); it!=node->output_connections().cend(); it++) { + if (scene_.item_map().contains(it->second.node())) { + scene_.AddEdge(it->first, it->second); + } + } } // Determine "view" position by averaging the Y value and "min"ing the X value of all contexts From 3bce63a0c4f43f746f3d04b280c85e6fda0eb73b Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 28 May 2021 17:59:21 +1000 Subject: [PATCH 19/72] mainwindow: select blocks and deps when setting node view context --- app/window/mainwindow/mainwindow.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 4acd6a562..573220973 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -731,6 +731,9 @@ void MainWindow::UpdateNodePanelContextFromTimelinePanel(TimelinePanel *panel) } node_panel_->SetGraph(viewer ? viewer->parent() : nullptr, context); + if (viewer) { + node_panel_->SelectWithDependencies(context); + } } void MainWindow::FocusedPanelChanged(PanelWidget *panel) From e7ec9ccde2546b9df75d433de8f871aea5e96b0c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 28 May 2021 17:59:37 +1000 Subject: [PATCH 20/72] nodeview: remove from positions map when removing node --- app/widget/nodeview/nodeview.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 31e8b0bb8..6b044a067 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -996,6 +996,7 @@ void NodeView::RemoveNodePosition(Node *node, Node *relative) } if (!found) { + positions_.remove(scene_.item_map().value(node)); scene_.RemoveNode(node); } } From 90d94075cfef3d0e5d2f664b23b2fb0b286654f2 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 17 Jun 2021 10:19:30 -0700 Subject: [PATCH 21/72] nodeview: improved bounding rect and centering --- app/panel/node/node.h | 8 +++---- app/widget/nodeview/nodeview.cpp | 31 ++++++++++++++++++++++++---- app/widget/nodeview/nodeview.h | 8 +++++-- app/window/mainwindow/mainwindow.cpp | 12 +++++++---- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/app/panel/node/node.h b/app/panel/node/node.h index c465bf725..447e88f9d 100644 --- a/app/panel/node/node.h +++ b/app/panel/node/node.h @@ -101,14 +101,14 @@ public: } public slots: - void Select(const QVector& nodes) + void Select(const QVector& nodes, bool center_view_on_item) { - node_view_->Select(nodes); + node_view_->Select(nodes, center_view_on_item); } - void SelectWithDependencies(const QVector& nodes) + void SelectWithDependencies(const QVector& nodes, bool center_view_on_item) { - node_view_->SelectWithDependencies(nodes); + node_view_->SelectWithDependencies(nodes, center_view_on_item); } signals: diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 6b044a067..6416b0a53 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -61,6 +61,9 @@ NodeView::NodeView(QWidget *parent) : ConnectSelectionChangedSignal(); SetFlowDirection(NodeViewCommon::kTopToBottom); + + UpdateSceneBoundingRect(); + connect(&scene_, &QGraphicsScene::changed, this, &NodeView::UpdateSceneBoundingRect); } NodeView::~NodeView() @@ -146,6 +149,9 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) AddNodePosition(it.key(), n); } } + + // Center on something + CenterOnItemsBoundingRect(); } } } @@ -245,7 +251,7 @@ void NodeView::DeselectAll() selected_nodes_.clear(); } -void NodeView::Select(QVector nodes) +void NodeView::Select(QVector nodes, bool center_view_on_item) { if (!graph_) { return; @@ -290,7 +296,7 @@ void NodeView::Select(QVector nodes) } // Center on something - if (first_item) { + if (center_view_on_item && first_item) { centerOn(first_item); } @@ -310,7 +316,7 @@ void NodeView::Select(QVector nodes) selected_nodes_ = nodes; } -void NodeView::SelectWithDependencies(QVector nodes) +void NodeView::SelectWithDependencies(QVector nodes, bool center_view_on_item) { if (!graph_) { return; @@ -321,7 +327,7 @@ void NodeView::SelectWithDependencies(QVector nodes) nodes.append(nodes.at(i)->GetDependencies()); } - Select(nodes); + Select(nodes, center_view_on_item); } void NodeView::CopySelected(bool cut) @@ -1003,6 +1009,23 @@ void NodeView::RemoveNodePosition(Node *node, Node *relative) } } +void NodeView::UpdateSceneBoundingRect() +{ + // 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::CenterOnItemsBoundingRect() +{ + centerOn(scene_.itemsBoundingRect().center()); +} + void NodeView::AttachNodesToCursor(const QVector &nodes) { QVector items(nodes.size()); diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 0332b7cf1..05b4f8ab4 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -63,8 +63,8 @@ public: void SelectAll(); void DeselectAll(); - void Select(QVector nodes); - void SelectWithDependencies(QVector nodes); + void Select(QVector nodes, bool center_view_on_item); + void SelectWithDependencies(QVector nodes, bool center_view_on_item); void CopySelected(bool cut); void Paste(); @@ -217,6 +217,10 @@ private slots: void AddNodePosition(Node *node, Node *relative); void RemoveNodePosition(Node *node, Node *relative); + void UpdateSceneBoundingRect(); + + void CenterOnItemsBoundingRect(); + }; } diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 573220973..011fee537 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -95,7 +95,9 @@ MainWindow::MainWindow(QWidget *parent) : connect(node_panel_, &NodePanel::NodesDeselected, param_panel_, &ParamPanel::DeselectNodes); connect(node_panel_, &NodePanel::NodesSelected, table_panel_, &NodeTablePanel::SelectNodes); connect(node_panel_, &NodePanel::NodesDeselected, table_panel_, &NodeTablePanel::DeselectNodes); - connect(param_panel_, &ParamPanel::RequestSelectNode, node_panel_, &NodePanel::Select); + connect(param_panel_, &ParamPanel::RequestSelectNode, this, [this](const QVector& target){ + node_panel_->Select(target, true); + }); connect(param_panel_, &ParamPanel::FocusedNodeChanged, sequence_viewer_panel_, &ViewerPanel::SetGizmos); // Connect time signals together @@ -480,7 +482,7 @@ void MainWindow::ProjectPanelSelectionChanged(const QVector &nodes) ProjectPanel *panel = static_cast(sender()); if (PanelManager::instance()->CurrentlyFocused(false) == panel) { - node_panel_->Select(nodes); + node_panel_->Select(nodes, true); } } @@ -732,7 +734,7 @@ void MainWindow::UpdateNodePanelContextFromTimelinePanel(TimelinePanel *panel) node_panel_->SetGraph(viewer ? viewer->parent() : nullptr, context); if (viewer) { - node_panel_->SelectWithDependencies(context); + node_panel_->SelectWithDependencies(context, false); } } @@ -752,7 +754,9 @@ void MainWindow::FocusedPanelChanged(PanelWidget *panel) // Signal project panel focus UpdateTitle(); if (project->project()) { - node_panel_->SetGraph(project->project(), {project->project()->root()}); + QVector context = {project->project()->root()}; + node_panel_->SetGraph(project->project(), context); + node_panel_->SelectWithDependencies(context, false); } } } From df42696bdb3810a5cb1e917e115231029e6eee3d Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 8 Jul 2021 11:36:46 -0700 Subject: [PATCH 22/72] core: position sequence in sequence's context --- app/core.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/app/core.cpp b/app/core.cpp index 4b9f7159d..0938330af 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -421,6 +421,7 @@ void Core::CreateNewSequence() command->add_child(new NodeAddCommand(active_project, new_sequence)); command->add_child(new FolderAddChild(GetSelectedFolderInActiveProject(), new_sequence)); + command->add_child(new NodeSetPositionCommand(new_sequence, new_sequence, QPointF(0, 0), false)); // Create and connect default nodes to new sequence new_sequence->add_default_nodes(command); From 0c04bcf045fd8e07f771b5e3e5a895612541a98c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 8 Jul 2021 11:39:29 -0700 Subject: [PATCH 23/72] timecode: allow specific rounding routines --- app/codec/ffmpeg/ffmpegencoder.cpp | 2 +- app/common/timecodefunctions.cpp | 16 ++++++++++------ app/common/timecodefunctions.h | 12 +++++++++--- app/render/framehashcache.cpp | 2 +- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index c1b54c5aa..a2926f15d 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -396,7 +396,7 @@ bool FFmpegEncoder::WriteSubtitle(const SubtitleBlock *sub_block) subtitle.num_rects = 1; subtitle.rects = &rect_array; - subtitle.pts = Timecode::time_to_timestamp(sub_block->in(), av_get_time_base_q(), true); + subtitle.pts = Timecode::time_to_timestamp(sub_block->in(), av_get_time_base_q(), Timecode::kFloor); subtitle.end_display_time = qRound64(sub_block->length().toDouble() * 1000); QVector out_buf(1024 * 1024); diff --git a/app/common/timecodefunctions.cpp b/app/common/timecodefunctions.cpp index 0501d5bb4..a90b9e023 100644 --- a/app/common/timecodefunctions.cpp +++ b/app/common/timecodefunctions.cpp @@ -239,7 +239,7 @@ rational Timecode::timecode_to_time(const QString &timecode, const rational &tim return timestamp_to_time(timestamp, timebase); } -rational Timecode::snap_time_to_timebase(const rational &time, const rational &timebase, bool floor) +rational Timecode::snap_time_to_timebase(const rational &time, const rational &timebase, Rounding floor) { // Just convert to a timestamp in timebase units and back int64_t timestamp = time_to_timestamp(time, timebase, floor); @@ -275,19 +275,23 @@ QString Timecode::TimeToString(int64_t ms) .arg(ss, 2, 10, QChar('0')); } -int64_t Timecode::time_to_timestamp(const rational &time, const rational &timebase, bool floor) +int64_t Timecode::time_to_timestamp(const rational &time, const rational &timebase, Rounding floor) { return time_to_timestamp(time.toDouble(), timebase, floor); } -int64_t Timecode::time_to_timestamp(const double &time, const rational &timebase, bool floor) +int64_t Timecode::time_to_timestamp(const double &time, const rational &timebase, Rounding floor) { double d = time * timebase.flipped().toDouble(); - if (floor) { - return qFloor(d); - } else { + switch (floor) { + case kRound: + default: return qRound64(d); + case kFloor: + return qFloor(d); + case kCeil: + return qCeil(d); } } diff --git a/app/common/timecodefunctions.h b/app/common/timecodefunctions.h index dea645b1c..7252f44db 100644 --- a/app/common/timecodefunctions.h +++ b/app/common/timecodefunctions.h @@ -47,6 +47,12 @@ public: kMilliseconds }; + enum Rounding { + kCeil, + kFloor, + kRound + }; + /** * @brief Convert a timestamp (according to a rational timebase) to a user-friendly string representation */ @@ -55,10 +61,10 @@ public: static int64_t timecode_to_timestamp(const QString& timecode, const rational& timebase, const Display& display, bool *ok = nullptr); static rational timecode_to_time(const QString& timecode, const rational& timebase, const Display& display, bool *ok = nullptr); - static rational snap_time_to_timebase(const rational& time, const rational& timebase, bool floor = false); + static rational snap_time_to_timebase(const rational& time, const rational& timebase, Rounding floor = kRound); - static int64_t time_to_timestamp(const rational& time, const rational& timebase, bool floor = false); - static int64_t time_to_timestamp(const double& time, const rational& timebase, bool floor = false); + static int64_t time_to_timestamp(const rational& time, const rational& timebase, Rounding floor = kRound); + static int64_t time_to_timestamp(const double& time, const rational& timebase, Rounding floor = kRound); static int64_t rescale_timestamp(const int64_t& ts, const rational& source, const rational& dest); static int64_t rescale_timestamp_ceil(const int64_t& ts, const rational& source, const rational& dest); diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index e78578488..21b087086 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -151,7 +151,7 @@ QVector FrameHashCache::GetFrameListFromTimeRange(TimeRangeList range_ QVector times; foreach (const TimeRange &range, range_list) { - rational frame = Timecode::snap_time_to_timebase(range.in(), timebase, true); + rational frame = Timecode::snap_time_to_timebase(range.in(), timebase, Timecode::kCeil); while (frame < range.out()) { times.append(frame); From 5dd21320373eb91978f6b24ab8f2e13453b2c9f3 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 8 Jul 2021 11:42:09 -0700 Subject: [PATCH 24/72] timeline: set sequence position in sequence's context --- app/widget/timelinewidget/tool/import.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 9612ddba2..137bbdc06 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -353,6 +353,7 @@ void ImportTool::DropGhosts(bool insert) command->add_child(new NodeAddCommand(dst_graph, new_sequence)); command->add_child(new FolderAddChild(Core::instance()->GetSelectedFolderInActiveProject(), new_sequence)); + command->add_child(new NodeSetPositionCommand(new_sequence, new_sequence, QPointF(0, 0), false)); new_sequence->add_default_nodes(command); FootageToGhosts(0, dragged_footage_, new_sequence->GetVideoParams().time_base(), 0); From dcac7f94f9a2bf91a3ab4affa884cbd5165a74f6 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 8 Jul 2021 11:42:53 -0700 Subject: [PATCH 25/72] nodeview: when removing a node, remove it from all contexts --- app/widget/nodeview/nodeviewundo.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/widget/nodeview/nodeviewundo.cpp b/app/widget/nodeview/nodeviewundo.cpp index 9723038f4..565a4d535 100644 --- a/app/widget/nodeview/nodeviewundo.cpp +++ b/app/widget/nodeview/nodeviewundo.cpp @@ -142,6 +142,8 @@ void NodeRemoveAndDisconnectCommand::prep() for (const Node::OutputConnection& conn : node_->output_connections()) { command_->add_child(new NodeEdgeRemoveCommand(conn.first, conn.second)); } + + command_->add_child(new NodeRemovePositionFromAllContextsCommand(node_)); } void NodeRenameCommand::AddNode(Node *node, const QString &new_name) From 17c9137bbd7092804e81f97bab409f54c8c5f502 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 8 Jul 2021 11:44:04 -0700 Subject: [PATCH 26/72] nodeviewedge: minor cleanup --- app/widget/nodeview/nodeviewedge.cpp | 35 ++++++++++++---------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/app/widget/nodeview/nodeviewedge.cpp b/app/widget/nodeview/nodeviewedge.cpp index e15f6125e..d6835ed57 100644 --- a/app/widget/nodeview/nodeviewedge.cpp +++ b/app/widget/nodeview/nodeviewedge.cpp @@ -185,25 +185,20 @@ void NodeViewEdge::UpdateCurve() if (!qFuzzyCompare(start.x(), end.x())) { double continue_x = end.x() - qCos(angle)*arrow_size_; - double x1, x2, x3, x4, y1, y2, y3, y4; - if (start.x() < end.x()) { - x1 = start.x(); - x2 = cp1.x(); - x3 = cp2.x(); - x4 = end.x(); - y1 = start.y(); - y2 = cp1.y(); - y3 = cp2.y(); - y4 = end.y(); - } else { - x1 = end.x(); - x2 = cp2.x(); - x3 = cp1.x(); - x4 = start.x(); - y1 = end.y(); - y2 = cp2.y(); - y3 = cp1.y(); - y4 = start.y(); + 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::CubicXtoT(continue_x, x1, x2, x3, x4); @@ -220,7 +215,7 @@ void NodeViewEdge::UpdateCurve() setPath(path); - const double arrow_angle = 150.0 * 3.141592 / 180.0; + const double arrow_angle = 150.0 * M_PI / 180.0; QVector arrow_points(4); arrow_points[0] = end; arrow_points[1] = end + QPointF(qCos(angle + arrow_angle) * arrow_size_, qSin(angle + arrow_angle) * arrow_size_); From 60240aefea7fefa48d03aa12cc0bbf91572b5405 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 8 Jul 2021 11:45:07 -0700 Subject: [PATCH 27/72] timeline: revise track positioning code --- app/widget/timelinewidget/timelineundo.h | 96 ++++++++++++++---------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/app/widget/timelinewidget/timelineundo.h b/app/widget/timelinewidget/timelineundo.h index 3f73fb687..da1239527 100644 --- a/app/widget/timelinewidget/timelineundo.h +++ b/app/widget/timelinewidget/timelineundo.h @@ -1151,14 +1151,38 @@ private: class TimelineAddTrackCommand : public UndoCommand { public: - TimelineAddTrackCommand(TrackList *timeline) + TimelineAddTrackCommand(TrackList *timeline) : + TimelineAddTrackCommand(timeline, Config::Current()[QStringLiteral("AutoMergeTracks")].toBool()) { - Init(timeline, Config::Current()[QStringLiteral("AutoMergeTracks")].toBool()); } TimelineAddTrackCommand(TrackList *timeline, bool automerge_tracks) { - Init(timeline, automerge_tracks); + timeline_ = timeline; + position_command_ = nullptr; + + track_ = new Track(); + track_->setParent(&memory_manager_); + + if (timeline->GetTrackCount() > 0 && automerge_tracks) { + if (timeline_->type() == Track::kVideo) { + merge_ = new MergeNode(); + base_ = NodeInput(merge_, MergeNode::kBaseIn); + blend_ = NodeInput(merge_, MergeNode::kBlendIn); + } else if (timeline_->type() == Track::kAudio) { + merge_ = new MathNode(); + base_ = NodeInput(merge_, MathNode::kParamAIn); + blend_ = NodeInput(merge_, MathNode::kParamBIn); + } else { + merge_ = nullptr; + } + } else { + merge_ = nullptr; + } + + if (merge_) { + merge_->setParent(&memory_manager_); + } } static Track* RunImmediately(TrackList *timeline) @@ -1195,18 +1219,14 @@ public: // Add track track_->setParent(timeline_->GetParentGraph()); timeline_->ArrayAppend(); - int track_total_index = timeline_->parent()->GetTracks().size(); - if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(track_, timeline_->parent(), timeline_->parent(), track_total_index, track_total_index + 1, true); - } - position_command_->redo(); Node::ConnectEdge(track_, timeline_->track_input(timeline_->ArraySize() - 1)); // Add merge if applicable + Track* last_track = nullptr; if (merge_) { merge_->setParent(timeline_->GetParentGraph()); - Track* last_track = timeline_->GetTrackAt(timeline_->GetTrackCount()-2); + last_track = timeline_->GetTrackAt(timeline_->GetTrackCount()-2); // Whatever this track used to be connected to, connect the merge instead const Node::OutputConnections edges = last_track->output_connections(); @@ -1241,10 +1261,36 @@ public: direct_ = NodeInput(); } } + + // Position track in context + if (!position_command_) { + int track_count = timeline_->parent()->GetTracks().size(); + position_command_ = new MultiUndoCommand(); + + // Position either the merge or the track as an "element" + Node *node_to_position = merge_ ? merge_ : track_; + double node_index = track_count - 1; + if (merge_) { + node_index -= 1; + position_command_->add_child(new NodeRemovePositionFromContextCommand(last_track, timeline_->parent())); + } + + position_command_->add_child(new NodeSetPositionAsChildCommand(node_to_position, timeline_->parent(), timeline_->parent(), node_index, track_count, true)); + + // If we positioned a merge, position the tracks as children of the merge + if (merge_) { + // `last_track` should be non-null if `merge_` is non-null + position_command_->add_child(new NodeSetPositionAsChildCommand(last_track, merge_, timeline_->parent(), 0, 2, true)); + position_command_->add_child(new NodeSetPositionAsChildCommand(track_, merge_, timeline_->parent(), 1, 2, true)); + } + } + position_command_->redo(); } virtual void undo() override { + position_command_->undo(); + // Remove merge if applicable if (merge_) { // Assume whatever this merge is connected to USED to be connected to the last track @@ -1269,41 +1315,11 @@ public: // Remove track Node::DisconnectEdge(track_, timeline_->track_input(timeline_->ArraySize() - 1)); - position_command_->undo(); timeline_->ArrayRemoveLast(); track_->setParent(&memory_manager_); } private: - void Init(TrackList* timeline, bool automerge) - { - timeline_ = timeline; - position_command_ = nullptr; - - track_ = new Track(); - track_->setParent(&memory_manager_); - - if (timeline->GetTrackCount() > 0 && automerge) { - if (timeline_->type() == Track::kVideo) { - merge_ = new MergeNode(); - base_ = NodeInput(merge_, MergeNode::kBaseIn); - blend_ = NodeInput(merge_, MergeNode::kBlendIn); - } else if (timeline_->type() == Track::kAudio) { - merge_ = new MathNode(); - base_ = NodeInput(merge_, MathNode::kParamAIn); - blend_ = NodeInput(merge_, MathNode::kParamBIn); - } else { - merge_ = nullptr; - } - } else { - merge_ = nullptr; - } - - if (merge_) { - merge_->setParent(&memory_manager_); - } - } - TrackList* timeline_; Track* track_; @@ -1313,7 +1329,7 @@ private: NodeInput direct_; - NodeSetPositionAsChildCommand* position_command_; + MultiUndoCommand* position_command_; QObject memory_manager_; From d83087a4efa1afb56df79d5a2fb19e2812c5276d Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 8 Jul 2021 18:48:02 -0700 Subject: [PATCH 28/72] nodegraph: added more helper functions for contexts --- app/node/graph.cpp | 25 +++++++++++++++++++++++++ app/node/graph.h | 4 ++++ 2 files changed, 29 insertions(+) diff --git a/app/node/graph.cpp b/app/node/graph.cpp index 0527b4d16..26bfe6937 100644 --- a/app/node/graph.cpp +++ b/app/node/graph.cpp @@ -58,6 +58,31 @@ qreal NodeGraph::GetNodeContextHeight(Node *context) return bottom - top; } +int NodeGraph::GetNumberOfContextsNodeIsIn(Node *node) const +{ + int count = 0; + + for (auto it=position_map_.cbegin(); it!=position_map_.cend(); it++) { + if (it.value().contains(node)) { + count++; + } + } + + return count; +} + +bool NodeGraph::NodeOutputsToContext(Node *node) const +{ + for (auto it=position_map_.cbegin(); it!=position_map_.cend(); it++) { + const PositionMap &pm = it.value(); + if (pm.contains(node) && node->OutputsTo(it.key(), true)) { + return true; + } + } + + return false; +} + void NodeGraph::childEvent(QChildEvent *event) { super::childEvent(event); diff --git a/app/node/graph.h b/app/node/graph.h index 9c67ed4bb..dbdfb990b 100644 --- a/app/node/graph.h +++ b/app/node/graph.h @@ -108,6 +108,10 @@ public: return position_map_; } + int GetNumberOfContextsNodeIsIn(Node *node) const; + + bool NodeOutputsToContext(Node *node) const; + signals: /** * @brief Signal emitted when a Node is added to the graph From f14a301ffaae5ede329199d4a3d2ea3587b4e9d8 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 9 Jul 2021 18:00:01 -0700 Subject: [PATCH 29/72] nodeview: updates to complete new context paradigm --- app/node/node.cpp | 92 +++-- app/node/node.h | 53 ++- app/widget/nodeview/nodeview.cpp | 480 ++++++++++++++++++++++---- app/widget/nodeview/nodeview.h | 36 ++ app/widget/nodeview/nodeviewitem.cpp | 122 ++++--- app/widget/nodeview/nodeviewitem.h | 18 +- app/widget/nodeview/nodeviewscene.cpp | 2 +- app/window/mainwindow/mainwindow.cpp | 12 +- 8 files changed, 666 insertions(+), 149 deletions(-) diff --git a/app/node/node.cpp b/app/node/node.cpp index 803b6d43c..51821e820 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -1622,15 +1622,28 @@ void Node::GenerateFrame(FramePtr frame, const GenerateJob &job) const Q_UNUSED(job) } -bool Node::OutputsTo(Node *n, bool recursively) const +bool Node::OutputsTo(Node *n, bool recursively, const OutputConnections &ignore_edges, const OutputConnection &added_edge) const { for (const OutputConnection& conn : output_connections_) { + if (std::find(ignore_edges.cbegin(), ignore_edges.cend(), conn) != ignore_edges.cend()) { + // If this edge is in the "ignore edges" list, skip it + continue; + } + Node* connected = conn.second.node(); if (connected == n) { return true; - } else if (recursively && connected->OutputsTo(n, recursively)) { + } else if (recursively && connected->OutputsTo(n, recursively, ignore_edges, added_edge)) { return true; + } else if (added_edge.first.node() == this) { + Node *proposed_connected = added_edge.second.node(); + + if (proposed_connected == n) { + return true; + } else if (recursively && proposed_connected->OutputsTo(n, recursively, ignore_edges, added_edge)) { + return true; + } } } @@ -1697,7 +1710,7 @@ bool Node::InputsFrom(const QString &id, bool recursively) const return false; } -int Node::GetRoutesTo(Node *n) const +int Node::GetNumberOfRoutesTo(Node *n) const { bool outputs_directly = false; int routes = 0; @@ -1708,7 +1721,7 @@ int Node::GetRoutesTo(Node *n) const if (connected_node == n) { outputs_directly = true; } else { - routes += connected_node->GetRoutesTo(n); + routes += connected_node->GetNumberOfRoutesTo(n); } } @@ -2264,25 +2277,29 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo() commands_.append(set_pos_command); // Get bounding rect - QRectF bounding_rect(position_.x() - 0.5, position_.y() - 0.5, 1, 1); + qreal bounding_rect_sz = 1.0; + qreal bounding_rect_half_sz = bounding_rect_sz * 0.5; + QRectF bounding_rect(position_.x() - bounding_rect_half_sz, position_.y() - bounding_rect_half_sz, bounding_rect_sz, bounding_rect_sz); // Start moving other nodes foreach (Node* surrounding, node_->parent()->nodes()) { - QPointF surrounding_position = node_->parent()->GetNodePosition(surrounding, relative_); - if (bounding_rect.contains(surrounding_position) && surrounding != node_) { - QPointF new_pos = surrounding_position; + if (surrounding != node_) { + QPointF surrounding_position = node_->parent()->GetNodePosition(surrounding, relative_); + if (bounding_rect.contains(surrounding_position)) { + QPointF new_pos = surrounding_position; - qreal move_rate = 0.50; + qreal move_rate = 0.50; - if (surrounding_position.y() < position_.y()) { - move_rate = -move_rate; + if (surrounding_position.y() < position_.y()) { + move_rate = -move_rate; + } + + new_pos.setY(new_pos.y() + move_rate); + + auto sur_command = new NodeSetPositionAndShiftSurroundingsCommand(surrounding, relative_, new_pos, true); + sur_command->redo(); + commands_.append(sur_command); } - - new_pos.setY(new_pos.y() + move_rate); - - auto sur_command = new NodeSetPositionAndShiftSurroundingsCommand(surrounding, relative_, new_pos, true); - sur_command->redo(); - commands_.append(sur_command); } } } else { @@ -2294,20 +2311,19 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo() void NodeSetPositionCommand::redo() { - NodeGraph* graph = node_->parent(); - if (!(added_ = !graph->NodeMapContainsNode(node_, relevant_))) { - old_pos_ = graph->GetNodePosition(node_, relevant_); + graph_ = node_->parent(); + if (!(added_ = !graph_->NodeMapContainsNode(node_, relevant_))) { + old_pos_ = graph_->GetNodePosition(node_, relevant_); } - graph->SetNodePosition(node_, relevant_, pos_); + graph_->SetNodePosition(node_, relevant_, pos_); } void NodeSetPositionCommand::undo() { - NodeGraph* graph = node_->parent(); if (added_) { - graph->RemoveNodePosition(node_, relevant_); + graph_->RemoveNodePosition(node_, relevant_); } else { - graph->SetNodePosition(node_, relevant_, old_pos_); + graph_->SetNodePosition(node_, relevant_, old_pos_); } } @@ -2368,4 +2384,32 @@ void NodeRemovePositionFromContextCommand::undo() } } +void NodeRemovePositionFromAllContextsCommand::redo() +{ + NodeGraph *graph = node_->parent(); + + if (points_.empty()) { + // No points yet, let's see what points we should remove + auto map = graph->GetPositionMap(); + for (auto it=map.cbegin(); it!=map.cend(); it++) { + if (it.value().contains(node_)) { + points_.insert({it.key(), it.value().value(node_)}); + } + } + } + + for (auto it=points_.cbegin(); it!=points_.cend(); it++) { + graph->RemoveNodePosition(node_, it->first); + } +} + +void NodeRemovePositionFromAllContextsCommand::undo() +{ + NodeGraph *graph = node_->parent(); + + for (auto it=points_.crbegin(); it!=points_.crend(); it++) { + graph->SetNodePosition(node_, it->first, it->second); + } +} + } diff --git a/app/node/node.h b/app/node/node.h index e0e6fd986..b730d8024 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -557,7 +557,8 @@ public: * Whether to keep traversing down outputs to find this node (TRUE) or stick to immediate outputs * (FALSE). */ - bool OutputsTo(Node* n, bool recursively) const; + bool OutputsTo(Node* n, bool recursively, const OutputConnections &ignore_edges = OutputConnections(), const OutputConnection &added_edge = OutputConnection()) const; + /** * @brief Same as OutputsTo(Node*), but for a node ID rather than a specific instance. */ @@ -581,7 +582,7 @@ public: /** * @brief Determines how many paths go from this node out to another node */ - int GetRoutesTo(Node* n) const; + int GetNumberOfRoutesTo(Node* n) const; /** * @brief Severs all input and output connections @@ -1327,6 +1328,7 @@ private: QPointF old_pos_; bool added_; bool move_deps_; + NodeGraph *graph_; }; @@ -1375,7 +1377,7 @@ private: class NodeSetPositionAsChildCommand : public UndoCommand { public: - NodeSetPositionAsChildCommand(Node* node, Node* parent, Node *relative, int this_index, int child_count, bool shift_surroundings) : + NodeSetPositionAsChildCommand(Node* node, Node* parent, Node *relative, double this_index, int child_count, bool shift_surroundings) : node_(node), parent_(parent), relative_(relative), @@ -1408,7 +1410,7 @@ private: Node* parent_; Node *relative_; - int this_index_; + double this_index_; int child_count_; bool shift_surroundings_; @@ -1417,6 +1419,25 @@ private: }; +class NodePositionCloseChildGapCommand : public UndoCommand +{ +public: + NodePositionCloseChildGapCommand(Node *parent, void *relative, int remove_index, int child_count, bool shift_surroundings); + + virtual Project * GetRelevantProject() const override + { + return parent_->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + Node *parent_; + +}; + class NodeSetPositionToOffsetOfAnotherNodeCommand : public UndoCommand { public: @@ -1474,6 +1495,30 @@ private: }; +class NodeRemovePositionFromAllContextsCommand : public UndoCommand +{ +public: + NodeRemovePositionFromAllContextsCommand(Node *node) : + node_(node) + { + } + + virtual Project * GetRelevantProject() const override + { + return node_->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + Node *node_; + + std::map points_; + +}; + } #endif // NODE_H diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 6416b0a53..bc943140b 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -149,10 +149,10 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) AddNodePosition(it.key(), n); } } - - // Center on something - CenterOnItemsBoundingRect(); } + + // Center on something + QMetaObject::invokeMethod(this, &NodeView::CenterOnItemsBoundingRect, Qt::QueuedConnection); } } } @@ -171,14 +171,25 @@ void NodeView::DeleteSelected() MultiUndoCommand* command = new MultiUndoCommand(); { + // First remove any selected edges QVector selected_edges = scene_.GetSelectedEdges(); - foreach (NodeViewEdge* edge, selected_edges) { - command->add_child(new NodeEdgeRemoveCommand(edge->output(), edge->input())); + if (!selected_edges.isEmpty()) { + Node::OutputConnections removed_connections(selected_edges.size()); + + for (int i=0; iadd_child(new NodeEdgeRemoveCommand(edge->output(), edge->input())); + removed_connections[i] = {edge->output(), edge->input()}; + } + + // Update contexts + UpdateContextsFromEdgeRemove(command, removed_connections); } } { + // Secondly remove any nodes QVector selected_nodes = scene_.GetSelectedNodes(); // Ensure no nodes are "undeletable" @@ -408,17 +419,69 @@ void NodeView::ZoomOut() void NodeView::keyPressEvent(QKeyEvent *event) { - super::keyPressEvent(event); + switch (event->key()) { + case Qt::Key_Left: + case Qt::Key_Right: + case Qt::Key_Up: + case Qt::Key_Down: + { + if (graph_) { + MultiUndoCommand *pos_command = new MultiUndoCommand(); + foreach (Node *n, selected_nodes_) { + foreach (Node *context, filter_nodes_) { + if (graph_->GetNodesForContext(context).contains(n)) { + QPointF old_pos = graph_->GetNodePosition(n, context); - if (event->key() == Qt::Key_Escape && !attached_items_.isEmpty()) { - DetachItemsFromCursor(); + // Determine one pixel in scene units + double movement_amt = 1.0 / scale_; - // We undo the last action which SHOULD be adding the node - if (paste_command_) { - paste_command_->undo(); - delete paste_command_; - paste_command_ = nullptr; + // 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::ScreenToNodePoint(node_movement, scene_.GetFlowDirection()); + + // Move command + pos_command->add_child(new NodeSetPositionCommand(n, context, old_pos + node_movement, false)); + } + } + } + Core::instance()->undo_stack()->pushIfHasChildren(pos_command); } + break; + } + case Qt::Key_Escape: + if (!attached_items_.isEmpty()) { + DetachItemsFromCursor(); + + // We undo the last action which SHOULD be adding the node + if (paste_command_) { + paste_command_->undo(); + delete paste_command_; + paste_command_ = nullptr; + } + + break; + } + + /* fall through */ + default: + super::keyPressEvent(event); + break; } } @@ -426,19 +489,20 @@ void NodeView::mousePressEvent(QMouseEvent *event) { if (HandPress(event)) return; - QGraphicsItem* item = itemAt(event->pos()); - if (event->button() == Qt::LeftButton) { - NodeViewEdge* edge_item = dynamic_cast(item); - if (edge_item && edge_item->arrow_bounding_rect().contains(mapToScene(event->pos()))) { - create_edge_src_ = scene_.NodeToUIObject(edge_item->output().node()); - create_edge_src_output_ = edge_item->output().output(); - create_edge_ = edge_item; - create_edge_already_exists_ = true; - return; + foreach (NodeViewEdge *edge_item, scene_.edges()) { + if (edge_item->arrow_bounding_rect().contains(mapToScene(event->pos()))) { + create_edge_src_ = scene_.NodeToUIObject(edge_item->output().node()); + create_edge_src_output_ = edge_item->output().output(); + create_edge_ = edge_item; + create_edge_already_exists_ = true; + return; + } } } + QGraphicsItem* item = itemAt(event->pos()); + if (event->button() == Qt::RightButton) { if (!item || !item->isSelected()) { // Qt doesn't do this by default for some reason @@ -623,13 +687,26 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) if (HandRelease(event)) return; if (create_edge_) { + // We are creating a new edge or moving an existing one MultiUndoCommand* command = new MultiUndoCommand(); + Node::OutputConnections removed_edges; + Node::OutputConnection added_edge; + + bool reconnected_to_itself = false; + if (create_edge_already_exists_) { - if (!create_edge_->IsConnected()) { + if (create_edge_dst_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())); + + // Update contexts for edge removal + removed_edges.push_back({create_edge_->output(), create_edge_->input()}); } } else { + // We're creating a new edge, which means this UI object is only temporary delete create_edge_; } @@ -645,15 +722,37 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) create_edge_dst_->setZValue(0); } - if (create_edge_dst_input_.IsValid()) { + NodeInput &creating_input = create_edge_dst_input_; + if (creating_input.IsValid()) { // Make connection - command->add_child(new NodeEdgeAddCommand(NodeOutput(create_edge_src_->GetNode(), create_edge_src_output_), create_edge_dst_input_)); - create_edge_dst_input_.Reset(); + if (!reconnected_to_itself) { + NodeOutput creating_output(create_edge_src_->GetNode(), create_edge_src_output_); + + if (creating_input.IsConnected()) { + Node::OutputConnection existing_edge_to_remove = {creating_input.GetConnectedOutput(), creating_input}; + command->add_child(new NodeEdgeRemoveCommand(existing_edge_to_remove.first, existing_edge_to_remove.second)); + removed_edges.push_back(existing_edge_to_remove); + } + + command->add_child(new NodeEdgeAddCommand(creating_output, creating_input)); + added_edge = {creating_output, creating_input}; + } + + creating_input.Reset(); } create_edge_dst_ = nullptr; } + // Update contexts + if (!removed_edges.empty()) { + UpdateContextsFromEdgeRemove(command, removed_edges); + } + + if (added_edge.first.IsValid()) { + UpdateContextsFromEdgeAdd(command, added_edge, removed_edges); + } + Core::instance()->undo_stack()->pushIfHasChildren(command); return; } @@ -668,44 +767,105 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) command->add_child(paste_command_); paste_command_ = nullptr; } - - if (attached_items_.size() == 1) { - Node* dropping_node = attached_items_.first().item->GetNode(); - - if (drop_edge_) { - // Remove old edge - command->add_child(new NodeEdgeRemoveCommand(drop_edge_->output(), drop_edge_->input())); - - // Place new edges - command->add_child(new NodeEdgeAddCommand(drop_edge_->output(), drop_input_)); - command->add_child(new NodeEdgeAddCommand(dropping_node, drop_edge_->input())); - } - - drop_edge_ = nullptr; - } - - DetachItemsFromCursor(); } - for (auto it=positions_.begin(); it!=positions_.end(); it++) { - NodeViewItem *item = it.key(); - Position &pos_data = it.value(); - QPointF current_item_pos = item->GetNodePosition(); - Node *node = pos_data.node; + { + // If any node positions changed, set them in their contexts now + MultiUndoCommand *set_pos_command = new MultiUndoCommand(); + for (auto it=positions_.begin(); it!=positions_.end(); it++) { + NodeViewItem *item = it.key(); + Position &pos_data = it.value(); + QPointF current_item_pos = item->GetNodePosition(); + Node *node = pos_data.node; - if (pos_data.original_item_pos != current_item_pos) { - QPointF diff = current_item_pos - pos_data.original_item_pos; + if (pos_data.original_item_pos != current_item_pos) { + QPointF diff = current_item_pos - pos_data.original_item_pos; - foreach (Node *context, filter_nodes_) { - if (graph_->ContextContainsNode(node, context)) { - QPointF current_node_pos_in_context = graph_->GetNodePosition(node, context); - current_node_pos_in_context += diff; - command->add_child(new NodeSetPositionCommand(node, context, current_node_pos_in_context, false)); + foreach (Node *context, filter_nodes_) { + if (graph_->ContextContainsNode(node, context)) { + QPointF current_node_pos_in_context = graph_->GetNodePosition(node, context); + current_node_pos_in_context += diff; + set_pos_command->add_child(new NodeSetPositionCommand(node, context, current_node_pos_in_context, false)); + } + } + + pos_data.original_item_pos = current_item_pos; + } + } + if (set_pos_command->child_count()) { + set_pos_command->redo(); + command->add_child(set_pos_command); + } else { + delete set_pos_command; + } + } + + + if (!attached_items_.isEmpty()) { + { + // Dropped attached item onto an edge, connect it between them + MultiUndoCommand *drop_edge_command = new MultiUndoCommand(); + if (attached_items_.size() == 1) { + Node* dropping_node = attached_items_.first().item->GetNode(); + + if (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(); + command->add_child(drop_edge_command); + } else { + delete drop_edge_command; + } + } + + { + // Remove from context any nodes that don't specifically output to said context + MultiUndoCommand *remove_pos_command = new MultiUndoCommand(); + + foreach (const AttachedItem &attached, attached_items_) { + MultiUndoCommand *remove_pos_subcommand = new MultiUndoCommand(); + Node *attached_node = scene_.item_map().key(attached.item); + + bool removed = false; + QVector relevant_contexts; + foreach (Node *context, filter_nodes_) { + if (attached_node->OutputsTo(context, true)) { + relevant_contexts.append(context); + } else { + remove_pos_subcommand->add_child(new NodeRemovePositionFromContextCommand(attached_node, context)); + removed = true; + } + } + + if (removed && !relevant_contexts.isEmpty()) { + foreach (Node *relevant, relevant_contexts) { + remove_pos_subcommand->add_child(new NodeSetPositionCommand(attached_node, relevant, GetEstimatedPositionForContext(attached.item, relevant), false)); + } + + remove_pos_command->add_child(remove_pos_subcommand); + } else { + delete remove_pos_subcommand; } } - pos_data.original_item_pos = current_item_pos; + if (remove_pos_command->child_count()) { + remove_pos_command->redo(); + command->add_child(remove_pos_command); + } else { + delete remove_pos_command; + } } + + DetachItemsFromCursor(); } Core::instance()->undo_stack()->pushIfHasChildren(command); @@ -991,19 +1151,29 @@ void NodeView::RemoveNodePosition(Node *node, Node *relative) { if (filter_mode_ == kFilterShowSelective) { if (filter_nodes_.contains(relative)) { - // Determine if any other contexts have this node - bool found = false; + NodeViewItem *item = scene_.item_map().value(node); - foreach (Node *context, filter_nodes_) { - if (graph_->ContextContainsNode(node, context)) { - found = true; - break; + if (item && !item->GetPreventRemoving()) { + // Determine if any other contexts have this node + bool found = false; + + foreach (Node *context, filter_nodes_) { + if (graph_->ContextContainsNode(node, context)) { + found = true; + break; + } } - } - if (!found) { - positions_.remove(scene_.item_map().value(node)); - scene_.RemoveNode(node); + if (!found) { + foreach (const Node::OutputConnection &oc, node->output_connections()) { + scene_.RemoveEdge(oc.first, oc.second); + } + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + scene_.RemoveEdge(it->second, it->first); + } + positions_.remove(item); + scene_.RemoveNode(node); + } } } } @@ -1098,6 +1268,22 @@ void NodeView::ZoomIntoCursorPosition(QWheelEvent *event, double multiplier, con } } +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); +} + void NodeView::ZoomFromKeyboard(double multiplier) { QPoint cursor_pos = mapFromGlobal(QCursor::pos()); @@ -1110,6 +1296,149 @@ void NodeView::ZoomFromKeyboard(double multiplier) ZoomIntoCursorPosition(nullptr, multiplier, cursor_pos); } +bool NodeView::DetermineIfNodeIsFloatingInContext(Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge) +{ + // Determines whether `node` outputs to another node in `context` besides `source` + foreach (const Node::OutputConnection &conn, node->output_connections()) { + Node *output_candidate = conn.second.node(); + + if (output_candidate == source) { + continue; + } + + if (graph_->ContextContainsNode(output_candidate, context)) { + if (!output_candidate->OutputsTo(source, true, removed_edges, added_edge)) { + return true; + } + } + } + + return false; +} + +void NodeView::UpdateContextsFromEdgeRemove(MultiUndoCommand *command, const Node::OutputConnections &remove_edges) +{ + // For each edge we remove, determine if we should remove the node from a context as well + foreach (const Node::OutputConnection &edge, remove_edges) { + Node *output_node = edge.first.node(); + QVector contexts_to_remove_from; + int contexts_containing = 0; + + for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { + Node *context = it.key(); + + if (it.value().contains(output_node)) { + bool currently_outputs = output_node->OutputsTo(context, true); + bool will_output_after_operation = output_node->OutputsTo(context, true, remove_edges); + + if (currently_outputs && !will_output_after_operation) { + // Will remove + contexts_to_remove_from.append(context); + } + + contexts_containing++; + } + } + + // Removing from all current contexts, convert to a floating node (i.e. don't remove from the context) + if (contexts_to_remove_from.size() != contexts_containing) { + // Not removing from all contexts, can remove + bool removing_from_all_current_contexts = true; + + foreach (Node *context, filter_nodes_) { + if (graph_->ContextContainsNode(output_node, context)) { + if (!contexts_to_remove_from.contains(context)) { + removing_from_all_current_contexts = false; + break; + } + } + } + + foreach (Node *context, contexts_to_remove_from) { + RecursivelyRemoveFloatingNodeFromContext(command, output_node, context, output_node, remove_edges, Node::OutputConnection(), removing_from_all_current_contexts); + } + } + } +} + +void NodeView::RecursivelyRemoveFloatingNodeFromContext(MultiUndoCommand *command, Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge, bool prevent_removing) +{ + if (prevent_removing) { + command->add_child(new NodeViewItemPreventRemovingCommand(this, node, true)); + } + + command->add_child(new NodeRemovePositionFromContextCommand(node, context)); + + // Remove any dependency from the context that's also floating + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + Node *dependency = it->second.node(); + + // Determine if this node happens to output to anything else in the context (which may be + // another floating node that won't be removed by this operation) + if (!DetermineIfNodeIsFloatingInContext(dependency, context, source, removed_edges, added_edge)) { + RecursivelyRemoveFloatingNodeFromContext(command, dependency, context, source, removed_edges, added_edge, prevent_removing); + } + } +} + +void NodeView::RecursivelyAddNodeToContext(MultiUndoCommand *command, Node *node, Node *context) +{ + command->add_child(new NodeSetPositionCommand(node, context, GetEstimatedPositionForContext(scene_.item_map().value(node), context), false)); + + // Add dependency + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + Node *dependency = it->second.node(); + RecursivelyAddNodeToContext(command, dependency, context); + } +} + +void NodeView::UpdateContextsFromEdgeAdd(MultiUndoCommand *command, const Node::OutputConnection &added_edge, const Node::OutputConnections &removed_edges) +{ + // Determine if node currently does NOT output to a context that it WILL after this operation + QVector contexts_to_add_to; + Node *connecting_node = added_edge.first.node(); + Node *input_node = added_edge.second.node(); + for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { + if (it.value().contains(input_node)) { + contexts_to_add_to.append(it.key()); + } + } + + if (!contexts_to_add_to.isEmpty()) { + // Determine whether the node is currently "floating", i.e. it outputs to none of the contexts + // that it currently belongs to. If so, we will take ownership of it with this node. + bool node_is_floating = true; + QVector current_contexts; + for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { + if (it.value().contains(connecting_node)) { + if (connecting_node->OutputsTo(it.key(), true, removed_edges)) { + node_is_floating = false; + break; + } else { + current_contexts.append(it.key()); + } + } + } + + if (node_is_floating) { + // This action will unfloat this node, so remove it from all current contexts + foreach (Node *context, current_contexts) { + RecursivelyRemoveFloatingNodeFromContext(command, connecting_node, context, connecting_node, removed_edges, added_edge, false); + } + } + + // Add nodes to contexts + foreach (Node *context, contexts_to_add_to) { + RecursivelyAddNodeToContext(command, connecting_node, context); + } + } +} + +QPointF NodeView::GetEstimatedPositionForContext(NodeViewItem *item, Node *context) const +{ + return item->GetNodePosition() - context_offsets_.value(context); +} + NodeView::NodeViewAttachNodesToCursor::NodeViewAttachNodesToCursor(NodeView *view, const QVector &nodes) : view_(view), nodes_(nodes) @@ -1132,4 +1461,23 @@ Project *NodeView::NodeViewAttachNodesToCursor::GetRelevantProject() const return dynamic_cast(view_->graph_); } +void NodeView::NodeViewItemPreventRemovingCommand::redo() +{ + NodeViewItem *item = view_->scene_.item_map().value(node_); + + if (item) { + old_prevent_removing_ = item->GetPreventRemoving(); + item->SetPreventRemoving(new_prevent_removing_); + } +} + +void NodeView::NodeViewItemPreventRemovingCommand::undo() +{ + NodeViewItem *item = view_->scene_.item_map().value(node_); + + if (item) { + item->SetPreventRemoving(old_prevent_removing_); + } +} + } diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 05b4f8ab4..1ba86998c 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -91,6 +91,8 @@ protected: virtual void ZoomIntoCursorPosition(QWheelEvent *event, double multiplier, const QPointF &cursor_pos) override; + virtual bool event(QEvent *event) override; + private: void AttachNodesToCursor(const QVector &nodes); @@ -107,6 +109,14 @@ private: void ZoomFromKeyboard(double multiplier); + bool DetermineIfNodeIsFloatingInContext(Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge); + void UpdateContextsFromEdgeRemove(MultiUndoCommand *command, const Node::OutputConnections &remove_edges); + void UpdateContextsFromEdgeAdd(MultiUndoCommand *command, const Node::OutputConnection &added_edge, const Node::OutputConnections &removed_edges = Node::OutputConnections()); + void RecursivelyAddNodeToContext(MultiUndoCommand *command, Node *node, Node *context); + void RecursivelyRemoveFloatingNodeFromContext(MultiUndoCommand *command, Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge, bool prevent_removing); + + QPointF GetEstimatedPositionForContext(NodeViewItem *item, Node *context) const; + class NodeViewAttachNodesToCursor : public UndoCommand { public: @@ -132,6 +142,32 @@ private: QPointF original_pos; }; + class NodeViewItemPreventRemovingCommand : public UndoCommand + { + public: + NodeViewItemPreventRemovingCommand(NodeView *view, Node *node, bool prevent_removing) : + view_(view), + node_(node), + new_prevent_removing_(prevent_removing) + {} + + virtual void redo() override; + + virtual void undo() override; + + virtual Project * GetRelevantProject() const override + { + return node_->project(); + } + + private: + NodeView *view_; + Node *node_; + bool new_prevent_removing_; + bool old_prevent_removing_; + + }; + QList attached_items_; NodeViewEdge* drop_edge_; diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index 7224edc4d..7978d6c32 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -45,7 +45,8 @@ NodeViewItem::NodeViewItem(QGraphicsItem *parent) : expanded_(false), hide_titlebar_(false), highlighted_index_(-1), - flow_dir_(NodeViewCommon::kLeftToRight) + flow_dir_(NodeViewCommon::kLeftToRight), + prevent_removing_(false) { // Set flags for this widget setFlag(QGraphicsItem::ItemIsMovable); @@ -68,31 +69,7 @@ NodeViewItem::NodeViewItem(QGraphicsItem *parent) : QPointF NodeViewItem::GetNodePosition() const { - QPointF node_pos; - - qreal adjusted_x = pos().x() / DefaultItemHorizontalPadding(); - qreal adjusted_y = pos().y() / DefaultItemVerticalPadding(); - - switch (flow_dir_) { - case NodeViewCommon::kLeftToRight: - node_pos.setX(adjusted_x); - node_pos.setY(adjusted_y); - break; - case NodeViewCommon::kRightToLeft: - node_pos.setX(-adjusted_x); - node_pos.setY(adjusted_y); - break; - case NodeViewCommon::kTopToBottom: - node_pos.setX(adjusted_y); - node_pos.setY(adjusted_x); - break; - case NodeViewCommon::kBottomToTop: - node_pos.setX(-adjusted_y); - node_pos.setY(adjusted_x); - break; - } - - return node_pos; + return ScreenToNodePoint(pos(), flow_dir_); } void NodeViewItem::SetNodePosition(const QPointF &pos) @@ -122,24 +99,88 @@ int NodeViewItem::DefaultItemBorder() return QFontMetrics(QFont()).height() / 12; } -qreal NodeViewItem::DefaultItemHorizontalPadding() const +QPointF NodeViewItem::NodeToScreenPoint(QPointF p, NodeViewCommon::FlowDirection direction) { - if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) { + switch (direction) { + case NodeViewCommon::kLeftToRight: + // NodeGraphs are always left-to-right internally, no need to translate + break; + case NodeViewCommon::kRightToLeft: + // Invert X value + p.setX(-p.x()); + break; + case NodeViewCommon::kTopToBottom: + // Swap X/Y + p = QPointF(p.y(), p.x()); + break; + case NodeViewCommon::kBottomToTop: + // Swap X/Y and invert Y + p = QPointF(p.y(), -p.x()); + break; + } + + // Multiply by item sizes for this direction + p.setX(p.x() * DefaultItemHorizontalPadding(direction)); + p.setY(p.y() * DefaultItemVerticalPadding(direction)); + + return p; +} + +QPointF NodeViewItem::ScreenToNodePoint(QPointF p, NodeViewCommon::FlowDirection direction) +{ + // Divide by item sizes for this direction + p.setX(p.x() / DefaultItemHorizontalPadding(direction)); + p.setY(p.y() / DefaultItemVerticalPadding(direction)); + + switch (direction) { + case NodeViewCommon::kLeftToRight: + // NodeGraphs are always left-to-right internally, no need to translate + break; + case NodeViewCommon::kRightToLeft: + // Invert X value + p.setX(-p.x()); + break; + case NodeViewCommon::kTopToBottom: + // Swap X/Y + p = QPointF(p.y(), p.x()); + break; + case NodeViewCommon::kBottomToTop: + // Swap X/Y and invert Y + p = QPointF(-p.y(), p.x()); + break; + } + + return p; +} + +qreal NodeViewItem::DefaultItemHorizontalPadding(NodeViewCommon::FlowDirection dir) +{ + if (NodeViewCommon::GetFlowOrientation(dir) == Qt::Horizontal) { return DefaultItemWidth() * 1.5; } else { return DefaultItemWidth() * 1.25; } } -qreal NodeViewItem::DefaultItemVerticalPadding() const +qreal NodeViewItem::DefaultItemVerticalPadding(NodeViewCommon::FlowDirection dir) { - if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) { + if (NodeViewCommon::GetFlowOrientation(dir) == Qt::Horizontal) { return DefaultItemHeight() * 1.5; } else { return DefaultItemHeight() * 2.0; } } +qreal NodeViewItem::DefaultItemHorizontalPadding() const +{ + return DefaultItemHorizontalPadding(flow_dir_); +} + +qreal NodeViewItem::DefaultItemVerticalPadding() const +{ + return DefaultItemVerticalPadding(flow_dir_); +} + void NodeViewItem::AddEdge(NodeViewEdge *edge) { edges_.append(edge); @@ -479,26 +520,7 @@ QPointF NodeViewItem::GetInputPointInternal(int index, const QPointF& source_pos void NodeViewItem::UpdateNodePosition() { - const QPointF &pos = cached_node_pos_; - - switch (flow_dir_) { - case NodeViewCommon::kLeftToRight: - setPos(pos.x() * DefaultItemHorizontalPadding(), - pos.y() * DefaultItemVerticalPadding()); - break; - case NodeViewCommon::kRightToLeft: - setPos(-pos.x() * DefaultItemHorizontalPadding(), - pos.y() * DefaultItemVerticalPadding()); - break; - case NodeViewCommon::kTopToBottom: - setPos(pos.y() * DefaultItemHorizontalPadding(), - pos.x() * DefaultItemVerticalPadding()); - break; - case NodeViewCommon::kBottomToTop: - setPos(pos.y() * DefaultItemHorizontalPadding(), - -pos.x() * DefaultItemVerticalPadding()); - break; - } + setPos(NodeToScreenPoint(cached_node_pos_, flow_dir_)); } } diff --git a/app/widget/nodeview/nodeviewitem.h b/app/widget/nodeview/nodeviewitem.h index 1edab3aa5..ffe3e0859 100644 --- a/app/widget/nodeview/nodeviewitem.h +++ b/app/widget/nodeview/nodeviewitem.h @@ -95,8 +95,12 @@ public: static int DefaultItemBorder(); - qreal DefaultItemHorizontalPadding() const; + static QPointF NodeToScreenPoint(QPointF p, NodeViewCommon::FlowDirection direction); + static QPointF ScreenToNodePoint(QPointF p, NodeViewCommon::FlowDirection direction); + static qreal DefaultItemHorizontalPadding(NodeViewCommon::FlowDirection dir); + static qreal DefaultItemVerticalPadding(NodeViewCommon::FlowDirection dir); + qreal DefaultItemHorizontalPadding() const; qreal DefaultItemVerticalPadding() const; void AddEdge(NodeViewEdge* edge); @@ -111,6 +115,16 @@ public: void SetHighlightedIndex(int index); + void SetPreventRemoving(bool e) + { + prevent_removing_ = e; + } + + bool GetPreventRemoving() const + { + return prevent_removing_; + } + protected: virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; @@ -174,6 +188,8 @@ private: QPointF cached_node_pos_; + bool prevent_removing_; + }; } diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index 267a3b164..4e1ca2207 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -200,7 +200,7 @@ int NodeViewScene::DetermineWeight(Node *n) int weight = 0; foreach (Node* i, inputs) { - if (i->GetRoutesTo(n) == 1) { + if (i->GetNumberOfRoutesTo(n) == 1) { weight += DetermineWeight(i); } } diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 011fee537..7db76d1ba 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -754,9 +754,15 @@ void MainWindow::FocusedPanelChanged(PanelWidget *panel) // Signal project panel focus UpdateTitle(); if (project->project()) { - QVector context = {project->project()->root()}; - node_panel_->SetGraph(project->project(), context); - node_panel_->SelectWithDependencies(context, false); + node_panel_->SetGraph(project->project(), {project->project()->root()}); + + bool center = true; + auto selected = project->SelectedItems(); + if (selected.isEmpty()) { + selected.append(project->project()->root()); + center = false; + } + node_panel_->Select(selected, center); } } } From ddee0f97d687a69b633e1bad5c58da99b992428a Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 9 Jul 2021 18:00:27 -0700 Subject: [PATCH 30/72] nodeview: start wip for "show all" mode --- app/widget/nodeview/nodeview.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index bc943140b..1b97a4c23 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -122,7 +122,15 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) if (refresh_required && nodes_visible) { if (filter_mode_ == kFilterShowAll) { - // FIXME: Implement + /* = WIP = + for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { + Node *context = it.key(); + const NodeGraph::PositionMap &map = it.value(); + + for (auto jt=map.cbegin(); jt!=map.cend(); jt++) { + + } + }*/ } else { // Reserve an arbitrary number to reduce the amount of reallocations qreal last_offset = 0; From 94e6fa843cef2698ef2bb2bbd1c5b00ebf15fc7f Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 9 Jul 2021 21:43:19 -0700 Subject: [PATCH 31/72] nodeview: implemented minimap --- app/widget/nodeview/CMakeLists.txt | 2 + app/widget/nodeview/nodeview.cpp | 48 ++++++++ app/widget/nodeview/nodeview.h | 11 ++ app/widget/nodeview/nodeviewminimap.cpp | 141 ++++++++++++++++++++++++ app/widget/nodeview/nodeviewminimap.h | 74 +++++++++++++ 5 files changed, 276 insertions(+) create mode 100644 app/widget/nodeview/nodeviewminimap.cpp create mode 100644 app/widget/nodeview/nodeviewminimap.h diff --git a/app/widget/nodeview/CMakeLists.txt b/app/widget/nodeview/CMakeLists.txt index 00e78e7db..ee13270b6 100644 --- a/app/widget/nodeview/CMakeLists.txt +++ b/app/widget/nodeview/CMakeLists.txt @@ -23,6 +23,8 @@ set(OLIVE_SOURCES widget/nodeview/nodeviewedge.h widget/nodeview/nodeviewitem.cpp widget/nodeview/nodeviewitem.h + widget/nodeview/nodeviewminimap.cpp + widget/nodeview/nodeviewminimap.h widget/nodeview/nodeviewscene.cpp widget/nodeview/nodeviewscene.h widget/nodeview/nodeviewundo.cpp diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 1b97a4c23..500d04684 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -64,6 +64,13 @@ NodeView::NodeView(QWidget *parent) : UpdateSceneBoundingRect(); connect(&scene_, &QGraphicsScene::changed, this, &NodeView::UpdateSceneBoundingRect); + + minimap_ = new NodeViewMiniMap(&scene_, this); + minimap_->show(); + connect(minimap_, &NodeViewMiniMap::Resized, this, &NodeView::RepositionMiniMap); + connect(minimap_, &NodeViewMiniMap::MoveToScenePoint, this, &NodeView::MoveToScenePoint); + connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &NodeView::UpdateViewportOnMiniMap); + connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &NodeView::UpdateViewportOnMiniMap); } NodeView::~NodeView() @@ -881,6 +888,13 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) super::mouseReleaseEvent(event); } +void NodeView::resizeEvent(QResizeEvent *event) +{ + super::resizeEvent(event); + + RepositionMiniMap(); +} + void NodeView::UpdateSelectionCache() { QVector current_selection = scene_.GetSelectedNodes(); @@ -1204,6 +1218,40 @@ void NodeView::CenterOnItemsBoundingRect() centerOn(scene_.itemsBoundingRect().center()); } +void NodeView::RepositionMiniMap() +{ + 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); + + UpdateViewportOnMiniMap(); + } +} + +void NodeView::UpdateViewportOnMiniMap() +{ + if (minimap_->isVisible()) { + minimap_->SetViewportRect(mapToScene(viewport()->rect())); + } +} + +void NodeView::MoveToScenePoint(const QPointF &pos) +{ + centerOn(pos); +} + void NodeView::AttachNodesToCursor(const QVector &nodes) { QVector items(nodes.size()); diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 1ba86998c..6c02ae837 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -27,6 +27,7 @@ #include "node/graph.h" #include "node/nodecopypaste.h" #include "nodeviewedge.h" +#include "nodeviewminimap.h" #include "nodeviewscene.h" #include "widget/handmovableview/handmovableview.h" @@ -89,6 +90,8 @@ protected: virtual void mouseMoveEvent(QMouseEvent *event) override; virtual void mouseReleaseEvent(QMouseEvent* event) override; + virtual void resizeEvent(QResizeEvent *event) override; + virtual void ZoomIntoCursorPosition(QWheelEvent *event, double multiplier, const QPointF &cursor_pos) override; virtual bool event(QEvent *event) override; @@ -135,6 +138,8 @@ private: }; + NodeViewMiniMap *minimap_; + NodeGraph* graph_; struct AttachedItem { @@ -257,6 +262,12 @@ private slots: void CenterOnItemsBoundingRect(); + void RepositionMiniMap(); + + void UpdateViewportOnMiniMap(); + + void MoveToScenePoint(const QPointF &pos); + }; } diff --git a/app/widget/nodeview/nodeviewminimap.cpp b/app/widget/nodeview/nodeviewminimap.cpp new file mode 100644 index 000000000..9be32452a --- /dev/null +++ b/app/widget/nodeview/nodeviewminimap.cpp @@ -0,0 +1,141 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "nodeviewminimap.h" + +#include + +namespace olive { + +#define super QGraphicsView + +NodeViewMiniMap::NodeViewMiniMap(NodeViewScene *scene, QWidget *parent) : + super(parent), + resizing_(false) +{ + connect(scene, &QGraphicsScene::sceneRectChanged, this, &NodeViewMiniMap::SceneChanged); + setScene(scene); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setViewportUpdateMode(FullViewportUpdate); + + QMetaObject::invokeMethod(this, &NodeViewMiniMap::SetDefaultSize, Qt::QueuedConnection); + + resize_triangle_sz_ = fontMetrics().height() / 2; +} + +void NodeViewMiniMap::SetViewportRect(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(); + + SceneChanged(sceneRect()); +} + +void NodeViewMiniMap::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + if (event->pos().x() <= resize_triangle_sz_ && event->pos().y() <= resize_triangle_sz_) { + // Resizing! + resizing_ = true; + resize_anchor_ = QCursor::pos(); + } else { + EmitMoveSignal(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 { + EmitMoveSignal(event); + } + } +} + +void NodeViewMiniMap::mouseReleaseEvent(QMouseEvent *event) +{ + resizing_ = false; +} + +void NodeViewMiniMap::SceneChanged(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::SetDefaultSize() +{ + if (parentWidget()) { + resize(parentWidget()->width()/4, parentWidget()->height()/4); + } +} + +void NodeViewMiniMap::EmitMoveSignal(QMouseEvent *event) +{ + emit MoveToScenePoint(mapToScene(event->pos())); +} + +} diff --git a/app/widget/nodeview/nodeviewminimap.h b/app/widget/nodeview/nodeviewminimap.h new file mode 100644 index 000000000..a63db1993 --- /dev/null +++ b/app/widget/nodeview/nodeviewminimap.h @@ -0,0 +1,74 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef NODEVIEWMINIMAP_H +#define NODEVIEWMINIMAP_H + +#include + +#include "nodeviewscene.h" + +namespace olive { + +class NodeViewMiniMap : public QGraphicsView +{ + Q_OBJECT +public: + NodeViewMiniMap(NodeViewScene *scene, QWidget *parent = nullptr); + +public slots: + void SetViewportRect(const QPolygonF &rect); + +signals: + void Resized(); + + void MoveToScenePoint(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 SceneChanged(const QRectF &bounding); + + void SetDefaultSize(); + +private: + void EmitMoveSignal(QMouseEvent *event); + + int resize_triangle_sz_; + + QPolygonF viewport_rect_; + + bool resizing_; + + QPoint resize_anchor_; + +}; + +} + +#endif // NODEVIEWMINIMAP_H From 652a53c18ec19775e87950e670f70c74c3574596 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 9 Jul 2021 21:57:53 -0700 Subject: [PATCH 32/72] nodeview: added rudimentary toolbar Only button there is a toggle for the mini-map. --- app/panel/node/node.cpp | 20 ++++++++++- app/panel/node/node.h | 1 + app/widget/nodeview/CMakeLists.txt | 2 ++ app/widget/nodeview/nodeview.h | 6 ++++ app/widget/nodeview/nodeviewtoolbar.cpp | 47 +++++++++++++++++++++++++ app/widget/nodeview/nodeviewtoolbar.h | 38 ++++++++++++++++++++ 6 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 app/widget/nodeview/nodeviewtoolbar.cpp create mode 100644 app/widget/nodeview/nodeviewtoolbar.h diff --git a/app/panel/node/node.cpp b/app/panel/node/node.cpp index 18e207918..10ec25ce7 100644 --- a/app/panel/node/node.cpp +++ b/app/panel/node/node.cpp @@ -20,20 +20,38 @@ #include "node.h" +#include + namespace olive { NodePanel::NodePanel(QWidget *parent) : PanelWidget(QStringLiteral("NodePanel"), parent) { + QWidget *outer_widget = new QWidget(this); + + QVBoxLayout *outer_layout = new QVBoxLayout(outer_widget); + outer_layout->setMargin(0); + + NodeViewToolBar *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::MiniMapEnabledToggled, node_view_, &NodeView::SetMiniMapEnabled); + + // Set defaults + toolbar->SetMiniMapEnabled(true); + node_view_->SetMiniMapEnabled(true); // Connect node view signals to this panel connect(node_view_, &NodeView::NodesSelected, this, &NodePanel::NodesSelected); connect(node_view_, &NodeView::NodesDeselected, this, &NodePanel::NodesDeselected); // Set it as the main widget of this panel - SetWidgetWithPadding(node_view_); + SetWidgetWithPadding(outer_widget); // Set strings Retranslate(); diff --git a/app/panel/node/node.h b/app/panel/node/node.h index 447e88f9d..607671055 100644 --- a/app/panel/node/node.h +++ b/app/panel/node/node.h @@ -22,6 +22,7 @@ #define NODEPANEL_H #include "widget/nodeview/nodeview.h" +#include "widget/nodeview/nodeviewtoolbar.h" #include "widget/panel/panel.h" namespace olive { diff --git a/app/widget/nodeview/CMakeLists.txt b/app/widget/nodeview/CMakeLists.txt index ee13270b6..e73798ae5 100644 --- a/app/widget/nodeview/CMakeLists.txt +++ b/app/widget/nodeview/CMakeLists.txt @@ -27,6 +27,8 @@ set(OLIVE_SOURCES widget/nodeview/nodeviewminimap.h widget/nodeview/nodeviewscene.cpp widget/nodeview/nodeviewscene.h + widget/nodeview/nodeviewtoolbar.cpp + widget/nodeview/nodeviewtoolbar.h widget/nodeview/nodeviewundo.cpp widget/nodeview/nodeviewundo.h PARENT_SCOPE diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 6c02ae837..7cec30e85 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -78,6 +78,12 @@ public: void ZoomOut(); +public slots: + void SetMiniMapEnabled(bool e) + { + minimap_->setVisible(e); + } + signals: void NodesSelected(const QVector& nodes); diff --git a/app/widget/nodeview/nodeviewtoolbar.cpp b/app/widget/nodeview/nodeviewtoolbar.cpp new file mode 100644 index 000000000..29fc65847 --- /dev/null +++ b/app/widget/nodeview/nodeviewtoolbar.cpp @@ -0,0 +1,47 @@ +#include "nodeviewtoolbar.h" + +#include +#include + +namespace olive { + +#define super QWidget + +NodeViewToolBar::NodeViewToolBar(QWidget *parent) : + QWidget(parent) +{ + QHBoxLayout *layout = new QHBoxLayout(this); + layout->setMargin(0); + + minimap_btn_ = new QPushButton(); + minimap_btn_->setCheckable(true); + connect(minimap_btn_, &QPushButton::clicked, this, &NodeViewToolBar::MiniMapEnabledToggled); + layout->addWidget(minimap_btn_); + + layout->addStretch(); + + Retranslate(); + UpdateIcons(); +} + +void NodeViewToolBar::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::LanguageChange) { + Retranslate(); + } else if (e->type() == QEvent::StyleChange) { + UpdateIcons(); + } + super::changeEvent(e); +} + +void NodeViewToolBar::Retranslate() +{ + minimap_btn_->setText(tr("Mini-Map")); + minimap_btn_->setToolTip(tr("Toggle Mini-Map")); +} + +void NodeViewToolBar::UpdateIcons() +{ +} + +} diff --git a/app/widget/nodeview/nodeviewtoolbar.h b/app/widget/nodeview/nodeviewtoolbar.h new file mode 100644 index 000000000..60585fc5d --- /dev/null +++ b/app/widget/nodeview/nodeviewtoolbar.h @@ -0,0 +1,38 @@ +#ifndef NODEVIEWTOOLBAR_H +#define NODEVIEWTOOLBAR_H + +#include +#include + +namespace olive { + +class NodeViewToolBar : public QWidget +{ + Q_OBJECT +public: + NodeViewToolBar(QWidget *parent = nullptr); + +public slots: + void SetMiniMapEnabled(bool e) + { + minimap_btn_->setChecked(e); + } + +signals: + void MiniMapEnabledToggled(bool e); + +protected: + virtual void changeEvent(QEvent *e) override; + +private: + void Retranslate(); + + void UpdateIcons(); + + QPushButton *minimap_btn_; + +}; + +} + +#endif // NODEVIEWTOOLBAR_H From 6e14e51f21975eeb80f3f60a6d948ba48b36ddf4 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 10 Jul 2021 08:51:51 -0700 Subject: [PATCH 33/72] nodeview: added 'add' button to toolbar --- app/panel/node/node.cpp | 1 + app/widget/nodeview/nodeview.cpp | 12 +++++++++--- app/widget/nodeview/nodeview.h | 10 ++++++++++ app/widget/nodeview/nodeviewtoolbar.cpp | 8 ++++++++ app/widget/nodeview/nodeviewtoolbar.h | 4 ++++ 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/app/panel/node/node.cpp b/app/panel/node/node.cpp index 10ec25ce7..4b7334378 100644 --- a/app/panel/node/node.cpp +++ b/app/panel/node/node.cpp @@ -41,6 +41,7 @@ NodePanel::NodePanel(QWidget *parent) : // Connect toolbar to NodeView connect(toolbar, &NodeViewToolBar::MiniMapEnabledToggled, node_view_, &NodeView::SetMiniMapEnabled); + connect(toolbar, &NodeViewToolBar::AddNodeClicked, node_view_, &NodeView::ShowAddMenu); // Set defaults toolbar->SetMiniMapEnabled(true); diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 500d04684..7168c4f23 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -1026,9 +1026,7 @@ void NodeView::ShowContextMenu(const QPoint &pos) m.addSeparator(); - Menu* add_menu = NodeFactory::CreateMenu(&m); - add_menu->setTitle(tr("Add")); - connect(add_menu, &Menu::triggered, this, &NodeView::CreateNodeSlot); + Menu* add_menu = CreateAddMenu(&m); m.addMenu(add_menu); } @@ -1495,6 +1493,14 @@ QPointF NodeView::GetEstimatedPositionForContext(NodeViewItem *item, Node *conte return item->GetNodePosition() - context_offsets_.value(context); } +Menu *NodeView::CreateAddMenu(Menu *parent) +{ + Menu* add_menu = NodeFactory::CreateMenu(parent); + add_menu->setTitle(tr("Add")); + connect(add_menu, &Menu::triggered, this, &NodeView::CreateNodeSlot); + return add_menu; +} + NodeView::NodeViewAttachNodesToCursor::NodeViewAttachNodesToCursor(NodeView *view, const QVector &nodes) : view_(view), nodes_(nodes) diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 7cec30e85..5fccd9b54 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -30,6 +30,7 @@ #include "nodeviewminimap.h" #include "nodeviewscene.h" #include "widget/handmovableview/handmovableview.h" +#include "widget/menu/menu.h" namespace olive { @@ -84,6 +85,13 @@ public slots: minimap_->setVisible(e); } + void ShowAddMenu() + { + Menu *m = CreateAddMenu(nullptr); + m->exec(QCursor::pos()); + delete m; + } + signals: void NodesSelected(const QVector& nodes); @@ -126,6 +134,8 @@ private: QPointF GetEstimatedPositionForContext(NodeViewItem *item, Node *context) const; + Menu *CreateAddMenu(Menu *parent); + class NodeViewAttachNodesToCursor : public UndoCommand { public: diff --git a/app/widget/nodeview/nodeviewtoolbar.cpp b/app/widget/nodeview/nodeviewtoolbar.cpp index 29fc65847..c63be73db 100644 --- a/app/widget/nodeview/nodeviewtoolbar.cpp +++ b/app/widget/nodeview/nodeviewtoolbar.cpp @@ -3,6 +3,8 @@ #include #include +#include "ui/icons/icons.h" + namespace olive { #define super QWidget @@ -13,6 +15,10 @@ NodeViewToolBar::NodeViewToolBar(QWidget *parent) : QHBoxLayout *layout = new QHBoxLayout(this); layout->setMargin(0); + add_node_btn_ = new QPushButton(); + connect(add_node_btn_, &QPushButton::clicked, this, &NodeViewToolBar::AddNodeClicked); + layout->addWidget(add_node_btn_); + minimap_btn_ = new QPushButton(); minimap_btn_->setCheckable(true); connect(minimap_btn_, &QPushButton::clicked, this, &NodeViewToolBar::MiniMapEnabledToggled); @@ -36,12 +42,14 @@ void NodeViewToolBar::changeEvent(QEvent *e) void NodeViewToolBar::Retranslate() { + add_node_btn_->setToolTip(tr("Add Node")); minimap_btn_->setText(tr("Mini-Map")); minimap_btn_->setToolTip(tr("Toggle Mini-Map")); } void NodeViewToolBar::UpdateIcons() { + add_node_btn_->setIcon(icon::Add); } } diff --git a/app/widget/nodeview/nodeviewtoolbar.h b/app/widget/nodeview/nodeviewtoolbar.h index 60585fc5d..e48ed4882 100644 --- a/app/widget/nodeview/nodeviewtoolbar.h +++ b/app/widget/nodeview/nodeviewtoolbar.h @@ -19,6 +19,8 @@ public slots: } signals: + void AddNodeClicked(); + void MiniMapEnabledToggled(bool e); protected: @@ -29,6 +31,8 @@ private: void UpdateIcons(); + QPushButton *add_node_btn_; + QPushButton *minimap_btn_; }; From 4d69e5579918a64a907d8d88d64c65ab361d9cd0 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 10 Jul 2021 10:19:36 -0700 Subject: [PATCH 34/72] nodeview: implemented dragger to create edge without holding key --- app/widget/nodeview/nodeview.cpp | 36 +++++++++++++++++++-------- app/widget/nodeview/nodeview.h | 2 ++ app/widget/nodeview/nodeviewitem.cpp | 37 ++++++++++++++++++++++++++++ app/widget/nodeview/nodeviewitem.h | 7 ++++++ 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 7168c4f23..624ced728 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -505,8 +505,11 @@ void NodeView::mousePressEvent(QMouseEvent *event) if (HandPress(event)) return; if (event->button() == Qt::LeftButton) { + // See if we're dragging the arrow of an edge + QPointF scene_pt = mapToScene(event->pos()); + foreach (NodeViewEdge *edge_item, scene_.edges()) { - if (edge_item->arrow_bounding_rect().contains(mapToScene(event->pos()))) { + if (edge_item->arrow_bounding_rect().contains(scene_pt)) { create_edge_src_ = scene_.NodeToUIObject(edge_item->output().node()); create_edge_src_output_ = edge_item->output().output(); create_edge_ = edge_item; @@ -514,6 +517,14 @@ void NodeView::mousePressEvent(QMouseEvent *event) return; } } + + // See if we're dragging the arrow of a node + foreach (NodeViewItem *node_item, scene_.item_map()) { + if (node_item->GetOutputTriangle().boundingRect().translated(node_item->pos()).contains(scene_pt)) { + CreateNewEdge(node_item); + return; + } + } } QGraphicsItem* item = itemAt(event->pos()); @@ -535,15 +546,7 @@ void NodeView::mousePressEvent(QMouseEvent *event) if (event->modifiers() & Qt::ControlModifier) { NodeViewItem* node_item = dynamic_cast(item); if (node_item) { - create_edge_ = new NodeViewEdge(); - create_edge_src_ = node_item; - create_edge_src_output_ = Node::kDefaultOutput; - create_edge_already_exists_ = false; - - create_edge_->SetCurved(scene_.GetEdgesAreCurved()); - create_edge_->SetFlowDirection(scene_.GetFlowDirection()); - - scene_.addItem(create_edge_); + CreateNewEdge(node_item); return; } } @@ -1501,6 +1504,19 @@ Menu *NodeView::CreateAddMenu(Menu *parent) return add_menu; } +void NodeView::CreateNewEdge(NodeViewItem *output_item) +{ + create_edge_ = new NodeViewEdge(); + create_edge_src_ = output_item; + create_edge_src_output_ = Node::kDefaultOutput; + create_edge_already_exists_ = false; + + create_edge_->SetCurved(scene_.GetEdgesAreCurved()); + create_edge_->SetFlowDirection(scene_.GetFlowDirection()); + + scene_.addItem(create_edge_); +} + NodeView::NodeViewAttachNodesToCursor::NodeViewAttachNodesToCursor(NodeView *view, const QVector &nodes) : view_(view), nodes_(nodes) diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 5fccd9b54..4eff25c02 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -136,6 +136,8 @@ private: Menu *CreateAddMenu(Menu *parent); + void CreateNewEdge(NodeViewItem *output_item); + class NodeViewAttachNodesToCursor : public UndoCommand { public: diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index 7978d6c32..0b53720e4 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -65,6 +65,8 @@ NodeViewItem::NodeViewItem(QGraphicsItem *parent) : title_bar_rect_ = QRectF(-widget_width/2, -widget_height/2, widget_width, widget_height); setRect(title_bar_rect_); + + output_triangle_.resize(3); } QPointF NodeViewItem::GetNodePosition() const @@ -341,6 +343,41 @@ void NodeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti painter->setBrush(Qt::NoBrush); painter->drawRect(rect()); + + // Draw output triangle + painter->setPen(Qt::NoPen); + painter->setBrush(app_pal.color(QPalette::Text)); + int triangle_sz = qMin(rect().width(), rect().height()) / 2; + int triangle_sz_half = triangle_sz / 2; + + switch (flow_dir_) { + case NodeViewCommon::kLeftToRight: + // Triangle pointing right + output_triangle_[0] = QPointF(rect().right(), rect().center().y() - triangle_sz_half); + output_triangle_[1] = QPointF(rect().right() + triangle_sz_half, rect().center().y()); + output_triangle_[2] = QPointF(rect().right(), rect().center().y() + triangle_sz_half); + break; + case NodeViewCommon::kTopToBottom: + // Triangle pointing down + output_triangle_[0] = QPointF(rect().center().x() - triangle_sz_half, rect().bottom()); + output_triangle_[1] = QPointF(rect().center().x(), rect().bottom() + triangle_sz_half); + output_triangle_[2] = QPointF(rect().center().x() + triangle_sz_half, rect().bottom()); + break; + case NodeViewCommon::kBottomToTop: + // Triangle pointing up + output_triangle_[0] = QPointF(rect().center().x() - triangle_sz_half, rect().top()); + output_triangle_[1] = QPointF(rect().center().x(), rect().top() - triangle_sz_half); + output_triangle_[2] = QPointF(rect().center().x() + triangle_sz_half, rect().top()); + break; + case NodeViewCommon::kRightToLeft: + // Triangle pointing left + output_triangle_[0] = QPointF(rect().left(), rect().center().y() - triangle_sz_half); + output_triangle_[1] = QPointF(rect().left() - triangle_sz_half, rect().center().y()); + output_triangle_[2] = QPointF(rect().left(), rect().center().y() + triangle_sz_half); + break; + } + + painter->drawPolygon(output_triangle_); } void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event) diff --git a/app/widget/nodeview/nodeviewitem.h b/app/widget/nodeview/nodeviewitem.h index ffe3e0859..a7a6854f0 100644 --- a/app/widget/nodeview/nodeviewitem.h +++ b/app/widget/nodeview/nodeviewitem.h @@ -125,6 +125,11 @@ public: return prevent_removing_; } + const QPolygonF &GetOutputTriangle() const + { + return output_triangle_; + } + protected: virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; @@ -190,6 +195,8 @@ private: bool prevent_removing_; + QPolygonF output_triangle_; + }; } From cae2c360c26f377ec7a8a55d320de6d0424dd7bb Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 10 Jul 2021 15:35:31 -0700 Subject: [PATCH 35/72] nodeview: added border to minimap --- app/widget/nodeview/nodeviewminimap.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/widget/nodeview/nodeviewminimap.cpp b/app/widget/nodeview/nodeviewminimap.cpp index 9be32452a..d1e20791c 100644 --- a/app/widget/nodeview/nodeviewminimap.cpp +++ b/app/widget/nodeview/nodeviewminimap.cpp @@ -36,6 +36,8 @@ NodeViewMiniMap::NodeViewMiniMap(NodeViewScene *scene, QWidget *parent) : setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setViewportUpdateMode(FullViewportUpdate); + setFrameShape(QFrame::Panel); + setFrameShadow(QFrame::Plain); QMetaObject::invokeMethod(this, &NodeViewMiniMap::SetDefaultSize, Qt::QueuedConnection); From 38c38daf8a2bf82ced70b67f0baf3c4477ac6374 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 10 Jul 2021 15:37:06 -0700 Subject: [PATCH 36/72] framehashcache: use timestamp based vector internally I think this will be significantly faster, but I guess we'll find out! --- app/render/framehashcache.cpp | 125 +++++++++++++++++----------------- app/render/framehashcache.h | 19 +++++- 2 files changed, 80 insertions(+), 64 deletions(-) diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index 21b087086..84857614d 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -32,7 +32,6 @@ #include "codec/frame.h" #include "common/filefunctions.h" -#include "common/timecodefunctions.h" #include "render/diskmanager.h" namespace olive { @@ -52,9 +51,18 @@ FrameHashCache::FrameHashCache(QObject *parent) : } } +QByteArray FrameHashCache::GetHash(const int64_t &time) +{ + if (time < GetMapSize()) { + return time_hash_map_.at(time); + } else { + return QByteArray(); + } +} + QByteArray FrameHashCache::GetHash(const rational &time) { - return time_hash_map_.value(time); + return GetHash(ToTimestamp(time)); } void FrameHashCache::SetHash(const rational &time, const QByteArray &hash, const qint64& job_time, bool frame_exists) @@ -69,7 +77,15 @@ void FrameHashCache::SetHash(const rational &time, const QByteArray &hash, const } } - time_hash_map_.insert(time, hash); + int64_t ts = ToTimestamp(time); + if (ts >= GetMapSize()) { + // Reserve an extra minute to cut down on the amount of reallocations to make + time_hash_map_.reserve(ts + timebase_.flipped().toDouble() * 60); + + // Add enough entries to insert this hash + time_hash_map_.resize(ts + 1); + } + time_hash_map_[ts] = hash; TimeRange validated_range; if (frame_exists) { @@ -87,9 +103,9 @@ void FrameHashCache::ValidateFramesWithHash(const QByteArray &hash) { const TimeRangeList& invalidated_ranges = GetInvalidatedRanges(); - for (auto iterator=time_hash_map_.begin();iterator!=time_hash_map_.end();iterator++) { - if (iterator.value() == hash) { - TimeRange frame_range(iterator.key(), iterator.key() + timebase_); + for (int64_t i=0; i FrameHashCache::GetFramesWithHash(const QByteArray &hash) { QList times; - for (auto iterator=time_hash_map_.begin();iterator!=time_hash_map_.end();iterator++) { - if (iterator.value() == hash) { - times.append(iterator.key()); + for (int64_t i=0; i FrameHashCache::TakeFramesWithHash(const QByteArray &hash) TimeRangeList range_to_invalidate; QList times; - auto iterator = time_hash_map_.begin(); + for (int64_t i=0; i FrameHashCache::TakeFramesWithHash(const QByteArray &hash) return times; } -QMap FrameHashCache::time_hash_map() -{ - return time_hash_map_; -} - QVector FrameHashCache::GetFrameListFromTimeRange(TimeRangeList range_list, const rational &timebase) { // If timebase is null, this will be an infinite loop @@ -316,15 +324,11 @@ FramePtr FrameHashCache::LoadCacheFrame(const QString &fn) void FrameHashCache::LengthChangedEvent(const rational &old, const rational &newlen) { if (newlen < old) { - auto i = time_hash_map_.begin(); + // Determine length in frames by ceil-ing the time + int64_t new_ts_length = ToTimestamp(newlen, Timecode::kCeil); - while (i != time_hash_map_.end()) { - if (i.key() >= newlen) { - i = time_hash_map_.erase(i); - } else { - i++; - } - } + // Resize vector to this length, which will discard all frames after it + time_hash_map_.resize(new_ts_length); } } @@ -335,51 +339,48 @@ struct HashTimePair { void FrameHashCache::ShiftEvent(const rational &from, const rational &to) { - auto i = time_hash_map_.begin(); - // POSITIVE if moving forward -> // NEGATIVE if moving backward <- rational diff = to - from; bool diff_is_negative = (diff < 0); - QList shifted_times; - - while (i != time_hash_map_.end()) { - if (diff_is_negative && i.key() >= to && i.key() < from) { - - // This time will be removed in the shift so we just discard it - i = time_hash_map_.erase(i); - - } else if (i.key() >= from) { - - // This time is after the from time and must be shifted - shifted_times.append({i.key() + diff, i.value()}); - i = time_hash_map_.erase(i); - - } else { - - // Do nothing - i++; + int64_t to_ts = ToTimestamp(to); + int64_t from_ts = ToTimestamp(from); + if (diff_is_negative) { + // We're moving the frames starting at `from` backwards to where `to` is + if (to_ts < GetMapSize()) { + time_hash_map_.erase(time_hash_map_.begin() + to_ts, time_hash_map_.begin() + from_ts); + } + } else { + // We're moving the frames starting at `from` forwards to where `to` is + if (from_ts < GetMapSize()) { + time_hash_map_.insert(time_hash_map_.begin() + from_ts, to_ts - from_ts, QByteArray()); } - } - - foreach (const HashTimePair& p, shifted_times) { - time_hash_map_.insert(p.time, p.hash); } } void FrameHashCache::InvalidateEvent(const TimeRange &range) { if (!timebase_.isNull()) { - QVector invalid_frames = GetFrameListFromTimeRange({range}); - - foreach (const rational& r, invalid_frames) { - time_hash_map_.remove(r); + int64_t start = ToTimestamp(range.in(), Timecode::kCeil); + int64_t end = ToTimestamp(range.out(), Timecode::kCeil); + for (int64_t i=start; i #include "common/rational.h" +#include "common/timecodefunctions.h" #include "common/timerange.h" #include "codec/frame.h" #include "render/playbackcache.h" @@ -37,8 +38,14 @@ class FrameHashCache : public PlaybackCache public: FrameHashCache(QObject* parent = nullptr); + QByteArray GetHash(const int64_t& time); QByteArray GetHash(const rational& time); + const rational &GetTimebase() const + { + return timebase_; + } + void SetTimebase(const rational& tb); void ValidateFramesWithHash(const QByteArray& hash); @@ -76,7 +83,7 @@ public: QVector GetInvalidatedFrames(const TimeRange& intersecting); public slots: - void SetHash(const olive::rational& time, const QByteArray& hash, const qint64 &job_time, bool frame_exists); + void SetHash(const olive::rational &time, const QByteArray& hash, const qint64 &job_time, bool frame_exists); protected: virtual void LengthChangedEvent(const rational& old, const rational& newlen) override; @@ -86,7 +93,15 @@ protected: virtual void InvalidateEvent(const TimeRange& range) override; private: - QMap time_hash_map_; + rational ToTime(const int64_t &ts) const; + int64_t ToTimestamp(const rational &ts, Timecode::Rounding rounding = Timecode::kRound) const; + + int64_t GetMapSize() const + { + return int64_t(time_hash_map_.size()); + } + + std::vector time_hash_map_; rational timebase_; From 9b0b3962fe88ed7f8f724cfdc91edc4696707618 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Jul 2021 16:44:07 -0700 Subject: [PATCH 37/72] nodeview: set focus when adding a node Ensures users can "Esc" out of it --- app/widget/nodeview/nodeview.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 624ced728..fe8ab80b9 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -1049,6 +1049,8 @@ void NodeView::CreateNodeSlot(QAction *action) } paste_command_->add_child(new NodeViewAttachNodesToCursor(this, {new_node})); paste_command_->redo(); + + this->setFocus(); } } From 8a58ee2bdbdd0dfdf11861936eca163bdeb2ea50 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Jul 2021 16:47:12 -0700 Subject: [PATCH 38/72] renderer: use iterator rather than creating vectors Creating vectors from the FrameHashCache was one of the slowest functions in the program, this should be significantly faster. --- app/common/timerange.cpp | 62 ++++++++++++++++++++++++++++ app/common/timerange.h | 46 +++++++++++++++++++++ app/render/framehashcache.cpp | 71 -------------------------------- app/render/framehashcache.h | 15 ------- app/render/previewautocacher.cpp | 12 ++++-- app/render/previewautocacher.h | 2 +- app/task/render/render.cpp | 11 +++-- tests/general/CMakeLists.txt | 1 + tests/testutil.h | 1 + 9 files changed, 126 insertions(+), 95 deletions(-) diff --git a/app/common/timerange.cpp b/app/common/timerange.cpp index 83d65e9f7..a9cbad326 100644 --- a/app/common/timerange.cpp +++ b/app/common/timerange.cpp @@ -296,6 +296,68 @@ uint qHash(const TimeRange &r, uint seed) return qHash(r.in(), seed) ^ qHash(r.out(), seed); } +TimeRangeListFrameIterator::TimeRangeListFrameIterator(const TimeRangeList &list, const rational &timebase) : + list_(list), + timebase_(timebase), + index_(-1), + size_(-1) +{ + UpdateIndexIfNecessary(); +} + +bool TimeRangeListFrameIterator::GetNext(rational *out) +{ + if (index_ == list_.size()) { + return false; + } + + // Output current value + *out = current_; + + // Determine next value by adding timebase + current_ += timebase_; + + // If this time is outside the current range, jump to the next one + UpdateIndexIfNecessary(); + + return true; +} + +int TimeRangeListFrameIterator::size() +{ + if (size_ == -1) { + // Size isn't calculated automatically for optimization, so we'll calculate it now + size_ = 0; + + foreach (const TimeRange &range, list_) { + rational start = Timecode::snap_time_to_timebase(range.in(), timebase_, Timecode::kCeil); + rational end = Timecode::snap_time_to_timebase(range.out(), timebase_, Timecode::kFloor); + + if (end == range.out()) { + end -= timebase_; + } + + int64_t start_ts = Timecode::time_to_timestamp(start, timebase_); + int64_t end_ts = Timecode::time_to_timestamp(end, timebase_); + + size_ += 1 + (end_ts - start_ts); + } + } + + return size_; +} + +void TimeRangeListFrameIterator::UpdateIndexIfNecessary() +{ + while (index_ < list_.size() && (index_ == -1 || current_ >= list_.at(index_).out())) { + index_++; + + if (index_ < list_.size()) { + current_ = Timecode::snap_time_to_timebase(list_.at(index_).in(), timebase_, Timecode::kCeil); + } + } +} + } QDebug operator<<(QDebug debug, const olive::TimeRange &r) diff --git a/app/common/timerange.h b/app/common/timerange.h index 28913f6df..78c95d8db 100644 --- a/app/common/timerange.h +++ b/app/common/timerange.h @@ -22,6 +22,7 @@ #define TIMERANGE_H #include "rational.h" +#include "timecodefunctions.h" namespace olive { @@ -127,16 +128,61 @@ public: return array_.last(); } + const TimeRange& at(int index) const + { + return array_.at(index); + } + const QVector& internal_array() const { return array_; } + bool operator==(const TimeRangeList &rhs) const + { + return array_ == rhs.array_; + } + private: QVector array_; }; +class TimeRangeListFrameIterator +{ +public: + TimeRangeListFrameIterator(const TimeRangeList &list, const rational &timebase); + + bool GetNext(rational *out); + + QVector ToVector() const + { + TimeRangeListFrameIterator copy(list_, timebase_); + QVector times; + rational r; + while (copy.GetNext(&r)) { + times.append(r); + } + return times; + } + + int size(); + +private: + void UpdateIndexIfNecessary(); + + TimeRangeList list_; + + rational timebase_; + + rational current_; + + int index_; + + int size_; + +}; + uint qHash(const TimeRange& r, uint seed = 0); } diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index 84857614d..52e6028f2 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -114,77 +114,6 @@ void FrameHashCache::ValidateFramesWithHash(const QByteArray &hash) } } -QList FrameHashCache::GetFramesWithHash(const QByteArray &hash) -{ - QList times; - - for (int64_t i=0; i FrameHashCache::TakeFramesWithHash(const QByteArray &hash) -{ - TimeRangeList range_to_invalidate; - QList times; - - for (int64_t i=0; i FrameHashCache::GetFrameListFromTimeRange(TimeRangeList range_list, const rational &timebase) -{ - // If timebase is null, this will be an infinite loop - Q_ASSERT(!timebase.isNull()); - - QVector times; - - foreach (const TimeRange &range, range_list) { - rational frame = Timecode::snap_time_to_timebase(range.in(), timebase, Timecode::kCeil); - - while (frame < range.out()) { - times.append(frame); - frame += timebase; - } - } - - return times; -} - -QVector FrameHashCache::GetFrameListFromTimeRange(const TimeRangeList &range) -{ - return GetFrameListFromTimeRange(range, timebase_); -} - -QVector FrameHashCache::GetInvalidatedFrames() -{ - return GetFrameListFromTimeRange(GetInvalidatedRanges()); -} - -QVector FrameHashCache::GetInvalidatedFrames(const TimeRange &intersecting) -{ - return GetFrameListFromTimeRange(GetInvalidatedRanges().Intersects(intersecting)); -} - bool FrameHashCache::SaveCacheFrame(const QByteArray& hash, char* data, const VideoParams& vparam, diff --git a/app/render/framehashcache.h b/app/render/framehashcache.h index 56753c199..b6904ec51 100644 --- a/app/render/framehashcache.h +++ b/app/render/framehashcache.h @@ -50,16 +50,6 @@ public: void ValidateFramesWithHash(const QByteArray& hash); - /** - * @brief Returns a list of frames that use a particular hash - */ - QList GetFramesWithHash(const QByteArray& hash); - - /** - * @brief Same as FramesWithHash() but also removes these frames from the map - */ - QList TakeFramesWithHash(const QByteArray& hash); - QMap time_hash_map(); /** @@ -77,11 +67,6 @@ public: FramePtr LoadCacheFrame(const QByteArray& hash) const; static FramePtr LoadCacheFrame(const QString& fn); - static QVector GetFrameListFromTimeRange(TimeRangeList range_list, const rational& timebase); - QVector GetFrameListFromTimeRange(const TimeRangeList &range); - QVector GetInvalidatedFrames(); - QVector GetInvalidatedFrames(const TimeRange& intersecting); - public slots: void SetHash(const olive::rational &time, const QByteArray& hash, const qint64 &job_time, bool frame_exists); diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 916be3318..5c2f70ac2 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -91,8 +91,10 @@ void GenerateHashesInternal(ViewerOutput *viewer, FrameHashCache* cache, const Q } } -void PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, const QVector ×, qint64 job_time) +void PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, TimeRangeListFrameIterator iterator, qint64 job_time) { + QVector times = iterator.ToVector(); + // Ensure number of threads doesn't exceed idealThreadCount for maximum concurrency int hashes_per_thread = times.size() / qMax(1, QThread::idealThreadCount()-1); @@ -493,7 +495,7 @@ void PreviewAutoCacher::TryRender() // If we're here, we must be able to render if (!invalidated_video_.isEmpty()) { - QVector frames = viewer_node_->video_frame_cache()->GetFrameListFromTimeRange(invalidated_video_); + TimeRangeListFrameIterator frames(invalidated_video_, viewer_node_->video_frame_cache()->GetTimebase()); QFutureWatcher* watcher = new QFutureWatcher(); hash_tasks_.append(watcher); @@ -578,9 +580,11 @@ void PreviewAutoCacher::RequeueFrames() using_range = cache_range_; } - QVector invalidated_ranges = viewer_node_->video_frame_cache()->GetInvalidatedFrames(using_range); + TimeRangeList invalidated = viewer_node_->video_frame_cache()->GetInvalidatedRanges().Intersects(using_range); + TimeRangeListFrameIterator invalidated_ranges(invalidated, viewer_node_->video_frame_cache()->GetTimebase()); - foreach (const rational& t, invalidated_ranges) { + rational t; + while (invalidated_ranges.GetNext(&t)) { const QByteArray& hash = viewer_node_->video_frame_cache()->GetHash(t); RenderTicketWatcher* render_task = video_tasks_.key(hash); diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index 6d0bd4f44..2c40ba7dc 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -80,7 +80,7 @@ public: void ClearVideoDownloadQueue(bool wait = false); private: - static void GenerateHashes(ViewerOutput *viewer, FrameHashCache *cache, const QVector& times, qint64 job_time); + static void GenerateHashes(ViewerOutput *viewer, FrameHashCache *cache, TimeRangeListFrameIterator times, qint64 job_time); void TryRender(); diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index ee559fe2c..2254477a5 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -84,16 +84,19 @@ bool RenderTask::Render(ColorManager* manager, if (!video_range.isEmpty()) { // Get list of discrete frames from range - QVector times = FrameHashCache::GetFrameListFromTimeRange(video_range, video_params().frame_rate_as_time_base()); - QVector hashes(times.size()); + TimeRangeListFrameIterator iterator(video_range, video_params().frame_rate_as_time_base()); + QVector times(iterator.size()); + QVector hashes(iterator.size()); // Generate hashes - for (int i=0; iHash(viewer()->GetConnectedTextureOutput(), video_params_, times.at(i)); + times[i] = r; + hashes[i] = RenderManager::instance()->Hash(viewer()->GetConnectedTextureOutput(), video_params_, r); } // Filter out duplicates diff --git a/tests/general/CMakeLists.txt b/tests/general/CMakeLists.txt index 11f3472ed..7f87c6302 100644 --- a/tests/general/CMakeLists.txt +++ b/tests/general/CMakeLists.txt @@ -16,3 +16,4 @@ olive_add_test(General common-tests common-tests.cpp) olive_add_test(General rational-tests rational-tests.cpp) +olive_add_test(General timerange-tests timerange-tests.cpp) diff --git a/tests/testutil.h b/tests/testutil.h index 9f916c2d0..3cfd6845d 100644 --- a/tests/testutil.h +++ b/tests/testutil.h @@ -21,6 +21,7 @@ #include #define OLIVE_ASSERT(x) if (!(x)) return false +#define OLIVE_ASSERT_EQUAL(x, y) if (x != y) {std::cout << " - Equal assert failed on line " << __LINE__ << ": " << x << " != " << y; return false;}void() #define OLIVE_TEST_END return true #define OLIVE_ADD_TEST(x) bool Test##x() From a9d6e0f3e59ba6a8686207bab721cdbc1c85bdc0 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Jul 2021 16:53:24 -0700 Subject: [PATCH 39/72] audioplaybackcache: increase size of segments --- app/render/audioplaybackcache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/render/audioplaybackcache.cpp b/app/render/audioplaybackcache.cpp index c29f03ed2..5fe8aeee9 100644 --- a/app/render/audioplaybackcache.cpp +++ b/app/render/audioplaybackcache.cpp @@ -28,7 +28,7 @@ namespace olive { -const qint64 AudioPlaybackCache::kDefaultSegmentSize = 5242880; +const qint64 AudioPlaybackCache::kDefaultSegmentSize = 40 * 1024 * 1024; AudioPlaybackCache::AudioPlaybackCache(QObject* parent) : PlaybackCache(parent) From 72ff996fa801000e2be53cefb2d412a7bee773b5 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Jul 2021 17:20:39 -0700 Subject: [PATCH 40/72] tests: added tests for timerangelist --- tests/general/timerange-tests.cpp | 108 ++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 tests/general/timerange-tests.cpp diff --git a/tests/general/timerange-tests.cpp b/tests/general/timerange-tests.cpp new file mode 100644 index 000000000..f15b36a93 --- /dev/null +++ b/tests/general/timerange-tests.cpp @@ -0,0 +1,108 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "testutil.h" + +#include "common/timerange.h" + +namespace olive { + +OLIVE_ADD_TEST(TimeRangeListMergeAdjacent) +{ + TimeRangeList t; + + // TimeRangeList should merge 1 and 3 together since they're adjacent + t.insert(TimeRange(0, 6)); + t.insert(TimeRange(20, 30)); + t.insert(TimeRange(6, 10)); + + OLIVE_ASSERT(t.size() == 2); + OLIVE_ASSERT(t.first() == TimeRange(20, 30)); + OLIVE_ASSERT(t.at(1) == TimeRange(0, 10)); + + // TimeRangeList should ignore these because it's already contained + TimeRangeList noop_test = t; + + noop_test.insert(TimeRange(4, 7)); + OLIVE_ASSERT(noop_test == t); + + noop_test.insert(TimeRange(0, 3)); + OLIVE_ASSERT(noop_test == t); + + noop_test.insert(TimeRange(25, 30)); + OLIVE_ASSERT(noop_test == t); + + // TimeRangeList should combine all these together + TimeRangeList combine_test_no_overlap = t; + combine_test_no_overlap.insert(TimeRange(10, 20)); + OLIVE_ASSERT(combine_test_no_overlap.size() == 1); + OLIVE_ASSERT(combine_test_no_overlap.first() == TimeRange(0, 30)); + + TimeRangeList combine_test_in_overlap = t; + combine_test_in_overlap.insert(TimeRange(9, 20)); + OLIVE_ASSERT(combine_test_in_overlap.size() == 1); + OLIVE_ASSERT(combine_test_in_overlap.first() == TimeRange(0, 30)); + + TimeRangeList combine_test_out_overlap = t; + combine_test_out_overlap.insert(TimeRange(10, 21)); + OLIVE_ASSERT(combine_test_out_overlap.size() == 1); + OLIVE_ASSERT(combine_test_out_overlap.first() == TimeRange(0, 30)); + + TimeRangeList combine_test_both_overlap = t; + combine_test_both_overlap.insert(TimeRange(9, 21)); + OLIVE_ASSERT(combine_test_both_overlap.size() == 1); + OLIVE_ASSERT(combine_test_both_overlap.first() == TimeRange(0, 30)); + + OLIVE_TEST_END; +} + +OLIVE_ADD_TEST(TimeRangeListFrameIteratorSize) +{ + const rational timebase(1, 10); + + TimeRangeList ranges; + + ranges.insert(TimeRange(0, 10)); // 100 + ranges.insert(TimeRange(25, 30)); // 50 + ranges.insert(TimeRange(50, 60)); // 100 + ranges.insert(TimeRange(70, rational(1401, 20))); // 1 + ranges.insert(TimeRange(rational(1402, 20), rational(1403, 20))); // 1 + ranges.insert(TimeRange(rational(10001, 40), rational(10002, 40))); // 0 + ranges.insert(TimeRange(rational(10001, 40), rational(10004, 40))); // 0 + ranges.insert(TimeRange(rational(10001, 40), rational(10005, 40))); // 1 + + TimeRangeListFrameIterator iterator(ranges, timebase); + + QVector vec = iterator.ToVector(); + + OLIVE_ASSERT_EQUAL(vec.size(), 253); + OLIVE_ASSERT_EQUAL(iterator.size(), vec.size()); + + TimeRangeListFrameIterator empty(TimeRangeList(), timebase); + + QVector empty_vec = empty.ToVector(); + + OLIVE_ASSERT_EQUAL(empty_vec.size(), 0) + OLIVE_ASSERT_EQUAL(empty_vec.size(), empty.size()); + + OLIVE_TEST_END; +} + +} From d18f650034e46c13890d4cffe0b92c5f9ab91ad1 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Jul 2021 17:20:50 -0700 Subject: [PATCH 41/72] framehashcache: disabled reserve --- app/render/framehashcache.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index 52e6028f2..553f45c8c 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -79,8 +79,9 @@ void FrameHashCache::SetHash(const rational &time, const QByteArray &hash, const int64_t ts = ToTimestamp(time); if (ts >= GetMapSize()) { + // Disabled: bizarrely causes the whole app to hang indefinitely when used // Reserve an extra minute to cut down on the amount of reallocations to make - time_hash_map_.reserve(ts + timebase_.flipped().toDouble() * 60); + //time_hash_map_.reserve(ts + timebase_.flipped().toDouble() * 60); // Add enough entries to insert this hash time_hash_map_.resize(ts + 1); From 40caf7b6e3607a695a3d73fded238682bf293e01 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Jul 2021 23:31:14 -0700 Subject: [PATCH 42/72] tests: fixed missing semi-colon --- tests/general/timerange-tests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/general/timerange-tests.cpp b/tests/general/timerange-tests.cpp index f15b36a93..b35d70102 100644 --- a/tests/general/timerange-tests.cpp +++ b/tests/general/timerange-tests.cpp @@ -99,7 +99,7 @@ OLIVE_ADD_TEST(TimeRangeListFrameIteratorSize) QVector empty_vec = empty.ToVector(); - OLIVE_ASSERT_EQUAL(empty_vec.size(), 0) + OLIVE_ASSERT_EQUAL(empty_vec.size(), 0); OLIVE_ASSERT_EQUAL(empty_vec.size(), empty.size()); OLIVE_TEST_END; From 43d3fc57f270e3a25d12f2f1224e19491f16b973 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Jul 2021 23:31:36 -0700 Subject: [PATCH 43/72] Update audiovisualwaveform.cpp audiovisualwaveform: fixed shift issue --- app/audio/audiovisualwaveform.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/audio/audiovisualwaveform.cpp b/app/audio/audiovisualwaveform.cpp index 24a735fc5..7cdc65d5f 100644 --- a/app/audio/audiovisualwaveform.cpp +++ b/app/audio/audiovisualwaveform.cpp @@ -197,11 +197,11 @@ void AudioVisualWaveform::Shift(const rational &from, const rational &to) int to_index = time_to_samples(to, rate_dbl); if (from_index == to_index) { - return; + continue; } if (from_index > data.size()) { - return; + continue; } if (from_index > to_index) { @@ -226,7 +226,7 @@ void AudioVisualWaveform::Shift(const rational &from, const rational &to) memcpy(temp.data(), &data.data()[from_index], temp.size()); memcpy(&data.data()[to_index], temp.data(), temp.size()); - memset(reinterpret_cast(&data[from_index]), 0, distance * sizeof(SamplePerChannel)); + memset(&data.data()[from_index], 0, distance * sizeof(SamplePerChannel)); } } From 310fd3541a98262001648991cbae82e157649db3 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Jul 2021 23:33:21 -0700 Subject: [PATCH 44/72] various: greatly simplified job times Hopefully this doesn't break everything --- app/common/CMakeLists.txt | 2 + app/common/jobtime.cpp | 30 +++++++++++++++ app/common/jobtime.h | 62 ++++++++++++++++++++++++++++++ app/node/block/clip/clip.cpp | 6 +-- app/node/block/clip/clip.h | 2 +- app/node/node.cpp | 22 ++--------- app/node/node.h | 14 ++----- app/node/output/track/track.cpp | 4 +- app/node/output/track/track.h | 2 +- app/node/output/viewer/viewer.cpp | 8 ++-- app/node/output/viewer/viewer.h | 2 +- app/render/audioplaybackcache.cpp | 6 +-- app/render/audioplaybackcache.h | 6 +-- app/render/framehashcache.cpp | 4 +- app/render/framehashcache.h | 2 +- app/render/playbackcache.cpp | 14 +++---- app/render/playbackcache.h | 5 ++- app/render/previewautocacher.cpp | 18 ++++----- app/render/previewautocacher.h | 6 +-- app/task/export/export.cpp | 7 +--- app/task/export/export.h | 4 +- app/task/precache/precachetask.cpp | 6 +-- app/task/precache/precachetask.h | 4 +- app/task/render/render.cpp | 8 +--- app/task/render/render.h | 4 +- app/threading/threadticket.h | 6 +-- app/widget/viewer/viewer.cpp | 1 - app/widget/viewer/viewer.h | 2 - 28 files changed, 157 insertions(+), 100 deletions(-) create mode 100644 app/common/jobtime.cpp create mode 100644 app/common/jobtime.h diff --git a/app/common/CMakeLists.txt b/app/common/CMakeLists.txt index 3524a50c2..2dd2eaf62 100644 --- a/app/common/CMakeLists.txt +++ b/app/common/CMakeLists.txt @@ -36,6 +36,8 @@ set(OLIVE_SOURCES common/flipmodifiers.cpp common/flipmodifiers.h common/functiontimer.h + common/jobtime.cpp + common/jobtime.h common/lerp.h common/memorypool.h common/ocioutils.cpp diff --git a/app/common/jobtime.cpp b/app/common/jobtime.cpp new file mode 100644 index 000000000..43077ebf1 --- /dev/null +++ b/app/common/jobtime.cpp @@ -0,0 +1,30 @@ +#include "jobtime.h" + +#include + +namespace olive { + +uint64_t job_time_index = 0; +QMutex job_time_mutex; + +JobTime::JobTime() +{ + Acquire(); +} + +void JobTime::Acquire() +{ + job_time_mutex.lock(); + + value_ = job_time_index; + job_time_index++; + + job_time_mutex.unlock(); +} + +} + +QDebug operator<<(QDebug debug, const olive::JobTime& r) +{ + return debug.space() << r.value(); +} diff --git a/app/common/jobtime.h b/app/common/jobtime.h new file mode 100644 index 000000000..27988b92e --- /dev/null +++ b/app/common/jobtime.h @@ -0,0 +1,62 @@ +#ifndef JOBTIME_H +#define JOBTIME_H + +#include +#include + +namespace olive { + +class JobTime +{ +public: + JobTime(); + + void Acquire(); + + uint64_t value() const + { + return value_; + } + + bool operator==(const JobTime &rhs) const + { + return value_ == rhs.value_; + } + + bool operator!=(const JobTime &rhs) const + { + return value_ != rhs.value_; + } + + bool operator<(const JobTime &rhs) const + { + return value_ < rhs.value_; + } + + bool operator>(const JobTime &rhs) const + { + return value_ > rhs.value_; + } + + bool operator<=(const JobTime &rhs) const + { + return value_ <= rhs.value_; + } + + bool operator>=(const JobTime &rhs) const + { + return value_ >= rhs.value_; + } + +private: + uint64_t value_; + +}; + +} + +QDebug operator<<(QDebug debug, const olive::JobTime& r); + +Q_DECLARE_METATYPE(olive::JobTime) + +#endif // JOBTIME_H diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index fe8cdc6e2..d6b66b1ad 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -53,7 +53,7 @@ QString ClipBlock::Description() const return tr("A time-based node that represents a media source."); } -void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int element, qint64 job_time) +void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int element) { Q_UNUSED(element) @@ -63,10 +63,10 @@ void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int rational start = MediaToSequenceTime(range.in()); rational end = MediaToSequenceTime(range.out()); - super::InvalidateCache(TimeRange(start, end), from, element, job_time); + super::InvalidateCache(TimeRange(start, end), from, element); } else { // Otherwise, pass signal along normally - super::InvalidateCache(range, from, element, job_time); + super::InvalidateCache(range, from, element); } } diff --git a/app/node/block/clip/clip.h b/app/node/block/clip/clip.h index ef542526f..e5d7ecbbe 100644 --- a/app/node/block/clip/clip.h +++ b/app/node/block/clip/clip.h @@ -42,7 +42,7 @@ public: virtual QString id() const override; virtual QString Description() const override; - virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, qint64 job_time) override; + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element) override; virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; diff --git a/app/node/node.cpp b/app/node/node.cpp index 51821e820..19294529a 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -46,7 +46,6 @@ const QString Node::kDefaultOutput = QStringLiteral("output"); Node::Node(bool create_default_output) : can_be_deleted_(true), override_color_(-1), - last_change_time_(0), folder_(nullptr), operation_stack_(0), cache_result_(false) @@ -263,9 +262,6 @@ void Node::ConnectEdge(const NodeOutput &output, const NodeInput &input) input.node()->input_connections_[input] = output; output.node()->output_connections_.push_back(std::pair({output, input})); - // Update change times - input.node()->UpdateLastChangedTime(); - // Call internal events input.node()->InputConnectedEvent(input.input(), input.element(), output); output.node()->OutputConnectedEvent(output.output(), input); @@ -295,9 +291,6 @@ void Node::DisconnectEdge(const NodeOutput &output, const NodeInput &input) OutputConnections& outputs = output.node()->output_connections_; outputs.erase(std::find(outputs.begin(), outputs.end(), std::pair({output, input}))); - // Update change times - input.node()->UpdateLastChangedTime(); - // Call internal events input.node()->InputDisconnectedEvent(input.input(), input.element(), output); output.node()->OutputDisconnectedEvent(output.output(), input); @@ -1026,12 +1019,12 @@ NodeValueTable Node::Value(const QString& output, NodeValueDatabase &value) cons return value.Merge(); } -void Node::InvalidateCache(const TimeRange &range, const QString &from, int element, qint64 job_time) +void Node::InvalidateCache(const TimeRange &range, const QString &from, int element) { Q_UNUSED(from) Q_UNUSED(element) - SendInvalidateCache(range, job_time); + SendInvalidateCache(range); } void Node::BeginOperation() @@ -1180,14 +1173,14 @@ Node *Node::CopyNodeInGraph(const Node *node, MultiUndoCommand *command) return copy; } -void Node::SendInvalidateCache(const TimeRange &range, qint64 job_time) +void Node::SendInvalidateCache(const TimeRange &range) { if (GetOperationStack() == 0) { for (const OutputConnection& conn : output_connections_) { // Send clear cache signal to the Node const NodeInput& in = conn.second; - in.node()->InvalidateCache(range, in.input(), in.element(), job_time); + in.node()->InvalidateCache(range, in.input(), in.element()); } } } @@ -1826,8 +1819,6 @@ QVariant Node::PtrToValue(void *ptr) void Node::ParameterValueChanged(const QString& input, int element, const TimeRange& range) { - UpdateLastChangedTime(); - InputValueChangedEvent(input, element); emit ValueChanged(NodeInput(this, input, element), range); @@ -2019,11 +2010,6 @@ void Node::SaveImmediate(QXmlStreamWriter *writer, const QString& input, int ele } } -void Node::UpdateLastChangedTime() -{ - last_change_time_ = QDateTime::currentMSecsSinceEpoch(); -} - TimeRange Node::GetRangeAffectedByKeyframe(NodeKeyframe *key) const { const NodeKeyframeTrack& key_track = GetTrackFromKeyframe(key); diff --git a/app/node/node.h b/app/node/node.h index b730d8024..939bc37d0 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -631,12 +632,7 @@ public: * the DAG. Even if the time needs to be transformed somehow (e.g. converting media time to sequence time), you can * call this function with transformed time and relay the signal that way. */ - virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, qint64 job_time); - - void InvalidateCache(const TimeRange& range, const QString& from, int element = -1) - { - InvalidateCache(range, from, element, last_change_time_); - } + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element = -1); void InvalidateCache(const TimeRange& range, const NodeInput& from) { @@ -886,7 +882,7 @@ protected: SetInputProperty(id, QStringLiteral("combo_str"), strings); } - void SendInvalidateCache(const TimeRange &range, qint64 job_time); + void SendInvalidateCache(const TimeRange &range); /** * @brief Don't send cache invalidation signals if `input` is connected or disconnected @@ -1154,8 +1150,6 @@ private: void SaveImmediate(QXmlStreamWriter *writer, const QString &input, int element) const; - void UpdateLastChangedTime(); - /** * @brief Intelligently determine how what time range is affected by a keyframe */ @@ -1205,8 +1199,6 @@ private: OutputConnections output_connections_; - qint64 last_change_time_; - QString tooltip_; Folder* folder_; diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 6aebb508c..3a79d8001 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -431,7 +431,7 @@ QVector Track::BlocksAtTimeRange(const TimeRange &range) const return list; } -void Track::InvalidateCache(const TimeRange& range, const QString& from, int element, qint64 job_time) +void Track::InvalidateCache(const TimeRange& range, const QString& from, int element) { if (GetOperationStack() != 0) { return; @@ -455,7 +455,7 @@ void Track::InvalidateCache(const TimeRange& range, const QString& from, int ele preop_track_length_ = track_length_; } - Node::InvalidateCache(limited, from, element, job_time); + Node::InvalidateCache(limited, from, element); } void Track::InsertBlockBefore(Block* block, Block* after) diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index 94b745963..ee31c3da6 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -286,7 +286,7 @@ public: return blocks_; } - virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, qint64 job_time) override; + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element) override; /** * @brief Adds Block `block` at the very beginning of the Sequence before all other clips diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 072086693..3b0fd9356 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -222,7 +222,7 @@ void ViewerOutput::ShiftCache(const rational &from, const rational &to) ShiftAudioCache(from, to); } -void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, int element, qint64 job_time) +void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, int element) { Q_UNUSED(element) @@ -233,16 +233,16 @@ void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, if (invalidated_range.in() != invalidated_range.out()) { if (from == kTextureInput || from == kVideoParamsInput) { - video_frame_cache_.Invalidate(invalidated_range, job_time); + video_frame_cache_.Invalidate(invalidated_range); } else { - audio_playback_cache_.Invalidate(invalidated_range, job_time); + audio_playback_cache_.Invalidate(invalidated_range); } } } VerifyLength(); - super::InvalidateCache(range, from, element, job_time); + super::InvalidateCache(range, from, element); } QVector ViewerOutput::inputs_for_output(const QString &output) const diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index 4ac02eaa7..9891829cd 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -66,7 +66,7 @@ public: void ShiftAudioCache(const rational& from, const rational& to); void ShiftCache(const rational& from, const rational& to); - virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, qint64 job_time) override; + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element) override; virtual QVector inputs_for_output(const QString& output) const override; diff --git a/app/render/audioplaybackcache.cpp b/app/render/audioplaybackcache.cpp index 5fe8aeee9..4a83b112b 100644 --- a/app/render/audioplaybackcache.cpp +++ b/app/render/audioplaybackcache.cpp @@ -56,7 +56,7 @@ void AudioPlaybackCache::SetParameters(const AudioParams ¶ms) emit ParametersChanged(); } -void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr samples, const AudioVisualWaveform *waveform, const qint64 &job_time) +void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr samples, const AudioVisualWaveform *waveform, const JobTime &job_time) { QList valid_ranges = GetValidRanges(range, job_time); if (valid_ranges.isEmpty()) { @@ -154,7 +154,7 @@ void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr sample } } -void AudioPlaybackCache::WriteSilence(const TimeRange &range, qint64 job_time) +void AudioPlaybackCache::WriteSilence(const TimeRange &range, JobTime job_time) { // WritePCM will automatically fill non-existent bytes with silence, so we just have to send // it an empty sample buffer @@ -391,7 +391,7 @@ void AudioPlaybackCache::UpdateOffsetsFrom(int index) } } -QList AudioPlaybackCache::GetValidRanges(const TimeRange& range, const qint64& job_time) +QList AudioPlaybackCache::GetValidRanges(const TimeRange& range, const JobTime& job_time) { QList valid_ranges; diff --git a/app/render/audioplaybackcache.h b/app/render/audioplaybackcache.h index eb61643ee..2e0989a53 100644 --- a/app/render/audioplaybackcache.h +++ b/app/render/audioplaybackcache.h @@ -66,11 +66,11 @@ public: void SetParameters(const AudioParams& params); - void WritePCM(const TimeRange &range, SampleBufferPtr samples, const AudioVisualWaveform *waveform, const qint64& job_time); + void WritePCM(const TimeRange &range, SampleBufferPtr samples, const AudioVisualWaveform *waveform, const JobTime& job_time); - void WriteSilence(const TimeRange &range, qint64 job_time); + void WriteSilence(const TimeRange &range, JobTime job_time); - QList GetValidRanges(const TimeRange &range, const qint64 &job_time); + QList GetValidRanges(const TimeRange &range, const JobTime &job_time); class Segment { diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index 553f45c8c..89c07ddda 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -65,7 +65,7 @@ QByteArray FrameHashCache::GetHash(const rational &time) return GetHash(ToTimestamp(time)); } -void FrameHashCache::SetHash(const rational &time, const QByteArray &hash, const qint64& job_time, bool frame_exists) +void FrameHashCache::SetHash(const rational &time, const QByteArray &hash, const JobTime& job_time, bool frame_exists) { for (int i=jobs_.size()-1; i>=0; i--) { const JobIdentifier& job = jobs_.at(i); @@ -328,7 +328,7 @@ void FrameHashCache::HashDeleted(const QString& s, const QByteArray &hash) foreach (const TimeRange& range, ranges_to_invalidate) { // We set job time to 0 because the nodes haven't changed and any render job should be up // to date - Invalidate(range, 0); + Invalidate(range); } } diff --git a/app/render/framehashcache.h b/app/render/framehashcache.h index b6904ec51..fce812fee 100644 --- a/app/render/framehashcache.h +++ b/app/render/framehashcache.h @@ -68,7 +68,7 @@ public: static FramePtr LoadCacheFrame(const QString& fn); public slots: - void SetHash(const olive::rational &time, const QByteArray& hash, const qint64 &job_time, bool frame_exists); + void SetHash(const olive::rational &time, const QByteArray& hash, const olive::JobTime &job_time, bool frame_exists); protected: virtual void LengthChangedEvent(const rational& old, const rational& newlen) override; diff --git a/app/render/playbackcache.cpp b/app/render/playbackcache.cpp index 2398a3f80..cbdd6c53b 100644 --- a/app/render/playbackcache.cpp +++ b/app/render/playbackcache.cpp @@ -27,7 +27,7 @@ namespace olive { -void PlaybackCache::Invalidate(const TimeRange &r, qint64 job_time) +void PlaybackCache::Invalidate(const TimeRange &r) { if (r.in() == r.out()) { qWarning() << "Tried to invalidate zero-length range"; @@ -37,7 +37,7 @@ void PlaybackCache::Invalidate(const TimeRange &r, qint64 job_time) invalidated_.insert(r); RemoveRangeFromJobs(r); - jobs_.append({r, job_time}); + jobs_.append({r, JobTime()}); InvalidateEvent(r); @@ -50,7 +50,7 @@ void PlaybackCache::InvalidateAll() return; } - Invalidate(TimeRange(0, length_), 0); + Invalidate(TimeRange(0, length_)); } void PlaybackCache::SetLength(const rational &r) @@ -70,7 +70,7 @@ void PlaybackCache::SetLength(const rational &r) } else if (r > length_) { // If new length is greater, simply extend the invalidated range for now invalidated_.insert(range_diff); - jobs_.append({range_diff, 0}); + jobs_.append({range_diff, JobTime()}); } else { // If new length is smaller, removed hashes invalidated_.remove(range_diff); @@ -105,8 +105,6 @@ void PlaybackCache::Shift(rational from, rational to) } } - qDebug() << "FIXME: 0 job time may cause cache desyncs"; - // An region between `from` and `to` will be inserted or spliced out TimeRangeList ranges_to_shift = invalidated_.Intersects(TimeRange(from, RATIONAL_MAX)); @@ -119,7 +117,7 @@ void PlaybackCache::Shift(rational from, rational to) // (`diff` is POSITIVE when moving forward -> and NEGATIVE when moving backward <-) rational diff = to - from; foreach (const TimeRange& r, ranges_to_shift) { - Invalidate(r + diff, 0); + Invalidate(r + diff); } ShiftEvent(from, to); @@ -128,7 +126,7 @@ void PlaybackCache::Shift(rational from, rational to) if (diff > 0) { // If shifting forward, add this section to the invalidated region - Invalidate(TimeRange(from, to), 0); + Invalidate(TimeRange(from, to)); } // Emit signals diff --git a/app/render/playbackcache.h b/app/render/playbackcache.h index e2fb48fcb..a23f51101 100644 --- a/app/render/playbackcache.h +++ b/app/render/playbackcache.h @@ -24,6 +24,7 @@ #include #include +#include "common/jobtime.h" #include "common/timerange.h" namespace olive { @@ -63,7 +64,7 @@ public: QString GetCacheDirectory() const; public slots: - void Invalidate(const TimeRange& r, qint64 job_time); + void Invalidate(const TimeRange& r); void InvalidateAll(); @@ -93,7 +94,7 @@ protected: struct JobIdentifier { TimeRange range; - qint64 job_time; + JobTime job_time; }; QList jobs_; diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 5c2f70ac2..a27313cfa 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -15,9 +15,7 @@ PreviewAutoCacher::PreviewAutoCacher() : has_changed_(false), use_custom_range_(false), single_frame_render_(nullptr), - last_update_time_(0), - ignore_next_mouse_button_(false), - last_conform_task_(0) + ignore_next_mouse_button_(false) { paused_ = !Config::Current()[QStringLiteral("AutoCacheEnabled")].toBool(), @@ -63,7 +61,7 @@ void PreviewAutoCacher::SetPaused(bool paused) paused_ = paused; } -void GenerateHashesInternal(ViewerOutput *viewer, FrameHashCache* cache, const QVector ×, qint64 job_time) +void GenerateHashesInternal(ViewerOutput *viewer, FrameHashCache* cache, const QVector ×, JobTime job_time) { std::vector existing_hashes; @@ -86,12 +84,12 @@ void GenerateHashesInternal(ViewerOutput *viewer, FrameHashCache* cache, const Q QMetaObject::invokeMethod(cache, "SetHash", Qt::QueuedConnection, OLIVE_NS_ARG(rational, time), Q_ARG(QByteArray, hash), - Q_ARG(qint64, job_time), + OLIVE_NS_ARG(JobTime, job_time), Q_ARG(bool, hash_exists)); } } -void PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, TimeRangeListFrameIterator iterator, qint64 job_time) +void PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, TimeRangeListFrameIterator iterator, JobTime job_time) { QVector times = iterator.ToVector(); @@ -185,7 +183,7 @@ void PreviewAutoCacher::AudioRendered() if (watcher->GetTicket()->property("incomplete").toBool()) { if (last_conform_task_ > watcher->GetTicket()->GetJobTime()) { // Requeue now - viewer_node_->audio_playback_cache()->Invalidate(range, QDateTime::currentMSecsSinceEpoch()); + viewer_node_->audio_playback_cache()->Invalidate(range); pcm_is_usable = false; } else { // Wait for conform @@ -398,7 +396,7 @@ void PreviewAutoCacher::InsertIntoCopyMap(Node *node, Node *copy) void PreviewAutoCacher::UpdateLastSyncedValue() { - last_update_time_ = QDateTime::currentMSecsSinceEpoch(); + last_update_time_.Acquire(); } void PreviewAutoCacher::CancelQueuedSingleFrameRender() @@ -613,11 +611,11 @@ void PreviewAutoCacher::RequeueFrames() void PreviewAutoCacher::ConformFinished() { - last_conform_task_ = QDateTime::currentMSecsSinceEpoch(); + last_conform_task_.Acquire(); if (viewer_node_) { foreach (const TimeRange &range, audio_needing_conform_) { - viewer_node_->audio_playback_cache()->Invalidate(range, QDateTime::currentMSecsSinceEpoch()); + viewer_node_->audio_playback_cache()->Invalidate(range); } audio_needing_conform_.clear(); } diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index 2c40ba7dc..7869249ef 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -80,7 +80,7 @@ public: void ClearVideoDownloadQueue(bool wait = false); private: - static void GenerateHashes(ViewerOutput *viewer, FrameHashCache *cache, TimeRangeListFrameIterator times, qint64 job_time); + static void GenerateHashes(ViewerOutput *viewer, FrameHashCache *cache, TimeRangeListFrameIterator times, JobTime job_time); void TryRender(); @@ -161,7 +161,7 @@ private: QMap video_download_tasks_; QMap > video_immediate_passthroughs_; - qint64 last_update_time_; + JobTime last_update_time_; bool ignore_next_mouse_button_; @@ -169,7 +169,7 @@ private: TimeRangeList audio_needing_conform_; - qint64 last_conform_task_; + JobTime last_conform_task_; private slots: /** diff --git a/app/task/export/export.cpp b/app/task/export/export.cpp index 814c31125..425d51f7b 100644 --- a/app/task/export/export.cpp +++ b/app/task/export/export.cpp @@ -147,9 +147,8 @@ bool ExportTask::Run() return success; } -void ExportTask::FrameDownloaded(FramePtr f, const QByteArray &hash, const QVector ×, qint64 job_time) +void ExportTask::FrameDownloaded(FramePtr f, const QByteArray &hash, const QVector ×) { - Q_UNUSED(job_time) Q_UNUSED(hash) foreach (const rational& t, times) { @@ -179,10 +178,8 @@ void ExportTask::FrameDownloaded(FramePtr f, const QByteArray &hash, const QVect } } -void ExportTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples, qint64 job_time) +void ExportTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples) { - Q_UNUSED(job_time) - TimeRange adjusted_range = range; if (params_.has_custom_range()) { diff --git a/app/task/export/export.h b/app/task/export/export.h index 7f1c0859a..f05440a8f 100644 --- a/app/task/export/export.h +++ b/app/task/export/export.h @@ -38,9 +38,9 @@ public: protected: virtual bool Run() override; - virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times, qint64 job_time) override; + virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times) override; - virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) override; + virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples) override; virtual void EncodeSubtitle(const SubtitleBlock *sub) override; diff --git a/app/task/precache/precachetask.cpp b/app/task/precache/precachetask.cpp index 1f6adc80e..6add67a25 100644 --- a/app/task/precache/precachetask.cpp +++ b/app/task/precache/precachetask.cpp @@ -74,7 +74,7 @@ bool PreCacheTask::Run() return true; } -void PreCacheTask::FrameDownloaded(FramePtr frame, const QByteArray &hash, const QVector ×, qint64 job_time) +void PreCacheTask::FrameDownloaded(FramePtr frame, const QByteArray &hash, const QVector ×) { // Do nothing. Pre-cache essentially just creates more frames in the cache, it doesn't need to do // anything else. @@ -82,16 +82,14 @@ void PreCacheTask::FrameDownloaded(FramePtr frame, const QByteArray &hash, const Q_UNUSED(frame) Q_UNUSED(hash) Q_UNUSED(times) - Q_UNUSED(job_time) } -void PreCacheTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples, qint64 job_time) +void PreCacheTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples) { // Pre-cache doesn't cache any audio Q_UNUSED(range) Q_UNUSED(samples) - Q_UNUSED(job_time) } } diff --git a/app/task/precache/precachetask.h b/app/task/precache/precachetask.h index 325a49b47..a9bf6497d 100644 --- a/app/task/precache/precachetask.h +++ b/app/task/precache/precachetask.h @@ -38,9 +38,9 @@ public: protected: virtual bool Run() override; - virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times, qint64 job_time) override; + virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times) override; - virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) override; + virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples) override; private: Project* project_; diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index 2254477a5..2f78a9ff0 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -55,8 +55,6 @@ bool RenderTask::Render(ColorManager* manager, double total_length = 0; // Store real time before any rendering takes place - qint64 job_time = QDateTime::currentMSecsSinceEpoch(); - // Queue audio jobs foreach (const TimeRange& range, audio_range) { // Don't count audio progress, since it's generally a lot faster than video and is weighted at @@ -193,9 +191,7 @@ bool RenderTask::Render(ColorManager* manager, TimeRange range = watcher->property("range").value(); - AudioDownloaded(range, - watcher->Get().value(), - job_time); + AudioDownloaded(range, watcher->Get().value()); // Don't count audio progress, since it's generally a lot faster than video and is weighted at // 50%, which makes the progress bar look weird to the uninitiated @@ -217,7 +213,7 @@ bool RenderTask::Render(ColorManager* manager, // Assume single-step video or video download ticket QByteArray rendered_hash = watcher->property("hash").toByteArray(); - FrameDownloaded(watcher->Get().value(), rendered_hash, time_map.value(rendered_hash), job_time); + FrameDownloaded(watcher->Get().value(), rendered_hash, time_map.value(rendered_hash)); if (native_progress_signalling_) { double progress_to_add = 1.0; diff --git a/app/task/render/render.h b/app/task/render/render.h index af11071a7..a41d9a55e 100644 --- a/app/task/render/render.h +++ b/app/task/render/render.h @@ -51,9 +51,9 @@ protected: virtual void DownloadFrame(QThread* thread, FramePtr frame, const QByteArray &hash); - virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times, qint64 job_time) = 0; + virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times) = 0; - virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) = 0; + virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples) = 0; virtual void EncodeSubtitle(const SubtitleBlock *subtitle); diff --git a/app/threading/threadticket.h b/app/threading/threadticket.h index 47f2bb586..82bd8b90a 100644 --- a/app/threading/threadticket.h +++ b/app/threading/threadticket.h @@ -38,14 +38,14 @@ class RenderTicket : public QObject public: RenderTicket(); - qint64 GetJobTime() const + JobTime GetJobTime() const { return job_time_; } void SetJobTime() { - job_time_ = QDateTime::currentMSecsSinceEpoch(); + job_time_.Acquire(); } /** @@ -137,7 +137,7 @@ private: QWaitCondition wait_; - qint64 job_time_; + JobTime job_time_; }; diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index fe474d4a6..e64d12edf 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -52,7 +52,6 @@ const int kMaxPreQueueSize = 8; ViewerWidget::ViewerWidget(QWidget *parent) : super(false, true, parent), playback_speed_(0), - frame_cache_job_time_(0), color_menu_enabled_(true), time_changed_from_timer_(false), prequeuing_(false), diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 8b0f93a1a..402164ee3 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -215,8 +215,6 @@ private: QAtomicInt playback_speed_; - qint64 frame_cache_job_time_; - int64_t last_time_; bool color_menu_enabled_; From 7ff419b0a31c9ed4063aaad20ffa4d9e75473c91 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 12 Jul 2021 00:48:26 -0700 Subject: [PATCH 45/72] render: use events to iterate gradually through frame queue Fixes #1665 --- app/common/timerange.h | 6 ++++ app/render/previewautocacher.cpp | 47 ++++++++++++++++---------------- app/render/previewautocacher.h | 3 ++ app/render/rendermanager.h | 5 ++++ 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/app/common/timerange.h b/app/common/timerange.h index 78c95d8db..3a7be3054 100644 --- a/app/common/timerange.h +++ b/app/common/timerange.h @@ -151,6 +151,7 @@ private: class TimeRangeListFrameIterator { public: + TimeRangeListFrameIterator() = default; TimeRangeListFrameIterator(const TimeRangeList &list, const rational &timebase); bool GetNext(rational *out); @@ -168,6 +169,11 @@ public: int size(); + void reset() + { + *this = TimeRangeListFrameIterator(); + } + private: void UpdateIndexIfNecessary(); diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index a27313cfa..567e5ed19 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -274,6 +274,8 @@ void PreviewAutoCacher::VideoRendered() TryRender(); } + QueueNextFrameInRange(1); + delete watcher; } @@ -442,6 +444,7 @@ void PreviewAutoCacher::ClearVideoQueue(bool hard) has_changed_ = true; use_custom_range_ = false; + queued_frame_iterator_.reset(); } void PreviewAutoCacher::ClearAudioQueue(bool hard) @@ -579,31 +582,9 @@ void PreviewAutoCacher::RequeueFrames() } TimeRangeList invalidated = viewer_node_->video_frame_cache()->GetInvalidatedRanges().Intersects(using_range); - TimeRangeListFrameIterator invalidated_ranges(invalidated, viewer_node_->video_frame_cache()->GetTimebase()); + queued_frame_iterator_ = TimeRangeListFrameIterator(invalidated, viewer_node_->video_frame_cache()->GetTimebase()); - rational t; - while (invalidated_ranges.GetNext(&t)) { - const QByteArray& hash = viewer_node_->video_frame_cache()->GetHash(t); - - RenderTicketWatcher* render_task = video_tasks_.key(hash); - - if (t >= using_range.in() - && t < using_range.out()) { - // We want this hash, if we're not already rendering, start render now - if (!render_task && !video_download_tasks_.key(hash)) { - // Don't render any hash more than once - RenderFrame(hash, t, false, false); - } - } else if (render_task) { - // Cancel this frame unless it's already started - QMutexLocker locker(render_task->GetTicket()->lock()); - - if (!render_task->GetTicket()->IsRunning(false)) { - video_tasks_.remove(render_task); - delete render_task; - } - } - } + QueueNextFrameInRange(RenderManager::GetNumberOfIdealConcurrentJobs()); has_changed_ = false; } @@ -777,6 +758,24 @@ void PreviewAutoCacher::ClearQueueRemoveEventInternal(QVectorvideo_frame_cache()->GetHash(t); + + RenderTicketWatcher* render_task = video_tasks_.key(hash); + + // We want this hash, if we're not already rendering, start render now + if (!render_task && !video_download_tasks_.key(hash)) { + // Don't render any hash more than once + RenderFrame(hash, t, false, false); + + max--; + } + } +} + template void PreviewAutoCacher::ClearQueueInternal(T& list, bool hard, Func member) { diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index 7869249ef..e60ca6c2a 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -115,6 +115,9 @@ private: void ClearQueueRemoveEventInternal(QMap::iterator it); void ClearQueueRemoveEventInternal(QVector::iterator it); + void QueueNextFrameInRange(int max); + TimeRangeListFrameIterator queued_frame_iterator_; + class QueuedJob { public: enum Type { diff --git a/app/render/rendermanager.h b/app/render/rendermanager.h index 34f8532d9..816394dac 100644 --- a/app/render/rendermanager.h +++ b/app/render/rendermanager.h @@ -123,6 +123,11 @@ public: return backend_; } + static int GetNumberOfIdealConcurrentJobs() + { + return QThread::idealThreadCount(); + } + signals: private: From dc34f80386ef14c1e46d4ad85a07f2ce60962cce Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 12 Jul 2021 11:56:00 -0700 Subject: [PATCH 46/72] timerangelistframeiterator: initialize values even with default constructor --- app/common/timerange.cpp | 5 +++++ app/common/timerange.h | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/common/timerange.cpp b/app/common/timerange.cpp index a9cbad326..ac04bdd88 100644 --- a/app/common/timerange.cpp +++ b/app/common/timerange.cpp @@ -296,6 +296,11 @@ uint qHash(const TimeRange &r, uint seed) return qHash(r.in(), seed) ^ qHash(r.out(), seed); } +TimeRangeListFrameIterator::TimeRangeListFrameIterator() : + TimeRangeListFrameIterator(TimeRangeList(), rational::NaN) +{ +} + TimeRangeListFrameIterator::TimeRangeListFrameIterator(const TimeRangeList &list, const rational &timebase) : list_(list), timebase_(timebase), diff --git a/app/common/timerange.h b/app/common/timerange.h index 3a7be3054..14b12ebae 100644 --- a/app/common/timerange.h +++ b/app/common/timerange.h @@ -151,7 +151,7 @@ private: class TimeRangeListFrameIterator { public: - TimeRangeListFrameIterator() = default; + TimeRangeListFrameIterator(); TimeRangeListFrameIterator(const TimeRangeList &list, const rational &timebase); bool GetNext(rational *out); From c374a03e9d952fb9e3ad2ccab36f1ea08a8b5774 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 12 Jul 2021 15:14:37 -0700 Subject: [PATCH 47/72] render: further simplified job times Outside of silly mistakes, this should be significantly faster and more stable. --- app/common/timerange.cpp | 32 ++---- app/common/timerange.h | 32 ++++++ app/render/CMakeLists.txt | 2 + app/render/audioplaybackcache.cpp | 26 +---- app/render/audioplaybackcache.h | 6 +- app/render/framehashcache.cpp | 12 +-- app/render/framehashcache.h | 3 +- app/render/playbackcache.cpp | 32 ------ app/render/playbackcache.h | 9 -- app/render/previewautocacher.cpp | 162 ++++++++++++++++++------------ app/render/previewautocacher.h | 23 ++++- app/render/renderjobtracker.cpp | 71 +++++++++++++ app/render/renderjobtracker.h | 67 ++++++++++++ app/threading/threadticket.cpp | 1 - app/threading/threadticket.h | 12 --- 15 files changed, 300 insertions(+), 190 deletions(-) create mode 100644 app/render/renderjobtracker.cpp create mode 100644 app/render/renderjobtracker.h diff --git a/app/common/timerange.cpp b/app/common/timerange.cpp index ac04bdd88..3f0f08829 100644 --- a/app/common/timerange.cpp +++ b/app/common/timerange.cpp @@ -199,30 +199,7 @@ void TimeRangeList::insert(TimeRange range_to_add) void TimeRangeList::remove(const TimeRange &remove) { - int sz = this->size(); - - for (int i=0;i remove.in()) { - // This element's out point overlaps the range's in, we'll trim it - compare.set_out(remove.in()); - } else if (compare.in() < remove.out() && compare.out() > remove.out()) { - // This element's in point overlaps the range's out, we'll trim it - compare.set_in(remove.out()); - } - } + util_remove(&array_, remove); } bool TimeRangeList::contains(const TimeRange &range, bool in_inclusive, bool out_inclusive) const @@ -312,7 +289,7 @@ TimeRangeListFrameIterator::TimeRangeListFrameIterator(const TimeRangeList &list bool TimeRangeListFrameIterator::GetNext(rational *out) { - if (index_ == list_.size()) { + if (!HasNext()) { return false; } @@ -328,6 +305,11 @@ bool TimeRangeListFrameIterator::GetNext(rational *out) return true; } +bool TimeRangeListFrameIterator::HasNext() const +{ + return index_ < list_.size(); +} + int TimeRangeListFrameIterator::size() { if (size_ == -1) { diff --git a/app/common/timerange.h b/app/common/timerange.h index 14b12ebae..7362f6518 100644 --- a/app/common/timerange.h +++ b/app/common/timerange.h @@ -81,6 +81,36 @@ public: void remove(const TimeRange& remove); + template + static void util_remove(QVector *list, const TimeRange &remove) + { + int sz = list->size(); + + for (int i=0;iremoveAt(i); + i--; + sz--; + } else if (compare.Contains(remove, false, false)) { + // The remove range is within this element, only choice is to split the element into two + T new_range = compare; + new_range.set_in(remove.out()); + compare.set_out(remove.in()); + list->append(new_range); + break; + } else if (compare.in() < remove.in() && compare.out() > remove.in()) { + // This element's out point overlaps the range's in, we'll trim it + compare.set_out(remove.in()); + } else if (compare.in() < remove.out() && compare.out() > remove.out()) { + // This element's in point overlaps the range's out, we'll trim it + compare.set_in(remove.out()); + } + } + } + bool contains(const TimeRange& range, bool in_inclusive = true, bool out_inclusive = true) const; bool isEmpty() const @@ -156,6 +186,8 @@ public: bool GetNext(rational *out); + bool HasNext() const; + QVector ToVector() const { TimeRangeListFrameIterator copy(list_, timebase_); diff --git a/app/render/CMakeLists.txt b/app/render/CMakeLists.txt index 6f6898154..122aa5422 100644 --- a/app/render/CMakeLists.txt +++ b/app/render/CMakeLists.txt @@ -46,6 +46,8 @@ set(OLIVE_SOURCES render/rendercache.h render/rendererthreadwrapper.cpp render/rendererthreadwrapper.h + render/renderjobtracker.cpp + render/renderjobtracker.h render/rendermanager.cpp render/rendermanager.h render/rendermodes.h diff --git a/app/render/audioplaybackcache.cpp b/app/render/audioplaybackcache.cpp index 4a83b112b..95a9ab30b 100644 --- a/app/render/audioplaybackcache.cpp +++ b/app/render/audioplaybackcache.cpp @@ -56,13 +56,8 @@ void AudioPlaybackCache::SetParameters(const AudioParams ¶ms) emit ParametersChanged(); } -void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr samples, const AudioVisualWaveform *waveform, const JobTime &job_time) +void AudioPlaybackCache::WritePCM(const TimeRange &range, const TimeRangeList &valid_ranges, SampleBufferPtr samples, const AudioVisualWaveform *waveform) { - QList valid_ranges = GetValidRanges(range, job_time); - if (valid_ranges.isEmpty()) { - return; - } - // Ensure if we have enough segments to write this data, creating more if not qint64 length_diff = params_.time_to_bytes(range.out()) - playlist_.GetLength(); while (length_diff > 0) { @@ -154,11 +149,11 @@ void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr sample } } -void AudioPlaybackCache::WriteSilence(const TimeRange &range, JobTime job_time) +void AudioPlaybackCache::WriteSilence(const TimeRange &range) { // WritePCM will automatically fill non-existent bytes with silence, so we just have to send // it an empty sample buffer - WritePCM(range, nullptr, nullptr, job_time); + WritePCM(range, {range}, nullptr, nullptr); } void AudioPlaybackCache::ShiftEvent(const rational &from_in_time, const rational &to_in_time) @@ -391,21 +386,6 @@ void AudioPlaybackCache::UpdateOffsetsFrom(int index) } } -QList AudioPlaybackCache::GetValidRanges(const TimeRange& range, const JobTime& job_time) -{ - QList valid_ranges; - - for (int i=jobs_.size()-1;i>=0;i--) { - const JobIdentifier& job = jobs_.at(i); - - if (job_time >= job.job_time && job.range.OverlapsWith(range)) { - valid_ranges.append(job.range.Intersected(range)); - } - } - - return valid_ranges; -} - AudioPlaybackCache::PlaybackDevice *AudioPlaybackCache::CreatePlaybackDevice(QObject* parent) const { return new PlaybackDevice(playlist_, parent); diff --git a/app/render/audioplaybackcache.h b/app/render/audioplaybackcache.h index 2e0989a53..a3f26b6d1 100644 --- a/app/render/audioplaybackcache.h +++ b/app/render/audioplaybackcache.h @@ -66,11 +66,9 @@ public: void SetParameters(const AudioParams& params); - void WritePCM(const TimeRange &range, SampleBufferPtr samples, const AudioVisualWaveform *waveform, const JobTime& job_time); + void WritePCM(const TimeRange &range, const TimeRangeList &valid_ranges, SampleBufferPtr samples, const AudioVisualWaveform *waveform); - void WriteSilence(const TimeRange &range, JobTime job_time); - - QList GetValidRanges(const TimeRange &range, const JobTime &job_time); + void WriteSilence(const TimeRange &range); class Segment { diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index 89c07ddda..4922afcba 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -65,18 +65,8 @@ QByteArray FrameHashCache::GetHash(const rational &time) return GetHash(ToTimestamp(time)); } -void FrameHashCache::SetHash(const rational &time, const QByteArray &hash, const JobTime& job_time, bool frame_exists) +void FrameHashCache::SetHash(const rational &time, const QByteArray &hash, bool frame_exists) { - for (int i=jobs_.size()-1; i>=0; i--) { - const JobIdentifier& job = jobs_.at(i); - - if (job.range.Contains(time) - && job_time < job.job_time) { - // Hash here has changed since this frame started rendering, discard it - return; - } - } - int64_t ts = ToTimestamp(time); if (ts >= GetMapSize()) { // Disabled: bizarrely causes the whole app to hang indefinitely when used diff --git a/app/render/framehashcache.h b/app/render/framehashcache.h index fce812fee..b95d5f977 100644 --- a/app/render/framehashcache.h +++ b/app/render/framehashcache.h @@ -67,8 +67,7 @@ public: FramePtr LoadCacheFrame(const QByteArray& hash) const; static FramePtr LoadCacheFrame(const QString& fn); -public slots: - void SetHash(const olive::rational &time, const QByteArray& hash, const olive::JobTime &job_time, bool frame_exists); + void SetHash(const olive::rational &time, const QByteArray& hash, bool frame_exists); protected: virtual void LengthChangedEvent(const rational& old, const rational& newlen) override; diff --git a/app/render/playbackcache.cpp b/app/render/playbackcache.cpp index cbdd6c53b..73cb827aa 100644 --- a/app/render/playbackcache.cpp +++ b/app/render/playbackcache.cpp @@ -36,9 +36,6 @@ void PlaybackCache::Invalidate(const TimeRange &r) invalidated_.insert(r); - RemoveRangeFromJobs(r); - jobs_.append({r, JobTime()}); - InvalidateEvent(r); emit Invalidated(r); @@ -66,15 +63,12 @@ void PlaybackCache::SetLength(const rational &r) if (r.isNull()) { invalidated_.clear(); - jobs_.clear(); } else if (r > length_) { // If new length is greater, simply extend the invalidated range for now invalidated_.insert(range_diff); - jobs_.append({range_diff, JobTime()}); } else { // If new length is smaller, removed hashes invalidated_.remove(range_diff); - RemoveRangeFromJobs(range_diff); } rational old_length = length_; @@ -110,7 +104,6 @@ void PlaybackCache::Shift(rational from, rational to) // Remove everything from the minimum point TimeRange remove_range = TimeRange(qMin(from, to), RATIONAL_MAX); - RemoveRangeFromJobs(remove_range); Validate(remove_range); // Shift invalidated ranges @@ -163,31 +156,6 @@ Project *PlaybackCache::GetProject() const return viewer->project(); } -void PlaybackCache::RemoveRangeFromJobs(const TimeRange &remove) -{ - // Code shamelessly copied from TimeRangeList::RemoveTimeRange - for (int i=0;i remove.in()) { - // This element's out point overlaps the range's in, we'll trim it - compare.set_out(remove.in()); - } else if (compare.in() < remove.out() && compare.out() > remove.out()) { - // This element's in point overlaps the range's out, we'll trim it - compare.set_in(remove.out()); - } - } -} - QString PlaybackCache::GetCacheDirectory() const { Project* project = GetProject(); diff --git a/app/render/playbackcache.h b/app/render/playbackcache.h index a23f51101..5036ccc2e 100644 --- a/app/render/playbackcache.h +++ b/app/render/playbackcache.h @@ -92,16 +92,7 @@ protected: Project* GetProject() const; - struct JobIdentifier { - TimeRange range; - JobTime job_time; - }; - - QList jobs_; - private: - void RemoveRangeFromJobs(const TimeRange& remove); - TimeRangeList invalidated_; rational length_; diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 567e5ed19..ce2c4fe5d 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -19,7 +19,7 @@ PreviewAutoCacher::PreviewAutoCacher() : { paused_ = !Config::Current()[QStringLiteral("AutoCacheEnabled")].toBool(), - SetPlayhead(0); + SetPlayhead(0); delayed_requeue_timer_.setInterval(Config::Current()[QStringLiteral("AutoCacheDelay")].toInt()); delayed_requeue_timer_.setSingleShot(true); @@ -61,16 +61,20 @@ void PreviewAutoCacher::SetPaused(bool paused) paused_ = paused; } -void GenerateHashesInternal(ViewerOutput *viewer, FrameHashCache* cache, const QVector ×, JobTime job_time) +QVector PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, const QVector ×) { - std::vector existing_hashes; + QVector hash_data(times.size()); + + QVector existing_hashes; + + for (int i=0; iGetConnectedTextureOutput(), viewer->GetVideoParams(), time); // Check memory list since disk checking is slow - bool hash_exists = (std::find(existing_hashes.begin(), existing_hashes.end(), hash) != existing_hashes.end()); + bool hash_exists = existing_hashes.contains(hash); if (!hash_exists) { hash_exists = QFileInfo::exists(cache->CachePathName(hash)); @@ -81,42 +85,10 @@ void GenerateHashesInternal(ViewerOutput *viewer, FrameHashCache* cache, const Q } // Set hash in FrameHashCache's thread rather than in ours to prevent race conditions - QMetaObject::invokeMethod(cache, "SetHash", Qt::QueuedConnection, - OLIVE_NS_ARG(rational, time), - Q_ARG(QByteArray, hash), - OLIVE_NS_ARG(JobTime, job_time), - Q_ARG(bool, hash_exists)); - } -} - -void PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, TimeRangeListFrameIterator iterator, JobTime job_time) -{ - QVector times = iterator.ToVector(); - - // Ensure number of threads doesn't exceed idealThreadCount for maximum concurrency - int hashes_per_thread = times.size() / qMax(1, QThread::idealThreadCount()-1); - - // Somewhat arbitrary (it felt right) number used to determine when the overhead of sending this - // to threads will exceed the benefit of multithreading - static const int kMinimumHashesPerThread = 500; - if (hashes_per_thread < kMinimumHashesPerThread) { - hashes_per_thread = kMinimumHashesPerThread; + hash_data[i] = {time, hash, hash_exists}; } - // Queue threaded tasks for each - if (hashes_per_thread >= times.size()) { - // Don't bother queuing in other thread, just run - GenerateHashesInternal(viewer, cache, times, job_time); - } else { - QVector > threads; - for (int i=0; i* watcher = static_cast*>(sender()); + QFutureWatcher< QVector >* watcher = static_cast >*>(sender()); if (hash_tasks_.contains(watcher)) { hash_tasks_.removeOne(watcher); - // Restart delayed requeue timer - delayed_requeue_timer_.stop(); - delayed_requeue_timer_.start(); + // Set all hashes we received + JobTime job_time = watcher->property("job").value(); + auto hashes = watcher->result(); + foreach (auto hash, hashes) { + if (video_job_tracker_.isCurrent(hash.time, job_time)) { + viewer_node_->video_frame_cache()->SetHash(hash.time, hash.hash, hash.exists); + } + } + + if (hash_iterator_.HasNext()) { + // Launch next hashes + QueueNextHashTask(); + } else { + // Restart delayed requeue timer + delayed_requeue_timer_.stop(); + delayed_requeue_timer_.start(); + } } // The cacher might be waiting for this job to finish @@ -170,18 +159,21 @@ void PreviewAutoCacher::AudioRendered() if (audio_tasks_.contains(watcher)) { if (watcher->HasResult()) { const TimeRange &range = audio_tasks_.value(watcher); + JobTime watcher_job_time = watcher->property("job").value(); + + TimeRangeList valid_ranges = audio_job_tracker_.getCurrentSubRanges(range, watcher_job_time); AudioVisualWaveform waveform = watcher->GetTicket()->property("waveform").value(); viewer_node_->audio_playback_cache()->WritePCM(range, + valid_ranges, watcher->Get().value(), - &waveform, - watcher->GetTicket()->GetJobTime()); + &waveform); bool pcm_is_usable = true; if (watcher->GetTicket()->property("incomplete").toBool()) { - if (last_conform_task_ > watcher->GetTicket()->GetJobTime()) { + if (last_conform_task_ > watcher_job_time) { // Requeue now viewer_node_->audio_playback_cache()->Invalidate(range); pcm_is_usable = false; @@ -205,19 +197,15 @@ void PreviewAutoCacher::AudioRendered() } } - if (track) { - QList valid_ranges = viewer_node_->audio_playback_cache()->GetValidRanges(waveform_info.range, - watcher->GetTicket()->GetJobTime()); - if (!valid_ranges.isEmpty()) { - // Generate visual waveform in this background thread - track->waveform().set_channel_count(viewer_node_->GetAudioParams().channel_count()); + if (track && !valid_ranges.isEmpty()) { + // Generate visual waveform in this background thread + track->waveform().set_channel_count(viewer_node_->GetAudioParams().channel_count()); - foreach (const TimeRange& r, valid_ranges) { - track->waveform().OverwriteSums(waveform_info.waveform, r.in(), r.in() - waveform_info.range.in(), r.length()); - } - - emit track->PreviewChanged(); + foreach (const TimeRange& r, valid_ranges) { + track->waveform().OverwriteSums(waveform_info.waveform, r.in(), r.in() - waveform_info.range.in(), r.length()); } + + emit track->PreviewChanged(); } } } @@ -246,6 +234,7 @@ void PreviewAutoCacher::VideoRendered() if (!hash.isEmpty() && VideoParams::FormatIsFloat(viewer_node_->GetVideoParams().format())) { FramePtr frame = watcher->Get().value(); RenderTicketWatcher* w = new RenderTicketWatcher(); + w->setProperty("job", QVariant::fromValue(last_update_time_)); w->setProperty("frame", QVariant::fromValue(frame)); video_download_tasks_.insert(w, hash); connect(w, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::VideoDownloaded); @@ -396,6 +385,11 @@ void PreviewAutoCacher::InsertIntoCopyMap(Node *node, Node *copy) Node::CopyInputs(node, copy, false); } +void PreviewAutoCacher::UpdateGraphChangeValue() +{ + graph_changed_time_.Acquire(); +} + void PreviewAutoCacher::UpdateLastSyncedValue() { last_update_time_.Acquire(); @@ -460,26 +454,31 @@ void PreviewAutoCacher::ClearVideoDownloadQueue(bool hard) void PreviewAutoCacher::NodeAdded(Node *node) { graph_update_queue_.append({QueuedJob::kNodeAdded, node, NodeInput(), NodeOutput()}); + UpdateGraphChangeValue(); } void PreviewAutoCacher::NodeRemoved(Node *node) { graph_update_queue_.append({QueuedJob::kNodeRemoved, node, NodeInput(), NodeOutput()}); + UpdateGraphChangeValue(); } void PreviewAutoCacher::EdgeAdded(const NodeOutput &output, const NodeInput &input) { graph_update_queue_.append({QueuedJob::kEdgeAdded, nullptr, input, output}); + UpdateGraphChangeValue(); } void PreviewAutoCacher::EdgeRemoved(const NodeOutput &output, const NodeInput &input) { graph_update_queue_.append({QueuedJob::kEdgeRemoved, nullptr, input, output}); + UpdateGraphChangeValue(); } void PreviewAutoCacher::ValueChanged(const NodeInput &input) { graph_update_queue_.append({QueuedJob::kValueChanged, nullptr, input, NodeOutput()}); + UpdateGraphChangeValue(); } void PreviewAutoCacher::TryRender() @@ -496,16 +495,11 @@ void PreviewAutoCacher::TryRender() // If we're here, we must be able to render if (!invalidated_video_.isEmpty()) { - TimeRangeListFrameIterator frames(invalidated_video_, viewer_node_->video_frame_cache()->GetTimebase()); + hash_iterator_ = TimeRangeListFrameIterator(invalidated_video_, viewer_node_->video_frame_cache()->GetTimebase()); - QFutureWatcher* watcher = new QFutureWatcher(); - hash_tasks_.append(watcher); - connect(watcher, &QFutureWatcher::finished, this, &PreviewAutoCacher::HashesProcessed); - watcher->setFuture(QtConcurrent::run(&PreviewAutoCacher::GenerateHashes, - copied_viewer_node_, - viewer_node_->video_frame_cache(), - frames, - last_update_time_)); + for (int i=0; isetProperty("job", QVariant::fromValue(last_update_time_)); connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered); audio_tasks_.insert(watcher, r); watcher->SetTicket(RenderManager::instance()->RenderAudio(copied_viewer_node_, r, RenderMode::kOffline, true)); @@ -550,6 +545,7 @@ RenderTicketWatcher* PreviewAutoCacher::RenderFrame(const QByteArray &hash, cons { RenderTicketWatcher* watcher = new RenderTicketWatcher(); watcher->setProperty("hash", hash); + watcher->setProperty("job", QVariant::fromValue(last_update_time_)); connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::VideoRendered); video_tasks_.insert(watcher, hash); watcher->SetTicket(RenderManager::instance()->RenderFrame(copied_viewer_node_, @@ -654,6 +650,8 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) copy_map_.clear(); copied_viewer_node_ = nullptr; graph_update_queue_.clear(); + video_job_tracker_.clear(); + audio_job_tracker_.clear(); // Disconnect signals for future node additions/deletions NodeGraph* graph = viewer_node_->parent(); @@ -701,6 +699,8 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) } } + // Ensure graph change value is just before the sync value + UpdateGraphChangeValue(); UpdateLastSyncedValue(); // Connect signals for future node additions/deletions @@ -712,7 +712,9 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) // Copy invalidated ranges - used to determine which frames need hashing invalidated_video_ = viewer_node_->video_frame_cache()->GetInvalidatedRanges(); + video_job_tracker_.insert(invalidated_video_, graph_changed_time_); invalidated_audio_ = viewer_node_->audio_playback_cache()->GetInvalidatedRanges(); + audio_job_tracker_.insert(invalidated_audio_, graph_changed_time_); connect(viewer_node_->video_frame_cache(), &PlaybackCache::Invalidated, @@ -776,6 +778,32 @@ void PreviewAutoCacher::QueueNextFrameInRange(int max) } } +void PreviewAutoCacher::QueueNextHashTask() +{ + // Magic number: dunno what the best number for this is yet + static const int kMaxFrames = 1000; + + QVector times(kMaxFrames); + for (int i=0; i >* watcher = new QFutureWatcher< QVector >(); + watcher->setProperty("job", QVariant::fromValue(last_update_time_)); + hash_tasks_.append(watcher); + connect(watcher, &QFutureWatcher< QVector >::finished, this, &PreviewAutoCacher::HashesProcessed); + watcher->setFuture(QtConcurrent::run(PreviewAutoCacher::GenerateHashes, + copied_viewer_node_, + viewer_node_->video_frame_cache(), + times)); +} + template void PreviewAutoCacher::ClearQueueInternal(T& list, bool hard, Func member) { diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index e60ca6c2a..8f865920e 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -9,6 +9,7 @@ #include "node/node.h" #include "node/output/viewer/viewer.h" #include "node/project/project.h" +#include "render/renderjobtracker.h" #include "threading/threadticketwatcher.h" namespace olive { @@ -80,8 +81,6 @@ public: void ClearVideoDownloadQueue(bool wait = false); private: - static void GenerateHashes(ViewerOutput *viewer, FrameHashCache *cache, TimeRangeListFrameIterator times, JobTime job_time); - void TryRender(); RenderTicketWatcher *RenderFrame(const QByteArray& hash, const rational &time, bool prioritize, bool texture_only); @@ -104,6 +103,7 @@ private: void InsertIntoCopyMap(Node* node, Node* copy); + void UpdateGraphChangeValue(); void UpdateLastSyncedValue(); void CancelQueuedSingleFrameRender(); @@ -116,7 +116,15 @@ private: void ClearQueueRemoveEventInternal(QVector::iterator it); void QueueNextFrameInRange(int max); - TimeRangeListFrameIterator queued_frame_iterator_; + void QueueNextHashTask(); + + struct HashData { + rational time; + QByteArray hash; + bool exists; + }; + + static QVector GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, const QVector ×); class QueuedJob { public: @@ -158,12 +166,13 @@ private: RenderTicketPtr single_frame_render_; - QList*> hash_tasks_; + QList >*> hash_tasks_; QMap audio_tasks_; QMap video_tasks_; QMap video_download_tasks_; QMap > video_immediate_passthroughs_; + JobTime graph_changed_time_; JobTime last_update_time_; bool ignore_next_mouse_button_; @@ -174,6 +183,12 @@ private: JobTime last_conform_task_; + RenderJobTracker video_job_tracker_; + RenderJobTracker audio_job_tracker_; + + TimeRangeListFrameIterator queued_frame_iterator_; + TimeRangeListFrameIterator hash_iterator_; + private slots: /** * @brief Handler for when the NodeGraph reports a video change over a certain time range diff --git a/app/render/renderjobtracker.cpp b/app/render/renderjobtracker.cpp new file mode 100644 index 000000000..29f07b3f0 --- /dev/null +++ b/app/render/renderjobtracker.cpp @@ -0,0 +1,71 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "renderjobtracker.h" + +namespace olive { + +void RenderJobTracker::insert(const TimeRange &range, JobTime job_time) +{ + // First remove any ranges with this (code copied + TimeRangeList::util_remove(&jobs_, range); + + // Now append the job + TimeRangeWithJob job(range, job_time); + jobs_.append(job); +} + +void RenderJobTracker::insert(const TimeRangeList &ranges, JobTime job_time) +{ + foreach (const TimeRange &r, ranges) { + insert(r, job_time); + } +} + +void RenderJobTracker::clear() +{ + jobs_.clear(); +} + +bool RenderJobTracker::isCurrent(const rational &time, JobTime job_time) const +{ + for (auto it=jobs_.crbegin(); it!=jobs_.crend(); it++) { + if (it->Contains(time)) { + return job_time >= it->GetJobTime(); + } + } + + return false; +} + +TimeRangeList RenderJobTracker::getCurrentSubRanges(const TimeRange &range, const JobTime &job_time) const +{ + TimeRangeList current_ranges; + + for (auto it=jobs_.crbegin(); it!=jobs_.crend(); it++) { + if (job_time >= it->GetJobTime() && it->OverlapsWith(range)) { + current_ranges.insert(it->Intersected(range)); + } + } + + return current_ranges; +} + +} diff --git a/app/render/renderjobtracker.h b/app/render/renderjobtracker.h new file mode 100644 index 000000000..169331072 --- /dev/null +++ b/app/render/renderjobtracker.h @@ -0,0 +1,67 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef RENDERJOBTRACKER_H +#define RENDERJOBTRACKER_H + +#include "common/jobtime.h" +#include "common/timerange.h" + +namespace olive { + +class RenderJobTracker +{ +public: + RenderJobTracker() = default; + + void insert(const TimeRange &range, JobTime job_time); + void insert(const TimeRangeList &ranges, JobTime job_time); + + void clear(); + + bool isCurrent(const rational &time, JobTime job_time) const; + + TimeRangeList getCurrentSubRanges(const TimeRange &range, const JobTime &job_time) const; + +private: + class TimeRangeWithJob : public TimeRange + { + public: + TimeRangeWithJob(const TimeRange &range, const JobTime &job_time) + { + set_range(range.in(), range.out()); + job_time_ = job_time; + } + + JobTime GetJobTime() const {return job_time_;} + void SetJobTime(JobTime jt) {job_time_ = jt;} + + private: + JobTime job_time_; + + }; + + QVector jobs_; + +}; + +} + +#endif // RENDERJOBTRACKER_H diff --git a/app/threading/threadticket.cpp b/app/threading/threadticket.cpp index 5dfd29d4c..d8d041d9e 100644 --- a/app/threading/threadticket.cpp +++ b/app/threading/threadticket.cpp @@ -27,7 +27,6 @@ RenderTicket::RenderTicket() : has_result_(false), finish_count_(0) { - SetJobTime(); } void RenderTicket::WaitForFinished(QMutex *mutex) diff --git a/app/threading/threadticket.h b/app/threading/threadticket.h index 82bd8b90a..d4035c832 100644 --- a/app/threading/threadticket.h +++ b/app/threading/threadticket.h @@ -38,16 +38,6 @@ class RenderTicket : public QObject public: RenderTicket(); - JobTime GetJobTime() const - { - return job_time_; - } - - void SetJobTime() - { - job_time_.Acquire(); - } - /** * @brief Get the ticket's current state * @@ -137,8 +127,6 @@ private: QWaitCondition wait_; - JobTime job_time_; - }; using RenderTicketPtr = std::shared_ptr; From 528852c14833784a33a54d2566193e7f0eec3672 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 12 Jul 2021 23:54:18 -0700 Subject: [PATCH 48/72] render: pack audio during playback Should optimize audio rendering --- app/render/audioparams.cpp | 31 +++- app/render/audioparams.h | 3 + app/render/audioplaybackcache.cpp | 250 ++++++++++++++++++------------ app/render/audioplaybackcache.h | 29 ++-- 4 files changed, 201 insertions(+), 112 deletions(-) diff --git a/app/render/audioparams.cpp b/app/render/audioparams.cpp index 062cf5be5..1654ef084 100644 --- a/app/render/audioparams.cpp +++ b/app/render/audioparams.cpp @@ -54,13 +54,6 @@ const QVector AudioParams::kSupportedChannelLayouts = { const AudioParams::Format AudioParams::kInternalFormat = AudioParams::kFormatFloat32; -qint64 AudioParams::time_to_bytes(const double &time) const -{ - Q_ASSERT(is_valid()); - - return qint64(time_to_samples(time)) * channel_count() * bytes_per_sample_per_channel(); -} - bool AudioParams::operator==(const AudioParams &other) const { return (format() == other.format() @@ -94,11 +87,28 @@ QAudioFormat::SampleType AudioParams::GetQtSampleType(AudioParams::Format format return QAudioFormat::Unknown; } +qint64 AudioParams::time_to_bytes(const double &time) const +{ + return time_to_bytes_per_channel(time) * channel_count(); +} + qint64 AudioParams::time_to_bytes(const rational &time) const { return time_to_bytes(time.toDouble()); } +qint64 AudioParams::time_to_bytes_per_channel(const double &time) const +{ + Q_ASSERT(is_valid()); + + return qint64(time_to_samples(time)) * bytes_per_sample_per_channel(); +} + +qint64 AudioParams::time_to_bytes_per_channel(const rational &time) const +{ + return time_to_bytes_per_channel(time.toDouble()); +} + qint64 AudioParams::time_to_samples(const double &time) const { Q_ASSERT(is_valid()); @@ -139,6 +149,13 @@ rational AudioParams::bytes_to_time(const qint64 &bytes) const return samples_to_time(bytes_to_samples(bytes)); } +rational AudioParams::bytes_per_channel_to_time(const qint64 &bytes) const +{ + Q_ASSERT(is_valid()); + + return samples_to_time(bytes_to_samples(bytes * channel_count())); +} + int AudioParams::channel_count() const { return channel_count_; diff --git a/app/render/audioparams.h b/app/render/audioparams.h index b79c14ede..d28105c40 100644 --- a/app/render/audioparams.h +++ b/app/render/audioparams.h @@ -161,12 +161,15 @@ public: qint64 time_to_bytes(const double& time) const; qint64 time_to_bytes(const rational& time) const; + qint64 time_to_bytes_per_channel(const double& time) const; + qint64 time_to_bytes_per_channel(const rational& time) const; qint64 time_to_samples(const double& time) const; qint64 time_to_samples(const rational& time) const; qint64 samples_to_bytes(const qint64& samples) const; rational samples_to_time(const qint64& samples) const; qint64 bytes_to_samples(const qint64 &bytes) const; rational bytes_to_time(const qint64 &bytes) const; + rational bytes_per_channel_to_time(const qint64 &bytes) const; int channel_count() const; int bytes_per_sample_per_channel() const; int bits_per_sample() const; diff --git a/app/render/audioplaybackcache.cpp b/app/render/audioplaybackcache.cpp index 95a9ab30b..50e004ad4 100644 --- a/app/render/audioplaybackcache.cpp +++ b/app/render/audioplaybackcache.cpp @@ -22,13 +22,14 @@ #include #include +#include #include #include "common/filefunctions.h" namespace olive { -const qint64 AudioPlaybackCache::kDefaultSegmentSize = 40 * 1024 * 1024; +const qint64 AudioPlaybackCache::kDefaultSegmentSizePerChannel = 10 * 1024 * 1024; AudioPlaybackCache::AudioPlaybackCache(QObject* parent) : PlaybackCache(parent) @@ -59,71 +60,78 @@ void AudioPlaybackCache::SetParameters(const AudioParams ¶ms) void AudioPlaybackCache::WritePCM(const TimeRange &range, const TimeRangeList &valid_ranges, SampleBufferPtr samples, const AudioVisualWaveform *waveform) { // Ensure if we have enough segments to write this data, creating more if not - qint64 length_diff = params_.time_to_bytes(range.out()) - playlist_.GetLength(); + qint64 length_diff = params_.time_to_bytes_per_channel(range.out()) - playlist_.GetLength(); while (length_diff > 0) { - qint64 seg_sz = qMin(kDefaultSegmentSize, length_diff); + qint64 seg_sz = qMin(kDefaultSegmentSizePerChannel, length_diff); playlist_.push_back(CreateSegment(seg_sz, playlist_.GetLength())); length_diff -= seg_sz; } - // Convert to packed data, which is what we store on disk so it can be played back easily - QByteArray a; - if (samples) { - a = samples->toPackedData(); - } - // Keep track of validated ranges so we can signal them all at once at the end TimeRangeList ranges_we_validated; + // Calculate buffer size per channel + qint64 buffer_size_per_channel = samples->sample_count() * params_.bytes_per_sample_per_channel(); + // Write each valid range to the segments foreach (const TimeRange& r, valid_ranges) { rational this_segment_in = 0; // Write PCM to playlist for (auto it=playlist_.begin(); it!=playlist_.end(); it++) { - rational this_segment_out = this_segment_in + params_.bytes_to_time((*it).size()); + rational this_segment_out = this_segment_in + params_.bytes_per_channel_to_time((*it).size()); if (r.in() < this_segment_out) { // We'll write at least something to this segment - QFile seg_file((*it).filename()); + bool succeeded = true; - if (seg_file.open(QFile::ReadWrite)) { - // Calculate how much to write - rational this_write_in_point = qMax(r.in(), this_segment_in); - rational this_write_out_point = qMin(r.out(), this_segment_out); + // Calculate how much to write + rational this_write_in_point = qMax(r.in(), this_segment_in); + rational this_write_out_point = qMin(r.out(), this_segment_out); - // Calculate what the byte offsets are going to be in this segment file - rational in_point_relative = this_write_in_point - this_segment_in; - qint64 dst_offset = params_.time_to_bytes(in_point_relative); + for (int i=0; i<(*it).channels(); i++) { + QFile seg_file((*it).filename(i)); - // Calculate where to retrieve data from in the source buffer - qint64 src_offset = params_.time_to_bytes(this_write_in_point - range.in()); + if (seg_file.open(QFile::ReadWrite)) { + // Calculate what the byte offsets are going to be in this segment file + rational in_point_relative = this_write_in_point - this_segment_in; + qint64 dst_offset = params_.time_to_bytes_per_channel(in_point_relative); - // Determine how many bytes need to be written - qint64 total_write_length = params_.time_to_bytes(this_write_out_point - this_write_in_point); + // Calculate where to retrieve data from in the source buffer + qint64 src_offset = params_.time_to_bytes_per_channel(this_write_in_point - range.in()); - // Determine how many bytes we actually have in the source buffer - qint64 possible_write_length = qMin(qMax(qint64(0), a.size() - src_offset), total_write_length); + // Determine how many bytes need to be written + qint64 total_write_length = params_.time_to_bytes_per_channel(this_write_out_point - this_write_in_point); - // Seek to our start offset - seg_file.seek(dst_offset); + // Retrieve data buffer + const char *a = reinterpret_cast(samples->data(i)); - // If we have source bytes to write, write them here - if (possible_write_length > 0) { - seg_file.write(a.data() + src_offset, possible_write_length); + // Determine how many bytes we actually have in the source buffer + qint64 possible_write_length = qMin(qMax(qint64(0), buffer_size_per_channel - src_offset), total_write_length); + + // Seek to our start offset + seg_file.seek(dst_offset); + + // If we have source bytes to write, write them here + if (possible_write_length > 0) { + seg_file.write(a + src_offset, possible_write_length); + } + + if (possible_write_length < total_write_length) { + // Fill remaining space with silence + QByteArray s(total_write_length - possible_write_length, 0x00); + seg_file.write(s); + } + + seg_file.close(); + } else { + qWarning() << "Failed to write PCM data to" << seg_file.fileName(); + succeeded = false; } + } - if (possible_write_length < total_write_length) { - // Fill remaining space with silence - QByteArray s(total_write_length - possible_write_length, 0x00); - seg_file.write(s); - } - - seg_file.close(); - + if (succeeded) { ranges_we_validated.insert(TimeRange(this_write_in_point, this_write_out_point)); - } else { - qWarning() << "Failed to write PCM data to" << seg_file.fileName(); } } @@ -163,8 +171,8 @@ void AudioPlaybackCache::ShiftEvent(const rational &from_in_time, const rational return; } - qint64 to = params_.time_to_bytes(to_in_time); - qint64 from = params_.time_to_bytes(from_in_time); + qint64 to = params_.time_to_bytes_per_channel(to_in_time); + qint64 from = params_.time_to_bytes_per_channel(from_in_time); int to_seg_index = playlist_.GetIndexOfPosition(to); int from_seg_index = playlist_.GetIndexOfPosition(from); @@ -197,7 +205,7 @@ void AudioPlaybackCache::ShiftEvent(const rational &from_in_time, const rational qint64 time_to_insert = to - from; while (time_to_insert) { - qint64 new_seg_sz = qMin(kDefaultSegmentSize, time_to_insert); + qint64 new_seg_sz = qMin(kDefaultSegmentSizePerChannel, time_to_insert); // Set offset to 0 for now and fill it in later playlist_.insert(insert_index, CreateSegment(new_seg_sz, 0)); @@ -265,7 +273,7 @@ void AudioPlaybackCache::LengthChangedEvent(const rational& old, const rational& return; } - qint64 new_len_in_bytes = params_.time_to_bytes(newlen); + qint64 new_len_in_bytes = params_.time_to_bytes_per_channel(newlen); while (new_len_in_bytes < playlist_.GetLength()) { Segment& last_seg = playlist_.back(); @@ -286,27 +294,41 @@ AudioPlaybackCache::Segment AudioPlaybackCache::CloneSegment(const AudioPlayback { Segment new_seg = s; - // Copy data to a new file - QString new_filename = GenerateSegmentFilename(); - QFile::copy(s.filename(), new_filename); + new_seg.set_channels(s.channels()); - new_seg.set_filename(new_filename); + // Copy data to a new file + for (int i=0; igenerate(); new_seg_filename = QDir(GetCacheDirectory()).filePath(QStringLiteral("%1.pcm").arg(r)); } while (QFileInfo::exists(new_seg_filename)); @@ -325,21 +347,23 @@ QString AudioPlaybackCache::GenerateSegmentFilename() const void AudioPlaybackCache::TrimSegmentIn(AudioPlaybackCache::Segment *s, qint64 new_length) { // Read filename - QFile f(s->filename()); - if (f.open(QFile::ReadWrite)) { - // Read segment into memory, according to the size we acknowledge - QByteArray data = f.read(s->size()); + for (int i=0; ichannels(); i++) { + QFile f(s->filename(i)); + if (f.open(QFile::ReadWrite)) { + // Read segment into memory, according to the size we acknowledge + QByteArray data = f.read(s->size()); - // Trim to new length - data = data.right(new_length); + // Trim to new length + data = data.right(new_length); - // Seek to start and write - f.seek(0); + // Seek to start and write + f.seek(0); - // Write trimmed data - f.write(data); + // Write trimmed data + f.write(data); - f.close(); + f.close(); + } } s->set_size(new_length); @@ -353,14 +377,19 @@ void AudioPlaybackCache::TrimSegmentOut(AudioPlaybackCache::Segment *s, qint64 n void AudioPlaybackCache::RemoveSegmentFromArray(int index) { - QFile::remove(playlist_.at(index).filename()); + const Segment &s = playlist_.at(index); + for (int i=0; i segment_files(cs.channels()); + segment_files.fill(nullptr); - // Determine how many bytes to read - qint64 this_read_length = qMin(current_segment_sz - segment_read_index_, - maxSize - read_size); + bool all_files_opened = true; - // Read those bytes - segment_file.read(data + read_size, this_read_length); + // Open all file handles + for (int i=0; iopen(QFile::ReadOnly)) { + // Seek to our stored index of this segment + f->seek(segment_read_index_); + } else { + all_files_opened = false; + break; + } + } - // Add to the read index - segment_read_index_ += this_read_length; + // If all file handles opened successfully, time to interleave and send them out + if (all_files_opened) { + // Determine how many bytes to read + qint64 this_read_length = qMin((current_segment_sz - segment_read_index_) * cs.channels(), maxSize - read_size); - // Add to the read size - read_size += this_read_length; + qint64 target = read_size + this_read_length; - // If we've reached the end of this segment, tick the counter over to the next segment - if (segment_read_index_ == current_segment_sz) { - // Jump to the next file - segment_read_index_ = 0; - current_segment_++; + while (read_size < target) { + for (int i=0; iread(data + read_size, sample_size_); + + // Add to the read size + read_size += sample_size_; + } + + // Add to the read index + segment_read_index_ += sample_size_; + + // If we've reached the end of this segment, tick the counter over to the next segment + if (segment_read_index_ == current_segment_sz) { + // Jump to the next file + segment_read_index_ = 0; + current_segment_++; + } + } + } + + // Close and delete file handles + for (int i=0; iisOpen()) { + f->close(); + } + delete f; } - } else { - qWarning() << "Failed to read data from segment"; - break; } } diff --git a/app/render/audioplaybackcache.h b/app/render/audioplaybackcache.h index a3f26b6d1..9bbe8717f 100644 --- a/app/render/audioplaybackcache.h +++ b/app/render/audioplaybackcache.h @@ -73,8 +73,7 @@ public: class Segment { public: - Segment() = default; - Segment(qint64 size, const QString& filename); + Segment(qint64 size = 0); qint64 size() const { @@ -96,14 +95,24 @@ public: offset_ = o; } - const QString& filename() const + int channels() const { - return filename_; + return filenames_.size(); } - void set_filename(const QString& filename) + void set_channels(int index) { - filename_ = filename; + filenames_.resize(index); + } + + const QString& filename(int index) const + { + return filenames_.at(index); + } + + void set_filename(int index, const QString& filename) + { + filenames_[index] = filename; } qint64 end() const @@ -112,7 +121,7 @@ public: } private: - QString filename_; + QVector filenames_; qint64 size_; @@ -134,7 +143,7 @@ public: class PlaybackDevice : public QIODevice { public: - PlaybackDevice(const Playlist& playlist, QObject* parent = nullptr); + PlaybackDevice(const Playlist& playlist, int sample_sz, QObject* parent = nullptr); virtual ~PlaybackDevice() override; @@ -167,6 +176,8 @@ public: qint64 segment_read_index_; + int sample_size_; + }; /** @@ -194,7 +205,7 @@ protected: virtual void LengthChangedEvent(const rational& old, const rational& newlen) override; private: - static const qint64 kDefaultSegmentSize; + static const qint64 kDefaultSegmentSizePerChannel; Segment CloneSegment(const Segment& s) const; From 13a7976a1b1e8db195b70c8cf72a8264c322690f Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 15 Jul 2021 20:36:41 -0700 Subject: [PATCH 49/72] viewer: fix incorrect seek caused by earlier audio overhaul --- app/widget/viewer/viewer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index e64d12edf..736450acf 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -452,7 +452,7 @@ void ViewerWidget::StartAudioOutput() if (audio_cache->GetParameters().is_valid()) { AudioManager::instance()->SetOutputParams(audio_cache->GetParameters()); AudioManager::instance()->StartOutput(audio_cache, - audio_cache->GetParameters().time_to_bytes(GetTime()), + audio_cache->GetParameters().time_to_bytes_per_channel(GetTime()), playback_speed_); emit AudioManager::instance()->OutputWaveformStarted(&audio_cache->visual(), GetTime(), playback_speed_); @@ -644,7 +644,7 @@ void ViewerWidget::PushScrubbedAudio() int size_of_sample = params.time_to_bytes(rational(20, 1000)); // Push audio - audio_src->seek(params.time_to_bytes(GetTime())); + audio_src->seek(params.time_to_bytes_per_channel(GetTime())); QByteArray frame_audio = audio_src->read(size_of_sample); AudioManager::instance()->SetOutputParams(params); AudioManager::instance()->PushToOutput(frame_audio); From 657f9c5520829c18866f489eb42efd4468bccaf4 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 15 Jul 2021 20:50:25 -0700 Subject: [PATCH 50/72] addtool: fixed positioning --- app/widget/timelinewidget/tool/add.cpp | 30 +++++++++++--------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index b33862d3d..fd0011bc1 100644 --- a/app/widget/timelinewidget/tool/add.cpp +++ b/app/widget/timelinewidget/tool/add.cpp @@ -108,15 +108,14 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event) NodeGraph* graph = static_cast(parent()->GetConnectedNode()->parent()); - command->add_child(new NodeAddCommand(graph, - clip)); - + command->add_child(new NodeAddCommand(graph, clip)); + command->add_child(new NodeSetPositionCommand(clip, clip, QPointF(0, 0), false)); command->add_child(new TrackPlaceBlockCommand(sequence()->track_list(track.type()), track.index(), clip, ghost_->GetAdjustedIn())); - QPointF extra_node_offset(-1, 0); + Node *node_to_add = nullptr; switch (Core::instance()->GetSelectedAddableObject()) { case olive::Tool::kAddableEmpty: @@ -124,24 +123,12 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event) break; case olive::Tool::kAddableSolid: { - Node* solid = new SolidGenerator(); - - command->add_child(new NodeAddCommand(graph, - solid)); - - command->add_child(new NodeEdgeAddCommand(solid, NodeInput(clip, ClipBlock::kBufferIn))); - command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(solid, clip, clip, extra_node_offset)); + node_to_add = new SolidGenerator(); break; } case olive::Tool::kAddableTitle: { - Node* text = new TextGenerator(); - - command->add_child(new NodeAddCommand(graph, - text)); - - command->add_child(new NodeEdgeAddCommand(text, NodeInput(clip, ClipBlock::kBufferIn))); - command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(text, clip, clip, extra_node_offset)); + node_to_add = new TextGenerator(); break; } case olive::Tool::kAddableBars: @@ -157,6 +144,13 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event) break; } + if (node_to_add) { + QPointF extra_node_offset(-1, 0); + command->add_child(new NodeAddCommand(graph, node_to_add)); + command->add_child(new NodeEdgeAddCommand(node_to_add, NodeInput(clip, ClipBlock::kBufferIn))); + command->add_child(new NodeSetPositionCommand(node_to_add, clip, extra_node_offset, false)); + } + Core::instance()->undo_stack()->push(command); } From 8350ce300ff95fa9bc651e4eaf44ffacab294981 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 15 Jul 2021 20:59:37 -0700 Subject: [PATCH 51/72] cache: improved invalidation system --- app/core.cpp | 3 ++- app/core.h | 9 +++++++ app/render/previewautocacher.cpp | 25 ++++++++----------- app/render/previewautocacher.h | 12 --------- .../nodeparamviewwidgetbridge.cpp | 25 ++++++++----------- app/widget/slider/base/numericsliderbase.cpp | 12 ++++++++- app/widget/slider/base/numericsliderbase.h | 4 +++ 7 files changed, 46 insertions(+), 44 deletions(-) diff --git a/app/core.cpp b/app/core.cpp index 0938330af..a4075b0a0 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -82,7 +82,8 @@ Core::Core(const CoreParams& params) : tool_(Tool::kPointer), addable_object_(Tool::kAddableEmpty), snapping_(true), - core_params_(params) + core_params_(params), + effects_slider_is_being_dragged_(false) { // Store reference to this object, making the assumption that Core will only ever be made in // main(). This will obviously break if not. diff --git a/app/core.h b/app/core.h index 67fb6a95d..83658d163 100644 --- a/app/core.h +++ b/app/core.h @@ -304,6 +304,10 @@ public: void OpenNodeInViewer(ViewerOutput* viewer); + bool EffectsSliderIsBeingDragged() const {return effects_slider_is_being_dragged_;} + + void SetEffectsSliderIsBeingDragged(bool e) {effects_slider_is_being_dragged_ = e;} + static const uint kProjectVersion; public slots: @@ -571,6 +575,11 @@ private: */ QVector autorecovered_projects_; + /** + * @brief An effects slider somewhere is being dragged + */ + bool effects_slider_is_being_dragged_; + private slots: void SaveAutorecovery(); diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index ce2c4fe5d..5801bd532 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -4,6 +4,7 @@ #include #include "codec/conformmanager.h" +#include "core.h" #include "node/project/project.h" #include "render/rendermanager.h" #include "render/renderprocessor.h" @@ -14,12 +15,11 @@ PreviewAutoCacher::PreviewAutoCacher() : viewer_node_(nullptr), has_changed_(false), use_custom_range_(false), - single_frame_render_(nullptr), - ignore_next_mouse_button_(false) + single_frame_render_(nullptr) { paused_ = !Config::Current()[QStringLiteral("AutoCacheEnabled")].toBool(), - SetPlayhead(0); + SetPlayhead(0); delayed_requeue_timer_.setInterval(Config::Current()[QStringLiteral("AutoCacheDelay")].toInt()); delayed_requeue_timer_.setSingleShot(true); @@ -96,9 +96,7 @@ void PreviewAutoCacher::VideoInvalidated(const TimeRange &range) ClearVideoQueue(); // Hash these frames since that should be relatively quick. - if (ignore_next_mouse_button_ || !(qApp->mouseButtons() & Qt::LeftButton)) { - ignore_next_mouse_button_ = false; - + if (!Core::instance()->EffectsSliderIsBeingDragged()) { invalidated_video_.insert(range); video_job_tracker_.insert(range, graph_changed_time_); @@ -563,7 +561,7 @@ void PreviewAutoCacher::RequeueFrames() delayed_requeue_timer_.stop(); if (viewer_node_ - && viewer_node_->video_frame_cache()->HasInvalidatedRanges() + && viewer_node_->video_frame_cache()->HasInvalidatedRanges(viewer_node_->GetVideoLength()) && hash_tasks_.isEmpty() && has_changed_ && VideoParams::FormatIsFloat(viewer_node_->GetVideoParams().format()) @@ -577,7 +575,7 @@ void PreviewAutoCacher::RequeueFrames() using_range = cache_range_; } - TimeRangeList invalidated = viewer_node_->video_frame_cache()->GetInvalidatedRanges().Intersects(using_range); + TimeRangeList invalidated = viewer_node_->video_frame_cache()->GetInvalidatedRanges(using_range); queued_frame_iterator_ = TimeRangeListFrameIterator(invalidated, viewer_node_->video_frame_cache()->GetTimebase()); QueueNextFrameInRange(RenderManager::GetNumberOfIdealConcurrentJobs()); @@ -598,11 +596,6 @@ void PreviewAutoCacher::ConformFinished() } } -void PreviewAutoCacher::IgnoreNextMouseButton() -{ - ignore_next_mouse_button_ = true; -} - void PreviewAutoCacher::ForceCacheRange(const TimeRange &range) { has_changed_ = true; @@ -690,6 +683,8 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) // Find copied viewer node copied_viewer_node_ = static_cast(copy_map_.value(viewer_node_)); + copied_viewer_node_->SetViewerVideoCacheEnabled(false); + copied_viewer_node_->SetViewerAudioCacheEnabled(false); copied_color_manager_ = static_cast(copy_map_.value(viewer_node_->project()->color_manager())); // Add all connections @@ -711,9 +706,9 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) connect(graph, &NodeGraph::ValueChanged, this, &PreviewAutoCacher::ValueChanged); // Copy invalidated ranges - used to determine which frames need hashing - invalidated_video_ = viewer_node_->video_frame_cache()->GetInvalidatedRanges(); + invalidated_video_ = viewer_node_->video_frame_cache()->GetInvalidatedRanges(viewer_node_->GetVideoLength()); video_job_tracker_.insert(invalidated_video_, graph_changed_time_); - invalidated_audio_ = viewer_node_->audio_playback_cache()->GetInvalidatedRanges(); + invalidated_audio_ = viewer_node_->audio_playback_cache()->GetInvalidatedRanges(viewer_node_->GetAudioLength()); audio_job_tracker_.insert(invalidated_audio_, graph_changed_time_); connect(viewer_node_->video_frame_cache(), diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index 8f865920e..0036a9738 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -34,16 +34,6 @@ public: */ void SetViewerNode(ViewerOutput *viewer_node); - /** - * @brief If the mouse is held during the next cache invalidation, cache anyway - * - * By default, PreviewAutoCacher ignores invalidations that occur while the mouse is held down, - * assuming that if the mouse is held, the user is dragging something. If you know the mouse will - * be held during a certain action and want PreviewAutoCacher to cache anyway, call this before - * the cache invalidates. - */ - void IgnoreNextMouseButton(); - /** * @brief Returns whether the auto-cache is currently paused or not */ @@ -175,8 +165,6 @@ private: JobTime graph_changed_time_; JobTime last_update_time_; - bool ignore_next_mouse_button_; - QTimer delayed_requeue_timer_; TimeRangeList audio_needing_conform_; diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index 8dfe37d58..78d54cc26 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -62,6 +62,11 @@ void NodeParamViewWidgetBridge::SetTime(const rational &time) } } +int GetSliderCount(NodeValue::Type type) +{ + return NodeValue::get_number_of_keyframe_tracks(type); +} + void NodeParamViewWidgetBridge::CreateWidgets() { if (input_.IsArray() && input_.element() == -1) { @@ -73,7 +78,8 @@ void NodeParamViewWidgetBridge::CreateWidgets() } else { // We assume the first data type is the "primary" type - switch (input_.GetDataType()) { + NodeValue::Type t = input_.GetDataType(); + switch (t) { // None of these inputs have applicable UI widgets case NodeValue::kNone: case NodeValue::kTexture: @@ -89,29 +95,17 @@ void NodeParamViewWidgetBridge::CreateWidgets() CreateSliders(1); break; } - case NodeValue::kFloat: - { - CreateSliders(1); - break; - } case NodeValue::kRational: { CreateSliders(1); break; } + case NodeValue::kFloat: case NodeValue::kVec2: - { - CreateSliders(2); - break; - } case NodeValue::kVec3: - { - CreateSliders(3); - break; - } case NodeValue::kVec4: { - CreateSliders(4); + CreateSliders(GetSliderCount(t)); break; } case NodeValue::kCombo: @@ -410,6 +404,7 @@ void NodeParamViewWidgetBridge::CreateSliders(int count) T* fs = new T(); fs->SliderBase::SetDefaultValue(input_.GetSplitDefaultValueForTrack(i)); fs->SetLadderElementCount(2); + fs->SetIsEffectsSlider(true); widgets_.append(fs); connect(fs, &T::ValueChanged, this, &NodeParamViewWidgetBridge::WidgetCallback); } diff --git a/app/widget/slider/base/numericsliderbase.cpp b/app/widget/slider/base/numericsliderbase.cpp index 38648b685..851733673 100644 --- a/app/widget/slider/base/numericsliderbase.cpp +++ b/app/widget/slider/base/numericsliderbase.cpp @@ -22,6 +22,7 @@ #include "common/qtutils.h" #include "config/config.h" +#include "core.h" namespace olive { @@ -34,7 +35,8 @@ NumericSliderBase::NumericSliderBase(QWidget *parent) : has_max_(false), dragged_diff_(0), drag_multiplier_(1.0), - setting_drag_value_(false) + setting_drag_value_(false), + is_effects_slider_(false) { // Numeric sliders are draggable, so we have a cursor that indicates that setCursor(Qt::SizeHorCursor); @@ -60,6 +62,10 @@ void NumericSliderBase::LabelPressed() connect(drag_ladder_, &SliderLadder::DraggedByValue, this, &NumericSliderBase::LadderDragged); connect(drag_ladder_, &SliderLadder::Released, this, &NumericSliderBase::LadderReleased); + + if (is_effects_slider_) { + Core::instance()->SetEffectsSliderIsBeingDragged(true); + } } void NumericSliderBase::LadderDragged(int value, double multiplier) @@ -90,6 +96,10 @@ void NumericSliderBase::LadderDragged(int value, double multiplier) void NumericSliderBase::LadderReleased() { + if (is_effects_slider_) { + Core::instance()->SetEffectsSliderIsBeingDragged(false); + } + drag_ladder_->deleteLater(); drag_ladder_ = nullptr; dragged_diff_ = 0; diff --git a/app/widget/slider/base/numericsliderbase.h b/app/widget/slider/base/numericsliderbase.h index 0f9e3df5d..3a02f49cc 100644 --- a/app/widget/slider/base/numericsliderbase.h +++ b/app/widget/slider/base/numericsliderbase.h @@ -42,6 +42,8 @@ public: bool IsDragging() const; + void SetIsEffectsSlider(bool e) {is_effects_slider_ = e;} + protected: const QVariant& GetOffset() const { @@ -87,6 +89,8 @@ private: bool setting_drag_value_; + bool is_effects_slider_; + private slots: void LabelPressed(); From 4bbc75392a6da1b675266af573304a28836c51c5 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 15 Jul 2021 21:00:18 -0700 Subject: [PATCH 52/72] nodes: rewrote and simplified length signaling system --- app/node/block/block.cpp | 26 ++++- app/node/block/block.h | 4 + app/node/block/clip/clip.cpp | 6 +- app/node/block/clip/clip.h | 2 +- app/node/node.cpp | 8 +- app/node/node.h | 10 +- app/node/output/track/track.cpp | 61 +++--------- app/node/output/track/track.h | 10 +- app/node/output/viewer/viewer.cpp | 10 +- app/node/output/viewer/viewer.h | 8 +- app/render/audioplaybackcache.cpp | 34 +------ app/render/audioplaybackcache.h | 2 - app/render/framehashcache.cpp | 13 +-- app/render/framehashcache.h | 4 - app/render/playbackcache.cpp | 110 ++++++++------------- app/render/playbackcache.h | 42 +++----- app/task/precache/precachetask.cpp | 11 ++- app/widget/timelinewidget/timelineundo.cpp | 8 +- app/widget/timelinewidget/timelineundo.h | 9 +- app/widget/timeruler/timeruler.cpp | 11 ++- 20 files changed, 148 insertions(+), 241 deletions(-) diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index 47fecec22..646736ccf 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -22,6 +22,7 @@ #include +#include "core.h" #include "node/output/track/track.h" #include "transition/transition.h" #include "widget/slider/floatslider.h" @@ -29,6 +30,8 @@ namespace olive { +#define super Node + const QString Block::kLengthInput = QStringLiteral("length_in"); const QString Block::kMediaInInput = QStringLiteral("media_in_in"); const QString Block::kEnabledInput = QStringLiteral("enabled_in"); @@ -47,7 +50,6 @@ Block::Block() : SetInputProperty(kLengthInput, QStringLiteral("min"), QVariant::fromValue(rational(0, 1))); SetInputProperty(kLengthInput, QStringLiteral("view"), RationalSlider::kTime); SetInputProperty(kLengthInput, QStringLiteral("viewlock"), true); - IgnoreInvalidationsFrom(kLengthInput); IgnoreHashingFrom(kLengthInput); AddInput(kMediaInInput, NodeValue::kRational, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); @@ -221,7 +223,7 @@ void Block::set_length_internal(const rational &length) void Block::Retranslate() { - Node::Retranslate(); + super::Retranslate(); SetInputName(kLengthInput, tr("Length")); SetInputName(kMediaInInput, tr("Media In")); @@ -235,4 +237,24 @@ void Block::Hash(const QString &, QCryptographicHash &, const rational &, const // A block does nothing by default, so we hash nothing } +void Block::InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) +{ + TimeRange r; + + if (from == kLengthInput) { + // We must intercept the signal here + r = TimeRange(qMin(length(), last_length_), RATIONAL_MAX); + + if (!Core::instance()->EffectsSliderIsBeingDragged()) { + last_length_ = length(); + } + + options.insert(QStringLiteral("lengthevent"), true); + } else { + r = range; + } + + super::InvalidateCache(r, from, element, options); +} + } diff --git a/app/node/block/block.h b/app/node/block/block.h index f8c6ab8ed..7593b1a81 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -155,6 +155,8 @@ public: virtual void Hash(const QString& output, QCryptographicHash &hash, const rational &time, const VideoParams& video_params) const override; + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element = -1, InvalidateCacheOptions options = InvalidateCacheOptions()) override; + static const QString kLengthInput; static const QString kMediaInInput; static const QString kEnabledInput; @@ -193,6 +195,8 @@ private: QVector block_links_; + rational last_length_; + }; } diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index d6b66b1ad..7db010721 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -53,7 +53,7 @@ QString ClipBlock::Description() const return tr("A time-based node that represents a media source."); } -void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int element) +void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) { Q_UNUSED(element) @@ -63,10 +63,10 @@ void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int rational start = MediaToSequenceTime(range.in()); rational end = MediaToSequenceTime(range.out()); - super::InvalidateCache(TimeRange(start, end), from, element); + super::InvalidateCache(TimeRange(start, end), from, element, options); } else { // Otherwise, pass signal along normally - super::InvalidateCache(range, from, element); + super::InvalidateCache(range, from, element, options); } } diff --git a/app/node/block/clip/clip.h b/app/node/block/clip/clip.h index e5d7ecbbe..c81d3b510 100644 --- a/app/node/block/clip/clip.h +++ b/app/node/block/clip/clip.h @@ -42,7 +42,7 @@ public: virtual QString id() const override; virtual QString Description() const override; - virtual void InvalidateCache(const TimeRange& range, const QString& from, int element) override; + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) override; virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; diff --git a/app/node/node.cpp b/app/node/node.cpp index 19294529a..f758b849a 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -1019,12 +1019,12 @@ NodeValueTable Node::Value(const QString& output, NodeValueDatabase &value) cons return value.Merge(); } -void Node::InvalidateCache(const TimeRange &range, const QString &from, int element) +void Node::InvalidateCache(const TimeRange &range, const QString &from, int element, InvalidateCacheOptions options) { Q_UNUSED(from) Q_UNUSED(element) - SendInvalidateCache(range); + SendInvalidateCache(range, options); } void Node::BeginOperation() @@ -1173,14 +1173,14 @@ Node *Node::CopyNodeInGraph(const Node *node, MultiUndoCommand *command) return copy; } -void Node::SendInvalidateCache(const TimeRange &range) +void Node::SendInvalidateCache(const TimeRange &range, const InvalidateCacheOptions &options) { if (GetOperationStack() == 0) { for (const OutputConnection& conn : output_connections_) { // Send clear cache signal to the Node const NodeInput& in = conn.second; - in.node()->InvalidateCache(range, in.input(), in.element()); + in.node()->InvalidateCache(range, in.input(), in.element(), options); } } } diff --git a/app/node/node.h b/app/node/node.h index 939bc37d0..5cb8d13b1 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -623,6 +623,8 @@ public: */ static T* ValueToPtr(const QVariant& ptr); + using InvalidateCacheOptions = QHash; + /** * @brief Signal all dependent Nodes that anything cached between start_range and end_range is now invalid and * requires re-rendering @@ -632,11 +634,11 @@ public: * the DAG. Even if the time needs to be transformed somehow (e.g. converting media time to sequence time), you can * call this function with transformed time and relay the signal that way. */ - virtual void InvalidateCache(const TimeRange& range, const QString& from, int element = -1); + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element = -1, InvalidateCacheOptions options = InvalidateCacheOptions()); - void InvalidateCache(const TimeRange& range, const NodeInput& from) + void InvalidateCache(const TimeRange& range, const NodeInput& from, const InvalidateCacheOptions &options = InvalidateCacheOptions()) { - InvalidateCache(range, from.input(), from.element()); + InvalidateCache(range, from.input(), from.element(), options); } /** @@ -882,7 +884,7 @@ protected: SetInputProperty(id, QStringLiteral("combo_str"), strings); } - void SendInvalidateCache(const TimeRange &range); + void SendInvalidateCache(const TimeRange &range, const InvalidateCacheOptions &options); /** * @brief Don't send cache invalidation signals if `input` is connected or disconnected diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 3a79d8001..4632b1ca4 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -41,8 +41,6 @@ const QString Track::kMutedInput = QStringLiteral("muted_in"); Track::Track() : track_type_(Track::kNone), track_length_(0), - midop_track_length_(0), - preop_track_length_(0), index_(-1), locked_(false) { @@ -431,7 +429,7 @@ QVector Track::BlocksAtTimeRange(const TimeRange &range) const return list; } -void Track::InvalidateCache(const TimeRange& range, const QString& from, int element) +void Track::InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) { if (GetOperationStack() != 0) { return; @@ -443,7 +441,8 @@ void Track::InvalidateCache(const TimeRange& range, const QString& from, int ele if (from == kBlockInput && element >= 0 - && (b = dynamic_cast(GetConnectedOutput(from, element).node()))) { + && (b = dynamic_cast(GetConnectedOutput(from, element).node())) + && !options.value(QStringLiteral("lengthevent")).toBool()) { // Limit the range signal to the corresponding block if (range.out() <= b->in() || range.in() >= b->out()) { return; @@ -451,11 +450,14 @@ void Track::InvalidateCache(const TimeRange& range, const QString& from, int ele limited = TimeRange(qMax(range.in(), b->in()), qMin(range.out(), b->out())); } else { - limited = TimeRange(qMax(range.in(), rational(0)), qMin(range.out(), qMax(preop_track_length_, track_length()))); - preop_track_length_ = track_length_; + limited = range; } - Node::InvalidateCache(limited, from, element); + // NOTE: For now, I figure we drop this key, but we may find in the future that it's advantageous + // to keep it + options.remove(QStringLiteral("lengthevent")); + + Node::InvalidateCache(limited, from, element, options); } void Track::InsertBlockBefore(Block* block, Block* after) @@ -600,15 +602,6 @@ void Track::Hash(const QString &output, QCryptographicHash &hash, const rational } } -void Track::EndOperation() -{ - super::EndOperation(); - - if (track_length_ != midop_track_length_) { - SetLengthInternal(midop_track_length_); - } -} - void Track::SetMuted(bool e) { SetStandardValue(kMutedInput, e); @@ -658,21 +651,10 @@ int Track::GetCacheIndexFromArrayIndex(int index) const return block_array_indexes_.indexOf(index); } -void Track::SetLengthInternal(const rational &r, bool invalidate) +void Track::SetLengthInternal(const rational &r) { - // Hold track length until operation stack is empty - midop_track_length_ = r; - - if (GetOperationStack() == 0 && track_length_ != r) { - TimeRange invalidate_range(track_length_, r); - track_length_ = r; - preop_track_length_ = qMax(preop_track_length_, track_length_); - emit TrackLengthChanged(); - - if (invalidate) { - Node::InvalidateCache(invalidate_range, kBlockInput); - } - } + track_length_ = r; + emit TrackLengthChanged(); } void Track::BlockLengthChanged() @@ -680,26 +662,7 @@ void Track::BlockLengthChanged() // Assumes sender is a Block Block* b = static_cast(sender()); - rational old_out = b->out(); - UpdateInOutFrom(blocks_.indexOf(b)); - - rational new_out = b->out(); - - TimeRange invalidate_region(qMin(old_out, new_out), track_length()); - - // The cache won't start while dragging, so we store up our invalidations if it's held down - // and release them once the mouse is no longer pressed - if (qApp->mouseButtons() & Qt::LeftButton) { - block_length_pending_invalidations_.insert(invalidate_region); - } else if (!block_length_pending_invalidations_.isEmpty()) { - foreach (const TimeRange& r, block_length_pending_invalidations_) { - Node::InvalidateCache(r, kBlockInput); - } - block_length_pending_invalidations_.clear(); - } - - Node::InvalidateCache(invalidate_region, kBlockInput); } uint qHash(const Track::Reference &r, uint seed) diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index ee31c3da6..bcd1b8418 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -286,7 +286,7 @@ public: return blocks_; } - virtual void InvalidateCache(const TimeRange& range, const QString& from, int element) override; + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) override; /** * @brief Adds Block `block` at the very beginning of the Sequence before all other clips @@ -345,8 +345,6 @@ public: return waveform_; } - virtual void EndOperation() override; - static const double kTrackHeightDefault; static const double kTrackHeightMinimum; static const double kTrackHeightInterval; @@ -420,7 +418,7 @@ private: int GetCacheIndexFromArrayIndex(int index) const; - void SetLengthInternal(const rational& r, bool invalidate = true); + void SetLengthInternal(const rational& r); TimeRangeList block_length_pending_invalidations_; @@ -431,10 +429,6 @@ private: rational track_length_; - rational midop_track_length_; - - rational preop_track_length_; - double track_height_; int index_; diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 3b0fd9356..910480576 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -222,7 +222,7 @@ void ViewerOutput::ShiftCache(const rational &from, const rational &to) ShiftAudioCache(from, to); } -void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, int element) +void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) { Q_UNUSED(element) @@ -242,7 +242,7 @@ void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, VerifyLength(); - super::InvalidateCache(range, from, element); + super::InvalidateCache(range, from, element, options); } QVector ViewerOutput::inputs_for_output(const QString &output) const @@ -300,14 +300,8 @@ void ViewerOutput::Retranslate() void ViewerOutput::VerifyLength() { video_length_ = VerifyLengthInternal(Track::kVideo); - if (video_cache_enabled_) { - video_frame_cache_.SetLength(video_length_); - } audio_length_ = VerifyLengthInternal(Track::kAudio); - if (audio_cache_enabled_) { - audio_playback_cache_.SetLength(audio_length_); - } rational subtitle_length = VerifyLengthInternal(Track::kSubtitle); diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index 9891829cd..df5fda49d 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -66,7 +66,7 @@ public: void ShiftAudioCache(const rational& from, const rational& to); void ShiftCache(const rational& from, const rational& to); - virtual void InvalidateCache(const TimeRange& range, const QString& from, int element) override; + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) override; virtual QVector inputs_for_output(const QString& output) const override; @@ -154,6 +154,9 @@ public: virtual NodeOutput GetConnectedSampleOutput(); + void SetViewerVideoCacheEnabled(bool e) { video_cache_enabled_ = e; } + void SetViewerAudioCacheEnabled(bool e) { audio_cache_enabled_ = e; } + static const QString kVideoParamsInput; static const QString kAudioParamsInput; @@ -202,9 +205,6 @@ protected: int AddStream(Track::Type type, const QVariant &value); - void SetViewerVideoCacheEnabled(bool e) { video_cache_enabled_ = e; } - void SetViewerAudioCacheEnabled(bool e) { audio_cache_enabled_ = e; } - private: rational last_length_; rational video_length_; diff --git a/app/render/audioplaybackcache.cpp b/app/render/audioplaybackcache.cpp index 50e004ad4..0f9a1183a 100644 --- a/app/render/audioplaybackcache.cpp +++ b/app/render/audioplaybackcache.cpp @@ -166,14 +166,13 @@ void AudioPlaybackCache::WriteSilence(const TimeRange &range) void AudioPlaybackCache::ShiftEvent(const rational &from_in_time, const rational &to_in_time) { - if (from_in_time == to_in_time || from_in_time >= GetLength()) { - // Nothing to be done - return; - } - qint64 to = params_.time_to_bytes_per_channel(to_in_time); qint64 from = params_.time_to_bytes_per_channel(from_in_time); + if (from >= playlist_.GetLength()) { + return; + } + int to_seg_index = playlist_.GetIndexOfPosition(to); int from_seg_index = playlist_.GetIndexOfPosition(from); @@ -265,31 +264,6 @@ void AudioPlaybackCache::ShiftEvent(const rational &from_in_time, const rational } } -void AudioPlaybackCache::LengthChangedEvent(const rational& old, const rational& newlen) -{ - Q_UNUSED(old) - - if (!params_.is_valid()) { - return; - } - - qint64 new_len_in_bytes = params_.time_to_bytes_per_channel(newlen); - - while (new_len_in_bytes < playlist_.GetLength()) { - Segment& last_seg = playlist_.back(); - - if (playlist_.GetLength() - last_seg.size() < new_len_in_bytes) { - // Truncate this segment rather than removing it - qint64 diff = playlist_.GetLength() - new_len_in_bytes; - - TrimSegmentOut(&last_seg, last_seg.size() - diff); - } else { - // Remove last segment - RemoveSegmentFromArray(playlist_.size() - 1); - } - } -} - AudioPlaybackCache::Segment AudioPlaybackCache::CloneSegment(const AudioPlaybackCache::Segment &s) const { Segment new_seg = s; diff --git a/app/render/audioplaybackcache.h b/app/render/audioplaybackcache.h index 9bbe8717f..4e4bc76de 100644 --- a/app/render/audioplaybackcache.h +++ b/app/render/audioplaybackcache.h @@ -202,8 +202,6 @@ signals: protected: virtual void ShiftEvent(const rational& from, const rational& to) override; - virtual void LengthChangedEvent(const rational& old, const rational& newlen) override; - private: static const qint64 kDefaultSegmentSizePerChannel; diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index 4922afcba..6317e4a7a 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -92,7 +92,7 @@ void FrameHashCache::SetTimebase(const rational &tb) void FrameHashCache::ValidateFramesWithHash(const QByteArray &hash) { - const TimeRangeList& invalidated_ranges = GetInvalidatedRanges(); + auto invalidated_ranges = GetInvalidatedRanges(ToTime(GetMapSize())); for (int64_t i=0; i time_hash_map(); - /** * @brief Return the path of the cached image at this time */ @@ -70,8 +68,6 @@ public: void SetHash(const olive::rational &time, const QByteArray& hash, bool frame_exists); protected: - virtual void LengthChangedEvent(const rational& old, const rational& newlen) override; - virtual void ShiftEvent(const rational& from, const rational& to) override; virtual void InvalidateEvent(const TimeRange& range) override; diff --git a/app/render/playbackcache.cpp b/app/render/playbackcache.cpp index 73cb827aa..1f50f736d 100644 --- a/app/render/playbackcache.cpp +++ b/app/render/playbackcache.cpp @@ -27,58 +27,25 @@ namespace olive { -void PlaybackCache::Invalidate(const TimeRange &r) +void PlaybackCache::Invalidate(const TimeRange &r, bool signal) { if (r.in() == r.out()) { qWarning() << "Tried to invalidate zero-length range"; return; } - invalidated_.insert(r); + validated_.remove(r); InvalidateEvent(r); - emit Invalidated(r); + if (signal) { + emit Invalidated(r); + } } void PlaybackCache::InvalidateAll() { - if (length_.isNull()) { - return; - } - - Invalidate(TimeRange(0, length_)); -} - -void PlaybackCache::SetLength(const rational &r) -{ - if (length_ == r) { - // Same length - do nothing - return; - } - - LengthChangedEvent(length_, r); - - TimeRange range_diff(length_, r); - - if (r.isNull()) { - invalidated_.clear(); - } else if (r > length_) { - // If new length is greater, simply extend the invalidated range for now - invalidated_.insert(range_diff); - } else { - // If new length is smaller, removed hashes - invalidated_.remove(range_diff); - } - - rational old_length = length_; - length_ = r; - - if (r > old_length) { - emit Invalidated(range_diff); - } else { - emit Validated(range_diff); - } + Invalidate(TimeRange(0, RATIONAL_MAX)); } void PlaybackCache::Shift(rational from, rational to) @@ -87,54 +54,33 @@ void PlaybackCache::Shift(rational from, rational to) return; } - if (from > length_) { - if (to > from) { - // No-op - return; - } else if (to >= length_) { - // No-op - return; - } else { - from = length_; - } - } - // An region between `from` and `to` will be inserted or spliced out - TimeRangeList ranges_to_shift = invalidated_.Intersects(TimeRange(from, RATIONAL_MAX)); + TimeRangeList ranges_to_shift = validated_.Intersects(TimeRange(from, RATIONAL_MAX)); // Remove everything from the minimum point TimeRange remove_range = TimeRange(qMin(from, to), RATIONAL_MAX); - Validate(remove_range); + Invalidate(remove_range, false); // Shift invalidated ranges // (`diff` is POSITIVE when moving forward -> and NEGATIVE when moving backward <-) rational diff = to - from; foreach (const TimeRange& r, ranges_to_shift) { - Invalidate(r + diff); + Validate(r + diff, false); } ShiftEvent(from, to); - length_ += diff; - - if (diff > 0) { - // If shifting forward, add this section to the invalidated region - Invalidate(TimeRange(from, to)); - } - // Emit signals emit Shifted(from, to); } -void PlaybackCache::Validate(const TimeRange &r) +void PlaybackCache::Validate(const TimeRange &r, bool signal) { - invalidated_.remove(r); + validated_.insert(r); - emit Validated(r); -} - -void PlaybackCache::LengthChangedEvent(const rational &, const rational &) -{ + if (signal) { + emit Validated(r); + } } void PlaybackCache::InvalidateEvent(const TimeRange &) @@ -156,6 +102,29 @@ Project *PlaybackCache::GetProject() const return viewer->project(); } +TimeRangeList PlaybackCache::GetInvalidatedRanges(TimeRange intersecting) +{ + TimeRangeList invalidated; + + // Prevent TimeRange from being below 0, some other behavior in Olive relies on this behavior + // and it seemed reasonable to have safety code in here + intersecting.set_out(qMax(rational(0), intersecting.out())); + intersecting.set_in(qMax(rational(0), intersecting.in())); + + invalidated.insert(intersecting); + + foreach (const TimeRange &range, validated_) { + invalidated.remove(range); + } + + return invalidated; +} + +bool PlaybackCache::HasInvalidatedRanges(const TimeRange &intersecting) +{ + return !validated_.contains(intersecting); +} + QString PlaybackCache::GetCacheDirectory() const { Project* project = GetProject(); @@ -167,4 +136,9 @@ QString PlaybackCache::GetCacheDirectory() const } } +ViewerOutput *PlaybackCache::viewer_parent() const +{ + return dynamic_cast(parent()); +} + } diff --git a/app/render/playbackcache.h b/app/render/playbackcache.h index 5036ccc2e..1f66994d2 100644 --- a/app/render/playbackcache.h +++ b/app/render/playbackcache.h @@ -30,46 +30,38 @@ namespace olive { class Project; +class ViewerOutput; class PlaybackCache : public QObject { Q_OBJECT public: PlaybackCache(QObject* parent = nullptr) : - QObject(parent), - length_(0) + QObject(parent) { } - const rational& GetLength() + TimeRangeList GetInvalidatedRanges(TimeRange intersecting); + TimeRangeList GetInvalidatedRanges(const rational &length) { - return length_; + return GetInvalidatedRanges(TimeRange(0, length)); } - bool IsFullyValidated() + bool HasInvalidatedRanges(const TimeRange &intersecting); + bool HasInvalidatedRanges(const rational &length) { - return invalidated_.isEmpty(); - } - - const TimeRangeList& GetInvalidatedRanges() - { - return invalidated_; - } - - bool HasInvalidatedRanges() - { - return !invalidated_.isEmpty(); + return HasInvalidatedRanges(TimeRange(0, length)); } QString GetCacheDirectory() const; + ViewerOutput *viewer_parent() const; + + void Invalidate(const TimeRange& r, bool signal = true); + public slots: - void Invalidate(const TimeRange& r); - void InvalidateAll(); - void SetLength(const rational& r); - void Shift(rational from, rational to); signals: @@ -79,12 +71,8 @@ signals: void Shifted(const olive::rational& from, const olive::rational& to); - void LengthChanged(const olive::rational& r); - protected: - void Validate(const TimeRange& r); - - virtual void LengthChangedEvent(const rational& old, const rational& newlen); + void Validate(const TimeRange& r, bool signal = true); virtual void InvalidateEvent(const TimeRange& range); @@ -93,9 +81,7 @@ protected: Project* GetProject() const; private: - TimeRangeList invalidated_; - - rational length_; + TimeRangeList validated_; }; diff --git a/app/task/precache/precachetask.cpp b/app/task/precache/precachetask.cpp index 6add67a25..2f207cf9c 100644 --- a/app/task/precache/precachetask.cpp +++ b/app/task/precache/precachetask.cpp @@ -57,13 +57,18 @@ PreCacheTask::~PreCacheTask() bool PreCacheTask::Run() { // Get list of invalidated ranges - TimeRangeList video_range = viewer()->video_frame_cache()->GetInvalidatedRanges(); + TimeRange intersection; - // If we're caching only in-out, limit the range to that if (footage_->GetTimelinePoints()->workarea()->enabled()) { - video_range = video_range.Intersects(footage_->GetTimelinePoints()->workarea()->range()); + // If we're caching only in-out, limit the range to that + intersection = footage_->GetTimelinePoints()->workarea()->range(); + } else { + // Otherwise use full length + intersection = TimeRange(0, footage_->GetVideoLength()); } + TimeRangeList video_range = viewer()->video_frame_cache()->GetInvalidatedRanges(intersection); + Render(project_->color_manager(), video_range, TimeRangeList(), diff --git a/app/widget/timelinewidget/timelineundo.cpp b/app/widget/timelinewidget/timelineundo.cpp index 51625ead2..0f9aa229a 100644 --- a/app/widget/timelinewidget/timelineundo.cpp +++ b/app/widget/timelinewidget/timelineundo.cpp @@ -271,18 +271,16 @@ void TrackReplaceBlockWithGapCommand::redo() } else { // Block is at the end of the track, simply remove it - - // Determine if it's proceeded by a gap, and remove that gap if so Block* preceding = block_->previous(); + track_->RippleRemoveBlock(block_); + + // Determine if it's preceded by a gap, and remove that gap if so if (dynamic_cast(preceding)) { track_->RippleRemoveBlock(preceding); preceding->setParent(&memory_manager_); existing_merged_gap_ = static_cast(preceding); } - - // Remove block in question - track_->RippleRemoveBlock(block_); } } diff --git a/app/widget/timelinewidget/timelineundo.h b/app/widget/timelinewidget/timelineundo.h index da1239527..cb2caedd7 100644 --- a/app/widget/timelinewidget/timelineundo.h +++ b/app/widget/timelinewidget/timelineundo.h @@ -1368,6 +1368,8 @@ public: virtual void redo() override { + TimeRangeList ranges_to_invalidate; + // Determine if we need to add tracks if (track_index_ >= timeline_->GetTracks().size()) { if (add_track_commands_.isEmpty()) { @@ -1400,6 +1402,7 @@ public: } gap_->setParent(track->parent()); track->AppendBlock(gap_); + ranges_to_invalidate.insert(gap_->range()); } track->AppendBlock(insert_); @@ -1429,8 +1432,10 @@ public: track->EndOperation(); - if (ripple_remove_command_) { - track->Node::InvalidateCache(TimeRange(insert_->in(), insert_->out()), Track::kBlockInput); + ranges_to_invalidate.insert(insert_->range()); + + foreach (const TimeRange &r, ranges_to_invalidate) { + track->Node::InvalidateCache(r, Track::kBlockInput); } for (int i=0; i(&TimeRuler::update)); disconnect(playback_cache_, &PlaybackCache::Validated, this, static_cast(&TimeRuler::update)); - disconnect(playback_cache_, &PlaybackCache::LengthChanged, this, static_cast(&TimeRuler::update)); + disconnect(playback_cache_, &PlaybackCache::Shifted, this, static_cast(&TimeRuler::update)); } playback_cache_ = cache; @@ -77,7 +77,7 @@ void TimeRuler::SetPlaybackCache(PlaybackCache *cache) if (playback_cache_) { connect(playback_cache_, &PlaybackCache::Invalidated, this, static_cast(&TimeRuler::update)); connect(playback_cache_, &PlaybackCache::Validated, this, static_cast(&TimeRuler::update)); - connect(playback_cache_, &PlaybackCache::LengthChanged, this, static_cast(&TimeRuler::update)); + connect(playback_cache_, &PlaybackCache::Shifted, this, static_cast(&TimeRuler::update)); } update(); @@ -254,14 +254,17 @@ void TimeRuler::paintEvent(QPaintEvent *) // If cache status is enabled if (show_cache_status_ && playback_cache_) { - int cache_screen_length = qMin(TimeToScreen(playback_cache_->GetLength()), width()); + // FIXME: Hardcoded to get video length, if we ever need audio length, this will have to change + rational len = playback_cache_->viewer_parent()->GetVideoLength(); + + int cache_screen_length = qMin(TimeToScreen(len), width()); if (cache_screen_length > 0) { int cache_y = height() - cache_status_height_; p.fillRect(0, cache_y, cache_screen_length , cache_status_height_, Qt::green); - foreach (const TimeRange& range, playback_cache_->GetInvalidatedRanges()) { + foreach (const TimeRange& range, playback_cache_->GetInvalidatedRanges(len)) { int range_left = TimeToScreen(range.in()); if (range_left >= width()) { continue; From 6ef0ef977de9a05d8b1008c5a5a3e95cbb4a4c9d Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 15 Jul 2021 22:35:49 -0700 Subject: [PATCH 53/72] timeline: update ripple remove area command for new code --- app/widget/timelinewidget/timelineundo.h | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/app/widget/timelinewidget/timelineundo.h b/app/widget/timelinewidget/timelineundo.h index cb2caedd7..928609607 100644 --- a/app/widget/timelinewidget/timelineundo.h +++ b/app/widget/timelinewidget/timelineundo.h @@ -805,8 +805,7 @@ class TrackListRippleRemoveAreaCommand : public UndoCommand { public: TrackListRippleRemoveAreaCommand(TrackList* list, rational in, rational out) : list_(list), - in_(in), - out_(out) + range_(in, out) { } @@ -832,7 +831,7 @@ public: continue; } - TrackRippleRemoveAreaCommand* c = new TrackRippleRemoveAreaCommand(track, TimeRange(in_, out_)); + TrackRippleRemoveAreaCommand* c = new TrackRippleRemoveAreaCommand(track, range_); commands_.append(c); working_tracks_.append(track); } @@ -842,9 +841,9 @@ public: // We can optimize here by simply shifting the whole cache forward instead of re-caching // everything following this time if (list_->type() == Track::kVideo) { - list_->parent()->ShiftVideoCache(out_, in_); + list_->parent()->ShiftVideoCache(range_.out(), range_.in()); } else if (list_->type() == Track::kAudio) { - list_->parent()->ShiftAudioCache(out_, in_); + list_->parent()->ShiftAudioCache(range_.out(), range_.in()); } foreach (Track* track, working_tracks_) { @@ -869,9 +868,9 @@ public: // We can optimize here by simply shifting the whole cache forward instead of re-caching // everything following this time if (list_->type() == Track::kVideo) { - list_->parent()->ShiftVideoCache(in_, out_); + list_->parent()->ShiftVideoCache(range_.in(), range_.out()); } else if (list_->type() == Track::kAudio) { - list_->parent()->ShiftAudioCache(in_, out_); + list_->parent()->ShiftAudioCache(range_.in(), range_.out()); } foreach (Track* track, working_tracks_) { @@ -886,6 +885,7 @@ public: if (all_tracks_unlocked_) { foreach (Track* track, working_tracks_) { track->EndOperation(); + track->Node::InvalidateCache(range_, Track::kBlockInput); } } } @@ -895,9 +895,7 @@ private: QList working_tracks_; - rational in_; - - rational out_; + TimeRange range_; bool all_tracks_unlocked_; From b478c765b67cb044c5a83fb72b190c88b44b4900 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 15 Jul 2021 23:58:22 -0700 Subject: [PATCH 54/72] track: remove length storage Code simplification --- app/node/output/track/track.cpp | 26 ++++++++++---------------- app/node/output/track/track.h | 6 +----- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 4632b1ca4..8b689afea 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -40,7 +40,6 @@ const QString Track::kMutedInput = QStringLiteral("muted_in"); Track::Track() : track_type_(Track::kNone), - track_length_(0), index_(-1), locked_(false) { @@ -278,11 +277,8 @@ void Track::InputDisconnectedEvent(const QString &input, int element, const Node // Update lengths if (next) { UpdateInOutFrom(blocks_.indexOf(next)); - } else if (blocks_.isEmpty()) { - SetLengthInternal(0); - } else { - SetLengthInternal(blocks_.last()->out()); } + emit TrackLengthChanged(); disconnect(b, &Block::LengthChanged, this, &Track::BlockLengthChanged); @@ -522,7 +518,7 @@ void Track::AppendBlock(Block *block) EndOperation(); // Invalidate area that block was added to - Node::InvalidateCache(TimeRange(block->in(), track_length()), kBlockInput); + Node::InvalidateCache(TimeRange(block->in(), block->out()), kBlockInput); } void Track::RippleRemoveBlock(Block *block) @@ -554,13 +550,17 @@ void Track::ReplaceBlock(Block *old, Block *replace) if (old->length() == replace->length()) { Node::InvalidateCache(TimeRange(replace->in(), replace->out()), kBlockInput); } else { - Node::InvalidateCache(TimeRange(replace->in(), RATIONAL_MAX), kBlockInput); + Node::InvalidateCache(TimeRange(replace->in(), track_length()), kBlockInput); } } -const rational &Track::track_length() const +rational Track::track_length() const { - return track_length_; + if (blocks_.isEmpty()) { + return 0; + } else { + return blocks_.last()->out(); + } } QString Track::GetDefaultTrackName(Track::Type type, int index) @@ -633,7 +633,7 @@ void Track::UpdateInOutFrom(int index) emit BlocksRefreshed(); // Update track length - SetLengthInternal(last_out); + emit TrackLengthChanged(); } int Track::GetArrayIndexFromBlock(Block *block) const @@ -651,12 +651,6 @@ int Track::GetCacheIndexFromArrayIndex(int index) const return block_array_indexes_.indexOf(index); } -void Track::SetLengthInternal(const rational &r) -{ - track_length_ = r; - emit TrackLengthChanged(); -} - void Track::BlockLengthChanged() { // Assumes sender is a Block diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index bcd1b8418..b69801384 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -330,7 +330,7 @@ public: */ void ReplaceBlock(Block* old, Block* replace); - const rational& track_length() const; + rational track_length() const; static QString GetDefaultTrackName(Track::Type type, int index); @@ -418,8 +418,6 @@ private: int GetCacheIndexFromArrayIndex(int index) const; - void SetLengthInternal(const rational& r); - TimeRangeList block_length_pending_invalidations_; QVector blocks_; @@ -427,8 +425,6 @@ private: Track::Type track_type_; - rational track_length_; - double track_height_; int index_; From 913f386188168bf4bafecc66a4f0869e80876c25 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 16 Jul 2021 00:07:53 -0700 Subject: [PATCH 55/72] timeline: fixed uninitialized variable in slide command --- app/widget/timelinewidget/timelineundo.h | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/widget/timelinewidget/timelineundo.h b/app/widget/timelinewidget/timelineundo.h index 928609607..2bfeea16a 100644 --- a/app/widget/timelinewidget/timelineundo.h +++ b/app/widget/timelinewidget/timelineundo.h @@ -2002,15 +2002,13 @@ private: we_created_in_adjacent_ = false; } - if (!out_adjacent_) { - if (blocks_.last()->next()) { - out_adjacent_ = new GapBlock(); - out_adjacent_->set_length_and_media_out(-movement_); - out_adjacent_->setParent(&memory_manager_); - we_created_out_adjacent_ = true; - } else { - we_created_out_adjacent_ = false; - } + if (!out_adjacent_ && blocks_.last()->next()) { + out_adjacent_ = new GapBlock(); + out_adjacent_->set_length_and_media_out(-movement_); + out_adjacent_->setParent(&memory_manager_); + we_created_out_adjacent_ = true; + } else { + we_created_out_adjacent_ = false; } } From c41047ab0233c2aaac4f027aa2412b48edd4fd7c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 16 Jul 2021 01:38:20 -0700 Subject: [PATCH 56/72] timeline: reorganized undo commands Split commands between several files rather than having them all in two monolithic ones. --- app/node/project/sequence/sequence.cpp | 1 + app/task/project/loadotio/loadotio.cpp | 3 +- app/undo/undocommand.cpp | 10 + app/undo/undocommand.h | 9 +- app/widget/nodeview/nodeviewundo.cpp | 1 - app/widget/timebased/timebasedwidget.cpp | 2 +- app/widget/timelinewidget/CMakeLists.txt | 3 +- app/widget/timelinewidget/timelineundo.cpp | 373 --- app/widget/timelinewidget/timelineundo.h | 2228 ----------------- app/widget/timelinewidget/timelinewidget.cpp | 4 + app/widget/timelinewidget/tool/add.cpp | 1 + app/widget/timelinewidget/tool/import.cpp | 1 + app/widget/timelinewidget/tool/pointer.cpp | 1 + app/widget/timelinewidget/tool/razor.cpp | 1 + app/widget/timelinewidget/tool/ripple.cpp | 1 + app/widget/timelinewidget/tool/slip.cpp | 1 + app/widget/timelinewidget/tool/tool.h | 1 - app/widget/timelinewidget/tool/transition.cpp | 2 +- app/widget/timelinewidget/undo/CMakeLists.txt | 33 + .../timelinewidget/undo/timelineundocommon.h | 48 + .../undo/timelineundogeneral.cpp | 611 +++++ .../timelinewidget/undo/timelineundogeneral.h | 325 +++ .../undo/timelineundopointer.cpp | 439 ++++ .../timelinewidget/undo/timelineundopointer.h | 204 ++ .../undo/timelineundoripple.cpp | 500 ++++ .../timelinewidget/undo/timelineundoripple.h | 237 ++ .../timelinewidget/undo/timelineundosplit.cpp | 181 ++ .../timelinewidget/undo/timelineundosplit.h | 159 ++ .../timelinewidget/undo/timelineundotrack.cpp | 25 + .../timelinewidget/undo/timelineundotrack.h | 163 ++ .../undo/timelineundoworkarea.cpp | 25 + .../undo/timelineundoworkarea.h | 103 + tests/timeline/timeline-tests.cpp | 3 +- 33 files changed, 3088 insertions(+), 2611 deletions(-) delete mode 100644 app/widget/timelinewidget/timelineundo.cpp delete mode 100644 app/widget/timelinewidget/timelineundo.h create mode 100644 app/widget/timelinewidget/undo/CMakeLists.txt create mode 100644 app/widget/timelinewidget/undo/timelineundocommon.h create mode 100644 app/widget/timelinewidget/undo/timelineundogeneral.cpp create mode 100644 app/widget/timelinewidget/undo/timelineundogeneral.h create mode 100644 app/widget/timelinewidget/undo/timelineundopointer.cpp create mode 100644 app/widget/timelinewidget/undo/timelineundopointer.h create mode 100644 app/widget/timelinewidget/undo/timelineundoripple.cpp create mode 100644 app/widget/timelinewidget/undo/timelineundoripple.h create mode 100644 app/widget/timelinewidget/undo/timelineundosplit.cpp create mode 100644 app/widget/timelinewidget/undo/timelineundosplit.h create mode 100644 app/widget/timelinewidget/undo/timelineundotrack.cpp create mode 100644 app/widget/timelinewidget/undo/timelineundotrack.h create mode 100644 app/widget/timelinewidget/undo/timelineundoworkarea.cpp create mode 100644 app/widget/timelinewidget/undo/timelineundoworkarea.h diff --git a/app/node/project/sequence/sequence.cpp b/app/node/project/sequence/sequence.cpp index 6ee484adc..4d13704b2 100644 --- a/app/node/project/sequence/sequence.cpp +++ b/app/node/project/sequence/sequence.cpp @@ -24,6 +24,7 @@ #include "panel/timeline/timeline.h" #include "ui/icons/icons.h" +#include "widget/timelinewidget/undo/timelineundogeneral.h" namespace olive { diff --git a/app/task/project/loadotio/loadotio.cpp b/app/task/project/loadotio/loadotio.cpp index 0d817104a..dd4428613 100644 --- a/app/task/project/loadotio/loadotio.cpp +++ b/app/task/project/loadotio/loadotio.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include "node/block/clip/clip.h" @@ -36,7 +37,7 @@ #include "node/project/folder/folder.h" #include "node/project/footage/footage.h" #include "node/project/sequence/sequence.h" -#include "widget/timelinewidget/timelineundo.h" +#include "widget/timelinewidget/undo/timelineundogeneral.h" namespace olive { diff --git a/app/undo/undocommand.cpp b/app/undo/undocommand.cpp index 668617b65..4cf60da80 100644 --- a/app/undo/undocommand.cpp +++ b/app/undo/undocommand.cpp @@ -49,8 +49,18 @@ void MultiUndoCommand::undo() } } +UndoCommand::UndoCommand() +{ + prepared_ = false; +} + void UndoCommand::redo_and_set_modified() { + if (!prepared_) { + prepare(); + prepared_ = true; + } + redo(); project_ = GetRelevantProject(); diff --git a/app/undo/undocommand.h b/app/undo/undocommand.h index c310bf131..f7fb98579 100644 --- a/app/undo/undocommand.h +++ b/app/undo/undocommand.h @@ -34,17 +34,20 @@ class Project; class UndoCommand { public: - UndoCommand() = default; + UndoCommand(); virtual ~UndoCommand(){} DISABLE_COPY_MOVE(UndoCommand) + virtual void prepare(){} virtual void redo() = 0; virtual void undo() = 0; - void redo_and_set_modified(); + bool has_prepared() const {return prepared_;} + void set_prepared(bool e) {prepared_ = true;} + void redo_and_set_modified(); void undo_and_set_modified(); virtual Project* GetRelevantProject() const = 0; @@ -66,6 +69,8 @@ private: Project* project_; + bool prepared_; + }; class MultiUndoCommand : public UndoCommand diff --git a/app/widget/nodeview/nodeviewundo.cpp b/app/widget/nodeview/nodeviewundo.cpp index 565a4d535..ca0a37b8a 100644 --- a/app/widget/nodeview/nodeviewundo.cpp +++ b/app/widget/nodeview/nodeviewundo.cpp @@ -21,7 +21,6 @@ #include "nodeviewundo.h" #include "node/project/sequence/sequence.h" -#include "widget/timelinewidget/timelineundo.h" namespace olive { diff --git a/app/widget/timebased/timebasedwidget.cpp b/app/widget/timebased/timebasedwidget.cpp index 6385a89c4..86befc724 100644 --- a/app/widget/timebased/timebasedwidget.cpp +++ b/app/widget/timebased/timebasedwidget.cpp @@ -27,7 +27,7 @@ #include "config/config.h" #include "core.h" #include "node/project/sequence/sequence.h" -#include "widget/timelinewidget/timelineundo.h" +#include "widget/timelinewidget/undo/timelineundoworkarea.h" namespace olive { diff --git a/app/widget/timelinewidget/CMakeLists.txt b/app/widget/timelinewidget/CMakeLists.txt index 04312d89d..4de23d7da 100644 --- a/app/widget/timelinewidget/CMakeLists.txt +++ b/app/widget/timelinewidget/CMakeLists.txt @@ -16,14 +16,13 @@ add_subdirectory(trackview) add_subdirectory(tool) +add_subdirectory(undo) add_subdirectory(view) set(OLIVE_SOURCES ${OLIVE_SOURCES} widget/timelinewidget/timelineandtrackview.cpp widget/timelinewidget/timelineandtrackview.h - widget/timelinewidget/timelineundo.cpp - widget/timelinewidget/timelineundo.h widget/timelinewidget/timelinewidget.cpp widget/timelinewidget/timelinewidget.h widget/timelinewidget/timelinewidgetselections.cpp diff --git a/app/widget/timelinewidget/timelineundo.cpp b/app/widget/timelinewidget/timelineundo.cpp deleted file mode 100644 index 0f9aa229a..000000000 --- a/app/widget/timelinewidget/timelineundo.cpp +++ /dev/null @@ -1,373 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2021 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include "timelineundo.h" - -namespace olive { - -BlockTrimCommand::BlockTrimCommand(Track *track, Block* block, rational new_length, Timeline::MovementMode mode) : - prepped_(false), - track_(track), - block_(block), - new_length_(new_length), - mode_(mode), - deleted_adjacent_command_(nullptr), - trim_is_a_roll_edit_(false) -{ -} - -void BlockTrimCommand::redo() -{ - if (!prepped_) { - prep(); - prepped_ = true; - } - - if (doing_nothing_) { - return; - } - - // Begin an operation since we'll be doing a lot - track_->BeginOperation(); - - // Determine how much time to invalidate - TimeRange invalidate_range; - - if (mode_ == Timeline::kTrimIn) { - invalidate_range = TimeRange(block_->in(), block_->in() + trim_diff_); - block_->set_length_and_media_in(new_length_); - } else { - invalidate_range = TimeRange(block_->out(), block_->out() - trim_diff_); - block_->set_length_and_media_out(new_length_); - } - - if (needs_adjacent_) { - if (we_created_adjacent_) { - // Add adjacent and insert it - adjacent_->setParent(track_->parent()); - - if (mode_ == Timeline::kTrimIn) { - track_->InsertBlockBefore(adjacent_, block_); - } else { - track_->InsertBlockAfter(adjacent_, block_); - } - } else if (we_removed_adjacent_) { - track_->RippleRemoveBlock(adjacent_); - - // It no longer inputs/outputs anything, remove it - if (remove_block_from_graph_ && NodeCanBeRemoved(adjacent_)) { - if (!deleted_adjacent_command_) { - deleted_adjacent_command_ = CreateAndRunRemoveCommand(adjacent_); - } else { - deleted_adjacent_command_->redo(); - } - } - } else { - rational adjacent_length = adjacent_->length() + trim_diff_; - - if (mode_ == Timeline::kTrimIn) { - adjacent_->set_length_and_media_out(adjacent_length); - } else { - adjacent_->set_length_and_media_in(adjacent_length); - } - } - } - - track_->EndOperation(); - - if (dynamic_cast(block_)) { - // Whole transition needs to be invalidated - invalidate_range = block_->range(); - } - - track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); -} - -void BlockTrimCommand::undo() -{ - if (doing_nothing_) { - return; - } - - track_->BeginOperation(); - - // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer - if (needs_adjacent_) { - if (we_created_adjacent_) { - // Adjacent is ours, just delete it - track_->RippleRemoveBlock(adjacent_); - adjacent_->setParent(&memory_manager_); - } else { - if (we_removed_adjacent_) { - if (deleted_adjacent_command_) { - // We deleted adjacent, restore it now - deleted_adjacent_command_->undo(); - } - - if (mode_ == Timeline::kTrimIn) { - track_->InsertBlockBefore(adjacent_, block_); - } else { - track_->InsertBlockAfter(adjacent_, block_); - } - } else { - rational adjacent_length = adjacent_->length() - trim_diff_; - - if (mode_ == Timeline::kTrimIn) { - adjacent_->set_length_and_media_out(adjacent_length); - } else { - adjacent_->set_length_and_media_in(adjacent_length); - } - } - } - } - - TimeRange invalidate_range; - - if (mode_ == Timeline::kTrimIn) { - block_->set_length_and_media_in(old_length_); - - invalidate_range = TimeRange(block_->in(), block_->in() + trim_diff_); - } else { - block_->set_length_and_media_out(old_length_); - - invalidate_range = TimeRange(block_->out(), block_->out() - trim_diff_); - } - - if (dynamic_cast(block_)) { - // Whole transition needs to be invalidated - invalidate_range = block_->range(); - } - - track_->EndOperation(); - - track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); -} - -void BlockTrimCommand::prep() -{ - // Store old length - old_length_ = block_->length(); - - // Determine if the length isn't changing, in which case we set a flag to do nothing - if ((doing_nothing_ = (old_length_ == new_length_))) { - return; - } - - // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer - trim_diff_ = old_length_ - new_length_; - - // Retrieve our adjacent block (or nullptr if none) - if (mode_ == Timeline::kTrimIn) { - adjacent_ = block_->previous(); - } else { - adjacent_ = block_->next(); - } - - // Ignore when trimming the out with no adjacent, because the user must have trimmed the end - // of the last block in the track, so we don't need to do anything elses - needs_adjacent_ = (mode_ == Timeline::kTrimIn || adjacent_); - - if (needs_adjacent_) { - // If we're trimming shorter, we need an adjacent, so check if we have a viable one. - we_created_adjacent_ = (trim_diff_ > 0 && (!adjacent_ || (!dynamic_cast(adjacent_) && !trim_is_a_roll_edit_))); - - if (we_created_adjacent_) { - // We shortened but don't have a viable adjacent to lengthen, so we create one - adjacent_ = new GapBlock(); - adjacent_->set_length_and_media_out(trim_diff_); - } else { - // Determine if we're removing the adjacent - rational adjacent_length = adjacent_->length() + trim_diff_; - we_removed_adjacent_ = adjacent_length.isNull(); - } - } -} - -void TrackReplaceBlockWithGapCommand::redo() -{ - // Determine if this block is connected to any transitions that should also be removed by this operation - if (transition_remove_commands_.isEmpty()) { - CreateRemoveTransitionCommandIfNecessary(false); - CreateRemoveTransitionCommandIfNecessary(true); - } - for (auto it=transition_remove_commands_.cbegin(); it!=transition_remove_commands_.cend(); it++) { - (*it)->redo(); - } - - if (block_->next()) { - track_->BeginOperation(); - - // Invalidate the range inhabited by this block - TimeRange invalidate_range(block_->in(), block_->out()); - - // Block has a next, which means it's NOT at the end of the sequence and thus requires a gap - rational new_gap_length = block_->length(); - - Block* previous = block_->previous(); - Block* next = block_->next(); - - bool previous_is_a_gap = dynamic_cast(previous); - bool next_is_a_gap = dynamic_cast(next); - - if (previous_is_a_gap && next_is_a_gap) { - // Clip is preceded and followed by a gap, so we'll merge the two - existing_gap_ = static_cast(previous); - - existing_merged_gap_ = static_cast(next); - new_gap_length += existing_merged_gap_->length(); - track_->RippleRemoveBlock(existing_merged_gap_); - existing_merged_gap_->setParent(&memory_manager_); - } else if (previous_is_a_gap) { - // Extend this gap to fill space left by block - existing_gap_ = static_cast(previous); - } else if (next_is_a_gap) { - // Extend this gap to fill space left by block - existing_gap_ = static_cast(next); - } - - if (existing_gap_) { - // Extend an existing gap - new_gap_length += existing_gap_->length(); - existing_gap_->set_length_and_media_out(new_gap_length); - track_->RippleRemoveBlock(block_); - - existing_gap_precedes_ = (existing_gap_ == previous); - } else { - // No gap exists to fill this space, create a new one and swap it in - if (!our_gap_) { - our_gap_ = new GapBlock(); - our_gap_->set_length_and_media_out(new_gap_length); - } - - our_gap_->setParent(track_->parent()); - track_->ReplaceBlock(block_, our_gap_); - - if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(our_gap_, track_, track_, our_gap_->index(), track_->Blocks().size(), true); - } - position_command_->redo(); - } - - track_->EndOperation(); - - track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); - - } else { - // Block is at the end of the track, simply remove it - Block* preceding = block_->previous(); - track_->RippleRemoveBlock(block_); - - // Determine if it's preceded by a gap, and remove that gap if so - if (dynamic_cast(preceding)) { - track_->RippleRemoveBlock(preceding); - preceding->setParent(&memory_manager_); - - existing_merged_gap_ = static_cast(preceding); - } - } -} - -void TrackReplaceBlockWithGapCommand::undo() -{ - if (our_gap_ || existing_gap_) { - track_->BeginOperation(); - - if (our_gap_) { - - // We made this gap, simply swap our gap back - track_->ReplaceBlock(our_gap_, block_); - our_gap_->setParent(&memory_manager_); - - position_command_->undo(); - - } else { - - // If we're here, assume that we extended an existing gap - rational original_gap_length = existing_gap_->length() - block_->length(); - - // If we merged two gaps together, restore the second one now - if (existing_merged_gap_) { - original_gap_length -= existing_merged_gap_->length(); - existing_merged_gap_->setParent(track_->parent()); - track_->InsertBlockAfter(existing_merged_gap_, existing_gap_); - existing_merged_gap_ = nullptr; - } - - // Restore original block - if (existing_gap_precedes_) { - track_->InsertBlockAfter(block_, existing_gap_); - } else { - track_->InsertBlockBefore(block_, existing_gap_); - } - - // Restore gap's original length - existing_gap_->set_length_and_media_out(original_gap_length); - - existing_gap_ = nullptr; - - } - - track_->EndOperation(); - - track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), Track::kBlockInput); - } else { - - // Our gap and existing gap were both null, our block must have been at the end and thus - // required no gap extension/replacement - - // However, we may have removed an unnecessary gap that preceded it - if (existing_merged_gap_) { - existing_merged_gap_->setParent(track_->parent()); - track_->AppendBlock(existing_merged_gap_); - existing_merged_gap_ = nullptr; - } - - // Restore block - track_->AppendBlock(block_); - - } - - for (auto it=transition_remove_commands_.crbegin(); it!=transition_remove_commands_.crend(); it++) { - (*it)->undo(); - } -} - -void TrackReplaceBlockWithGapCommand::CreateRemoveTransitionCommandIfNecessary(bool next) -{ - Block* relevant_block; - - if (next) { - relevant_block = block_->next(); - } else { - relevant_block = block_->previous(); - } - - TransitionBlock* transition_cast_test = dynamic_cast(relevant_block); - - if (transition_cast_test) { - if ((next && transition_cast_test->connected_out_block() == block_ && !transition_cast_test->connected_in_block()) - || (!next && transition_cast_test->connected_in_block() == block_ && !transition_cast_test->connected_out_block())) { - TransitionRemoveCommand* command = new TransitionRemoveCommand(transition_cast_test, true); - transition_remove_commands_.append(command); - } - } -} - -} diff --git a/app/widget/timelinewidget/timelineundo.h b/app/widget/timelinewidget/timelineundo.h deleted file mode 100644 index 2bfeea16a..000000000 --- a/app/widget/timelinewidget/timelineundo.h +++ /dev/null @@ -1,2228 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2021 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#ifndef TIMELINEUNDOABLE_H -#define TIMELINEUNDOABLE_H - -#include "config/config.h" -#include "core.h" -#include "node/block/block.h" -#include "node/block/clip/clip.h" -#include "node/block/gap/gap.h" -#include "node/block/transition/transition.h" -#include "node/math/math/math.h" -#include "node/math/merge/merge.h" -#include "node/graph.h" -#include "node/output/track/track.h" -#include "node/output/track/tracklist.h" -#include "timeline/timelinepoints.h" -#include "undo/undocommand.h" -#include "widget/nodeview/nodeviewundo.h" - -namespace olive { - -inline bool NodeCanBeRemoved(Node* n) -{ - return n->output_connections().empty(); -} - -inline UndoCommand* CreateRemoveCommand(Node* n) -{ - return new NodeRemoveWithExclusiveDependenciesAndDisconnect(n); -} - -inline UndoCommand* CreateAndRunRemoveCommand(Node* n) -{ - UndoCommand* command = CreateRemoveCommand(n); - command->redo(); - return command; -} - -class BlockResizeCommand : public UndoCommand { -public: - BlockResizeCommand(Block* block, rational new_length) : - block_(block), - new_length_(new_length) - { - } - - virtual Project* GetRelevantProject() const override - { - return block_->project(); - } - - virtual void redo() override - { - old_length_ = block_->length(); - block_->set_length_and_media_out(new_length_); - } - - virtual void undo() override - { - block_->set_length_and_media_out(old_length_); - } - -private: - Block* block_; - rational old_length_; - rational new_length_; - -}; - -class BlockResizeWithMediaInCommand : public UndoCommand { -public: - BlockResizeWithMediaInCommand(Block* block, rational new_length) : - block_(block), - new_length_(new_length) - { - } - - virtual Project* GetRelevantProject() const override - { - return block_->project(); - } - - virtual void redo() override - { - old_length_ = block_->length(); - block_->set_length_and_media_in(new_length_); - } - - virtual void undo() override - { - block_->set_length_and_media_in(old_length_); - } - -private: - Block* block_; - rational old_length_; - rational new_length_; -}; - -/** - * @brief Performs a trim in the timeline that only affects the block and the block adjacent - * - * Changes the length of one block while also changing the length of the block directly adjacent - * to compensate so that the rest of the track is unaffected. - * - * By default, this will only affect the length of gaps. If the adjacent needs to increase its - * length and is not a gap, a gap will be created and inserted to fill that time. This command can - * be set to always trim even if the adjacent clip isn't a gap with SetTrimIsARollEdit() - */ -class BlockTrimCommand : public UndoCommand { -public: - BlockTrimCommand(Track *track, Block* block, rational new_length, Timeline::MovementMode mode); - - virtual ~BlockTrimCommand() override - { - delete deleted_adjacent_command_; - } - - virtual Project* GetRelevantProject() const override - { - return track_->project(); - } - - /** - * @brief Set this if the trim should always affect the adjacent clip and not create a gap - */ - void SetTrimIsARollEdit(bool e) - { - trim_is_a_roll_edit_ = e; - } - - /** - * @brief Set whether adjacent blocks set to zero length should be removed from the whole graph - * - * If an adjacent block's length is set to 0, it's automatically removed from the track. By - * default it also gets removed from the whole graph. Set this to FALSE to disable that - * functionality. - */ - void SetRemoveZeroLengthFromGraph(bool e) - { - remove_block_from_graph_ = e; - } - - virtual void redo() override; - virtual void undo() override; - -private: - void prep(); - - bool prepped_; - bool doing_nothing_; - rational trim_diff_; - - Track* track_; - Block* block_; - rational old_length_; - rational new_length_; - Timeline::MovementMode mode_; - - Block* adjacent_; - bool needs_adjacent_; - bool we_created_adjacent_; - bool we_removed_adjacent_; - UndoCommand* deleted_adjacent_command_; - - bool trim_is_a_roll_edit_; - bool remove_block_from_graph_; - - QObject memory_manager_; - -}; - -class BlockSetMediaInCommand : public UndoCommand { -public: - BlockSetMediaInCommand(Block* block, rational new_media_in) : - block_(block), - new_media_in_(new_media_in) - { - } - - virtual Project* GetRelevantProject() const override - { - return block_->project(); - } - - virtual void redo() override - { - old_media_in_ = block_->media_in(); - block_->set_media_in(new_media_in_); - } - - virtual void undo() override - { - block_->set_media_in(old_media_in_); - } - -private: - Block* block_; - rational old_media_in_; - rational new_media_in_; -}; - -class TrackRippleRemoveBlockCommand : public UndoCommand { -public: - TrackRippleRemoveBlockCommand(Track* track, Block* block) : - track_(track), - block_(block) - { - } - - virtual Project* GetRelevantProject() const override - { - return track_->project(); - } - - virtual void redo() override - { - before_ = block_->previous(); - track_->RippleRemoveBlock(block_); - } - - virtual void undo() override - { - track_->InsertBlockAfter(block_, before_); - } - -private: - Track* track_; - - Block* block_; - - Block* before_; - -}; - -class TrackPrependBlockCommand : public UndoCommand { -public: - TrackPrependBlockCommand(Track* track, Block* block) : - track_(track), - block_(block) - { - } - - virtual Project* GetRelevantProject() const override - { - return track_->project(); - } - - virtual void redo() override - { - track_->PrependBlock(block_); - } - - virtual void undo() override - { - track_->RippleRemoveBlock(block_); - } - -private: - Track* track_; - Block* block_; -}; - -class TrackInsertBlockAfterCommand : public UndoCommand { -public: - TrackInsertBlockAfterCommand(Track* track, Block* block, Block* before) : - track_(track), - block_(block), - before_(before) - { - } - - virtual Project* GetRelevantProject() const override - { - return block_->project(); - } - - virtual void redo() override - { - track_->InsertBlockAfter(block_, before_); - } - - virtual void undo() override - { - track_->RippleRemoveBlock(block_); - } - -private: - Track* track_; - - Block* block_; - - Block* before_; -}; - -class BlockSplitCommand : public UndoCommand { -public: - BlockSplitCommand(Block* block, rational point) : - block_(block), - new_block_(nullptr), - point_(point), - reconnect_tree_command_(nullptr), - position_command_(nullptr) - { - } - - virtual ~BlockSplitCommand() override - { - delete reconnect_tree_command_; - delete position_command_; - } - - virtual Project* GetRelevantProject() const override - { - return block_->project(); - } - - /** - * @brief Access the second block created as a result. Only valid after redo(). - */ - Block* new_block() - { - return new_block_; - } - - virtual void redo() override - { - old_length_ = block_->length(); - - Q_ASSERT(point_ > block_->in() && point_ < block_->out()); - - if (!reconnect_tree_command_) { - reconnect_tree_command_ = new MultiUndoCommand(); - new_block_ = static_cast(Node::CopyNodeInGraph(block_, reconnect_tree_command_)); - } - - reconnect_tree_command_->redo(); - - // Determine our new lengths - rational new_length = point_ - block_->in(); - rational new_part_length = block_->out() - point_; - - // Begin an operation - Track* track = block_->track(); - track->BeginOperation(); - - // Set lengths - block_->set_length_and_media_out(new_length); - new_block()->set_length_and_media_in(new_part_length); - - // Insert new block - track->InsertBlockAfter(new_block(), block_); - - // Position the block - if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(new_block(), track, nullptr, new_block()->index(), track->Blocks().size(), true); - } - position_command_->redo(); - - // If the block had an out transition, we move it to the new block - moved_transition_ = NodeInput(); - - TransitionBlock* potential_transition = dynamic_cast(new_block()->next()); - if (potential_transition) { - for (const Node::OutputConnection& output : block_->output_connections()) { - if (output.second.node() == potential_transition) { - moved_transition_ = NodeInput(potential_transition, TransitionBlock::kOutBlockInput); - Node::DisconnectEdge(block_, moved_transition_); - Node::ConnectEdge(new_block(), moved_transition_); - break; - } - } - } - - track->EndOperation(); - } - - virtual void undo() override - { - Track* track = block_->track(); - - track->BeginOperation(); - - if (moved_transition_.IsValid()) { - Node::DisconnectEdge(new_block(), moved_transition_); - Node::ConnectEdge(block_, moved_transition_); - } - - position_command_->undo(); - - block_->set_length_and_media_out(old_length_); - track->RippleRemoveBlock(new_block()); - - // If we ran a reconnect command, disconnect now - reconnect_tree_command_->undo(); - - track->EndOperation(); - } - -private: - Block* block_; - Block* new_block_; - - rational old_length_; - rational point_; - - MultiUndoCommand* reconnect_tree_command_; - - NodeInput moved_transition_; - - NodeSetPositionAsChildCommand* position_command_; - -}; - -class BlockSplitPreservingLinksCommand : public UndoCommand { -public: - BlockSplitPreservingLinksCommand(const QVector &blocks, const QList& times) : - blocks_(blocks), - times_(times) - { - } - - virtual ~BlockSplitPreservingLinksCommand() override - { - qDeleteAll(commands_); - } - - virtual Project* GetRelevantProject() const override - { - return blocks_.first()->project(); - } - - virtual void redo() override - { - if (commands_.isEmpty()) { - QVector< QVector > split_blocks(times_.size()); - - for (int i=0;i times_.at(i-1)); - - QVector splits(blocks_.size()); - - for (int j=0;jin() < time && b->out() > time) { - BlockSplitCommand* split_command = new BlockSplitCommand(b, time); - split_command->redo(); - splits.replace(j, split_command->new_block()); - commands_.append(split_command); - } else { - splits.replace(j, nullptr); - } - } - - split_blocks.replace(i, splits); - } - - // Now that we've determined all the splits, we can relink everything - for (int i=0;i& split_list, split_blocks) { - NodeLinkCommand* blc = new NodeLinkCommand(split_list.at(i), split_list.at(j), true); - blc->redo(); - commands_.append(blc); - } - } - } - } - } else { - for (int i=0; iredo(); - } - } - } - - virtual void undo() override - { - for (int i=commands_.size()-1; i>=0; i--) { - commands_.at(i)->undo(); - } - } - -private: - QVector blocks_; - - QList times_; - - QVector commands_; - -}; - -class TrackSplitAtTimeCommand : public UndoCommand { -public: - TrackSplitAtTimeCommand(Track* track, rational point) : - prepped_(false), - track_(track), - point_(point), - command_(nullptr) - { - } - - virtual ~TrackSplitAtTimeCommand() override - { - delete command_; - } - - virtual Project* GetRelevantProject() const override - { - return track_->project(); - } - - virtual void redo() override - { - if (!prepped_) { - // Find Block that contains this time - Block* b = track_->BlockContainingTime(point_); - - if (b) { - command_ = new BlockSplitCommand(b, point_); - } - - prepped_ = true; - } - - if (command_) { - command_->redo(); - } - } - - virtual void undo() override - { - if (command_) { - command_->undo(); - } - } - -private: - bool prepped_; - - Track* track_; - - rational point_; - - UndoCommand* command_; - -}; - -/** - * @brief Clears the area between in and out - * - * The area between `in` and `out` is guaranteed to be freed. BLocks are trimmed and removed to free this space. - * By default, nothing takes this area meaning all subsequent clips are pushed backward, however you can specify - * a block to insert at the `in` point. No checking is done to ensure `insert` is the same length as `in` to `out`. - */ -class TrackRippleRemoveAreaCommand : public UndoCommand { -public: - TrackRippleRemoveAreaCommand(Track* track, const TimeRange& range) : - prepped_(false), - track_(track), - range_(range), - splice_split_command_(nullptr) - { - trim_out_.block = nullptr; - trim_in_.block = nullptr; - } - - virtual ~TrackRippleRemoveAreaCommand() override - { - delete splice_split_command_; - qDeleteAll(remove_block_commands_); - } - - virtual Project* GetRelevantProject() const override - { - return track_->project(); - } - - /** - * @brief Block to insert after if you want to insert something between this ripple - */ - Block* GetInsertionIndex() const - { - return insert_previous_; - } - - Block* GetSplicedBlock() const - { - if (splice_split_command_) { - return splice_split_command_->new_block(); - } - - return nullptr; - } - - virtual void redo() override - { - if (!prepped_) { - prep(); - prepped_ = true; - } - - track_->BeginOperation(); - - if (splice_split_command_) { - // We're just splicing - splice_split_command_->redo(); - - // Trim the in of the split - Block* split = splice_split_command_->new_block(); - split->set_length_and_media_in(split->length() - (range_.out() - split->in())); - } else { - if (trim_out_.block) { - trim_out_.block->set_length_and_media_out(trim_out_.new_length); - } - - if (trim_in_.block) { - trim_in_.block->set_length_and_media_in(trim_in_.new_length); - } - - // Perform removals - if (!removals_.isEmpty()) { - foreach (auto op, removals_) { - // Ripple remove them all first - track_->RippleRemoveBlock(op.block); - } - - // Create undo commands for node removals where possible - if (remove_block_commands_.isEmpty()) { - foreach (auto op, removals_) { - if (NodeCanBeRemoved(op.block)) { - remove_block_commands_.append(CreateRemoveCommand(op.block)); - } - } - } - - foreach (UndoCommand* c, remove_block_commands_) { - c->redo(); - } - } - } - - track_->EndOperation(); - - track_->Node::InvalidateCache(TimeRange(range_.in(), RATIONAL_MAX), Track::kBlockInput); - } - - virtual void undo() override - { - // Begin operations - track_->BeginOperation(); - - if (splice_split_command_) { - splice_split_command_->undo(); - } else { - if (trim_out_.block) { - trim_out_.block->set_length_and_media_out(trim_out_.old_length); - } - - if (trim_in_.block) { - trim_in_.block->set_length_and_media_in(trim_in_.old_length); - } - - // Un-remove any blocks - for (int i=remove_block_commands_.size()-1; i>=0; i--) { - remove_block_commands_.at(i)->undo(); - } - - foreach (auto op, removals_) { - track_->InsertBlockAfter(op.block, op.before); - } - } - - // End operations and invalidate - track_->EndOperation(); - - track_->Node::InvalidateCache(TimeRange(range_.in(), RATIONAL_MAX), Track::kBlockInput); - } - -private: - bool prepped_; - - void prep() - { - // Determine precisely what will be happening to these tracks - Block* first_block = track_->NearestBlockBeforeOrAt(range_.in()); - - if (!first_block) { - // No blocks at this time, nothing to be done on this track - return; - } - - // Determine if this first block is getting trimmed or removed - bool first_block_is_out_trimmed = first_block->in() < range_.in(); - bool first_block_is_in_trimmed = first_block->out() > range_.out(); - - // Set's the block that any insert command should insert AFTER. If the first block is not - // getting out-trimmed, that means first block is either getting removed or in-trimmed, which - // means any insert should happen before it - insert_previous_ = first_block_is_out_trimmed ? first_block : first_block->previous(); - - // If it's getting trimmed, determine if it's actually getting spliced - if (first_block_is_out_trimmed && first_block_is_in_trimmed) { - // This block is getting spliced, so we'll handle that later - splice_split_command_ = new BlockSplitCommand(first_block, range_.in()); - } else { - // It's just getting trimmed or removed, so we'll append that operation - if (first_block_is_out_trimmed) { - trim_out_ = {first_block, - first_block->length(), - first_block->length() - (first_block->out() - range_.in())}; - } else if (first_block_is_in_trimmed) { - // Block is getting in trimmed - trim_in_ = {first_block, - first_block->length(), - first_block->length() - (range_.out() - first_block->in())}; - } else { - // We know for sure this block is within the range so it will be removed - removals_.append(RemoveOperation({first_block, first_block->previous()})); - } - - // If the first block is getting in trimmed, we're already at the end of our range - if (!first_block_is_in_trimmed) { - // Loop through the rest of the blocks and determine what to do with those - for (Block* next=first_block->next(); next; next=next->next()) { - bool trimming = (next->out() > range_.out()); - - if (trimming) { - trim_in_ = {next, - next->length(), - next->length() - (range_.out() - next->in())}; - break; - } else { - removals_.append(RemoveOperation({next, next->previous()})); - - if (next->out() == range_.out()) { - break; - } - } - } - } - } - } - - struct TrimOperation { - Block* block; - rational old_length; - rational new_length; - }; - - struct RemoveOperation { - Block* block; - Block* before; - }; - - Track* track_; - TimeRange range_; - - TrimOperation trim_out_; - QVector removals_; - TrimOperation trim_in_; - Block* insert_previous_; - - BlockSplitCommand* splice_split_command_; - QVector remove_block_commands_; - -}; - -class TrackListRippleRemoveAreaCommand : public UndoCommand { -public: - TrackListRippleRemoveAreaCommand(TrackList* list, rational in, rational out) : - list_(list), - range_(in, out) - { - } - - virtual ~TrackListRippleRemoveAreaCommand() override - { - qDeleteAll(commands_); - } - - virtual Project* GetRelevantProject() const override - { - return list_->parent()->project(); - } - - virtual void redo() override - { - // Code that's only run on the first redo - if (commands_.isEmpty()) { - all_tracks_unlocked_ = true; - - foreach (Track* track, list_->GetTracks()) { - if (track->IsLocked()) { - all_tracks_unlocked_ = false; - continue; - } - - TrackRippleRemoveAreaCommand* c = new TrackRippleRemoveAreaCommand(track, range_); - commands_.append(c); - working_tracks_.append(track); - } - } - - if (all_tracks_unlocked_) { - // We can optimize here by simply shifting the whole cache forward instead of re-caching - // everything following this time - if (list_->type() == Track::kVideo) { - list_->parent()->ShiftVideoCache(range_.out(), range_.in()); - } else if (list_->type() == Track::kAudio) { - list_->parent()->ShiftAudioCache(range_.out(), range_.in()); - } - - foreach (Track* track, working_tracks_) { - track->BeginOperation(); - } - } - - foreach (TrackRippleRemoveAreaCommand* c, commands_) { - c->redo(); - } - - if (all_tracks_unlocked_) { - foreach (Track* track, working_tracks_) { - track->EndOperation(); - } - } - } - - virtual void undo() override - { - if (all_tracks_unlocked_) { - // We can optimize here by simply shifting the whole cache forward instead of re-caching - // everything following this time - if (list_->type() == Track::kVideo) { - list_->parent()->ShiftVideoCache(range_.in(), range_.out()); - } else if (list_->type() == Track::kAudio) { - list_->parent()->ShiftAudioCache(range_.in(), range_.out()); - } - - foreach (Track* track, working_tracks_) { - track->BeginOperation(); - } - } - - foreach (TrackRippleRemoveAreaCommand* c, commands_) { - c->undo(); - } - - if (all_tracks_unlocked_) { - foreach (Track* track, working_tracks_) { - track->EndOperation(); - track->Node::InvalidateCache(range_, Track::kBlockInput); - } - } - } - -private: - TrackList* list_; - - QList working_tracks_; - - TimeRange range_; - - bool all_tracks_unlocked_; - - QVector commands_; - -}; - -class TimelineRippleRemoveAreaCommand : public MultiUndoCommand { -public: - TimelineRippleRemoveAreaCommand(Sequence* timeline, rational in, rational out) : - timeline_(timeline) - { - for (int i=0; itrack_list(static_cast(i)), - in, - out)); - } - } - - virtual Project* GetRelevantProject() const override - { - return timeline_->project(); - } - -private: - Sequence* timeline_; - -}; - -class TrackListRippleToolCommand : public UndoCommand { -public: - struct RippleInfo { - Block* block; - bool append_gap; - }; - - TrackListRippleToolCommand(TrackList* track_list, - const QHash& info, - const rational& ripple_movement, - const Timeline::MovementMode& movement_mode) : - track_list_(track_list), - info_(info), - ripple_movement_(ripple_movement), - movement_mode_(movement_mode) - { - all_tracks_unlocked_ = (info_.size() == track_list_->GetTrackCount()); - } - - virtual Project* GetRelevantProject() const override - { - return track_list_->parent()->project(); - } - - virtual void redo() override - { - ripple(true); - } - - virtual void undo() override - { - ripple(false); - } - -private: - void ripple(bool redo) - { - if (info_.isEmpty()) { - return; - } - - // The following variables are used to determine how much of the cache to invalidate - - // If we can shift, we will shift from the latest out before the ripple to the latest out after, - // since those sections will be unchanged by this ripple - rational pre_latest_out = RATIONAL_MIN; - rational post_latest_out = RATIONAL_MIN; - - // Make timeline changes - for (auto it=info_.cbegin(); it!=info_.cend(); it++) { - Track* track = it.key(); - const RippleInfo& info = it.value(); - WorkingData working_data = working_data_.value(track); - Block* b = info.block; - - // Generate block length - rational new_block_length; - rational operation_movement = ripple_movement_; - - if (movement_mode_ == Timeline::kTrimIn) { - operation_movement = -operation_movement; - } - - if (!redo) { - operation_movement = -operation_movement; - } - - if (b) { - new_block_length = b->length() + operation_movement; - } - - rational pre_shift; - rational post_shift; - - // Begin operation so we can invalidate better later - track->BeginOperation(); - - if (info.append_gap) { - - // Rather than rippling the referenced block, we'll insert a gap and ripple with that - GapBlock* gap = working_data.created_gap; - - if (redo) { - if (!gap) { - gap = new GapBlock(); - gap->set_length_and_media_out(qAbs(ripple_movement_)); - working_data.created_gap = gap; - } - - gap->setParent(track->parent()); - track->InsertBlockBefore(gap, b); - - // As an insertion, we will shift from the gap's in to the gap's out - pre_shift = gap->in(); - post_shift = gap->out(); - working_data.earliest_point_of_change = gap->in(); - } else { - // As a removal, we will shift from the gap's out to the gap's in - pre_shift = gap->out(); - post_shift = gap->in(); - - track->RippleRemoveBlock(gap); - gap->setParent(&memory_manager_); - } - - } else if ((redo && new_block_length.isNull()) || (!redo && !b->track())) { - - // The ripple is the length of this block. We assume that for this to happen, it must have - // been a gap that we will now remove. - - if (redo) { - // The earliest point changes will happen is at the start of this block - working_data.earliest_point_of_change = b->in(); - - // As a removal, we will be shifting from the out point to the in point - pre_shift = b->out(); - post_shift = b->in(); - - // Remove gap from track and from graph - working_data.removed_gap_after = b->previous(); - track->RippleRemoveBlock(b); - b->setParent(&memory_manager_); - } else { - // Restore gap to graph and track - b->setParent(track->parent()); - track->InsertBlockAfter(b, working_data.removed_gap_after); - - // The earliest point changes will happen is at the start of this block - working_data.earliest_point_of_change = b->in(); - - // As an insert, we will be shifting from the block's in point to its out point - pre_shift = b->in(); - post_shift = b->out(); - } - - } else { - - // Store old length - working_data.old_length = b->length(); - - if (movement_mode_ == Timeline::kTrimIn) { - // The earliest point changes will occur is in point of this bloc - working_data.earliest_point_of_change = b->in(); - - // Undo the trim in inversion we do above, this will still be inverted accurately for - // undoing where appropriate - rational inverted = -operation_movement; - if (inverted > 0) { - pre_shift = b->in() + inverted; - post_shift = b->in(); - } else { - pre_shift = b->in(); - post_shift = b->in() - inverted; - } - - // Update length - b->set_length_and_media_in(new_block_length); - } else { - // The earliest point changes will occur is the out point if trimming out or the in point - // if trimming in - working_data.earliest_point_of_change = b->out(); - - // The latest out before the ripple is this block's current out point - pre_shift = b->out(); - - // Update length - b->set_length_and_media_out(new_block_length); - - // The latest out after the ripple is this block's out point after the length change - post_shift = b->out(); - } - - } - - working_data_.insert(it.key(), working_data); - - pre_latest_out = qMax(pre_latest_out, pre_shift); - post_latest_out = qMax(post_latest_out, post_shift); - } - - if (all_tracks_unlocked_) { - // We rippled all the tracks, so we can shift the whole cache - if (track_list_->type() == Track::kVideo) { - track_list_->parent()->ShiftVideoCache(pre_latest_out, post_latest_out); - } else if (track_list_->type() == Track::kAudio) { - track_list_->parent()->ShiftAudioCache(pre_latest_out, post_latest_out); - } - } - - for (auto it=working_data_.cbegin(); it!=working_data_.cend(); it++) { - Track* track = it.key(); - - track->EndOperation(); - - if (!all_tracks_unlocked_) { - // If we're not shifting, the whole track must get invalidated - track->Node::InvalidateCache(TimeRange(it.value().earliest_point_of_change, RATIONAL_MAX), Track::kBlockInput); - } - } - } - - TrackList* track_list_; - - QHash info_; - rational ripple_movement_; - Timeline::MovementMode movement_mode_; - - struct WorkingData { - GapBlock* created_gap = nullptr; - Block* removed_gap_after; - rational old_length; - rational earliest_point_of_change; - }; - - QHash working_data_; - - QObject memory_manager_; - - bool all_tracks_unlocked_; - -}; - -class TimelineAddTrackCommand : public UndoCommand { -public: - TimelineAddTrackCommand(TrackList *timeline) : - TimelineAddTrackCommand(timeline, Config::Current()[QStringLiteral("AutoMergeTracks")].toBool()) - { - } - - TimelineAddTrackCommand(TrackList *timeline, bool automerge_tracks) - { - timeline_ = timeline; - position_command_ = nullptr; - - track_ = new Track(); - track_->setParent(&memory_manager_); - - if (timeline->GetTrackCount() > 0 && automerge_tracks) { - if (timeline_->type() == Track::kVideo) { - merge_ = new MergeNode(); - base_ = NodeInput(merge_, MergeNode::kBaseIn); - blend_ = NodeInput(merge_, MergeNode::kBlendIn); - } else if (timeline_->type() == Track::kAudio) { - merge_ = new MathNode(); - base_ = NodeInput(merge_, MathNode::kParamAIn); - blend_ = NodeInput(merge_, MathNode::kParamBIn); - } else { - merge_ = nullptr; - } - } else { - merge_ = nullptr; - } - - if (merge_) { - merge_->setParent(&memory_manager_); - } - } - - static Track* RunImmediately(TrackList *timeline) - { - TimelineAddTrackCommand c(timeline); - c.redo(); - return c.track(); - } - - static Track* RunImmediately(TrackList *timeline, bool automerge) - { - TimelineAddTrackCommand c(timeline, automerge); - c.redo(); - return c.track(); - } - - virtual ~TimelineAddTrackCommand() override - { - delete position_command_; - } - - Track* track() const - { - return track_; - } - - virtual Project* GetRelevantProject() const override - { - return timeline_->parent()->project(); - } - - virtual void redo() override - { - // Add track - track_->setParent(timeline_->GetParentGraph()); - timeline_->ArrayAppend(); - Node::ConnectEdge(track_, timeline_->track_input(timeline_->ArraySize() - 1)); - - // Add merge if applicable - Track* last_track = nullptr; - if (merge_) { - merge_->setParent(timeline_->GetParentGraph()); - - last_track = timeline_->GetTrackAt(timeline_->GetTrackCount()-2); - - // Whatever this track used to be connected to, connect the merge instead - const Node::OutputConnections edges = last_track->output_connections(); - for (const Node::OutputConnection& ic : edges) { - const NodeInput& i = ic.second; - - // Ignore the track input, but funnel everything else through our merge - if (i.node() != timeline_->parent() || i.input() != timeline_->track_input()) { - Node::DisconnectEdge(last_track, i); - Node::ConnectEdge(merge_, i); - } - } - - // Connect this as the "blend" track - Node::ConnectEdge(track_, blend_); - Node::ConnectEdge(last_track, base_); - } else if (timeline_->GetTrackCount() == 1) { - // If this was the first track we added, - QString relevant_input; - - if (timeline_->type() == Track::kVideo) { - relevant_input = ViewerOutput::kTextureInput; - } else if (timeline_->type() == Track::kAudio) { - relevant_input = ViewerOutput::kSamplesInput; - } - - if (!relevant_input.isEmpty() && !timeline_->parent()->IsInputConnected(relevant_input)) { - direct_ = NodeInput(timeline_->parent(), relevant_input); - - Node::ConnectEdge(track_, direct_); - } else { - direct_ = NodeInput(); - } - } - - // Position track in context - if (!position_command_) { - int track_count = timeline_->parent()->GetTracks().size(); - position_command_ = new MultiUndoCommand(); - - // Position either the merge or the track as an "element" - Node *node_to_position = merge_ ? merge_ : track_; - double node_index = track_count - 1; - if (merge_) { - node_index -= 1; - position_command_->add_child(new NodeRemovePositionFromContextCommand(last_track, timeline_->parent())); - } - - position_command_->add_child(new NodeSetPositionAsChildCommand(node_to_position, timeline_->parent(), timeline_->parent(), node_index, track_count, true)); - - // If we positioned a merge, position the tracks as children of the merge - if (merge_) { - // `last_track` should be non-null if `merge_` is non-null - position_command_->add_child(new NodeSetPositionAsChildCommand(last_track, merge_, timeline_->parent(), 0, 2, true)); - position_command_->add_child(new NodeSetPositionAsChildCommand(track_, merge_, timeline_->parent(), 1, 2, true)); - } - } - position_command_->redo(); - } - - virtual void undo() override - { - position_command_->undo(); - - // Remove merge if applicable - if (merge_) { - // Assume whatever this merge is connected to USED to be connected to the last track - Track* last_track = timeline_->GetTrackAt(timeline_->GetTrackCount()-2); - - Node::DisconnectEdge(track_, blend_); - Node::DisconnectEdge(last_track, base_); - - // Make copy of edges since the node's internal array will change as we disconnect things - const Node::OutputConnections edges = merge_->output_connections(); - for (const Node::OutputConnection& ic : edges) { - const NodeInput& i = ic.second; - - Node::DisconnectEdge(merge_, i); - Node::ConnectEdge(last_track, i); - } - - merge_->setParent(&memory_manager_); - } else if (direct_.IsValid()) { - Node::DisconnectEdge(track_, direct_); - } - - // Remove track - Node::DisconnectEdge(track_, timeline_->track_input(timeline_->ArraySize() - 1)); - timeline_->ArrayRemoveLast(); - track_->setParent(&memory_manager_); - } - -private: - TrackList* timeline_; - - Track* track_; - Node* merge_; - NodeInput base_; - NodeInput blend_; - - NodeInput direct_; - - MultiUndoCommand* position_command_; - - QObject memory_manager_; - -}; - -/** - * @brief Destructively places `block` at the in point `start` - * - * The Block is guaranteed to be placed at the starting point specified. If there are Blocks in this area, they are - * either trimmed or removed to make space for this Block. Additionally, if the Block is placed beyond the end of - * the Sequence, a GapBlock is inserted to compensate. - */ -class TrackPlaceBlockCommand : public UndoCommand { -public: - TrackPlaceBlockCommand(TrackList *timeline, int track, Block* block, rational in) : - timeline_(timeline), - track_index_(track), - in_(in), - gap_(nullptr), - insert_(block), - ripple_remove_command_(nullptr) - { - } - - virtual ~TrackPlaceBlockCommand() override - { - delete ripple_remove_command_; - qDeleteAll(add_track_commands_); - qDeleteAll(position_commands_); - } - - virtual Project* GetRelevantProject() const override - { - return timeline_->parent()->project(); - } - - virtual void redo() override - { - TimeRangeList ranges_to_invalidate; - - // Determine if we need to add tracks - if (track_index_ >= timeline_->GetTracks().size()) { - if (add_track_commands_.isEmpty()) { - // First redo, create tracks now - add_track_commands_.resize(track_index_ - timeline_->GetTracks().size() + 1); - - for (int i=0; iredo(); - } - } - - Track* track = timeline_->GetTrackAt(track_index_); - - track->BeginOperation(); - - bool append = (in_ >= track->track_length()); - - // Check if the placement location is past the end of the timeline - if (append) { - if (in_ > track->track_length()) { - // If so, insert a gap here - if (!gap_) { - gap_ = new GapBlock(); - gap_->set_length_and_media_out(in_ - track->track_length()); - } - gap_->setParent(track->parent()); - track->AppendBlock(gap_); - ranges_to_invalidate.insert(gap_->range()); - } - - track->AppendBlock(insert_); - - if (position_commands_.isEmpty()) { - // Create position commands for insert and gap if necessary - if (gap_) { - position_commands_.append(new NodeSetPositionAsChildCommand(gap_, track, track, gap_->index(), track->Blocks().size(), true)); - } - position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, track, insert_->index(), track->Blocks().size(), true)); - } - } else { - // Place the Block at this point - if (!ripple_remove_command_) { - ripple_remove_command_ = new TrackRippleRemoveAreaCommand(track, - TimeRange(in_, in_ + insert_->length())); - - } - - ripple_remove_command_->redo(); - track->InsertBlockAfter(insert_, ripple_remove_command_->GetInsertionIndex()); - - if (position_commands_.isEmpty()) { - position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, track, insert_->index(), track->Blocks().size(), true)); - } - } - - track->EndOperation(); - - ranges_to_invalidate.insert(insert_->range()); - - foreach (const TimeRange &r, ranges_to_invalidate) { - track->Node::InvalidateCache(r, Track::kBlockInput); - } - - for (int i=0; iredo(); - } - } - - virtual void undo() override - { - for (int i=position_commands_.size()-1; i>=0; i--) { - position_commands_.at(i)->undo(); - } - - Track* t = timeline_->GetTrackAt(track_index_); - - TimeRange insert_range(insert_->in(), insert_->out()); - - // Firstly, remove our insert - t->BeginOperation(); - t->RippleRemoveBlock(insert_); - - if (ripple_remove_command_) { - // If we ripple removed, just undo that - ripple_remove_command_->undo(); - } else if (gap_) { - t->RippleRemoveBlock(gap_); - gap_->setParent(&memory_manager_); - } - t->EndOperation(); - - if (ripple_remove_command_) { - t->Node::InvalidateCache(insert_range, Track::kBlockInput); - } - - // Remove tracks if we added them - for (int i=add_track_commands_.size()-1; i>=0; i--) { - add_track_commands_.at(i)->undo(); - } - } - -private: - TrackList* timeline_; - int track_index_; - rational in_; - GapBlock* gap_; - Block* insert_; - QVector add_track_commands_; - QObject memory_manager_; - TrackRippleRemoveAreaCommand* ripple_remove_command_; - QVector position_commands_; - -}; - -/** - * @brief Replaces Block `old` with Block `replace` - * - * Both blocks must have equal lengths. - */ -class TrackReplaceBlockCommand : public UndoCommand { -public: - TrackReplaceBlockCommand(Track* track, Block* old, Block* replace) : - track_(track), - old_(old), - replace_(replace) - { - } - - virtual Project* GetRelevantProject() const override - { - return track_->project(); - } - - virtual void redo() override - { - track_->ReplaceBlock(old_, replace_); - } - - virtual void undo() override - { - track_->ReplaceBlock(replace_, old_); - } - -private: - Track* track_; - Block* old_; - Block* replace_; - -}; - -class TransitionRemoveCommand : public UndoCommand { -public: - TransitionRemoveCommand(TransitionBlock* block, bool remove_from_graph) : - block_(block), - remove_from_graph_(remove_from_graph), - remove_command_(nullptr) - { - } - - virtual Project* GetRelevantProject() const override - { - return track_->project(); - } - - virtual void redo() override - { - track_ = block_->track(); - out_block_ = block_->connected_out_block(); - in_block_ = block_->connected_in_block(); - - Q_ASSERT(out_block_ || in_block_); - - track_->BeginOperation(); - - TimeRange invalidate_range(block_->in(), block_->out()); - - if (in_block_) { - in_block_->set_length_and_media_in(in_block_->length() + block_->in_offset()); - } - - if (out_block_) { - out_block_->set_length_and_media_out(out_block_->length() + block_->out_offset()); - } - - if (in_block_) { - Node::DisconnectEdge(in_block_, NodeInput(block_, TransitionBlock::kInBlockInput)); - } - - if (out_block_) { - Node::DisconnectEdge(out_block_, NodeInput(block_, TransitionBlock::kOutBlockInput)); - } - - track_->RippleRemoveBlock(block_); - - track_->EndOperation(); - - track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); - - if (remove_from_graph_) { - if (!remove_command_) { - remove_command_ = CreateRemoveCommand(block_); - } - - remove_command_->redo(); - } - } - - virtual void undo() override - { - if (remove_from_graph_) { - remove_command_->undo(); - } - - track_->BeginOperation(); - - if (in_block_) { - track_->InsertBlockBefore(block_, in_block_); - } else { - track_->InsertBlockAfter(block_, out_block_); - } - - if (in_block_) { - Node::ConnectEdge(in_block_, NodeInput(block_, TransitionBlock::kInBlockInput)); - } - - if (out_block_) { - Node::ConnectEdge(out_block_, NodeInput(block_, TransitionBlock::kOutBlockInput)); - } - - // These if statements must be separated because in_offset and out_offset report different things - // if only one block is connected vs two. So we have to connect the blocks first before we have - // an accurate return value from these offset functions. - if (in_block_) { - in_block_->set_length_and_media_in(in_block_->length() - block_->in_offset()); - } - - if (out_block_) { - out_block_->set_length_and_media_out(out_block_->length() - block_->out_offset()); - } - - track_->EndOperation(); - - track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), Track::kBlockInput); - } - -private: - TransitionBlock* block_; - - Track* track_; - - Block* out_block_; - Block* in_block_; - - bool remove_from_graph_; - UndoCommand* remove_command_; - -}; - -class TrackReplaceBlockWithGapCommand : public UndoCommand { -public: - TrackReplaceBlockWithGapCommand(Track* track, Block* block) : - track_(track), - block_(block), - existing_gap_(nullptr), - existing_merged_gap_(nullptr), - our_gap_(nullptr), - position_command_(nullptr) - { - } - - virtual ~TrackReplaceBlockWithGapCommand() override - { - delete position_command_; - } - - virtual Project* GetRelevantProject() const override - { - return block_->project(); - } - - virtual void redo() override; - - virtual void undo() override; - -private: - void CreateRemoveTransitionCommandIfNecessary(bool next); - - Track* track_; - Block* block_; - - GapBlock* existing_gap_; - GapBlock* existing_merged_gap_; - bool existing_gap_precedes_; - GapBlock* our_gap_; - - NodeSetPositionAsChildCommand* position_command_; - - QObject memory_manager_; - - QVector transition_remove_commands_; - -}; - -class TimelineRippleDeleteGapsAtRegionsCommand : public UndoCommand { -public: - TimelineRippleDeleteGapsAtRegionsCommand(Sequence* vo, const TimeRangeList& regions) : - timeline_(vo), - regions_(regions) - { - } - - virtual ~TimelineRippleDeleteGapsAtRegionsCommand() override - { - qDeleteAll(commands_); - } - - virtual Project* GetRelevantProject() const override - { - return timeline_->project(); - } - - virtual void redo() override - { - if (commands_.isEmpty()) { - foreach (const TimeRange& range, regions_) { - rational max_ripple_length = range.length(); - - QVector blocks_around_range; - - foreach (Track* track, timeline_->GetTracks()) { - // Get the block from every other track that is either at or just before our block's in point - Block* block_at_time = track->NearestBlockBeforeOrAt(range.in()); - - if (block_at_time) { - if (dynamic_cast(block_at_time)) { - max_ripple_length = qMin(block_at_time->length(), max_ripple_length); - } else { - max_ripple_length = 0; - break; - } - - blocks_around_range.append(block_at_time); - } - } - - if (max_ripple_length > 0) { - foreach (Block* resize, blocks_around_range) { - if (resize->length() == max_ripple_length) { - // Remove block entirely - commands_.append(new TrackRippleRemoveBlockCommand(resize->track(), resize)); - } else { - // Resize block - commands_.append(new BlockResizeCommand(resize, resize->length() - max_ripple_length)); - } - } - } - } - } - - foreach (UndoCommand* c, commands_) { - c->redo(); - } - } - - virtual void undo() override - { - for (int i=commands_.size()-1;i>=0;i--) { - commands_.at(i)->undo(); - } - } - -private: - Sequence* timeline_; - TimeRangeList regions_; - - QVector commands_; - -}; - -class WorkareaSetEnabledCommand : public UndoCommand { -public: - WorkareaSetEnabledCommand(Project *project, TimelinePoints* points, bool enabled) : - project_(project), - points_(points), - old_enabled_(points_->workarea()->enabled()), - new_enabled_(enabled) - { - } - - virtual Project* GetRelevantProject() const override - { - return project_; - } - - virtual void redo() override - { - points_->workarea()->set_enabled(new_enabled_); - } - - virtual void undo() override - { - points_->workarea()->set_enabled(old_enabled_); - } - -private: - Project* project_; - - TimelinePoints* points_; - - bool old_enabled_; - - bool new_enabled_; - -}; - -class WorkareaSetRangeCommand : public UndoCommand { -public: - WorkareaSetRangeCommand(Project *project, TimelinePoints* points, const TimeRange& range) : - project_(project), - points_(points), - old_range_(points_->workarea()->range()), - new_range_(range) - { - } - - virtual Project* GetRelevantProject() const override - { - return project_; - } - - virtual void redo() override - { - points_->workarea()->set_range(new_range_); - } - - virtual void undo() override - { - points_->workarea()->set_range(old_range_); - } - -private: - Project* project_; - - TimelinePoints* points_; - - TimeRange old_range_; - - TimeRange new_range_; - -}; - -class BlockEnableDisableCommand : public UndoCommand { -public: - BlockEnableDisableCommand(Block* block, bool enabled) : - block_(block), - old_enabled_(block_->is_enabled()), - new_enabled_(enabled) - { - } - - virtual Project* GetRelevantProject() const override - { - return block_->project(); - } - - virtual void redo() override - { - block_->set_enabled(new_enabled_); - } - - virtual void undo() override - { - block_->set_enabled(old_enabled_); - } - -private: - Block* block_; - - bool old_enabled_; - - bool new_enabled_; - -}; - -class TrackSlideCommand : public UndoCommand { -public: - TrackSlideCommand(Track* track, const QList& moving_blocks, Block* in_adjacent, Block* out_adjacent, const rational& movement) : - prepped_(false), - track_(track), - blocks_(moving_blocks), - movement_(movement), - in_adjacent_(in_adjacent), - in_adjacent_remove_command_(nullptr), - out_adjacent_(out_adjacent), - out_adjacent_remove_command_(nullptr) - { - Q_ASSERT(!movement_.isNull()); - } - - virtual ~TrackSlideCommand() override - { - delete in_adjacent_remove_command_; - delete out_adjacent_remove_command_; - } - - virtual Project* GetRelevantProject() const override - { - return track_->project(); - } - - virtual void redo() override - { - if (!prepped_) { - prep(); - prepped_ = true; - } - - // Make sure all movement blocks' old positions are invalidated - TimeRange invalidate_range(blocks_.first()->in(), blocks_.last()->out()); - - track_->BeginOperation(); - - // We will always have an in adjacent if there was a valid slide - if (we_created_in_adjacent_) { - // We created in adjacent, so all we have to do is insert it - in_adjacent_->setParent(track_->parent()); - track_->InsertBlockBefore(in_adjacent_, blocks_.first()); - } else if (-movement_ == in_adjacent_->length()) { - // Movement will remove in adjacent - track_->RippleRemoveBlock(in_adjacent_); - - if (NodeCanBeRemoved(in_adjacent_)) { - if (!in_adjacent_remove_command_) { - in_adjacent_remove_command_ = CreateRemoveCommand(in_adjacent_); - } - - in_adjacent_remove_command_->redo(); - } - } else { - // Simply resize adjacent - in_adjacent_->set_length_and_media_out(in_adjacent_->length() + movement_); - } - - // We may not have an out adjacent if the slide was at the end of the track - if (out_adjacent_) { - if (we_created_out_adjacent_) { - // We created out adjacent, so we just have to insert it - out_adjacent_->setParent(track_->parent()); - track_->InsertBlockAfter(out_adjacent_, blocks_.last()); - } else if (movement_ == out_adjacent_->length()) { - // Movement will remove out adjacent - track_->RippleRemoveBlock(out_adjacent_); - - if (NodeCanBeRemoved(out_adjacent_)) { - if (!out_adjacent_remove_command_) { - out_adjacent_remove_command_ = CreateRemoveCommand(out_adjacent_); - } - - out_adjacent_remove_command_->redo(); - } - } else { - // Simply resize adjacent - out_adjacent_->set_length_and_media_in(out_adjacent_->length() - movement_); - } - } - - track_->EndOperation(); - - // Make sure all movement blocks' new positions are invalidated - invalidate_range.set_range(qMin(invalidate_range.in(), blocks_.first()->in()), - qMax(invalidate_range.out(), blocks_.last()->out())); - - track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); - } - - virtual void undo() override - { - // Make sure all movement blocks' old positions are invalidated - TimeRange invalidate_range(blocks_.first()->in(), blocks_.last()->out()); - - track_->BeginOperation(); - - if (we_created_in_adjacent_) { - // We created this, so we can remove it now - track_->RippleRemoveBlock(in_adjacent_); - in_adjacent_->setParent(&memory_manager_); - } else if (in_adjacent_remove_command_) { - // We removed this, so we can restore it now - in_adjacent_remove_command_->undo(); - } else { - // Simply resize adjacent - in_adjacent_->set_length_and_media_out(in_adjacent_->length() - movement_); - } - - if (out_adjacent_) { - if (we_created_out_adjacent_) { - // We created this, so we can remove it now - track_->RippleRemoveBlock(out_adjacent_); - out_adjacent_->setParent(&memory_manager_); - } else if (out_adjacent_remove_command_) { - out_adjacent_remove_command_->undo(); - } else { - out_adjacent_->set_length_and_media_in(out_adjacent_->length() + movement_); - } - } - - track_->EndOperation(); - - // Make sure all movement blocks' new positions are invalidated - invalidate_range.set_range(qMin(invalidate_range.in(), blocks_.first()->in()), - qMax(invalidate_range.out(), blocks_.last()->out())); - - track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); - } - -private: - bool prepped_; - - void prep() - { - if (!in_adjacent_) { - in_adjacent_ = new GapBlock(); - in_adjacent_->set_length_and_media_out(movement_); - in_adjacent_->setParent(&memory_manager_); - we_created_in_adjacent_ = true; - } else { - we_created_in_adjacent_ = false; - } - - if (!out_adjacent_ && blocks_.last()->next()) { - out_adjacent_ = new GapBlock(); - out_adjacent_->set_length_and_media_out(-movement_); - out_adjacent_->setParent(&memory_manager_); - we_created_out_adjacent_ = true; - } else { - we_created_out_adjacent_ = false; - } - } - - Track* track_; - QList blocks_; - rational movement_; - - bool we_created_in_adjacent_; - Block* in_adjacent_; - UndoCommand* in_adjacent_remove_command_; - bool we_created_out_adjacent_; - Block* out_adjacent_; - UndoCommand* out_adjacent_remove_command_; - - QObject memory_manager_; - -}; - -class TrackListInsertGaps : public UndoCommand { -public: - TrackListInsertGaps(TrackList* track_list, const rational& point, const rational& length) : - prepped_(false), - track_list_(track_list), - point_(point), - length_(length), - split_command_(nullptr) - { - } - - virtual ~TrackListInsertGaps() override - { - delete split_command_; - } - - virtual Project* GetRelevantProject() const override - { - return track_list_->parent()->project(); - } - - virtual void redo() override - { - if (!prepped_) { - prep(); - prepped_ = true; - } - - if (all_tracks_unlocked_) { - // Optimize by shifting over since we have a constant amount of time being inserted - if (track_list_->type() == Track::kVideo) { - track_list_->parent()->ShiftVideoCache(point_, point_ + length_); - } else if (track_list_->type() == Track::kAudio) { - track_list_->parent()->ShiftAudioCache(point_, point_ + length_); - } - } - - foreach (Track* track, working_tracks_) { - track->BeginOperation(); - } - - foreach (Block* gap, gaps_to_extend_) { - gap->set_length_and_media_out(gap->length() + length_); - } - - if (split_command_) { - split_command_->redo(); - } - - foreach (auto add_gap, gaps_added_) { - add_gap.gap->setParent(add_gap.track->parent()); - add_gap.track->InsertBlockAfter(add_gap.gap, add_gap.before); - } - - foreach (Track* track, working_tracks_) { - track->EndOperation(); - } - - if (!all_tracks_unlocked_) { - foreach (Track* track, working_tracks_) { - track->Node::InvalidateCache(TimeRange(point_, RATIONAL_MAX), Track::kBlockInput); - } - } - } - - virtual void undo() override - { - if (all_tracks_unlocked_) { - // Optimize by shifting over since we have a constant amount of time being inserted - if (track_list_->type() == Track::kVideo) { - track_list_->parent()->ShiftVideoCache(point_ + length_, point_); - } else if (track_list_->type() == Track::kAudio) { - track_list_->parent()->ShiftAudioCache(point_ + length_, point_); - } - } - - foreach (Track* track, working_tracks_) { - track->BeginOperation(); - } - - // Remove added gaps - foreach (auto add_gap, gaps_added_) { - add_gap.gap->track()->RippleRemoveBlock(add_gap.gap); - add_gap.gap->setParent(&memory_manager_); - } - - // Un-split blocks - if (split_command_) { - split_command_->undo(); - } - - // Restore original length of gaps - foreach (Block* gap, gaps_to_extend_) { - gap->set_length_and_media_out(gap->length() - length_); - } - - foreach (Track* track, working_tracks_) { - track->EndOperation(); - } - - if (!all_tracks_unlocked_) { - foreach (Track* track, working_tracks_) { - track->Node::InvalidateCache(TimeRange(point_, RATIONAL_MAX), Track::kBlockInput); - } - } - } - -private: - bool prepped_; - - void prep() - { - // Determine if all tracks will be affected, which will allow us to make some optimizations - all_tracks_unlocked_ = true; - - foreach (Track* track, track_list_->GetTracks()) { - if (track->IsLocked()) { - all_tracks_unlocked_ = false; - continue; - } - - working_tracks_.append(track); - } - - QVector blocks_to_split; - QVector blocks_to_append_gap_to; - QVector tracks_to_append_gap_to; - - foreach (Track* track, working_tracks_) { - foreach (Block* b, track->Blocks()) { - if (dynamic_cast(b) && b->in() <= point_ && b->out() >= point_) { - // Found a gap at the location - gaps_to_extend_.append(b); - break; - } else if (dynamic_cast(b) && b->out() >= point_) { - bool append_gap = true; - - if (b->in() == point_) { - // The only reason we should be here is if this block is at the start of the track, - // in which case no split needs to occur - b = nullptr; - } else if (b->out() > point_) { - // Block must be split as well as having a gap appended to it - blocks_to_split.append(b); - } else if (!b->next()) { - // At the end of a track, no gap needs to be added at all - append_gap = false; - } - - if (append_gap) { - tracks_to_append_gap_to.append(track); - blocks_to_append_gap_to.append(b); - } - break; - } - } - } - - if (!blocks_to_split.isEmpty()) { - split_command_ = new BlockSplitPreservingLinksCommand(blocks_to_split, {point_}); - } - - for (int i=0; iset_length_and_media_out(length_); - gap->setParent(&memory_manager_); - gaps_added_.append({gap, blocks_to_append_gap_to.at(i), tracks_to_append_gap_to.at(i)}); - } - } - - TrackList* track_list_; - - rational point_; - - rational length_; - - QVector working_tracks_; - - bool all_tracks_unlocked_; - - QVector gaps_to_extend_; - - struct AddGap { - GapBlock* gap; - Block* before; - Track* track; - }; - - QVector gaps_added_; - - BlockSplitPreservingLinksCommand* split_command_; - - QObject memory_manager_; - -}; - -} - -#endif // TIMELINEUNDOABLE_H diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 63fda18a4..8b973b31c 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -44,6 +44,10 @@ #include "tool/zoom.h" #include "tool/tool.h" #include "trackview/trackview.h" +#include "undo/timelineundogeneral.h" +#include "undo/timelineundopointer.h" +#include "undo/timelineundoripple.h" +#include "undo/timelineundoworkarea.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" #include "widget/nodeview/nodeviewundo.h" diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index fd0011bc1..9a1273bed 100644 --- a/app/widget/timelinewidget/tool/add.cpp +++ b/app/widget/timelinewidget/tool/add.cpp @@ -25,6 +25,7 @@ #include "node/generator/solid/solid.h" #include "node/generator/text/text.h" #include "widget/timelinewidget/timelinewidget.h" +#include "widget/timelinewidget/undo/timelineundopointer.h" namespace olive { diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 137bbdc06..ff9ee2629 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -35,6 +35,7 @@ #include "node/math/math/math.h" #include "node/project/sequence/sequence.h" #include "widget/nodeview/nodeviewundo.h" +#include "widget/timelinewidget/undo/timelineundopointer.h" #include "window/mainwindow/mainwindow.h" #include "window/mainwindow/mainwindowundo.h" diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index 3d2ffee40..dade2c454 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -34,6 +34,7 @@ #include "node/block/transition/transition.h" #include "pointer.h" #include "widget/nodeview/nodeviewundo.h" +#include "widget/timelinewidget/undo/timelineundopointer.h" namespace olive { diff --git a/app/widget/timelinewidget/tool/razor.cpp b/app/widget/timelinewidget/tool/razor.cpp index fadc7b7c0..ae461da2b 100644 --- a/app/widget/timelinewidget/tool/razor.cpp +++ b/app/widget/timelinewidget/tool/razor.cpp @@ -20,6 +20,7 @@ #include "razor.h" #include "widget/timelinewidget/timelinewidget.h" +#include "widget/timelinewidget/undo/timelineundosplit.h" namespace olive { diff --git a/app/widget/timelinewidget/tool/ripple.cpp b/app/widget/timelinewidget/tool/ripple.cpp index ba5e3dce9..3d0861a28 100644 --- a/app/widget/timelinewidget/tool/ripple.cpp +++ b/app/widget/timelinewidget/tool/ripple.cpp @@ -23,6 +23,7 @@ #include "node/block/gap/gap.h" #include "ripple.h" #include "widget/nodeview/nodeviewundo.h" +#include "widget/timelinewidget/undo/timelineundoripple.h" namespace olive { diff --git a/app/widget/timelinewidget/tool/slip.cpp b/app/widget/timelinewidget/tool/slip.cpp index 21d3db970..e7f66881f 100644 --- a/app/widget/timelinewidget/tool/slip.cpp +++ b/app/widget/timelinewidget/tool/slip.cpp @@ -25,6 +25,7 @@ #include "common/timecodefunctions.h" #include "config/config.h" #include "slip.h" +#include "widget/timelinewidget/undo/timelineundogeneral.h" namespace olive { diff --git a/app/widget/timelinewidget/tool/tool.h b/app/widget/timelinewidget/tool/tool.h index b898d1c48..d229e1ad3 100644 --- a/app/widget/timelinewidget/tool/tool.h +++ b/app/widget/timelinewidget/tool/tool.h @@ -25,7 +25,6 @@ #include "common/rational.h" #include "widget/nodeview/nodeviewundo.h" -#include "widget/timelinewidget/timelineundo.h" #include "widget/timelinewidget/view/timelineviewghostitem.h" #include "widget/timelinewidget/view/timelineviewmouseevent.h" diff --git a/app/widget/timelinewidget/tool/transition.cpp b/app/widget/timelinewidget/tool/transition.cpp index b66ba7394..cd9c72717 100644 --- a/app/widget/timelinewidget/tool/transition.cpp +++ b/app/widget/timelinewidget/tool/transition.cpp @@ -25,7 +25,7 @@ #include "node/factory.h" #include "transition.h" #include "widget/nodeview/nodeviewundo.h" -#include "widget/timelinewidget/timelineundo.h" +#include "widget/timelinewidget/undo/timelineundopointer.h" namespace olive { diff --git a/app/widget/timelinewidget/undo/CMakeLists.txt b/app/widget/timelinewidget/undo/CMakeLists.txt new file mode 100644 index 000000000..a3b72c454 --- /dev/null +++ b/app/widget/timelinewidget/undo/CMakeLists.txt @@ -0,0 +1,33 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + widget/timelinewidget/undo/timelineundocommon.h + widget/timelinewidget/undo/timelineundogeneral.cpp + widget/timelinewidget/undo/timelineundogeneral.h + widget/timelinewidget/undo/timelineundopointer.cpp + widget/timelinewidget/undo/timelineundopointer.h + widget/timelinewidget/undo/timelineundoripple.cpp + widget/timelinewidget/undo/timelineundoripple.h + widget/timelinewidget/undo/timelineundosplit.cpp + widget/timelinewidget/undo/timelineundosplit.h + widget/timelinewidget/undo/timelineundotrack.cpp + widget/timelinewidget/undo/timelineundotrack.h + widget/timelinewidget/undo/timelineundoworkarea.cpp + widget/timelinewidget/undo/timelineundoworkarea.h + PARENT_SCOPE +) diff --git a/app/widget/timelinewidget/undo/timelineundocommon.h b/app/widget/timelinewidget/undo/timelineundocommon.h new file mode 100644 index 000000000..d25de0679 --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundocommon.h @@ -0,0 +1,48 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef TIMELINEUNDOCOMMON_H +#define TIMELINEUNDOCOMMON_H + +#include "node/node.h" +#include "widget/nodeview/nodeviewundo.h" + +namespace olive { + +inline bool NodeCanBeRemoved(Node* n) +{ + return n->output_connections().empty(); +} + +inline UndoCommand* CreateRemoveCommand(Node* n) +{ + return new NodeRemoveWithExclusiveDependenciesAndDisconnect(n); +} + +inline UndoCommand* CreateAndRunRemoveCommand(Node* n) +{ + UndoCommand* command = CreateRemoveCommand(n); + command->redo(); + return command; +} + +} + +#endif // TIMELINEUNDOCOMMON_H diff --git a/app/widget/timelinewidget/undo/timelineundogeneral.cpp b/app/widget/timelinewidget/undo/timelineundogeneral.cpp new file mode 100644 index 000000000..6deb5586d --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundogeneral.cpp @@ -0,0 +1,611 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "timelineundogeneral.h" + +#include "node/block/clip/clip.h" +#include "node/block/transition/transition.h" +#include "node/math/math/math.h" +#include "node/math/merge/merge.h" +#include "timelineundocommon.h" + +namespace olive { + +// +// BlockResizeCommand +// +void BlockResizeCommand::redo() +{ + old_length_ = block_->length(); + block_->set_length_and_media_out(new_length_); +} + +void BlockResizeCommand::undo() +{ + block_->set_length_and_media_out(old_length_); +} + +// +// BlockResizeWithMediaInCommand +// +void BlockResizeWithMediaInCommand::redo() +{ + old_length_ = block_->length(); + block_->set_length_and_media_in(new_length_); +} + +void BlockResizeWithMediaInCommand::undo() +{ + block_->set_length_and_media_in(old_length_); +} + +// +// BlockSetMediaInCommand +// +void BlockSetMediaInCommand::redo() +{ + old_media_in_ = block_->media_in(); + block_->set_media_in(new_media_in_); +} + +void BlockSetMediaInCommand::undo() +{ + block_->set_media_in(old_media_in_); +} + +// +// TimelineAddTrackCommand +// +TimelineAddTrackCommand::TimelineAddTrackCommand(TrackList *timeline, bool automerge_tracks) +{ + timeline_ = timeline; + position_command_ = nullptr; + + track_ = new Track(); + track_->setParent(&memory_manager_); + + if (timeline->GetTrackCount() > 0 && automerge_tracks) { + if (timeline_->type() == Track::kVideo) { + merge_ = new MergeNode(); + base_ = NodeInput(merge_, MergeNode::kBaseIn); + blend_ = NodeInput(merge_, MergeNode::kBlendIn); + } else if (timeline_->type() == Track::kAudio) { + merge_ = new MathNode(); + base_ = NodeInput(merge_, MathNode::kParamAIn); + blend_ = NodeInput(merge_, MathNode::kParamBIn); + } else { + merge_ = nullptr; + } + } else { + merge_ = nullptr; + } + + if (merge_) { + merge_->setParent(&memory_manager_); + } +} + +void TimelineAddTrackCommand::redo() +{ + // Add track + track_->setParent(timeline_->GetParentGraph()); + timeline_->ArrayAppend(); + Node::ConnectEdge(track_, timeline_->track_input(timeline_->ArraySize() - 1)); + + // Add merge if applicable + Track* last_track = nullptr; + if (merge_) { + merge_->setParent(timeline_->GetParentGraph()); + + last_track = timeline_->GetTrackAt(timeline_->GetTrackCount()-2); + + // Whatever this track used to be connected to, connect the merge instead + const Node::OutputConnections edges = last_track->output_connections(); + for (const Node::OutputConnection& ic : edges) { + const NodeInput& i = ic.second; + + // Ignore the track input, but funnel everything else through our merge + if (i.node() != timeline_->parent() || i.input() != timeline_->track_input()) { + Node::DisconnectEdge(last_track, i); + Node::ConnectEdge(merge_, i); + } + } + + // Connect this as the "blend" track + Node::ConnectEdge(track_, blend_); + Node::ConnectEdge(last_track, base_); + } else if (timeline_->GetTrackCount() == 1) { + // If this was the first track we added, + QString relevant_input; + + if (timeline_->type() == Track::kVideo) { + relevant_input = ViewerOutput::kTextureInput; + } else if (timeline_->type() == Track::kAudio) { + relevant_input = ViewerOutput::kSamplesInput; + } + + if (!relevant_input.isEmpty() && !timeline_->parent()->IsInputConnected(relevant_input)) { + direct_ = NodeInput(timeline_->parent(), relevant_input); + + Node::ConnectEdge(track_, direct_); + } else { + direct_ = NodeInput(); + } + } + + // Position track in context + if (!position_command_) { + int track_count = timeline_->parent()->GetTracks().size(); + position_command_ = new MultiUndoCommand(); + + // Position either the merge or the track as an "element" + Node *node_to_position = merge_ ? merge_ : track_; + double node_index = track_count - 1; + if (merge_) { + node_index -= 1; + position_command_->add_child(new NodeRemovePositionFromContextCommand(last_track, timeline_->parent())); + } + + position_command_->add_child(new NodeSetPositionAsChildCommand(node_to_position, timeline_->parent(), timeline_->parent(), node_index, track_count, true)); + + // If we positioned a merge, position the tracks as children of the merge + if (merge_) { + // `last_track` should be non-null if `merge_` is non-null + position_command_->add_child(new NodeSetPositionAsChildCommand(last_track, merge_, timeline_->parent(), 0, 2, true)); + position_command_->add_child(new NodeSetPositionAsChildCommand(track_, merge_, timeline_->parent(), 1, 2, true)); + } + } + position_command_->redo(); +} + +void TimelineAddTrackCommand::undo() +{ + position_command_->undo(); + + // Remove merge if applicable + if (merge_) { + // Assume whatever this merge is connected to USED to be connected to the last track + Track* last_track = timeline_->GetTrackAt(timeline_->GetTrackCount()-2); + + Node::DisconnectEdge(track_, blend_); + Node::DisconnectEdge(last_track, base_); + + // Make copy of edges since the node's internal array will change as we disconnect things + const Node::OutputConnections edges = merge_->output_connections(); + for (const Node::OutputConnection& ic : edges) { + const NodeInput& i = ic.second; + + Node::DisconnectEdge(merge_, i); + Node::ConnectEdge(last_track, i); + } + + merge_->setParent(&memory_manager_); + } else if (direct_.IsValid()) { + Node::DisconnectEdge(track_, direct_); + } + + // Remove track + Node::DisconnectEdge(track_, timeline_->track_input(timeline_->ArraySize() - 1)); + timeline_->ArrayRemoveLast(); + track_->setParent(&memory_manager_); +} + +// +// TransitionRemoveCommand +// +void TransitionRemoveCommand::redo() +{ + track_ = block_->track(); + out_block_ = block_->connected_out_block(); + in_block_ = block_->connected_in_block(); + + Q_ASSERT(out_block_ || in_block_); + + track_->BeginOperation(); + + TimeRange invalidate_range(block_->in(), block_->out()); + + if (in_block_) { + in_block_->set_length_and_media_in(in_block_->length() + block_->in_offset()); + } + + if (out_block_) { + out_block_->set_length_and_media_out(out_block_->length() + block_->out_offset()); + } + + if (in_block_) { + Node::DisconnectEdge(in_block_, NodeInput(block_, TransitionBlock::kInBlockInput)); + } + + if (out_block_) { + Node::DisconnectEdge(out_block_, NodeInput(block_, TransitionBlock::kOutBlockInput)); + } + + track_->RippleRemoveBlock(block_); + + track_->EndOperation(); + + track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); + + if (remove_from_graph_) { + if (!remove_command_) { + remove_command_ = CreateRemoveCommand(block_); + } + + remove_command_->redo(); + } +} + +void TransitionRemoveCommand::undo() +{ + if (remove_from_graph_) { + remove_command_->undo(); + } + + track_->BeginOperation(); + + if (in_block_) { + track_->InsertBlockBefore(block_, in_block_); + } else { + track_->InsertBlockAfter(block_, out_block_); + } + + if (in_block_) { + Node::ConnectEdge(in_block_, NodeInput(block_, TransitionBlock::kInBlockInput)); + } + + if (out_block_) { + Node::ConnectEdge(out_block_, NodeInput(block_, TransitionBlock::kOutBlockInput)); + } + + // These if statements must be separated because in_offset and out_offset report different things + // if only one block is connected vs two. So we have to connect the blocks first before we have + // an accurate return value from these offset functions. + if (in_block_) { + in_block_->set_length_and_media_in(in_block_->length() - block_->in_offset()); + } + + if (out_block_) { + out_block_->set_length_and_media_out(out_block_->length() - block_->out_offset()); + } + + track_->EndOperation(); + + track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), Track::kBlockInput); +} + +// +// TrackListInsertGaps +// +void TrackListInsertGaps::prepare() +{ + // Determine if all tracks will be affected, which will allow us to make some optimizations + all_tracks_unlocked_ = true; + + foreach (Track* track, track_list_->GetTracks()) { + if (track->IsLocked()) { + all_tracks_unlocked_ = false; + continue; + } + + working_tracks_.append(track); + } + + QVector blocks_to_split; + QVector blocks_to_append_gap_to; + QVector tracks_to_append_gap_to; + + foreach (Track* track, working_tracks_) { + foreach (Block* b, track->Blocks()) { + if (dynamic_cast(b) && b->in() <= point_ && b->out() >= point_) { + // Found a gap at the location + gaps_to_extend_.append(b); + break; + } else if (dynamic_cast(b) && b->out() >= point_) { + bool append_gap = true; + + if (b->in() == point_) { + // The only reason we should be here is if this block is at the start of the track, + // in which case no split needs to occur + b = nullptr; + } else if (b->out() > point_) { + // Block must be split as well as having a gap appended to it + blocks_to_split.append(b); + } else if (!b->next()) { + // At the end of a track, no gap needs to be added at all + append_gap = false; + } + + if (append_gap) { + tracks_to_append_gap_to.append(track); + blocks_to_append_gap_to.append(b); + } + break; + } + } + } + + if (!blocks_to_split.isEmpty()) { + split_command_ = new BlockSplitPreservingLinksCommand(blocks_to_split, {point_}); + } + + for (int i=0; iset_length_and_media_out(length_); + gap->setParent(&memory_manager_); + gaps_added_.append({gap, blocks_to_append_gap_to.at(i), tracks_to_append_gap_to.at(i)}); + } +} + +void TrackListInsertGaps::redo() +{ + if (all_tracks_unlocked_) { + // Optimize by shifting over since we have a constant amount of time being inserted + if (track_list_->type() == Track::kVideo) { + track_list_->parent()->ShiftVideoCache(point_, point_ + length_); + } else if (track_list_->type() == Track::kAudio) { + track_list_->parent()->ShiftAudioCache(point_, point_ + length_); + } + } + + foreach (Track* track, working_tracks_) { + track->BeginOperation(); + } + + foreach (Block* gap, gaps_to_extend_) { + gap->set_length_and_media_out(gap->length() + length_); + } + + if (split_command_) { + split_command_->redo(); + } + + foreach (auto add_gap, gaps_added_) { + add_gap.gap->setParent(add_gap.track->parent()); + add_gap.track->InsertBlockAfter(add_gap.gap, add_gap.before); + } + + foreach (Track* track, working_tracks_) { + track->EndOperation(); + } + + if (!all_tracks_unlocked_) { + foreach (Track* track, working_tracks_) { + track->Node::InvalidateCache(TimeRange(point_, RATIONAL_MAX), Track::kBlockInput); + } + } +} + +void TrackListInsertGaps::undo() +{ + if (all_tracks_unlocked_) { + // Optimize by shifting over since we have a constant amount of time being inserted + if (track_list_->type() == Track::kVideo) { + track_list_->parent()->ShiftVideoCache(point_ + length_, point_); + } else if (track_list_->type() == Track::kAudio) { + track_list_->parent()->ShiftAudioCache(point_ + length_, point_); + } + } + + foreach (Track* track, working_tracks_) { + track->BeginOperation(); + } + + // Remove added gaps + foreach (auto add_gap, gaps_added_) { + add_gap.gap->track()->RippleRemoveBlock(add_gap.gap); + add_gap.gap->setParent(&memory_manager_); + } + + // Un-split blocks + if (split_command_) { + split_command_->undo(); + } + + // Restore original length of gaps + foreach (Block* gap, gaps_to_extend_) { + gap->set_length_and_media_out(gap->length() - length_); + } + + foreach (Track* track, working_tracks_) { + track->EndOperation(); + } + + if (!all_tracks_unlocked_) { + foreach (Track* track, working_tracks_) { + track->Node::InvalidateCache(TimeRange(point_, RATIONAL_MAX), Track::kBlockInput); + } + } +} + +// +// TrackReplaceBlockWithGapCommand +// +void TrackReplaceBlockWithGapCommand::redo() +{ + // Determine if this block is connected to any transitions that should also be removed by this operation + if (transition_remove_commands_.isEmpty()) { + CreateRemoveTransitionCommandIfNecessary(false); + CreateRemoveTransitionCommandIfNecessary(true); + } + for (auto it=transition_remove_commands_.cbegin(); it!=transition_remove_commands_.cend(); it++) { + (*it)->redo(); + } + + if (block_->next()) { + track_->BeginOperation(); + + // Invalidate the range inhabited by this block + TimeRange invalidate_range(block_->in(), block_->out()); + + // Block has a next, which means it's NOT at the end of the sequence and thus requires a gap + rational new_gap_length = block_->length(); + + Block* previous = block_->previous(); + Block* next = block_->next(); + + bool previous_is_a_gap = dynamic_cast(previous); + bool next_is_a_gap = dynamic_cast(next); + + if (previous_is_a_gap && next_is_a_gap) { + // Clip is preceded and followed by a gap, so we'll merge the two + existing_gap_ = static_cast(previous); + + existing_merged_gap_ = static_cast(next); + new_gap_length += existing_merged_gap_->length(); + track_->RippleRemoveBlock(existing_merged_gap_); + existing_merged_gap_->setParent(&memory_manager_); + } else if (previous_is_a_gap) { + // Extend this gap to fill space left by block + existing_gap_ = static_cast(previous); + } else if (next_is_a_gap) { + // Extend this gap to fill space left by block + existing_gap_ = static_cast(next); + } + + if (existing_gap_) { + // Extend an existing gap + new_gap_length += existing_gap_->length(); + existing_gap_->set_length_and_media_out(new_gap_length); + track_->RippleRemoveBlock(block_); + + existing_gap_precedes_ = (existing_gap_ == previous); + } else { + // No gap exists to fill this space, create a new one and swap it in + if (!our_gap_) { + our_gap_ = new GapBlock(); + our_gap_->set_length_and_media_out(new_gap_length); + } + + our_gap_->setParent(track_->parent()); + track_->ReplaceBlock(block_, our_gap_); + + if (!position_command_) { + position_command_ = new NodeSetPositionAsChildCommand(our_gap_, track_, track_, our_gap_->index(), track_->Blocks().size(), true); + } + position_command_->redo(); + } + + track_->EndOperation(); + + track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); + + } else { + // Block is at the end of the track, simply remove it + Block* preceding = block_->previous(); + track_->RippleRemoveBlock(block_); + + // Determine if it's preceded by a gap, and remove that gap if so + if (dynamic_cast(preceding)) { + track_->RippleRemoveBlock(preceding); + preceding->setParent(&memory_manager_); + + existing_merged_gap_ = static_cast(preceding); + } + } +} + +void TrackReplaceBlockWithGapCommand::undo() +{ + if (our_gap_ || existing_gap_) { + track_->BeginOperation(); + + if (our_gap_) { + + // We made this gap, simply swap our gap back + track_->ReplaceBlock(our_gap_, block_); + our_gap_->setParent(&memory_manager_); + + position_command_->undo(); + + } else { + + // If we're here, assume that we extended an existing gap + rational original_gap_length = existing_gap_->length() - block_->length(); + + // If we merged two gaps together, restore the second one now + if (existing_merged_gap_) { + original_gap_length -= existing_merged_gap_->length(); + existing_merged_gap_->setParent(track_->parent()); + track_->InsertBlockAfter(existing_merged_gap_, existing_gap_); + existing_merged_gap_ = nullptr; + } + + // Restore original block + if (existing_gap_precedes_) { + track_->InsertBlockAfter(block_, existing_gap_); + } else { + track_->InsertBlockBefore(block_, existing_gap_); + } + + // Restore gap's original length + existing_gap_->set_length_and_media_out(original_gap_length); + + existing_gap_ = nullptr; + + } + + track_->EndOperation(); + + track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), Track::kBlockInput); + } else { + + // Our gap and existing gap were both null, our block must have been at the end and thus + // required no gap extension/replacement + + // However, we may have removed an unnecessary gap that preceded it + if (existing_merged_gap_) { + existing_merged_gap_->setParent(track_->parent()); + track_->AppendBlock(existing_merged_gap_); + existing_merged_gap_ = nullptr; + } + + // Restore block + track_->AppendBlock(block_); + + } + + for (auto it=transition_remove_commands_.crbegin(); it!=transition_remove_commands_.crend(); it++) { + (*it)->undo(); + } +} + +void TrackReplaceBlockWithGapCommand::CreateRemoveTransitionCommandIfNecessary(bool next) +{ + Block* relevant_block; + + if (next) { + relevant_block = block_->next(); + } else { + relevant_block = block_->previous(); + } + + TransitionBlock* transition_cast_test = dynamic_cast(relevant_block); + + if (transition_cast_test) { + if ((next && transition_cast_test->connected_out_block() == block_ && !transition_cast_test->connected_in_block()) + || (!next && transition_cast_test->connected_in_block() == block_ && !transition_cast_test->connected_out_block())) { + TransitionRemoveCommand* command = new TransitionRemoveCommand(transition_cast_test, true); + transition_remove_commands_.append(command); + } + } +} + +} diff --git a/app/widget/timelinewidget/undo/timelineundogeneral.h b/app/widget/timelinewidget/undo/timelineundogeneral.h new file mode 100644 index 000000000..f1dd6075f --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundogeneral.h @@ -0,0 +1,325 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef TIMELINEUNDOGENERAL_H +#define TIMELINEUNDOGENERAL_H + +#include "config/config.h" +#include "node/block/gap/gap.h" +#include "node/output/track/track.h" +#include "node/output/track/tracklist.h" +#include "node/output/viewer/viewer.h" +#include "node/project/sequence/sequence.h" +#include "timelineundosplit.h" + +namespace olive { + +class BlockResizeCommand : public UndoCommand { +public: + BlockResizeCommand(Block* block, rational new_length) : + block_(block), + new_length_(new_length) + { + } + + virtual Project* GetRelevantProject() const override + { + return block_->project(); + } + + virtual void redo() override; + virtual void undo() override; + +private: + Block* block_; + rational old_length_; + rational new_length_; + +}; + +class BlockResizeWithMediaInCommand : public UndoCommand { +public: + BlockResizeWithMediaInCommand(Block* block, rational new_length) : + block_(block), + new_length_(new_length) + { + } + + virtual Project* GetRelevantProject() const + { + return block_->project(); + } + + virtual void redo(); + virtual void undo(); + +private: + Block* block_; + rational old_length_; + rational new_length_; + +}; + +class BlockSetMediaInCommand : public UndoCommand { +public: + BlockSetMediaInCommand(Block* block, rational new_media_in) : + block_(block), + new_media_in_(new_media_in) + { + } + + virtual Project* GetRelevantProject() const + { + return block_->project(); + } + + virtual void redo(); + virtual void undo(); + +private: + Block* block_; + rational old_media_in_; + rational new_media_in_; + +}; + +class TimelineAddTrackCommand : public UndoCommand { +public: + TimelineAddTrackCommand(TrackList *timeline) : + TimelineAddTrackCommand(timeline, Config::Current()[QStringLiteral("AutoMergeTracks")].toBool()) + { + } + + TimelineAddTrackCommand(TrackList *timeline, bool automerge_tracks); + + static Track* RunImmediately(TrackList *timeline) + { + TimelineAddTrackCommand c(timeline); + c.redo(); + return c.track(); + } + + static Track* RunImmediately(TrackList *timeline, bool automerge) + { + TimelineAddTrackCommand c(timeline, automerge); + c.redo(); + return c.track(); + } + + virtual ~TimelineAddTrackCommand() override + { + delete position_command_; + } + + Track* track() const + { + return track_; + } + + virtual Project* GetRelevantProject() const override + { + return timeline_->parent()->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + TrackList* timeline_; + + Track* track_; + Node* merge_; + NodeInput base_; + NodeInput blend_; + + NodeInput direct_; + + MultiUndoCommand* position_command_; + + QObject memory_manager_; + +}; + +class TransitionRemoveCommand : public UndoCommand { +public: + TransitionRemoveCommand(TransitionBlock* block, bool remove_from_graph) : + block_(block), + remove_from_graph_(remove_from_graph), + remove_command_(nullptr) + { + } + + virtual Project* GetRelevantProject() const override + { + return track_->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + TransitionBlock* block_; + + Track* track_; + + Block* out_block_; + Block* in_block_; + + bool remove_from_graph_; + UndoCommand* remove_command_; + +}; + +class TrackReplaceBlockWithGapCommand : public UndoCommand { +public: + TrackReplaceBlockWithGapCommand(Track* track, Block* block) : + track_(track), + block_(block), + existing_gap_(nullptr), + existing_merged_gap_(nullptr), + our_gap_(nullptr), + position_command_(nullptr) + { + } + + virtual ~TrackReplaceBlockWithGapCommand() override + { + delete position_command_; + } + + virtual Project* GetRelevantProject() const override + { + return block_->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + void CreateRemoveTransitionCommandIfNecessary(bool next); + + Track* track_; + Block* block_; + + GapBlock* existing_gap_; + GapBlock* existing_merged_gap_; + bool existing_gap_precedes_; + GapBlock* our_gap_; + + NodeSetPositionAsChildCommand* position_command_; + + QObject memory_manager_; + + QVector transition_remove_commands_; + +}; + +class BlockEnableDisableCommand : public UndoCommand { +public: + BlockEnableDisableCommand(Block* block, bool enabled) : + block_(block), + old_enabled_(block_->is_enabled()), + new_enabled_(enabled) + { + } + + virtual Project* GetRelevantProject() const override + { + return block_->project(); + } + + virtual void redo() override + { + block_->set_enabled(new_enabled_); + } + + virtual void undo() override + { + block_->set_enabled(old_enabled_); + } + +private: + Block* block_; + + bool old_enabled_; + + bool new_enabled_; + +}; + +class TrackListInsertGaps : public UndoCommand { +public: + TrackListInsertGaps(TrackList* track_list, const rational& point, const rational& length) : + track_list_(track_list), + point_(point), + length_(length), + split_command_(nullptr) + { + } + + virtual ~TrackListInsertGaps() override + { + delete split_command_; + } + + virtual Project* GetRelevantProject() const override + { + return track_list_->parent()->project(); + } + + virtual void prepare() override; + + virtual void redo() override; + + virtual void undo() override; + +private: + TrackList* track_list_; + + rational point_; + + rational length_; + + QVector working_tracks_; + + bool all_tracks_unlocked_; + + QVector gaps_to_extend_; + + struct AddGap { + GapBlock* gap; + Block* before; + Track* track; + }; + + QVector gaps_added_; + + BlockSplitPreservingLinksCommand* split_command_; + + QObject memory_manager_; + +}; + +} + +#endif // TIMELINEUNDOGENERAL_H diff --git a/app/widget/timelinewidget/undo/timelineundopointer.cpp b/app/widget/timelinewidget/undo/timelineundopointer.cpp new file mode 100644 index 000000000..0bac3b4af --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundopointer.cpp @@ -0,0 +1,439 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "timelineundopointer.h" + +#include "node/block/gap/gap.h" +#include "node/block/transition/transition.h" +#include "node/graph.h" +#include "timelineundocommon.h" + +namespace olive { + +// +// BlockTrimCommand +// +void BlockTrimCommand::redo() +{ + if (doing_nothing_) { + return; + } + + // Begin an operation since we'll be doing a lot + track_->BeginOperation(); + + // Determine how much time to invalidate + TimeRange invalidate_range; + + if (mode_ == Timeline::kTrimIn) { + invalidate_range = TimeRange(block_->in(), block_->in() + trim_diff_); + block_->set_length_and_media_in(new_length_); + } else { + invalidate_range = TimeRange(block_->out(), block_->out() - trim_diff_); + block_->set_length_and_media_out(new_length_); + } + + if (needs_adjacent_) { + if (we_created_adjacent_) { + // Add adjacent and insert it + adjacent_->setParent(track_->parent()); + + if (mode_ == Timeline::kTrimIn) { + track_->InsertBlockBefore(adjacent_, block_); + } else { + track_->InsertBlockAfter(adjacent_, block_); + } + } else if (we_removed_adjacent_) { + track_->RippleRemoveBlock(adjacent_); + + // It no longer inputs/outputs anything, remove it + if (remove_block_from_graph_ && NodeCanBeRemoved(adjacent_)) { + if (!deleted_adjacent_command_) { + deleted_adjacent_command_ = CreateAndRunRemoveCommand(adjacent_); + } else { + deleted_adjacent_command_->redo(); + } + } + } else { + rational adjacent_length = adjacent_->length() + trim_diff_; + + if (mode_ == Timeline::kTrimIn) { + adjacent_->set_length_and_media_out(adjacent_length); + } else { + adjacent_->set_length_and_media_in(adjacent_length); + } + } + } + + track_->EndOperation(); + + if (dynamic_cast(block_)) { + // Whole transition needs to be invalidated + invalidate_range = block_->range(); + } + + track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); +} + +void BlockTrimCommand::undo() +{ + if (doing_nothing_) { + return; + } + + track_->BeginOperation(); + + // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer + if (needs_adjacent_) { + if (we_created_adjacent_) { + // Adjacent is ours, just delete it + track_->RippleRemoveBlock(adjacent_); + adjacent_->setParent(&memory_manager_); + } else { + if (we_removed_adjacent_) { + if (deleted_adjacent_command_) { + // We deleted adjacent, restore it now + deleted_adjacent_command_->undo(); + } + + if (mode_ == Timeline::kTrimIn) { + track_->InsertBlockBefore(adjacent_, block_); + } else { + track_->InsertBlockAfter(adjacent_, block_); + } + } else { + rational adjacent_length = adjacent_->length() - trim_diff_; + + if (mode_ == Timeline::kTrimIn) { + adjacent_->set_length_and_media_out(adjacent_length); + } else { + adjacent_->set_length_and_media_in(adjacent_length); + } + } + } + } + + TimeRange invalidate_range; + + if (mode_ == Timeline::kTrimIn) { + block_->set_length_and_media_in(old_length_); + + invalidate_range = TimeRange(block_->in(), block_->in() + trim_diff_); + } else { + block_->set_length_and_media_out(old_length_); + + invalidate_range = TimeRange(block_->out(), block_->out() - trim_diff_); + } + + if (dynamic_cast(block_)) { + // Whole transition needs to be invalidated + invalidate_range = block_->range(); + } + + track_->EndOperation(); + + track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); +} + +void BlockTrimCommand::prepare() +{ + // Store old length + old_length_ = block_->length(); + + // Determine if the length isn't changing, in which case we set a flag to do nothing + if ((doing_nothing_ = (old_length_ == new_length_))) { + return; + } + + // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer + trim_diff_ = old_length_ - new_length_; + + // Retrieve our adjacent block (or nullptr if none) + if (mode_ == Timeline::kTrimIn) { + adjacent_ = block_->previous(); + } else { + adjacent_ = block_->next(); + } + + // Ignore when trimming the out with no adjacent, because the user must have trimmed the end + // of the last block in the track, so we don't need to do anything elses + needs_adjacent_ = (mode_ == Timeline::kTrimIn || adjacent_); + + if (needs_adjacent_) { + // If we're trimming shorter, we need an adjacent, so check if we have a viable one. + we_created_adjacent_ = (trim_diff_ > 0 && (!adjacent_ || (!dynamic_cast(adjacent_) && !trim_is_a_roll_edit_))); + + if (we_created_adjacent_) { + // We shortened but don't have a viable adjacent to lengthen, so we create one + adjacent_ = new GapBlock(); + adjacent_->set_length_and_media_out(trim_diff_); + } else { + // Determine if we're removing the adjacent + rational adjacent_length = adjacent_->length() + trim_diff_; + we_removed_adjacent_ = adjacent_length.isNull(); + } + } +} + +// +// TrackSlideCommand +// +void TrackSlideCommand::redo() +{ + // Make sure all movement blocks' old positions are invalidated + TimeRange invalidate_range(blocks_.first()->in(), blocks_.last()->out()); + + track_->BeginOperation(); + + // We will always have an in adjacent if there was a valid slide + if (we_created_in_adjacent_) { + // We created in adjacent, so all we have to do is insert it + in_adjacent_->setParent(track_->parent()); + track_->InsertBlockBefore(in_adjacent_, blocks_.first()); + } else if (-movement_ == in_adjacent_->length()) { + // Movement will remove in adjacent + track_->RippleRemoveBlock(in_adjacent_); + + if (NodeCanBeRemoved(in_adjacent_)) { + if (!in_adjacent_remove_command_) { + in_adjacent_remove_command_ = CreateRemoveCommand(in_adjacent_); + } + + in_adjacent_remove_command_->redo(); + } + } else { + // Simply resize adjacent + in_adjacent_->set_length_and_media_out(in_adjacent_->length() + movement_); + } + + // We may not have an out adjacent if the slide was at the end of the track + if (out_adjacent_) { + if (we_created_out_adjacent_) { + // We created out adjacent, so we just have to insert it + out_adjacent_->setParent(track_->parent()); + track_->InsertBlockAfter(out_adjacent_, blocks_.last()); + } else if (movement_ == out_adjacent_->length()) { + // Movement will remove out adjacent + track_->RippleRemoveBlock(out_adjacent_); + + if (NodeCanBeRemoved(out_adjacent_)) { + if (!out_adjacent_remove_command_) { + out_adjacent_remove_command_ = CreateRemoveCommand(out_adjacent_); + } + + out_adjacent_remove_command_->redo(); + } + } else { + // Simply resize adjacent + out_adjacent_->set_length_and_media_in(out_adjacent_->length() - movement_); + } + } + + track_->EndOperation(); + + // Make sure all movement blocks' new positions are invalidated + invalidate_range.set_range(qMin(invalidate_range.in(), blocks_.first()->in()), + qMax(invalidate_range.out(), blocks_.last()->out())); + + track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); +} + + +void TrackSlideCommand::undo() +{ + // Make sure all movement blocks' old positions are invalidated + TimeRange invalidate_range(blocks_.first()->in(), blocks_.last()->out()); + + track_->BeginOperation(); + + if (we_created_in_adjacent_) { + // We created this, so we can remove it now + track_->RippleRemoveBlock(in_adjacent_); + in_adjacent_->setParent(&memory_manager_); + } else if (in_adjacent_remove_command_) { + // We removed this, so we can restore it now + in_adjacent_remove_command_->undo(); + } else { + // Simply resize adjacent + in_adjacent_->set_length_and_media_out(in_adjacent_->length() - movement_); + } + + if (out_adjacent_) { + if (we_created_out_adjacent_) { + // We created this, so we can remove it now + track_->RippleRemoveBlock(out_adjacent_); + out_adjacent_->setParent(&memory_manager_); + } else if (out_adjacent_remove_command_) { + out_adjacent_remove_command_->undo(); + } else { + out_adjacent_->set_length_and_media_in(out_adjacent_->length() + movement_); + } + } + + track_->EndOperation(); + + // Make sure all movement blocks' new positions are invalidated + invalidate_range.set_range(qMin(invalidate_range.in(), blocks_.first()->in()), + qMax(invalidate_range.out(), blocks_.last()->out())); + + track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); +} + +void TrackSlideCommand::prepare() +{ + if (!in_adjacent_) { + in_adjacent_ = new GapBlock(); + in_adjacent_->set_length_and_media_out(movement_); + in_adjacent_->setParent(&memory_manager_); + we_created_in_adjacent_ = true; + } else { + we_created_in_adjacent_ = false; + } + + if (!out_adjacent_ && blocks_.last()->next()) { + out_adjacent_ = new GapBlock(); + out_adjacent_->set_length_and_media_out(-movement_); + out_adjacent_->setParent(&memory_manager_); + we_created_out_adjacent_ = true; + } else { + we_created_out_adjacent_ = false; + } +} + +// +// TrackPlaceBlockCommand +// +TrackPlaceBlockCommand::~TrackPlaceBlockCommand() +{ + delete ripple_remove_command_; + qDeleteAll(add_track_commands_); + qDeleteAll(position_commands_); +} + +void TrackPlaceBlockCommand::redo() +{ + TimeRangeList ranges_to_invalidate; + + // Determine if we need to add tracks + if (track_index_ >= timeline_->GetTracks().size()) { + if (add_track_commands_.isEmpty()) { + // First redo, create tracks now + add_track_commands_.resize(track_index_ - timeline_->GetTracks().size() + 1); + + for (int i=0; iredo(); + } + } + + Track* track = timeline_->GetTrackAt(track_index_); + + track->BeginOperation(); + + bool append = (in_ >= track->track_length()); + + // Check if the placement location is past the end of the timeline + if (append) { + if (in_ > track->track_length()) { + // If so, insert a gap here + if (!gap_) { + gap_ = new GapBlock(); + gap_->set_length_and_media_out(in_ - track->track_length()); + } + gap_->setParent(track->parent()); + track->AppendBlock(gap_); + ranges_to_invalidate.insert(gap_->range()); + } + + track->AppendBlock(insert_); + + if (position_commands_.isEmpty()) { + // Create position commands for insert and gap if necessary + if (gap_) { + position_commands_.append(new NodeSetPositionAsChildCommand(gap_, track, track, gap_->index(), track->Blocks().size(), true)); + } + position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, track, insert_->index(), track->Blocks().size(), true)); + } + } else { + // Place the Block at this point + if (!ripple_remove_command_) { + ripple_remove_command_ = new TrackRippleRemoveAreaCommand(track, TimeRange(in_, in_ + insert_->length())); + + } + + ripple_remove_command_->redo(); + track->InsertBlockAfter(insert_, ripple_remove_command_->GetInsertionIndex()); + + if (position_commands_.isEmpty()) { + position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, track, insert_->index(), track->Blocks().size(), true)); + } + } + + track->EndOperation(); + + ranges_to_invalidate.insert(insert_->range()); + + foreach (const TimeRange &r, ranges_to_invalidate) { + track->Node::InvalidateCache(r, Track::kBlockInput); + } + + for (int i=0; iredo(); + } +} + +void TrackPlaceBlockCommand::undo() +{ + for (int i=position_commands_.size()-1; i>=0; i--) { + position_commands_.at(i)->undo(); + } + + Track* t = timeline_->GetTrackAt(track_index_); + + TimeRange insert_range(insert_->in(), insert_->out()); + + // Firstly, remove our insert + t->BeginOperation(); + t->RippleRemoveBlock(insert_); + + if (ripple_remove_command_) { + // If we ripple removed, just undo that + ripple_remove_command_->undo(); + } else if (gap_) { + t->RippleRemoveBlock(gap_); + gap_->setParent(&memory_manager_); + } + t->EndOperation(); + + if (ripple_remove_command_) { + t->Node::InvalidateCache(insert_range, Track::kBlockInput); + } + + // Remove tracks if we added them + for (int i=add_track_commands_.size()-1; i>=0; i--) { + add_track_commands_.at(i)->undo(); + } +} + +} diff --git a/app/widget/timelinewidget/undo/timelineundopointer.h b/app/widget/timelinewidget/undo/timelineundopointer.h new file mode 100644 index 000000000..d3abf3a6f --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundopointer.h @@ -0,0 +1,204 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef TIMELINEUNDOPOINTER_H +#define TIMELINEUNDOPOINTER_H + +#include "node/block/gap/gap.h" +#include "node/output/track/track.h" +#include "node/output/track/tracklist.h" +#include "node/project/sequence/sequence.h" +#include "timelineundogeneral.h" +#include "timelineundoripple.h" + +namespace olive { + +/** + * @brief Performs a trim in the timeline that only affects the block and the block adjacent + * + * Changes the length of one block while also changing the length of the block directly adjacent + * to compensate so that the rest of the track is unaffected. + * + * By default, this will only affect the length of gaps. If the adjacent needs to increase its + * length and is not a gap, a gap will be created and inserted to fill that time. This command can + * be set to always trim even if the adjacent clip isn't a gap with SetTrimIsARollEdit() + */ +class BlockTrimCommand : public UndoCommand { +public: + BlockTrimCommand(Track *track, Block* block, rational new_length, Timeline::MovementMode mode) : + track_(track), + block_(block), + new_length_(new_length), + mode_(mode), + deleted_adjacent_command_(nullptr), + trim_is_a_roll_edit_(false) + { + } + + virtual ~BlockTrimCommand() override + { + delete deleted_adjacent_command_; + } + + virtual Project* GetRelevantProject() const override + { + return track_->project(); + } + + /** + * @brief Set this if the trim should always affect the adjacent clip and not create a gap + */ + void SetTrimIsARollEdit(bool e) + { + trim_is_a_roll_edit_ = e; + } + + /** + * @brief Set whether adjacent blocks set to zero length should be removed from the whole graph + * + * If an adjacent block's length is set to 0, it's automatically removed from the track. By + * default it also gets removed from the whole graph. Set this to FALSE to disable that + * functionality. + */ + void SetRemoveZeroLengthFromGraph(bool e) + { + remove_block_from_graph_ = e; + } + + virtual void prepare() override; + virtual void redo() override; + virtual void undo() override; + +private: + bool doing_nothing_; + rational trim_diff_; + + Track* track_; + Block* block_; + rational old_length_; + rational new_length_; + Timeline::MovementMode mode_; + + Block* adjacent_; + bool needs_adjacent_; + bool we_created_adjacent_; + bool we_removed_adjacent_; + UndoCommand* deleted_adjacent_command_; + + bool trim_is_a_roll_edit_; + bool remove_block_from_graph_; + + QObject memory_manager_; + +}; + +class TrackSlideCommand : public UndoCommand { +public: + TrackSlideCommand(Track* track, const QList& moving_blocks, Block* in_adjacent, Block* out_adjacent, const rational& movement) : + track_(track), + blocks_(moving_blocks), + movement_(movement), + in_adjacent_(in_adjacent), + in_adjacent_remove_command_(nullptr), + out_adjacent_(out_adjacent), + out_adjacent_remove_command_(nullptr) + { + Q_ASSERT(!movement_.isNull()); + } + + virtual ~TrackSlideCommand() override + { + delete in_adjacent_remove_command_; + delete out_adjacent_remove_command_; + } + + virtual Project* GetRelevantProject() const override + { + return track_->project(); + } + + virtual void prepare() override; + + virtual void redo() override; + + virtual void undo() override; + +private: + Track* track_; + QList blocks_; + rational movement_; + + bool we_created_in_adjacent_; + Block* in_adjacent_; + UndoCommand* in_adjacent_remove_command_; + bool we_created_out_adjacent_; + Block* out_adjacent_; + UndoCommand* out_adjacent_remove_command_; + + QObject memory_manager_; + +}; + +/** + * @brief Destructively places `block` at the in point `start` + * + * The Block is guaranteed to be placed at the starting point specified. If there are Blocks in this area, they are + * either trimmed or removed to make space for this Block. Additionally, if the Block is placed beyond the end of + * the Sequence, a GapBlock is inserted to compensate. + */ +class TrackPlaceBlockCommand : public UndoCommand { +public: + TrackPlaceBlockCommand(TrackList *timeline, int track, Block* block, rational in) : + timeline_(timeline), + track_index_(track), + in_(in), + gap_(nullptr), + insert_(block), + ripple_remove_command_(nullptr) + { + } + + virtual ~TrackPlaceBlockCommand() override; + + virtual Project* GetRelevantProject() const override + { + return timeline_->parent()->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + TrackList* timeline_; + int track_index_; + rational in_; + GapBlock* gap_; + Block* insert_; + QVector add_track_commands_; + QObject memory_manager_; + TrackRippleRemoveAreaCommand* ripple_remove_command_; + QVector position_commands_; + +}; + +} + +#endif // TIMELINEUNDOPOINTER_H diff --git a/app/widget/timelinewidget/undo/timelineundoripple.cpp b/app/widget/timelinewidget/undo/timelineundoripple.cpp new file mode 100644 index 000000000..78b377cae --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundoripple.cpp @@ -0,0 +1,500 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "timelineundoripple.h" + +#include "timelineundocommon.h" + +namespace olive { + +// +// TrackRippleRemoveAreaCommand +// +TrackRippleRemoveAreaCommand::TrackRippleRemoveAreaCommand(Track* track, const TimeRange& range) : + track_(track), + range_(range), + splice_split_command_(nullptr) +{ + trim_out_.block = nullptr; + trim_in_.block = nullptr; +} + +TrackRippleRemoveAreaCommand::~TrackRippleRemoveAreaCommand() +{ + delete splice_split_command_; + qDeleteAll(remove_block_commands_); +} + +void TrackRippleRemoveAreaCommand::prepare() +{ + // Determine precisely what will be happening to these tracks + Block* first_block = track_->NearestBlockBeforeOrAt(range_.in()); + + if (!first_block) { + // No blocks at this time, nothing to be done on this track + return; + } + + // Determine if this first block is getting trimmed or removed + bool first_block_is_out_trimmed = first_block->in() < range_.in(); + bool first_block_is_in_trimmed = first_block->out() > range_.out(); + + // Set's the block that any insert command should insert AFTER. If the first block is not + // getting out-trimmed, that means first block is either getting removed or in-trimmed, which + // means any insert should happen before it + insert_previous_ = first_block_is_out_trimmed ? first_block : first_block->previous(); + + // If it's getting trimmed, determine if it's actually getting spliced + if (first_block_is_out_trimmed && first_block_is_in_trimmed) { + // This block is getting spliced, so we'll handle that later + splice_split_command_ = new BlockSplitCommand(first_block, range_.in()); + } else { + // It's just getting trimmed or removed, so we'll append that operation + if (first_block_is_out_trimmed) { + trim_out_ = {first_block, + first_block->length(), + first_block->length() - (first_block->out() - range_.in())}; + } else if (first_block_is_in_trimmed) { + // Block is getting in trimmed + trim_in_ = {first_block, + first_block->length(), + first_block->length() - (range_.out() - first_block->in())}; + } else { + // We know for sure this block is within the range so it will be removed + removals_.append(RemoveOperation({first_block, first_block->previous()})); + } + + // If the first block is getting in trimmed, we're already at the end of our range + if (!first_block_is_in_trimmed) { + // Loop through the rest of the blocks and determine what to do with those + for (Block* next=first_block->next(); next; next=next->next()) { + bool trimming = (next->out() > range_.out()); + + if (trimming) { + trim_in_ = {next, + next->length(), + next->length() - (range_.out() - next->in())}; + break; + } else { + removals_.append(RemoveOperation({next, next->previous()})); + + if (next->out() == range_.out()) { + break; + } + } + } + } + } +} + +void TrackRippleRemoveAreaCommand::redo() +{ + track_->BeginOperation(); + + if (splice_split_command_) { + // We're just splicing + splice_split_command_->redo(); + + // Trim the in of the split + Block* split = splice_split_command_->new_block(); + split->set_length_and_media_in(split->length() - (range_.out() - split->in())); + } else { + if (trim_out_.block) { + trim_out_.block->set_length_and_media_out(trim_out_.new_length); + } + + if (trim_in_.block) { + trim_in_.block->set_length_and_media_in(trim_in_.new_length); + } + + // Perform removals + if (!removals_.isEmpty()) { + foreach (auto op, removals_) { + // Ripple remove them all first + track_->RippleRemoveBlock(op.block); + } + + // Create undo commands for node removals where possible + if (remove_block_commands_.isEmpty()) { + foreach (auto op, removals_) { + if (NodeCanBeRemoved(op.block)) { + remove_block_commands_.append(CreateRemoveCommand(op.block)); + } + } + } + + foreach (UndoCommand* c, remove_block_commands_) { + c->redo(); + } + } + } + + track_->EndOperation(); + + track_->Node::InvalidateCache(TimeRange(range_.in(), RATIONAL_MAX), Track::kBlockInput); +} + +void TrackRippleRemoveAreaCommand::undo() +{ + // Begin operations + track_->BeginOperation(); + + if (splice_split_command_) { + splice_split_command_->undo(); + } else { + if (trim_out_.block) { + trim_out_.block->set_length_and_media_out(trim_out_.old_length); + } + + if (trim_in_.block) { + trim_in_.block->set_length_and_media_in(trim_in_.old_length); + } + + // Un-remove any blocks + for (int i=remove_block_commands_.size()-1; i>=0; i--) { + remove_block_commands_.at(i)->undo(); + } + + foreach (auto op, removals_) { + track_->InsertBlockAfter(op.block, op.before); + } + } + + // End operations and invalidate + track_->EndOperation(); + + track_->Node::InvalidateCache(TimeRange(range_.in(), RATIONAL_MAX), Track::kBlockInput); +} + +// +// TrackListRippleRemoveAreaCommand +// +void TrackListRippleRemoveAreaCommand::redo() +{ + // Code that's only run on the first redo + if (commands_.isEmpty()) { + all_tracks_unlocked_ = true; + + foreach (Track* track, list_->GetTracks()) { + if (track->IsLocked()) { + all_tracks_unlocked_ = false; + continue; + } + + TrackRippleRemoveAreaCommand* c = new TrackRippleRemoveAreaCommand(track, range_); + commands_.append(c); + working_tracks_.append(track); + } + } + + if (all_tracks_unlocked_) { + // We can optimize here by simply shifting the whole cache forward instead of re-caching + // everything following this time + if (list_->type() == Track::kVideo) { + list_->parent()->ShiftVideoCache(range_.out(), range_.in()); + } else if (list_->type() == Track::kAudio) { + list_->parent()->ShiftAudioCache(range_.out(), range_.in()); + } + + foreach (Track* track, working_tracks_) { + track->BeginOperation(); + } + } + + foreach (TrackRippleRemoveAreaCommand* c, commands_) { + c->redo(); + } + + if (all_tracks_unlocked_) { + foreach (Track* track, working_tracks_) { + track->EndOperation(); + } + } +} + +void TrackListRippleRemoveAreaCommand::undo() +{ + if (all_tracks_unlocked_) { + // We can optimize here by simply shifting the whole cache forward instead of re-caching + // everything following this time + if (list_->type() == Track::kVideo) { + list_->parent()->ShiftVideoCache(range_.in(), range_.out()); + } else if (list_->type() == Track::kAudio) { + list_->parent()->ShiftAudioCache(range_.in(), range_.out()); + } + + foreach (Track* track, working_tracks_) { + track->BeginOperation(); + } + } + + foreach (TrackRippleRemoveAreaCommand* c, commands_) { + c->undo(); + } + + if (all_tracks_unlocked_) { + foreach (Track* track, working_tracks_) { + track->EndOperation(); + track->Node::InvalidateCache(range_, Track::kBlockInput); + } + } +} + +// +// TimelineRippleRemoveAreaCommand +// +TimelineRippleRemoveAreaCommand::TimelineRippleRemoveAreaCommand(Sequence* timeline, rational in, rational out) : + timeline_(timeline) +{ + for (int i=0; itrack_list(static_cast(i)), + in, + out)); + } +} + +// +// TrackListRippleToolCommand +// +TrackListRippleToolCommand::TrackListRippleToolCommand(TrackList* track_list, + const QHash& info, + const rational& ripple_movement, + const Timeline::MovementMode& movement_mode) : + track_list_(track_list), + info_(info), + ripple_movement_(ripple_movement), + movement_mode_(movement_mode) +{ + all_tracks_unlocked_ = (info_.size() == track_list_->GetTrackCount()); +} + +void TrackListRippleToolCommand::ripple(bool redo) +{ + if (info_.isEmpty()) { + return; + } + + // The following variables are used to determine how much of the cache to invalidate + + // If we can shift, we will shift from the latest out before the ripple to the latest out after, + // since those sections will be unchanged by this ripple + rational pre_latest_out = RATIONAL_MIN; + rational post_latest_out = RATIONAL_MIN; + + // Make timeline changes + for (auto it=info_.cbegin(); it!=info_.cend(); it++) { + Track* track = it.key(); + const RippleInfo& info = it.value(); + WorkingData working_data = working_data_.value(track); + Block* b = info.block; + + // Generate block length + rational new_block_length; + rational operation_movement = ripple_movement_; + + if (movement_mode_ == Timeline::kTrimIn) { + operation_movement = -operation_movement; + } + + if (!redo) { + operation_movement = -operation_movement; + } + + if (b) { + new_block_length = b->length() + operation_movement; + } + + rational pre_shift; + rational post_shift; + + // Begin operation so we can invalidate better later + track->BeginOperation(); + + if (info.append_gap) { + + // Rather than rippling the referenced block, we'll insert a gap and ripple with that + GapBlock* gap = working_data.created_gap; + + if (redo) { + if (!gap) { + gap = new GapBlock(); + gap->set_length_and_media_out(qAbs(ripple_movement_)); + working_data.created_gap = gap; + } + + gap->setParent(track->parent()); + track->InsertBlockBefore(gap, b); + + // As an insertion, we will shift from the gap's in to the gap's out + pre_shift = gap->in(); + post_shift = gap->out(); + working_data.earliest_point_of_change = gap->in(); + } else { + // As a removal, we will shift from the gap's out to the gap's in + pre_shift = gap->out(); + post_shift = gap->in(); + + track->RippleRemoveBlock(gap); + gap->setParent(&memory_manager_); + } + + } else if ((redo && new_block_length.isNull()) || (!redo && !b->track())) { + + // The ripple is the length of this block. We assume that for this to happen, it must have + // been a gap that we will now remove. + + if (redo) { + // The earliest point changes will happen is at the start of this block + working_data.earliest_point_of_change = b->in(); + + // As a removal, we will be shifting from the out point to the in point + pre_shift = b->out(); + post_shift = b->in(); + + // Remove gap from track and from graph + working_data.removed_gap_after = b->previous(); + track->RippleRemoveBlock(b); + b->setParent(&memory_manager_); + } else { + // Restore gap to graph and track + b->setParent(track->parent()); + track->InsertBlockAfter(b, working_data.removed_gap_after); + + // The earliest point changes will happen is at the start of this block + working_data.earliest_point_of_change = b->in(); + + // As an insert, we will be shifting from the block's in point to its out point + pre_shift = b->in(); + post_shift = b->out(); + } + + } else { + + // Store old length + working_data.old_length = b->length(); + + if (movement_mode_ == Timeline::kTrimIn) { + // The earliest point changes will occur is in point of this bloc + working_data.earliest_point_of_change = b->in(); + + // Undo the trim in inversion we do above, this will still be inverted accurately for + // undoing where appropriate + rational inverted = -operation_movement; + if (inverted > 0) { + pre_shift = b->in() + inverted; + post_shift = b->in(); + } else { + pre_shift = b->in(); + post_shift = b->in() - inverted; + } + + // Update length + b->set_length_and_media_in(new_block_length); + } else { + // The earliest point changes will occur is the out point if trimming out or the in point + // if trimming in + working_data.earliest_point_of_change = b->out(); + + // The latest out before the ripple is this block's current out point + pre_shift = b->out(); + + // Update length + b->set_length_and_media_out(new_block_length); + + // The latest out after the ripple is this block's out point after the length change + post_shift = b->out(); + } + + } + + working_data_.insert(it.key(), working_data); + + pre_latest_out = qMax(pre_latest_out, pre_shift); + post_latest_out = qMax(post_latest_out, post_shift); + } + + if (all_tracks_unlocked_) { + // We rippled all the tracks, so we can shift the whole cache + if (track_list_->type() == Track::kVideo) { + track_list_->parent()->ShiftVideoCache(pre_latest_out, post_latest_out); + } else if (track_list_->type() == Track::kAudio) { + track_list_->parent()->ShiftAudioCache(pre_latest_out, post_latest_out); + } + } + + for (auto it=working_data_.cbegin(); it!=working_data_.cend(); it++) { + Track* track = it.key(); + + track->EndOperation(); + + if (!all_tracks_unlocked_) { + // If we're not shifting, the whole track must get invalidated + track->Node::InvalidateCache(TimeRange(it.value().earliest_point_of_change, RATIONAL_MAX), Track::kBlockInput); + } + } +} + +// +// TimelineRippleDeleteGapsAtRegionsCommand +// +void TimelineRippleDeleteGapsAtRegionsCommand::redo() +{ + if (commands_.isEmpty()) { + foreach (const TimeRange& range, regions_) { + rational max_ripple_length = range.length(); + + QVector blocks_around_range; + + foreach (Track* track, timeline_->GetTracks()) { + // Get the block from every other track that is either at or just before our block's in point + Block* block_at_time = track->NearestBlockBeforeOrAt(range.in()); + + if (block_at_time) { + if (dynamic_cast(block_at_time)) { + max_ripple_length = qMin(block_at_time->length(), max_ripple_length); + } else { + max_ripple_length = 0; + break; + } + + blocks_around_range.append(block_at_time); + } + } + + if (max_ripple_length > 0) { + foreach (Block* resize, blocks_around_range) { + if (resize->length() == max_ripple_length) { + // Remove block entirely + commands_.append(new TrackRippleRemoveBlockCommand(resize->track(), resize)); + } else { + // Resize block + commands_.append(new BlockResizeCommand(resize, resize->length() - max_ripple_length)); + } + } + } + } + } + + foreach (UndoCommand* c, commands_) { + c->redo(); + } +} + +} diff --git a/app/widget/timelinewidget/undo/timelineundoripple.h b/app/widget/timelinewidget/undo/timelineundoripple.h new file mode 100644 index 000000000..c98cdc03b --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundoripple.h @@ -0,0 +1,237 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef TIMELINEUNDORIPPLE_H +#define TIMELINEUNDORIPPLE_H + +#include "node/block/gap/gap.h" +#include "node/output/track/track.h" +#include "node/output/track/tracklist.h" +#include "node/project/sequence/sequence.h" +#include "timelineundogeneral.h" +#include "timelineundosplit.h" +#include "timelineundotrack.h" + +namespace olive { + +/** + * @brief Clears the area between in and out + * + * The area between `in` and `out` is guaranteed to be freed. BLocks are trimmed and removed to free this space. + * By default, nothing takes this area meaning all subsequent clips are pushed backward, however you can specify + * a block to insert at the `in` point. No checking is done to ensure `insert` is the same length as `in` to `out`. + */ +class TrackRippleRemoveAreaCommand : public UndoCommand { +public: + TrackRippleRemoveAreaCommand(Track* track, const TimeRange& range); + + virtual ~TrackRippleRemoveAreaCommand() override; + + virtual Project* GetRelevantProject() const override + { + return track_->project(); + } + + /** + * @brief Block to insert after if you want to insert something between this ripple + */ + Block* GetInsertionIndex() const + { + return insert_previous_; + } + + Block* GetSplicedBlock() const + { + if (splice_split_command_) { + return splice_split_command_->new_block(); + } + + return nullptr; + } + + virtual void prepare() override; + + virtual void redo() override; + + virtual void undo() override; + +private: + struct TrimOperation { + Block* block; + rational old_length; + rational new_length; + }; + + struct RemoveOperation { + Block* block; + Block* before; + }; + + Track* track_; + TimeRange range_; + + TrimOperation trim_out_; + QVector removals_; + TrimOperation trim_in_; + Block* insert_previous_; + + BlockSplitCommand* splice_split_command_; + QVector remove_block_commands_; + +}; + +class TrackListRippleRemoveAreaCommand : public UndoCommand { +public: + TrackListRippleRemoveAreaCommand(TrackList* list, rational in, rational out) : + list_(list), + range_(in, out) + { + } + + virtual ~TrackListRippleRemoveAreaCommand() override + { + qDeleteAll(commands_); + } + + virtual Project* GetRelevantProject() const override + { + return list_->parent()->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + TrackList* list_; + + QList working_tracks_; + + TimeRange range_; + + bool all_tracks_unlocked_; + + QVector commands_; + +}; + +class TimelineRippleRemoveAreaCommand : public MultiUndoCommand { +public: + TimelineRippleRemoveAreaCommand(Sequence* timeline, rational in, rational out); + + virtual Project* GetRelevantProject() const override + { + return timeline_->project(); + } + +private: + Sequence* timeline_; + +}; + +class TrackListRippleToolCommand : public UndoCommand { +public: + struct RippleInfo { + Block* block; + bool append_gap; + }; + + TrackListRippleToolCommand(TrackList* track_list, + const QHash& info, + const rational& ripple_movement, + const Timeline::MovementMode& movement_mode); + + virtual Project* GetRelevantProject() const override + { + return track_list_->parent()->project(); + } + + virtual void redo() override + { + ripple(true); + } + + virtual void undo() override + { + ripple(false); + } + +private: + void ripple(bool redo); + + TrackList* track_list_; + + QHash info_; + rational ripple_movement_; + Timeline::MovementMode movement_mode_; + + struct WorkingData { + GapBlock* created_gap = nullptr; + Block* removed_gap_after; + rational old_length; + rational earliest_point_of_change; + }; + + QHash working_data_; + + QObject memory_manager_; + + bool all_tracks_unlocked_; + +}; + +class TimelineRippleDeleteGapsAtRegionsCommand : public UndoCommand { +public: + TimelineRippleDeleteGapsAtRegionsCommand(Sequence* vo, const TimeRangeList& regions) : + timeline_(vo), + regions_(regions) + { + } + + virtual ~TimelineRippleDeleteGapsAtRegionsCommand() override + { + qDeleteAll(commands_); + } + + virtual Project* GetRelevantProject() const override + { + return timeline_->project(); + } + + virtual void redo() override; + + virtual void undo() override + { + for (int i=commands_.size()-1;i>=0;i--) { + commands_.at(i)->undo(); + } + } + +private: + Sequence* timeline_; + TimeRangeList regions_; + + QVector commands_; + +}; + +} + +#endif // TIMELINEUNDORIPPLE_H diff --git a/app/widget/timelinewidget/undo/timelineundosplit.cpp b/app/widget/timelinewidget/undo/timelineundosplit.cpp new file mode 100644 index 000000000..93018596a --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundosplit.cpp @@ -0,0 +1,181 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "timelineundosplit.h" + +#include "node/block/transition/transition.h" +#include "widget/nodeview/nodeviewundo.h" + +namespace olive { + +// +// BlockSplitCommand +// +void BlockSplitCommand::redo() +{ + old_length_ = block_->length(); + + Q_ASSERT(point_ > block_->in() && point_ < block_->out()); + + if (!reconnect_tree_command_) { + reconnect_tree_command_ = new MultiUndoCommand(); + new_block_ = static_cast(Node::CopyNodeInGraph(block_, reconnect_tree_command_)); + } + + reconnect_tree_command_->redo(); + + // Determine our new lengths + rational new_length = point_ - block_->in(); + rational new_part_length = block_->out() - point_; + + // Begin an operation + Track* track = block_->track(); + track->BeginOperation(); + + // Set lengths + block_->set_length_and_media_out(new_length); + new_block()->set_length_and_media_in(new_part_length); + + // Insert new block + track->InsertBlockAfter(new_block(), block_); + + // Position the block + if (!position_command_) { + position_command_ = new NodeSetPositionAsChildCommand(new_block(), track, nullptr, new_block()->index(), track->Blocks().size(), true); + } + position_command_->redo(); + + // If the block had an out transition, we move it to the new block + moved_transition_ = NodeInput(); + + TransitionBlock* potential_transition = dynamic_cast(new_block()->next()); + if (potential_transition) { + for (const Node::OutputConnection& output : block_->output_connections()) { + if (output.second.node() == potential_transition) { + moved_transition_ = NodeInput(potential_transition, TransitionBlock::kOutBlockInput); + Node::DisconnectEdge(block_, moved_transition_); + Node::ConnectEdge(new_block(), moved_transition_); + break; + } + } + } + + track->EndOperation(); +} + +void BlockSplitCommand::undo() +{ + Track* track = block_->track(); + + track->BeginOperation(); + + if (moved_transition_.IsValid()) { + Node::DisconnectEdge(new_block(), moved_transition_); + Node::ConnectEdge(block_, moved_transition_); + } + + position_command_->undo(); + + block_->set_length_and_media_out(old_length_); + track->RippleRemoveBlock(new_block()); + + // If we ran a reconnect command, disconnect now + reconnect_tree_command_->undo(); + + track->EndOperation(); +} + +// +// BlockSplitPreservingLinksCommand +// +void BlockSplitPreservingLinksCommand::redo() +{ + if (commands_.isEmpty()) { + QVector< QVector > split_blocks(times_.size()); + + for (int i=0;i times_.at(i-1)); + + QVector splits(blocks_.size()); + + for (int j=0;jin() < time && b->out() > time) { + BlockSplitCommand* split_command = new BlockSplitCommand(b, time); + split_command->redo(); + splits.replace(j, split_command->new_block()); + commands_.append(split_command); + } else { + splits.replace(j, nullptr); + } + } + + split_blocks.replace(i, splits); + } + + // Now that we've determined all the splits, we can relink everything + for (int i=0;i& split_list, split_blocks) { + NodeLinkCommand* blc = new NodeLinkCommand(split_list.at(i), split_list.at(j), true); + blc->redo(); + commands_.append(blc); + } + } + } + } + } else { + for (int i=0; iredo(); + } + } +} + +// +// TrackSplitAtTimeCommand +// +void TrackSplitAtTimeCommand::prepare() +{ + // Find Block that contains this time + Block* b = track_->BlockContainingTime(point_); + + if (b) { + command_ = new BlockSplitCommand(b, point_); + } +} + +} diff --git a/app/widget/timelinewidget/undo/timelineundosplit.h b/app/widget/timelinewidget/undo/timelineundosplit.h new file mode 100644 index 000000000..6e0c1e6d6 --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundosplit.h @@ -0,0 +1,159 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef TIMELINEUNDOSPLIT_H +#define TIMELINEUNDOSPLIT_H + +#include "node/output/track/track.h" + +namespace olive { + +class BlockSplitCommand : public UndoCommand { +public: + BlockSplitCommand(Block* block, rational point) : + block_(block), + new_block_(nullptr), + point_(point), + reconnect_tree_command_(nullptr), + position_command_(nullptr) + { + } + + virtual ~BlockSplitCommand() override + { + delete reconnect_tree_command_; + delete position_command_; + } + + virtual Project* GetRelevantProject() const override + { + return block_->project(); + } + + /** + * @brief Access the second block created as a result. Only valid after redo(). + */ + Block* new_block() + { + return new_block_; + } + + virtual void redo() override; + + virtual void undo() override; + +private: + Block* block_; + Block* new_block_; + + rational old_length_; + rational point_; + + MultiUndoCommand* reconnect_tree_command_; + + NodeInput moved_transition_; + + NodeSetPositionAsChildCommand* position_command_; + +}; + +class BlockSplitPreservingLinksCommand : public UndoCommand { +public: + BlockSplitPreservingLinksCommand(const QVector &blocks, const QList& times) : + blocks_(blocks), + times_(times) + { + } + + virtual ~BlockSplitPreservingLinksCommand() override + { + qDeleteAll(commands_); + } + + virtual Project* GetRelevantProject() const override + { + return blocks_.first()->project(); + } + + virtual void redo() override; + + virtual void undo() override + { + for (int i=commands_.size()-1; i>=0; i--) { + commands_.at(i)->undo(); + } + } + +private: + QVector blocks_; + + QList times_; + + QVector commands_; + +}; + +class TrackSplitAtTimeCommand : public UndoCommand { +public: + TrackSplitAtTimeCommand(Track* track, rational point) : + track_(track), + point_(point), + command_(nullptr) + { + } + + virtual ~TrackSplitAtTimeCommand() override + { + delete command_; + } + + virtual Project* GetRelevantProject() const override + { + return track_->project(); + } + + virtual void prepare() override; + + virtual void redo() override + { + if (command_) { + command_->redo(); + } + } + + virtual void undo() override + { + if (command_) { + command_->undo(); + } + } + +private: + Track* track_; + + rational point_; + + UndoCommand* command_; + +}; + +} + +#endif // TIMELINEUNDOSPLIT_H diff --git a/app/widget/timelinewidget/undo/timelineundotrack.cpp b/app/widget/timelinewidget/undo/timelineundotrack.cpp new file mode 100644 index 000000000..7e14c10b1 --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundotrack.cpp @@ -0,0 +1,25 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "timelineundotrack.h" + +namespace olive { + +} diff --git a/app/widget/timelinewidget/undo/timelineundotrack.h b/app/widget/timelinewidget/undo/timelineundotrack.h new file mode 100644 index 000000000..8e6706f7c --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundotrack.h @@ -0,0 +1,163 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef TIMELINEUNDOTRACK_H +#define TIMELINEUNDOTRACK_H + +#include "node/output/track/track.h" + +namespace olive { + +class TrackRippleRemoveBlockCommand : public UndoCommand +{ +public: + TrackRippleRemoveBlockCommand(Track* track, Block* block) : + track_(track), + block_(block) + { + } + + virtual Project* GetRelevantProject() const override + { + return track_->project(); + } + + virtual void redo() override + { + before_ = block_->previous(); + track_->RippleRemoveBlock(block_); + } + + virtual void undo() override + { + track_->InsertBlockAfter(block_, before_); + } + +private: + Track* track_; + + Block* block_; + + Block* before_; + +}; + +class TrackPrependBlockCommand : public UndoCommand +{ +public: + TrackPrependBlockCommand(Track* track, Block* block) : + track_(track), + block_(block) + { + } + + virtual Project* GetRelevantProject() const override + { + return track_->project(); + } + + virtual void redo() override + { + track_->PrependBlock(block_); + } + + virtual void undo() override + { + track_->RippleRemoveBlock(block_); + } + +private: + Track* track_; + Block* block_; +}; + +class TrackInsertBlockAfterCommand : public UndoCommand +{ +public: + TrackInsertBlockAfterCommand(Track* track, Block* block, Block* before) : + track_(track), + block_(block), + before_(before) + { + } + + virtual Project* GetRelevantProject() const override + { + return block_->project(); + } + + virtual void redo() override + { + track_->InsertBlockAfter(block_, before_); + } + + virtual void undo() override + { + track_->RippleRemoveBlock(block_); + } + +private: + Track* track_; + + Block* block_; + + Block* before_; +}; + +/** + * @brief Replaces Block `old` with Block `replace` + * + * Both blocks must have equal lengths. + */ +class TrackReplaceBlockCommand : public UndoCommand +{ +public: + TrackReplaceBlockCommand(Track* track, Block* old, Block* replace) : + track_(track), + old_(old), + replace_(replace) + { + } + + virtual Project* GetRelevantProject() const override + { + return track_->project(); + } + + virtual void redo() override + { + track_->ReplaceBlock(old_, replace_); + } + + virtual void undo() override + { + track_->ReplaceBlock(replace_, old_); + } + +private: + Track* track_; + Block* old_; + Block* replace_; + +}; + +} + +#endif // TIMELINEUNDOTRACK_H diff --git a/app/widget/timelinewidget/undo/timelineundoworkarea.cpp b/app/widget/timelinewidget/undo/timelineundoworkarea.cpp new file mode 100644 index 000000000..fcdd9b8b1 --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundoworkarea.cpp @@ -0,0 +1,25 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "timelineundoworkarea.h" + +namespace olive { + +} diff --git a/app/widget/timelinewidget/undo/timelineundoworkarea.h b/app/widget/timelinewidget/undo/timelineundoworkarea.h new file mode 100644 index 000000000..b44437a47 --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundoworkarea.h @@ -0,0 +1,103 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef TIMELINEUNDOWORKAREA_H +#define TIMELINEUNDOWORKAREA_H + +#include "node/project/project.h" +#include "timeline/timelinepoints.h" + +namespace olive { + +class WorkareaSetEnabledCommand : public UndoCommand { +public: + WorkareaSetEnabledCommand(Project *project, TimelinePoints* points, bool enabled) : + project_(project), + points_(points), + old_enabled_(points_->workarea()->enabled()), + new_enabled_(enabled) + { + } + + virtual Project* GetRelevantProject() const override + { + return project_; + } + + virtual void redo() override + { + points_->workarea()->set_enabled(new_enabled_); + } + + virtual void undo() override + { + points_->workarea()->set_enabled(old_enabled_); + } + +private: + Project* project_; + + TimelinePoints* points_; + + bool old_enabled_; + + bool new_enabled_; + +}; + +class WorkareaSetRangeCommand : public UndoCommand { +public: + WorkareaSetRangeCommand(Project *project, TimelinePoints* points, const TimeRange& range) : + project_(project), + points_(points), + old_range_(points_->workarea()->range()), + new_range_(range) + { + } + + virtual Project* GetRelevantProject() const override + { + return project_; + } + + virtual void redo() override + { + points_->workarea()->set_range(new_range_); + } + + virtual void undo() override + { + points_->workarea()->set_range(old_range_); + } + +private: + Project* project_; + + TimelinePoints* points_; + + TimeRange old_range_; + + TimeRange new_range_; + +}; + +} + +#endif // TIMELINEUNDOWORKAREA_H diff --git a/tests/timeline/timeline-tests.cpp b/tests/timeline/timeline-tests.cpp index 529168e30..17df4ee03 100644 --- a/tests/timeline/timeline-tests.cpp +++ b/tests/timeline/timeline-tests.cpp @@ -26,7 +26,8 @@ #include "node/project/project.h" #include "node/project/sequence/sequence.h" #include "undo/undocommand.h" -#include "widget/timelinewidget/timelineundo.h" +#include "widget/timelinewidget/undo/timelineundogeneral.h" +#include "widget/timelinewidget/undo/timelineundopointer.h" #include "testutil.h" namespace olive { From 2229ce9a696aa88d87f31676311485957eb4449a Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 16 Jul 2021 01:48:11 -0700 Subject: [PATCH 57/72] timeline: updated ripple for new invalidation scheme --- app/widget/timelinewidget/undo/timelineundoripple.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/widget/timelinewidget/undo/timelineundoripple.cpp b/app/widget/timelinewidget/undo/timelineundoripple.cpp index 78b377cae..d8716c2e2 100644 --- a/app/widget/timelinewidget/undo/timelineundoripple.cpp +++ b/app/widget/timelinewidget/undo/timelineundoripple.cpp @@ -447,6 +447,9 @@ void TrackListRippleToolCommand::ripple(bool redo) if (!all_tracks_unlocked_) { // If we're not shifting, the whole track must get invalidated track->Node::InvalidateCache(TimeRange(it.value().earliest_point_of_change, RATIONAL_MAX), Track::kBlockInput); + } else if (pre_latest_out < post_latest_out) { + // If we're here, then a new section has been rippled in that needs to be rendered + track->Node::InvalidateCache(TimeRange(pre_latest_out, post_latest_out), Track::kBlockInput); } } } From 61010ad6a64d89cdc13f3d62ad5433cc1a635cae Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 16 Jul 2021 02:06:02 -0700 Subject: [PATCH 58/72] nodeview: updated commands for new streamlined system --- app/widget/nodeview/nodeviewundo.cpp | 2 +- app/widget/nodeview/nodeviewundo.h | 47 ++++++++++------------------ 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/app/widget/nodeview/nodeviewundo.cpp b/app/widget/nodeview/nodeviewundo.cpp index ca0a37b8a..f62f2a497 100644 --- a/app/widget/nodeview/nodeviewundo.cpp +++ b/app/widget/nodeview/nodeviewundo.cpp @@ -124,7 +124,7 @@ void NodeCopyInputsCommand::redo() Node::CopyInputs(src_, dest_, include_connections_); } -void NodeRemoveAndDisconnectCommand::prep() +void NodeRemoveAndDisconnectCommand::prepare() { command_ = new MultiUndoCommand(); diff --git a/app/widget/nodeview/nodeviewundo.h b/app/widget/nodeview/nodeviewundo.h index 2df7613b5..b661ce8da 100644 --- a/app/widget/nodeview/nodeviewundo.h +++ b/app/widget/nodeview/nodeviewundo.h @@ -95,8 +95,7 @@ public: NodeRemoveAndDisconnectCommand(Node* node) : node_(node), graph_(nullptr), - command_(nullptr), - prepped_(false) + command_(nullptr) { } @@ -110,13 +109,10 @@ public: return dynamic_cast(graph_); } + virtual void prepare() override; + virtual void redo() override { - if (!prepped_) { - prep(); - prepped_ = true; - } - command_->redo(); graph_ = node_->parent(); @@ -132,8 +128,6 @@ public: } private: - void prep(); - QObject memory_manager_; Node* node_; @@ -141,16 +135,13 @@ private: MultiUndoCommand* command_; - bool prepped_; - }; class NodeRemoveWithExclusiveDependenciesAndDisconnect : public UndoCommand { public: NodeRemoveWithExclusiveDependenciesAndDisconnect(Node* node) : node_(node), - command_(nullptr), - prepped_(false) + command_(nullptr) { } @@ -168,23 +159,7 @@ public: } } - virtual void redo() override - { - if (!prepped_) { - prep(); - prepped_ = true; - } - - command_->redo(); - } - - virtual void undo() override - { - command_->undo(); - } - -private: - void prep() + virtual void prepare() override { command_ = new MultiUndoCommand(); @@ -197,9 +172,19 @@ private: } } + virtual void redo() override + { + command_->redo(); + } + + virtual void undo() override + { + command_->undo(); + } + +private: Node* node_; MultiUndoCommand* command_; - bool prepped_; }; From ad8d63d37a326fb6b2651d147924168f65f23252 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 16 Jul 2021 02:06:37 -0700 Subject: [PATCH 59/72] nodes: copy position map when copying dep graph --- app/node/node.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/node/node.cpp b/app/node/node.cpp index f758b849a..1ab4c8c15 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -1145,6 +1145,26 @@ Node *Node::CopyNodeAndDependencyGraphMinusItemsInternal(QMapparent()->GetPositionMap().contains(node)) { + // This node is a context, copy the context + const NodeGraph::PositionMap &map = node->parent()->GetPositionMap().value(node); + + command->add_child(new NodeSetPositionCommand(copy, copy, map.value(node), false); + + for (auto it=map.cbegin(); it!=map.cend(); it++) { + Node *context_child = it.key(); + + // See if we created a copy of this + Node *context_child_copy = created.value(context_child, nullptr); + + // Add to the context + command->add_child(new NodeSetPositionCommand(context_child_copy ? context_child_copy : context_child, + copy, + it.value(), + false)); + } + } + return copy; } From 8f413d37f057fd73d364255f520c03a9cd7713b5 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 16 Jul 2021 02:20:59 -0700 Subject: [PATCH 60/72] nodes: copy map when copying node with context --- app/node/node.cpp | 32 +++++++++++++++----------------- app/node/node.h | 6 +++--- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/app/node/node.cpp b/app/node/node.cpp index 1ab4c8c15..62818716e 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -1108,7 +1108,7 @@ void Node::CopyDependencyGraph(const QVector &src, const QVector } } -Node *Node::CopyNodeAndDependencyGraphMinusItemsInternal(QMap& created, const Node *node, MultiUndoCommand *command) +Node *Node::CopyNodeAndDependencyGraphMinusItemsInternal(QMap& created, Node *node, MultiUndoCommand *command) { // Make a new node of the same type Node* copy = node->copy(); @@ -1148,34 +1148,23 @@ Node *Node::CopyNodeAndDependencyGraphMinusItemsInternal(QMapparent()->GetPositionMap().contains(node)) { // This node is a context, copy the context const NodeGraph::PositionMap &map = node->parent()->GetPositionMap().value(node); - - command->add_child(new NodeSetPositionCommand(copy, copy, map.value(node), false); - for (auto it=map.cbegin(); it!=map.cend(); it++) { - Node *context_child = it.key(); - - // See if we created a copy of this - Node *context_child_copy = created.value(context_child, nullptr); - - // Add to the context - command->add_child(new NodeSetPositionCommand(context_child_copy ? context_child_copy : context_child, - copy, - it.value(), - false)); + // Add either the copy (if it exists) or the original node to the context + command->add_child(new NodeSetPositionCommand(created.value(it.key(), it.key()), copy, it.value(), false)); } } return copy; } -Node *Node::CopyNodeAndDependencyGraphMinusItems(const Node *node, MultiUndoCommand *command) +Node *Node::CopyNodeAndDependencyGraphMinusItems(Node *node, MultiUndoCommand *command) { - QMap created; + QMap created; return CopyNodeAndDependencyGraphMinusItemsInternal(created, node, command); } -Node *Node::CopyNodeInGraph(const Node *node, MultiUndoCommand *command) +Node *Node::CopyNodeInGraph(Node *node, MultiUndoCommand *command) { Node* copy; @@ -1188,6 +1177,15 @@ Node *Node::CopyNodeInGraph(const Node *node, MultiUndoCommand *command) copy)); command->add_child(new NodeCopyInputsCommand(node, copy, true)); + + if (node->parent()->GetPositionMap().contains(node)) { + // This node is a context, copy the context + const NodeGraph::PositionMap &map = node->parent()->GetPositionMap().value(node); + for (auto it=map.cbegin(); it!=map.cend(); it++) { + // Add to the context + command->add_child(new NodeSetPositionCommand(it.key(), copy, it.value(), false)); + } + } } return copy; diff --git a/app/node/node.h b/app/node/node.h index 5cb8d13b1..24727d39d 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -688,9 +688,9 @@ public: static QVector CopyDependencyGraph(const QVector& nodes, MultiUndoCommand *command); static void CopyDependencyGraph(const QVector& src, const QVector& dst, MultiUndoCommand *command); - static Node* CopyNodeAndDependencyGraphMinusItems(const Node* node, MultiUndoCommand* command); + static Node* CopyNodeAndDependencyGraphMinusItems(Node* node, MultiUndoCommand* command); - static Node* CopyNodeInGraph(const Node* node, MultiUndoCommand* command); + static Node* CopyNodeInGraph(Node *node, MultiUndoCommand* command); /** * @brief Return whether this Node can be deleted or not @@ -1125,7 +1125,7 @@ private: void ArrayResizeInternal(const QString& id, int size); - static Node *CopyNodeAndDependencyGraphMinusItemsInternal(QMap& created, const Node *node, MultiUndoCommand *command); + static Node *CopyNodeAndDependencyGraphMinusItemsInternal(QMap &created, Node *node, MultiUndoCommand *command); /** * @brief Immediates aren't deleted, so the actual array size may be larger than ArraySize() From 710b931bbb7311653f8e24178c31d328c129ce62 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 16 Jul 2021 15:53:01 -0700 Subject: [PATCH 61/72] render: queue audio over time rather than all at once --- app/render/previewautocacher.cpp | 46 ++++++++++++++++++++++---------- app/render/previewautocacher.h | 2 ++ 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 5801bd532..ceba5b335 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -132,10 +132,7 @@ void PreviewAutoCacher::HashesProcessed() } } - if (hash_iterator_.HasNext()) { - // Launch next hashes - QueueNextHashTask(); - } else { + if (!hash_iterator_.HasNext()) { // Restart delayed requeue timer delayed_requeue_timer_.stop(); delayed_requeue_timer_.start(); @@ -145,6 +142,9 @@ void PreviewAutoCacher::HashesProcessed() // The cacher might be waiting for this job to finish if (!graph_update_queue_.isEmpty()) { TryRender(); + } else if (hash_iterator_.HasNext()) { + // Launch next hashes + QueueNextHashTask(); } delete watcher; @@ -213,7 +213,9 @@ void PreviewAutoCacher::AudioRendered() } // The cacher might be waiting for this job to finish - if (!graph_update_queue_.isEmpty()) { + if (graph_update_queue_.isEmpty()) { + QueueNextAudioTask(); + } else { TryRender(); } @@ -442,6 +444,8 @@ void PreviewAutoCacher::ClearVideoQueue(bool hard) void PreviewAutoCacher::ClearAudioQueue(bool hard) { ClearQueueInternal(audio_tasks_, hard, &PreviewAutoCacher::AudioRendered); + + audio_iterator_.clear(); } void PreviewAutoCacher::ClearVideoDownloadQueue(bool hard) @@ -503,16 +507,10 @@ void PreviewAutoCacher::TryRender() } if (!invalidated_audio_.isEmpty()) { - foreach (const TimeRange& range, invalidated_audio_) { - std::list chunks = range.Split(30); + audio_iterator_ = invalidated_audio_; - foreach (const TimeRange& r, chunks) { - RenderTicketWatcher* watcher = new RenderTicketWatcher(); - watcher->setProperty("job", QVariant::fromValue(last_update_time_)); - connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered); - audio_tasks_.insert(watcher, r); - watcher->SetTicket(RenderManager::instance()->RenderAudio(copied_viewer_node_, r, RenderMode::kOffline, true)); - } + for (int i=0; isetProperty("job", QVariant::fromValue(last_update_time_)); + connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered); + audio_tasks_.insert(watcher, r); + watcher->SetTicket(RenderManager::instance()->RenderAudio(copied_viewer_node_, r, RenderMode::kOffline, false)); + + audio_iterator_.remove(r); + } +} + template void PreviewAutoCacher::ClearQueueInternal(T& list, bool hard, Func member) { diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index 0036a9738..af59b4cf7 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -107,6 +107,7 @@ private: void QueueNextFrameInRange(int max); void QueueNextHashTask(); + void QueueNextAudioTask(); struct HashData { rational time; @@ -176,6 +177,7 @@ private: TimeRangeListFrameIterator queued_frame_iterator_; TimeRangeListFrameIterator hash_iterator_; + TimeRangeList audio_iterator_; private slots: /** From c1c910af1b1c0c44c8bb57c23b4dc7d61a25e312 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 16 Jul 2021 20:29:20 -0700 Subject: [PATCH 62/72] render: accidentally committed no waveforms --- app/render/previewautocacher.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index ceba5b335..2b289195a 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -811,7 +811,7 @@ void PreviewAutoCacher::QueueNextAudioTask() watcher->setProperty("job", QVariant::fromValue(last_update_time_)); connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered); audio_tasks_.insert(watcher, r); - watcher->SetTicket(RenderManager::instance()->RenderAudio(copied_viewer_node_, r, RenderMode::kOffline, false)); + watcher->SetTicket(RenderManager::instance()->RenderAudio(copied_viewer_node_, r, RenderMode::kOffline, true)); audio_iterator_.remove(r); } From 7a51e521a241e2e3b1dfef1aaf3fda26405bdcf7 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 16 Jul 2021 20:29:39 -0700 Subject: [PATCH 63/72] timeline: don't set null context --- app/widget/timelinewidget/undo/timelineundosplit.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/widget/timelinewidget/undo/timelineundosplit.cpp b/app/widget/timelinewidget/undo/timelineundosplit.cpp index 93018596a..aff11d099 100644 --- a/app/widget/timelinewidget/undo/timelineundosplit.cpp +++ b/app/widget/timelinewidget/undo/timelineundosplit.cpp @@ -58,7 +58,7 @@ void BlockSplitCommand::redo() // Position the block if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(new_block(), track, nullptr, new_block()->index(), track->Blocks().size(), true); + position_command_ = new NodeSetPositionAsChildCommand(new_block(), track, track, new_block()->index(), track->Blocks().size(), true); } position_command_->redo(); From a341fb1a1a9488637c599e93ceb23386b4f3f48d Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 16 Jul 2021 20:30:18 -0700 Subject: [PATCH 64/72] render: add default constructor to TimeRangeWithJob --- app/render/renderjobtracker.h | 1 + 1 file changed, 1 insertion(+) diff --git a/app/render/renderjobtracker.h b/app/render/renderjobtracker.h index 169331072..baaddfd72 100644 --- a/app/render/renderjobtracker.h +++ b/app/render/renderjobtracker.h @@ -44,6 +44,7 @@ private: class TimeRangeWithJob : public TimeRange { public: + TimeRangeWithJob() = default; TimeRangeWithJob(const TimeRange &range, const JobTime &job_time) { set_range(range.in(), range.out()); From 07c841f76d4a2042bd1979a3761617aa22f68c36 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 16 Jul 2021 20:30:46 -0700 Subject: [PATCH 65/72] nodeview: prelim work towards show all functionality --- app/widget/nodeview/nodeview.cpp | 206 ++++++++++++++++--------------- app/widget/nodeview/nodeview.h | 2 +- 2 files changed, 106 insertions(+), 102 deletions(-) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index fe8ab80b9..22e8520e4 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -101,7 +101,7 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) if (graph_changed) { if (graph_) { // Disconnect from current graph - disconnect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); + //disconnect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); disconnect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); disconnect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); disconnect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); @@ -113,7 +113,7 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) if (graph_) { // Connect to new graph - connect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); + //connect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); connect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); connect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); connect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); @@ -122,47 +122,50 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) } } - if (context_changed) { + if (context_changed && filter_mode_ == kFilterShowSelective) { filter_nodes_ = nodes; } if (refresh_required && nodes_visible) { if (filter_mode_ == kFilterShowAll) { - /* = WIP = - for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { + // Determine which contexts in the graph have no outputs, these are considered "root-level" + filter_nodes_.clear(); + for (auto it=graph->GetPositionMap().cbegin(); it!=graph->GetPositionMap().cend(); it++) { Node *context = it.key(); - const NodeGraph::PositionMap &map = it.value(); - - for (auto jt=map.cbegin(); jt!=map.cend(); jt++) { - + if (context->output_connections().empty()) { + filter_nodes_.append(context); } - }*/ - } else { - // Reserve an arbitrary number to reduce the amount of reallocations - qreal last_offset = 0; - int additional_spacing = 0; + } + } - foreach (Node *n, filter_nodes_) { - const NodeGraph::PositionMap &map = graph_->GetNodesForContext(n); + // Standard root-level positioning code + qreal last_offset = 0; + int additional_spacing = 0; - // First determine the total "height" of this graph and how much we need to offset it - qreal top = 0; - qreal bottom = 0; - for (auto it=map.cbegin(); it!=map.cend(); it++) { - const QPointF &node_pos_in_context = it.value(); - top = qMin(node_pos_in_context.y(), top); - bottom = qMax(node_pos_in_context.y(), bottom); - } + // Contexts may be added to this later, so to ensure we only process the root-level nodes we + // found above, we'll store the current size and stop iterating after that + int sz = filter_nodes_.size(); + for (int i=0; iGetNodesForContext(n); - last_offset += (additional_spacing + (bottom - top)); - additional_spacing = 1; - context_offsets_.insert(n, QPointF(0, last_offset)); + // First determine the total "height" of this graph and how much we need to offset it + qreal top = 0; + qreal bottom = 0; + for (auto it=map.cbegin(); it!=map.cend(); it++) { + const QPointF &node_pos_in_context = it.value(); + top = qMin(node_pos_in_context.y(), top); + bottom = qMax(node_pos_in_context.y(), bottom); + } - // Finally add all nodes - for (auto it=map.cbegin(); it!=map.cend(); it++) { - AddNodePosition(it.key(), n); - } + last_offset += (additional_spacing + (bottom - top)); + additional_spacing = 1; + context_offsets_.insert(n, QPointF(0, last_offset)); + + // Finally add all nodes + for (auto it=map.cbegin(); it!=map.cend(); it++) { + AddNodePosition(it.key(), n); } } @@ -994,13 +997,9 @@ void NodeView::ShowContextMenu(const QPoint &pos) Menu* filter_menu = new Menu(tr("Filter"), &m); m.addMenu(filter_menu); - filter_menu->AddActionWithData(tr("Show All Nodes"), - kFilterShowAll, - filter_mode_); + filter_menu->AddActionWithData(tr("Show All Nodes"), kFilterShowAll, filter_mode_); - filter_menu->AddActionWithData(tr("Show Selected"), - kFilterShowSelective, - filter_mode_); + filter_menu->AddActionWithData(tr("Show Selected"), kFilterShowSelective, filter_mode_); connect(filter_menu, &Menu::triggered, this, &NodeView::ContextMenuFilterChanged); @@ -1098,12 +1097,15 @@ void NodeView::OpenSelectedNodeInViewer() } } -void NodeView::AddNode(Node *node) -{ - if (filter_mode_ == kFilterShowAll) { - scene_.AddNode(node); - } -} +// Commenting out because there shouldn't be any situations where a node would be added without +// being in a context. We're keeping RemoveNode as a fail-safe because it could provide crash +// resistance where RemoveNodePosition might be missed. +//void NodeView::AddNode(Node *node) +//{ +// if (filter_mode_ == kFilterShowAll) { +// scene_.AddNode(node); +// } +//} void NodeView::RemoveNode(Node *node) { @@ -1112,15 +1114,11 @@ void NodeView::RemoveNode(Node *node) void NodeView::AddEdge(const NodeOutput &output, const NodeInput &input) { - if (filter_mode_ == kFilterShowAll) { - scene_.AddEdge(output, input); - } else if (filter_mode_ == kFilterShowSelective) { - Node *output_node = output.node(); - Node *input_node = input.node(); + Node *output_node = output.node(); + Node *input_node = input.node(); - if (scene_.item_map().contains(output_node) && scene_.item_map().contains(input_node)) { - scene_.AddEdge(output, input); - } + if (scene_.item_map().contains(output_node) && scene_.item_map().contains(input_node)) { + scene_.AddEdge(output, input); } } @@ -1131,74 +1129,80 @@ void NodeView::RemoveEdge(const NodeOutput &output, const NodeInput &input) void NodeView::AddNodePosition(Node *node, Node *relative) { - if (filter_mode_ == kFilterShowSelective) { - if (filter_nodes_.contains(relative)) { - // Get UI item or create if it doesn't exist - NodeViewItem *item = scene_.item_map().value(node); - if (!item) { - item = scene_.AddNode(node); + if (filter_nodes_.contains(relative)) { + // Get UI item or create if it doesn't exist + NodeViewItem *item = scene_.item_map().value(node); + if (!item) { + item = scene_.AddNode(node); - for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { - if (scene_.item_map().contains(it->second.node())) { - scene_.AddEdge(it->second, it->first); - } - } - - for (auto it=node->output_connections().cbegin(); it!=node->output_connections().cend(); it++) { - if (scene_.item_map().contains(it->second.node())) { - scene_.AddEdge(it->first, it->second); - } + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + if (scene_.item_map().contains(it->second.node())) { + scene_.AddEdge(it->second, it->first); } } - // Determine "view" position by averaging the Y value and "min"ing the X value of all contexts - QPointF item_pos(DBL_MAX, 0.0); - int average_count = 0; - foreach (Node *context, filter_nodes_) { - if (graph_->GetNodesForContext(context).contains(node)) { - QPointF this_context_pos = graph_->GetNodePosition(node, context); - this_context_pos += context_offsets_.value(context); - item_pos.setX(qMin(item_pos.x(), this_context_pos.x())); - item_pos.setY(item_pos.y() + this_context_pos.y()); - average_count++; + for (auto it=node->output_connections().cbegin(); it!=node->output_connections().cend(); it++) { + if (scene_.item_map().contains(it->second.node())) { + scene_.AddEdge(it->first, it->second); } } - item_pos.setY(item_pos.y() / average_count); + } - // Set position - item->SetNodePosition(item_pos); - positions_.insert(item, {node, item_pos}); + // Determine "view" position by averaging the Y value and "min"ing the X value of all contexts + QPointF item_pos(DBL_MAX, 0.0); + int average_count = 0; + foreach (Node *context, filter_nodes_) { + if (graph_->GetNodesForContext(context).contains(node)) { + QPointF this_context_pos = graph_->GetNodePosition(node, context); + this_context_pos += context_offsets_.value(context); + item_pos.setX(qMin(item_pos.x(), this_context_pos.x())); + item_pos.setY(item_pos.y() + this_context_pos.y()); + average_count++; + } + } + item_pos.setY(item_pos.y() / average_count); + + // Set position + item->SetNodePosition(item_pos); + positions_.insert(item, {node, item_pos}); + + // For "show all" mode, we recursively add more nodes to the graph + if (filter_mode_ == kFilterShowAll && !filter_nodes_.contains(node) && graph_->GetPositionMap().contains(node)) { + filter_nodes_.append(node); + context_offsets_.insert(node, item_pos); + const NodeGraph::PositionMap &map = graph_->GetNodesForContext(node); + for (auto it=map.cbegin(); it!=map.cend(); it++) { + AddNodePosition(it.key(), node); + } } } } void NodeView::RemoveNodePosition(Node *node, Node *relative) { - if (filter_mode_ == kFilterShowSelective) { - if (filter_nodes_.contains(relative)) { - NodeViewItem *item = scene_.item_map().value(node); + if (filter_nodes_.contains(relative)) { + NodeViewItem *item = scene_.item_map().value(node); - if (item && !item->GetPreventRemoving()) { - // Determine if any other contexts have this node - bool found = false; + if (item && !item->GetPreventRemoving()) { + // Determine if any other contexts have this node + bool found = false; - foreach (Node *context, filter_nodes_) { - if (graph_->ContextContainsNode(node, context)) { - found = true; - break; - } + foreach (Node *context, filter_nodes_) { + if (graph_->ContextContainsNode(node, context)) { + found = true; + break; } + } - if (!found) { - foreach (const Node::OutputConnection &oc, node->output_connections()) { - scene_.RemoveEdge(oc.first, oc.second); - } - for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { - scene_.RemoveEdge(it->second, it->first); - } - positions_.remove(item); - scene_.RemoveNode(node); + if (!found) { + foreach (const Node::OutputConnection &oc, node->output_connections()) { + scene_.RemoveEdge(oc.first, oc.second); } + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + scene_.RemoveEdge(it->second, it->first); + } + positions_.remove(item); + scene_.RemoveNode(node); } } } diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 4eff25c02..e4337769e 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -268,7 +268,7 @@ private slots: */ void OpenSelectedNodeInViewer(); - void AddNode(Node *node); + //void AddNode(Node *node); void RemoveNode(Node *node); void AddEdge(const NodeOutput& output, const NodeInput& input); void RemoveEdge(const NodeOutput& output, const NodeInput& input); From 84e639606fe1bbeb6c8b67ff68377434e440840a Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 16 Jul 2021 21:32:34 -0700 Subject: [PATCH 66/72] nodeview: ensure nodes are disconnected when clearing --- app/widget/nodeview/nodeviewscene.cpp | 23 ++++++++++++++++++----- app/widget/nodeview/nodeviewscene.h | 4 ++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index 4e1ca2207..68b673561 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -65,7 +65,10 @@ void NodeViewScene::clear() // deleted. Calling this function appears to update the internal cache and prevent this. selectedItems(); - qDeleteAll(item_map_); + for (auto it=item_map_.cbegin(); it!=item_map_.cend(); it++) { + DisconnectNode(it.key()); + delete it.value(); + } item_map_.clear(); qDeleteAll(edges_); @@ -157,16 +160,14 @@ NodeViewItem* NodeViewScene::AddNode(Node* node) addItem(item); item_map_.insert(node, item); - connect(node, &Node::LabelChanged, this, &NodeViewScene::NodeAppearanceChanged); - connect(node, &Node::ColorChanged, this, &NodeViewScene::NodeAppearanceChanged); + ConnectNode(node); return item; } void NodeViewScene::RemoveNode(Node *node) { - disconnect(node, &Node::ColorChanged, this, &NodeViewScene::NodeAppearanceChanged); - disconnect(node, &Node::LabelChanged, this, &NodeViewScene::NodeAppearanceChanged); + DisconnectNode(node); delete item_map_.take(node); } @@ -224,6 +225,18 @@ NodeViewEdge* NodeViewScene::AddEdgeInternal(const NodeOutput& output, const Nod return edge_ui; } +void NodeViewScene::ConnectNode(Node *n) +{ + connect(n, &Node::LabelChanged, this, &NodeViewScene::NodeAppearanceChanged); + connect(n, &Node::ColorChanged, this, &NodeViewScene::NodeAppearanceChanged); +} + +void NodeViewScene::DisconnectNode(Node *n) +{ + disconnect(n, &Node::ColorChanged, this, &NodeViewScene::NodeAppearanceChanged); + disconnect(n, &Node::LabelChanged, this, &NodeViewScene::NodeAppearanceChanged); +} + Qt::Orientation NodeViewScene::GetFlowOrientation() const { return NodeViewCommon::GetFlowOrientation(direction_); diff --git a/app/widget/nodeview/nodeviewscene.h b/app/widget/nodeview/nodeviewscene.h index 94cadd463..53b74e074 100644 --- a/app/widget/nodeview/nodeviewscene.h +++ b/app/widget/nodeview/nodeviewscene.h @@ -111,6 +111,10 @@ private: NodeViewEdge* AddEdgeInternal(const NodeOutput &output, const NodeInput &input, NodeViewItem* from, NodeViewItem* to); + void ConnectNode(Node *n); + + void DisconnectNode(Node *n); + QHash item_map_; QVector edges_; From 52c561715997737850455fc07c44f409584a94ff Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 17 Jul 2021 00:14:24 -0700 Subject: [PATCH 67/72] undo: wrapped redo/undo functions to ensure prepare is called --- app/dialog/sequence/sequence.h | 1 + app/node/node.cpp | 6 +- app/node/node.h | 15 ++++- app/node/project/folder/folder.cpp | 4 +- app/node/project/folder/folder.h | 2 + app/node/project/sequence/sequence.cpp | 4 +- app/task/project/loadotio/loadotio.cpp | 4 +- app/undo/undocommand.cpp | 24 +++++--- app/undo/undocommand.h | 12 ++-- app/widget/keyframeview/keyframeviewundo.h | 2 + app/widget/nodeparamview/nodeparamviewundo.h | 7 +++ app/widget/nodeview/nodeview.h | 14 +++-- app/widget/nodeview/nodeviewundo.cpp | 4 +- app/widget/nodeview/nodeviewundo.h | 17 ++++-- app/widget/timebased/timebasedwidget.h | 1 + app/widget/timelinewidget/timelinewidget.h | 5 +- .../timelinewidget/undo/timelineundocommon.h | 2 +- .../undo/timelineundogeneral.cpp | 12 ++-- .../timelinewidget/undo/timelineundogeneral.h | 8 +++ .../undo/timelineundopointer.cpp | 24 ++++---- .../timelinewidget/undo/timelineundopointer.h | 3 + .../undo/timelineundoripple.cpp | 10 ++-- .../timelinewidget/undo/timelineundoripple.h | 6 +- .../timelinewidget/undo/timelineundosplit.cpp | 8 +-- .../timelinewidget/undo/timelineundosplit.h | 6 +- .../timelinewidget/undo/timelineundotrack.h | 4 ++ .../undo/timelineundoworkarea.h | 2 + app/window/mainwindow/mainwindowundo.h | 10 ++-- tests/timeline/timeline-tests.cpp | 60 +++++++++---------- 29 files changed, 174 insertions(+), 103 deletions(-) diff --git a/app/dialog/sequence/sequence.h b/app/dialog/sequence/sequence.h index 9e3ac5362..ad9036be4 100644 --- a/app/dialog/sequence/sequence.h +++ b/app/dialog/sequence/sequence.h @@ -115,6 +115,7 @@ private: virtual Project* GetRelevantProject() const override; + protected: virtual void redo() override; virtual void undo() override; diff --git a/app/node/node.cpp b/app/node/node.cpp index 62818716e..23f022641 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -927,7 +927,7 @@ void Node::InputArrayResize(const QString &id, int size, bool undoable) if (undoable) { Core::instance()->undo_stack()->push(c); } else { - c->redo(); + c->redo_now(); delete c; } } @@ -2277,7 +2277,7 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo() if (commands_.isEmpty()) { // Move first node NodeSetPositionCommand* set_pos_command = new NodeSetPositionCommand(node_, relative_, position_, move_dependencies_); - set_pos_command->redo(); + set_pos_command->redo_now(); commands_.append(set_pos_command); // Get bounding rect @@ -2308,7 +2308,7 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo() } } else { for (int i=0; iredo(); + commands_.at(i)->redo_now(); } } } diff --git a/app/node/node.h b/app/node/node.h index 24727d39d..31c144d8f 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -1009,6 +1009,7 @@ private: virtual Project* GetRelevantProject() const override; + protected: virtual void redo() override { node_->InputArrayInsert(input_, index_, false); @@ -1035,6 +1036,9 @@ private: size_(size) {} + virtual Project* GetRelevantProject() const override; + + protected: virtual void redo() override { old_size_ = node_->InputArraySize(input_); @@ -1067,8 +1071,6 @@ private: node_->ArrayResizeInternal(input_, old_size_); } - virtual Project* GetRelevantProject() const override; - private: Node* node_; QString input_; @@ -1311,6 +1313,7 @@ public: return node_->project(); } +protected: virtual void redo() override; virtual void undo() override; @@ -1346,12 +1349,13 @@ public: return node_->project(); } +protected: virtual void redo() override; virtual void undo() override { for (int i=commands_.size()-1; i>=0; i--) { - commands_.at(i)->undo(); + commands_.at(i)->undo_now(); } } @@ -1392,6 +1396,7 @@ public: return node_->project(); } +protected: virtual void redo() override; virtual void undo() override @@ -1423,6 +1428,7 @@ public: return parent_->project(); } +protected: virtual void redo() override; virtual void undo() override; @@ -1447,6 +1453,7 @@ public: return node_->project(); } +protected: virtual void redo() override; virtual void undo() override; @@ -1474,6 +1481,7 @@ public: return node_->project(); } +protected: virtual void redo() override; virtual void undo() override; @@ -1502,6 +1510,7 @@ public: return node_->project(); } +protected: virtual void redo() override; virtual void undo() override; diff --git a/app/node/project/folder/folder.cpp b/app/node/project/folder/folder.cpp index 336feb6bc..91fda055f 100644 --- a/app/node/project/folder/folder.cpp +++ b/app/node/project/folder/folder.cpp @@ -141,14 +141,14 @@ void FolderAddChild::redo() if (!position_command_) { position_command_ = new NodeSetPositionAsChildCommand(child_, folder_, folder_->project()->root(), array_index, array_index+1, true); } - position_command_->redo(); + position_command_->redo_now(); } } void FolderAddChild::undo() { if (position_command_) { - position_command_->undo(); + position_command_->undo_now(); } Node::DisconnectEdge(child_, NodeInput(folder_, Folder::kChildInput, folder_->InputArraySize(Folder::kChildInput)-1)); diff --git a/app/node/project/folder/folder.h b/app/node/project/folder/folder.h index e131adec3..ce71033b6 100644 --- a/app/node/project/folder/folder.h +++ b/app/node/project/folder/folder.h @@ -135,6 +135,7 @@ public: return folder_->project(); } + protected: virtual void redo() override; virtual void undo() override @@ -209,6 +210,7 @@ public: virtual Project * GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; diff --git a/app/node/project/sequence/sequence.cpp b/app/node/project/sequence/sequence.cpp index 4d13704b2..5773e42c5 100644 --- a/app/node/project/sequence/sequence.cpp +++ b/app/node/project/sequence/sequence.cpp @@ -64,8 +64,8 @@ void Sequence::add_default_nodes(MultiUndoCommand* command) command->add_child(video_track_command); command->add_child(audio_track_command); } else { - video_track_command->redo(); - audio_track_command->redo(); + video_track_command->redo_now(); + audio_track_command->redo_now(); delete video_track_command; delete audio_track_command; } diff --git a/app/task/project/loadotio/loadotio.cpp b/app/task/project/loadotio/loadotio.cpp index dd4428613..1d672ace4 100644 --- a/app/task/project/loadotio/loadotio.cpp +++ b/app/task/project/loadotio/loadotio.cpp @@ -87,7 +87,7 @@ bool LoadOTIOTask::Run() Sequence* sequence = new Sequence(); sequence->SetLabel(QString::fromStdString(timeline->name())); sequence->setParent(project_); - FolderAddChild(project_->root(), sequence).redo(); + FolderAddChild(project_->root(), sequence).redo_now(); // FIXME: As far as I know, OTIO doesn't store video/audio parameters? sequence->set_default_parameters(); @@ -111,7 +111,7 @@ bool LoadOTIOTask::Run() // Create track TimelineAddTrackCommand t(sequence->track_list(type)); - t.redo(); + t.redo_now(); track = t.track(); } else { qWarning() << "Found unknown track type:" << otio_track->kind().c_str(); diff --git a/app/undo/undocommand.cpp b/app/undo/undocommand.cpp index 4cf60da80..075348c15 100644 --- a/app/undo/undocommand.cpp +++ b/app/undo/undocommand.cpp @@ -56,12 +56,7 @@ UndoCommand::UndoCommand() void UndoCommand::redo_and_set_modified() { - if (!prepared_) { - prepare(); - prepared_ = true; - } - - redo(); + redo_now(); project_ = GetRelevantProject(); if (project_) { @@ -72,11 +67,26 @@ void UndoCommand::redo_and_set_modified() void UndoCommand::undo_and_set_modified() { - undo(); + undo_now(); if (project_) { project_->set_modified(modified_); } } +void UndoCommand::redo_now() +{ + if (!prepared_) { + prepare(); + prepared_ = true; + } + + redo(); +} + +void UndoCommand::undo_now() +{ + undo(); +} + } diff --git a/app/undo/undocommand.h b/app/undo/undocommand.h index f7fb98579..f63ae745e 100644 --- a/app/undo/undocommand.h +++ b/app/undo/undocommand.h @@ -40,13 +40,12 @@ public: DISABLE_COPY_MOVE(UndoCommand) - virtual void prepare(){} - virtual void redo() = 0; - virtual void undo() = 0; - bool has_prepared() const {return prepared_;} void set_prepared(bool e) {prepared_ = true;} + void redo_now(); + void undo_now(); + void redo_and_set_modified(); void undo_and_set_modified(); @@ -62,6 +61,11 @@ public: name_ = name; } +protected: + virtual void prepare(){} + virtual void redo() = 0; + virtual void undo() = 0; + private: bool modified_; diff --git a/app/widget/keyframeview/keyframeviewundo.h b/app/widget/keyframeview/keyframeviewundo.h index e8fc3aa55..c994d6f1a 100644 --- a/app/widget/keyframeview/keyframeviewundo.h +++ b/app/widget/keyframeview/keyframeviewundo.h @@ -32,6 +32,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -51,6 +52,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; diff --git a/app/widget/nodeparamview/nodeparamviewundo.h b/app/widget/nodeparamview/nodeparamviewundo.h index ffc2f5b99..2e9623652 100644 --- a/app/widget/nodeparamview/nodeparamviewundo.h +++ b/app/widget/nodeparamview/nodeparamviewundo.h @@ -35,6 +35,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -51,6 +52,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -70,6 +72,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -90,6 +93,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -109,6 +113,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -128,6 +133,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -146,6 +152,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index e4337769e..dafb58812 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -143,12 +143,13 @@ private: public: NodeViewAttachNodesToCursor(NodeView* view, const QVector& nodes); + virtual Project * GetRelevantProject() const override; + + protected: virtual void redo() override; virtual void undo() override; - virtual Project * GetRelevantProject() const override; - private: NodeView* view_; @@ -174,15 +175,16 @@ private: new_prevent_removing_(prevent_removing) {} - virtual void redo() override; - - virtual void undo() override; - virtual Project * GetRelevantProject() const override { return node_->project(); } + protected: + virtual void redo() override; + + virtual void undo() override; + private: NodeView *view_; Node *node_; diff --git a/app/widget/nodeview/nodeviewundo.cpp b/app/widget/nodeview/nodeviewundo.cpp index f62f2a497..0197dcf94 100644 --- a/app/widget/nodeview/nodeviewundo.cpp +++ b/app/widget/nodeview/nodeviewundo.cpp @@ -43,7 +43,7 @@ void NodeEdgeAddCommand::redo() remove_command_ = new NodeEdgeRemoveCommand(input_.GetConnectedOutput(), input_); } - remove_command_->redo(); + remove_command_->redo_now(); } Node::ConnectEdge(output_, input_); @@ -54,7 +54,7 @@ void NodeEdgeAddCommand::undo() Node::DisconnectEdge(output_, input_); if (remove_command_) { - remove_command_->undo(); + remove_command_->undo_now(); } } diff --git a/app/widget/nodeview/nodeviewundo.h b/app/widget/nodeview/nodeviewundo.h index b661ce8da..c2f5aae8e 100644 --- a/app/widget/nodeview/nodeviewundo.h +++ b/app/widget/nodeview/nodeviewundo.h @@ -39,6 +39,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -61,6 +62,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -80,6 +82,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -109,6 +112,7 @@ public: return dynamic_cast(graph_); } +protected: virtual void prepare() override; virtual void redo() override @@ -159,6 +163,7 @@ public: } } +protected: virtual void prepare() override { command_ = new MultiUndoCommand(); @@ -194,12 +199,13 @@ public: Node* dest, bool include_connections); + virtual Project* GetRelevantProject() const override {return nullptr;} + +protected: virtual void redo() override; virtual void undo() override {} - virtual Project* GetRelevantProject() const override {return nullptr;} - private: const Node* src_; @@ -223,6 +229,7 @@ public: return a_->project(); } +protected: virtual void redo() override { if (link_) { @@ -263,6 +270,7 @@ public: return node_->project(); } +protected: virtual void redo() override { unlinked_ = node_->links(); @@ -319,12 +327,13 @@ public: void AddNode(Node* node, const QString& new_name); + virtual Project * GetRelevantProject() const override; + +protected: virtual void redo() override; virtual void undo() override; - virtual Project * GetRelevantProject() const override; - private: QVector nodes_; diff --git a/app/widget/timebased/timebasedwidget.h b/app/widget/timebased/timebasedwidget.h index 1f27300cc..e0cc79b41 100644 --- a/app/widget/timebased/timebasedwidget.h +++ b/app/widget/timebased/timebasedwidget.h @@ -148,6 +148,7 @@ private: virtual Project* GetRelevantProject() const override; + protected: virtual void redo() override; virtual void undo() override; diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 6b5ddc7d8..971e57866 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -214,6 +214,9 @@ public: { } + virtual Project* GetRelevantProject() const override {return nullptr;} + + protected: virtual void redo() override { timeline_->SetSelections(now_); @@ -224,8 +227,6 @@ public: timeline_->SetSelections(old_); } - virtual Project* GetRelevantProject() const override {return nullptr;} - private: TimelineWidget* timeline_; TimelineWidgetSelections old_; diff --git a/app/widget/timelinewidget/undo/timelineundocommon.h b/app/widget/timelinewidget/undo/timelineundocommon.h index d25de0679..3a0d28f19 100644 --- a/app/widget/timelinewidget/undo/timelineundocommon.h +++ b/app/widget/timelinewidget/undo/timelineundocommon.h @@ -39,7 +39,7 @@ inline UndoCommand* CreateRemoveCommand(Node* n) inline UndoCommand* CreateAndRunRemoveCommand(Node* n) { UndoCommand* command = CreateRemoveCommand(n); - command->redo(); + command->redo_now(); return command; } diff --git a/app/widget/timelinewidget/undo/timelineundogeneral.cpp b/app/widget/timelinewidget/undo/timelineundogeneral.cpp index 6deb5586d..e9b2cda64 100644 --- a/app/widget/timelinewidget/undo/timelineundogeneral.cpp +++ b/app/widget/timelinewidget/undo/timelineundogeneral.cpp @@ -249,14 +249,14 @@ void TransitionRemoveCommand::redo() remove_command_ = CreateRemoveCommand(block_); } - remove_command_->redo(); + remove_command_->redo_now(); } } void TransitionRemoveCommand::undo() { if (remove_from_graph_) { - remove_command_->undo(); + remove_command_->undo_now(); } track_->BeginOperation(); @@ -446,7 +446,7 @@ void TrackReplaceBlockWithGapCommand::redo() CreateRemoveTransitionCommandIfNecessary(true); } for (auto it=transition_remove_commands_.cbegin(); it!=transition_remove_commands_.cend(); it++) { - (*it)->redo(); + (*it)->redo_now(); } if (block_->next()) { @@ -500,7 +500,7 @@ void TrackReplaceBlockWithGapCommand::redo() if (!position_command_) { position_command_ = new NodeSetPositionAsChildCommand(our_gap_, track_, track_, our_gap_->index(), track_->Blocks().size(), true); } - position_command_->redo(); + position_command_->redo_now(); } track_->EndOperation(); @@ -533,7 +533,7 @@ void TrackReplaceBlockWithGapCommand::undo() track_->ReplaceBlock(our_gap_, block_); our_gap_->setParent(&memory_manager_); - position_command_->undo(); + position_command_->undo_now(); } else { @@ -583,7 +583,7 @@ void TrackReplaceBlockWithGapCommand::undo() } for (auto it=transition_remove_commands_.crbegin(); it!=transition_remove_commands_.crend(); it++) { - (*it)->undo(); + (*it)->undo_now(); } } diff --git a/app/widget/timelinewidget/undo/timelineundogeneral.h b/app/widget/timelinewidget/undo/timelineundogeneral.h index f1dd6075f..b47587ff5 100644 --- a/app/widget/timelinewidget/undo/timelineundogeneral.h +++ b/app/widget/timelinewidget/undo/timelineundogeneral.h @@ -44,6 +44,7 @@ public: return block_->project(); } +protected: virtual void redo() override; virtual void undo() override; @@ -67,6 +68,7 @@ public: return block_->project(); } +protected: virtual void redo(); virtual void undo(); @@ -90,6 +92,7 @@ public: return block_->project(); } +protected: virtual void redo(); virtual void undo(); @@ -138,6 +141,7 @@ public: return timeline_->parent()->project(); } +protected: virtual void redo() override; virtual void undo() override; @@ -172,6 +176,7 @@ public: return track_->project(); } +protected: virtual void redo() override; virtual void undo() override; @@ -211,6 +216,7 @@ public: return block_->project(); } +protected: virtual void redo() override; virtual void undo() override; @@ -248,6 +254,7 @@ public: return block_->project(); } +protected: virtual void redo() override { block_->set_enabled(new_enabled_); @@ -287,6 +294,7 @@ public: return track_list_->parent()->project(); } +protected: virtual void prepare() override; virtual void redo() override; diff --git a/app/widget/timelinewidget/undo/timelineundopointer.cpp b/app/widget/timelinewidget/undo/timelineundopointer.cpp index 0bac3b4af..0954cd41c 100644 --- a/app/widget/timelinewidget/undo/timelineundopointer.cpp +++ b/app/widget/timelinewidget/undo/timelineundopointer.cpp @@ -68,7 +68,7 @@ void BlockTrimCommand::redo() if (!deleted_adjacent_command_) { deleted_adjacent_command_ = CreateAndRunRemoveCommand(adjacent_); } else { - deleted_adjacent_command_->redo(); + deleted_adjacent_command_->redo_now(); } } } else { @@ -110,7 +110,7 @@ void BlockTrimCommand::undo() if (we_removed_adjacent_) { if (deleted_adjacent_command_) { // We deleted adjacent, restore it now - deleted_adjacent_command_->undo(); + deleted_adjacent_command_->undo_now(); } if (mode_ == Timeline::kTrimIn) { @@ -216,7 +216,7 @@ void TrackSlideCommand::redo() in_adjacent_remove_command_ = CreateRemoveCommand(in_adjacent_); } - in_adjacent_remove_command_->redo(); + in_adjacent_remove_command_->redo_now(); } } else { // Simply resize adjacent @@ -238,7 +238,7 @@ void TrackSlideCommand::redo() out_adjacent_remove_command_ = CreateRemoveCommand(out_adjacent_); } - out_adjacent_remove_command_->redo(); + out_adjacent_remove_command_->redo_now(); } } else { // Simply resize adjacent @@ -269,7 +269,7 @@ void TrackSlideCommand::undo() in_adjacent_->setParent(&memory_manager_); } else if (in_adjacent_remove_command_) { // We removed this, so we can restore it now - in_adjacent_remove_command_->undo(); + in_adjacent_remove_command_->undo_now(); } else { // Simply resize adjacent in_adjacent_->set_length_and_media_out(in_adjacent_->length() - movement_); @@ -281,7 +281,7 @@ void TrackSlideCommand::undo() track_->RippleRemoveBlock(out_adjacent_); out_adjacent_->setParent(&memory_manager_); } else if (out_adjacent_remove_command_) { - out_adjacent_remove_command_->undo(); + out_adjacent_remove_command_->undo_now(); } else { out_adjacent_->set_length_and_media_in(out_adjacent_->length() + movement_); } @@ -343,7 +343,7 @@ void TrackPlaceBlockCommand::redo() } for (int i=0; iredo(); + add_track_commands_.at(i)->redo_now(); } } @@ -382,7 +382,7 @@ void TrackPlaceBlockCommand::redo() } - ripple_remove_command_->redo(); + ripple_remove_command_->redo_now(); track->InsertBlockAfter(insert_, ripple_remove_command_->GetInsertionIndex()); if (position_commands_.isEmpty()) { @@ -399,14 +399,14 @@ void TrackPlaceBlockCommand::redo() } for (int i=0; iredo(); + position_commands_.at(i)->redo_now(); } } void TrackPlaceBlockCommand::undo() { for (int i=position_commands_.size()-1; i>=0; i--) { - position_commands_.at(i)->undo(); + position_commands_.at(i)->undo_now(); } Track* t = timeline_->GetTrackAt(track_index_); @@ -419,7 +419,7 @@ void TrackPlaceBlockCommand::undo() if (ripple_remove_command_) { // If we ripple removed, just undo that - ripple_remove_command_->undo(); + ripple_remove_command_->undo_now(); } else if (gap_) { t->RippleRemoveBlock(gap_); gap_->setParent(&memory_manager_); @@ -432,7 +432,7 @@ void TrackPlaceBlockCommand::undo() // Remove tracks if we added them for (int i=add_track_commands_.size()-1; i>=0; i--) { - add_track_commands_.at(i)->undo(); + add_track_commands_.at(i)->undo_now(); } } diff --git a/app/widget/timelinewidget/undo/timelineundopointer.h b/app/widget/timelinewidget/undo/timelineundopointer.h index d3abf3a6f..fc7cf6566 100644 --- a/app/widget/timelinewidget/undo/timelineundopointer.h +++ b/app/widget/timelinewidget/undo/timelineundopointer.h @@ -82,6 +82,7 @@ public: remove_block_from_graph_ = e; } +protected: virtual void prepare() override; virtual void redo() override; virtual void undo() override; @@ -134,6 +135,7 @@ public: return track_->project(); } +protected: virtual void prepare() override; virtual void redo() override; @@ -182,6 +184,7 @@ public: return timeline_->parent()->project(); } +protected: virtual void redo() override; virtual void undo() override; diff --git a/app/widget/timelinewidget/undo/timelineundoripple.cpp b/app/widget/timelinewidget/undo/timelineundoripple.cpp index d8716c2e2..4d8ff9fb3 100644 --- a/app/widget/timelinewidget/undo/timelineundoripple.cpp +++ b/app/widget/timelinewidget/undo/timelineundoripple.cpp @@ -141,7 +141,7 @@ void TrackRippleRemoveAreaCommand::redo() } foreach (UndoCommand* c, remove_block_commands_) { - c->redo(); + c->redo_now(); } } } @@ -169,7 +169,7 @@ void TrackRippleRemoveAreaCommand::undo() // Un-remove any blocks for (int i=remove_block_commands_.size()-1; i>=0; i--) { - remove_block_commands_.at(i)->undo(); + remove_block_commands_.at(i)->undo_now(); } foreach (auto op, removals_) { @@ -219,7 +219,7 @@ void TrackListRippleRemoveAreaCommand::redo() } foreach (TrackRippleRemoveAreaCommand* c, commands_) { - c->redo(); + c->redo_now(); } if (all_tracks_unlocked_) { @@ -246,7 +246,7 @@ void TrackListRippleRemoveAreaCommand::undo() } foreach (TrackRippleRemoveAreaCommand* c, commands_) { - c->undo(); + c->undo_now(); } if (all_tracks_unlocked_) { @@ -496,7 +496,7 @@ void TimelineRippleDeleteGapsAtRegionsCommand::redo() } foreach (UndoCommand* c, commands_) { - c->redo(); + c->redo_now(); } } diff --git a/app/widget/timelinewidget/undo/timelineundoripple.h b/app/widget/timelinewidget/undo/timelineundoripple.h index c98cdc03b..9164ce9f3 100644 --- a/app/widget/timelinewidget/undo/timelineundoripple.h +++ b/app/widget/timelinewidget/undo/timelineundoripple.h @@ -66,6 +66,7 @@ public: return nullptr; } +protected: virtual void prepare() override; virtual void redo() override; @@ -115,6 +116,7 @@ public: return list_->parent()->project(); } +protected: virtual void redo() override; virtual void undo() override; @@ -163,6 +165,7 @@ public: return track_list_->parent()->project(); } +protected: virtual void redo() override { ripple(true); @@ -215,12 +218,13 @@ public: return timeline_->project(); } +protected: virtual void redo() override; virtual void undo() override { for (int i=commands_.size()-1;i>=0;i--) { - commands_.at(i)->undo(); + commands_.at(i)->undo_now(); } } diff --git a/app/widget/timelinewidget/undo/timelineundosplit.cpp b/app/widget/timelinewidget/undo/timelineundosplit.cpp index aff11d099..637b1e0a7 100644 --- a/app/widget/timelinewidget/undo/timelineundosplit.cpp +++ b/app/widget/timelinewidget/undo/timelineundosplit.cpp @@ -60,7 +60,7 @@ void BlockSplitCommand::redo() if (!position_command_) { position_command_ = new NodeSetPositionAsChildCommand(new_block(), track, track, new_block()->index(), track->Blocks().size(), true); } - position_command_->redo(); + position_command_->redo_now(); // If the block had an out transition, we move it to the new block moved_transition_ = NodeInput(); @@ -91,7 +91,7 @@ void BlockSplitCommand::undo() Node::ConnectEdge(block_, moved_transition_); } - position_command_->undo(); + position_command_->undo_now(); block_->set_length_and_media_out(old_length_); track->RippleRemoveBlock(new_block()); @@ -152,7 +152,7 @@ void BlockSplitPreservingLinksCommand::redo() foreach (const QVector& split_list, split_blocks) { NodeLinkCommand* blc = new NodeLinkCommand(split_list.at(i), split_list.at(j), true); - blc->redo(); + blc->redo_now(); commands_.append(blc); } } @@ -160,7 +160,7 @@ void BlockSplitPreservingLinksCommand::redo() } } else { for (int i=0; iredo(); + commands_.at(i)->redo_now(); } } } diff --git a/app/widget/timelinewidget/undo/timelineundosplit.h b/app/widget/timelinewidget/undo/timelineundosplit.h index 6e0c1e6d6..82b57ebca 100644 --- a/app/widget/timelinewidget/undo/timelineundosplit.h +++ b/app/widget/timelinewidget/undo/timelineundosplit.h @@ -97,7 +97,7 @@ public: virtual void undo() override { for (int i=commands_.size()-1; i>=0; i--) { - commands_.at(i)->undo(); + commands_.at(i)->undo_now(); } } @@ -134,14 +134,14 @@ public: virtual void redo() override { if (command_) { - command_->redo(); + command_->redo_now(); } } virtual void undo() override { if (command_) { - command_->undo(); + command_->undo_now(); } } diff --git a/app/widget/timelinewidget/undo/timelineundotrack.h b/app/widget/timelinewidget/undo/timelineundotrack.h index 8e6706f7c..ca2587b5a 100644 --- a/app/widget/timelinewidget/undo/timelineundotrack.h +++ b/app/widget/timelinewidget/undo/timelineundotrack.h @@ -39,6 +39,7 @@ public: return track_->project(); } +protected: virtual void redo() override { before_ = block_->previous(); @@ -73,6 +74,7 @@ public: return track_->project(); } +protected: virtual void redo() override { track_->PrependBlock(block_); @@ -103,6 +105,7 @@ public: return block_->project(); } +protected: virtual void redo() override { track_->InsertBlockAfter(block_, before_); @@ -141,6 +144,7 @@ public: return track_->project(); } +protected: virtual void redo() override { track_->ReplaceBlock(old_, replace_); diff --git a/app/widget/timelinewidget/undo/timelineundoworkarea.h b/app/widget/timelinewidget/undo/timelineundoworkarea.h index b44437a47..c431178ec 100644 --- a/app/widget/timelinewidget/undo/timelineundoworkarea.h +++ b/app/widget/timelinewidget/undo/timelineundoworkarea.h @@ -41,6 +41,7 @@ public: return project_; } +protected: virtual void redo() override { points_->workarea()->set_enabled(new_enabled_); @@ -77,6 +78,7 @@ public: return project_; } +protected: virtual void redo() override { points_->workarea()->set_range(new_range_); diff --git a/app/window/mainwindow/mainwindowundo.h b/app/window/mainwindow/mainwindowundo.h index 6e048f283..51bc52c64 100644 --- a/app/window/mainwindow/mainwindowundo.h +++ b/app/window/mainwindow/mainwindowundo.h @@ -32,12 +32,13 @@ public: sequence_(sequence) {} + virtual Project* GetRelevantProject() const override {return nullptr;} + +protected: virtual void redo() override; virtual void undo() override; - virtual Project* GetRelevantProject() const override {return nullptr;} - private: Sequence* sequence_; @@ -50,12 +51,13 @@ public: sequence_(sequence) {} + virtual Project* GetRelevantProject() const override {return nullptr;} + +protected: virtual void redo() override; virtual void undo() override; - virtual Project* GetRelevantProject() const override {return nullptr;} - private: Sequence* sequence_; diff --git a/tests/timeline/timeline-tests.cpp b/tests/timeline/timeline-tests.cpp index 17df4ee03..0f0c266b9 100644 --- a/tests/timeline/timeline-tests.cpp +++ b/tests/timeline/timeline-tests.cpp @@ -135,14 +135,14 @@ OLIVE_ADD_TEST(Trim) { // Trim out point of second block BlockTrimCommand command(track, block2, 1, Timeline::kTrimOut); - command.redo(); + command.redo_now(); // No block should have been added OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 1); OLIVE_ASSERT(block1->length() == 2); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 2); @@ -152,7 +152,7 @@ OLIVE_ADD_TEST(Trim) { // Trim in point of second block BlockTrimCommand command(track, block2, 1, Timeline::kTrimIn); - command.redo(); + command.redo_now(); // Gap should be inserted in between OLIVE_ASSERT(track->Blocks().size() == 3); @@ -164,7 +164,7 @@ OLIVE_ADD_TEST(Trim) OLIVE_ASSERT(block1->next() == gap); OLIVE_ASSERT(block2->previous() == gap); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 2); @@ -174,7 +174,7 @@ OLIVE_ADD_TEST(Trim) { // Trim out point of first block BlockTrimCommand command(track, block1, 1, Timeline::kTrimOut); - command.redo(); + command.redo_now(); // Gap should be inserted in between OLIVE_ASSERT(track->Blocks().size() == 3); @@ -186,7 +186,7 @@ OLIVE_ADD_TEST(Trim) OLIVE_ASSERT(block1->next() == gap); OLIVE_ASSERT(block2->previous() == gap); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 2); @@ -196,7 +196,7 @@ OLIVE_ADD_TEST(Trim) { // Trim in point of first block BlockTrimCommand command(track, block1, 1, Timeline::kTrimIn); - command.redo(); + command.redo_now(); // Gap should be prepended to the start OLIVE_ASSERT(track->Blocks().size() == 3); @@ -208,7 +208,7 @@ OLIVE_ADD_TEST(Trim) OLIVE_ASSERT(block1->next() == block2); OLIVE_ASSERT(block1->previous() == gap); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 2); @@ -241,7 +241,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsOnly) { // Replace clip C with a gap TrackReplaceBlockWithGapCommand command(track, c); - command.redo(); + command.redo_now(); // Clip should be removed without any gap actually taking its place, since the clip is at the // end of the track @@ -249,7 +249,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsOnly) OLIVE_ASSERT(track->Blocks().at(0) == a); OLIVE_ASSERT(track->Blocks().at(1) == b); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 3); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -260,7 +260,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsOnly) { // Replace clip B with a gap TrackReplaceBlockWithGapCommand command(track, b); - command.redo(); + command.redo_now(); // B should be replaced with a gap OLIVE_ASSERT(track->Blocks().size() == 3); @@ -270,7 +270,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsOnly) OLIVE_ASSERT(track->Blocks().at(1)->length() == b->length()); OLIVE_ASSERT(track->Blocks().at(2) == c); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 3); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -312,7 +312,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) { // Replace clip E with a gap TrackReplaceBlockWithGapCommand command(track, e); - command.redo(); + command.redo_now(); // Both clips D and E should be removed because this command should remove any trailing gaps OLIVE_ASSERT(track->Blocks().size() == 3); @@ -321,7 +321,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) OLIVE_ASSERT(track->Blocks().at(2) == c); // Test undo - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 5); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -337,7 +337,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) rational original_length_of_b = b->length(); TrackReplaceBlockWithGapCommand command(track, a); - command.redo(); + command.redo_now(); // A should be removed and B should take its place OLIVE_ASSERT(track->Blocks().size() == 4); @@ -349,7 +349,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) OLIVE_ASSERT(b->length() == original_length_of_a + original_length_of_b); // Test undo - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 5); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -368,7 +368,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) rational original_length_of_d = d->length(); TrackReplaceBlockWithGapCommand command(track, c); - command.redo(); + command.redo_now(); // C and D should be removed, and B should take both of their places OLIVE_ASSERT(track->Blocks().size() == 3); @@ -378,7 +378,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) OLIVE_ASSERT(b->length() == original_length_of_b + original_length_of_c + original_length_of_d); // Test undo - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 5); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -401,7 +401,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) TrackReplaceBlockWithGapCommand command(track, e); rational original_length_of_d = d->length(); rational original_length_of_e = e->length(); - command.redo(); + command.redo_now(); // E should be removed and D should have taken its place OLIVE_ASSERT(track->Blocks().size() == 5); @@ -412,7 +412,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) OLIVE_ASSERT(track->Blocks().at(4) == f); OLIVE_ASSERT(d->length() == original_length_of_d + original_length_of_e); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 6); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -466,7 +466,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndTransitions) { // Replace A with gap TrackReplaceBlockWithGapCommand command(track, a); - command.redo(); + command.redo_now(); // A should be replaced with a gap and so should A_IN since A was the only clip connected to it. // Also A_TO_B should only be connected to B now @@ -476,7 +476,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndTransitions) OLIVE_ASSERT(track->Blocks().at(2) == b); OLIVE_ASSERT(track->Blocks().at(3) == b_out); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 5); OLIVE_ASSERT(track->Blocks().at(0) == a_in); @@ -518,7 +518,7 @@ OLIVE_ADD_TEST(InsertGaps_SingleTrack) { // Insert gap at the start of the track, all blocks should be unsplit and shifted to the right TrackListInsertGaps command(list, 0, 2); - command.redo(); + command.redo_now(); OLIVE_ASSERT(track->Blocks().size() == 4); OLIVE_ASSERT(dynamic_cast(track->Blocks().at(0))); @@ -527,7 +527,7 @@ OLIVE_ADD_TEST(InsertGaps_SingleTrack) OLIVE_ASSERT(track->Blocks().at(2) == b); OLIVE_ASSERT(track->Blocks().at(3) == c); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 3); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -538,7 +538,7 @@ OLIVE_ADD_TEST(InsertGaps_SingleTrack) { // Insert gap in the middle of block A, block A should be halved with a copy at 2 and the gap at 1 TrackListInsertGaps command(list, rational(1, 2), 2); - command.redo(); + command.redo_now(); OLIVE_ASSERT(track->Blocks().size() == 5); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -548,7 +548,7 @@ OLIVE_ADD_TEST(InsertGaps_SingleTrack) OLIVE_ASSERT(track->Blocks().at(3) == b); OLIVE_ASSERT(track->Blocks().at(4) == c); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 3); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -560,7 +560,7 @@ OLIVE_ADD_TEST(InsertGaps_SingleTrack) { // Insert gap between block A and B, blocks should be unsplit with a gap at 1 TrackListInsertGaps command(list, 1, 2); - command.redo(); + command.redo_now(); OLIVE_ASSERT(track->Blocks().size() == 4); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -568,7 +568,7 @@ OLIVE_ADD_TEST(InsertGaps_SingleTrack) OLIVE_ASSERT(track->Blocks().at(2) == b); OLIVE_ASSERT(track->Blocks().at(3) == c); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 3); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -579,14 +579,14 @@ OLIVE_ADD_TEST(InsertGaps_SingleTrack) { // Insert gap at end, nothing should be added TrackListInsertGaps command(list, 3, 2); - command.redo(); + command.redo_now(); OLIVE_ASSERT(track->Blocks().size() == 3); OLIVE_ASSERT(track->Blocks().at(0) == a); OLIVE_ASSERT(track->Blocks().at(1) == b); OLIVE_ASSERT(track->Blocks().at(2) == c); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 3); OLIVE_ASSERT(track->Blocks().at(0) == a); From 59e228439fa073a81ebad575c0e191d2d8a88ee2 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 18 Jul 2021 11:05:33 -0700 Subject: [PATCH 68/72] cache: ignore shifts beyond map size Fixes crash --- app/render/framehashcache.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index 6317e4a7a..5f0b9a921 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -256,6 +256,10 @@ void FrameHashCache::ShiftEvent(const rational &from, const rational &to) int64_t to_ts = ToTimestamp(to); int64_t from_ts = ToTimestamp(from); + if (from_ts >= GetMapSize()) { + return; + } + if (diff_is_negative) { // We're moving the frames starting at `from` backwards to where `to` is if (to_ts < GetMapSize()) { From a8abe5167ce9532c8d99bdc44f9e629388581179 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 19 Jul 2021 01:44:04 -0700 Subject: [PATCH 69/72] nodeview: calculate triangle size from title bar rect --- app/widget/nodeview/nodeviewitem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index 0b53720e4..c3d75cb66 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -347,7 +347,7 @@ void NodeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti // Draw output triangle painter->setPen(Qt::NoPen); painter->setBrush(app_pal.color(QPalette::Text)); - int triangle_sz = qMin(rect().width(), rect().height()) / 2; + int triangle_sz = title_bar_rect_.height() / 2; int triangle_sz_half = triangle_sz / 2; switch (flow_dir_) { From 9c63393f5004caa072d0bc794f77c12d80b9c8b3 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 19 Jul 2021 02:27:40 -0700 Subject: [PATCH 70/72] nodeview: implemented workable solution to connecting contexts together to show all Functional, now we just need to fine tune and optimize it. --- app/widget/nodeview/nodeview.cpp | 335 +++++++++++++++++++++---------- app/widget/nodeview/nodeview.h | 6 + 2 files changed, 233 insertions(+), 108 deletions(-) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 22e8520e4..01d55d6da 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -27,6 +27,8 @@ #include "core.h" #include "nodeviewundo.h" +#include "node/audio/volume/volume.h" +#include "node/distort/transform/transformdistortnode.h" #include "node/factory.h" #include "node/traverser.h" #include "widget/menu/menushared.h" @@ -71,6 +73,10 @@ NodeView::NodeView(QWidget *parent) : connect(minimap_, &NodeViewMiniMap::MoveToScenePoint, this, &NodeView::MoveToScenePoint); connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &NodeView::UpdateViewportOnMiniMap); connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &NodeView::UpdateViewportOnMiniMap); + + reposition_contexts_timer_.setInterval(1); + reposition_contexts_timer_.setSingleShot(true); + connect(&reposition_contexts_timer_, &QTimer::timeout, this, &NodeView::RepositionContexts); } NodeView::~NodeView() @@ -95,6 +101,7 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) DeselectAll(); positions_.clear(); scene_.clear(); + context_offsets_.clear(); } // Handle graph change @@ -127,47 +134,12 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) } if (refresh_required && nodes_visible) { - if (filter_mode_ == kFilterShowAll) { - // Determine which contexts in the graph have no outputs, these are considered "root-level" - filter_nodes_.clear(); - for (auto it=graph->GetPositionMap().cbegin(); it!=graph->GetPositionMap().cend(); it++) { - Node *context = it.key(); - if (context->output_connections().empty()) { - filter_nodes_.append(context); - } - } + // Just make the filter nodes all of the graph's contexts + filter_nodes_ = graph->GetPositionMap().keys().toVector(); } - // Standard root-level positioning code - qreal last_offset = 0; - int additional_spacing = 0; - - // Contexts may be added to this later, so to ensure we only process the root-level nodes we - // found above, we'll store the current size and stop iterating after that - int sz = filter_nodes_.size(); - for (int i=0; iGetNodesForContext(n); - - // First determine the total "height" of this graph and how much we need to offset it - qreal top = 0; - qreal bottom = 0; - for (auto it=map.cbegin(); it!=map.cend(); it++) { - const QPointF &node_pos_in_context = it.value(); - top = qMin(node_pos_in_context.y(), top); - bottom = qMax(node_pos_in_context.y(), bottom); - } - - last_offset += (additional_spacing + (bottom - top)); - additional_spacing = 1; - context_offsets_.insert(n, QPointF(0, last_offset)); - - // Finally add all nodes - for (auto it=map.cbegin(); it!=map.cend(); it++) { - AddNodePosition(it.key(), n); - } - } + RepositionContexts(); // Center on something QMetaObject::invokeMethod(this, &NodeView::CenterOnItemsBoundingRect, Qt::QueuedConnection); @@ -219,7 +191,7 @@ void NodeView::DeleteSelected() } if (!selected_nodes.isEmpty()) { - foreach (Node* node, selected_nodes) { + for (Node* node : selected_nodes) { command->add_child(new NodeRemoveAndDisconnectCommand(node)); } } @@ -248,7 +220,7 @@ void NodeView::SelectAll() } else { // We have to determine the difference QVector new_selection; - foreach (Node* n, graph_->nodes()) { + for (Node* n : graph_->nodes()) { if (!selected_nodes_.contains(n)) { new_selection.append(n); } @@ -300,7 +272,7 @@ void NodeView::Select(QVector nodes, bool center_view_on_item) NodeViewItem *first_item = nullptr; - foreach (Node* n, nodes) { + for (Node* n : nodes) { if (processed.contains(n)) { continue; } @@ -420,7 +392,7 @@ void NodeView::Duplicate() void NodeView::SetColorLabel(int index) { - foreach (Node* node, selected_nodes_) { + for (Node* node : selected_nodes_) { node->SetOverrideColor(index); } } @@ -445,8 +417,8 @@ void NodeView::keyPressEvent(QKeyEvent *event) { if (graph_) { MultiUndoCommand *pos_command = new MultiUndoCommand(); - foreach (Node *n, selected_nodes_) { - foreach (Node *context, filter_nodes_) { + for (Node *n : selected_nodes_) { + for (Node *context : filter_nodes_) { if (graph_->GetNodesForContext(context).contains(n)) { QPointF old_pos = graph_->GetNodePosition(n, context); @@ -511,7 +483,7 @@ void NodeView::mousePressEvent(QMouseEvent *event) // See if we're dragging the arrow of an edge QPointF scene_pt = mapToScene(event->pos()); - foreach (NodeViewEdge *edge_item, scene_.edges()) { + for (NodeViewEdge *edge_item : scene_.edges()) { if (edge_item->arrow_bounding_rect().contains(scene_pt)) { create_edge_src_ = scene_.NodeToUIObject(edge_item->output().node()); create_edge_src_output_ = edge_item->output().output(); @@ -522,7 +494,7 @@ void NodeView::mousePressEvent(QMouseEvent *event) } // See if we're dragging the arrow of a node - foreach (NodeViewItem *node_item, scene_.item_map()) { + for (NodeViewItem *node_item : scene_.item_map()) { if (node_item->GetOutputTriangle().boundingRect().translated(node_item->pos()).contains(scene_pt)) { CreateNewEdge(node_item); return; @@ -648,7 +620,7 @@ void NodeView::mouseMoveEvent(QMouseEvent *event) NodeViewEdge* new_drop_edge = nullptr; // See if there is an edge here - foreach (QGraphicsItem* item, items) { + for (QGraphicsItem* item : items) { new_drop_edge = dynamic_cast(item); if (new_drop_edge) { @@ -665,7 +637,7 @@ void NodeView::mouseMoveEvent(QMouseEvent *event) // Iterate through the inputs of our dragging node and see if our node has any acceptable // inputs to connect to for this type - foreach (const QString& input, attached_node->inputs()) { + for (const QString& input : attached_node->inputs()) { NodeInput i(attached_node, input); if (attached_node->IsInputConnectable(input)) { @@ -802,7 +774,7 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) if (pos_data.original_item_pos != current_item_pos) { QPointF diff = current_item_pos - pos_data.original_item_pos; - foreach (Node *context, filter_nodes_) { + for (Node *context : filter_nodes_) { if (graph_->ContextContainsNode(node, context)) { QPointF current_node_pos_in_context = graph_->GetNodePosition(node, context); current_node_pos_in_context += diff; @@ -852,13 +824,13 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) // Remove from context any nodes that don't specifically output to said context MultiUndoCommand *remove_pos_command = new MultiUndoCommand(); - foreach (const AttachedItem &attached, attached_items_) { + for (const AttachedItem &attached : attached_items_) { MultiUndoCommand *remove_pos_subcommand = new MultiUndoCommand(); Node *attached_node = scene_.item_map().key(attached.item); bool removed = false; QVector relevant_contexts; - foreach (Node *context, filter_nodes_) { + for (Node *context : filter_nodes_) { if (attached_node->OutputsTo(context, true)) { relevant_contexts.append(context); } else { @@ -868,7 +840,7 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) } if (removed && !relevant_contexts.isEmpty()) { - foreach (Node *relevant, relevant_contexts) { + for (Node *relevant : relevant_contexts) { remove_pos_subcommand->add_child(new NodeSetPositionCommand(attached_node, relevant, GetEstimatedPositionForContext(attached.item, relevant), false)); } @@ -913,7 +885,7 @@ void NodeView::UpdateSelectionCache() // All nodes in the current selection have just been selected selected = current_selection; } else { - foreach (Node* n, current_selection) { + for (Node* n : current_selection) { if (!selected_nodes_.contains(n)) { selected.append(n); } @@ -925,7 +897,7 @@ void NodeView::UpdateSelectionCache() // All nodes that were selected have been deselected deselected = selected_nodes_; } else { - foreach (Node* n, selected_nodes_) { + for (Node* n : selected_nodes_) { if (!current_selection.contains(n)) { deselected.append(n); } @@ -1043,7 +1015,7 @@ void NodeView::CreateNodeSlot(QAction *action) if (new_node) { paste_command_ = new MultiUndoCommand(); paste_command_->add_child(new NodeAddCommand(graph_, new_node)); - foreach (Node *context, filter_nodes_) { + for (Node *context : filter_nodes_) { paste_command_->add_child(new NodeSetPositionCommand(new_node, context, QPointF(0, 0), false)); } paste_command_->add_child(new NodeViewAttachNodesToCursor(this, {new_node})); @@ -1062,7 +1034,7 @@ void NodeView::AutoPositionDescendents() { QVector selected = scene_.GetSelectedNodes(); - foreach (Node* n, selected) { + for (Node* n : selected) { scene_.ReorganizeFrom(n); } } @@ -1129,52 +1101,25 @@ void NodeView::RemoveEdge(const NodeOutput &output, const NodeInput &input) void NodeView::AddNodePosition(Node *node, Node *relative) { - if (filter_nodes_.contains(relative)) { - // Get UI item or create if it doesn't exist - NodeViewItem *item = scene_.item_map().value(node); - if (!item) { - item = scene_.AddNode(node); + bool listening_to_node = filter_nodes_.contains(relative); - for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { - if (scene_.item_map().contains(it->second.node())) { - scene_.AddEdge(it->second, it->first); - } - } - - for (auto it=node->output_connections().cbegin(); it!=node->output_connections().cend(); it++) { - if (scene_.item_map().contains(it->second.node())) { - scene_.AddEdge(it->first, it->second); - } - } + if (!listening_to_node) { + if (filter_mode_ == kFilterShowAll) { + // We're not listening to this context, but because we're showing all, add it + filter_nodes_.append(relative); + } else { + // Ignore signal + return; } + } - // Determine "view" position by averaging the Y value and "min"ing the X value of all contexts - QPointF item_pos(DBL_MAX, 0.0); - int average_count = 0; - foreach (Node *context, filter_nodes_) { - if (graph_->GetNodesForContext(context).contains(node)) { - QPointF this_context_pos = graph_->GetNodePosition(node, context); - this_context_pos += context_offsets_.value(context); - item_pos.setX(qMin(item_pos.x(), this_context_pos.x())); - item_pos.setY(item_pos.y() + this_context_pos.y()); - average_count++; - } - } - item_pos.setY(item_pos.y() / average_count); + // Reposition contexts because one of their heights may have changed or a new one may have been + // added + UpdateNodeItem(node); - // Set position - item->SetNodePosition(item_pos); - positions_.insert(item, {node, item_pos}); - - // For "show all" mode, we recursively add more nodes to the graph - if (filter_mode_ == kFilterShowAll && !filter_nodes_.contains(node) && graph_->GetPositionMap().contains(node)) { - filter_nodes_.append(node); - context_offsets_.insert(node, item_pos); - const NodeGraph::PositionMap &map = graph_->GetNodesForContext(node); - for (auto it=map.cbegin(); it!=map.cend(); it++) { - AddNodePosition(it.key(), node); - } - } + if (filter_mode_ == kFilterShowAll) { + reposition_contexts_timer_.stop(); + reposition_contexts_timer_.start(); } } @@ -1187,7 +1132,7 @@ void NodeView::RemoveNodePosition(Node *node, Node *relative) // Determine if any other contexts have this node bool found = false; - foreach (Node *context, filter_nodes_) { + for (Node *context : filter_nodes_) { if (graph_->ContextContainsNode(node, context)) { found = true; break; @@ -1195,7 +1140,7 @@ void NodeView::RemoveNodePosition(Node *node, Node *relative) } if (!found) { - foreach (const Node::OutputConnection &oc, node->output_connections()) { + for (const Node::OutputConnection &oc : node->output_connections()) { scene_.RemoveEdge(oc.first, oc.second); } for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { @@ -1205,6 +1150,10 @@ void NodeView::RemoveNodePosition(Node *node, Node *relative) scene_.RemoveNode(node); } } + + if (filter_mode_ == kFilterShowAll) { + RepositionContexts(); + } } } @@ -1275,7 +1224,7 @@ void NodeView::AttachItemsToCursor(const QVector& items) DetachItemsFromCursor(); if (!items.isEmpty()) { - foreach (NodeViewItem* i, items) { + for (NodeViewItem* i : items) { attached_items_.append({i, i->pos() - items.first()->pos()}); } @@ -1297,7 +1246,7 @@ void NodeView::MoveAttachedNodesToCursor(const QPoint& p) { QPointF item_pos = mapToScene(p); - foreach (const AttachedItem& i, attached_items_) { + for (const AttachedItem& i : attached_items_) { i.item->setPos(item_pos + i.original_pos); } } @@ -1362,7 +1311,7 @@ void NodeView::ZoomFromKeyboard(double multiplier) bool NodeView::DetermineIfNodeIsFloatingInContext(Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge) { // Determines whether `node` outputs to another node in `context` besides `source` - foreach (const Node::OutputConnection &conn, node->output_connections()) { + for (const Node::OutputConnection &conn : node->output_connections()) { Node *output_candidate = conn.second.node(); if (output_candidate == source) { @@ -1382,7 +1331,7 @@ bool NodeView::DetermineIfNodeIsFloatingInContext(Node *node, Node *context, Nod void NodeView::UpdateContextsFromEdgeRemove(MultiUndoCommand *command, const Node::OutputConnections &remove_edges) { // For each edge we remove, determine if we should remove the node from a context as well - foreach (const Node::OutputConnection &edge, remove_edges) { + for (const Node::OutputConnection &edge : remove_edges) { Node *output_node = edge.first.node(); QVector contexts_to_remove_from; int contexts_containing = 0; @@ -1408,7 +1357,7 @@ void NodeView::UpdateContextsFromEdgeRemove(MultiUndoCommand *command, const Nod // Not removing from all contexts, can remove bool removing_from_all_current_contexts = true; - foreach (Node *context, filter_nodes_) { + for (Node *context : filter_nodes_) { if (graph_->ContextContainsNode(output_node, context)) { if (!contexts_to_remove_from.contains(context)) { removing_from_all_current_contexts = false; @@ -1417,7 +1366,7 @@ void NodeView::UpdateContextsFromEdgeRemove(MultiUndoCommand *command, const Nod } } - foreach (Node *context, contexts_to_remove_from) { + for (Node *context : contexts_to_remove_from) { RecursivelyRemoveFloatingNodeFromContext(command, output_node, context, output_node, remove_edges, Node::OutputConnection(), removing_from_all_current_contexts); } } @@ -1485,13 +1434,13 @@ void NodeView::UpdateContextsFromEdgeAdd(MultiUndoCommand *command, const Node:: if (node_is_floating) { // This action will unfloat this node, so remove it from all current contexts - foreach (Node *context, current_contexts) { + for (Node *context : current_contexts) { RecursivelyRemoveFloatingNodeFromContext(command, connecting_node, context, connecting_node, removed_edges, added_edge, false); } } // Add nodes to contexts - foreach (Node *context, contexts_to_add_to) { + for (Node *context : contexts_to_add_to) { RecursivelyAddNodeToContext(command, connecting_node, context); } } @@ -1523,6 +1472,176 @@ void NodeView::CreateNewEdge(NodeViewItem *output_item) scene_.addItem(create_edge_); } +void NodeView::RepositionContexts() +{ + // Determine which contexts are root-level + QVector root_level_nodes; + QVector non_root_level_nodes; + + for (Node *context : filter_nodes_) { + bool is_root_level = true; + + for (Node *context2 : filter_nodes_) { + if (context != context2 && graph_->ContextContainsNode(context, context2)) { + is_root_level = false; + break; + } + } + + if (is_root_level) { + root_level_nodes.append(context); + } else { + non_root_level_nodes.append(context); + } + } + + { + // Position root-level nodes + qreal last_offset = 0; + int additional_spacing = 0; + + for (Node *n : root_level_nodes) { + const NodeGraph::PositionMap &map = graph_->GetNodesForContext(n); + + // First determine the total "height" of this graph and how much we need to offset it + qreal top = 0; + qreal bottom = 0; + for (auto it=map.cbegin(); it!=map.cend(); it++) { + const QPointF &node_pos_in_context = it.value(); + top = qMin(node_pos_in_context.y(), top); + bottom = qMax(node_pos_in_context.y(), bottom); + } + + last_offset += (additional_spacing + (bottom - top)); + additional_spacing = 1; + context_offsets_.insert(n, QPointF(0, last_offset)); + } + } + + { + // Position non-root-level nodes + const bool optimized = true; + + qint64 t = QDateTime::currentMSecsSinceEpoch(); + + if (optimized) { + + while (!non_root_level_nodes.isEmpty()) { + QVector next_level_nodes; + + for (int i=0; iContextContainsNode(context, context2)) { + next_level = false; + break; + } + } + + if (next_level) { + next_level_nodes.append(context); + non_root_level_nodes.removeAt(i); + i--; + } + } + + for (Node *n : next_level_nodes) { + NodeViewItem *item = UpdateNodeItem(n); + + QPointF pos_in_context = graph_->GetNodesForContext(n).value(n); + + QPointF context_pos = item->GetNodePosition() - pos_in_context; + + context_offsets_.insert(n, context_pos); + } + } + + } else { + int iter = 0; + + while (true) { + bool changed = false; + + for (Node *n : non_root_level_nodes) { + NodeViewItem *item = UpdateNodeItem(n); + + QPointF pos_in_context = graph_->GetNodesForContext(n).value(n); + + QPointF context_pos = item->GetNodePosition() - pos_in_context; + + if (!context_offsets_.contains(n) || context_offsets_.value(n) != context_pos) { + context_offsets_.insert(n, context_pos); + changed = true; + } + } + + iter++; + + if (!changed) { + break; + } + } + } + + qDebug() << "Workflow took:" << (QDateTime::currentMSecsSinceEpoch() - t); + } + + { + // Position all other nodes + for (Node *context : filter_nodes_) { + const NodeGraph::PositionMap &map = graph_->GetNodesForContext(context); + for (auto it=map.cbegin(); it!=map.cend(); it++) { + UpdateNodeItem(it.key()); + } + } + } +} + +NodeViewItem *NodeView::UpdateNodeItem(Node *node) +{ + // Get UI item or create if it doesn't exist + NodeViewItem *item = scene_.item_map().value(node); + if (!item) { + item = scene_.AddNode(node); + + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + if (scene_.item_map().contains(it->second.node())) { + scene_.AddEdge(it->second, it->first); + } + } + + for (auto it=node->output_connections().cbegin(); it!=node->output_connections().cend(); it++) { + if (scene_.item_map().contains(it->second.node())) { + scene_.AddEdge(it->first, it->second); + } + } + } + + // Determine "view" position by averaging the Y value and "min"ing the X value of all contexts + QPointF item_pos(DBL_MAX, 0.0); + int average_count = 0; + for (Node *context : filter_nodes_) { + if (graph_->GetNodesForContext(context).contains(node)) { + QPointF this_context_pos = graph_->GetNodePosition(node, context); + this_context_pos += context_offsets_.value(context); + + item_pos.setX(qMin(item_pos.x(), this_context_pos.x())); + item_pos.setY(item_pos.y() + this_context_pos.y()); + average_count++; + } + } + item_pos.setY(item_pos.y() / average_count); + + // Set position + item->SetNodePosition(item_pos); + positions_.insert(item, {node, item_pos}); + + return item; +} + NodeView::NodeViewAttachNodesToCursor::NodeViewAttachNodesToCursor(NodeView *view, const QVector &nodes) : view_(view), nodes_(nodes) diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index dafb58812..99cbbaa1f 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -138,6 +138,8 @@ private: void CreateNewEdge(NodeViewItem *output_item); + NodeViewItem *UpdateNodeItem(Node *node); + class NodeViewAttachNodesToCursor : public UndoCommand { public: @@ -232,6 +234,8 @@ private: bool create_edge_already_exists_; + QTimer reposition_contexts_timer_; + static const double kMinimumScale; private slots: @@ -288,6 +292,8 @@ private slots: void MoveToScenePoint(const QPointF &pos); + void RepositionContexts(); + }; } From 8d72685eaa33e7f7f0f35fd859bc387ed798c3b5 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 19 Jul 2021 18:40:22 -0700 Subject: [PATCH 71/72] nodeview: stripped out auto-position function Hopefully this shouldn't be necessary anymore --- app/widget/nodeview/nodeview.cpp | 17 ------------- app/widget/nodeview/nodeview.h | 5 ---- app/widget/nodeview/nodeviewscene.cpp | 35 --------------------------- app/widget/nodeview/nodeviewscene.h | 2 -- 4 files changed, 59 deletions(-) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 01d55d6da..6f5972eb5 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -108,7 +108,6 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) if (graph_changed) { if (graph_) { // Disconnect from current graph - //disconnect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); disconnect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); disconnect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); disconnect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); @@ -120,7 +119,6 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) if (graph_) { // Connect to new graph - //connect(graph_, &NodeGraph::NodeAdded, this, &NodeView::AddNode); connect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); connect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); connect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); @@ -940,12 +938,6 @@ void NodeView::ShowContextMenu(const QPoint &pos) // Color menu MenuShared::instance()->AddColorCodingMenu(&m); - m.addSeparator(); - - // Auto-position action - QAction* autopos = m.addAction(tr("Auto-Position")); - connect(autopos, &QAction::triggered, this, &NodeView::AutoPositionDescendents); - ViewerOutput* viewer = dynamic_cast(selected.first()->GetNode()); if (viewer) { m.addSeparator(); @@ -1030,15 +1022,6 @@ void NodeView::ContextMenuSetDirection(QAction *action) SetFlowDirection(static_cast(action->data().toInt())); } -void NodeView::AutoPositionDescendents() -{ - QVector selected = scene_.GetSelectedNodes(); - - for (Node* n : selected) { - scene_.ReorganizeFrom(n); - } -} - void NodeView::ContextMenuFilterChanged(QAction *action) { FilterMode mode = static_cast(action->data().toInt()); diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 99cbbaa1f..a31f173c6 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -259,11 +259,6 @@ private slots: */ void ContextMenuSetDirection(QAction* action); - /** - * @brief Receiver for auto-position descendents menu action - */ - void AutoPositionDescendents(); - /** * @brief Receiver for the user changing the filter */ diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index 68b673561..e40db9ebe 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -247,41 +247,6 @@ NodeViewCommon::FlowDirection NodeViewScene::GetFlowDirection() const return direction_; } -void NodeViewScene::ReorganizeFrom(Node* n) -{ - /* - QVector immediates = n->GetImmediateDependencies(); - - if (immediates.isEmpty()) { - // Nothing to do - return; - } - - QPointF parent_pos = n->GetPosition(); - - int weight_count = DetermineWeight(n); - - qreal child_x = parent_pos.x() - 1.0; - qreal children_height = weight_count-1; - qreal children_y = parent_pos.y() - children_height * 0.5; - - int weight_counter = 0; - - foreach (Node* i, immediates) { - if (i->GetRoutesTo(n) == 1) { - int weight = DetermineWeight(i); - - i->SetPosition(QPointF(child_x, - children_y + weight_counter + (weight - 1) * 0.5)); - - weight_counter += weight; - - ReorganizeFrom(i); - } - } - */ -} - void NodeViewScene::SetEdgesAreCurved(bool curved) { if (curved_edges_ != curved) { diff --git a/app/widget/nodeview/nodeviewscene.h b/app/widget/nodeview/nodeviewscene.h index 53b74e074..e388af140 100644 --- a/app/widget/nodeview/nodeviewscene.h +++ b/app/widget/nodeview/nodeviewscene.h @@ -79,8 +79,6 @@ public: return curved_edges_; } - void ReorganizeFrom(Node* n); - public slots: /** * @brief Slot when a Node is added to a graph (SetGraph() connects this) From 9f9b007cbf14eec1b7a187fb2e666c9f80bf22d9 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 19 Jul 2021 18:40:56 -0700 Subject: [PATCH 72/72] nodeview: significantly improve nodeview "show all" algorithm --- app/widget/nodeview/nodeview.cpp | 249 ++++++++++++++----------------- app/widget/nodeview/nodeview.h | 7 +- 2 files changed, 115 insertions(+), 141 deletions(-) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 6f5972eb5..6f277e5a9 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -49,7 +49,8 @@ NodeView::NodeView(QWidget *parent) : create_edge_dst_temp_expanded_(false), paste_command_(nullptr), filter_mode_(kFilterShowSelective), - scale_(1.0) + scale_(1.0), + queue_reposition_contexts_(false) { setScene(&scene_); SetDefaultDragMode(RubberBandDrag); @@ -74,9 +75,7 @@ NodeView::NodeView(QWidget *parent) : connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &NodeView::UpdateViewportOnMiniMap); connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &NodeView::UpdateViewportOnMiniMap); - reposition_contexts_timer_.setInterval(1); - reposition_contexts_timer_.setSingleShot(true); - connect(&reposition_contexts_timer_, &QTimer::timeout, this, &NodeView::RepositionContexts); + viewport()->installEventFilter(this); } NodeView::~NodeView() @@ -88,7 +87,7 @@ NodeView::~NodeView() void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) { bool graph_changed = graph_ != graph; - bool context_changed = filter_nodes_ != nodes; + bool context_changed = last_set_filter_nodes_ != nodes; if (graph_changed || context_changed) { // Clear nodes if necessary @@ -127,8 +126,12 @@ void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) } } - if (context_changed && filter_mode_ == kFilterShowSelective) { - filter_nodes_ = nodes; + if (context_changed) { + last_set_filter_nodes_ = nodes; + + if (filter_mode_ == kFilterShowSelective) { + filter_nodes_ = nodes; + } } if (refresh_required && nodes_visible) { @@ -189,7 +192,7 @@ void NodeView::DeleteSelected() } if (!selected_nodes.isEmpty()) { - for (Node* node : selected_nodes) { + for (Node* node : qAsConst(selected_nodes)) { command->add_child(new NodeRemoveAndDisconnectCommand(node)); } } @@ -270,7 +273,7 @@ void NodeView::Select(QVector nodes, bool center_view_on_item) NodeViewItem *first_item = nullptr; - for (Node* n : nodes) { + for (Node* n : qAsConst(nodes)) { if (processed.contains(n)) { continue; } @@ -390,7 +393,7 @@ void NodeView::Duplicate() void NodeView::SetColorLabel(int index) { - for (Node* node : selected_nodes_) { + for (Node* node : qAsConst(selected_nodes_)) { node->SetOverrideColor(index); } } @@ -415,8 +418,8 @@ void NodeView::keyPressEvent(QKeyEvent *event) { if (graph_) { MultiUndoCommand *pos_command = new MultiUndoCommand(); - for (Node *n : selected_nodes_) { - for (Node *context : filter_nodes_) { + for (Node *n : qAsConst(selected_nodes_)) { + for (Node *context : qAsConst(filter_nodes_)) { if (graph_->GetNodesForContext(context).contains(n)) { QPointF old_pos = graph_->GetNodePosition(n, context); @@ -618,7 +621,7 @@ void NodeView::mouseMoveEvent(QMouseEvent *event) NodeViewEdge* new_drop_edge = nullptr; // See if there is an edge here - for (QGraphicsItem* item : items) { + for (QGraphicsItem* item : qAsConst(items)) { new_drop_edge = dynamic_cast(item); if (new_drop_edge) { @@ -772,7 +775,7 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) if (pos_data.original_item_pos != current_item_pos) { QPointF diff = current_item_pos - pos_data.original_item_pos; - for (Node *context : filter_nodes_) { + for (Node *context : qAsConst(filter_nodes_)) { if (graph_->ContextContainsNode(node, context)) { QPointF current_node_pos_in_context = graph_->GetNodePosition(node, context); current_node_pos_in_context += diff; @@ -822,13 +825,13 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) // Remove from context any nodes that don't specifically output to said context MultiUndoCommand *remove_pos_command = new MultiUndoCommand(); - for (const AttachedItem &attached : attached_items_) { + for (const AttachedItem &attached : qAsConst(attached_items_)) { MultiUndoCommand *remove_pos_subcommand = new MultiUndoCommand(); Node *attached_node = scene_.item_map().key(attached.item); bool removed = false; QVector relevant_contexts; - for (Node *context : filter_nodes_) { + for (Node *context : qAsConst(filter_nodes_)) { if (attached_node->OutputsTo(context, true)) { relevant_contexts.append(context); } else { @@ -838,7 +841,7 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) } if (removed && !relevant_contexts.isEmpty()) { - for (Node *relevant : relevant_contexts) { + for (Node *relevant : qAsConst(relevant_contexts)) { remove_pos_subcommand->add_child(new NodeSetPositionCommand(attached_node, relevant, GetEstimatedPositionForContext(attached.item, relevant), false)); } @@ -883,7 +886,7 @@ void NodeView::UpdateSelectionCache() // All nodes in the current selection have just been selected selected = current_selection; } else { - for (Node* n : current_selection) { + for (Node* n : qAsConst(current_selection)) { if (!selected_nodes_.contains(n)) { selected.append(n); } @@ -895,7 +898,7 @@ void NodeView::UpdateSelectionCache() // All nodes that were selected have been deselected deselected = selected_nodes_; } else { - for (Node* n : selected_nodes_) { + for (Node* n : qAsConst(selected_nodes_)) { if (!current_selection.contains(n)) { deselected.append(n); } @@ -1007,7 +1010,7 @@ void NodeView::CreateNodeSlot(QAction *action) if (new_node) { paste_command_ = new MultiUndoCommand(); paste_command_->add_child(new NodeAddCommand(graph_, new_node)); - for (Node *context : filter_nodes_) { + for (Node *context : qAsConst(filter_nodes_)) { paste_command_->add_child(new NodeSetPositionCommand(new_node, context, QPointF(0, 0), false)); } paste_command_->add_child(new NodeViewAttachNodesToCursor(this, {new_node})); @@ -1029,7 +1032,7 @@ void NodeView::ContextMenuFilterChanged(QAction *action) if (filter_mode_ != mode) { // Store temporary graph variables NodeGraph *graph = graph_; - QVector nodes = filter_nodes_; + QVector nodes = last_set_filter_nodes_; // Unset graph with current filter mode ClearGraph(); @@ -1101,8 +1104,8 @@ void NodeView::AddNodePosition(Node *node, Node *relative) UpdateNodeItem(node); if (filter_mode_ == kFilterShowAll) { - reposition_contexts_timer_.stop(); - reposition_contexts_timer_.start(); + queue_reposition_contexts_ = true; + viewport()->update(); } } @@ -1115,7 +1118,7 @@ void NodeView::RemoveNodePosition(Node *node, Node *relative) // Determine if any other contexts have this node bool found = false; - for (Node *context : filter_nodes_) { + for (Node *context : qAsConst(filter_nodes_)) { if (graph_->ContextContainsNode(node, context)) { found = true; break; @@ -1229,7 +1232,7 @@ void NodeView::MoveAttachedNodesToCursor(const QPoint& p) { QPointF item_pos = mapToScene(p); - for (const AttachedItem& i : attached_items_) { + for (const AttachedItem& i : qAsConst(attached_items_)) { i.item->setPos(item_pos + i.original_pos); } } @@ -1279,6 +1282,18 @@ bool NodeView::event(QEvent *event) return super::event(event); } +bool NodeView::eventFilter(QObject *object, QEvent *event) +{ + if (object == viewport() && event->type() == QEvent::Paint) { + if (queue_reposition_contexts_) { + RepositionContexts(); + queue_reposition_contexts_ = false; + } + } + + return super::eventFilter(object, event); +} + void NodeView::ZoomFromKeyboard(double multiplier) { QPoint cursor_pos = mapFromGlobal(QCursor::pos()); @@ -1340,7 +1355,7 @@ void NodeView::UpdateContextsFromEdgeRemove(MultiUndoCommand *command, const Nod // Not removing from all contexts, can remove bool removing_from_all_current_contexts = true; - for (Node *context : filter_nodes_) { + for (Node *context : qAsConst(filter_nodes_)) { if (graph_->ContextContainsNode(output_node, context)) { if (!contexts_to_remove_from.contains(context)) { removing_from_all_current_contexts = false; @@ -1349,7 +1364,7 @@ void NodeView::UpdateContextsFromEdgeRemove(MultiUndoCommand *command, const Nod } } - for (Node *context : contexts_to_remove_from) { + for (Node *context : qAsConst(contexts_to_remove_from)) { RecursivelyRemoveFloatingNodeFromContext(command, output_node, context, output_node, remove_edges, Node::OutputConnection(), removing_from_all_current_contexts); } } @@ -1417,13 +1432,13 @@ void NodeView::UpdateContextsFromEdgeAdd(MultiUndoCommand *command, const Node:: if (node_is_floating) { // This action will unfloat this node, so remove it from all current contexts - for (Node *context : current_contexts) { + for (Node *context : qAsConst(current_contexts)) { RecursivelyRemoveFloatingNodeFromContext(command, connecting_node, context, connecting_node, removed_edges, added_edge, false); } } // Add nodes to contexts - for (Node *context : contexts_to_add_to) { + for (Node *context : qAsConst(contexts_to_add_to)) { RecursivelyAddNodeToContext(command, connecting_node, context); } } @@ -1458,132 +1473,84 @@ void NodeView::CreateNewEdge(NodeViewItem *output_item) void NodeView::RepositionContexts() { // Determine which contexts are root-level - QVector root_level_nodes; - QVector non_root_level_nodes; + QVector processing_filters = filter_nodes_; - for (Node *context : filter_nodes_) { - bool is_root_level = true; + // Level counter as we iterate through the list a few times + int level = 0; - for (Node *context2 : filter_nodes_) { - if (context != context2 && graph_->ContextContainsNode(context, context2)) { - is_root_level = false; - break; - } - } + // Root-level positioning variables + qreal last_offset = 0; + int additional_spacing = 0; - if (is_root_level) { - root_level_nodes.append(context); - } else { - non_root_level_nodes.append(context); - } - } + while (!processing_filters.isEmpty()) { + QVector contexts_on_this_level; - { - // Position root-level nodes - qreal last_offset = 0; - int additional_spacing = 0; + for (int i=0; iGetNodesForContext(n); + // Determine if this context is on an upper level or not + bool this_level = true; + for (int j=0; j next_level_nodes; - - for (int i=0; iContextContainsNode(context, context2)) { - next_level = false; - break; - } - } - - if (next_level) { - next_level_nodes.append(context); - non_root_level_nodes.removeAt(i); - i--; - } - } - - for (Node *n : next_level_nodes) { - NodeViewItem *item = UpdateNodeItem(n); - - QPointF pos_in_context = graph_->GetNodesForContext(n).value(n); - - QPointF context_pos = item->GetNodePosition() - pos_in_context; - - context_offsets_.insert(n, context_pos); - } - } - - } else { - int iter = 0; - - while (true) { - bool changed = false; - - for (Node *n : non_root_level_nodes) { - NodeViewItem *item = UpdateNodeItem(n); - - QPointF pos_in_context = graph_->GetNodesForContext(n).value(n); - - QPointF context_pos = item->GetNodePosition() - pos_in_context; - - if (!context_offsets_.contains(n) || context_offsets_.value(n) != context_pos) { - context_offsets_.insert(n, context_pos); - changed = true; - } - } - - iter++; - - if (!changed) { + if (i != j && graph_->ContextContainsNode(context, other_context)) { + this_level = false; break; } } + + if (this_level) { + contexts_on_this_level.append(context); + } } - qDebug() << "Workflow took:" << (QDateTime::currentMSecsSinceEpoch() - t); + for (Node *context : qAsConst(contexts_on_this_level)) { + if (level == 0) { + const NodeGraph::PositionMap &map = graph_->GetNodesForContext(context); + + // First determine the total "height" of this graph and how much we need to offset it + qreal top = 0; + qreal bottom = 0; + for (auto it=map.cbegin(); it!=map.cend(); it++) { + const QPointF &node_pos_in_context = it.value(); + top = qMin(node_pos_in_context.y(), top); + bottom = qMax(node_pos_in_context.y(), bottom); + } + + last_offset += (additional_spacing + (bottom - top)); + additional_spacing = 1; + context_offsets_.insert(context, QPointF(0, last_offset)); + } else { + // Create/update item + NodeViewItem *item = UpdateNodeItem(context, true); + + // Get position generated by UpdateNodeItem + QPointF context_pos = item->GetNodePosition(); + + // Adjust by the context node's position in its own context (this will usually be 0,0) + context_pos -= graph_->GetNodesForContext(context).value(context); + + // Insert this context's offset + context_offsets_.insert(context, context_pos); + } + + // Remove from list so we don't process again + processing_filters.removeOne(context); + } + + level++; } - { - // Position all other nodes - for (Node *context : filter_nodes_) { - const NodeGraph::PositionMap &map = graph_->GetNodesForContext(context); - for (auto it=map.cbegin(); it!=map.cend(); it++) { - UpdateNodeItem(it.key()); - } + // Now that we've positioned all the contexts, position all other nodes relative to those contexts + for (Node *context : qAsConst(filter_nodes_)) { + const NodeGraph::PositionMap &map = graph_->GetNodesForContext(context); + for (auto it=map.cbegin(); it!=map.cend(); it++) { + UpdateNodeItem(it.key()); } } } -NodeViewItem *NodeView::UpdateNodeItem(Node *node) +NodeViewItem *NodeView::UpdateNodeItem(Node *node, bool ignore_own_context) { // Get UI item or create if it doesn't exist NodeViewItem *item = scene_.item_map().value(node); @@ -1606,7 +1573,11 @@ NodeViewItem *NodeView::UpdateNodeItem(Node *node) // Determine "view" position by averaging the Y value and "min"ing the X value of all contexts QPointF item_pos(DBL_MAX, 0.0); int average_count = 0; - for (Node *context : filter_nodes_) { + for (Node *context : qAsConst(filter_nodes_)) { + if (context == node && ignore_own_context) { + continue; + } + if (graph_->GetNodesForContext(context).contains(node)) { QPointF this_context_pos = graph_->GetNodePosition(node, context); this_context_pos += context_offsets_.value(context); diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index a31f173c6..402c643d3 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -110,6 +110,8 @@ protected: virtual bool event(QEvent *event) override; + virtual bool eventFilter(QObject *object, QEvent *event) override; + private: void AttachNodesToCursor(const QVector &nodes); @@ -138,7 +140,7 @@ private: void CreateNewEdge(NodeViewItem *output_item); - NodeViewItem *UpdateNodeItem(Node *node); + NodeViewItem *UpdateNodeItem(Node *node, bool ignore_own_context = false); class NodeViewAttachNodesToCursor : public UndoCommand { @@ -228,13 +230,14 @@ private: FilterMode filter_mode_; QVector filter_nodes_; + QVector last_set_filter_nodes_; QMap context_offsets_; double scale_; bool create_edge_already_exists_; - QTimer reposition_contexts_timer_; + bool queue_reposition_contexts_; static const double kMinimumScale;