1845 lines
49 KiB
C++
1845 lines
49 KiB
C++
/***
|
|
|
|
Olive - Non-Linear Video Editor
|
|
Copyright (C) 2022 Olive Team
|
|
Modifications Copyright (C) 2025 mikesolar
|
|
|
|
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 <http://www.gnu.org/licenses/>.
|
|
|
|
***/
|
|
|
|
#include "core.h"
|
|
|
|
#include <QApplication>
|
|
#include <QClipboard>
|
|
#include <QDebug>
|
|
#include <QFileDialog>
|
|
#include <QFileInfo>
|
|
#include <QHBoxLayout>
|
|
#include <QInputDialog>
|
|
#include <QMessageBox>
|
|
#include <QStatusBar>
|
|
#include <QStyleFactory>
|
|
#include "window/mainwindow/mainwindowundo.h"
|
|
#ifdef Q_OS_WINDOWS
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
|
#include <QtPlatformHeaders/QWindowsWindowFunctions>
|
|
#endif
|
|
#endif
|
|
|
|
#include "audio/audiomanager.h"
|
|
#include "cli/clitask/clitaskdialog.h"
|
|
#include "codec/conformmanager.h"
|
|
#include "common/filefunctions.h"
|
|
#include "common/xmlutils.h"
|
|
#include "config/config.h"
|
|
#include "dialog/about/about.h"
|
|
#include "dialog/autorecovery/autorecoverydialog.h"
|
|
#include "dialog/export/export.h"
|
|
#include "dialog/footagerelink/footagerelinkdialog.h"
|
|
#ifdef USE_OTIO
|
|
#include "dialog/otioproperties/otiopropertiesdialog.h"
|
|
#endif
|
|
#include "dialog/projectproperties/projectproperties.h"
|
|
#include "dialog/sequence/sequence.h"
|
|
#include "dialog/task/task.h"
|
|
#include "dialog/preferences/preferences.h"
|
|
#include "node/color/colormanager/colormanager.h"
|
|
#include "node/factory.h"
|
|
#include "node/nodeundo.h"
|
|
#include "node/project/serializer/serializer.h"
|
|
#include "panel/panelmanager.h"
|
|
#include "panel/project/project.h"
|
|
#include "panel/viewer/viewer.h"
|
|
#include "render/diskmanager.h"
|
|
#include "render/framemanager.h"
|
|
#include "render/rendermanager.h"
|
|
#ifdef USE_OTIO
|
|
#include "task/project/loadotio/loadotio.h"
|
|
#include "task/project/saveotio/saveotio.h"
|
|
#endif
|
|
#include "task/project/import/import.h"
|
|
#include "task/project/import/importerrordialog.h"
|
|
#include "task/project/load/load.h"
|
|
#include "task/project/save/save.h"
|
|
#include "task/taskmanager.h"
|
|
#include "ui/style/style.h"
|
|
#include "undo/undostack.h"
|
|
#include "widget/menu/menushared.h"
|
|
#include "window/mainwindow/mainwindow.h"
|
|
|
|
namespace {
|
|
|
|
QStringList FootageVideoExtensions()
|
|
{
|
|
return QStringList{
|
|
QStringLiteral("mp4"), QStringLiteral("mov"), QStringLiteral("m4v"),
|
|
QStringLiteral("avi"), QStringLiteral("mpg"), QStringLiteral("mpeg"),
|
|
QStringLiteral("m2ts"), QStringLiteral("mts"), QStringLiteral("ts"),
|
|
QStringLiteral("webm"), QStringLiteral("wmv"), QStringLiteral("flv"),
|
|
QStringLiteral("3gp"), QStringLiteral("3g2"), QStringLiteral("mxf")
|
|
};
|
|
}
|
|
|
|
QStringList FootageAudioExtensions()
|
|
{
|
|
return QStringList{
|
|
QStringLiteral("wav"), QStringLiteral("mp3"), QStringLiteral("flac"),
|
|
QStringLiteral("aac"), QStringLiteral("ogg"), QStringLiteral("opus"),
|
|
QStringLiteral("m4a"), QStringLiteral("alac"), QStringLiteral("aif"),
|
|
QStringLiteral("aiff"), QStringLiteral("aifc"), QStringLiteral("wma")
|
|
};
|
|
}
|
|
|
|
QStringList FootageImageExtensions()
|
|
{
|
|
return QStringList{
|
|
QStringLiteral("png"), QStringLiteral("jpg"), QStringLiteral("jpeg"),
|
|
QStringLiteral("tif"), QStringLiteral("tiff"), QStringLiteral("bmp"),
|
|
QStringLiteral("gif"), QStringLiteral("exr"), QStringLiteral("dpx"),
|
|
QStringLiteral("webp")
|
|
};
|
|
}
|
|
|
|
QString BuildFootageFilterGroup(const QString &label,
|
|
const QStringList &extensions)
|
|
{
|
|
QStringList patterns;
|
|
patterns.reserve(extensions.size());
|
|
for (const QString &ext : extensions) {
|
|
patterns.append(QStringLiteral("*.%1").arg(ext));
|
|
}
|
|
|
|
return QStringLiteral("%1 (%2)")
|
|
.arg(label, patterns.join(QLatin1Char(' ')));
|
|
}
|
|
|
|
QString BuildFootageFileDialogFilter()
|
|
{
|
|
QStringList all = FootageVideoExtensions() + FootageAudioExtensions() +
|
|
FootageImageExtensions();
|
|
all.removeDuplicates();
|
|
|
|
QStringList groups;
|
|
groups << BuildFootageFilterGroup(QObject::tr("Common Media Files"), all);
|
|
groups << BuildFootageFilterGroup(QObject::tr("Video Files"),
|
|
FootageVideoExtensions());
|
|
groups << BuildFootageFilterGroup(QObject::tr("Audio Files"),
|
|
FootageAudioExtensions());
|
|
groups << BuildFootageFilterGroup(QObject::tr("Image Files"),
|
|
FootageImageExtensions());
|
|
|
|
return groups.join(QStringLiteral(";;"));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace olive
|
|
{
|
|
|
|
Core *Core::instance_ = nullptr;
|
|
|
|
Core::Core(const CoreParams ¶ms)
|
|
: main_window_(nullptr)
|
|
, open_project_(nullptr)
|
|
, tool_(Tool::kPointer)
|
|
, addable_object_(Tool::kAddableEmpty)
|
|
, snapping_(true)
|
|
, core_params_(params)
|
|
, magic_(false)
|
|
, pixel_sampling_users_(0)
|
|
, shown_cache_full_warning_(false)
|
|
{
|
|
// Store reference to this object, making the assumption that Core will only ever be made in
|
|
// main(). This will obviously break if not.
|
|
instance_ = this;
|
|
|
|
translator_ = new QTranslator(this);
|
|
}
|
|
|
|
Core *Core::instance()
|
|
{
|
|
return instance_;
|
|
}
|
|
|
|
QString Core::FootageFileDialogFilter()
|
|
{
|
|
return BuildFootageFileDialogFilter();
|
|
}
|
|
|
|
QStringList Core::AllowedFootageExtensions()
|
|
{
|
|
QStringList all = FootageVideoExtensions() + FootageAudioExtensions() +
|
|
FootageImageExtensions();
|
|
all.removeDuplicates();
|
|
return all;
|
|
}
|
|
|
|
bool Core::IsFootageExtensionAllowed(const QString &path)
|
|
{
|
|
const QString ext = QFileInfo(path).suffix().toLower();
|
|
if (ext.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
return AllowedFootageExtensions().contains(ext);
|
|
}
|
|
|
|
void Core::DeclareTypesForQt()
|
|
{
|
|
qRegisterMetaType<olive::core::rational>();
|
|
qRegisterMetaType<NodeValue>();
|
|
qRegisterMetaType<NodeValueTable>();
|
|
qRegisterMetaType<NodeValueDatabase>();
|
|
qRegisterMetaType<FramePtr>();
|
|
qRegisterMetaType<SampleBuffer>();
|
|
qRegisterMetaType<AudioParams>();
|
|
qRegisterMetaType<NodeKeyframe::Type>();
|
|
qRegisterMetaType<Decoder::RetrieveState>();
|
|
qRegisterMetaType<olive::core::TimeRange>();
|
|
qRegisterMetaType<olive::core::Color>();
|
|
qRegisterMetaType<olive::AudioVisualWaveform>();
|
|
qRegisterMetaType<olive::VideoParams>();
|
|
qRegisterMetaType<olive::VideoParams::Interlacing>();
|
|
qRegisterMetaType<olive::MainWindowLayoutInfo>();
|
|
qRegisterMetaType<olive::RenderTicketPtr>();
|
|
}
|
|
|
|
void Core::Start()
|
|
{
|
|
// Load application config
|
|
Config::Load();
|
|
|
|
// Set locale based on either startup arg, config, or auto-detect
|
|
SetStartupLocale();
|
|
|
|
// Declare custom types for Qt signal/slot system
|
|
DeclareTypesForQt();
|
|
|
|
// Set up node factory/library
|
|
NodeFactory::Initialize();
|
|
|
|
// Set up color manager's default config
|
|
ColorManager::SetUpDefaultConfig();
|
|
|
|
// Initialize task manager
|
|
TaskManager::CreateInstance();
|
|
|
|
// Initialize ConformManager
|
|
ConformManager::CreateInstance();
|
|
|
|
// Initialize RenderManager
|
|
RenderManager::CreateInstance();
|
|
|
|
// Initialize FrameManager
|
|
FrameManager::CreateInstance();
|
|
|
|
// Initialize project serializers
|
|
ProjectSerializer::Initialize();
|
|
|
|
//
|
|
// Start application
|
|
//
|
|
|
|
qInfo() << "Using Qt version:" << qVersion();
|
|
|
|
switch (core_params_.run_mode()) {
|
|
case CoreParams::kRunNormal:
|
|
// Start GUI
|
|
StartGUI(core_params_.fullscreen());
|
|
|
|
// If we have a startup
|
|
QMetaObject::invokeMethod(this, "OpenStartupProject",
|
|
Qt::QueuedConnection);
|
|
break;
|
|
case CoreParams::kHeadlessExport:
|
|
qInfo() << "Headless export is not fully implemented yet";
|
|
break;
|
|
case CoreParams::kHeadlessPreCache:
|
|
qInfo() << "Headless pre-cache is not fully implemented yet";
|
|
break;
|
|
}
|
|
|
|
// Manual crash triggering
|
|
if (core_params_.crash_on_startup()) {
|
|
const int interval = 5000;
|
|
qInfo() << "Manual crash was triggered. Application will crash in"
|
|
<< interval << "ms";
|
|
QTimer *crash_timer = new QTimer(this);
|
|
crash_timer->setInterval(interval);
|
|
connect(crash_timer, &QTimer::timeout, this, [] { abort(); });
|
|
crash_timer->start();
|
|
}
|
|
}
|
|
|
|
void Core::Stop()
|
|
{
|
|
// Assume all projects have closed gracefully and no auto-recovery is necessary
|
|
autorecovered_projects_.clear();
|
|
SaveUnrecoveredList();
|
|
|
|
// Save Config
|
|
Config::Save();
|
|
|
|
ProjectSerializer::Destroy();
|
|
|
|
ConformManager::DestroyInstance();
|
|
|
|
FrameManager::DestroyInstance();
|
|
|
|
RenderManager::DestroyInstance();
|
|
|
|
MenuShared::DestroyInstance();
|
|
|
|
TaskManager::DestroyInstance();
|
|
|
|
PanelManager::DestroyInstance();
|
|
|
|
AudioManager::DestroyInstance();
|
|
|
|
DiskManager::DestroyInstance();
|
|
|
|
NodeFactory::Destroy();
|
|
|
|
delete main_window_;
|
|
main_window_ = nullptr;
|
|
}
|
|
|
|
MainWindow *Core::main_window()
|
|
{
|
|
return main_window_;
|
|
}
|
|
|
|
UndoStack *Core::undo_stack()
|
|
{
|
|
return &undo_stack_;
|
|
}
|
|
|
|
void Core::ImportFiles(const QStringList &urls, Folder *parent)
|
|
{
|
|
if (urls.isEmpty()) {
|
|
QMessageBox::critical(main_window_, tr("Import error"),
|
|
tr("Nothing to import"));
|
|
return;
|
|
}
|
|
|
|
QStringList filtered_urls;
|
|
QStringList rejected_urls;
|
|
filtered_urls.reserve(urls.size());
|
|
|
|
for (const QString &url : urls) {
|
|
if (IsFootageExtensionAllowed(url)) {
|
|
filtered_urls.append(url);
|
|
} else {
|
|
rejected_urls.append(url);
|
|
}
|
|
}
|
|
|
|
if (!rejected_urls.isEmpty()) {
|
|
QMessageBox::warning(
|
|
main_window_, tr("Unsupported media"),
|
|
tr("Skipped %1 file(s) that are not allowed by the current media "
|
|
"type filter.")
|
|
.arg(rejected_urls.size()));
|
|
}
|
|
|
|
if (filtered_urls.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
ProjectImportTask *pim = new ProjectImportTask(parent, filtered_urls);
|
|
|
|
if (!pim->GetFileCount()) {
|
|
// No files to import
|
|
delete pim;
|
|
return;
|
|
}
|
|
|
|
TaskDialog *task_dialog =
|
|
new TaskDialog(pim, tr("Importing..."), main_window());
|
|
|
|
connect(task_dialog, &TaskDialog::TaskSucceeded, this,
|
|
&Core::ImportTaskComplete);
|
|
|
|
task_dialog->open();
|
|
}
|
|
|
|
const Tool::Item &Core::tool() const
|
|
{
|
|
return tool_;
|
|
}
|
|
|
|
const Tool::AddableObject &Core::GetSelectedAddableObject() const
|
|
{
|
|
return addable_object_;
|
|
}
|
|
|
|
const QString &Core::GetSelectedTransition() const
|
|
{
|
|
return selected_transition_;
|
|
}
|
|
|
|
void Core::SetSelectedAddableObject(const Tool::AddableObject &obj)
|
|
{
|
|
addable_object_ = obj;
|
|
emit AddableObjectChanged(addable_object_);
|
|
}
|
|
|
|
void Core::SetSelectedTransitionObject(const QString &obj)
|
|
{
|
|
selected_transition_ = obj;
|
|
}
|
|
|
|
void Core::ClearOpenRecentList()
|
|
{
|
|
recent_projects_.clear();
|
|
SaveRecentProjectsList();
|
|
emit OpenRecentListChanged();
|
|
}
|
|
|
|
void Core::CreateNewProject()
|
|
{
|
|
// If we already have an empty/new project, switch to it
|
|
if (CloseProject(false)) {
|
|
Project *p = new Project();
|
|
p->Initialize();
|
|
AddOpenProject(p);
|
|
}
|
|
}
|
|
|
|
const bool &Core::snapping() const
|
|
{
|
|
return snapping_;
|
|
}
|
|
|
|
const QStringList &Core::GetRecentProjects() const
|
|
{
|
|
return recent_projects_;
|
|
}
|
|
|
|
void Core::SetTool(const Tool::Item &tool)
|
|
{
|
|
tool_ = tool;
|
|
|
|
emit ToolChanged(tool_);
|
|
}
|
|
|
|
void Core::SetSnapping(const bool &b)
|
|
{
|
|
snapping_ = b;
|
|
|
|
emit SnappingChanged(snapping_);
|
|
}
|
|
|
|
void Core::DialogAboutShow()
|
|
{
|
|
AboutDialog a(false, main_window_);
|
|
a.exec();
|
|
}
|
|
|
|
void Core::DialogImportShow()
|
|
{
|
|
// Open dialog for user to select files
|
|
QStringList files = QFileDialog::getOpenFileNames(
|
|
main_window_, tr("Import footage..."), QString(),
|
|
FootageFileDialogFilter());
|
|
|
|
// Check if the user actually selected files to import
|
|
if (!files.isEmpty()) {
|
|
// Locate the most recently focused Project panel (assume that's the panel the user wants to import into)
|
|
ProjectPanel *active_project_panel =
|
|
PanelManager::instance()->MostRecentlyFocused<ProjectPanel>();
|
|
Project *active_project;
|
|
|
|
if (active_project_panel ==
|
|
nullptr // Check that we found a Project panel
|
|
|| (active_project = active_project_panel->project()) ==
|
|
nullptr) { // and that we could find an active Project
|
|
QMessageBox::critical(main_window_, tr("Failed to import footage"),
|
|
tr("Failed to find active Project panel"));
|
|
return;
|
|
}
|
|
|
|
// Get the selected folder in this panel
|
|
Folder *folder = active_project_panel->GetSelectedFolder();
|
|
|
|
ImportFiles(files, folder);
|
|
}
|
|
}
|
|
|
|
void Core::DialogPreferencesShow()
|
|
{
|
|
PreferencesDialog pd(main_window_);
|
|
pd.exec();
|
|
}
|
|
|
|
void Core::DialogProjectPropertiesShow()
|
|
{
|
|
Project *proj = GetActiveProject();
|
|
|
|
if (proj) {
|
|
ProjectPropertiesDialog ppd(proj, main_window_);
|
|
ppd.exec();
|
|
} else {
|
|
QMessageBox::critical(
|
|
main_window_, tr("No Active Project"),
|
|
tr("No project is currently open to set the properties for"),
|
|
QMessageBox::Ok);
|
|
}
|
|
}
|
|
|
|
void Core::DialogExportShow()
|
|
{
|
|
if (ViewerOutput *viewer = GetSequenceToExport()) {
|
|
OpenExportDialogForViewer(viewer, false);
|
|
}
|
|
}
|
|
|
|
#ifdef USE_OTIO
|
|
bool Core::DialogImportOTIOShow(const QList<Sequence *> &sequences)
|
|
{
|
|
Project *active_project = GetActiveProject();
|
|
OTIOPropertiesDialog opd(sequences, active_project);
|
|
return opd.exec() == QDialog::Accepted;
|
|
}
|
|
#endif
|
|
|
|
void Core::CreateNewFolder()
|
|
{
|
|
// Locate the most recently focused Project panel (assume that's the panel the user wants to import into)
|
|
ProjectPanel *active_project_panel =
|
|
PanelManager::instance()->MostRecentlyFocused<ProjectPanel>();
|
|
Project *active_project;
|
|
|
|
if (active_project_panel == nullptr // Check that we found a Project panel
|
|
|| (active_project = active_project_panel->project()) ==
|
|
nullptr) { // and that we could find an active Project
|
|
QMessageBox::critical(main_window_, tr("Failed to create new folder"),
|
|
tr("Failed to find active project"));
|
|
return;
|
|
}
|
|
|
|
// Get the selected folder in this panel
|
|
Folder *folder = active_project_panel->GetSelectedFolder();
|
|
|
|
// Create new folder
|
|
Folder *new_folder = new Folder();
|
|
|
|
// Set a default name
|
|
new_folder->SetLabel(tr("New Folder"));
|
|
|
|
// Create an undoable command
|
|
MultiUndoCommand *command = new MultiUndoCommand();
|
|
|
|
command->add_child(new NodeAddCommand(active_project, new_folder));
|
|
command->add_child(new FolderAddChild(folder, new_folder));
|
|
|
|
Core::instance()->undo_stack()->push(command, tr("Created New Folder"));
|
|
|
|
// Trigger an automatic rename so users can enter the folder name
|
|
active_project_panel->Edit(new_folder);
|
|
}
|
|
|
|
void Core::CreateNewSequence()
|
|
{
|
|
Project *active_project = GetActiveProject();
|
|
|
|
if (!active_project) {
|
|
QMessageBox::critical(main_window_, tr("Failed to create new sequence"),
|
|
tr("Failed to find active project"));
|
|
return;
|
|
}
|
|
|
|
// Create new sequence
|
|
Sequence *new_sequence = CreateNewSequenceForProject(active_project);
|
|
|
|
SequenceDialog sd(new_sequence, SequenceDialog::kNew, main_window_);
|
|
|
|
// Make sure SequenceDialog doesn't make an undo command for editing the sequence, since we make an undo command for
|
|
// adding it later on
|
|
sd.SetUndoable(false);
|
|
|
|
if (sd.exec() == QDialog::Accepted) {
|
|
// Create an undoable command
|
|
MultiUndoCommand *command = new MultiUndoCommand();
|
|
|
|
command->add_child(new NodeAddCommand(active_project, new_sequence));
|
|
command->add_child(new FolderAddChild(
|
|
GetSelectedFolderInActiveProject(), new_sequence));
|
|
command->add_child(new NodeSetPositionCommand(
|
|
new_sequence, new_sequence, Node::Position()));
|
|
command->add_child(new OpenSequenceCommand(new_sequence));
|
|
|
|
// Create and connect default nodes to new sequence
|
|
new_sequence->add_default_nodes(command);
|
|
|
|
Core::instance()->undo_stack()->push(command,
|
|
tr("Created New Sequence"));
|
|
|
|
} else {
|
|
// If the dialog was accepted, ownership goes to the AddItemCommand. But if we get here, just delete
|
|
delete new_sequence;
|
|
}
|
|
}
|
|
|
|
void Core::AddOpenProject(Project *p, bool add_to_recents)
|
|
{
|
|
// Ensure project is not open at the moment
|
|
if (open_project_ == p) {
|
|
return;
|
|
}
|
|
|
|
// If we currently have an empty project, close it first
|
|
if (open_project_) {
|
|
CloseProject(false);
|
|
}
|
|
|
|
SetActiveProject(p);
|
|
|
|
if (!p->filename().isEmpty() && add_to_recents) {
|
|
PushRecentlyOpenedProject(p->filename());
|
|
}
|
|
}
|
|
|
|
bool Core::AddOpenProjectFromTask(Task *task, bool add_to_recents)
|
|
{
|
|
ProjectLoadBaseTask *load_task = static_cast<ProjectLoadBaseTask *>(task);
|
|
|
|
if (!load_task->IsCancelled()) {
|
|
Project *project = load_task->GetLoadedProject();
|
|
|
|
if (ValidateFootageInLoadedProject(project, project->GetSavedURL())) {
|
|
AddOpenProject(project, add_to_recents);
|
|
main_window_->LoadLayout(load_task->GetLoadedLayout());
|
|
|
|
return true;
|
|
} else {
|
|
delete project;
|
|
CreateNewProject();
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void Core::SetActiveProject(Project *p)
|
|
{
|
|
if (open_project_) {
|
|
disconnect(open_project_, &Project::ModifiedChanged, this,
|
|
&Core::ProjectWasModified);
|
|
}
|
|
|
|
open_project_ = p;
|
|
RenderManager::instance()->SetProject(p);
|
|
main_window_->SetProject(p);
|
|
|
|
if (open_project_) {
|
|
connect(open_project_, &Project::ModifiedChanged, this,
|
|
&Core::ProjectWasModified);
|
|
}
|
|
}
|
|
|
|
void Core::ImportTaskComplete(Task *task)
|
|
{
|
|
ProjectImportTask *import_task = static_cast<ProjectImportTask *>(task);
|
|
|
|
MultiUndoCommand *command = import_task->GetCommand();
|
|
|
|
foreach (Footage *f, import_task->GetImportedFootage()) {
|
|
// Look for multi-layer images
|
|
if (f->GetAudioStreamCount() == 0 && f->GetVideoStreamCount() > 1) {
|
|
bool all_stills = true;
|
|
|
|
for (int i = 0; i < f->GetVideoStreamCount(); i++) {
|
|
const VideoParams &vs = f->GetVideoParams(i);
|
|
if (!(vs.video_type() == VideoParams::kVideoTypeStill &&
|
|
vs.enabled() == (i == 0))) {
|
|
all_stills = false;
|
|
}
|
|
}
|
|
|
|
if (all_stills) {
|
|
QMessageBox d(main_window());
|
|
|
|
d.setIcon(QMessageBox::Question);
|
|
d.setWindowTitle(tr("Multi-Layer Image"));
|
|
d.setText(
|
|
tr("The file '%1' has multiple layers. Would you like these layers to be "
|
|
"separated across multiple tracks or merged into a single image?")
|
|
.arg(f->filename()));
|
|
|
|
auto multi_btn =
|
|
d.addButton(tr("Multiple Layers"), QMessageBox::YesRole);
|
|
auto single_btn =
|
|
d.addButton(tr("Single Layer"), QMessageBox::NoRole);
|
|
auto cancel_btn = d.addButton(QMessageBox::Cancel);
|
|
|
|
d.exec();
|
|
|
|
if (d.clickedButton() == multi_btn) {
|
|
for (int i = 0; i < f->GetVideoStreamCount(); i++) {
|
|
VideoParams vs = f->GetVideoParams(i);
|
|
vs.set_enabled(!vs.enabled());
|
|
f->SetVideoParams(vs, i);
|
|
}
|
|
} else if (d.clickedButton() == single_btn) {
|
|
// Do nothing, footage will already be set up this way
|
|
} else if (d.clickedButton() == cancel_btn) {
|
|
// Cancel import
|
|
delete command;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (import_task->HasInvalidFiles()) {
|
|
ProjectImportErrorDialog d(import_task->GetInvalidFiles(),
|
|
main_window_);
|
|
d.exec();
|
|
}
|
|
|
|
undo_stack_.push(
|
|
command,
|
|
tr("Imported %1 File(s)").arg(import_task->GetImportedFootage().size()));
|
|
|
|
main_window_->SelectFootage(import_task->GetImportedFootage());
|
|
}
|
|
|
|
bool Core::ConfirmImageSequence(const QString &filename)
|
|
{
|
|
QMessageBox mb(main_window_);
|
|
|
|
mb.setIcon(QMessageBox::Question);
|
|
mb.setWindowTitle(tr("Possible image sequence detected"));
|
|
mb.setText(tr("The file '%1' looks like it might be part of an image "
|
|
"sequence. Would you like to import it as such?")
|
|
.arg(filename));
|
|
|
|
mb.addButton(QMessageBox::Yes);
|
|
mb.addButton(QMessageBox::No);
|
|
|
|
return (mb.exec() == QMessageBox::Yes);
|
|
}
|
|
|
|
void Core::ProjectWasModified(bool e)
|
|
{
|
|
main_window_->setWindowModified(e);
|
|
}
|
|
|
|
bool Core::StartHeadlessExport()
|
|
{
|
|
const QString &startup_project = core_params_.startup_project();
|
|
|
|
if (startup_project.isEmpty()) {
|
|
qCritical().noquote()
|
|
<< tr("You must specify a project file to export");
|
|
return false;
|
|
}
|
|
|
|
if (!QFileInfo::exists(startup_project)) {
|
|
qCritical().noquote() << tr("Specified project does not exist");
|
|
return false;
|
|
}
|
|
|
|
// Start a load task and try running it
|
|
ProjectLoadTask plm(startup_project);
|
|
CLITaskDialog task_dialog(&plm);
|
|
|
|
/*
|
|
if (task_dialog.Run()) {
|
|
std::unique_ptr<Project> p = std::unique_ptr<Project>(plm.GetLoadedProject());
|
|
QVector<Item*> items = p->get_items_of_type(Item::kSequence);
|
|
|
|
// Check if this project contains sequences
|
|
if (items.isEmpty()) {
|
|
qCritical().noquote() << tr("Project contains no sequences, nothing to export");
|
|
return false;
|
|
}
|
|
|
|
Sequence* sequence = nullptr;
|
|
|
|
// Check if this project contains multiple sequences
|
|
if (items.size() > 1) {
|
|
qInfo().noquote() << tr("This project has multiple sequences. Which do you wish to export?");
|
|
for (int i=0;i<items.size();i++) {
|
|
std::cout << "[" << i << "] " << items.at(i)->GetLabel().toStdString();
|
|
}
|
|
|
|
QTextStream stream(stdin);
|
|
QString sequence_read;
|
|
int sequence_index = -1;
|
|
QString quit_code = QStringLiteral("q");
|
|
std::string prompt = tr("Enter number (or %1 to cancel): ").arg(quit_code).toStdString();
|
|
forever {
|
|
std::cout << prompt;
|
|
|
|
stream.readLineInto(&sequence_read);
|
|
|
|
if (!QString::compare(sequence_read, quit_code, Qt::CaseInsensitive)) {
|
|
return false;
|
|
}
|
|
|
|
bool ok;
|
|
sequence_index = sequence_read.toInt(&ok);
|
|
|
|
if (ok && sequence_index >= 0 && sequence_index < items.size()) {
|
|
break;
|
|
} else {
|
|
qCritical().noquote() << tr("Invalid sequence number");
|
|
}
|
|
}
|
|
|
|
sequence = static_cast<Sequence*>(items.at(sequence_index));
|
|
} else {
|
|
sequence = static_cast<Sequence*>(items.first());
|
|
}
|
|
|
|
ExportParams params;
|
|
ExportTask export_task(sequence->viewer_output(), p->color_manager(), params);
|
|
CLITaskDialog export_dialog(&export_task);
|
|
if (export_dialog.Run()) {
|
|
qInfo().noquote() << tr("Export succeeded");
|
|
return true;
|
|
} else {
|
|
qInfo().noquote() << tr("Export failed: %1").arg(export_task.GetError());
|
|
return false;
|
|
}
|
|
} else {
|
|
qCritical().noquote() << tr("Project failed to load: %1").arg(plm.GetError());
|
|
return false;
|
|
}
|
|
*/
|
|
|
|
return false;
|
|
}
|
|
|
|
void Core::OpenStartupProject()
|
|
{
|
|
const QString &startup_project = core_params_.startup_project();
|
|
bool startup_project_exists = !startup_project.isEmpty() &&
|
|
QFileInfo::exists(startup_project);
|
|
|
|
// Load startup project
|
|
if (!startup_project_exists && !startup_project.isEmpty()) {
|
|
QMessageBox::warning(main_window_, tr("Failed to open startup file"),
|
|
tr("The project \"%1\" doesn't exist. "
|
|
"A new project will be started instead.")
|
|
.arg(startup_project),
|
|
QMessageBox::Ok);
|
|
}
|
|
|
|
if (startup_project_exists) {
|
|
// If a startup project was set and exists, open it now
|
|
OpenProjectInternal(startup_project);
|
|
} else {
|
|
// If no load project is set, create a new one on open
|
|
CreateNewProject();
|
|
}
|
|
}
|
|
|
|
void Core::AddRecoveryProjectFromTask(Task *task)
|
|
{
|
|
if (AddOpenProjectFromTask(task, false)) {
|
|
ProjectLoadBaseTask *load_task =
|
|
static_cast<ProjectLoadBaseTask *>(task);
|
|
|
|
Project *project = load_task->GetLoadedProject();
|
|
|
|
// Clearing the filename will force the user to re-save it somewhere else
|
|
project->set_filename(QString());
|
|
|
|
// Forcing a UUID regeneration will prevent it from saving auto-recoveries in the same place
|
|
// the original project did
|
|
project->RegenerateUuid();
|
|
|
|
// Setting modified will ensure that the program doesn't close and lose the project without
|
|
// prompting the user first
|
|
project->set_modified(true);
|
|
}
|
|
}
|
|
|
|
void Core::StartGUI(bool full_screen)
|
|
{
|
|
// Set UI style
|
|
StyleManager::Init();
|
|
|
|
// Set up shared menus
|
|
MenuShared::CreateInstance();
|
|
|
|
// Since we're starting GUI mode, create a PanelFocusManager (auto-deletes with QObject)
|
|
PanelManager::CreateInstance();
|
|
|
|
// Initialize audio service
|
|
AudioManager::CreateInstance();
|
|
|
|
// Initialize disk service
|
|
DiskManager::CreateInstance();
|
|
|
|
// Connect the PanelFocusManager to the application's focus change signal
|
|
connect(qApp, &QApplication::focusChanged, PanelManager::instance(),
|
|
&PanelManager::FocusChanged);
|
|
|
|
KDDockWidgets::initFrontend(KDDockWidgets::FrontendType::QtWidgets);
|
|
// Set KDDockWidgets flags
|
|
auto &config = KDDockWidgets::Config::self();
|
|
auto flags = config.flags();
|
|
flags |= KDDockWidgets::Config::Flag_TabsHaveCloseButton;
|
|
flags |= KDDockWidgets::Config::Flag_HideTitleBarWhenTabsVisible;
|
|
flags |= KDDockWidgets::Config::Flag_AlwaysShowTabs;
|
|
flags |= KDDockWidgets::Config::Flag_AllowReorderTabs;
|
|
config.setFlags(flags);
|
|
config.setAbsoluteWidgetMinSize(QSize(1, 1));
|
|
|
|
// Create main window and open it
|
|
main_window_ = new MainWindow();
|
|
|
|
if (full_screen) {
|
|
main_window_->showFullScreen();
|
|
} else {
|
|
main_window_->showMaximized();
|
|
}
|
|
|
|
#ifdef Q_OS_WINDOWS
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
|
// Workaround for Qt bug where menus don't appear in full screen mode
|
|
// See: https://doc.qt.io/qt-5/windows-issues.html
|
|
QWindowsWindowFunctions::setHasBorderInFullScreen(
|
|
main_window_->windowHandle(), true);
|
|
#endif
|
|
#endif
|
|
|
|
// Start autorecovery timer using the config value as its interval
|
|
SetAutorecoveryInterval(OLIVE_CONFIG("AutorecoveryInterval").toInt());
|
|
connect(&autorecovery_timer_, &QTimer::timeout, this,
|
|
&Core::SaveAutorecovery);
|
|
autorecovery_timer_.start();
|
|
|
|
// Load recently opened projects list
|
|
{
|
|
QFile recent_projects_file(GetRecentProjectsFilePath());
|
|
if (recent_projects_file.open(QFile::ReadOnly | QFile::Text)) {
|
|
QString r = QString::fromUtf8(recent_projects_file.readAll());
|
|
if (!r.isEmpty()) {
|
|
recent_projects_ = r.split('\n');
|
|
}
|
|
recent_projects_file.close();
|
|
}
|
|
|
|
emit OpenRecentListChanged();
|
|
}
|
|
}
|
|
|
|
void Core::SaveProjectInternal(const QString &override_filename)
|
|
{
|
|
// Create save manager
|
|
Task *psm;
|
|
|
|
if (open_project_->filename().endsWith(QStringLiteral(".otio"),
|
|
Qt::CaseInsensitive)) {
|
|
#ifdef USE_OTIO
|
|
psm = new SaveOTIOTask(open_project_);
|
|
#else
|
|
QMessageBox::critical(
|
|
main_window_, tr("Missing OpenTimelineIO Libraries"),
|
|
tr("This build was compiled without OpenTimelineIO and therefore "
|
|
"cannot open OpenTimelineIO files."));
|
|
return;
|
|
#endif
|
|
} else {
|
|
bool use_compression = !open_project_->filename().endsWith(
|
|
QStringLiteral(".ovexml"), Qt::CaseInsensitive);
|
|
psm = new ProjectSaveTask(open_project_, use_compression);
|
|
static_cast<ProjectSaveTask *>(psm)->SetLayout(
|
|
main_window_->SaveLayout());
|
|
|
|
if (!override_filename.isEmpty()) {
|
|
// Set override filename if provided
|
|
static_cast<ProjectSaveTask *>(psm)->SetOverrideFilename(
|
|
override_filename);
|
|
}
|
|
}
|
|
|
|
// We don't use a TaskDialog here because a model save dialog is annoying, particularly when
|
|
// saving auto-recoveries that the user can't anticipate. Doing this in the main thread will
|
|
// cause a brief (but often unnoticeable) pause in the GUI, which, while not ideal, is not that
|
|
// different from what already happened (modal dialog preventing use of the GUI) and in many ways
|
|
// less annoying (doesn't disrupt any current actions or pull focus from elsewhere).
|
|
//
|
|
// Ideally we could do this in a background thread and show progress in the status bar like
|
|
// Microsoft Word, but that would be far more complex. If it becomes necessary in the future,
|
|
// we will look into an approach like that.
|
|
if (psm->Start()) {
|
|
if (override_filename.isEmpty()) {
|
|
ProjectSaveSucceeded(psm);
|
|
}
|
|
}
|
|
|
|
psm->deleteLater();
|
|
}
|
|
|
|
ViewerOutput *Core::GetSequenceToExport()
|
|
{
|
|
// First try the most recently focused time based window
|
|
TimeBasedPanel *time_panel =
|
|
PanelManager::instance()->MostRecentlyFocused<TimeBasedPanel>();
|
|
|
|
// If that fails try defaulting to the first timeline (i.e. if a project has just been loaded).
|
|
if (!time_panel->GetConnectedViewer()) {
|
|
// Safe to assume there will always be one timeline.
|
|
time_panel =
|
|
PanelManager::instance()->GetPanelsOfType<TimelinePanel>().first();
|
|
}
|
|
|
|
if (time_panel && time_panel->GetConnectedViewer()) {
|
|
if (time_panel->GetConnectedViewer()->GetLength() == 0) {
|
|
QMessageBox::critical(
|
|
main_window_, tr("Error"),
|
|
tr("This Sequence is empty. There is nothing to export."),
|
|
QMessageBox::Ok);
|
|
} else {
|
|
return time_panel->GetConnectedViewer();
|
|
}
|
|
} else {
|
|
QMessageBox::critical(
|
|
main_window_, tr("Error"),
|
|
tr("No valid sequence detected.\n\nMake sure a sequence is loaded and it has a connected Viewer node."),
|
|
QMessageBox::Ok);
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
QString Core::GetAutoRecoveryIndexFilename()
|
|
{
|
|
return QDir(QStandardPaths::writableLocation(
|
|
QStandardPaths::AppLocalDataLocation))
|
|
.filePath(QStringLiteral("unrecovered"));
|
|
}
|
|
|
|
void Core::SaveUnrecoveredList()
|
|
{
|
|
QFile autorecovery_index(GetAutoRecoveryIndexFilename());
|
|
|
|
if (autorecovered_projects_.isEmpty()) {
|
|
// Recovery list is empty, delete file if exists
|
|
if (autorecovery_index.exists()) {
|
|
autorecovery_index.remove();
|
|
}
|
|
} else if (autorecovery_index.open(QFile::WriteOnly)) {
|
|
// Overwrite recovery list with current list
|
|
QTextStream ts(&autorecovery_index);
|
|
|
|
bool first = true;
|
|
foreach (const QUuid &uuid, autorecovered_projects_) {
|
|
if (first) {
|
|
first = false;
|
|
} else {
|
|
ts << QStringLiteral("\n");
|
|
}
|
|
ts << uuid.toString();
|
|
}
|
|
|
|
autorecovery_index.close();
|
|
} else {
|
|
qWarning() << "Failed to save unrecovered list";
|
|
}
|
|
}
|
|
|
|
bool Core::RevertProjectInternal(bool by_opening_existing)
|
|
{
|
|
if (open_project_->filename().isEmpty()) {
|
|
QMessageBox::critical(
|
|
main_window_, tr("Revert"),
|
|
tr("This project has not yet been saved, therefore there is no last saved state to revert to."));
|
|
} else {
|
|
QString msg;
|
|
|
|
if (by_opening_existing) {
|
|
msg =
|
|
tr("The project \"%1\" is already open. By re-opening it, the project will revert to "
|
|
"its last saved state. Any unsaved changes will be lost. Do you wish to continue?")
|
|
.arg(open_project_->filename());
|
|
} else {
|
|
msg =
|
|
tr("This will revert the project \"%1\" back to its last saved state. "
|
|
"All unsaved changes will be lost. Do you wish to continue?")
|
|
.arg(open_project_->name());
|
|
}
|
|
|
|
if (QMessageBox::question(main_window_, tr("Revert"), msg,
|
|
QMessageBox::Ok | QMessageBox::Cancel) ==
|
|
QMessageBox::Ok) {
|
|
// Copy filename because CloseProject is going to delete `p`
|
|
QString filename = open_project_->filename();
|
|
|
|
// Close project without prompting to save it
|
|
CloseProject(false, true);
|
|
|
|
// NOTE: `open_project_` will be deleted now, so don't try accessing it
|
|
|
|
// Re-open project at the filename
|
|
OpenProjectInternal(filename);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void Core::SaveRecentProjectsList()
|
|
{
|
|
// Save recently opened projects
|
|
QFile recent_projects_file(GetRecentProjectsFilePath());
|
|
if (recent_projects_file.open(QFile::WriteOnly | QFile::Text)) {
|
|
recent_projects_file.write(recent_projects_.join('\n').toUtf8());
|
|
recent_projects_file.close();
|
|
}
|
|
}
|
|
|
|
void Core::SaveAutorecovery()
|
|
{
|
|
if (OLIVE_CONFIG("AutorecoveryEnabled").toBool()) {
|
|
if (open_project_ && !open_project_->has_autorecovery_been_saved()) {
|
|
QDir project_autorecovery_dir(
|
|
QDir(FileFunctions::GetAutoRecoveryRoot())
|
|
.filePath(open_project_->GetUuid().toString()));
|
|
if (FileFunctions::DirectoryIsValid(project_autorecovery_dir)) {
|
|
QString this_autorecovery_path =
|
|
project_autorecovery_dir.filePath(
|
|
QStringLiteral("%1.ove").arg(QString::number(
|
|
QDateTime::currentSecsSinceEpoch())));
|
|
|
|
SaveProjectInternal(this_autorecovery_path);
|
|
|
|
open_project_->set_autorecovery_saved(true);
|
|
|
|
// Keep track of projects that where the "newest" save is the recovery project
|
|
if (!autorecovered_projects_.contains(
|
|
open_project_->GetUuid())) {
|
|
autorecovered_projects_.append(open_project_->GetUuid());
|
|
}
|
|
|
|
qDebug() << "Saved auto-recovery to:" << this_autorecovery_path;
|
|
|
|
// Write human-readable real name so it's not just a UUID
|
|
{
|
|
QFile realname_file(project_autorecovery_dir.filePath(
|
|
QStringLiteral("realname.txt")));
|
|
realname_file.open(QFile::WriteOnly);
|
|
realname_file.write(
|
|
open_project_->pretty_filename().toUtf8());
|
|
realname_file.close();
|
|
}
|
|
|
|
int64_t max_recoveries_per_file =
|
|
OLIVE_CONFIG("AutorecoveryMaximum").toLongLong();
|
|
|
|
// Since we write an extra file, increment total allowed files by 1
|
|
max_recoveries_per_file++;
|
|
|
|
// Delete old entries
|
|
QStringList recovery_files = project_autorecovery_dir.entryList(
|
|
QDir::Files | QDir::NoDotAndDotDot, QDir::Name);
|
|
while (recovery_files.size() > max_recoveries_per_file) {
|
|
bool deleted = false;
|
|
for (int i = 0; i < recovery_files.size(); i++) {
|
|
const QString &f = recovery_files.at(i);
|
|
|
|
if (f.endsWith(QStringLiteral(".ove"),
|
|
Qt::CaseInsensitive)) {
|
|
QString delete_full_path =
|
|
project_autorecovery_dir.filePath(f);
|
|
qDebug()
|
|
<< "Deleted old recovery:" << delete_full_path;
|
|
QFile::remove(delete_full_path);
|
|
recovery_files.removeAt(i);
|
|
deleted = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!deleted) {
|
|
// For some reason none of the files were deletable. Break so we don't end up in
|
|
// an infinite loop.
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
QMessageBox::critical(
|
|
main_window_, tr("Auto-Recovery Error"),
|
|
tr("Failed to save auto-recovery to \"%1\". "
|
|
"Oak Video Editor may not have permission to this directory.")
|
|
.arg(project_autorecovery_dir.absolutePath()));
|
|
}
|
|
}
|
|
|
|
// Save index
|
|
SaveUnrecoveredList();
|
|
}
|
|
}
|
|
|
|
void Core::ProjectSaveSucceeded(Task *task)
|
|
{
|
|
Project *p = static_cast<ProjectSaveTask *>(task)->GetProject();
|
|
|
|
PushRecentlyOpenedProject(p->filename());
|
|
|
|
p->set_modified(false);
|
|
|
|
autorecovered_projects_.removeOne(p->GetUuid());
|
|
SaveUnrecoveredList();
|
|
|
|
ShowStatusBarMessage(tr("Saved to \"%1\" successfully").arg(p->filename()));
|
|
}
|
|
|
|
Project *Core::GetActiveProject() const
|
|
{
|
|
return open_project_;
|
|
}
|
|
|
|
Folder *Core::GetSelectedFolderInActiveProject() const
|
|
{
|
|
ProjectPanel *active_project_panel =
|
|
PanelManager::instance()->MostRecentlyFocused<ProjectPanel>();
|
|
|
|
if (active_project_panel) {
|
|
return active_project_panel->GetSelectedFolder();
|
|
} else {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
Timecode::Display Core::GetTimecodeDisplay() const
|
|
{
|
|
return static_cast<Timecode::Display>(
|
|
OLIVE_CONFIG("TimecodeDisplay").toInt());
|
|
}
|
|
|
|
void Core::SetTimecodeDisplay(Timecode::Display d)
|
|
{
|
|
OLIVE_CONFIG("TimecodeDisplay") = d;
|
|
|
|
emit TimecodeDisplayChanged(d);
|
|
}
|
|
|
|
void Core::SetAutorecoveryInterval(int minutes)
|
|
{
|
|
// Convert minutes to milliseconds
|
|
autorecovery_timer_.setInterval(minutes * 60000);
|
|
}
|
|
|
|
void Core::CopyStringToClipboard(const QString &s)
|
|
{
|
|
QGuiApplication::clipboard()->setText(s);
|
|
}
|
|
|
|
QString Core::PasteStringFromClipboard()
|
|
{
|
|
return QGuiApplication::clipboard()->text();
|
|
}
|
|
|
|
QString Core::GetProjectFilter(bool include_any_filter)
|
|
{
|
|
static const QVector<QPair<QString, QString>> FILTERS = {
|
|
// Standard compressed Oak project
|
|
{ tr("Oak Project"), QStringLiteral("ove") },
|
|
|
|
// Uncompressed XML Oak project
|
|
{ tr("Oak Project (Uncompressed XML)"), QStringLiteral("ovexml") },
|
|
|
|
// OpenTimelineIO project, if available
|
|
#ifdef USE_OTIO
|
|
{ tr("OpenTimelineIO"), QStringLiteral("otio") }
|
|
#endif
|
|
};
|
|
|
|
QStringList filters;
|
|
filters.reserve(FILTERS.size() + 1);
|
|
|
|
if (include_any_filter) {
|
|
QStringList combined;
|
|
for (auto it = FILTERS.cbegin(); it != FILTERS.cend(); it++) {
|
|
combined.append(QStringLiteral("*.%1").arg(it->second));
|
|
}
|
|
filters.append(QStringLiteral("%1 (%2)").arg(
|
|
tr("All Supported Projects"), combined.join(' ')));
|
|
}
|
|
|
|
for (auto it = FILTERS.cbegin(); it != FILTERS.cend(); it++) {
|
|
filters.append(QStringLiteral("%1 (*.%2)").arg(it->first, it->second));
|
|
}
|
|
|
|
return filters.join(QStringLiteral(";;"));
|
|
}
|
|
|
|
QString Core::GetRecentProjectsFilePath()
|
|
{
|
|
return QDir(FileFunctions::GetConfigurationLocation())
|
|
.filePath(QStringLiteral("recent"));
|
|
}
|
|
|
|
void Core::SetStartupLocale()
|
|
{
|
|
// Set language
|
|
if (!core_params_.startup_language().isEmpty()) {
|
|
if (translator_->load(core_params_.startup_language()) &&
|
|
QApplication::installTranslator(translator_)) {
|
|
return;
|
|
} else {
|
|
qWarning()
|
|
<< "Failed to load translation file. Falling back to defaults.";
|
|
}
|
|
}
|
|
|
|
QString use_locale = OLIVE_CONFIG("Language").toString();
|
|
|
|
if (use_locale.isEmpty()) {
|
|
// No configured locale, auto-detect the system's locale
|
|
use_locale = QLocale::system().name();
|
|
}
|
|
|
|
if (!SetLanguage(use_locale)) {
|
|
qWarning() << "Trying to use locale" << use_locale
|
|
<< "but couldn't find a translation for it";
|
|
}
|
|
}
|
|
|
|
bool Core::SaveProject()
|
|
{
|
|
if (open_project_->filename().isEmpty()) {
|
|
return SaveProjectAs();
|
|
} else {
|
|
SaveProjectInternal();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void Core::ShowStatusBarMessage(const QString &s, int timeout)
|
|
{
|
|
main_window_->statusBar()->showMessage(s, timeout);
|
|
}
|
|
|
|
void Core::ClearStatusBarMessage()
|
|
{
|
|
main_window_->statusBar()->clearMessage();
|
|
}
|
|
|
|
void Core::OpenRecoveryProject(const QString &filename)
|
|
{
|
|
OpenProjectInternal(filename, true);
|
|
}
|
|
|
|
void Core::OpenNodeInViewer(ViewerOutput *viewer)
|
|
{
|
|
main_window_->OpenNodeInViewer(viewer);
|
|
}
|
|
|
|
void Core::OpenExportDialogForViewer(ViewerOutput *viewer,
|
|
bool start_still_image)
|
|
{
|
|
ExportDialog *ed =
|
|
new ExportDialog(viewer, start_still_image, main_window_);
|
|
connect(ed, &ExportDialog::finished, ed, &ExportDialog::deleteLater);
|
|
ed->open();
|
|
connect(ed, &ExportDialog::RequestImportFile, this,
|
|
&Core::ImportSingleFile);
|
|
}
|
|
|
|
void Core::CheckForAutoRecoveries()
|
|
{
|
|
QFile autorecovery_index(GetAutoRecoveryIndexFilename());
|
|
if (autorecovery_index.exists()) {
|
|
// Uh-oh, we have auto-recoveries to prompt
|
|
if (autorecovery_index.open(QFile::ReadOnly)) {
|
|
QStringList recovery_filenames =
|
|
QString::fromUtf8(autorecovery_index.readAll()).split('\n');
|
|
|
|
AutoRecoveryDialog ard(
|
|
tr("The following projects had unsaved changes when Oak Video Editor "
|
|
"forcefully quit. Would you like to load them?"),
|
|
recovery_filenames, true, main_window_);
|
|
ard.exec();
|
|
|
|
autorecovery_index.close();
|
|
|
|
// Delete recovery index since we don't need it anymore
|
|
QFile::remove(GetAutoRecoveryIndexFilename());
|
|
} else {
|
|
QMessageBox::critical(
|
|
main_window_, tr("Auto-Recovery Error"),
|
|
tr("Found auto-recoveries but failed to load the auto-recovery index. "
|
|
"Auto-recover projects will have to be opened manually.\n\n"
|
|
"Your recoverable projects are still available at: %1")
|
|
.arg(FileFunctions::GetAutoRecoveryRoot()));
|
|
}
|
|
}
|
|
}
|
|
|
|
void Core::BrowseAutoRecoveries()
|
|
{
|
|
// List all auto-recovery entries
|
|
AutoRecoveryDialog ard(
|
|
tr("The following project versions have been auto-saved:"),
|
|
QDir(FileFunctions::GetAutoRecoveryRoot())
|
|
.entryList(QDir::Dirs | QDir::NoDotAndDotDot),
|
|
false, main_window_);
|
|
ard.exec();
|
|
}
|
|
|
|
void Core::RequestPixelSamplingInViewers(bool e)
|
|
{
|
|
if (e) {
|
|
if (pixel_sampling_users_ == 0) {
|
|
// Signal to start pixel sampling
|
|
emit ColorPickerEnabled(true);
|
|
}
|
|
|
|
pixel_sampling_users_++;
|
|
} else {
|
|
pixel_sampling_users_--;
|
|
|
|
if (pixel_sampling_users_ == 0) {
|
|
// Signal to end pixel sampling
|
|
emit ColorPickerEnabled(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Core::WarnCacheFull()
|
|
{
|
|
if (!shown_cache_full_warning_ && main_window_) {
|
|
shown_cache_full_warning_ = true;
|
|
|
|
QMessageBox::warning(
|
|
main_window_, tr("Disk Cache Full"),
|
|
tr("The disk cache is currently full and Oak Video Editor is having to delete old "
|
|
"frames to keep it within the limits set in the Disk preferences. This "
|
|
"will result in SIGNIFICANTLY reduced cache performance.\n\n"
|
|
"To remedy this, please do one of the following:\n\n"
|
|
"1. Manually clear the disk cache in Disk preferences.\n"
|
|
"2. Increase the maximum disk cache size in Disk preferences.\n"
|
|
"3. Reduce usage of the disk cache (e.g. disable auto-cache or only cache specific sections of your sequence)."));
|
|
}
|
|
}
|
|
|
|
bool Core::SaveProjectAs()
|
|
{
|
|
QFileDialog fd(main_window_, tr("Save Project As"));
|
|
|
|
fd.setAcceptMode(QFileDialog::AcceptSave);
|
|
fd.setNameFilter(GetProjectFilter(false));
|
|
|
|
if (fd.exec() == QDialog::Accepted) {
|
|
QString fn = fd.selectedFiles().first();
|
|
|
|
// Somewhat hacky method of extracting the extension from the name filter
|
|
const QString &name_filter = fd.selectedNameFilter();
|
|
int ext_index = name_filter.indexOf(QStringLiteral("(*.")) + 3;
|
|
QString extension =
|
|
name_filter.mid(ext_index, name_filter.size() - ext_index - 1);
|
|
|
|
fn = FileFunctions::EnsureFilenameExtension(fn, extension);
|
|
|
|
open_project_->set_filename(fn);
|
|
|
|
SaveProjectInternal();
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void Core::RevertProject()
|
|
{
|
|
RevertProjectInternal(false);
|
|
}
|
|
|
|
void Core::PushRecentlyOpenedProject(const QString &s)
|
|
{
|
|
if (s.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
int existing_index = recent_projects_.indexOf(s);
|
|
|
|
if (existing_index >= 0) {
|
|
recent_projects_.move(existing_index, 0);
|
|
} else {
|
|
recent_projects_.prepend(s);
|
|
|
|
const int kMaximumRecentProjects = 10;
|
|
while (recent_projects_.size() > kMaximumRecentProjects) {
|
|
recent_projects_.removeLast();
|
|
}
|
|
}
|
|
|
|
SaveRecentProjectsList();
|
|
|
|
emit OpenRecentListChanged();
|
|
}
|
|
|
|
void Core::OpenProjectInternal(const QString &filename, bool recovery_project)
|
|
{
|
|
if (open_project_) {
|
|
// Comparing QFileInfos will handle case insensitivity and both slash directions on platforms
|
|
// where this is necessary (not naming any names *cough* Windows)
|
|
if (QFileInfo(open_project_->filename()) == QFileInfo(filename)) {
|
|
// This project is already open
|
|
bool reverted = RevertProjectInternal(true);
|
|
|
|
if (!reverted) {
|
|
// Calling this will focus attention to the project that the user just tried to re-open
|
|
AddOpenProject(open_project_);
|
|
}
|
|
|
|
// Don't do anything else
|
|
return;
|
|
}
|
|
}
|
|
|
|
Task *load_task;
|
|
|
|
if (filename.endsWith(QStringLiteral(".otio"), Qt::CaseInsensitive)) {
|
|
// Load OpenTimelineIO project
|
|
#ifdef USE_OTIO
|
|
load_task = new LoadOTIOTask(filename);
|
|
#else
|
|
QMessageBox::critical(
|
|
main_window_, tr("Missing OpenTimelineIO Libraries"),
|
|
tr("This build was compiled without OpenTimelineIO and therefore "
|
|
"cannot open OpenTimelineIO files."));
|
|
return;
|
|
#endif
|
|
} else {
|
|
// Fallback to regular OVE project
|
|
load_task = new ProjectLoadTask(filename);
|
|
}
|
|
|
|
TaskDialog *task_dialog =
|
|
new TaskDialog(load_task, tr("Load Project"), main_window());
|
|
|
|
if (recovery_project) {
|
|
connect(task_dialog, &TaskDialog::TaskSucceeded, this,
|
|
&Core::AddRecoveryProjectFromTask);
|
|
} else {
|
|
connect(task_dialog, &TaskDialog::TaskSucceeded, this,
|
|
&Core::AddOpenProjectFromTaskAndAddToRecents);
|
|
}
|
|
|
|
task_dialog->open();
|
|
}
|
|
|
|
void Core::ImportSingleFile(const QString &f)
|
|
{
|
|
if (Project *p = GetActiveProject()) {
|
|
ImportFiles({ f }, p->root());
|
|
}
|
|
}
|
|
|
|
int Core::CountFilesInFileList(const QFileInfoList &filenames)
|
|
{
|
|
int file_count = 0;
|
|
|
|
foreach (const QFileInfo &f, filenames) {
|
|
// For some reason QDir::NoDotAndDotDot doesn't work with entryInfoList, so we have to check manually
|
|
if (f.fileName() == "." || f.fileName() == "..") {
|
|
continue;
|
|
} else if (f.isDir()) {
|
|
QFileInfoList info_list =
|
|
QDir(f.absoluteFilePath()).entryInfoList();
|
|
|
|
file_count += CountFilesInFileList(info_list);
|
|
} else {
|
|
file_count++;
|
|
}
|
|
}
|
|
|
|
return file_count;
|
|
}
|
|
|
|
bool Core::LabelNodes(const QVector<Node *> &nodes, MultiUndoCommand *parent)
|
|
{
|
|
if (nodes.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
bool ok;
|
|
|
|
QString start_label = nodes.first()->GetLabel();
|
|
|
|
for (int i = 1; i < nodes.size(); i++) {
|
|
if (nodes.at(i)->GetLabel() != start_label) {
|
|
// Not all the nodes share the same name, so we'll start with a blank one
|
|
start_label.clear();
|
|
break;
|
|
}
|
|
}
|
|
|
|
QString s = QInputDialog::getText(main_window_, tr("Label Node"),
|
|
tr("Set node label"), QLineEdit::Normal,
|
|
start_label, &ok);
|
|
|
|
if (ok) {
|
|
NodeRenameCommand *rename_command = new NodeRenameCommand();
|
|
|
|
foreach (Node *n, nodes) {
|
|
rename_command->AddNode(n, s);
|
|
}
|
|
|
|
if (parent) {
|
|
parent->add_child(rename_command);
|
|
} else {
|
|
undo_stack_.push(rename_command,
|
|
tr("Renamed %1 Node(s)").arg(nodes.size()));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Sequence *Core::CreateNewSequenceForProject(const QString &format,
|
|
Project *project)
|
|
{
|
|
Sequence *new_sequence = new Sequence();
|
|
|
|
// Get default name for this sequence (in the format "Sequence N", the first that doesn't exist)
|
|
int sequence_number = 1;
|
|
QString sequence_name;
|
|
do {
|
|
sequence_name = format.arg(sequence_number);
|
|
sequence_number++;
|
|
} while (project->root()->ChildExistsWithName(sequence_name));
|
|
new_sequence->SetLabel(sequence_name);
|
|
|
|
return new_sequence;
|
|
}
|
|
|
|
void Core::OpenProjectFromRecentList(int index)
|
|
{
|
|
const QString &open_fn = recent_projects_.at(index);
|
|
|
|
if (QFileInfo::exists(open_fn)) {
|
|
OpenProjectInternal(open_fn);
|
|
} else if (
|
|
QMessageBox::information(
|
|
main_window(), tr("Cannot open recent project"),
|
|
tr("The project \"%1\" doesn't exist. Would you like to remove this file from the recent list?")
|
|
.arg(open_fn),
|
|
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
|
|
recent_projects_.removeAt(index);
|
|
|
|
SaveRecentProjectsList();
|
|
|
|
emit OpenRecentListChanged();
|
|
}
|
|
}
|
|
|
|
bool Core::CloseProject(bool auto_open_new, bool ignore_modified)
|
|
{
|
|
if (open_project_) {
|
|
if (open_project_->is_modified() && !ignore_modified) {
|
|
QMessageBox mb(main_window_);
|
|
|
|
mb.setWindowModality(Qt::WindowModal);
|
|
mb.setIcon(QMessageBox::Question);
|
|
mb.setWindowTitle(tr("Unsaved Changes"));
|
|
mb.setText(
|
|
tr("The project '%1' has unsaved changes. Would you like to save them?")
|
|
.arg(open_project_->name()));
|
|
|
|
QPushButton *yes_btn =
|
|
mb.addButton(tr("Save"), QMessageBox::YesRole);
|
|
|
|
mb.addButton(tr("Don't Save"), QMessageBox::NoRole);
|
|
|
|
QPushButton *cancel_btn = mb.addButton(QMessageBox::Cancel);
|
|
|
|
mb.exec();
|
|
|
|
if (mb.clickedButton() == cancel_btn) {
|
|
// Stop closing projects if the user clicked cancel
|
|
return false;
|
|
}
|
|
|
|
if (mb.clickedButton() == yes_btn && !SaveProject()) {
|
|
// The save failed, stop closing projects
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// For safety, the undo stack is cleared so no commands try to affect a freed project
|
|
undo_stack_.clear();
|
|
|
|
Project *tmp = open_project_;
|
|
SetActiveProject(nullptr);
|
|
delete tmp;
|
|
}
|
|
|
|
// Ensure a project is always active
|
|
if (auto_open_new) {
|
|
CreateNewProject();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Core::CacheActiveSequence(bool in_out_only)
|
|
{
|
|
TimeBasedPanel *p =
|
|
PanelManager::instance()->MostRecentlyFocused<TimeBasedPanel>();
|
|
|
|
if (p && p->GetConnectedViewer()) {
|
|
// Hacky but works for now
|
|
|
|
// Find Viewer attached to this TimeBasedPanel
|
|
QList<ViewerPanel *> all_viewers =
|
|
PanelManager::instance()->GetPanelsOfType<ViewerPanel>();
|
|
|
|
ViewerPanel *found_panel = nullptr;
|
|
|
|
foreach (ViewerPanel *viewer, all_viewers) {
|
|
if (viewer->GetConnectedViewer() == p->GetConnectedViewer()) {
|
|
found_panel = viewer;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (found_panel) {
|
|
if (in_out_only) {
|
|
found_panel->CacheSequenceInOut();
|
|
} else {
|
|
found_panel->CacheEntireSequence();
|
|
}
|
|
} else {
|
|
QMessageBox::critical(
|
|
main_window_, tr("Failed to cache sequence"),
|
|
tr("No active viewer found with this sequence."),
|
|
QMessageBox::Ok);
|
|
}
|
|
}
|
|
}
|
|
|
|
QString StripWindowsDriveLetter(QString s)
|
|
{
|
|
// HACK: On Windows, absolute paths are saved with a drive letter (e.g. "C:\video.mp4"). Below,
|
|
// we use Qt's relative path system to resolve when an entire project may be in a different
|
|
// folder, but the files are all in the same place relatively to the project. Unfortunately,
|
|
// Qt chooses not to understand paths from Windows on non-Windows platforms, which causes
|
|
// this to break when a project is moving from Windows to non-Windows. To resolve that, if
|
|
// we're on a non-Windows platform and we detect a Windows path (i.e. a path with a drive
|
|
// letter at the start), we strip it off. We also convert any back-slashes to forward-slashes
|
|
// because on Windows they are interchangeable and on non-Windows they are not.
|
|
#ifndef Q_OS_WINDOWS
|
|
if (s.size() >= 2) {
|
|
if (s.at(0).isLetter() && s.at(1) == ':') {
|
|
s = s.mid(2);
|
|
s.replace('\\', '/');
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return s;
|
|
}
|
|
|
|
bool Core::ValidateFootageInLoadedProject(Project *project,
|
|
const QString &project_saved_url)
|
|
{
|
|
QVector<Footage *> footage_we_couldnt_validate;
|
|
|
|
for (Node *n : project->nodes()) {
|
|
if (Footage *footage = dynamic_cast<Footage *>(n)) {
|
|
QString footage_fn = StripWindowsDriveLetter(footage->filename());
|
|
QString project_fn = StripWindowsDriveLetter(project_saved_url);
|
|
|
|
if (!QFileInfo::exists(footage_fn) &&
|
|
!project_saved_url.isEmpty()) {
|
|
// If the footage doesn't exist, it might have moved with the project
|
|
const QString &project_current_url = project->filename();
|
|
|
|
if (project_current_url != project_fn) {
|
|
// Project has definitely moved, try to resolve relative paths
|
|
QDir saved_dir(QFileInfo(project_fn).dir());
|
|
QDir true_dir(QFileInfo(project_current_url).dir());
|
|
|
|
QString relative_filename =
|
|
saved_dir.relativeFilePath(footage_fn);
|
|
QString transformed_abs_filename =
|
|
true_dir.filePath(relative_filename);
|
|
|
|
if (QFileInfo::exists(transformed_abs_filename)) {
|
|
// Use this file instead
|
|
qInfo() << "Resolved" << footage_fn << "relatively to"
|
|
<< transformed_abs_filename;
|
|
footage->set_filename(transformed_abs_filename);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (QFileInfo::exists(footage->filename())) {
|
|
// Assume valid
|
|
footage->SetValid();
|
|
} else {
|
|
footage_we_couldnt_validate.append(footage);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!footage_we_couldnt_validate.isEmpty()) {
|
|
FootageRelinkDialog frd(footage_we_couldnt_validate, main_window_);
|
|
if (frd.exec() == QDialog::Rejected) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Core::SetLanguage(const QString &locale)
|
|
{
|
|
QApplication::removeTranslator(translator_);
|
|
|
|
QString resource_path = QStringLiteral(":/ts/%1").arg(locale);
|
|
if (translator_->load(resource_path) &&
|
|
QApplication::installTranslator(translator_)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void Core::OpenProject()
|
|
{
|
|
QString file = QFileDialog::getOpenFileName(
|
|
main_window_, tr("Open Project"), QString(), GetProjectFilter(true));
|
|
|
|
if (!file.isEmpty()) {
|
|
OpenProjectInternal(file);
|
|
}
|
|
}
|
|
|
|
Core::CoreParams::CoreParams()
|
|
: mode_(kRunNormal)
|
|
, run_fullscreen_(false)
|
|
, crash_(false)
|
|
{
|
|
}
|
|
|
|
}
|