From 7c7db5c9331b6b1e98a46d4871ed15dcabe9f374 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Sun, 27 Sep 2020 00:05:47 +0100 Subject: [PATCH 01/19] Check footage in use before deleteing Initial very hacky implementation that checks if the footgae is in use before deleting it and makes sure all refernences to it are properly cleaned up. Also added IsMedia() function to Node to simplify things a bit. --- app/node/input/media/audio/audio.cpp | 5 ++ app/node/input/media/audio/audio.h | 2 + app/node/input/media/media.cpp | 5 ++ app/node/input/media/media.h | 6 +++ app/node/input/media/video/video.cpp | 5 ++ app/node/input/media/video/video.h | 2 + app/node/node.cpp | 5 ++ app/node/node.h | 9 ++++ .../projectexplorer/projectexplorer.cpp | 51 +++++++++++++++++++ 9 files changed, 90 insertions(+) diff --git a/app/node/input/media/audio/audio.cpp b/app/node/input/media/audio/audio.cpp index 5f34543ec..49463aa22 100644 --- a/app/node/input/media/audio/audio.cpp +++ b/app/node/input/media/audio/audio.cpp @@ -27,6 +27,11 @@ Node *AudioInput::copy() const return new AudioInput(); } +Stream::Type AudioInput::type() const +{ + return Stream::kAudio; +} + QString AudioInput::Name() const { return tr("Audio Input"); diff --git a/app/node/input/media/audio/audio.h b/app/node/input/media/audio/audio.h index 62914b693..5cebdcc3c 100644 --- a/app/node/input/media/audio/audio.h +++ b/app/node/input/media/audio/audio.h @@ -32,6 +32,8 @@ public: virtual Node* copy() const override; + virtual Stream::Type type() const override; + virtual QString Name() const override; virtual QString ShortName() const override; virtual QString id() const override; diff --git a/app/node/input/media/media.cpp b/app/node/input/media/media.cpp index c0e1b3feb..8bfe11bb9 100644 --- a/app/node/input/media/media.cpp +++ b/app/node/input/media/media.cpp @@ -50,6 +50,11 @@ void MediaInput::SetFootage(StreamPtr f) footage_input_->set_standard_value(QVariant::fromValue(f)); } +bool MediaInput::IsMedia() const +{ + return true; +} + void MediaInput::Retranslate() { footage_input_->set_name(tr("Footage")); diff --git a/app/node/input/media/media.h b/app/node/input/media/media.h index ba89f3168..8934e321e 100644 --- a/app/node/input/media/media.h +++ b/app/node/input/media/media.h @@ -23,6 +23,7 @@ #include "codec/decoder.h" #include "node/node.h" +#include "project/item/footage/stream.h" OLIVE_NAMESPACE_ENTER @@ -35,11 +36,16 @@ class MediaInput : public Node public: MediaInput(); + virtual Stream::Type type() const = 0; + virtual QList Category() const override; StreamPtr footage(); void SetFootage(StreamPtr f); + virtual bool IsMedia() const override; + + virtual void Retranslate() override; virtual NodeValueTable Value(NodeValueDatabase& value) const override; diff --git a/app/node/input/media/video/video.cpp b/app/node/input/media/video/video.cpp index d451daeaf..c02aeaf39 100644 --- a/app/node/input/media/video/video.cpp +++ b/app/node/input/media/video/video.cpp @@ -36,6 +36,11 @@ Node *VideoInput::copy() const return new VideoInput(); } +Stream::Type VideoInput::type() const +{ + return Stream::kVideo; +} + QString VideoInput::Name() const { return tr("Video Input"); diff --git a/app/node/input/media/video/video.h b/app/node/input/media/video/video.h index 3ef9f54e8..7000ea82b 100644 --- a/app/node/input/media/video/video.h +++ b/app/node/input/media/video/video.h @@ -35,6 +35,8 @@ public: virtual Node* copy() const override; + virtual Stream::Type type() const override; + virtual QString Name() const override; virtual QString ShortName() const override; virtual QString id() const override; diff --git a/app/node/node.cpp b/app/node/node.cpp index 15e28512a..feda8c540 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -440,6 +440,11 @@ bool Node::IsTrack() const return false; } +bool Node::IsMedia() const +{ + return false; +} + const QList& Node::parameters() const { return params_; diff --git a/app/node/node.h b/app/node/node.h index ba05359ce..478d1e1ab 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -363,6 +363,15 @@ public: */ virtual bool IsTrack() const; + + /** + * @brief Returns whether this Node is a "Media" type or not + * + * You shouldn't ever need to override this since all derivatives of Media will automatically have this set to true. + * It's just a more convenient way of checking than dynamic_casting. + */ + virtual bool IsMedia() const; + /** * @brief The main processing function * diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 0a1dd0ab7..b47cbbaa7 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -558,6 +558,57 @@ void ProjectExplorer::DeleteSelected() } } + if (item_ptr->type() == Item::kFootage) { + // Get all sequences + QList sequences = model_.project()->get_items_of_type(Item::kSequence); + + // If no sequences exist we don't need to do anything clever here + if (!sequences.isEmpty()) { + // Footage can contain multiple streams, all of which need to be dealt with + foreach (StreamPtr stream, static_cast(item_ptr.get())->streams()) { + + // Check each sequence to see if it contains the footage in question + foreach (ItemPtr seq, sequences) { + + Sequence* s = static_cast(seq.get()); + + // Loop through nodes to find our Footage node + foreach (Node* node, s->nodes()) { + + // Check if node is of the right type + if (node->IsMedia() && static_cast(node)->type() == stream.get()->type()) { + // Check the streams are the same + if (static_cast(node)->footage() == stream) { + // Loop through nodes and set any that point to the Footage node to null + foreach (Node* check_node, s->nodes()) { + // Skip itself + if (check_node == node) { + continue; + } + if (check_node->GetImmediateDependencies().contains(node)) { + QList inputs = check_node->GetInputsIncludingArrays(); + + foreach (NodeInput* input, inputs) { + foreach (NodeEdgePtr edge, input->edges()) { + Node* connected = edge->output()->parentNode(); + + if (connected == node) { + input->DisconnectEdge(edge); + } + } + } + } + } + static_cast(node)->SetFootage(nullptr); + break; + } + } + } + } + } + } + } + new ProjectViewModel::RemoveItemCommand(&model_, item_ptr, command); } From 3ed0cb2ab9992d45a4033d4ca930fba4be01bd4f Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Sun, 27 Sep 2020 01:33:31 +0100 Subject: [PATCH 02/19] Move search code to seperate function. --- .../projectexplorer/projectexplorer.cpp | 96 ++++++++++++------- app/widget/projectexplorer/projectexplorer.h | 6 ++ 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index b47cbbaa7..3e216ec4b 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -536,6 +536,43 @@ void ProjectExplorer::DeselectAll() CurrentView()->selectionModel()->clearSelection(); } +QList ProjectExplorer::GetItemNodes(Item* item, Item::Type type) +{ + // Output list list + QList nodes; + + // Get all sequences. + QList sequences = model_.project()->get_items_of_type(Item::kSequence); + // Get item pointer. + ItemPtr item_ptr = item->get_shared_ptr(); + + if (type == Item::kFootage) { + // If no sequences exist we don't need to do anything clever here + if (!sequences.isEmpty()) { + // Footage can contain multiple streams, all of which need to be dealt with + foreach (StreamPtr stream, static_cast(item_ptr.get())->streams()) { + // Check each sequence to see if it contains the footage in question + foreach (ItemPtr seq, sequences) { + Sequence* s = static_cast(seq.get()); + + // Loop through nodes to find our Footage node + foreach (Node* node, s->nodes()) { + // Check if node is of the right type + if (node->IsMedia() && static_cast(node)->type() == stream.get()->type() || + static_cast(node)->type() == Stream::kImage) { + // Check the streams are the same + if (static_cast(node)->footage() == stream) { + nodes.append(node); + } + } + } + } + } + } + } + return nodes; +} + void ProjectExplorer::DeleteSelected() { QList selected = SelectedItems(); @@ -558,53 +595,44 @@ void ProjectExplorer::DeleteSelected() } } + // If this is a footage item, clean up if necessary if (item_ptr->type() == Item::kFootage) { - // Get all sequences - QList sequences = model_.project()->get_items_of_type(Item::kSequence); + + // Check if nodes exists + QList nodes = GetItemNodes(item, Item::kFootage); + if (!nodes.isEmpty()){ + // Loop through Footage nodes + foreach (Node* node, nodes) { - // If no sequences exist we don't need to do anything clever here - if (!sequences.isEmpty()) { - // Footage can contain multiple streams, all of which need to be dealt with - foreach (StreamPtr stream, static_cast(item_ptr.get())->streams()) { - - // Check each sequence to see if it contains the footage in question + // Loop through sequences + QList sequences = model_.project()->get_items_of_type(Item::kSequence); foreach (ItemPtr seq, sequences) { - Sequence* s = static_cast(seq.get()); - // Loop through nodes to find our Footage node - foreach (Node* node, s->nodes()) { + // Check all nodes to see if they're linked + foreach (Node* check_node, s->nodes()) { + // Skip itself + if (nodes.contains(check_node)) { + continue; + } + if (check_node->GetImmediateDependencies().contains(node)) { + QList inputs = check_node->GetInputsIncludingArrays(); - // Check if node is of the right type - if (node->IsMedia() && static_cast(node)->type() == stream.get()->type()) { - // Check the streams are the same - if (static_cast(node)->footage() == stream) { - // Loop through nodes and set any that point to the Footage node to null - foreach (Node* check_node, s->nodes()) { - // Skip itself - if (check_node == node) { - continue; - } - if (check_node->GetImmediateDependencies().contains(node)) { - QList inputs = check_node->GetInputsIncludingArrays(); + foreach (NodeInput* input, inputs) { + foreach (NodeEdgePtr edge, input->edges()) { + Node* connected = edge->output()->parentNode(); - foreach (NodeInput* input, inputs) { - foreach (NodeEdgePtr edge, input->edges()) { - Node* connected = edge->output()->parentNode(); - - if (connected == node) { - input->DisconnectEdge(edge); - } - } - } + if (connected == node) { + input->DisconnectEdge(edge); } } - static_cast(node)->SetFootage(nullptr); - break; } } } } + + // Set footage to be null + static_cast(node)->SetFootage(nullptr); } } } diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index 2fe097bbd..6ec2f9af9 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -85,6 +85,12 @@ public: void DeleteSelected(); + /** + * @brief Check if an item is in use anywhere and return any relevant input nodes + * kFootage has two streams that need to be handled + */ + QList GetItemNodes(Item* item, Item::Type type); + public slots: void set_view_type(ProjectToolbar::ViewType type); From 0e272b6bba2c84e27b944198f80842ab873e6bd3 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Sun, 27 Sep 2020 15:52:14 +0100 Subject: [PATCH 03/19] Cleanup and add warning message. --- .../projectexplorer/projectexplorer.cpp | 66 +++++++++---------- app/widget/projectexplorer/projectexplorer.h | 26 ++++++-- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 3e216ec4b..fabf30cab 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -558,8 +559,7 @@ QList ProjectExplorer::GetItemNodes(Item* item, Item::Type type) // Loop through nodes to find our Footage node foreach (Node* node, s->nodes()) { // Check if node is of the right type - if (node->IsMedia() && static_cast(node)->type() == stream.get()->type() || - static_cast(node)->type() == Stream::kImage) { + if (node->IsMedia() && static_cast(node)->type() == stream.get()->type()) { // Check the streams are the same if (static_cast(node)->footage() == stream) { nodes.append(node); @@ -573,6 +573,28 @@ QList ProjectExplorer::GetItemNodes(Item* item, Item::Type type) return nodes; } +ProjectExplorer::FootageDeleteResponse ProjectExplorer::DeleteWarningMessage() +{ + QMessageBox msgBox; + msgBox.setText(tr("This footage is in use.")); + msgBox.setInformativeText(tr("Do you want to offline the footage or entirely delete it from the timeline?")); + QPushButton* offline = msgBox.addButton(tr("Offline Footage"), QMessageBox::ApplyRole); + QPushButton* deleteClips = msgBox.addButton(tr("Delete Clips"), QMessageBox::ApplyRole); + msgBox.setStandardButtons(QMessageBox::Cancel); + msgBox.setIcon(QMessageBox::Warning); + + msgBox.exec(); + + if (msgBox.clickedButton() == offline) { + return kOffline; + } + if (msgBox.clickedButton() == deleteClips) { + return kDelete; + } + + return kCancel; +} + void ProjectExplorer::DeleteSelected() { QList selected = SelectedItems(); @@ -601,38 +623,16 @@ void ProjectExplorer::DeleteSelected() // Check if nodes exists QList nodes = GetItemNodes(item, Item::kFootage); if (!nodes.isEmpty()){ - // Loop through Footage nodes - foreach (Node* node, nodes) { - - // Loop through sequences - QList sequences = model_.project()->get_items_of_type(Item::kSequence); - foreach (ItemPtr seq, sequences) { - Sequence* s = static_cast(seq.get()); - - // Check all nodes to see if they're linked - foreach (Node* check_node, s->nodes()) { - // Skip itself - if (nodes.contains(check_node)) { - continue; - } - if (check_node->GetImmediateDependencies().contains(node)) { - QList inputs = check_node->GetInputsIncludingArrays(); - - foreach (NodeInput* input, inputs) { - foreach (NodeEdgePtr edge, input->edges()) { - Node* connected = edge->output()->parentNode(); - - if (connected == node) { - input->DisconnectEdge(edge); - } - } - } - } - } + FootageDeleteResponse response = DeleteWarningMessage(); + if (response == kOffline) { + // Loop through Footage nodes + foreach (Node* node, nodes) { + // Set footage to be null + static_cast(node)->SetFootage(nullptr); } - - // Set footage to be null - static_cast(node)->SetFootage(nullptr); + } + if (response == kCancel) { + return; } } } diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index 6ec2f9af9..0e69b427c 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -85,12 +85,6 @@ public: void DeleteSelected(); - /** - * @brief Check if an item is in use anywhere and return any relevant input nodes - * kFootage has two streams that need to be handled - */ - QList GetItemNodes(Item* item, Item::Type type); - public slots: void set_view_type(ProjectToolbar::ViewType type); @@ -107,6 +101,26 @@ signals: void DoubleClickedItem(Item* item); private: + enum FootageDeleteResponse { + kDelete, + kOffline, + kCancel + }; + + + /** + * @brief Pop up a QMessageBox to warn the user if the deleted clips are in use + * + * Returns a FootageDeleteResponse + */ + FootageDeleteResponse DeleteWarningMessage(); + + /** + * @brief Check if an item is in use anywhere and return any relevant input nodes + * kFootage has two streams that need to be handled + */ + QList GetItemNodes(Item* item, Item::Type type); + /** * @brief Simple convenience function for adding a view to this stacked widget * From 0f1d4ec1b829a426f46932b2bba24d809fb75762 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Sun, 27 Sep 2020 15:59:51 +0100 Subject: [PATCH 04/19] Make sure image sequences are caught. --- app/widget/projectexplorer/projectexplorer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index fabf30cab..419f6a1ea 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -559,7 +559,7 @@ QList ProjectExplorer::GetItemNodes(Item* item, Item::Type type) // Loop through nodes to find our Footage node foreach (Node* node, s->nodes()) { // Check if node is of the right type - if (node->IsMedia() && static_cast(node)->type() == stream.get()->type()) { + if (node->IsMedia()){ // Check the streams are the same if (static_cast(node)->footage() == stream) { nodes.append(node); From c1e345611de58fb09e4c467222d8b618a7a1e9a5 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Sun, 27 Sep 2020 16:14:56 +0100 Subject: [PATCH 05/19] Cleanup. --- app/widget/projectexplorer/projectexplorer.cpp | 6 +++--- app/widget/projectexplorer/projectexplorer.h | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 419f6a1ea..0694185e3 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -537,7 +537,7 @@ void ProjectExplorer::DeselectAll() CurrentView()->selectionModel()->clearSelection(); } -QList ProjectExplorer::GetItemNodes(Item* item, Item::Type type) +QList ProjectExplorer::GetFootageNodes(Item* item) { // Output list list QList nodes; @@ -547,7 +547,7 @@ QList ProjectExplorer::GetItemNodes(Item* item, Item::Type type) // Get item pointer. ItemPtr item_ptr = item->get_shared_ptr(); - if (type == Item::kFootage) { + if (item_ptr.get()->type() == Item::kFootage) { // If no sequences exist we don't need to do anything clever here if (!sequences.isEmpty()) { // Footage can contain multiple streams, all of which need to be dealt with @@ -621,7 +621,7 @@ void ProjectExplorer::DeleteSelected() if (item_ptr->type() == Item::kFootage) { // Check if nodes exists - QList nodes = GetItemNodes(item, Item::kFootage); + QList nodes = GetFootageNodes(item); if (!nodes.isEmpty()){ FootageDeleteResponse response = DeleteWarningMessage(); if (response == kOffline) { diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index 0e69b427c..12c999d5e 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -117,9 +117,9 @@ private: /** * @brief Check if an item is in use anywhere and return any relevant input nodes - * kFootage has two streams that need to be handled + * Returns a QList as Footage has two streams that need to be handled */ - QList GetItemNodes(Item* item, Item::Type type); + QList GetFootageNodes(Item* item); /** * @brief Simple convenience function for adding a view to this stacked widget From 5bb9cb0ccad27544898bbe7d5e0af36ccbf5f71f Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Sun, 27 Sep 2020 17:42:09 +0100 Subject: [PATCH 06/19] Make Offlining footage an UndoCommand Created a new UndoCommand in ProjectViewModel that set the footage input to a nullptr and resets it to the correct stream on undo. --- app/project/projectviewmodel.cpp | 38 +++++++++++++++++++ app/project/projectviewmodel.h | 21 ++++++++++ .../projectexplorer/projectexplorer.cpp | 27 +++++++------ app/widget/projectexplorer/projectexplorer.h | 2 +- 4 files changed, 75 insertions(+), 13 deletions(-) diff --git a/app/project/projectviewmodel.cpp b/app/project/projectviewmodel.cpp index 7dfaf453a..8c176f24f 100644 --- a/app/project/projectviewmodel.cpp +++ b/app/project/projectviewmodel.cpp @@ -25,6 +25,7 @@ #include #include "core.h" +#include "node/input/media/media.h" OLIVE_NAMESPACE_ENTER @@ -603,4 +604,41 @@ void ProjectViewModel::RemoveItemCommand::undo_internal() model_->AddChild(parent_, item_); } +ProjectViewModel::OfflineFootageCommand::OfflineFootageCommand(ProjectViewModel* model, ItemPtr item, + QMap nodes, QUndoCommand* parent) : + UndoCommand(parent), + model_(model), + item_(item), + nodes_(nodes) +{ +} + +Project *ProjectViewModel::OfflineFootageCommand::GetRelevantProject() const +{ + return model_->project(); +} + +void ProjectViewModel::OfflineFootageCommand::redo_internal() +{ + QMap::const_iterator it = nodes_.constBegin(); + while (it != nodes_.constEnd()) { + static_cast(it.key())->SetFootage(nullptr); + ++it; + } + + parent_ = item_->parent(); + model_->RemoveChild(parent_, item_.get()); +} + +void ProjectViewModel::OfflineFootageCommand::undo_internal() +{ + model_->AddChild(parent_, item_); + + QMap::const_iterator it = nodes_.constBegin(); + while (it != nodes_.constEnd()) { + static_cast(it.key())->SetFootage(it.value()); + ++it; + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/project/projectviewmodel.h b/app/project/projectviewmodel.h index 964378316..d79542aa0 100644 --- a/app/project/projectviewmodel.h +++ b/app/project/projectviewmodel.h @@ -194,6 +194,27 @@ public: }; + class OfflineFootageCommand : public UndoCommand { + public: + OfflineFootageCommand(ProjectViewModel* model, ItemPtr item, QMap nodes, QUndoCommand* parent = nullptr); + + virtual Project* GetRelevantProject() const override; + + protected: + virtual void redo_internal() override; + + virtual void undo_internal() override; + + private: + ProjectViewModel* model_; + + ItemPtr item_; + + Item* parent_; + + QMap nodes_; + }; + private: /** * @brief Retrieve the index of `item` in its parent diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 0694185e3..3c5ce6f07 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -537,11 +537,11 @@ void ProjectExplorer::DeselectAll() CurrentView()->selectionModel()->clearSelection(); } -QList ProjectExplorer::GetFootageNodes(Item* item) +QMap ProjectExplorer::GetFootageNodes(Item* item) { // Output list list - QList nodes; - + QMap nodes; + // Get all sequences. QList sequences = model_.project()->get_items_of_type(Item::kSequence); // Get item pointer. @@ -562,7 +562,7 @@ QList ProjectExplorer::GetFootageNodes(Item* item) if (node->IsMedia()){ // Check the streams are the same if (static_cast(node)->footage() == stream) { - nodes.append(node); + nodes.insert(node, stream); } } } @@ -615,29 +615,32 @@ void ProjectExplorer::DeleteSelected() if (Core::instance()->main_window()->IsSequenceOpen(s)) { Core::instance()->main_window()->CloseSequence(s); } + + new ProjectViewModel::RemoveItemCommand(&model_, item_ptr, command); } // If this is a footage item, clean up if necessary if (item_ptr->type() == Item::kFootage) { // Check if nodes exists - QList nodes = GetFootageNodes(item); + QMap nodes = GetFootageNodes(item); if (!nodes.isEmpty()){ + // Warn user and ask them what to do FootageDeleteResponse response = DeleteWarningMessage(); if (response == kOffline) { - // Loop through Footage nodes - foreach (Node* node, nodes) { - // Set footage to be null - static_cast(node)->SetFootage(nullptr); - } + new ProjectViewModel::OfflineFootageCommand(&model_, item_ptr, nodes, command); + } + if (response == kDelete) { + // Get all sequences. + QList sequences = model_.project()->get_items_of_type(Item::kSequence); + } if (response == kCancel) { + delete command; return; } } } - - new ProjectViewModel::RemoveItemCommand(&model_, item_ptr, command); } Core::instance()->undo_stack()->pushIfHasChildren(command); diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index 12c999d5e..b755401ac 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -119,7 +119,7 @@ private: * @brief Check if an item is in use anywhere and return any relevant input nodes * Returns a QList as Footage has two streams that need to be handled */ - QList GetFootageNodes(Item* item); + QMap GetFootageNodes(Item* item); /** * @brief Simple convenience function for adding a view to this stacked widget From 602f96b2b6e63ff3a0e8369941a1edcefc765edf Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Sun, 27 Sep 2020 22:15:32 +0100 Subject: [PATCH 07/19] Cleanup comment --- app/widget/projectexplorer/projectexplorer.cpp | 1 - app/widget/projectexplorer/projectexplorer.h | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 3c5ce6f07..e0ca1aff2 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -577,7 +577,6 @@ ProjectExplorer::FootageDeleteResponse ProjectExplorer::DeleteWarningMessage() { QMessageBox msgBox; msgBox.setText(tr("This footage is in use.")); - msgBox.setInformativeText(tr("Do you want to offline the footage or entirely delete it from the timeline?")); QPushButton* offline = msgBox.addButton(tr("Offline Footage"), QMessageBox::ApplyRole); QPushButton* deleteClips = msgBox.addButton(tr("Delete Clips"), QMessageBox::ApplyRole); msgBox.setStandardButtons(QMessageBox::Cancel); diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index b755401ac..907da2964 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -117,7 +117,8 @@ private: /** * @brief Check if an item is in use anywhere and return any relevant input nodes - * Returns a QList as Footage has two streams that need to be handled + * + * Returns a QMap pairing a Footage node to its StreamPtr */ QMap GetFootageNodes(Item* item); From 92db73502229cf0cb1fd36d293893ec008512662 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Tue, 29 Sep 2020 00:06:38 +0100 Subject: [PATCH 08/19] Add Delete functionality. Still buggy. --- app/project/projectviewmodel.cpp | 33 +++++++++++++++++++ app/project/projectviewmodel.h | 24 ++++++++++++++ .../projectexplorer/projectexplorer.cpp | 19 +++++++++++ 3 files changed, 76 insertions(+) diff --git a/app/project/projectviewmodel.cpp b/app/project/projectviewmodel.cpp index 8c176f24f..5b90fde71 100644 --- a/app/project/projectviewmodel.cpp +++ b/app/project/projectviewmodel.cpp @@ -26,6 +26,7 @@ #include "core.h" #include "node/input/media/media.h" +#include "widget/timelinewidget/timelinewidget.h" OLIVE_NAMESPACE_ENTER @@ -641,4 +642,36 @@ void ProjectViewModel::OfflineFootageCommand::undo_internal() } } +ProjectViewModel::DeleteFootageCommand::DeleteFootageCommand(ProjectViewModel* model, ItemPtr item, + QList blocks, QUndoCommand* parent) : + UndoCommand(parent), + model_(model), + item_(item), + blocks_(blocks) +{ + deleteCommand_ = new QUndoCommand(); + removalCommand_ = new QUndoCommand(); +} + +Project *ProjectViewModel::DeleteFootageCommand::GetRelevantProject() const +{ + return model_->project(); +} + +void ProjectViewModel::DeleteFootageCommand::redo_internal() +{ + TimelineWidget::ReplaceBlocksWithGaps(blocks_, true, deleteCommand_); + Core::instance()->undo_stack()->pushIfHasChildren(deleteCommand_); + + new ProjectViewModel::RemoveItemCommand(model_, item_, removalCommand_); + Core::instance()->undo_stack()->pushIfHasChildren(removalCommand_); +} + +void ProjectViewModel::DeleteFootageCommand::undo_internal() +{ + removalCommand_->undo(); + deleteCommand_->undo(); +} + + OLIVE_NAMESPACE_EXIT diff --git a/app/project/projectviewmodel.h b/app/project/projectviewmodel.h index d79542aa0..16e56fffa 100644 --- a/app/project/projectviewmodel.h +++ b/app/project/projectviewmodel.h @@ -25,6 +25,7 @@ #include "project.h" #include "undo/undocommand.h" +#include "node/block/block.h" OLIVE_NAMESPACE_ENTER @@ -215,6 +216,29 @@ public: QMap nodes_; }; + class DeleteFootageCommand : public UndoCommand { + public: + DeleteFootageCommand(ProjectViewModel* model, ItemPtr item, QList blocks, QUndoCommand* parent = nullptr); + + virtual Project* GetRelevantProject() const override; + + protected: + virtual void redo_internal() override; + + virtual void undo_internal() override; + + private: + ProjectViewModel* model_; + + ItemPtr item_; + + QList blocks_; + + QUndoCommand* deleteCommand_; + + QUndoCommand* removalCommand_; + }; + private: /** * @brief Retrieve the index of `item` in its parent diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index e0ca1aff2..dfdaa7228 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -37,6 +37,7 @@ #include "widget/menu/menu.h" #include "widget/menu/menushared.h" #include "window/mainwindow/mainwindow.h" +#include "widget/timelinewidget/timelinewidget.h" OLIVE_NAMESPACE_ENTER @@ -633,6 +634,24 @@ void ProjectExplorer::DeleteSelected() // Get all sequences. QList sequences = model_.project()->get_items_of_type(Item::kSequence); + QList blocks; + + foreach(ItemPtr seq, sequences) { + Sequence* s = static_cast(seq.get()); + // Loop through nodes in sequence + foreach(Node* node, s->nodes()) { + + // For each Block see if it is linked to one of the Footage nodes add it to the delete list + if (node->IsBlock()) { + foreach(Node* input, nodes.keys()) { + if(node->GetExclusiveDependencies().contains(input)) + blocks.append(static_cast(node)); + } + } + } + } + new ProjectViewModel::DeleteFootageCommand(&model_, item_ptr, blocks, command); + } if (response == kCancel) { delete command; From aef907859880cb9517d838c034475da5947558f2 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Tue, 29 Sep 2020 10:56:30 +0100 Subject: [PATCH 09/19] Fix delete redo code --- app/project/projectviewmodel.cpp | 30 ++++++++++++++----- app/project/projectviewmodel.h | 9 ++++-- .../projectexplorer/projectexplorer.cpp | 6 +++- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/app/project/projectviewmodel.cpp b/app/project/projectviewmodel.cpp index 5b90fde71..0947a6635 100644 --- a/app/project/projectviewmodel.cpp +++ b/app/project/projectviewmodel.cpp @@ -642,15 +642,15 @@ void ProjectViewModel::OfflineFootageCommand::undo_internal() } } -ProjectViewModel::DeleteFootageCommand::DeleteFootageCommand(ProjectViewModel* model, ItemPtr item, - QList blocks, QUndoCommand* parent) : +ProjectViewModel::DeleteFootageCommand::DeleteFootageCommand(ProjectViewModel *model, ItemPtr item, + QMap nodes, QUndoCommand *parent) + : UndoCommand(parent), model_(model), item_(item), - blocks_(blocks) + nodes_(nodes) { deleteCommand_ = new QUndoCommand(); - removalCommand_ = new QUndoCommand(); } Project *ProjectViewModel::DeleteFootageCommand::GetRelevantProject() const @@ -660,16 +660,32 @@ Project *ProjectViewModel::DeleteFootageCommand::GetRelevantProject() const void ProjectViewModel::DeleteFootageCommand::redo_internal() { + QList sequences = model_->project()->get_items_of_type(Item::kSequence); + + blocks_.clear(); + + foreach (ItemPtr seq, sequences) { + Sequence *s = static_cast(seq.get()); + // Loop through nodes in sequence + foreach (Node *node, s->nodes()) { + // For each Block see if it is linked to one of the Footage nodes add it to the delete list + if (node->IsBlock()) { + foreach (Node *input, nodes_.keys()) { + if (node->GetExclusiveDependencies().contains(input)) blocks_.append(static_cast(node)); + } + } + } + } TimelineWidget::ReplaceBlocksWithGaps(blocks_, true, deleteCommand_); Core::instance()->undo_stack()->pushIfHasChildren(deleteCommand_); - new ProjectViewModel::RemoveItemCommand(model_, item_, removalCommand_); - Core::instance()->undo_stack()->pushIfHasChildren(removalCommand_); + parent_ = item_->parent(); + model_->RemoveChild(parent_, item_.get()); } void ProjectViewModel::DeleteFootageCommand::undo_internal() { - removalCommand_->undo(); + model_->AddChild(parent_, item_); deleteCommand_->undo(); } diff --git a/app/project/projectviewmodel.h b/app/project/projectviewmodel.h index 16e56fffa..05c2671c8 100644 --- a/app/project/projectviewmodel.h +++ b/app/project/projectviewmodel.h @@ -218,7 +218,8 @@ public: class DeleteFootageCommand : public UndoCommand { public: - DeleteFootageCommand(ProjectViewModel* model, ItemPtr item, QList blocks, QUndoCommand* parent = nullptr); + DeleteFootageCommand(ProjectViewModel* model, ItemPtr item, QMap nodes, + QUndoCommand* parent = nullptr); virtual Project* GetRelevantProject() const override; @@ -232,11 +233,13 @@ public: ItemPtr item_; + Item* parent_; + + QMap nodes_; + QList blocks_; QUndoCommand* deleteCommand_; - - QUndoCommand* removalCommand_; }; private: diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index dfdaa7228..e666ddf1d 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -650,7 +650,11 @@ void ProjectExplorer::DeleteSelected() } } } - new ProjectViewModel::DeleteFootageCommand(&model_, item_ptr, blocks, command); + //new ProjectViewModel::DeleteFootageCommand(&model_, item_ptr, blocks, command); + + QUndoCommand* deleteCommand = new QUndoCommand(command); + TimelineWidget::ReplaceBlocksWithGaps(blocks, true, deleteCommand); + //Core::instance()->undo_stack()->pushIfHasChildren(deleteCommand); } if (response == kCancel) { From 8e6b42a160408111c0ddb15c119d99c2b1209356 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Tue, 29 Sep 2020 11:31:03 +0100 Subject: [PATCH 10/19] Simplify and clean up Remove the Undo code for deleting a clip and replace it with simpler parent/child undo relationship. Move block gathering code to seperate function. It's quite brute force and might lead to issues further down the line with more complex composited node setups. --- app/project/projectviewmodel.cpp | 48 ------------------ app/project/projectviewmodel.h | 26 ---------- .../projectexplorer/projectexplorer.cpp | 50 ++++++++++--------- app/widget/projectexplorer/projectexplorer.h | 5 ++ 4 files changed, 32 insertions(+), 97 deletions(-) diff --git a/app/project/projectviewmodel.cpp b/app/project/projectviewmodel.cpp index 0947a6635..3a94252d6 100644 --- a/app/project/projectviewmodel.cpp +++ b/app/project/projectviewmodel.cpp @@ -642,52 +642,4 @@ void ProjectViewModel::OfflineFootageCommand::undo_internal() } } -ProjectViewModel::DeleteFootageCommand::DeleteFootageCommand(ProjectViewModel *model, ItemPtr item, - QMap nodes, QUndoCommand *parent) - : - UndoCommand(parent), - model_(model), - item_(item), - nodes_(nodes) -{ - deleteCommand_ = new QUndoCommand(); -} - -Project *ProjectViewModel::DeleteFootageCommand::GetRelevantProject() const -{ - return model_->project(); -} - -void ProjectViewModel::DeleteFootageCommand::redo_internal() -{ - QList sequences = model_->project()->get_items_of_type(Item::kSequence); - - blocks_.clear(); - - foreach (ItemPtr seq, sequences) { - Sequence *s = static_cast(seq.get()); - // Loop through nodes in sequence - foreach (Node *node, s->nodes()) { - // For each Block see if it is linked to one of the Footage nodes add it to the delete list - if (node->IsBlock()) { - foreach (Node *input, nodes_.keys()) { - if (node->GetExclusiveDependencies().contains(input)) blocks_.append(static_cast(node)); - } - } - } - } - TimelineWidget::ReplaceBlocksWithGaps(blocks_, true, deleteCommand_); - Core::instance()->undo_stack()->pushIfHasChildren(deleteCommand_); - - parent_ = item_->parent(); - model_->RemoveChild(parent_, item_.get()); -} - -void ProjectViewModel::DeleteFootageCommand::undo_internal() -{ - model_->AddChild(parent_, item_); - deleteCommand_->undo(); -} - - OLIVE_NAMESPACE_EXIT diff --git a/app/project/projectviewmodel.h b/app/project/projectviewmodel.h index 05c2671c8..bd3e068ed 100644 --- a/app/project/projectviewmodel.h +++ b/app/project/projectviewmodel.h @@ -216,32 +216,6 @@ public: QMap nodes_; }; - class DeleteFootageCommand : public UndoCommand { - public: - DeleteFootageCommand(ProjectViewModel* model, ItemPtr item, QMap nodes, - QUndoCommand* parent = nullptr); - - virtual Project* GetRelevantProject() const override; - - protected: - virtual void redo_internal() override; - - virtual void undo_internal() override; - - private: - ProjectViewModel* model_; - - ItemPtr item_; - - Item* parent_; - - QMap nodes_; - - QList blocks_; - - QUndoCommand* deleteCommand_; - }; - private: /** * @brief Retrieve the index of `item` in its parent diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index e666ddf1d..26fedcd2d 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -574,6 +574,31 @@ QMap ProjectExplorer::GetFootageNodes(Item* item) return nodes; } +QList ProjectExplorer::GetFootageBlocks(QList nodes) +{ + // Get all sequences. + QList sequences = model_.project()->get_items_of_type(Item::kSequence); + + QList blocks; + + foreach (ItemPtr seq, sequences) { + Sequence* s = static_cast(seq.get()); + // Loop through nodes in sequence + foreach (Node* node, s->nodes()) { + // For each Block see if it is linked to one of the Footage nodes add it to the delete list + if (node->IsBlock()) { + foreach (Node* input, nodes) { + if (node->GetExclusiveDependencies().contains(input)) { + blocks.append(static_cast(node)); + } + } + } + } + } + + return blocks; +} + ProjectExplorer::FootageDeleteResponse ProjectExplorer::DeleteWarningMessage() { QMessageBox msgBox; @@ -631,31 +656,10 @@ void ProjectExplorer::DeleteSelected() new ProjectViewModel::OfflineFootageCommand(&model_, item_ptr, nodes, command); } if (response == kDelete) { - // Get all sequences. - QList sequences = model_.project()->get_items_of_type(Item::kSequence); - - QList blocks; - - foreach(ItemPtr seq, sequences) { - Sequence* s = static_cast(seq.get()); - // Loop through nodes in sequence - foreach(Node* node, s->nodes()) { - - // For each Block see if it is linked to one of the Footage nodes add it to the delete list - if (node->IsBlock()) { - foreach(Node* input, nodes.keys()) { - if(node->GetExclusiveDependencies().contains(input)) - blocks.append(static_cast(node)); - } - } - } - } - //new ProjectViewModel::DeleteFootageCommand(&model_, item_ptr, blocks, command); - QUndoCommand* deleteCommand = new QUndoCommand(command); - TimelineWidget::ReplaceBlocksWithGaps(blocks, true, deleteCommand); - //Core::instance()->undo_stack()->pushIfHasChildren(deleteCommand); + TimelineWidget::ReplaceBlocksWithGaps(GetFootageBlocks(nodes.keys()), true, deleteCommand); + new ProjectViewModel::RemoveItemCommand(&model_, item_ptr, command); } if (response == kCancel) { delete command; diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index 907da2964..8900fc9de 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -122,6 +122,11 @@ private: */ QMap GetFootageNodes(Item* item); + /** + * @brief Get all the blocks associated with the given footage nodes + */ + QList GetFootageBlocks(QList nodes); + /** * @brief Simple convenience function for adding a view to this stacked widget * From 791857fba1dcdce2860029fde56d2fd309149249 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Tue, 29 Sep 2020 12:03:01 +0100 Subject: [PATCH 11/19] Cleanup. --- app/project/projectviewmodel.cpp | 1 - app/project/projectviewmodel.h | 3 +++ app/widget/projectexplorer/projectexplorer.cpp | 2 +- app/widget/projectexplorer/projectexplorer.h | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/project/projectviewmodel.cpp b/app/project/projectviewmodel.cpp index 3a94252d6..8c176f24f 100644 --- a/app/project/projectviewmodel.cpp +++ b/app/project/projectviewmodel.cpp @@ -26,7 +26,6 @@ #include "core.h" #include "node/input/media/media.h" -#include "widget/timelinewidget/timelinewidget.h" OLIVE_NAMESPACE_ENTER diff --git a/app/project/projectviewmodel.h b/app/project/projectviewmodel.h index bd3e068ed..8da1c2472 100644 --- a/app/project/projectviewmodel.h +++ b/app/project/projectviewmodel.h @@ -195,6 +195,9 @@ public: }; + /** + * @brief An undo command for offlining footage when it is deleted from the project explorer + */ class OfflineFootageCommand : public UndoCommand { public: OfflineFootageCommand(ProjectViewModel* model, ItemPtr item, QMap nodes, QUndoCommand* parent = nullptr); diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 26fedcd2d..fecb0ac6f 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -588,7 +588,7 @@ QList ProjectExplorer::GetFootageBlocks(QList nodes) // For each Block see if it is linked to one of the Footage nodes add it to the delete list if (node->IsBlock()) { foreach (Node* input, nodes) { - if (node->GetExclusiveDependencies().contains(input)) { + if (node->GetDependencies().contains(input)) { blocks.append(static_cast(node)); } } diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index 8900fc9de..a91af4cd2 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -124,6 +124,8 @@ private: /** * @brief Get all the blocks associated with the given footage nodes + * + * Currently quite brute force. */ QList GetFootageBlocks(QList nodes); From a71df11fa657b373a404b5907550d82326d6dc07 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Tue, 29 Sep 2020 13:29:59 +0100 Subject: [PATCH 12/19] Deal with composite blocks If a block relies on mulitple inputs (i.e. a composite) then don't delete these from the timeline. We catch these inputs afterwards. --- .../projectexplorer/projectexplorer.cpp | 41 ++++++++++++++++--- app/widget/projectexplorer/projectexplorer.h | 6 +-- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index fecb0ac6f..54b27d5da 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -38,6 +38,7 @@ #include "widget/menu/menushared.h" #include "window/mainwindow/mainwindow.h" #include "widget/timelinewidget/timelinewidget.h" +#include "widget/nodeview/nodeviewundo.h" OLIVE_NAMESPACE_ENTER @@ -585,13 +586,23 @@ QList ProjectExplorer::GetFootageBlocks(QList nodes) Sequence* s = static_cast(seq.get()); // Loop through nodes in sequence foreach (Node* node, s->nodes()) { - // For each Block see if it is linked to one of the Footage nodes add it to the delete list + // For each Block see if it solely depends on one of our input nodes and if so add to the block list if (node->IsBlock()) { - foreach (Node* input, nodes) { - if (node->GetDependencies().contains(input)) { - blocks.append(static_cast(node)); + int footage_deps = 0; + + QList dependancies = node->GetDependencies(); + QSet intersection = QSet(dependancies.begin(), dependancies.end()) + .intersect(QSet(nodes.begin(), nodes.end())); + if (!intersection.isEmpty()) { + foreach (Node* dep, dependancies) { + if (dep->IsMedia()) { + footage_deps++; + } + } + if (footage_deps == 1) { + blocks.append(static_cast(node)); + } } - } } } } @@ -658,8 +669,26 @@ void ProjectExplorer::DeleteSelected() if (response == kDelete) { QUndoCommand* deleteCommand = new QUndoCommand(command); TimelineWidget::ReplaceBlocksWithGaps(GetFootageBlocks(nodes.keys()), true, deleteCommand); - new ProjectViewModel::RemoveItemCommand(&model_, item_ptr, command); + //Core::instance()->undo_stack()->pushIfHasChildren(command); + + // Catch any input nodes we missed do to complex composites etc. + + QList sequences = model_.project()->get_items_of_type(Item::kSequence); + + QList nodes_to_delete; + foreach (ItemPtr seq, sequences) { + Sequence* s = static_cast(seq.get()); + foreach (Node* node, s->nodes()) { + if (node->IsMedia()) { + if (nodes.contains(node)) { + nodes_to_delete.append(node); + } + } + } + QUndoCommand* deleteNodesCommand = new QUndoCommand(deleteCommand); + new NodeRemoveCommand(static_cast(s), nodes_to_delete, deleteNodesCommand); + } } if (response == kCancel) { delete command; diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index a91af4cd2..d393959fa 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -123,9 +123,9 @@ private: QMap GetFootageNodes(Item* item); /** - * @brief Get all the blocks associated with the given footage nodes - * - * Currently quite brute force. + * @brief Get all the blocks that solely rely on an input node + * + * Ignores blocks that depend on multiple inputs */ QList GetFootageBlocks(QList nodes); From 56e2b510417e11f649bdcfc5aae317e5410ea622 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Tue, 29 Sep 2020 14:03:25 +0100 Subject: [PATCH 13/19] Add filename to warning message. --- .../projectexplorer/projectexplorer.cpp | 36 +++++++++++-------- app/widget/projectexplorer/projectexplorer.h | 2 +- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 54b27d5da..2294ea413 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -610,24 +610,30 @@ QList ProjectExplorer::GetFootageBlocks(QList nodes) return blocks; } -ProjectExplorer::FootageDeleteResponse ProjectExplorer::DeleteWarningMessage() +ProjectExplorer::FootageDeleteResponse ProjectExplorer::DeleteWarningMessage(Item* item) { - QMessageBox msgBox; - msgBox.setText(tr("This footage is in use.")); - QPushButton* offline = msgBox.addButton(tr("Offline Footage"), QMessageBox::ApplyRole); - QPushButton* deleteClips = msgBox.addButton(tr("Delete Clips"), QMessageBox::ApplyRole); - msgBox.setStandardButtons(QMessageBox::Cancel); - msgBox.setIcon(QMessageBox::Warning); + ItemPtr item_ptr = item->get_shared_ptr(); - msgBox.exec(); + if (item_ptr->type() == Item::kFootage) { + QString clip_name = static_cast(item_ptr.get())->filename().split("/").last(); - if (msgBox.clickedButton() == offline) { - return kOffline; + QMessageBox msgBox; + msgBox.setWindowTitle(clip_name); + msgBox.setText(clip_name + tr(" is in use.")); + QPushButton* offline = msgBox.addButton(tr("Offline Footage"), QMessageBox::ApplyRole); + QPushButton* deleteClips = msgBox.addButton(tr("Delete Clips"), QMessageBox::ApplyRole); + msgBox.setStandardButtons(QMessageBox::Cancel); + msgBox.setIcon(QMessageBox::Warning); + + msgBox.exec(); + + if (msgBox.clickedButton() == offline) { + return kOffline; + } + if (msgBox.clickedButton() == deleteClips) { + return kDelete; + } } - if (msgBox.clickedButton() == deleteClips) { - return kDelete; - } - return kCancel; } @@ -662,7 +668,7 @@ void ProjectExplorer::DeleteSelected() QMap nodes = GetFootageNodes(item); if (!nodes.isEmpty()){ // Warn user and ask them what to do - FootageDeleteResponse response = DeleteWarningMessage(); + FootageDeleteResponse response = DeleteWarningMessage(item); if (response == kOffline) { new ProjectViewModel::OfflineFootageCommand(&model_, item_ptr, nodes, command); } diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index d393959fa..daca3aec1 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -113,7 +113,7 @@ private: * * Returns a FootageDeleteResponse */ - FootageDeleteResponse DeleteWarningMessage(); + FootageDeleteResponse DeleteWarningMessage(Item* item); /** * @brief Check if an item is in use anywhere and return any relevant input nodes From 7704e004430b34b506654d39874034e49d61ec49 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Tue, 29 Sep 2020 14:14:21 +0100 Subject: [PATCH 14/19] Cleanup --- .../projectexplorer/projectexplorer.cpp | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 2294ea413..b63a40919 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -542,7 +542,7 @@ void ProjectExplorer::DeselectAll() QMap ProjectExplorer::GetFootageNodes(Item* item) { // Output list list - QMap nodes; + QMap footage_nodes; // Get all sequences. QList sequences = model_.project()->get_items_of_type(Item::kSequence); @@ -564,7 +564,7 @@ QMap ProjectExplorer::GetFootageNodes(Item* item) if (node->IsMedia()){ // Check the streams are the same if (static_cast(node)->footage() == stream) { - nodes.insert(node, stream); + footage_nodes.insert(node, stream); } } } @@ -572,7 +572,7 @@ QMap ProjectExplorer::GetFootageNodes(Item* item) } } } - return nodes; + return footage_nodes; } QList ProjectExplorer::GetFootageBlocks(QList nodes) @@ -594,11 +594,13 @@ QList ProjectExplorer::GetFootageBlocks(QList nodes) QSet intersection = QSet(dependancies.begin(), dependancies.end()) .intersect(QSet(nodes.begin(), nodes.end())); if (!intersection.isEmpty()) { + // Count how many Media inputs this block depends on foreach (Node* dep, dependancies) { if (dep->IsMedia()) { footage_deps++; } } + // If it only depends on one input we can safely delete it if (footage_deps == 1) { blocks.append(static_cast(node)); } @@ -665,20 +667,20 @@ void ProjectExplorer::DeleteSelected() if (item_ptr->type() == Item::kFootage) { // Check if nodes exists - QMap nodes = GetFootageNodes(item); - if (!nodes.isEmpty()){ + QMap footage_nodes = GetFootageNodes(item); + if (!footage_nodes.isEmpty()){ // Warn user and ask them what to do FootageDeleteResponse response = DeleteWarningMessage(item); if (response == kOffline) { - new ProjectViewModel::OfflineFootageCommand(&model_, item_ptr, nodes, command); + new ProjectViewModel::OfflineFootageCommand(&model_, item_ptr, footage_nodes, command); } if (response == kDelete) { QUndoCommand* deleteCommand = new QUndoCommand(command); - TimelineWidget::ReplaceBlocksWithGaps(GetFootageBlocks(nodes.keys()), true, deleteCommand); + // Delete any non-composite blocks + TimelineWidget::ReplaceBlocksWithGaps(GetFootageBlocks(footage_nodes.keys()), true, deleteCommand); new ProjectViewModel::RemoveItemCommand(&model_, item_ptr, command); - //Core::instance()->undo_stack()->pushIfHasChildren(command); - // Catch any input nodes we missed do to complex composites etc. + // Catch any input nodes we missed due to composites etc. QList sequences = model_.project()->get_items_of_type(Item::kSequence); @@ -687,7 +689,7 @@ void ProjectExplorer::DeleteSelected() Sequence* s = static_cast(seq.get()); foreach (Node* node, s->nodes()) { if (node->IsMedia()) { - if (nodes.contains(node)) { + if (footage_nodes.contains(node)) { nodes_to_delete.append(node); } } From e09afd7f6831e39fc66f29a4f1bddd9840c7e936 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Tue, 29 Sep 2020 18:27:30 +0100 Subject: [PATCH 15/19] Fix Issue#1241 When a Block node was deleted its linked blocks were not unlinked which caused a crash when the user tried to select a linked block. This fixes the issue by ensuring all links are removed when a Block node is delete. Undo/redo is also supported. --- app/widget/nodeview/nodeviewundo.cpp | 18 ++++++++++++++++++ app/widget/nodeview/nodeviewundo.h | 1 + 2 files changed, 19 insertions(+) diff --git a/app/widget/nodeview/nodeviewundo.cpp b/app/widget/nodeview/nodeviewundo.cpp index b3eef2300..4c05dd303 100644 --- a/app/widget/nodeview/nodeviewundo.cpp +++ b/app/widget/nodeview/nodeviewundo.cpp @@ -154,6 +154,16 @@ void NodeRemoveCommand::redo_internal() // Take nodes from graph (TakeNode() will automatically disconnect edges) foreach (Node* n, nodes_) { + // If the node is a block, unlink any linked blocks before removing + if (n->IsBlock()) { + Block *b = static_cast(n); + if (b->HasLinks()) { + linked_blocks_.insert(b, b->linked_clips().toList()); + foreach(Block * link, b->linked_clips()) { + b->Unlink(b, link); + } + } + } graph_->TakeNode(n, &memory_manager_); } } @@ -163,6 +173,13 @@ void NodeRemoveCommand::undo_internal() // Re-add nodes to graph foreach (Node* n, nodes_) { graph_->AddNode(n); + // If the node is a block re-link any previous links + if (n->IsBlock()) { + Block *b = static_cast(n); + foreach(Block * link, linked_blocks_[b]) { + b->Link(b, link); + } + } } // Re-connect edges @@ -171,6 +188,7 @@ void NodeRemoveCommand::undo_internal() } edges_.clear(); + linked_blocks_.clear(); } Project *NodeRemoveCommand::GetRelevantProject() const diff --git a/app/widget/nodeview/nodeviewundo.h b/app/widget/nodeview/nodeviewundo.h index 4a9faefd4..181b0e34b 100644 --- a/app/widget/nodeview/nodeviewundo.h +++ b/app/widget/nodeview/nodeviewundo.h @@ -112,6 +112,7 @@ private: NodeGraph* graph_; QList nodes_; QList edges_; + QMap> linked_blocks_; }; class NodeRemoveWithExclusiveDeps : public UndoCommand { From 9d27d557cdbad0eb3ac77e8a99bd2e4fcc66e361 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Wed, 30 Sep 2020 18:14:11 +0100 Subject: [PATCH 16/19] Replace custom unlink with BlockUnlinkAllCommand Re-use existing unlink command and move all the unlinking code to NodeRemoveCommand to cover all bases (deletion of a block in the timeline or in the node editor). --- app/widget/nodeview/nodeviewundo.cpp | 22 +++++++++----------- app/widget/nodeview/nodeviewundo.h | 3 ++- app/widget/timelinewidget/timelinewidget.cpp | 2 -- app/widget/timelinewidget/undo/undo.cpp | 4 ---- 4 files changed, 12 insertions(+), 19 deletions(-) diff --git a/app/widget/nodeview/nodeviewundo.cpp b/app/widget/nodeview/nodeviewundo.cpp index 4c05dd303..032cd0bcd 100644 --- a/app/widget/nodeview/nodeviewundo.cpp +++ b/app/widget/nodeview/nodeviewundo.cpp @@ -158,10 +158,9 @@ void NodeRemoveCommand::redo_internal() if (n->IsBlock()) { Block *b = static_cast(n); if (b->HasLinks()) { - linked_blocks_.insert(b, b->linked_clips().toList()); - foreach(Block * link, b->linked_clips()) { - b->Unlink(b, link); - } + BlockUnlinkAllCommand *unlink_command = new BlockUnlinkAllCommand(b); + unlink_command->redo(); + block_unlink_commands_.append(unlink_command); } } graph_->TakeNode(n, &memory_manager_); @@ -173,13 +172,12 @@ void NodeRemoveCommand::undo_internal() // Re-add nodes to graph foreach (Node* n, nodes_) { graph_->AddNode(n); - // If the node is a block re-link any previous links - if (n->IsBlock()) { - Block *b = static_cast(n); - foreach(Block * link, linked_blocks_[b]) { - b->Link(b, link); - } - } + } + + // Relink any blocks that were unlinked + foreach(BlockUnlinkAllCommand* command, block_unlink_commands_) { + command->undo(); + delete command; } // Re-connect edges @@ -188,7 +186,7 @@ void NodeRemoveCommand::undo_internal() } edges_.clear(); - linked_blocks_.clear(); + block_unlink_commands_.clear(); } Project *NodeRemoveCommand::GetRelevantProject() const diff --git a/app/widget/nodeview/nodeviewundo.h b/app/widget/nodeview/nodeviewundo.h index 181b0e34b..85230c803 100644 --- a/app/widget/nodeview/nodeviewundo.h +++ b/app/widget/nodeview/nodeviewundo.h @@ -27,6 +27,7 @@ #include "node/node.h" #include "nodeviewitem.h" #include "undo/undocommand.h" +#include "widget/timelinewidget/undo/undo.h" OLIVE_NAMESPACE_ENTER @@ -112,7 +113,7 @@ private: NodeGraph* graph_; QList nodes_; QList edges_; - QMap> linked_blocks_; + QList block_unlink_commands_; }; class NodeRemoveWithExclusiveDeps : public UndoCommand { diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index f8b9cb73d..076af1cd1 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -494,8 +494,6 @@ void TimelineWidget::ReplaceBlocksWithGaps(const QList &blocks, new TrackReplaceBlockWithGapCommand(original_track, b, command); if (remove_from_graph) { - new BlockUnlinkAllCommand(b, command); - new NodeRemoveWithExclusiveDeps(static_cast(b->parent()), b, command); } } diff --git a/app/widget/timelinewidget/undo/undo.cpp b/app/widget/timelinewidget/undo/undo.cpp index 36ab7a6bd..9732ffa08 100644 --- a/app/widget/timelinewidget/undo/undo.cpp +++ b/app/widget/timelinewidget/undo/undo.cpp @@ -243,10 +243,6 @@ void TrackRippleRemoveAreaCommand::redo_internal() foreach (Block* remove_block, removed_blocks_) { track_->RippleRemoveBlock(remove_block); - BlockUnlinkAllCommand* unlink_command = new BlockUnlinkAllCommand(remove_block); - unlink_command->redo(); - remove_block_commands_.append(unlink_command); - NodeRemoveWithExclusiveDeps* remove_command = new NodeRemoveWithExclusiveDeps(static_cast(remove_block->parent()), remove_block); remove_command->redo(); remove_block_commands_.append(remove_command); From de9c67a0f8db440942dd88faa339b3ba0805929b Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 3 Oct 2020 23:44:32 +1000 Subject: [PATCH 17/19] project: detect project version and show errors on incompatibility --- app/core.cpp | 1 + app/core.h | 2 ++ app/task/project/load/load.cpp | 18 +++++++++++++++++- app/task/project/save/save.cpp | 5 ++++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/core.cpp b/app/core.cpp index 4aa3a02de..4ed9f7d66 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -68,6 +68,7 @@ OLIVE_NAMESPACE_ENTER Core* Core::instance_ = nullptr; +const uint Core::kProjectVersion = 201003; Core::Core(const CoreParams& params) : main_window_(nullptr), diff --git a/app/core.h b/app/core.h index 71fa43b75..728f4e840 100644 --- a/app/core.h +++ b/app/core.h @@ -268,6 +268,8 @@ public: */ void CacheActiveSequence(bool in_out_only); + static const uint kProjectVersion; + public slots: /** * @brief Starts an open file dialog to load a project from file diff --git a/app/task/project/load/load.cpp b/app/task/project/load/load.cpp index 3d945023a..babfce5ff 100644 --- a/app/task/project/load/load.cpp +++ b/app/task/project/load/load.cpp @@ -25,6 +25,7 @@ #include #include "common/xmlutils.h" +#include "core.h" OLIVE_NAMESPACE_ENTER @@ -45,7 +46,17 @@ bool ProjectLoadTask::Run() if (reader.name() == QStringLiteral("olive")) { while(XMLReadNextStartElement(&reader)) { if (reader.name() == QStringLiteral("version")) { - qDebug() << "Project version:" << reader.readElementText(); + uint project_version = reader.readElementText().toUInt(); + + if (project_version > Core::kProjectVersion) { + // 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 < 201003) { // 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; + } } else if (reader.name() == QStringLiteral("project")) { ProjectPtr project = std::make_shared(); @@ -66,6 +77,11 @@ bool ProjectLoadTask::Run() reader.skipCurrentElement(); } } + } else if (reader.name() == QStringLiteral("project")) { + // 0.1 projects use "project" as the root instead of Olive. We don't currently support + // these projects + SetError(tr("This project is from a version of Olive that is no longer supported in this version.")); + return false; } else { reader.skipCurrentElement(); } diff --git a/app/task/project/save/save.cpp b/app/task/project/save/save.cpp index 211a70c2a..c4947ed31 100644 --- a/app/task/project/save/save.cpp +++ b/app/task/project/save/save.cpp @@ -25,6 +25,7 @@ #include #include "common/filefunctions.h" +#include "core.h" OLIVE_NAMESPACE_ENTER @@ -49,7 +50,9 @@ bool ProjectSaveTask::Run() writer.writeStartElement("olive"); - writer.writeTextElement("version", "0.2.0"); + // Version is stored in YYMMDD from whenever the project format was last changed + // Allows easy integer math for checking project versions. + writer.writeTextElement("version", QString::number(Core::kProjectVersion)); project_->Save(&writer); From d552e7184510848ca43e9717b338b299325fd7eb Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 5 Oct 2020 01:04:45 +1100 Subject: [PATCH 18/19] cleanup and included closing footage viewer if footage is in use --- app/project/projectviewmodel.cpp | 37 --- app/project/projectviewmodel.h | 24 -- app/widget/projectexplorer/CMakeLists.txt | 2 + .../projectexplorer/projectexplorer.cpp | 218 ++++++++---------- app/widget/projectexplorer/projectexplorer.h | 19 +- .../projectexplorer/projectexplorerundo.cpp | 54 +++++ .../projectexplorer/projectexplorerundo.h | 52 +++++ 7 files changed, 206 insertions(+), 200 deletions(-) create mode 100644 app/widget/projectexplorer/projectexplorerundo.cpp create mode 100644 app/widget/projectexplorer/projectexplorerundo.h diff --git a/app/project/projectviewmodel.cpp b/app/project/projectviewmodel.cpp index 8c176f24f..a1ba9b481 100644 --- a/app/project/projectviewmodel.cpp +++ b/app/project/projectviewmodel.cpp @@ -604,41 +604,4 @@ void ProjectViewModel::RemoveItemCommand::undo_internal() model_->AddChild(parent_, item_); } -ProjectViewModel::OfflineFootageCommand::OfflineFootageCommand(ProjectViewModel* model, ItemPtr item, - QMap nodes, QUndoCommand* parent) : - UndoCommand(parent), - model_(model), - item_(item), - nodes_(nodes) -{ -} - -Project *ProjectViewModel::OfflineFootageCommand::GetRelevantProject() const -{ - return model_->project(); -} - -void ProjectViewModel::OfflineFootageCommand::redo_internal() -{ - QMap::const_iterator it = nodes_.constBegin(); - while (it != nodes_.constEnd()) { - static_cast(it.key())->SetFootage(nullptr); - ++it; - } - - parent_ = item_->parent(); - model_->RemoveChild(parent_, item_.get()); -} - -void ProjectViewModel::OfflineFootageCommand::undo_internal() -{ - model_->AddChild(parent_, item_); - - QMap::const_iterator it = nodes_.constBegin(); - while (it != nodes_.constEnd()) { - static_cast(it.key())->SetFootage(it.value()); - ++it; - } -} - OLIVE_NAMESPACE_EXIT diff --git a/app/project/projectviewmodel.h b/app/project/projectviewmodel.h index 8da1c2472..e3de6e33a 100644 --- a/app/project/projectviewmodel.h +++ b/app/project/projectviewmodel.h @@ -195,30 +195,6 @@ public: }; - /** - * @brief An undo command for offlining footage when it is deleted from the project explorer - */ - class OfflineFootageCommand : public UndoCommand { - public: - OfflineFootageCommand(ProjectViewModel* model, ItemPtr item, QMap nodes, QUndoCommand* parent = nullptr); - - virtual Project* GetRelevantProject() const override; - - protected: - virtual void redo_internal() override; - - virtual void undo_internal() override; - - private: - ProjectViewModel* model_; - - ItemPtr item_; - - Item* parent_; - - QMap nodes_; - }; - private: /** * @brief Retrieve the index of `item` in its parent diff --git a/app/widget/projectexplorer/CMakeLists.txt b/app/widget/projectexplorer/CMakeLists.txt index e999f1d1d..cc8d45b76 100644 --- a/app/widget/projectexplorer/CMakeLists.txt +++ b/app/widget/projectexplorer/CMakeLists.txt @@ -32,5 +32,7 @@ set(OLIVE_SOURCES widget/projectexplorer/projectexplorericonviewitemdelegate.cpp widget/projectexplorer/projectexplorernavigation.h widget/projectexplorer/projectexplorernavigation.cpp + widget/projectexplorer/projectexplorerundo.h + widget/projectexplorer/projectexplorerundo.cpp PARENT_SCOPE ) diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index b63a40919..9041e9f38 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -32,6 +32,7 @@ #include "core.h" #include "dialog/footageproperties/footageproperties.h" #include "dialog/sequence/sequence.h" +#include "projectexplorerundo.h" #include "task/precache/precachetask.h" #include "task/taskmanager.h" #include "widget/menu/menu.h" @@ -539,104 +540,28 @@ void ProjectExplorer::DeselectAll() CurrentView()->selectionModel()->clearSelection(); } -QMap ProjectExplorer::GetFootageNodes(Item* item) +QList ProjectExplorer::GetMediaNodesUsingFootage(Footage *item) { - // Output list list - QMap footage_nodes; + QList list; // Get all sequences. QList sequences = model_.project()->get_items_of_type(Item::kSequence); - // Get item pointer. - ItemPtr item_ptr = item->get_shared_ptr(); - if (item_ptr.get()->type() == Item::kFootage) { - // If no sequences exist we don't need to do anything clever here - if (!sequences.isEmpty()) { - // Footage can contain multiple streams, all of which need to be dealt with - foreach (StreamPtr stream, static_cast(item_ptr.get())->streams()) { - // Check each sequence to see if it contains the footage in question - foreach (ItemPtr seq, sequences) { - Sequence* s = static_cast(seq.get()); + // Footage can contain multiple streams, all of which need to be dealt with + foreach (ItemPtr s, sequences) { + const QList& nodes = static_cast(s.get())->nodes(); + foreach (Node* n, nodes) { + if (n->IsMedia()) { + MediaInput* media_node = static_cast(n); - // Loop through nodes to find our Footage node - foreach (Node* node, s->nodes()) { - // Check if node is of the right type - if (node->IsMedia()){ - // Check the streams are the same - if (static_cast(node)->footage() == stream) { - footage_nodes.insert(node, stream); - } - } - } + if (media_node->footage()->footage() == item) { + list.append(media_node); } } } } - return footage_nodes; -} -QList ProjectExplorer::GetFootageBlocks(QList nodes) -{ - // Get all sequences. - QList sequences = model_.project()->get_items_of_type(Item::kSequence); - - QList blocks; - - foreach (ItemPtr seq, sequences) { - Sequence* s = static_cast(seq.get()); - // Loop through nodes in sequence - foreach (Node* node, s->nodes()) { - // For each Block see if it solely depends on one of our input nodes and if so add to the block list - if (node->IsBlock()) { - int footage_deps = 0; - - QList dependancies = node->GetDependencies(); - QSet intersection = QSet(dependancies.begin(), dependancies.end()) - .intersect(QSet(nodes.begin(), nodes.end())); - if (!intersection.isEmpty()) { - // Count how many Media inputs this block depends on - foreach (Node* dep, dependancies) { - if (dep->IsMedia()) { - footage_deps++; - } - } - // If it only depends on one input we can safely delete it - if (footage_deps == 1) { - blocks.append(static_cast(node)); - } - } - } - } - } - - return blocks; -} - -ProjectExplorer::FootageDeleteResponse ProjectExplorer::DeleteWarningMessage(Item* item) -{ - ItemPtr item_ptr = item->get_shared_ptr(); - - if (item_ptr->type() == Item::kFootage) { - QString clip_name = static_cast(item_ptr.get())->filename().split("/").last(); - - QMessageBox msgBox; - msgBox.setWindowTitle(clip_name); - msgBox.setText(clip_name + tr(" is in use.")); - QPushButton* offline = msgBox.addButton(tr("Offline Footage"), QMessageBox::ApplyRole); - QPushButton* deleteClips = msgBox.addButton(tr("Delete Clips"), QMessageBox::ApplyRole); - msgBox.setStandardButtons(QMessageBox::Cancel); - msgBox.setIcon(QMessageBox::Warning); - - msgBox.exec(); - - if (msgBox.clickedButton() == offline) { - return kOffline; - } - if (msgBox.clickedButton() == deleteClips) { - return kDelete; - } - } - return kCancel; + return list; } void ProjectExplorer::DeleteSelected() @@ -650,60 +575,109 @@ void ProjectExplorer::DeleteSelected() QUndoCommand* command = new QUndoCommand(); foreach (Item* item, selected) { - ItemPtr item_ptr = item->get_shared_ptr(); - - // If this is a sequence, close it - if (item_ptr->type() == Item::kSequence) { - Sequence* s = static_cast(item_ptr.get()); + // Verify whether this item is in use anywhere + switch (item->type()) { + case Item::kSequence: + { + // If this is a sequence, check if it's open and close it if necessary + Sequence* s = static_cast(item); if (Core::instance()->main_window()->IsSequenceOpen(s)) { Core::instance()->main_window()->CloseSequence(s); } - - new ProjectViewModel::RemoveItemCommand(&model_, item_ptr, command); + break; } + case Item::kFootage: + { + // If this is footage, check if it's used anywhere in any sequence + Footage* footage = static_cast(item); - // If this is a footage item, clean up if necessary - if (item_ptr->type() == Item::kFootage) { - - // Check if nodes exists - QMap footage_nodes = GetFootageNodes(item); - if (!footage_nodes.isEmpty()){ - // Warn user and ask them what to do - FootageDeleteResponse response = DeleteWarningMessage(item); - if (response == kOffline) { - new ProjectViewModel::OfflineFootageCommand(&model_, item_ptr, footage_nodes, command); + QList footage_nodes = GetMediaNodesUsingFootage(footage); + + if (!footage_nodes.isEmpty()) { + // Footage is in use, show messagebox asking what to do about it + QList used_in_sequences; + + // Compile list of sequences to assist the user in making this decision + foreach (MediaInput* i, footage_nodes) { + Sequence* media_parent = static_cast(i->parent()); + + if (!used_in_sequences.contains(media_parent)) { + used_in_sequences.append(media_parent); + } } - if (response == kDelete) { - QUndoCommand* deleteCommand = new QUndoCommand(command); - // Delete any non-composite blocks - TimelineWidget::ReplaceBlocksWithGaps(GetFootageBlocks(footage_nodes.keys()), true, deleteCommand); - new ProjectViewModel::RemoveItemCommand(&model_, item_ptr, command); - // Catch any input nodes we missed due to composites etc. + QString sequence_list_str; + foreach (Sequence* s, used_in_sequences) { + sequence_list_str.append(QStringLiteral("%1\n").arg(s->name())); + } - QList sequences = model_.project()->get_items_of_type(Item::kSequence); + QMessageBox msgbox(this); + msgbox.setWindowTitle(tr("Confirm Footage Deletion")); + msgbox.setText(tr("The footage \"%1\" is currently used in the following sequence(s):\n\n" + "%2\nWhat would you like to do with these clips?") + .arg(footage->filename(), sequence_list_str)); + msgbox.setIcon(QMessageBox::Warning); - QList nodes_to_delete; - foreach (ItemPtr seq, sequences) { - Sequence* s = static_cast(seq.get()); - foreach (Node* node, s->nodes()) { - if (node->IsMedia()) { - if (footage_nodes.contains(node)) { - nodes_to_delete.append(node); + // Set up buttons + QPushButton* offline_btn = msgbox.addButton(tr("Offline Footage"), QMessageBox::YesRole); + QPushButton* delete_clip_btn = msgbox.addButton(tr("Delete Clips"), QMessageBox::NoRole); + msgbox.addButton(QMessageBox::Cancel); + + // Run messagebox + msgbox.exec(); + + if (msgbox.clickedButton() == offline_btn || msgbox.clickedButton() == delete_clip_btn) { + + // For safety, even if we're deleting clips, we'll offline the footage nodes too + new OfflineFootageCommand(footage_nodes, command); + + } + + if (msgbox.clickedButton() == delete_clip_btn) { + + // Delete any blocks that use this footage + QList blocks_to_remove; + + foreach (Sequence* s, used_in_sequences) { + foreach (TrackOutput* track, s->viewer_output()->GetTracks()) { + foreach (Block* b, track->Blocks()) { + QList deps = b->GetDependencies(); + + foreach (MediaInput* i, footage_nodes) { + if (deps.contains(i)) { + blocks_to_remove.append(b); + break; + } } } } - QUndoCommand* deleteNodesCommand = new QUndoCommand(deleteCommand); - new NodeRemoveCommand(static_cast(s), nodes_to_delete, deleteNodesCommand); } - } - if (response == kCancel) { + + TimelineWidget::ReplaceBlocksWithGaps(blocks_to_remove, true, command); + + } else if (msgbox.clickedButton() != offline_btn) { + + // Must have cancelled delete command; return; + } } + + // Close footage if currently open in footage panel + FootageViewerPanel* footage_panel = PanelManager::instance()->GetPanelsOfType().first(); + if (footage_panel->GetSelectedFootage().contains(footage)) { + footage_panel->SetFootage(nullptr); + } + break; } + case Item::kFolder: + // Do nothing + break; + } + + new ProjectViewModel::RemoveItemCommand(&model_, item->get_shared_ptr(), command); } Core::instance()->undo_stack()->pushIfHasChildren(command); diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index daca3aec1..90dd10841 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -25,6 +25,7 @@ #include #include +#include "node/input/media/media.h" #include "project/project.h" #include "project/projectviewmodel.h" #include "widget/projectexplorer/projectexplorericonview.h" @@ -101,26 +102,10 @@ signals: void DoubleClickedItem(Item* item); private: - enum FootageDeleteResponse { - kDelete, - kOffline, - kCancel - }; - - - /** - * @brief Pop up a QMessageBox to warn the user if the deleted clips are in use - * - * Returns a FootageDeleteResponse - */ - FootageDeleteResponse DeleteWarningMessage(Item* item); - /** * @brief Check if an item is in use anywhere and return any relevant input nodes - * - * Returns a QMap pairing a Footage node to its StreamPtr */ - QMap GetFootageNodes(Item* item); + QList GetMediaNodesUsingFootage(Footage* item); /** * @brief Get all the blocks that solely rely on an input node diff --git a/app/widget/projectexplorer/projectexplorerundo.cpp b/app/widget/projectexplorer/projectexplorerundo.cpp new file mode 100644 index 000000000..584d4a56c --- /dev/null +++ b/app/widget/projectexplorer/projectexplorerundo.cpp @@ -0,0 +1,54 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 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 "projectexplorerundo.h" + +OLIVE_NAMESPACE_ENTER + +OfflineFootageCommand::OfflineFootageCommand(const QList &media, QUndoCommand* parent) : + UndoCommand(parent) +{ + foreach (MediaInput* i, media) { + stream_data_.insert(i, i->footage()); + } + + project_ = static_cast(media.first()->parent())->project(); +} + +Project *OfflineFootageCommand::GetRelevantProject() const +{ + return project_; +} + +void OfflineFootageCommand::redo_internal() +{ + for (auto it=stream_data_.cbegin(); it!=stream_data_.cend(); it++) { + it.key()->SetFootage(nullptr); + } +} + +void OfflineFootageCommand::undo_internal() +{ + for (auto it=stream_data_.cbegin(); it!=stream_data_.cend(); it++) { + it.key()->SetFootage(it.value()); + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/widget/projectexplorer/projectexplorerundo.h b/app/widget/projectexplorer/projectexplorerundo.h new file mode 100644 index 000000000..8aaabdc0e --- /dev/null +++ b/app/widget/projectexplorer/projectexplorerundo.h @@ -0,0 +1,52 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 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 PROJECTEXPLORERUNDO_H +#define PROJECTEXPLORERUNDO_H + +#include "node/input/media/media.h" +#include "undo/undocommand.h" + +OLIVE_NAMESPACE_ENTER + +/** + * @brief An undo command for offlining footage when it is deleted from the project explorer + */ +class OfflineFootageCommand : public UndoCommand { +public: + OfflineFootageCommand(const QList& media, QUndoCommand* parent = nullptr); + + virtual Project* GetRelevantProject() const override; + +protected: + virtual void redo_internal() override; + + virtual void undo_internal() override; + +private: + QMap stream_data_; + + Project* project_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // PROJECTEXPLORERUNDO_H From ac4be0907f73400e60cbb373d539ef32fea39f9c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 5 Oct 2020 01:07:30 +1100 Subject: [PATCH 19/19] cmake: move application install outside of crashpad if statement --- app/CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 1e35af257..3df35b575 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -168,6 +168,10 @@ elseif (APPLE) ) endif() +if(UNIX AND NOT APPLE) + install(TARGETS ${OLIVE_TARGET} RUNTIME DESTINATION bin) +endif() + # Enable Crashpad if found if (GoogleCrashpad_FOUND) set(OLIVE_DEFINITIONS ${OLIVE_DEFINITIONS} USE_CRASHPAD) @@ -225,7 +229,7 @@ if (GoogleCrashpad_FOUND) set(MINIDUMP_STACKWALK "minidump_stackwalk${CMAKE_EXECUTABLE_SUFFIX}") if(UNIX AND NOT APPLE) - install(TARGETS ${OLIVE_TARGET} ${OLIVE_CRASH_TARGET} RUNTIME DESTINATION bin) + install(TARGETS ${OLIVE_CRASH_TARGET} RUNTIME DESTINATION bin) install(PROGRAMS ${CRASHPAD_LIBRARY_DIRS}/${CRASHPAD_HANDLER} DESTINATION bin) install(PROGRAMS ${BREAKPAD_BIN_DIR}/${MINIDUMP_STACKWALK} DESTINATION bin) endif()