diff --git a/app/core.cpp b/app/core.cpp index 468c1a7ed..d117313b6 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -32,11 +32,13 @@ #include #include "oakengine/audio.h" #include "oakengine/disk.h" +#include "oakengine/footage.h" #include "oakengine/plugin.h" #include "oakengine/project.h" #include "oakengine/task.h" #include "oakengine/node.h" #include "oakengine/undo.h" +#include "oakengine/viewer.h" #include "window/mainwindow/mainwindowundo.h" #ifdef Q_OS_WINDOWS #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) @@ -140,10 +142,10 @@ Core::Core(const OakEngineAppParams *params) #ifdef USE_OTIO cb.otio_import = [](OakEngineSequence **sequences, int count, void *userdata) -> int { - QList sq; + QList sq; sq.reserve(count); for (int i = 0; i < count; i++) { - sq.append(reinterpret_cast(sequences[i])); + sq.append(sequences[i]); } return static_cast(userdata)->DialogImportOTIOShow(sq) ? 1 : 0; @@ -262,7 +264,7 @@ MainWindow *Core::main_window() return main_window_; } -void Core::import_files(const QStringList &urls, Folder *parent) +void Core::import_files(const QStringList &urls, OakEngineNode *parent) { if (urls.isEmpty()) { QMessageBox::critical(main_window_, tr("Import error"), @@ -304,7 +306,7 @@ void Core::import_files(const QStringList &urls, Folder *parent) } OakEngineTask *pim = oakengine_task_create_project_import( - reinterpret_cast(parent), + parent, url_ptrs.data(), url_ptrs.size()); if (oakengine_task_import_file_count(pim) == 0) { @@ -339,11 +341,11 @@ void Core::dialog_import_show() // Locate the most recently focused Project panel (assume that's the panel the user wants to import into) ProjectPanel *active_project_panel = PanelManager::instance()->most_recently_focused(); - Project *active_project; + OakEngineProject *active_project; if (active_project_panel == nullptr // Check that we found a Project panel - || (active_project = active_project_panel->project()) == + || (active_project = reinterpret_cast(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")); @@ -351,7 +353,7 @@ void Core::dialog_import_show() } // Get the selected folder in this panel - Folder *folder = active_project_panel->get_selected_folder(); + OakEngineNode *folder = reinterpret_cast(active_project_panel->get_selected_folder()); import_files(files, folder); } @@ -365,10 +367,10 @@ void Core::dialog_preferences_show(int start_tab) void Core::dialog_project_properties_show() { - Project *proj = get_active_project(); + OakEngineProject *proj = get_active_project(); if (proj) { - ProjectPropertiesDialog ppd(proj, main_window_); + ProjectPropertiesDialog ppd(reinterpret_cast(proj), main_window_); ppd.exec(); } else { QMessageBox::critical( @@ -380,16 +382,21 @@ void Core::dialog_project_properties_show() void Core::dialog_export_show() { - if (ViewerOutput *viewer = get_sequence_to_export()) { + if (OakEngineNode *viewer = get_sequence_to_export()) { open_export_dialog_for_viewer(viewer, false); } } #ifdef USE_OTIO -bool Core::DialogImportOTIOShow(const QList &sequences) +bool Core::DialogImportOTIOShow(const QList &sequences) { - Project *active_project = get_active_project(); - OTIOPropertiesDialog opd(sequences, active_project); + OakEngineProject *active_project = get_active_project(); + QList sq; + sq.reserve(sequences.size()); + for (auto *s : sequences) { + sq.append(reinterpret_cast(s)); + } + OTIOPropertiesDialog opd(sq, reinterpret_cast(active_project)); return opd.exec() == QDialog::Accepted; } #endif @@ -399,10 +406,10 @@ void Core::create_new_folder() // Locate the most recently focused Project panel (assume that's the panel the user wants to import into) ProjectPanel *active_project_panel = PanelManager::instance()->most_recently_focused(); - Project *active_project; + OakEngineProject *active_project; if (active_project_panel == nullptr // Check that we found a Project panel - || (active_project = active_project_panel->project()) == + || (active_project = reinterpret_cast(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")); @@ -410,14 +417,14 @@ void Core::create_new_folder() } // Get the selected folder in this panel - Folder *folder = active_project_panel->get_selected_folder(); + OakEngineNode *folder = reinterpret_cast(active_project_panel->get_selected_folder()); // Group the three facade edits into a single undo entry. oakengine_undo_group_begin(tr("Create New Folder").toUtf8().constData()); // Create new folder via facade (creates and adds to project, undoable) OakEngineNode *new_folder_oak = oakengine_project_add_node( - reinterpret_cast(active_project), + active_project, "org.olivevideoeditor.Olive.folder"); // Set a default name (undoable) @@ -426,7 +433,7 @@ void Core::create_new_folder() // Add to the selected folder (undoable) oakengine_folder_add_child( - reinterpret_cast(folder), + folder, new_folder_oak); oakengine_undo_group_end(); @@ -437,7 +444,7 @@ void Core::create_new_folder() void Core::create_new_sequence() { - Project *active_project = get_active_project(); + OakEngineProject *active_project = get_active_project(); if (!active_project) { QMessageBox::critical(main_window_, tr("Failed to create new sequence"), @@ -446,9 +453,9 @@ void Core::create_new_sequence() } // Create new sequence - Sequence *new_sequence = create_new_sequence_for_project(active_project); + OakEngineSequence *new_sequence = create_new_sequence_for_project(active_project); - SequenceDialog sd(new_sequence, SequenceDialog::k_new, main_window_); + SequenceDialog sd(reinterpret_cast(new_sequence), SequenceDialog::k_new, 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 @@ -460,24 +467,23 @@ void Core::create_new_sequence() oakengine_undo_command_multi_add_child(command, oakengine_node_add_to_project_command( - reinterpret_cast(active_project), + active_project, reinterpret_cast(new_sequence))); oakengine_folder_add_child( - reinterpret_cast(get_selected_folder_in_active_project()), + get_selected_folder_in_active_project(), reinterpret_cast(new_sequence)); oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(new_sequence), reinterpret_cast(new_sequence), 0.0, 0.0, 0)); - oakengine_undo_command_multi_add_child(command, make_open_sequence_command(new_sequence)); + oakengine_undo_command_multi_add_child(command, make_open_sequence_command(reinterpret_cast(new_sequence))); // Create and connect default nodes to new sequence - oakengine_sequence_add_default_nodes( - reinterpret_cast(new_sequence)); + oakengine_sequence_add_default_nodes(new_sequence); oakengine_undo_push(command, tr("Created New Sequence").toUtf8().constData()); } else { // If the dialog was accepted, ownership goes to the AddItemCommand. But if we get here, just delete - delete new_sequence; + oakengine_node_delete_later(reinterpret_cast(new_sequence)); } } @@ -487,10 +493,10 @@ void Core::import_task_complete(OakEngineTask *task) oakengine_task_import_get_command(task)); int footage_count = oakengine_task_import_footage_count(task); - QVector imported_footage; + QVector imported_footage; imported_footage.reserve(footage_count); for (int i = 0; i < footage_count; i++) { - Footage *f = reinterpret_cast( + OakEngineFootage *f = reinterpret_cast( oakengine_task_import_footage_at(task, i)); imported_footage.append(f); @@ -502,15 +508,17 @@ void Core::import_task_complete(OakEngineTask *task) if (aud_count == 0 && vid_count > 1) { bool all_stills = true; - for (int i = 0; i < vid_count; i++) { - const VideoParams &vs = viewer_output_video_params(f, i); + for (int j = 0; j < vid_count; j++) { + const VideoParams &vs = viewer_output_video_params(f, j); if (!(vs.video_type() == 1 && - vs.enabled() == (i == 0))) { + vs.enabled() == (j == 0))) { all_stills = false; } } if (all_stills) { + char fn_buf[512]; + oakengine_footage_get_filename(f, fn_buf, sizeof(fn_buf)); QMessageBox d(main_window()); d.setIcon(QMessageBox::Question); @@ -518,7 +526,7 @@ void Core::import_task_complete(OakEngineTask *task) 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())); + .arg(QString::fromUtf8(fn_buf))); auto multi_btn = d.addButton(tr("Multiple Layers"), QMessageBox::YesRole); @@ -531,11 +539,11 @@ void Core::import_task_complete(OakEngineTask *task) if (d.clickedButton() == multi_btn) { OakEngineFootage *fh = oakengine_footage_borrow( reinterpret_cast(f)); - for (int i = 0; i < vid_count; i++) { + for (int j = 0; j < vid_count; j++) { int enabled = oakengine_footage_get_stream_enabled( - fh, OAKENGINE_TRACK_TYPE_VIDEO, i); + fh, OAKENGINE_TRACK_TYPE_VIDEO, j); oakengine_footage_set_stream_enabled( - fh, OAKENGINE_TRACK_TYPE_VIDEO, i, + fh, OAKENGINE_TRACK_TYPE_VIDEO, j, enabled ? 0 : 1); } oakengine_footage_free(fh); @@ -571,7 +579,7 @@ void Core::import_task_complete(OakEngineTask *task) command, tr("Imported %1 File(s)").arg(imported_footage.size()).toUtf8().constData()); - main_window_->select_footage(imported_footage); + main_window_->select_footage(reinterpret_cast &>(imported_footage)); } bool Core::confirm_image_sequence(const QString &filename) @@ -785,13 +793,11 @@ void Core::start_gui(bool full_screen) void Core::save_project_internal(const QString &override_filename) { - Project *open_proj_ = reinterpret_cast(oakengine_app_open_project()); + OakEngineProject *open_proj_ = oakengine_app_open_project(); // Get project filename via facade char fn_buf[512]; - oakengine_project_filename( - reinterpret_cast(open_proj_), - fn_buf, sizeof(fn_buf)); + oakengine_project_filename(open_proj_, fn_buf, sizeof(fn_buf)); QString fn = QString::fromUtf8(fn_buf); // Create save manager @@ -800,8 +806,7 @@ void Core::save_project_internal(const QString &override_filename) if (fn.endsWith(QStringLiteral(".otio"), Qt::CaseInsensitive)) { #ifdef USE_OTIO - psm = oakengine_task_create_project_save_otio( - reinterpret_cast(open_proj_)); + psm = oakengine_task_create_project_save_otio(open_proj_); #else QMessageBox::critical( main_window_, tr("Missing OpenTimelineIO Libraries"), @@ -814,7 +819,7 @@ void Core::save_project_internal(const QString &override_filename) QStringLiteral(".ovexml"), Qt::CaseInsensitive); SerializedLayoutInfo layout = main_window_->save_layout(); psm = oakengine_task_create_project_save( - reinterpret_cast(open_proj_), + open_proj_, use_compression ? 1 : 0, override_filename.isEmpty() ? nullptr : override_filename.toUtf8().constData(), @@ -839,7 +844,7 @@ void Core::save_project_internal(const QString &override_filename) oakengine_task_free(psm); } -ViewerOutput *Core::get_sequence_to_export() +OakEngineNode *Core::get_sequence_to_export() { // First try the most recently focused time based window TimeBasedPanel *time_panel = @@ -853,13 +858,16 @@ ViewerOutput *Core::get_sequence_to_export() } if (time_panel && time_panel->get_connected_viewer()) { - if (time_panel->get_connected_viewer()->get_length() == 0) { + OakEngineNode *viewer = reinterpret_cast(time_panel->get_connected_viewer()); + int64_t len_num = 0, len_den = 1; + oakengine_viewer_get_length(viewer, &len_num, &len_den); + if (len_num == 0) { QMessageBox::critical( main_window_, tr("Error"), tr("This Sequence is empty. There is nothing to export."), QMessageBox::Ok); } else { - return time_panel->get_connected_viewer(); + return viewer; } } else { QMessageBox::critical( @@ -873,16 +881,13 @@ ViewerOutput *Core::get_sequence_to_export() bool Core::revert_project_internal(bool by_opening_existing) { - Project *cur_proj = reinterpret_cast(oakengine_app_open_project()); + OakEngineProject *cur_proj = oakengine_app_open_project(); char fn_buf[512]; - oakengine_project_filename( - reinterpret_cast(cur_proj), - fn_buf, sizeof(fn_buf)); + oakengine_project_filename(cur_proj, fn_buf, sizeof(fn_buf)); QString cur_fn = QString::fromUtf8(fn_buf); char name_buf[256]; - oakengine_project_name(reinterpret_cast(cur_proj), - name_buf, sizeof(name_buf)); + oakengine_project_name(cur_proj, name_buf, sizeof(name_buf)); QString cur_name = QString::fromUtf8(name_buf); if (cur_fn.isEmpty()) { @@ -927,29 +932,28 @@ bool Core::revert_project_internal(bool by_opening_existing) void Core::project_save_succeeded(OakEngineTask *task) { - Project *p = reinterpret_cast( + OakEngineProject *p = reinterpret_cast( oakengine_task_save_get_project(task)); - oakengine_app_on_project_saved(reinterpret_cast(p)); + oakengine_app_on_project_saved(p); char fn_buf[512]; - oakengine_project_filename(reinterpret_cast(p), - fn_buf, sizeof(fn_buf)); + oakengine_project_filename(p, fn_buf, sizeof(fn_buf)); show_status_bar_message(tr("Saved to \"%1\" successfully").arg(fn_buf)); } -Project *Core::get_active_project() const +OakEngineProject *Core::get_active_project() const { - return reinterpret_cast(oakengine_app_open_project()); + return oakengine_app_open_project(); } -Folder *Core::get_selected_folder_in_active_project() const +OakEngineNode *Core::get_selected_folder_in_active_project() const { ProjectPanel *active_project_panel = PanelManager::instance()->most_recently_focused(); if (active_project_panel) { - return active_project_panel->get_selected_folder(); + return reinterpret_cast(active_project_panel->get_selected_folder()); } else { return nullptr; } @@ -991,12 +995,10 @@ QString Core::get_project_filter(bool include_any_filter) bool Core::save_project() { - Project *saved_proj = reinterpret_cast(oakengine_app_open_project()); + OakEngineProject *saved_proj = oakengine_app_open_project(); char fn_buf[512]; - oakengine_project_filename( - reinterpret_cast(saved_proj), - fn_buf, sizeof(fn_buf)); + oakengine_project_filename(saved_proj, fn_buf, sizeof(fn_buf)); if (fn_buf[0] == '\0') { return save_project_as(); } else { @@ -1010,16 +1012,16 @@ void Core::open_recovery_project(const QString &filename) open_project_internal(filename, true); } -void Core::open_node_in_viewer(ViewerOutput *viewer) +void Core::open_node_in_viewer(OakEngineNode *viewer) { - main_window_->open_node_in_viewer(viewer); + main_window_->open_node_in_viewer(reinterpret_cast(viewer)); } -void Core::open_export_dialog_for_viewer(ViewerOutput *viewer, +void Core::open_export_dialog_for_viewer(OakEngineNode *viewer, bool start_still_image) { ExportDialog *ed = - new ExportDialog(viewer, start_still_image, main_window_); + new ExportDialog(reinterpret_cast(viewer), start_still_image, main_window_); connect(ed, &ExportDialog::finished, ed, &ExportDialog::deleteLater); ed->open(); connect(ed, &ExportDialog::request_import_file, this, @@ -1089,15 +1091,14 @@ void Core::show_cache_full_warning() "3. Reduce usage of the disk cache (e.g. disable auto-cache or only cache specific sections of your sequence).")); } -void Core::on_active_project_changed(Project *p) +void Core::on_active_project_changed(OakEngineProject *p) { - main_window_->set_project(p); + main_window_->set_project(reinterpret_cast(p)); if (p) { - auto *ph = reinterpret_cast(p); // Keep the window's modified state in sync via event subscription // (connection is removed automatically when the project is deleted). - oakengine_event_subscribe(ph, OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED, + oakengine_event_subscribe(p, OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED, [](const oakengine_event *event, void *userdata) { QMainWindow *mw = static_cast(userdata); mw->setWindowModified(event->a != 0); @@ -1125,7 +1126,7 @@ bool Core::save_project_as() fn = FileFunctions::ensure_filename_extension(fn, extension); oakengine_project_set_filename( - reinterpret_cast(static_cast(reinterpret_cast(oakengine_app_open_project()))), + oakengine_app_open_project(), fn.toUtf8().constData()); save_project_internal(); @@ -1143,12 +1144,10 @@ void Core::revert_project() void Core::open_project_internal(const QString &filename, bool recovery_project) { - Project *open_proj = reinterpret_cast(oakengine_app_open_project()); + OakEngineProject *open_proj = oakengine_app_open_project(); if (open_proj) { char fn_buf[512]; - oakengine_project_filename( - reinterpret_cast(open_proj), - fn_buf, sizeof(fn_buf)); + oakengine_project_filename(open_proj, fn_buf, sizeof(fn_buf)); // Comparing QFileInfos will handle case insensitivity and both slash directions on platforms // where this is necessary (not naming any names *cough* Windows) if (QFileInfo(fn_buf) == QFileInfo(filename)) { @@ -1201,12 +1200,12 @@ void Core::open_project_internal(const QString &filename, bool recovery_project) void Core::import_single_file(const QString &f) { - if (Project *p = get_active_project()) { - import_files({ f }, p->root()); + if (OakEngineProject *p = get_active_project()) { + import_files({ f }, oakengine_project_root(p)); } } -bool Core::label_nodes(const QVector &nodes, void *parent) +bool Core::label_nodes(const QVector &nodes, void *parent) { if (nodes.isEmpty()) { return false; @@ -1214,10 +1213,14 @@ bool Core::label_nodes(const QVector &nodes, void *parent) bool ok; - QString start_label = nodes.first()->get_label(); + char label_buf[256]; + oakengine_node_get_label(nodes.first(), label_buf, sizeof(label_buf)); + QString start_label = QString::fromUtf8(label_buf); for (int i = 1; i < nodes.size(); i++) { - if (nodes.at(i)->get_label() != start_label) { + char buf_i[256]; + oakengine_node_get_label(nodes.at(i), buf_i, sizeof(buf_i)); + if (QString::fromUtf8(buf_i) != start_label) { // Not all the nodes share the same name, so we'll start with a blank one start_label.clear(); break; @@ -1229,12 +1232,8 @@ bool Core::label_nodes(const QVector &nodes, void *parent) start_label, &ok); if (ok) { - QVector oak_nodes; - oak_nodes.reserve(nodes.size()); - foreach (Node *n, nodes) { - oak_nodes.append(reinterpret_cast(n)); - } - oakengine_node_rename_many(oak_nodes.data(), oak_nodes.size(), + oakengine_node_rename_many(const_cast(nodes.data()), + nodes.size(), s.toUtf8().constData(), parent); return true; @@ -1265,13 +1264,11 @@ void Core::open_project_from_recent_list(int index) bool Core::close_project(bool auto_open_new, bool ignore_modified) { - Project *close_proj = reinterpret_cast(oakengine_app_open_project()); + OakEngineProject *close_proj = oakengine_app_open_project(); if (close_proj) { char name_buf[256]; - oakengine_project_name( - reinterpret_cast(close_proj), - name_buf, sizeof(name_buf)); - if (close_proj->is_modified() && !ignore_modified) { + oakengine_project_name(close_proj, name_buf, sizeof(name_buf)); + if (oakengine_project_is_modified(close_proj) && !ignore_modified) { QMessageBox mb(main_window_); mb.setWindowModality(Qt::WindowModal); @@ -1304,9 +1301,9 @@ bool Core::close_project(bool auto_open_new, bool ignore_modified) // For safety, the undo stack is cleared so no commands try to affect a freed project oakengine_undo_clear(); - Project *tmp = reinterpret_cast(oakengine_app_open_project()); + OakEngineProject *tmp = oakengine_app_open_project(); oakengine_app_set_active_project_vp(nullptr); - delete tmp; + oakengine_project_free(tmp); } // Ensure a project is always active @@ -1436,16 +1433,14 @@ void Core::create_new_project() oakengine_app_create_new_project(); } -Sequence *Core::create_new_sequence_for_project(const QString &format, - Project *project) +OakEngineSequence *Core::create_new_sequence_for_project(const QString &format, + OakEngineProject *project) { - return reinterpret_cast( - oakengine_app_create_sequence( - reinterpret_cast(project), - format.toUtf8().constData())); + return oakengine_app_create_sequence( + project, format.toUtf8().constData()); } -Sequence *Core::create_new_sequence_for_project(Project *project) +OakEngineSequence *Core::create_new_sequence_for_project(OakEngineProject *project) { return instance()->create_new_sequence_for_project(QStringLiteral("Sequence %1"), project); } @@ -1531,9 +1526,9 @@ void Core::set_autorecovery_interval(int minutes) oakengine_app_set_autorecovery_interval(minutes); } -void Core::on_project_saved(Project *p) +void Core::on_project_saved(OakEngineProject *p) { - oakengine_app_on_project_saved(reinterpret_cast(p)); + oakengine_app_on_project_saved(p); } QString Core::get_auto_recovery_index_filename() @@ -1545,10 +1540,9 @@ QString Core::get_auto_recovery_index_filename() return QString::fromUtf8(buf.constData()); } -void Core::add_open_project(olive::Project *p, bool add_to_recents) +void Core::add_open_project(OakEngineProject *p, bool add_to_recents) { - oakengine_app_add_open_project(reinterpret_cast(p), - add_to_recents ? 1 : 0); + oakengine_app_add_open_project(p, add_to_recents ? 1 : 0); } void Core::remove_recently_opened_project(int index) @@ -1556,9 +1550,9 @@ void Core::remove_recently_opened_project(int index) oakengine_app_remove_recently_opened_project(index); } -void Core::set_active_project(Project *p) +void Core::set_active_project(OakEngineProject *p) { - oakengine_app_set_active_project(reinterpret_cast(p)); + oakengine_app_set_active_project(p); } QString Core::get_selected_transition() const diff --git a/app/core.h b/app/core.h index deade3a1c..1e8ef262b 100644 --- a/app/core.h +++ b/app/core.h @@ -25,6 +25,9 @@ #include "coreengine.h" #include #include "oakengine/app.h" +#include "oakengine/node.h" +#include "oakengine/project.h" +#include "oakengine/timeline.h" #include "oakengine/undo.h" #include "oakengine/init.h" #include "oakengine/task.h" @@ -104,7 +107,7 @@ public: * * @param urls */ - void import_files(const QStringList &urls, Folder *parent); + void import_files(const QStringList &urls, OakEngineNode *parent); /** * @brief Get the currently active project @@ -116,13 +119,13 @@ public: * * The active Project file, or nullptr if the heuristic couldn't find one. */ - Project *get_active_project() const; - Folder *get_selected_folder_in_active_project() const; + OakEngineProject *get_active_project() const; + OakEngineNode *get_selected_folder_in_active_project() const; /** * @brief Show a dialog to the user to rename a set of nodes */ - bool label_nodes(const QVector &nodes, + bool label_nodes(const QVector &nodes, void *parent = nullptr); /** @@ -142,9 +145,9 @@ public: void open_recovery_project(const QString &filename); - void open_node_in_viewer(ViewerOutput *viewer); + void open_node_in_viewer(OakEngineNode *viewer); - void open_export_dialog_for_viewer(ViewerOutput *viewer, + void open_export_dialog_for_viewer(OakEngineNode *viewer, bool start_still_image); bool add_open_project_from_task(OakEngineTask *task, bool add_to_recents); @@ -220,7 +223,7 @@ public: * @brief Show OTIO import dialog */ #ifdef USE_OTIO - bool DialogImportOTIOShow(const QList &sequences); + bool DialogImportOTIOShow(const QList &sequences); #endif // ---- Facade-wrapping methods (shadow EngineCore to avoid symbol refs) ---- @@ -243,9 +246,9 @@ public: static bool is_footage_extension_allowed(const QString &path); void create_new_project(); - Sequence *create_new_sequence_for_project(const QString &format, - Project *project); - static Sequence *create_new_sequence_for_project(Project *project); + OakEngineSequence *create_new_sequence_for_project(const QString &format, + OakEngineProject *project); + static OakEngineSequence *create_new_sequence_for_project(OakEngineProject *project); void clear_open_recent_list(); void set_use_proxy_media(bool enabled); @@ -269,11 +272,11 @@ public: bool set_language(const QString &locale); void set_autorecovery_interval(int minutes); - void on_project_saved(Project *p); + void on_project_saved(OakEngineProject *p); static QString get_auto_recovery_index_filename(); - void add_open_project(olive::Project *p, bool add_to_recents = false); + void add_open_project(OakEngineProject *p, bool add_to_recents = false); void remove_recently_opened_project(int index); - void set_active_project(Project *p); + void set_active_project(OakEngineProject *p); QString get_selected_transition() const; signals: @@ -308,7 +311,7 @@ private: /** * @brief Retrieves the currently most active sequence for exporting */ - ViewerOutput *get_sequence_to_export(); + OakEngineNode *get_sequence_to_export(); bool revert_project_internal(bool by_opening_existing); @@ -320,7 +323,7 @@ private: /** * @brief Applies a new active project to the main window (connected to EngineCore::active_project_changed) */ - void on_active_project_changed(Project *p); + void on_active_project_changed(OakEngineProject *p); /** * @brief Internal main window object diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 2de5d0c87..b975e9afc 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -1120,7 +1120,8 @@ void NodeView::open_selected_node_in_viewer() // Find first viewer in list of selected nodes and open it foreach (Node *n, selected_nodes_) { if (ViewerOutput *viewer = dynamic_cast(n)) { - Core::instance()->open_node_in_viewer(viewer); + Core::instance()->open_node_in_viewer( + reinterpret_cast(viewer)); break; } } @@ -1734,7 +1735,9 @@ void NodeView::group_nodes() oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(group), reinterpret_cast(context), avg_pos.x(), avg_pos.y(), 0)); // Do command - Core::instance()->label_nodes({ group }, command); + Core::instance()->label_nodes( + QVector{ reinterpret_cast(group) }, + command); oakengine_undo_push(command, tr("Grouped Nodes").toUtf8().constData()); } @@ -1877,7 +1880,8 @@ void NodeView::show_selected_node_in_param_editor() void NodeView::label_selected_nodes() { - Core::instance()->label_nodes(selected_nodes_); + Core::instance()->label_nodes( + reinterpret_cast &>(selected_nodes_)); } void NodeView::item_about_to_be_deleted(NodeViewItem *item) diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 52db93a0f..e5584d310 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -537,7 +537,8 @@ void ProjectExplorer::show_item_properties_dialog() fpd.exec(); } else if (dynamic_cast(sel)) { - Core::instance()->label_nodes(context_menu_items_); + Core::instance()->label_nodes( + reinterpret_cast &>(context_menu_items_)); } else if (dynamic_cast(sel)) { SequenceDialog sd(static_cast(sel), diff --git a/app/widget/projectexplorer/projectviewmodel.cpp b/app/widget/projectexplorer/projectviewmodel.cpp index 1ec8f0ffe..f9dc922bb 100644 --- a/app/widget/projectexplorer/projectviewmodel.cpp +++ b/app/widget/projectexplorer/projectviewmodel.cpp @@ -463,7 +463,7 @@ bool ProjectViewModel::dropMimeData(const QMimeData *data, } // Trigger an import - Core::instance()->import_files(urls, static_cast(drop_item)); + Core::instance()->import_files(urls, reinterpret_cast(drop_item)); return true; } diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 402297c95..5c5202fc1 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -1737,8 +1737,10 @@ void TimelineWidget::nest_selected_clips() // Create new sequence Project *project = this->get_connected_node()->project(); - Sequence *nest = - Core::instance()->create_new_sequence_for_project(tr("Nested Sequence %1"), project); + Sequence *nest = reinterpret_cast( + Core::instance()->create_new_sequence_for_project( + tr("Nested Sequence %1"), + reinterpret_cast(project))); { oak_video_params vpod; oakengine_viewer_get_video_params( diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 1faf4db75..ac21b465a 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -409,12 +409,12 @@ void ImportTool::drop_ghosts(bool insert, void *parent_command) } if (behavior != k_dws_disable) { - Project *active_project = Core::instance()->get_active_project(); + OakEngineProject *active_project = Core::instance()->get_active_project(); if (active_project) { - Sequence *new_sequence = + Sequence *new_sequence = reinterpret_cast( Core::instance()->create_new_sequence_for_project( - active_project); + active_project)); oakengine_viewer_set_default_parameters( reinterpret_cast(new_sequence)); @@ -453,7 +453,7 @@ void ImportTool::drop_ghosts(bool insert, void *parent_command) } if (sequence_is_valid) { - dst_graph = Core::instance()->get_active_project(); + dst_graph = reinterpret_cast(Core::instance()->get_active_project()); oakengine_undo_command_multi_add_child(command, oakengine_node_add_to_project_command( diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 1df900650..8dd2365d9 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -854,7 +854,8 @@ void ViewerWidget::request_next_dry_run() void ViewerWidget::save_frame_as_image() { - Core::instance()->open_export_dialog_for_viewer(get_connected_node(), true); + Core::instance()->open_export_dialog_for_viewer( + reinterpret_cast(get_connected_node()), true); } void ViewerWidget::detect_multicam_node_now() diff --git a/app/window/mainwindow/mainmenu.cpp b/app/window/mainwindow/mainmenu.cpp index 7af153675..74447e2ea 100644 --- a/app/window/mainwindow/mainmenu.cpp +++ b/app/window/mainwindow/mainmenu.cpp @@ -435,16 +435,14 @@ void MainMenu::tool_item_triggered() void MainMenu::file_menu_about_to_show() { - Project *active_project = Core::instance()->get_active_project(); + OakEngineProject *active_project = Core::instance()->get_active_project(); file_save_item_->setEnabled(active_project); file_save_as_item_->setEnabled(active_project); if (active_project) { char name_buf[256]; - oakengine_project_name( - reinterpret_cast(active_project), - name_buf, sizeof(name_buf)); + oakengine_project_name(active_project, name_buf, sizeof(name_buf)); file_save_item_->setText(tr("&Save '%1'").arg(name_buf)); file_save_as_item_->setText( tr("Save '%1' &As").arg(name_buf)); diff --git a/engine/include/oakengine/node.h b/engine/include/oakengine/node.h index b902d55e1..061f33009 100644 --- a/engine/include/oakengine/node.h +++ b/engine/include/oakengine/node.h @@ -445,6 +445,15 @@ oakengine_project_add_node(OakEngineProject *project, const char *type_id); OAKENGINE_API int oakengine_project_remove_node(OakEngineProject *project, OakEngineNode *node); +/** + * @brief Schedule a node for deferred deletion (QObject::deleteLater). + * + * Use this to dispose of orphaned nodes that were created but never added + * to a project (e.g. a sequence whose creation dialog was cancelled). + * The handle becomes invalid after the next event-loop iteration. + */ +OAKENGINE_API void oakengine_node_delete_later(OakEngineNode *node); + /** * @brief Connect `output_node`'s output into `input_node`'s `input_id` * (undoable, olive::NodeEdgeAddCommand). diff --git a/engine/src/capi/node.cpp b/engine/src/capi/node.cpp index 99fa20273..4e41d31f5 100644 --- a/engine/src/capi/node.cpp +++ b/engine/src/capi/node.cpp @@ -1286,6 +1286,13 @@ int oakengine_project_remove_node(OakEngineProject *project, return OAKENGINE_OK; } +void oakengine_node_delete_later(OakEngineNode *node) +{ + if (olive::Node *n = impl(node)) { + n->deleteLater(); + } +} + int oakengine_node_connect(OakEngineNode *output_node, OakEngineNode *input_node, const char *input_id) { diff --git a/engine/tests/gtest_main.cpp b/engine/tests/gtest_main.cpp new file mode 100644 index 000000000..5efb3cb25 --- /dev/null +++ b/engine/tests/gtest_main.cpp @@ -0,0 +1,35 @@ +/*** + + Oak - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +// Shared entry point for the liboakengine facade GTest binaries. +// +// Unlike the Qt-based olive-gtest harness, these facade tests manage their own +// oakengine_init()/oakengine_shutdown() lifecycle inside each TEST (their init +// flags differ -- headless vs. render vs. lifecycle probing -- so a single +// shared fixture is not possible). This main therefore only bootstraps Google +// Test; no global engine state is created here. + +#include + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/gtest/timeline_legacy_test.cpp b/tests/gtest/timeline_legacy_test.cpp new file mode 100644 index 000000000..ab7abfd9c --- /dev/null +++ b/tests/gtest/timeline_legacy_test.cpp @@ -0,0 +1,620 @@ +/*** + + 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 . + +***/ + +// Migrated to Google Test from the legacy OAK_ADD_TEST macro framework +// (tests/timeline/timeline-tests.cpp). See docs/zh/plans/gtest-migration-guide.md. + +#include + +#include "node/block/clip/clip.h" +#include "node/block/transition/crossdissolve/crossdissolvetransition.h" +#include "node/color/colormanager/colormanager.h" +#include "node/math/math/math.h" +#include "node/math/merge/merge.h" +#include "node/output/track/track.h" +#include "node/output/track/tracklist.h" +#include "node/project.h" +#include "node/project/sequence/sequence.h" +#include "timeline/timelineundogeneral.h" +#include "timeline/timelineundopointer.h" +#include "undo/undocommand.h" + +using namespace olive; + +namespace +{ + +// Mirrors the legacy TIMELINE_TEST_START prologue: default color config, +// a project and a sequence parented to it. +class TimelineLegacyTest : public ::testing::Test { +protected: + void SetUp() override + { + ColorManager::set_up_default_config(); + sequence.setParent(&project); + } + + Project project; + Sequence sequence; +}; + +#define UsingTransition CrossDissolveTransition + +} // namespace + +TEST_F(TimelineLegacyTest, add_track) +{ + Track *first_video_track, *first_audio_track; + + { + // Test creating initial video track + first_video_track = TimelineAddTrackCommand::run_immediately( + sequence.track_list(Track::k_video)); + + ASSERT_TRUE(sequence.get_connected_output(Sequence::k_texture_input) == + first_video_track); + ASSERT_TRUE(sequence.track_list(Track::k_video)->get_track_count() == 1); + ASSERT_TRUE(sequence.track_list(Track::k_video)->get_track_at(0) == + first_video_track); + } + + { + // Test creating initial audio track + first_audio_track = TimelineAddTrackCommand::run_immediately( + sequence.track_list(Track::k_audio)); + + ASSERT_TRUE(sequence.get_connected_output(Sequence::k_samples_input) == + first_audio_track); + ASSERT_TRUE(sequence.track_list(Track::k_audio)->get_track_count() == 1); + ASSERT_TRUE(sequence.track_list(Track::k_audio)->get_track_at(0) == + first_audio_track); + } + + { + // Test creating second video track with merge + Track *second_video_track = TimelineAddTrackCommand::run_immediately( + sequence.track_list(Track::k_video), true); + ASSERT_TRUE(sequence.get_connected_output(Sequence::k_texture_input) != + first_video_track); + ASSERT_TRUE(sequence.get_connected_output(Sequence::k_texture_input) != + second_video_track); + ASSERT_TRUE(sequence.track_list(Track::k_video)->get_track_count() == 2); + ASSERT_TRUE(sequence.track_list(Track::k_video)->get_track_at(1) == + second_video_track); + + MergeNode *merge = dynamic_cast( + sequence.get_connected_output(Sequence::k_texture_input)); + ASSERT_TRUE(merge); + ASSERT_TRUE(merge->get_connected_output(MergeNode::k_base_in) == + first_video_track); + ASSERT_TRUE(merge->get_connected_output(MergeNode::k_blend_in) == + second_video_track); + } + + { + // Test creating second audio track with merge + Track *second_audio_track = TimelineAddTrackCommand::run_immediately( + sequence.track_list(Track::k_audio), true); + ASSERT_TRUE(sequence.get_connected_output(Sequence::k_samples_input) != + first_audio_track); + ASSERT_TRUE(sequence.get_connected_output(Sequence::k_samples_input) != + second_audio_track); + ASSERT_TRUE(sequence.track_list(Track::k_audio)->get_track_count() == 2); + ASSERT_TRUE(sequence.track_list(Track::k_audio)->get_track_at(1) == + second_audio_track); + + MathNode *merge = dynamic_cast( + sequence.get_connected_output(Sequence::k_samples_input)); + ASSERT_TRUE(merge); + ASSERT_TRUE(merge->get_connected_output(MathNode::k_param_a_in) == + first_audio_track); + ASSERT_TRUE(merge->get_connected_output(MathNode::k_param_b_in) == + second_audio_track); + } +} + +TEST_F(TimelineLegacyTest, SequenceDefaults) +{ + sequence.add_default_nodes(); + + ASSERT_TRUE(sequence.get_tracks().size() == 2); + + Track *tex_connect = + dynamic_cast(sequence.get_connected_texture_output()); + ASSERT_TRUE(tex_connect); + Track *smp_connect = + dynamic_cast(sequence.get_connected_sample_output()); + ASSERT_TRUE(smp_connect); + ASSERT_TRUE(tex_connect != smp_connect); + ASSERT_TRUE(sequence.get_tracks().contains(tex_connect)); + ASSERT_TRUE(sequence.get_tracks().contains(smp_connect)); +} + +TEST_F(TimelineLegacyTest, Trim) +{ + sequence.add_default_nodes(); + + Track *track = sequence.get_tracks().first(); + + ClipBlock *block1 = new ClipBlock(); + block1->set_length_and_media_out(2); + block1->setParent(&project); + track->append_block(block1); + + ClipBlock *block2 = new ClipBlock(); + block2->set_length_and_media_out(2); + block2->setParent(&project); + track->append_block(block2); + + // There should be two blocks right now + ASSERT_TRUE(track->blocks().size() == 2); + + { + // Trim out point of second block + BlockTrimCommand command(track, block2, 1, Timeline::k_trim_out); + command.redo_now(); + + // No block should have been added + ASSERT_TRUE(track->blocks().size() == 2); + ASSERT_TRUE(block2->length() == 1); + ASSERT_TRUE(block1->length() == 2); + + command.undo_now(); + + ASSERT_TRUE(track->blocks().size() == 2); + ASSERT_TRUE(block2->length() == 2); + ASSERT_TRUE(block1->length() == 2); + } + + { + // Trim in point of second block + BlockTrimCommand command(track, block2, 1, Timeline::k_trim_in); + command.redo_now(); + + // Gap should be inserted in between + ASSERT_TRUE(track->blocks().size() == 3); + GapBlock *gap = dynamic_cast(track->blocks().at(1)); + ASSERT_TRUE(gap); + ASSERT_TRUE(gap->length() == 1); + ASSERT_TRUE(block2->length() == 1); + ASSERT_TRUE(block1->length() == 2); + ASSERT_TRUE(block1->next() == gap); + ASSERT_TRUE(block2->previous() == gap); + + command.undo_now(); + + ASSERT_TRUE(track->blocks().size() == 2); + ASSERT_TRUE(block2->length() == 2); + ASSERT_TRUE(block1->length() == 2); + } + + { + // Trim out point of first block + BlockTrimCommand command(track, block1, 1, Timeline::k_trim_out); + command.redo_now(); + + // Gap should be inserted in between + ASSERT_TRUE(track->blocks().size() == 3); + GapBlock *gap = dynamic_cast(track->blocks().at(1)); + ASSERT_TRUE(gap); + ASSERT_TRUE(gap->length() == 1); + ASSERT_TRUE(block1->length() == 1); + ASSERT_TRUE(block2->length() == 2); + ASSERT_TRUE(block1->next() == gap); + ASSERT_TRUE(block2->previous() == gap); + + command.undo_now(); + + ASSERT_TRUE(track->blocks().size() == 2); + ASSERT_TRUE(block2->length() == 2); + ASSERT_TRUE(block1->length() == 2); + } + + { + // Trim in point of first block + BlockTrimCommand command(track, block1, 1, Timeline::k_trim_in); + command.redo_now(); + + // Gap should be prepended to the start + ASSERT_TRUE(track->blocks().size() == 3); + GapBlock *gap = dynamic_cast(track->blocks().at(0)); + ASSERT_TRUE(gap); + ASSERT_TRUE(gap->length() == 1); + ASSERT_TRUE(block1->length() == 1); + ASSERT_TRUE(block2->length() == 2); + ASSERT_TRUE(block1->next() == block2); + ASSERT_TRUE(block1->previous() == gap); + + command.undo_now(); + + ASSERT_TRUE(track->blocks().size() == 2); + ASSERT_TRUE(block2->length() == 2); + ASSERT_TRUE(block1->length() == 2); + } +} + +TEST_F(TimelineLegacyTest, ReplaceBlockWithGap_ClipsOnly) +{ + // create a track that goes clip -> clip -> clip + sequence.add_default_nodes(); + Track *track = sequence.track_list(Track::k_video)->get_tracks().first(); + + ClipBlock *a = new ClipBlock(); + a->setParent(&project); + track->append_block(a); + + ClipBlock *b = new ClipBlock(); + b->setParent(&project); + track->append_block(b); + + ClipBlock *c = new ClipBlock(); + c->setParent(&project); + track->append_block(c); + + { + // Replace clip c with a gap + TrackReplaceBlockWithGapCommand command(track, c); + command.redo_now(); + + // Clip should be removed without any gap actually taking its place, since the clip is at the + // end of the track + ASSERT_TRUE(track->blocks().size() == 2); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + + command.undo_now(); + + ASSERT_TRUE(track->blocks().size() == 3); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + ASSERT_TRUE(track->blocks().at(2) == c); + } + + { + // Replace clip B with a gap + TrackReplaceBlockWithGapCommand command(track, b); + command.redo_now(); + + // B should be replaced with a gap + ASSERT_TRUE(track->blocks().size() == 3); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) != b); + ASSERT_TRUE(dynamic_cast(track->blocks().at(1))); + ASSERT_TRUE(track->blocks().at(1)->length() == b->length()); + ASSERT_TRUE(track->blocks().at(2) == c); + + command.undo_now(); + + ASSERT_TRUE(track->blocks().size() == 3); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + ASSERT_TRUE(track->blocks().at(2) == c); + } +} + +TEST_F(TimelineLegacyTest, ReplaceBlockWithGap_ClipsAndGaps) +{ + // create a track that goes clip -> gap -> clip -> clip -> gap -> clip + sequence.add_default_nodes(); + Track *track = sequence.track_list(Track::k_video)->get_tracks().first(); + + ClipBlock *a = new ClipBlock(); + a->setParent(&project); + track->append_block(a); + + GapBlock *b = new GapBlock(); + b->setParent(&project); + track->append_block(b); + + ClipBlock *c = new ClipBlock(); + c->setParent(&project); + track->append_block(c); + + GapBlock *d = new GapBlock(); + d->setParent(&project); + track->append_block(d); + + ClipBlock *e = new ClipBlock(); + e->setParent(&project); + track->append_block(e); + + { + // Replace clip E with a gap + TrackReplaceBlockWithGapCommand command(track, e); + command.redo_now(); + + // Both clips D and E should be removed because this command should remove any trailing gaps + ASSERT_TRUE(track->blocks().size() == 3); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + ASSERT_TRUE(track->blocks().at(2) == c); + + // Test undo + command.undo_now(); + + ASSERT_TRUE(track->blocks().size() == 5); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + ASSERT_TRUE(track->blocks().at(2) == c); + ASSERT_TRUE(track->blocks().at(3) == d); + ASSERT_TRUE(track->blocks().at(4) == e); + } + + { + // Replace clip A with a gap + Rational original_length_of_a = a->length(); + Rational original_length_of_b = b->length(); + + TrackReplaceBlockWithGapCommand command(track, a); + command.redo_now(); + + // A should be removed and B should take its place + ASSERT_TRUE(track->blocks().size() == 4); + + ASSERT_TRUE(track->blocks().at(0) == b); + ASSERT_TRUE(track->blocks().at(1) == c); + ASSERT_TRUE(track->blocks().at(2) == d); + ASSERT_TRUE(track->blocks().at(3) == e); + ASSERT_TRUE(b->length() == + original_length_of_a + original_length_of_b); + + // Test undo + command.undo_now(); + + ASSERT_TRUE(track->blocks().size() == 5); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + ASSERT_TRUE(track->blocks().at(2) == c); + ASSERT_TRUE(track->blocks().at(3) == d); + ASSERT_TRUE(track->blocks().at(4) == e); + ASSERT_TRUE(a->length() == original_length_of_a); + ASSERT_TRUE(b->length() == original_length_of_b); + } + + { + // Replace clip c with a gap + Rational original_length_of_b = b->length(); + Rational original_length_of_c = c->length(); + Rational original_length_of_d = d->length(); + + TrackReplaceBlockWithGapCommand command(track, c); + command.redo_now(); + + // c and D should be removed, and B should take both of their places + ASSERT_TRUE(track->blocks().size() == 3); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + ASSERT_TRUE(track->blocks().at(2) == e); + ASSERT_TRUE(b->length() == original_length_of_b + + original_length_of_c + + original_length_of_d); + + // Test undo + command.undo_now(); + + ASSERT_TRUE(track->blocks().size() == 5); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + ASSERT_TRUE(track->blocks().at(2) == c); + ASSERT_TRUE(track->blocks().at(3) == d); + ASSERT_TRUE(track->blocks().at(4) == e); + ASSERT_TRUE(b->length() == original_length_of_b); + ASSERT_TRUE(c->length() == original_length_of_c); + ASSERT_TRUE(d->length() == original_length_of_d); + } + + { + // add a fourth clip at the end of the track + ClipBlock *f = new ClipBlock(); + f->setParent(&project); + track->append_block(f); + + // Try replacing E with a block again + TrackReplaceBlockWithGapCommand command(track, e); + Rational original_length_of_d = d->length(); + Rational original_length_of_e = e->length(); + command.redo_now(); + + // E should be removed and D should have taken its place + ASSERT_TRUE(track->blocks().size() == 5); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + ASSERT_TRUE(track->blocks().at(2) == c); + ASSERT_TRUE(track->blocks().at(3) == d); + ASSERT_TRUE(track->blocks().at(4) == f); + ASSERT_TRUE(d->length() == + original_length_of_d + original_length_of_e); + + command.undo_now(); + + ASSERT_TRUE(track->blocks().size() == 6); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + ASSERT_TRUE(track->blocks().at(2) == c); + ASSERT_TRUE(track->blocks().at(3) == d); + ASSERT_TRUE(track->blocks().at(4) == e); + ASSERT_TRUE(track->blocks().at(5) == f); + ASSERT_TRUE(d->length() == original_length_of_d); + ASSERT_TRUE(e->length() == original_length_of_e); + } +} + +TEST_F(TimelineLegacyTest, ReplaceBlockWithGap_ClipsAndTransitions) +{ + // create a track that goes clip -> gap -> clip -> clip -> gap -> clip + sequence.add_default_nodes(); + Track *track = sequence.track_list(Track::k_video)->get_tracks().first(); + + UsingTransition *a_in = new UsingTransition(); + a_in->setParent(&project); + track->append_block(a_in); + + ClipBlock *a = new ClipBlock(); + a->setParent(&project); + track->append_block(a); + + UsingTransition *a_to_b = new UsingTransition(); + a_to_b->setParent(&project); + track->append_block(a_to_b); + + ClipBlock *b = new ClipBlock(); + b->setParent(&project); + track->append_block(b); + + UsingTransition *b_out = new UsingTransition(); + b_out->setParent(&project); + track->append_block(b_out); + + Node::connect_edge(a, NodeInput(a_in, UsingTransition::k_in_block_input)); + Node::connect_edge(a, NodeInput(a_to_b, UsingTransition::k_out_block_input)); + Node::connect_edge(b, NodeInput(a_to_b, UsingTransition::k_in_block_input)); + Node::connect_edge(b, NodeInput(b_out, UsingTransition::k_out_block_input)); + + { + // Replace A with gap + TrackReplaceBlockWithGapCommand command(track, a); + command.redo_now(); + + // A should be replaced with a gap and so should A_IN since A was the only clip connected to it. + // Also A_TO_B should only be connected to B now + ASSERT_TRUE(track->blocks().size() == 4); + ASSERT_TRUE(dynamic_cast(track->blocks().at(0))); + ASSERT_TRUE(track->blocks().at(1) == a_to_b); + ASSERT_TRUE(track->blocks().at(2) == b); + ASSERT_TRUE(track->blocks().at(3) == b_out); + + command.undo_now(); + + ASSERT_TRUE(track->blocks().size() == 5); + ASSERT_TRUE(track->blocks().at(0) == a_in); + ASSERT_TRUE(track->blocks().at(1) == a); + ASSERT_TRUE(track->blocks().at(2) == a_to_b); + ASSERT_TRUE(track->blocks().at(3) == b); + ASSERT_TRUE(track->blocks().at(4) == b_out); + } +} + +TEST_F(TimelineLegacyTest, InsertGaps_SingleTrack) +{ + sequence.add_default_nodes(); + + TrackList *list = sequence.track_list(Track::k_video); + Track *track = list->get_tracks().first(); + + ClipBlock *a = new ClipBlock(); + a->set_length_and_media_out(1); + a->setParent(&project); + track->append_block(a); + + ClipBlock *b = new ClipBlock(); + b->set_length_and_media_out(1); + b->setParent(&project); + track->append_block(b); + + ClipBlock *c = new ClipBlock(); + c->set_length_and_media_out(1); + c->setParent(&project); + track->append_block(c); + + ASSERT_TRUE(track->blocks().size() == 3); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + ASSERT_TRUE(track->blocks().at(2) == c); + + { + // insert gap at the start of the track, all blocks should be unsplit and shifted to the right + TrackListInsertGaps command(list, 0, 2); + command.redo_now(); + + ASSERT_TRUE(track->blocks().size() == 4); + ASSERT_TRUE(dynamic_cast(track->blocks().at(0))); + ASSERT_TRUE(track->blocks().at(0)->length() == 2); + ASSERT_TRUE(track->blocks().at(1) == a); + ASSERT_TRUE(track->blocks().at(2) == b); + ASSERT_TRUE(track->blocks().at(3) == c); + + command.undo_now(); + + ASSERT_TRUE(track->blocks().size() == 3); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + ASSERT_TRUE(track->blocks().at(2) == c); + } + + { + // insert gap in the middle of block A, block A should be halved with a copy at 2 and the gap at 1 + TrackListInsertGaps command(list, Rational(1, 2), 2); + command.redo_now(); + + ASSERT_TRUE(track->blocks().size() == 5); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(0)->length() == Rational(1, 2)); + ASSERT_TRUE(dynamic_cast(track->blocks().at(1))); + ASSERT_TRUE(dynamic_cast(track->blocks().at(2))); + ASSERT_TRUE(track->blocks().at(3) == b); + ASSERT_TRUE(track->blocks().at(4) == c); + + command.undo_now(); + + ASSERT_EQ(track->blocks().size(), 3); + ASSERT_EQ(track->blocks().at(0), a); + ASSERT_EQ(track->blocks().at(0)->length(), 1); + ASSERT_EQ(track->blocks().at(1), b); + ASSERT_EQ(track->blocks().at(2), c); + } + + { + // insert gap between block A and B, blocks should be unsplit with a gap at 1 + TrackListInsertGaps command(list, 1, 2); + command.redo_now(); + + ASSERT_TRUE(track->blocks().size() == 4); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(dynamic_cast(track->blocks().at(1))); + ASSERT_TRUE(track->blocks().at(2) == b); + ASSERT_TRUE(track->blocks().at(3) == c); + + command.undo_now(); + + ASSERT_TRUE(track->blocks().size() == 3); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + ASSERT_TRUE(track->blocks().at(2) == c); + } + + { + // insert gap at end, nothing should be added + TrackListInsertGaps command(list, 3, 2); + command.redo_now(); + + ASSERT_TRUE(track->blocks().size() == 3); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + ASSERT_TRUE(track->blocks().at(2) == c); + + command.undo_now(); + + ASSERT_TRUE(track->blocks().size() == 3); + ASSERT_TRUE(track->blocks().at(0) == a); + ASSERT_TRUE(track->blocks().at(1) == b); + ASSERT_TRUE(track->blocks().at(2) == c); + } +}