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() 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/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 04b96113c..109be2345 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..a1ba9b481 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 diff --git a/app/project/projectviewmodel.h b/app/project/projectviewmodel.h index 964378316..e3de6e33a 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 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); diff --git a/app/widget/nodeview/nodeviewundo.cpp b/app/widget/nodeview/nodeviewundo.cpp index b3eef2300..032cd0bcd 100644 --- a/app/widget/nodeview/nodeviewundo.cpp +++ b/app/widget/nodeview/nodeviewundo.cpp @@ -154,6 +154,15 @@ 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()) { + BlockUnlinkAllCommand *unlink_command = new BlockUnlinkAllCommand(b); + unlink_command->redo(); + block_unlink_commands_.append(unlink_command); + } + } graph_->TakeNode(n, &memory_manager_); } } @@ -165,12 +174,19 @@ void NodeRemoveCommand::undo_internal() graph_->AddNode(n); } + // Relink any blocks that were unlinked + foreach(BlockUnlinkAllCommand* command, block_unlink_commands_) { + command->undo(); + delete command; + } + // Re-connect edges foreach (NodeEdgePtr edge, edges_) { NodeParam::ConnectEdge(edge->output(), edge->input()); } edges_.clear(); + block_unlink_commands_.clear(); } Project *NodeRemoveCommand::GetRelevantProject() const diff --git a/app/widget/nodeview/nodeviewundo.h b/app/widget/nodeview/nodeviewundo.h index 05837a21d..6b2854e38 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,6 +113,7 @@ private: NodeGraph* graph_; QList nodes_; QList edges_; + QList block_unlink_commands_; }; class NodeRemoveWithExclusiveDeps : public UndoCommand { 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 0a1dd0ab7..9041e9f38 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -31,11 +32,14 @@ #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" #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 +540,30 @@ void ProjectExplorer::DeselectAll() CurrentView()->selectionModel()->clearSelection(); } +QList ProjectExplorer::GetMediaNodesUsingFootage(Footage *item) +{ + QList list; + + // Get all sequences. + QList sequences = model_.project()->get_items_of_type(Item::kSequence); + + // 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); + + if (media_node->footage()->footage() == item) { + list.append(media_node); + } + } + } + } + + return list; +} + void ProjectExplorer::DeleteSelected() { QList selected = SelectedItems(); @@ -547,18 +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); } + break; + } + case Item::kFootage: + { + // If this is footage, check if it's used anywhere in any sequence + Footage* footage = static_cast(item); + + 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); + } + } + + QString sequence_list_str; + foreach (Sequence* s, used_in_sequences) { + sequence_list_str.append(QStringLiteral("%1\n").arg(s->name())); + } + + 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); + + // 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; + } + } + } + } + } + + 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_ptr, command); + 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 2fe097bbd..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,6 +102,18 @@ signals: void DoubleClickedItem(Item* item); private: + /** + * @brief Check if an item is in use anywhere and return any relevant input nodes + */ + QList GetMediaNodesUsingFootage(Footage* 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 * 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 diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 2d6fe406f..9a0751df1 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -487,8 +487,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 824228eb9..7f968e29e 100644 --- a/app/widget/timelinewidget/undo/undo.cpp +++ b/app/widget/timelinewidget/undo/undo.cpp @@ -244,10 +244,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);