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/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..8da1c2472 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 @@ -194,6 +195,30 @@ 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/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 0a1dd0ab7..b63a40919 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,8 @@ #include "widget/menu/menu.h" #include "widget/menu/menushared.h" #include "window/mainwindow/mainwindow.h" +#include "widget/timelinewidget/timelinewidget.h" +#include "widget/nodeview/nodeviewundo.h" OLIVE_NAMESPACE_ENTER @@ -536,6 +539,106 @@ void ProjectExplorer::DeselectAll() CurrentView()->selectionModel()->clearSelection(); } +QMap ProjectExplorer::GetFootageNodes(Item* item) +{ + // Output list list + QMap footage_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 (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()); + + // 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); + } + } + } + } + } + } + } + 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; +} + void ProjectExplorer::DeleteSelected() { QList selected = SelectedItems(); @@ -556,9 +659,51 @@ void ProjectExplorer::DeleteSelected() if (Core::instance()->main_window()->IsSequenceOpen(s)) { Core::instance()->main_window()->CloseSequence(s); } + + new ProjectViewModel::RemoveItemCommand(&model_, item_ptr, command); } - 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 + 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); + } + 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. + + 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 (footage_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; + return; + } + } + } } Core::instance()->undo_stack()->pushIfHasChildren(command); diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index 2fe097bbd..daca3aec1 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -101,6 +101,34 @@ 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); + + /** + * @brief Get all the blocks that solely rely on an input node + * + * Ignores blocks that depend on multiple inputs + */ + QList GetFootageBlocks(QList nodes); + /** * @brief Simple convenience function for adding a view to this stacked widget *