diff --git a/effects/effect.h b/effects/effect.h index 6871009a7..47dec5629 100644 --- a/effects/effect.h +++ b/effects/effect.h @@ -81,8 +81,6 @@ double log_volume(double linear); enum EffectType { EFFECT_TYPE_INVALID, - EFFECT_TYPE_VIDEO, - EFFECT_TYPE_AUDIO, EFFECT_TYPE_EFFECT, EFFECT_TYPE_TRANSITION }; diff --git a/global/config.cpp b/global/config.cpp index 228d8d9de..799abe529 100644 --- a/global/config.cpp +++ b/global/config.cpp @@ -82,7 +82,8 @@ Config::Config() default_sequence_audio_channel_layout(3), playback_bit_depth(olive::PIX_FMT_RGBA16F), export_bit_depth(olive::PIX_FMT_RGBA32F), - dont_use_proxies_on_export(true) + dont_use_proxies_on_export(true), + maximum_recent_projects(10) {} void Config::load(QString path) { diff --git a/global/config.h b/global/config.h index 3d7109e1c..3b7bbf2f6 100644 --- a/global/config.h +++ b/global/config.h @@ -615,6 +615,11 @@ struct Config { */ bool dont_use_proxies_on_export; + /** + * @brief The maximum amount of recent projects stored in the Open Recent list + */ + int maximum_recent_projects; + /** * @brief Load config from file * diff --git a/global/global.cpp b/global/global.cpp index a32015b26..72a0fb173 100644 --- a/global/global.cpp +++ b/global/global.cpp @@ -38,9 +38,11 @@ #include "dialogs/aboutdialog.h" #include "dialogs/speeddialog.h" #include "dialogs/actionsearch.h" +#include "dialogs/newsequencedialog.h" #include "dialogs/loaddialog.h" #include "dialogs/autocutsilencedialog.h" #include "project/loadthread.h" +#include "project/savethread.h" #include "timeline/sequence.h" #include "ui/mediaiconservice.h" #include "ui/mainwindow.h" @@ -175,16 +177,66 @@ void OliveGlobal::SetNativeStyling(QWidget *w) w->setStyleSheet(""); w->setPalette(w->style()->standardPalette()); w->setStyle(QStyleFactory::create("windowsvista")); +#else + Q_UNUSED(w); #endif } +void OliveGlobal::add_recent_project(const QString &url) +{ + bool found = false; + for (int i=0;i olive::CurrentConfig.maximum_recent_projects) { + recent_projects.removeLast(); + } + } + save_recent_projects(); +} + +void OliveGlobal::load_recent_projects() +{ + QFile f(get_recent_project_list_file()); + if (f.exists() && f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream text_stream(&f); + while (true) { + QString line = text_stream.readLine(); + if (line.isNull()) { + break; + } else { + recent_projects.append(line); + } + } + f.close(); + } +} + +int OliveGlobal::recent_project_count() +{ + return recent_projects.size(); +} + +const QString &OliveGlobal::recent_project(int index) +{ + return recent_projects.at(index); +} + void OliveGlobal::LoadProject(const QString &fn, bool autorecovery) { // QSortFilterProxyModels are not thread-safe, and as we'll be loading in another thread, leaving it connected // can cause glitches in its presentation. Therefore for the duration of the loading process, we disconnect it, // and reconnect it later once the loading is complete. - panel_project->DisconnectFilterToModel(); + for (int i=0;iDisconnectFilterToModel(); + } LoadDialog ld(olive::MainWindow); @@ -198,7 +250,9 @@ void OliveGlobal::LoadProject(const QString &fn, bool autorecovery) connect(lt, SIGNAL(report_progress(int)), &ld, SLOT(setValue(int))); lt->start(); - panel_project->ConnectFilterToModel(); + for (int i=0;iConnectFilterToModel(); + } } void OliveGlobal::ClearProject() @@ -210,11 +264,17 @@ void OliveGlobal::ClearProject() panel_effect_controls->Clear(true); // clear existing project - olive::Global->set_sequence(nullptr); + set_sequence(nullptr); panel_footage_viewer->set_media(nullptr); + // delete sequences first because it's important to close all the clips before deleting the media + QVector sequences = olive::project_model.GetAllSequences(); + for (int i=0;iset_sequence(nullptr); + } + // clear project contents (footage, sequences, etc.) - panel_project->clear(); + olive::project_model.clear(); // clear undo stack olive::UndoStack.clear(); @@ -226,7 +286,25 @@ void OliveGlobal::ClearProject() update_ui(false); // set to unmodified - olive::Global->set_modified(false); + set_modified(false); +} + +void OliveGlobal::save_recent_projects() +{ + // save to file + QFile f(get_recent_project_list_file()); + if (f.open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) { + QTextStream out(&f); + for (int i=0;i 0) { + out << "\n"; + } + out << recent_projects.at(i); + } + f.close(); + } else { + qWarning() << "Could not save recent projects"; + } } void OliveGlobal::ImportProject(const QString &fn) @@ -257,7 +335,7 @@ void OliveGlobal::open_recent(int index) { tr("The project '%1' no longer exists. Would you like to remove it from the recent projects list?").arg(recent_url), QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) { recent_projects.removeAt(index); - panel_project->save_recent_projects(); + save_recent_projects(); } } else if (can_close_project()) { OpenProjectWorker(recent_url, false); @@ -271,7 +349,7 @@ bool OliveGlobal::save_project_as() { fn += ".ove"; } update_project_filename(fn); - panel_project->save_project(false); + olive::Save(false); return true; } return false; @@ -281,7 +359,7 @@ bool OliveGlobal::save_project() { if (olive::ActiveProjectFilename.isEmpty()) { return save_project_as(); } else { - panel_project->save_project(false); + olive::Save(false); return true; } } @@ -307,6 +385,33 @@ bool OliveGlobal::can_close_project() { return true; } +void OliveGlobal::new_sequence() +{ + NewSequenceDialog nsd(olive::MainWindow); + nsd.set_sequence_name(olive::project_model.GetNextSequenceName()); + nsd.exec(); +} + +void OliveGlobal::open_import_dialog() +{ + QFileDialog fd(olive::MainWindow, tr("Import media..."), "", tr("All Files") + " (*)"); + fd.setFileMode(QFileDialog::ExistingFiles); + + if (fd.exec()) { + QStringList files = fd.selectedFiles(); + + Media* parent = nullptr; + for (int i=0;ifocused()) { + parent = panel_project.at(i)->get_selected_folder(); + break; + } + } + + olive::project_model.process_file_list(files, false, nullptr, parent); + } +} + void OliveGlobal::open_export_dialog() { if (CheckForActiveSequence()) { ExportDialog e(olive::MainWindow); @@ -345,7 +450,7 @@ void OliveGlobal::finished_initialize() { void OliveGlobal::save_autorecovery_file() { if (changed_since_last_autorecovery) { - panel_project->save_project(true); + olive::Save(true); changed_since_last_autorecovery = false; @@ -372,6 +477,12 @@ void OliveGlobal::set_sequence(SequencePtr s) panel_timeline->setFocus(); } +void OliveGlobal::clear_recent_projects() +{ + recent_projects.clear(); + save_recent_projects(); +} + void OliveGlobal::OpenProjectWorker(QString fn, bool autorecovery) { ClearProject(); update_project_filename(fn); diff --git a/global/global.h b/global/global.h index 050a28bc8..8d66c6ee6 100644 --- a/global/global.h +++ b/global/global.h @@ -159,6 +159,51 @@ public: */ static void SetNativeStyling(QWidget* w); + /** + * @brief Adds a project URL to the recent projects list + * + * @param url + * + * The project URL to add + */ + void add_recent_project(const QString& url); + + /** + * @brief Load recent projects from file + * + * Should be called on application startup. + */ + void load_recent_projects(); + + /** + * @brief Total count of recent projects + * + * @return + * + * Number of recent projects in the list + */ + int recent_project_count(); + + /** + * @brief Get the recent project at a given index + * + * @param index + * + * @return + * + * The recent project at index + */ + const QString& recent_project(int index); + + /** + * @brief Retrieves the filename of the autorecovery file to save to during this session + * + * @return + * + * A URL pointing to the autorecovery file + */ + const QString& get_autorecovery_filename(); + public slots: /** * @brief Undo user's last action @@ -265,6 +310,16 @@ public slots: */ bool can_close_project(); + /** + * @brief Opens the NewSequenceDialog to create a new Sequence + */ + void new_sequence(); + + /** + * @brief Open a file dialog for importing files into the project + */ + void open_import_dialog(); + /** * @brief Open the Export dialog to trigger an export of the current sequence. */ @@ -336,6 +391,13 @@ public slots: */ void set_sequence(SequencePtr s); + /** + * @brief Clear the recent projects list + * + * Also saves the cleared recent projects to the config file making it permanent. + */ + void clear_recent_projects(); + private: /** * @brief Internal function to handle loading a project from file @@ -403,6 +465,13 @@ private: */ void ClearProject(); + /** + * @brief Saves current recent project list to the configuration file + * + * This should be called whenever the recent projects change so the changes can be persistent. + */ + void save_recent_projects(); + /** * @brief File filter used for any file dialogs relating to Olive project files. */ @@ -438,6 +507,16 @@ private: */ bool rendering_; + /** + * @brief Internal variable for the filename to the autorecovery project file + */ + QString autorecovery_filename; + + /** + * @brief Internal list of recent projects + */ + QStringList recent_projects; + private slots: }; diff --git a/olive.pro b/olive.pro index cdff79126..77966607a 100644 --- a/olive.pro +++ b/olive.pro @@ -176,7 +176,9 @@ SOURCES += \ rendering/pixelformats.cpp \ timeline/timelinefunctions.cpp \ timeline/track.cpp \ - timeline/tracklist.cpp + timeline/tracklist.cpp \ + project/savethread.cpp \ + project/projectfunctions.cpp HEADERS += \ ui/mainwindow.h \ @@ -312,7 +314,9 @@ HEADERS += \ timeline/ghost.h \ timeline/timelinefunctions.h \ timeline/track.h \ - timeline/tracklist.h + timeline/tracklist.h \ + project/savethread.h \ + project/projectfunctions.h FORMS += diff --git a/panels/effectcontrols.cpp b/panels/effectcontrols.cpp index 49a45db7b..08bc48629 100644 --- a/panels/effectcontrols.cpp +++ b/panels/effectcontrols.cpp @@ -86,10 +86,6 @@ EffectControls::~EffectControls() Clear(true); } -bool EffectControls::keyframe_focus() { - return headers->hasFocus() || keyframeView->hasFocus(); -} - void EffectControls::set_zoom(bool in) { zoom *= (in) ? 2 : 0.5; update_keyframes(); @@ -100,7 +96,7 @@ void EffectControls::menu_select(QAction* q) { ComboAction* ca = new ComboAction(); for (int i=0;itrack() < 0) == (effect_menu_subtype == EFFECT_TYPE_VIDEO)) { + if (c->type() == effect_menu_subtype) { const EffectMeta* meta = reinterpret_cast(q->data().value()); if (effect_menu_type == EFFECT_TYPE_TRANSITION) { if (c->opening_transition == nullptr) { @@ -194,7 +190,7 @@ void EffectControls::cut() { copy(true); } -void EffectControls::show_effect_menu(int type, int subtype) { +void EffectControls::show_effect_menu(int type, Track::Type subtype) { effect_menu_type = type; effect_menu_subtype = subtype; @@ -598,6 +594,23 @@ void EffectControls::DeleteSelectedEffects() { } } +bool EffectControls::focused() +{ + if (this->hasFocus() + || headers->hasFocus() + || keyframeView->hasFocus()) { + return true; + } + + for (int i=0;iIsFocused()) { + return true; + } + } + + return false; +} + void EffectControls::Reload() { Clear(false); Load(); @@ -708,37 +721,25 @@ void EffectControls::Load() { } void EffectControls::video_effect_click() { - show_effect_menu(EFFECT_TYPE_EFFECT, EFFECT_TYPE_VIDEO); + show_effect_menu(EFFECT_TYPE_EFFECT, Track::kTypeVideo); } void EffectControls::audio_effect_click() { - show_effect_menu(EFFECT_TYPE_EFFECT, EFFECT_TYPE_AUDIO); + show_effect_menu(EFFECT_TYPE_EFFECT, Track::kTypeAudio); } void EffectControls::video_transition_click() { - show_effect_menu(EFFECT_TYPE_TRANSITION, EFFECT_TYPE_VIDEO); + show_effect_menu(EFFECT_TYPE_TRANSITION, Track::kTypeVideo); } void EffectControls::audio_transition_click() { - show_effect_menu(EFFECT_TYPE_TRANSITION, EFFECT_TYPE_AUDIO); + show_effect_menu(EFFECT_TYPE_TRANSITION, Track::kTypeAudio); } void EffectControls::resizeEvent(QResizeEvent*) { update_scrollbar(); } -bool EffectControls::is_focused() { - if (this->hasFocus()) return true; - - for (int i=0;iIsFocused()) { - return true; - } - } - - return false; -} - EffectsArea::EffectsArea(QWidget* parent) : QWidget(parent) {} diff --git a/panels/effectcontrols.h b/panels/effectcontrols.h index 0b6c6ce4b..aefd75ffa 100644 --- a/panels/effectcontrols.h +++ b/panels/effectcontrols.h @@ -32,6 +32,7 @@ #include #include "project/projectelements.h" +#include "timeline/track.h" #include "ui/timelineheader.h" #include "ui/keyframeview.h" #include "ui/resizablescrollbar.h" @@ -67,9 +68,8 @@ public: bool IsEffectSelected(Effect* e); void DeleteSelectedEffects(); - bool is_focused(); + virtual bool focused() override; void set_zoom(bool in); - bool keyframe_focus(); void delete_selected_keyframes(); void scroll_to_frame(long frame); @@ -110,7 +110,7 @@ private: void DeleteEffect(ComboAction* ca, Effect* effect_ref); - void show_effect_menu(int type, int subtype); + void show_effect_menu(int type, Track::Type subtype); void load_keyframes(); void open_effect(QVBoxLayout* hlayout, Effect *e); void UpdateTitle(); @@ -118,7 +118,7 @@ private: void setup_ui(); int effect_menu_type; - int effect_menu_subtype; + Track::Type effect_menu_subtype; QString panel_name; QWidget* video_effect_area; diff --git a/panels/grapheditor.cpp b/panels/grapheditor.cpp index c598cc5d5..dd27a705f 100644 --- a/panels/grapheditor.cpp +++ b/panels/grapheditor.cpp @@ -161,6 +161,11 @@ void GraphEditor::update_panel() { } } +bool GraphEditor::focused() +{ + return hasFocus() || view->hasFocus() || header->hasFocus(); +} + void GraphEditor::set_row(EffectRow *r) { for (int i=0;ihasFocus() || header->hasFocus(); -} - -bool GraphEditor::view_is_under_mouse() { - return view->underMouse() || header->underMouse(); -} - void GraphEditor::delete_selected_keys() { view->delete_selected_keys(); } diff --git a/panels/grapheditor.h b/panels/grapheditor.h index 577a80b11..e0d7355dc 100644 --- a/panels/grapheditor.h +++ b/panels/grapheditor.h @@ -41,8 +41,7 @@ public: void set_row(EffectRow* r); void update_panel(); - bool view_is_focused(); - bool view_is_under_mouse(); + virtual bool focused() override; void delete_selected_keys(); void select_all(); diff --git a/panels/panels.cpp b/panels/panels.cpp index eef2e83a8..f8ec065cd 100644 --- a/panels/panels.cpp +++ b/panels/panels.cpp @@ -29,7 +29,7 @@ #include #include -Project* panel_project = nullptr; +QVector panel_project; EffectControls* panel_effect_controls = nullptr; Viewer* panel_sequence_viewer = nullptr; Viewer* panel_footage_viewer = nullptr; @@ -49,33 +49,19 @@ void update_ui(bool modified) { QDockWidget *get_focused_panel(bool force_hover) { QDockWidget* w = nullptr; if (olive::CurrentConfig.hover_focus || force_hover) { - if (panel_project->underMouse()) { - w = panel_project; - } else if (panel_effect_controls->underMouse()) { - w = panel_effect_controls; - } else if (panel_sequence_viewer->underMouse()) { - w = panel_sequence_viewer; - } else if (panel_footage_viewer->underMouse()) { - w = panel_footage_viewer; - } else if (panel_timeline->underMouse()) { - w = panel_timeline; - } else if (panel_graph_editor->view_is_under_mouse()) { - w = panel_graph_editor; + for (int i=0;iunderMouse()) { + w = olive::panels.at(i); + break; + } } } if (w == nullptr) { - if (panel_project->is_focused()) { - w = panel_project; - } else if (panel_effect_controls->keyframe_focus() || panel_effect_controls->is_focused()) { - w = panel_effect_controls; - } else if (panel_sequence_viewer->is_focused()) { - w = panel_sequence_viewer; - } else if (panel_footage_viewer->is_focused()) { - w = panel_footage_viewer; - } else if (panel_timeline->focused()) { - w = panel_timeline; - } else if (panel_graph_editor->view_is_focused()) { - w = panel_graph_editor; + for (int i=0;ifocused()) { + w = olive::panels.at(i); + break; + } } } return w; @@ -87,8 +73,9 @@ void alloc_panels(QWidget* parent) { panel_footage_viewer = new Viewer(parent); panel_footage_viewer->setObjectName("footage_viewer"); panel_footage_viewer->show_videoaudio_buttons(true); - panel_project = new Project(parent); - panel_project->setObjectName("proj_root"); + Project* first_project_panel = new Project(parent); + first_project_panel->setObjectName("proj_root"); + panel_project.append(first_project_panel); panel_effect_controls = new EffectControls(parent); panel_effect_controls->setObjectName("fx_controls"); panel_timeline = new Timeline(parent); diff --git a/panels/panels.h b/panels/panels.h index 52a32da15..a051f84c0 100644 --- a/panels/panels.h +++ b/panels/panels.h @@ -27,7 +27,7 @@ #include "grapheditor.h" #include "project.h" -extern Project* panel_project; +extern QVector panel_project; extern EffectControls* panel_effect_controls; extern Viewer* panel_sequence_viewer; extern Viewer* panel_footage_viewer; diff --git a/panels/project.cpp b/panels/project.cpp index c75a1ab35..1c423a0ef 100644 --- a/panels/project.cpp +++ b/panels/project.cpp @@ -51,8 +51,8 @@ extern "C" { #include "rendering/cacher.h" #include "dialogs/replaceclipmediadialog.h" #include "panels/effectcontrols.h" -#include "dialogs/newsequencedialog.h" #include "dialogs/mediapropertiesdialog.h" +#include "dialogs/newsequencedialog.h" #include "dialogs/loaddialog.h" #include "project/clipboard.h" #include "ui/sourcetable.h" @@ -62,14 +62,10 @@ extern "C" { #include "ui/mediaiconservice.h" #include "project/sourcescommon.h" #include "project/projectfilter.h" +#include "project/projectfunctions.h" #include "global/debug.h" #include "ui/menu.h" -#define MAXIMUM_RECENT_PROJECTS 10 // FIXME: should be configurable - -QString autorecovery_filename; -QStringList recent_projects; - Project::Project(QWidget *parent) : Panel(parent), sorter(this), @@ -225,97 +221,6 @@ void Project::Retranslate() { setWindowTitle(tr("Project")); } -QString Project::get_next_sequence_name(QString start) { - if (start.isEmpty()) start = tr("Sequence"); - - int n = 1; - bool found = true; - QString name; - while (found) { - found = false; - name = start + " "; - if (n < 10) { - name += "0"; - } - name += QString::number(n); - for (int i=0;iget_name(), name, Qt::CaseInsensitive) == 0) { - found = true; - n++; - break; - } - } - } - return name; -} - -SequencePtr create_sequence_from_media(QVector& media_list) { - SequencePtr s(new Sequence()); - - s->name = panel_project->get_next_sequence_name(); - - // Retrieve default Sequence settings from Config - s->width = olive::CurrentConfig.default_sequence_width; - s->height = olive::CurrentConfig.default_sequence_height; - s->frame_rate = olive::CurrentConfig.default_sequence_framerate; - s->audio_frequency = olive::CurrentConfig.default_sequence_audio_frequency; - s->audio_layout = olive::CurrentConfig.default_sequence_audio_channel_layout; - - bool got_video_values = false; - bool got_audio_values = false; - for (int i=0;iget_type()) { - case MEDIA_TYPE_FOOTAGE: - { - Footage* m = media->to_footage(); - if (m->ready) { - if (!got_video_values) { - for (int j=0;jvideo_tracks.size();j++) { - const FootageStream& ms = m->video_tracks.at(j); - s->width = ms.video_width; - s->height = ms.video_height; - if (!qFuzzyCompare(ms.video_frame_rate, 0.0)) { - s->frame_rate = ms.video_frame_rate * m->speed; - - if (ms.video_interlacing != VIDEO_PROGRESSIVE) s->frame_rate *= 2; - - // only break with a decent frame rate, otherwise there may be a better candidate - got_video_values = true; - break; - } - } - } - if (!got_audio_values && m->audio_tracks.size() > 0) { - const FootageStream& ms = m->audio_tracks.at(0); - s->audio_frequency = ms.audio_frequency; - got_audio_values = true; - } - } - } - break; - case MEDIA_TYPE_SEQUENCE: - { - // Clone all attributes of the original sequence (seq) into the new one (s) - Sequence* seq = media->to_sequence().get(); - - s->width = seq->width; - s->height = seq->height; - s->frame_rate = seq->frame_rate; - s->audio_frequency = seq->audio_frequency; - s->audio_layout = seq->audio_layout; - - got_video_values = true; - got_audio_values = true; - } - break; - } - if (got_video_values && got_audio_values) break; - } - - return s; -} - void Project::duplicate_selected() { QModelIndexList items = get_current_selected(); bool duped = false; @@ -323,7 +228,7 @@ void Project::duplicate_selected() { for (int j=0;jget_type() == MEDIA_TYPE_SEQUENCE) { - create_sequence_internal(ca, i->to_sequence()->copy(), false, item_to_media(items.at(j).parent())); + olive::project_model.CreateSequence(ca, i->to_sequence()->copy(), false, item_to_media(items.at(j).parent())); duped = true; } } @@ -339,25 +244,11 @@ void Project::replace_selected_file() { if (selected_items.size() == 1) { MediaPtr item = item_to_media_ptr(selected_items.at(0)); if (item->get_type() == MEDIA_TYPE_FOOTAGE) { - replace_media(item, nullptr); + sources_common.replace_media(item, nullptr); } } } -void Project::replace_media(MediaPtr item, QString filename) { - if (filename.isEmpty()) { - filename = QFileDialog::getOpenFileName( - this, - tr("Replace '%1'").arg(item->get_name()), - "", - tr("All Files") + " (*)"); - } - if (!filename.isEmpty()) { - ReplaceMediaCommand* rmc = new ReplaceMediaCommand(item, filename); - olive::UndoStack.push(rmc); - } -} - void Project::replace_clip_media() { if (olive::ActiveSequence == nullptr) { QMessageBox::critical(this, @@ -416,7 +307,7 @@ void Project::open_properties() { } void Project::new_folder() { - MediaPtr m = create_folder_internal(nullptr); + MediaPtr m = olive::project::CreateFolder(nullptr); olive::UndoStack.push(new AddMediaCommand(m, get_selected_folder())); QModelIndex index = olive::project_model.create_index(m->row(), 0, m.get()); @@ -430,54 +321,10 @@ void Project::new_folder() { } } -void Project::new_sequence() { - NewSequenceDialog nsd(this); - nsd.set_sequence_name(get_next_sequence_name()); - nsd.exec(); -} - -MediaPtr Project::create_sequence_internal(ComboAction *ca, SequencePtr s, bool open, Media* parent) { - - MediaPtr item = std::make_shared(); - item->set_sequence(s); - - if (ca != nullptr) { - - ca->append(new AddMediaCommand(item, parent)); - - if (open) { - ca->append(new ChangeSequenceAction(s)); - } - - } else { - - olive::project_model.appendChild(parent, item); - - if (open) { - olive::Global->set_sequence(s); - } - - } - - return item; - -} - -QString Project::get_file_name_from_path(const QString& path) { - return path.mid(path.lastIndexOf('/')+1); -} - -bool Project::is_focused() { +bool Project::focused() { return tree_view->hasFocus() || icon_view->hasFocus(); } -MediaPtr Project::create_folder_internal(QString name) { - MediaPtr item = std::make_shared(); - item->set_folder(); - item->set_name(name); - return item; -} - Media* Project::item_to_media(const QModelIndex &index) { return static_cast(sorter.mapToSource(index).internalPointer()); } @@ -665,258 +512,6 @@ void Project::delete_selected_media() { } } -void Project::process_file_list(QStringList& files, bool recursive, MediaPtr replace, Media* parent) { - bool imported = false; - - // retrieve the array of image formats from the user's configuration - QStringList image_sequence_formats = olive::CurrentConfig.img_seq_formats.split("|"); - - // a cache of image sequence formatted URLS to assist the user in importing image sequences - QVector image_sequence_urls; - QVector image_sequence_importassequence; - - if (!recursive) last_imported_media.clear(); - - bool create_undo_action = (!recursive && replace == nullptr); - ComboAction* ca = nullptr; - if (create_undo_action) ca = new ComboAction(); - - // Loop through received files - for (int i=0;iappend(new AddMediaCommand(folder, parent)); - } else { - olive::project_model.appendChild(parent, folder); - } - - process_file_list(subdir_filenames, true, nullptr, folder.get()); - - imported = true; - - } else if (!files.at(i).isEmpty()) { - QString file = files.at(i); - - // Check if the user is importing an Olive project file - if (file.endsWith(".ove", Qt::CaseInsensitive)) { - - // This file is an Olive project file. Ask the user if they really want to import it. - if (QMessageBox::question(this, - tr("Import a Project"), - tr("\"%1\" is an Olive project file. It will merge with this project. " - "Do you wish to continue?").arg(file), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - - // load the project without clearing the current one - olive::Global->ImportProject(file); - - } - - } else { - - // This file is NOT an Olive project file - - // Used later if this file is part of an already processed image sequence - bool skip = false; - - /* Heuristic to determine whether file is part of an image sequence */ - - // Firstly, we run a heuristic on whether this file is an image by checking its file extension - - bool file_is_an_image = false; - - // Get the string position of the extension in the filename - int lastcharindex = file.lastIndexOf("."); - - if (lastcharindex != -1 && lastcharindex > file.lastIndexOf('/')) { - - QString ext = file.mid(lastcharindex+1); - - // If the file extension is part of a predetermined list (from Config::img_seq_formats), we'll treat it - // as an image - if (image_sequence_formats.contains(ext, Qt::CaseInsensitive)) { - file_is_an_image = true; - } - - } else { - - // If we're here, the file has no extension, but we'll still check if its an image sequence just in case - lastcharindex = file.length(); - file_is_an_image = true; - - } - - // Some image sequence's don't start at "0", if it is indeed an image sequence, we'll use this variable - // later to determine where it does start - int start_number = 0; - - // Check if we passed the earlier heuristic to check whether this is a file, and whether the last number in - // the filename (before the extension) is a number - if (file_is_an_image && file[lastcharindex-1].isDigit()) { - - // Check how many digits are at the end of this filename - int digit_count = 0; - int digit_test = lastcharindex-1; - while (file[digit_test].isDigit()) { - digit_count++; - digit_test--; - } - - // Retrieve the integer represented at the end of this filename - digit_test++; - int file_number = file.mid(digit_test, digit_count).toInt(); - - // Check whether a file exists with the same format but one number higher or one number lower - if (QFileInfo::exists(QString(file.left(digit_test) + QString("%1").arg(file_number-1, digit_count, 10, QChar('0')) + file.mid(lastcharindex))) - || QFileInfo::exists(QString(file.left(digit_test) + QString("%1").arg(file_number+1, digit_count, 10, QChar('0')) + file.mid(lastcharindex)))) { - - // - // If so, it certainly looks like it *could* be an image sequence, but we'll ask the user just in case - // - - // Firstly we should check if this file is part of a sequence the user has already confirmed as either a - // sequence or not a sequence (e.g. if the user happened to select a bunch of images that happen to increase - // consecutively). We format the filename with FFmpeg's '%Nd' (N = digits) formatting for reading image - // sequences - QString new_filename = file.left(digit_test) + "%" + QString::number(digit_count) + "d" + file.mid(lastcharindex); - - int does_url_cache_already_contain_this = image_sequence_urls.indexOf(new_filename); - - if (does_url_cache_already_contain_this > -1) { - - // We've already processed an image with the same formatting - - // Check if the last time we saw this formatting, the user chose to import as a sequence - if (image_sequence_importassequence.at(does_url_cache_already_contain_this)) { - - // If so, no need to import this file too, so we signal to the rest of the function to skip this file - skip = true; - - } - - // If not, we can fall-through to the next step which is importing normally - - } else { - - // If we're here, we've never seen a file with this formatting before, so we'll ask whether to import - // as a sequence or not - - // Add this file formatting file to the URL cache - image_sequence_urls.append(new_filename); - - // This does look like an image sequence, let's ask the user if it'll indeed be an image sequence - if (QMessageBox::question(this, - tr("Image sequence detected"), - tr("The file '%1' appears to be part of an image sequence. " - "Would you like to import it as such?").arg(file), - QMessageBox::Yes | QMessageBox::No, - QMessageBox::Yes) == QMessageBox::Yes) { - - // Proceed to the next step of this with the formatted filename - file = new_filename; - - // Cache the user's answer alongside the image_sequence_urls value - in this case, YES, this will be an - // image sequence - image_sequence_importassequence.append(true); - - - // FFmpeg needs to know what file number to start at in the sequence. In case the image sequence doesn't - // start at a zero, we'll loop decreasing the number until it doesn't exist anymore - QString test_filename_format = QString("%1%2%3").arg(file.left(digit_test), "%1", file.mid(lastcharindex)); - int test_file_number = file_number; - do { - test_file_number--; - } while (QFileInfo::exists(test_filename_format.arg(QString("%1").arg(test_file_number, digit_count, 10, QChar('0'))))); - - // set the image sequence's start number to the last that existed - start_number = test_file_number + 1; - - } else { - - // Cache the user's response to the image sequence question - i.e. none of the files imported with this - // formatting should be imported as an image sequence - image_sequence_importassequence.append(false); - - } - } - - } - - } - - // If we're not skipping this file, let's import it - if (!skip) { - MediaPtr item; - FootagePtr m; - - if (replace != nullptr) { - item = replace; - } else { - item = std::make_shared(); - } - - m = std::make_shared(); - - // Edge case for PNGs that standardized unassociated alpha - if (file.endsWith("png", Qt::CaseInsensitive)) { - m->alpha_is_associated = false; - } - - m->using_inout = false; - m->url = file; - m->name = get_file_name_from_path(files.at(i)); - m->start_number = start_number; - - item->set_footage(m); - - last_imported_media.append(item.get()); - - if (replace == nullptr) { - if (create_undo_action) { - ca->append(new AddMediaCommand(item, parent)); - } else { - olive::project_model.appendChild(parent, item); - } - } - - imported = true; - } - - } - - - } - } - if (create_undo_action) { - if (imported) { - olive::UndoStack.push(ca); - - for (int i=0;iset_row(nullptr); - - // clear effects cache - panel_effect_controls->Clear(true); - - // delete sequences first because it's important to close all the clips before deleting the media - QVector sequences = list_all_project_sequences(); - for (int i=0;iset_sequence(nullptr); - } - - // delete everything else - olive::project_model.clear(); - - // update tree view (sometimes this doesn't seem to update reliably) - tree_view->update(); -} - -void Project::save_folder(QXmlStreamWriter& stream, int type, bool set_ids_only, const QModelIndex& parent) { - for (int i=0;iget_type()) { - if (m->get_type() == MEDIA_TYPE_FOLDER) { - if (set_ids_only) { - m->temp_id = folder_id; // saves a temporary ID for matching in the project file - folder_id++; - } else { - // if we're saving folders, save the folder - stream.writeStartElement("folder"); - stream.writeAttribute("name", m->get_name()); - stream.writeAttribute("id", QString::number(m->temp_id)); - if (!item.parent().isValid()) { - stream.writeAttribute("parent", "0"); - } else { - stream.writeAttribute("parent", QString::number(olive::project_model.getItem(item.parent())->temp_id)); - } - stream.writeEndElement(); - } - // save_folder(stream, item, type, set_ids_only); - } else { - int folder = m->parentItem()->temp_id; - if (type == MEDIA_TYPE_FOOTAGE) { - Footage* f = m->to_footage(); - f->save_id = media_id; - stream.writeStartElement("footage"); - stream.writeAttribute("id", QString::number(media_id)); - stream.writeAttribute("folder", QString::number(folder)); - stream.writeAttribute("name", f->name); - stream.writeAttribute("url", proj_dir.relativeFilePath(f->url)); - stream.writeAttribute("duration", QString::number(f->length)); - stream.writeAttribute("using_inout", QString::number(f->using_inout)); - stream.writeAttribute("in", QString::number(f->in)); - stream.writeAttribute("out", QString::number(f->out)); - stream.writeAttribute("speed", QString::number(f->speed)); - stream.writeAttribute("alphapremul", QString::number(f->alpha_is_associated)); - stream.writeAttribute("startnumber", QString::number(f->start_number)); - stream.writeAttribute("colorspace", f->Colorspace()); - - stream.writeAttribute("proxy", QString::number(f->proxy)); - stream.writeAttribute("proxypath", f->proxy_path); - - // save video stream metadata - for (int j=0;jvideo_tracks.size();j++) { - const FootageStream& ms = f->video_tracks.at(j); - stream.writeStartElement("video"); - stream.writeAttribute("id", QString::number(ms.file_index)); - stream.writeAttribute("width", QString::number(ms.video_width)); - stream.writeAttribute("height", QString::number(ms.video_height)); - stream.writeAttribute("framerate", QString::number(ms.video_frame_rate, 'f', 10)); - stream.writeAttribute("infinite", QString::number(ms.infinite_length)); - stream.writeEndElement(); // video - } - - // save audio stream metadata - for (int j=0;jaudio_tracks.size();j++) { - const FootageStream& ms = f->audio_tracks.at(j); - stream.writeStartElement("audio"); - stream.writeAttribute("id", QString::number(ms.file_index)); - stream.writeAttribute("channels", QString::number(ms.audio_channels)); - stream.writeAttribute("layout", QString::number(ms.audio_layout)); - stream.writeAttribute("frequency", QString::number(ms.audio_frequency)); - stream.writeEndElement(); // audio - } - - // save footage markers - for (int j=0;jmarkers.size();j++) { - f->markers.at(j).Save(stream); - } - - stream.writeEndElement(); // footage - media_id++; - } else if (type == MEDIA_TYPE_SEQUENCE) { - Sequence* s = m->to_sequence().get(); - if (set_ids_only) { - s->save_id = sequence_id; - sequence_id++; - } else { - s->Save(stream); - } - } - } - } - - if (m->get_type() == MEDIA_TYPE_FOLDER) { - save_folder(stream, type, set_ids_only, item); - } - } -} - -void Project::save_project(bool autorecovery) { - folder_id = 1; - media_id = 1; - sequence_id = 1; - - QFile file(autorecovery ? autorecovery_filename : olive::ActiveProjectFilename); - if (!file.open(QIODevice::WriteOnly)) { - qCritical() << "Could not open file"; - return; - } - - QXmlStreamWriter stream(&file); - stream.setAutoFormatting(true); - stream.writeStartDocument(); // doc - - stream.writeStartElement("project"); // project - - stream.writeTextElement("version", QString::number(olive::kSaveVersion)); - - stream.writeTextElement("url", olive::ActiveProjectFilename); - proj_dir = QFileInfo(olive::ActiveProjectFilename).absoluteDir(); - - save_folder(stream, MEDIA_TYPE_FOLDER, true); - - stream.writeStartElement("folders"); // folders - save_folder(stream, MEDIA_TYPE_FOLDER, false); - stream.writeEndElement(); // folders - - stream.writeStartElement("media"); // media - save_folder(stream, MEDIA_TYPE_FOOTAGE, false); - stream.writeEndElement(); // media - - save_folder(stream, MEDIA_TYPE_SEQUENCE, true); - - stream.writeStartElement("sequences"); // sequences - save_folder(stream, MEDIA_TYPE_SEQUENCE, false); - stream.writeEndElement();// sequences - - stream.writeEndElement(); // project - - stream.writeEndDocument(); // doc - - file.close(); - - if (!autorecovery) { - add_recent_project(olive::ActiveProjectFilename); - olive::Global->set_modified(false); - } -} - void Project::update_view_type() { tree_view->setVisible(olive::CurrentConfig.project_view_type == olive::PROJECT_VIEW_TREE); icon_view_container->setVisible(olive::CurrentConfig.project_view_type == olive::PROJECT_VIEW_ICON @@ -1229,28 +651,6 @@ void Project::set_tree_view() { update_view_type(); } -void Project::save_recent_projects() { - // save to file - QFile f(olive::Global->get_recent_project_list_file()); - if (f.open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) { - QTextStream out(&f); - for (int i=0;i 0) { - out << "\n"; - } - out << recent_projects.at(i); - } - f.close(); - } else { - qWarning() << "Could not save recent projects"; - } -} - -void Project::clear_recent_projects() { - recent_projects.clear(); - save_recent_projects(); -} - void Project::set_icon_view_size(int s) { if (icon_view->viewMode() == QListView::IconMode) { icon_view->setGridSize(QSize(s, s)); @@ -1275,44 +675,6 @@ void Project::make_new_menu() { new_menu.exec(QCursor::pos()); } -void Project::add_recent_project(QString url) { - bool found = false; - for (int i=0;i MAXIMUM_RECENT_PROJECTS) { - recent_projects.removeLast(); - } - } - save_recent_projects(); -} - -void Project::list_all_sequences_worker(QVector* list, Media* parent) { - for (int i=0;iget_type()) { - case MEDIA_TYPE_SEQUENCE: - list->append(item); - break; - case MEDIA_TYPE_FOLDER: - list_all_sequences_worker(list, item); - break; - } - } -} - -QVector Project::list_all_project_sequences() { - QVector list; - list_all_sequences_worker(&list, nullptr); - return list; -} - QModelIndexList Project::get_current_selected() { if (olive::CurrentConfig.project_view_type == olive::PROJECT_VIEW_TREE) { return tree_view->selectionModel()->selectedRows(); diff --git a/panels/project.h b/panels/project.h index 7d959a3e4..2cc8c826c 100644 --- a/panels/project.h +++ b/panels/project.h @@ -37,17 +37,8 @@ #include "ui/sourceiconview.h" #include "timeline/mediaimportdata.h" #include "undo/undo.h" - #include "ui/sourcetable.h" -extern QString autorecovery_filename; -extern QStringList recent_projects; - -SequencePtr create_sequence_from_media(QVector &media_list); - -QString get_channel_layout_name(int channels, uint64_t layout); -QString get_interlacing_name(int interlacing); - class Project : public Panel { Q_OBJECT public: @@ -56,29 +47,14 @@ public: void ConnectFilterToModel(); void DisconnectFilterToModel(); - bool is_focused(); - void clear(); - MediaPtr create_sequence_internal(ComboAction *ca, SequencePtr s, bool open, Media* parent); - QString get_next_sequence_name(QString start = nullptr); - void process_file_list(QStringList& files, bool recursive = false, MediaPtr replace = nullptr, Media *parent = nullptr); - void replace_media(MediaPtr item, QString filename); + virtual bool focused() override; + Media* get_selected_folder(); bool reveal_media(Media *media, QModelIndex parent = QModelIndex()); - void add_recent_project(QString url); - - void save_project(bool autorecovery); - - MediaPtr create_folder_internal(QString name); Media* item_to_media(const QModelIndex& index); MediaPtr item_to_media_ptr(const QModelIndex &index); - void save_recent_projects(); - - QVector list_all_project_sequences(); - - QVector last_imported_media; - QModelIndexList get_current_selected(); bool IsToolbarVisible(); @@ -87,7 +63,6 @@ public: virtual void Retranslate() override; protected: public slots: - void import_dialog(); void delete_selected_media(); void duplicate_selected(); void delete_clips_using_selected_media(); @@ -95,17 +70,8 @@ public slots: void replace_clip_media(); void open_properties(); void new_folder(); - void new_sequence(); - void SetToolbarVisible(bool visible); private: - void save_folder(QXmlStreamWriter& stream, int type, bool set_ids_only, const QModelIndex &parent = QModelIndex()); - int folder_id; - int media_id; - int sequence_id; - void list_all_sequences_worker(QVector *list, Media* parent); - QString get_file_name_from_path(const QString &path); - QDir proj_dir; QWidget* icon_view_container; QSlider* icon_size_slider; QPushButton* directory_up; @@ -122,7 +88,6 @@ private slots: void set_icon_view(); void set_list_view(); void set_tree_view(); - void clear_recent_projects(); void set_icon_view_size(int); void set_up_dir_enabled(); void go_up_dir(); diff --git a/panels/timeline.h b/panels/timeline.h index f1ce05260..9f15ab10c 100644 --- a/panels/timeline.h +++ b/panels/timeline.h @@ -51,7 +51,7 @@ class Timeline : public Panel public: explicit Timeline(QWidget *parent = nullptr); - bool focused(); + virtual bool focused() override; void multiply_zoom(double m); void copy(bool del); ClipPtr split_clip(ComboAction* ca, bool transitions, int p, long frame); diff --git a/panels/viewer.cpp b/panels/viewer.cpp index fd60fd994..afe282de2 100644 --- a/panels/viewer.cpp +++ b/panels/viewer.cpp @@ -34,6 +34,7 @@ extern "C" { #include #include #include +#include #include "rendering/audio.h" #include "timeline.h" @@ -105,7 +106,7 @@ void Viewer::Retranslate() { // update_window_title(); } -bool Viewer::is_focused() { +bool Viewer::focused() { return headers->hasFocus() || viewer_widget_->hasFocus() || go_to_start_button->hasFocus() @@ -324,29 +325,47 @@ void Viewer::pause() { // import audio QStringList file_list; file_list.append(get_recorded_audio_filename()); - panel_project->process_file_list(file_list); + olive::project_model.process_file_list(file_list); // add it to the sequence - ClipPtr c = std::make_shared(seq.get()); - Media* m = panel_project->last_imported_media.at(0); + QVector last_imported_media = olive::project_model.GetLastImportedMedia(); + Media* m = last_imported_media.first(); Footage* f = m->to_footage(); // wait for footage to be completely ready before taking metadata from it f->ready_lock.lock(); - - c->set_media(m, 0); // latest media - c->set_timeline_in(recording_start); - c->set_timeline_out(recording_start + f->get_length_in_frames(seq->frame_rate)); - c->set_clip_in(0); - c->set_track(recording_track); - c->set_color(128, 192, 128); - c->set_name(m->get_name()); - f->ready_lock.unlock(); - QVector add_clips; - add_clips.append(c); - olive::UndoStack.push(new AddClipCommand(seq.get(), add_clips)); // add clip + // Check if we were able to import the audio we just recorded + if (f->invalid) { + + QMessageBox::critical(this, + tr("Failed to import recorded file"), + tr("An error occurred trying to import the recorded audio"), + QMessageBox::Ok); + + } else { + + // Make a clip out of it and add it to the Sequence + + ClipPtr c = std::make_shared(seq.get()); + + c->set_media(m, 0); // latest media + c->set_timeline_in(recording_start); + c->set_timeline_out(recording_start + f->get_length_in_frames(seq->frame_rate)); + c->set_clip_in(0); + c->set_track(recording_track); + c->set_color(128, 192, 128); + c->set_name(m->get_name()); + + QVector add_clips; + add_clips.append(c); + olive::UndoStack.push(new AddClipCommand(seq.get(), add_clips)); // add clip + + } + + + } } } diff --git a/panels/viewer.h b/panels/viewer.h index e488a9696..431c5eb06 100644 --- a/panels/viewer.h +++ b/panels/viewer.h @@ -44,7 +44,7 @@ class Viewer : public Panel public: explicit Viewer(QWidget *parent = nullptr); - bool is_focused(); + virtual bool focused() override; bool is_main_sequence(); void set_main_sequence(); void set_media(Media *m); diff --git a/project/footage.cpp b/project/footage.cpp index 61c37b13c..8048e9a0c 100644 --- a/project/footage.cpp +++ b/project/footage.cpp @@ -29,6 +29,7 @@ namespace OCIO = OCIO_NAMESPACE::v1; #include "project/previewgenerator.h" #include "timeline/clip.h" #include "global/config.h" +#include "global/global.h" Footage::Footage() : ready(false), @@ -48,6 +49,58 @@ Footage::~Footage() { reset(); } +void Footage::Save(QXmlStreamWriter &stream) +{ + QDir proj_dir = QFileInfo(olive::ActiveProjectFilename).absoluteDir(); + + stream.writeStartElement("footage"); + stream.writeAttribute("id", QString::number(media_id)); + stream.writeAttribute("name", name); + stream.writeAttribute("url", proj_dir.relativeFilePath(url)); + stream.writeAttribute("duration", QString::number(length)); + stream.writeAttribute("using_inout", QString::number(using_inout)); + stream.writeAttribute("in", QString::number(in)); + stream.writeAttribute("out", QString::number(out)); + stream.writeAttribute("speed", QString::number(speed)); + stream.writeAttribute("alphapremul", QString::number(alpha_is_associated)); + stream.writeAttribute("startnumber", QString::number(start_number)); + stream.writeAttribute("colorspace", Colorspace()); + + stream.writeAttribute("proxy", QString::number(proxy)); + stream.writeAttribute("proxypath", proxy_path); + + // save video stream metadata + for (int j=0;jvideo_tracks.size();j++) { + const FootageStream& ms = f->video_tracks.at(j); + stream.writeStartElement("video"); + stream.writeAttribute("id", QString::number(ms.file_index)); + stream.writeAttribute("width", QString::number(ms.video_width)); + stream.writeAttribute("height", QString::number(ms.video_height)); + stream.writeAttribute("framerate", QString::number(ms.video_frame_rate, 'f', 10)); + stream.writeAttribute("infinite", QString::number(ms.infinite_length)); + stream.writeEndElement(); // video + } + + // save audio stream metadata + for (int j=0;jaudio_tracks.size();j++) { + const FootageStream& ms = f->audio_tracks.at(j); + stream.writeStartElement("audio"); + stream.writeAttribute("id", QString::number(ms.file_index)); + stream.writeAttribute("channels", QString::number(ms.audio_channels)); + stream.writeAttribute("layout", QString::number(ms.audio_layout)); + stream.writeAttribute("frequency", QString::number(ms.audio_frequency)); + stream.writeEndElement(); // audio + } + + // save footage markers + for (int j=0;jmarkers.size();j++) { + f->markers.at(j).Save(stream); + } + + stream.writeEndElement(); // footage + media_id++; +} + QString Footage::Colorspace() { if (!colorspace_.isEmpty()) { diff --git a/project/footage.h b/project/footage.h index 2678769d3..e77ee6af1 100644 --- a/project/footage.h +++ b/project/footage.h @@ -70,6 +70,8 @@ public: Footage(); ~Footage(); + void Save(QXmlStreamWriter& stream); + // footage metadata QString url; QString name; @@ -107,6 +109,9 @@ public: long get_length_in_frames(double frame_rate); FootageStream *get_stream_from_file_index(bool video, int index); void reset(); + + static QString get_channel_layout_name(int channels, uint64_t layout); + static QString get_interlacing_name(int interlacing); private: QString colorspace_; }; diff --git a/project/media.cpp b/project/media.cpp index 53914a4c5..a45560436 100644 --- a/project/media.cpp +++ b/project/media.cpp @@ -68,6 +68,23 @@ Media::Media() : { } +void Media::Save(QXmlStreamWriter &stream) +{ + switch (type) { + case MEDIA_TYPE_FOLDER: + stream.writeStartElement("folder"); + stream.writeAttribute("name", get_name()); + stream.writeEndElement(); + break; + case MEDIA_TYPE_FOOTAGE: + to_footage()->Save(stream); + break; + case MEDIA_TYPE_SEQUENCE: + to_sequence()->Save(stream); + break; + } +} + Footage* Media::to_footage() { return static_cast(object.get()); } diff --git a/project/media.h b/project/media.h index a604e2180..e9555b310 100644 --- a/project/media.h +++ b/project/media.h @@ -47,6 +47,8 @@ class Media public: Media(); + void Save(QXmlStreamWriter& stream); + Footage *to_footage(); SequencePtr to_sequence(); void set_icon(const QString& str); diff --git a/project/projectfunctions.cpp b/project/projectfunctions.cpp new file mode 100644 index 000000000..5d9b8c486 --- /dev/null +++ b/project/projectfunctions.cpp @@ -0,0 +1,83 @@ +#include "projectfunctions.h" + +#include "projectmodel.h" +#include "global/config.h" + +MediaPtr olive::project::CreateFolder(QString name) { + + MediaPtr item = std::make_shared(); + + item->set_folder(); + item->set_name(name); + + return item; + +} + +SequencePtr olive::project::CreateSequenceFromMedia(QVector &media_list) +{ + SequencePtr s = std::make_shared(); + + s->name = olive::project_model.GetNextSequenceName(); + + // Retrieve default Sequence settings from Config + s->width = olive::CurrentConfig.default_sequence_width; + s->height = olive::CurrentConfig.default_sequence_height; + s->frame_rate = olive::CurrentConfig.default_sequence_framerate; + s->audio_frequency = olive::CurrentConfig.default_sequence_audio_frequency; + s->audio_layout = olive::CurrentConfig.default_sequence_audio_channel_layout; + + bool got_video_values = false; + bool got_audio_values = false; + for (int i=0;iget_type()) { + case MEDIA_TYPE_FOOTAGE: + { + Footage* m = media->to_footage(); + if (m->ready) { + if (!got_video_values) { + for (int j=0;jvideo_tracks.size();j++) { + const FootageStream& ms = m->video_tracks.at(j); + s->width = ms.video_width; + s->height = ms.video_height; + if (!qFuzzyCompare(ms.video_frame_rate, 0.0)) { + s->frame_rate = ms.video_frame_rate * m->speed; + + if (ms.video_interlacing != VIDEO_PROGRESSIVE) s->frame_rate *= 2; + + // only break with a decent frame rate, otherwise there may be a better candidate + got_video_values = true; + break; + } + } + } + if (!got_audio_values && m->audio_tracks.size() > 0) { + const FootageStream& ms = m->audio_tracks.at(0); + s->audio_frequency = ms.audio_frequency; + got_audio_values = true; + } + } + } + break; + case MEDIA_TYPE_SEQUENCE: + { + // Clone all attributes of the original sequence (seq) into the new one (s) + Sequence* seq = media->to_sequence().get(); + + s->width = seq->width; + s->height = seq->height; + s->frame_rate = seq->frame_rate; + s->audio_frequency = seq->audio_frequency; + s->audio_layout = seq->audio_layout; + + got_video_values = true; + got_audio_values = true; + } + break; + } + if (got_video_values && got_audio_values) break; + } + + return s; +} diff --git a/project/projectfunctions.h b/project/projectfunctions.h new file mode 100644 index 000000000..1c62d8f24 --- /dev/null +++ b/project/projectfunctions.h @@ -0,0 +1,18 @@ +#ifndef PROJECTFUNCTIONS_H +#define PROJECTFUNCTIONS_H + +#include "project/media.h" +#include "timeline/sequence.h" +#include "timeline/mediaimportdata.h" + +namespace olive { +namespace project { + +MediaPtr CreateFolder(QString name); + +SequencePtr CreateSequenceFromMedia(QVector &media_list); + +} +} + +#endif // PROJECTFUNCTIONS_H diff --git a/project/projectmodel.cpp b/project/projectmodel.cpp index 57099ac35..001a35362 100644 --- a/project/projectmodel.cpp +++ b/project/projectmodel.cpp @@ -36,6 +36,57 @@ ProjectModel::~ProjectModel() { destroy_root(); } +int ProjectModel::PrepareToSave() +{ + int element_count = 0; + + PrepareToSaveInternal(element_count, get_root()); + + return element_count; +} + +void ProjectModel::PrepareToSaveInternal(int& element_count, Media *root) +{ + for (int i=0;ichildCount();i++) { + Media* child = root->child(i); + + switch (child->get_type()) { + case MEDIA_TYPE_FOLDER: + child->temp_id = element_count; + break; + case MEDIA_TYPE_FOOTAGE: + child->to_footage()->save_id = element_count; + break; + case MEDIA_TYPE_SEQUENCE: + child->to_sequence()->save_id = element_count; + break; + } + + element_count++; + + if (child->childCount() > 0) { + PrepareToSaveInternal(element_count, child); + } + } +} + +void ProjectModel::Save(QXmlStreamWriter &stream, Media* root) +{ + if (root == nullptr) { + root = get_root(); + } + + for (int i=0;ichildCount();i++) { + Media* child = root->child(i); + + child->Save(stream); + + if (child->childCount() > 0) { + Save(stream, child); + } + } +} + void ProjectModel::make_root() { root_item_ = std::make_shared(); root_item_->temp_id = 0; @@ -211,6 +262,58 @@ QVector ProjectModel::GetAllFolders() return GetAllMediaOfType(MEDIA_TYPE_FOLDER); } +QString ProjectModel::GetNextSequenceName(QString prepend) +{ + if (prepend.isEmpty()) { + prepend = tr("Sequence %1"); + } + + int sequence_number = 1; + QString test; + QVector all_sequences = GetAllSequences(); + bool found; + + do { + found = false; + test = prepend.arg(sequence_number); + for (int i=0;iget_name() == test) { + found = true; + sequence_number++; + break; + } + } + } while (found); + + return test; +} + +MediaPtr ProjectModel::CreateSequence(ComboAction *ca, SequencePtr s, bool open, Media *parent) +{ + MediaPtr item = std::make_shared(); + item->set_sequence(s); + + if (ca != nullptr) { + + ca->append(new AddMediaCommand(item, parent)); + + if (open) { + ca->append(new ChangeSequenceAction(s)); + } + + } else { + + appendChild(parent, item); + + if (open) { + olive::Global->set_sequence(s); + } + + } + + return item; +} + QVector ProjectModel::GetAllMediaOfType(int search_type) { QVector media_list; @@ -296,3 +399,255 @@ int ProjectModel::childCount(Media *parent) { } return parent->childCount(); } + +void ProjectModel::process_file_list(QStringList& files, bool recursive, MediaPtr replace, Media* parent) { + bool imported = false; + + // retrieve the array of image formats from the user's configuration + QStringList image_sequence_formats = olive::CurrentConfig.img_seq_formats.split("|"); + + // a cache of image sequence formatted URLS to assist the user in importing image sequences + QVector image_sequence_urls; + QVector image_sequence_importassequence; + + if (!recursive) last_imported_media.clear(); + + bool create_undo_action = (!recursive && replace == nullptr); + ComboAction* ca = nullptr; + if (create_undo_action) ca = new ComboAction(); + + // Loop through received files + for (int i=0;iappend(new AddMediaCommand(folder, parent)); + } else { + appendChild(parent, folder); + } + + process_file_list(subdir_filenames, true, nullptr, folder.get()); + + imported = true; + + } else if (!files.at(i).isEmpty()) { + QString file = files.at(i); + + // Check if the user is importing an Olive project file + if (file.endsWith(".ove", Qt::CaseInsensitive)) { + + // This file is an Olive project file. Ask the user if they really want to import it. + if (QMessageBox::question(this, + tr("Import a Project"), + tr("\"%1\" is an Olive project file. It will merge with this project. " + "Do you wish to continue?").arg(file), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + + // load the project without clearing the current one + olive::Global->ImportProject(file); + + } + + } else { + + // This file is NOT an Olive project file + + // Used later if this file is part of an already processed image sequence + bool skip = false; + + /* Heuristic to determine whether file is part of an image sequence */ + + // Firstly, we run a heuristic on whether this file is an image by checking its file extension + + bool file_is_an_image = false; + + // Get the string position of the extension in the filename + int lastcharindex = file.lastIndexOf("."); + + if (lastcharindex != -1 && lastcharindex > file.lastIndexOf('/')) { + + QString ext = file.mid(lastcharindex+1); + + // If the file extension is part of a predetermined list (from Config::img_seq_formats), we'll treat it + // as an image + if (image_sequence_formats.contains(ext, Qt::CaseInsensitive)) { + file_is_an_image = true; + } + + } else { + + // If we're here, the file has no extension, but we'll still check if its an image sequence just in case + lastcharindex = file.length(); + file_is_an_image = true; + + } + + // Some image sequence's don't start at "0", if it is indeed an image sequence, we'll use this variable + // later to determine where it does start + int start_number = 0; + + // Check if we passed the earlier heuristic to check whether this is a file, and whether the last number in + // the filename (before the extension) is a number + if (file_is_an_image && file[lastcharindex-1].isDigit()) { + + // Check how many digits are at the end of this filename + int digit_count = 0; + int digit_test = lastcharindex-1; + while (file[digit_test].isDigit()) { + digit_count++; + digit_test--; + } + + // Retrieve the integer represented at the end of this filename + digit_test++; + int file_number = file.mid(digit_test, digit_count).toInt(); + + // Check whether a file exists with the same format but one number higher or one number lower + if (QFileInfo::exists(QString(file.left(digit_test) + QString("%1").arg(file_number-1, digit_count, 10, QChar('0')) + file.mid(lastcharindex))) + || QFileInfo::exists(QString(file.left(digit_test) + QString("%1").arg(file_number+1, digit_count, 10, QChar('0')) + file.mid(lastcharindex)))) { + + // + // If so, it certainly looks like it *could* be an image sequence, but we'll ask the user just in case + // + + // Firstly we should check if this file is part of a sequence the user has already confirmed as either a + // sequence or not a sequence (e.g. if the user happened to select a bunch of images that happen to increase + // consecutively). We format the filename with FFmpeg's '%Nd' (N = digits) formatting for reading image + // sequences + QString new_filename = file.left(digit_test) + "%" + QString::number(digit_count) + "d" + file.mid(lastcharindex); + + int does_url_cache_already_contain_this = image_sequence_urls.indexOf(new_filename); + + if (does_url_cache_already_contain_this > -1) { + + // We've already processed an image with the same formatting + + // Check if the last time we saw this formatting, the user chose to import as a sequence + if (image_sequence_importassequence.at(does_url_cache_already_contain_this)) { + + // If so, no need to import this file too, so we signal to the rest of the function to skip this file + skip = true; + + } + + // If not, we can fall-through to the next step which is importing normally + + } else { + + // If we're here, we've never seen a file with this formatting before, so we'll ask whether to import + // as a sequence or not + + // Add this file formatting file to the URL cache + image_sequence_urls.append(new_filename); + + // This does look like an image sequence, let's ask the user if it'll indeed be an image sequence + if (QMessageBox::question(this, + tr("Image sequence detected"), + tr("The file '%1' appears to be part of an image sequence. " + "Would you like to import it as such?").arg(file), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes) == QMessageBox::Yes) { + + // Proceed to the next step of this with the formatted filename + file = new_filename; + + // Cache the user's answer alongside the image_sequence_urls value - in this case, YES, this will be an + // image sequence + image_sequence_importassequence.append(true); + + + // FFmpeg needs to know what file number to start at in the sequence. In case the image sequence doesn't + // start at a zero, we'll loop decreasing the number until it doesn't exist anymore + QString test_filename_format = QString("%1%2%3").arg(file.left(digit_test), "%1", file.mid(lastcharindex)); + int test_file_number = file_number; + do { + test_file_number--; + } while (QFileInfo::exists(test_filename_format.arg(QString("%1").arg(test_file_number, digit_count, 10, QChar('0'))))); + + // set the image sequence's start number to the last that existed + start_number = test_file_number + 1; + + } else { + + // Cache the user's response to the image sequence question - i.e. none of the files imported with this + // formatting should be imported as an image sequence + image_sequence_importassequence.append(false); + + } + } + + } + + } + + // If we're not skipping this file, let's import it + if (!skip) { + MediaPtr item; + FootagePtr m; + + if (replace != nullptr) { + item = replace; + } else { + item = std::make_shared(); + } + + m = std::make_shared(); + + // Edge case for PNGs that standardized unassociated alpha + if (file.endsWith("png", Qt::CaseInsensitive)) { + m->alpha_is_associated = false; + } + + m->using_inout = false; + m->url = file; + m->name = QFileInfo(files.at(i)).fileName(); + m->start_number = start_number; + + item->set_footage(m); + + last_imported_media.append(item.get()); + + if (replace == nullptr) { + if (create_undo_action) { + ca->append(new AddMediaCommand(item, parent)); + } else { + appendChild(parent, item); + } + } + + imported = true; + } + + } + + + } + } + if (create_undo_action) { + if (imported) { + olive::UndoStack.push(ca); + + for (int i=0;i #include "project/media.h" +#include "undo/comboaction.h" class ProjectModel : public QAbstractItemModel { @@ -32,6 +33,42 @@ public: ProjectModel(QObject* parent = nullptr); ~ProjectModel() override; + /** + * @brief Makes preparations for saving the project file. + * + * Some items require IDs to link between them (e.g. the Footage or Nested Sequences used by Clips, linked clips, + * etc). This function sets up those IDs before saving. + * + * NOTE: This function should **always** be called before Save(). + * + * @return + * + * A count of the elements in the project to save into the project file. The loading system can later use this value + * to determine the load progress. + */ + int PrepareToSave(); + + /** + * @brief Initiate a save of all the project data + * + * Recursively goes through the entire project tree saving everything to a specified QXmlStreamWriter object. + * + * It's not recommended to use this function directly as it expects an existing QXmlStreamWriter and doesn't write a + * header and footer for the resulting XML document. Instead use olive::Save(). + * + * NOTE: **Always** call PrepareToSave() just before calling this function to set up valid IDs for saving. + * + * @param stream + * + * A QXmlStreamWriter object. + * + * @param root + * + * Used for recursion, set to any child and called again whenever a child is found with children. If this is nullptr, + * this function will loop over the root item. + */ + void Save(QXmlStreamWriter& stream, Media *root = nullptr); + void make_root(); void destroy_root(); void clear(); @@ -58,15 +95,34 @@ public: int childCount(Media* parent = nullptr); void set_icon(Media* m, const QIcon &ico); + void process_file_list(QStringList& files, bool recursive = false, MediaPtr replace = nullptr, Media *parent = nullptr); + + /** + * @brief Get a list of the last imported media + * + * @return + * + * Returns a list of all the Media processed by the last call to process_file_list(). + */ + QVector GetLastImportedMedia(); + QVector GetAllSequences(); QVector GetAllFootage(); QVector GetAllFolders(); + QString GetNextSequenceName(QString prepend = QString()); + + MediaPtr CreateSequence(ComboAction *ca, SequencePtr s, bool open, Media* parent); + private: MediaPtr root_item_; + QVector last_imported_media; + QVector GetAllMediaOfType(int search_type); void RecurseTree(Media* parent, QVector &list, int search_type); + + void PrepareToSaveInternal(int& element_count, Media* root); }; namespace olive { diff --git a/project/savethread.cpp b/project/savethread.cpp new file mode 100644 index 000000000..8fcca1753 --- /dev/null +++ b/project/savethread.cpp @@ -0,0 +1,97 @@ +#include "savethread.h" + +#include +#include +#include +#include + +#include "global/global.h" +#include "global/config.h" +#include "projectmodel.h" + +void RecursiveSave() { + +} + +void Project::save_folder(QXmlStreamWriter& stream, int type, bool set_ids_only, const QModelIndex& parent) { + for (int i=0;iget_type()) { + if (m->get_type() == MEDIA_TYPE_FOLDER) { + if (set_ids_only) { + m->temp_id = folder_id; // saves a temporary ID for matching in the project file + folder_id++; + } else { + // if we're saving folders, save the folder + stream.writeStartElement("folder"); + stream.writeAttribute("name", m->get_name()); + stream.writeAttribute("id", QString::number(m->temp_id)); + if (!item.parent().isValid()) { + stream.writeAttribute("parent", "0"); + } else { + stream.writeAttribute("parent", QString::number(olive::project_model.getItem(item.parent())->temp_id)); + } + stream.writeEndElement(); + } + // save_folder(stream, item, type, set_ids_only); + } else { + int folder = m->parentItem()->temp_id; + if (type == MEDIA_TYPE_FOOTAGE) { + + } else if (type == MEDIA_TYPE_SEQUENCE) { + Sequence* s = m->to_sequence().get(); + if (set_ids_only) { + s->save_id = sequence_id; + sequence_id++; + } else { + s->Save(stream); + } + } + } + } + + if (m->get_type() == MEDIA_TYPE_FOLDER) { + save_folder(stream, type, set_ids_only, item); + } + } +} + +void olive::Save(bool autorecovery) +{ + QFile file(autorecovery ? olive::Global->get_autorecovery_filename() : olive::ActiveProjectFilename); + if (!file.open(QIODevice::WriteOnly)) { + qCritical() << "Could not open file"; + return; + } + + QXmlStreamWriter stream(&file); + stream.setAutoFormatting(true); + stream.writeStartDocument(); // doc + + stream.writeStartElement("project"); // project + + stream.writeTextElement("version", QString::number(olive::kSaveVersion)); + + stream.writeTextElement("url", olive::ActiveProjectFilename); + + // Prepare project for saving and retrieve element count + int element_count = olive::project_model.PrepareToSave(); + + // Write element count to file - used by the loading thread to determine its loading progress + stream.writeTextElement("elements", QString::number(element_count)); + + olive::project_model.Save(stream); + + stream.writeEndElement(); // project + + stream.writeEndDocument(); // doc + + file.close(); + + if (!autorecovery) { + add_recent_project(olive::ActiveProjectFilename); + olive::Global->set_modified(false); + } +} diff --git a/project/savethread.h b/project/savethread.h new file mode 100644 index 000000000..0687defd5 --- /dev/null +++ b/project/savethread.h @@ -0,0 +1,8 @@ +#ifndef SAVETHREAD_H +#define SAVETHREAD_H + +namespace olive { +void Save(bool autorecovery); +} + +#endif // SAVETHREAD_H diff --git a/project/sourcescommon.cpp b/project/sourcescommon.cpp index d9967ecbb..f140cf764 100644 --- a/project/sourcescommon.cpp +++ b/project/sourcescommon.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include "ui/menuhelper.h" @@ -43,6 +44,7 @@ #include "dialogs/proxydialog.h" #include "ui/viewerwidget.h" #include "project/proxygenerator.h" +#include "project/projectfunctions.h" #include "ui/mainwindow.h" #include "ui/menu.h" #include "undo/undostack.h" @@ -64,13 +66,13 @@ void SourcesCommon::create_seq_from_selected() { } ComboAction* ca = new ComboAction(); - SequencePtr s = create_sequence_from_media(media_list); + SequencePtr s = olive::project::CreateSequenceFromMedia(media_list); // add clips to it panel_timeline->create_ghosts_from_media(s.get(), 0, media_list); panel_timeline->add_clips_from_ghosts(ca, s.get()); - project_parent->create_sequence_internal(ca, s, true, nullptr); + olive::project_model.CreateSequence(ca, s, true, nullptr); olive::UndoStack.push(ca); } } @@ -239,6 +241,21 @@ void SourcesCommon::show_context_menu(QWidget* parent, const QModelIndexList& it menu.exec(QCursor::pos()); } +void SourcesCommon::replace_media(MediaPtr item, QString filename) +{ + if (filename.isEmpty()) { + filename = QFileDialog::getOpenFileName( + this, + tr("Replace '%1'").arg(item->get_name()), + "", + tr("All Files") + " (*)"); + } + if (!filename.isEmpty()) { + ReplaceMediaCommand* rmc = new ReplaceMediaCommand(item, filename); + olive::UndoStack.push(rmc); + } +} + void SourcesCommon::mousePressEvent(QMouseEvent *) { stop_rename_timer(); } @@ -255,7 +272,7 @@ void SourcesCommon::item_click(Media *m, const QModelIndex& index) { void SourcesCommon::mouseDoubleClickEvent(const QModelIndexList& selected_items) { stop_rename_timer(); if (selected_items.size() == 0) { - project_parent->import_dialog(); + olive::Global->open_import_dialog(); } else if (selected_items.size() == 1) { Media* media = project_parent->item_to_media(selected_items.at(0)); if (media->get_type() == MEDIA_TYPE_SEQUENCE) { @@ -292,7 +309,7 @@ void SourcesCommon::dropEvent(QWidget* parent, tr("You dropped a file onto '%1'. Would you like to replace it with the dropped file?").arg(m->get_name()), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { replace = true; - project_parent->replace_media(m, paths.at(0)); + replace_media(m, paths.at(0)); } if (!replace) { QModelIndex parent; @@ -303,7 +320,7 @@ void SourcesCommon::dropEvent(QWidget* parent, parent = drop_item.parent(); } } - project_parent->process_file_list(paths, false, nullptr, panel_project->item_to_media(parent)); + olive::project_model.process_file_list(paths, false, nullptr, panel_project->item_to_media(parent)); } } event->acceptProposedAction(); diff --git a/project/sourcescommon.h b/project/sourcescommon.h index 4b00071ed..b74b1165b 100644 --- a/project/sourcescommon.h +++ b/project/sourcescommon.h @@ -27,10 +27,10 @@ #include "project/footage.h" #include "project/projectfilter.h" +#include "media.h" class Project; class QMouseEvent; -class Media; class QAbstractItemView; class QDropEvent; @@ -41,6 +41,8 @@ public: QAbstractItemView* view; void show_context_menu(QWidget* parent, const QModelIndexList &items); + void replace_media(MediaPtr item, QString filename); + void mousePressEvent(QMouseEvent* e); void mouseDoubleClickEvent(const QModelIndexList& selected_items); void dropEvent(QWidget *parent, QDropEvent* e, const QModelIndex& drop_item, const QModelIndexList &items); diff --git a/ui/mainwindow.cpp b/ui/mainwindow.cpp index 660633e07..298750e98 100644 --- a/ui/mainwindow.cpp +++ b/ui/mainwindow.cpp @@ -146,7 +146,7 @@ void MainWindow::setup_layout(bool reset) { removeDockWidget(olive::panels.at(i)); } - addDockWidget(Qt::TopDockWidgetArea, panel_project); + addDockWidget(Qt::TopDockWidgetArea, panel_project.first()); addDockWidget(Qt::TopDockWidgetArea, panel_graph_editor); addDockWidget(Qt::TopDockWidgetArea, panel_footage_viewer); tabifyDockWidget(panel_footage_viewer, panel_effect_controls); @@ -154,25 +154,25 @@ void MainWindow::setup_layout(bool reset) { addDockWidget(Qt::TopDockWidgetArea, panel_sequence_viewer); addDockWidget(Qt::BottomDockWidgetArea, panel_timeline); - panel_project->show(); + panel_project.first()->show(); panel_effect_controls->show(); panel_footage_viewer->show(); panel_sequence_viewer->show(); panel_timeline->show(); panel_graph_editor->hide(); - panel_project->setFloating(false); + panel_project.first()->setFloating(false); panel_effect_controls->setFloating(false); panel_footage_viewer->setFloating(false); panel_sequence_viewer->setFloating(false); panel_timeline->setFloating(false); panel_graph_editor->setFloating(true); - resizeDocks({panel_project, panel_footage_viewer, panel_sequence_viewer}, + resizeDocks({panel_project.first(), panel_footage_viewer, panel_sequence_viewer}, {width()/3, width()/3, width()/3}, Qt::Horizontal); - resizeDocks({panel_project, panel_timeline}, + resizeDocks({panel_project.first(), panel_timeline}, {height()/2, height()/2}, Qt::Vertical); } @@ -242,19 +242,7 @@ MainWindow::MainWindow(QWidget *parent) : } // search for open recents list - QFile f(olive::Global->get_recent_project_list_file()); - if (f.exists() && f.open(QFile::ReadOnly | QFile::Text)) { - QTextStream text_stream(&f); - while (true) { - QString line = text_stream.readLine(); - if (line.isNull()) { - break; - } else { - recent_projects.append(line); - } - } - f.close(); - } + olive::Global->load_recent_projects(); } } QString config_path = get_config_path(); @@ -518,7 +506,7 @@ void MainWindow::setup_menus() { open_recent = MenuHelper::create_submenu(file_menu); - clear_open_recent_action = MenuHelper::create_menu_action(nullptr, "clearopenrecent", panel_project, SLOT(clear_recent_projects())); + clear_open_recent_action = MenuHelper::create_menu_action(nullptr, "clearopenrecent", olive::Global.get(), SLOT(clear_recent_projects())); save_project = MenuHelper::create_menu_action(file_menu, "saveproj", olive::Global.get(), SLOT(save_project()), QKeySequence("Ctrl+S")); @@ -526,7 +514,7 @@ void MainWindow::setup_menus() { file_menu->addSeparator(); - import_action = MenuHelper::create_menu_action(file_menu, "import", panel_project, SLOT(import_dialog()), QKeySequence("Ctrl+I")); + import_action = MenuHelper::create_menu_action(file_menu, "import", olive::Global.get(), SLOT(open_import_dialog()), QKeySequence("Ctrl+I")); file_menu->addSeparator(); @@ -978,9 +966,12 @@ void MainWindow::closeEvent(QCloseEvent *e) { QString data_dir = get_data_path(); QString config_path = get_config_path(); + + const QString& autorecovery_filename = olive::Global->get_autorecovery_filename(); if (!data_dir.isEmpty() && !autorecovery_filename.isEmpty()) { if (QFile::exists(autorecovery_filename)) { - QFile::rename(autorecovery_filename, autorecovery_filename + "." + QDateTime::currentDateTimeUtc().toString("yyyyMMddHHmmss")); + QFile::rename(autorecovery_filename, + autorecovery_filename + "." + QDateTime::currentDateTimeUtc().toString("yyyyMMddHHmmss")); } } if (!config_path.isEmpty()) { @@ -1208,11 +1199,11 @@ void MainWindow::set_panels_locked(bool locked) } void MainWindow::fileMenu_About_To_Be_Shown() { - if (recent_projects.size() > 0) { + if (olive::Global->recent_project_count() > 0) { open_recent->clear(); open_recent->setEnabled(true); - for (int i=0;iaddAction(recent_projects.at(i)); + for (int i=0;irecent_project_count();i++) { + QAction* action = open_recent->addAction(olive::Global->recent_project(i)); action->setProperty("keyignore", true); action->setData(i); connect(action, SIGNAL(triggered()), &olive::MenuHelper, SLOT(open_recent_from_menu())); diff --git a/ui/panel.cpp b/ui/panel.cpp index ce74c1437..27d80afdf 100644 --- a/ui/panel.cpp +++ b/ui/panel.cpp @@ -26,9 +26,6 @@ QVector olive::panels; Panel::Panel(QWidget *parent) : QDockWidget (parent) { -// setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); -// setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - olive::panels.append(this); } @@ -37,6 +34,11 @@ Panel::~Panel() olive::panels.removeAll(this); } +bool Panel::focused() +{ + return hasFocus(); +} + void Panel::LoadLayoutState(const QByteArray &) {} QByteArray Panel::SaveLayoutState() diff --git a/ui/panel.h b/ui/panel.h index b0141cec4..4c97ee178 100644 --- a/ui/panel.h +++ b/ui/panel.h @@ -31,6 +31,8 @@ public: virtual void Retranslate() = 0; + virtual bool focused(); + virtual void LoadLayoutState(const QByteArray& data); virtual QByteArray SaveLayoutState(); protected: