Merge branch 'delete_footage' of https://github.com/ThomasWilshaw/olive into ThomasWilshaw-delete_footage

This commit is contained in:
itsmattkc
2020-10-04 21:42:11 +11:00
12 changed files with 276 additions and 1 deletions
+5
View File
@@ -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");
+2
View File
@@ -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;
+5
View File
@@ -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"));
+6
View File
@@ -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<CategoryID> 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;
+5
View File
@@ -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");
+2
View File
@@ -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;
+5
View File
@@ -440,6 +440,11 @@ bool Node::IsTrack() const
return false;
}
bool Node::IsMedia() const
{
return false;
}
const QList<NodeParam *>& Node::parameters() const
{
return params_;
+9
View File
@@ -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
*
+38
View File
@@ -25,6 +25,7 @@
#include <QUrl>
#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<Node*, StreamPtr> 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<Node *, StreamPtr>::const_iterator it = nodes_.constBegin();
while (it != nodes_.constEnd()) {
static_cast<MediaInput *>(it.key())->SetFootage(nullptr);
++it;
}
parent_ = item_->parent();
model_->RemoveChild(parent_, item_.get());
}
void ProjectViewModel::OfflineFootageCommand::undo_internal()
{
model_->AddChild(parent_, item_);
QMap<Node *, StreamPtr>::const_iterator it = nodes_.constBegin();
while (it != nodes_.constEnd()) {
static_cast<MediaInput *>(it.key())->SetFootage(it.value());
++it;
}
}
OLIVE_NAMESPACE_EXIT
+25
View File
@@ -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<Node*, StreamPtr> 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<Node*, StreamPtr> nodes_;
};
private:
/**
* @brief Retrieve the index of `item` in its parent
+146 -1
View File
@@ -23,6 +23,7 @@
#include <QDebug>
#include <QDesktopServices>
#include <QDir>
#include <QMessageBox>
#include <QProcess>
#include <QUrl>
#include <QVBoxLayout>
@@ -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<Node*, StreamPtr> ProjectExplorer::GetFootageNodes(Item* item)
{
// Output list list
QMap<Node*, StreamPtr> footage_nodes;
// Get all sequences.
QList<ItemPtr> 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<Footage*>(item_ptr.get())->streams()) {
// Check each sequence to see if it contains the footage in question
foreach (ItemPtr seq, sequences) {
Sequence* s = static_cast<Sequence*>(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<MediaInput*>(node)->footage() == stream) {
footage_nodes.insert(node, stream);
}
}
}
}
}
}
}
return footage_nodes;
}
QList<Block*> ProjectExplorer::GetFootageBlocks(QList<Node*> nodes)
{
// Get all sequences.
QList<ItemPtr> sequences = model_.project()->get_items_of_type(Item::kSequence);
QList<Block*> blocks;
foreach (ItemPtr seq, sequences) {
Sequence* s = static_cast<Sequence*>(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<Node*> dependancies = node->GetDependencies();
QSet<Node*> intersection = QSet<Node*>(dependancies.begin(), dependancies.end())
.intersect(QSet<Node*>(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<Block*>(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<Footage*>(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<Item*> 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<Node*, StreamPtr> 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<ItemPtr> sequences = model_.project()->get_items_of_type(Item::kSequence);
QList<Node*> nodes_to_delete;
foreach (ItemPtr seq, sequences) {
Sequence* s = static_cast<Sequence*>(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<NodeGraph*>(s), nodes_to_delete, deleteNodesCommand);
}
}
if (response == kCancel) {
delete command;
return;
}
}
}
}
Core::instance()->undo_stack()->pushIfHasChildren(command);
@@ -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<Node*, StreamPtr> GetFootageNodes(Item* item);
/**
* @brief Get all the blocks that solely rely on an input node
*
* Ignores blocks that depend on multiple inputs
*/
QList<Block*> GetFootageBlocks(QList<Node*> nodes);
/**
* @brief Simple convenience function for adding a view to this stacked widget
*