From 928502ec5b73bc6f482afd54f0fbcb6e7e59066c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 20 Mar 2019 16:49:01 +1100 Subject: [PATCH 01/10] queue init function instead of direct connect --- main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.cpp b/main.cpp index ed250177e..2cf002480 100644 --- a/main.cpp +++ b/main.cpp @@ -133,7 +133,7 @@ int main(int argc, char *argv[]) { olive::timeline::MultiplyTrackSizesByDPI(); // connect main window's first paint to global's init finished function - QObject::connect(&w, SIGNAL(finished_first_paint()), olive::Global.get(), SLOT(finished_initialize())); + QObject::connect(&w, SIGNAL(finished_first_paint()), olive::Global.get(), SLOT(finished_initialize()), Qt::QueuedConnection); if (!load_proj.isEmpty()) { olive::Global->load_project_on_launch(load_proj); From 67399702a0c73829cc734581517e2731e6609f83 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 20 Mar 2019 23:15:12 +1100 Subject: [PATCH 02/10] fixed crash with text effect --- effects/effect.cpp | 8 ++++---- effects/internal/texteffect.cpp | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/effects/effect.cpp b/effects/effect.cpp index 64386d2ff..5690000a9 100644 --- a/effects/effect.cpp +++ b/effects/effect.cpp @@ -893,6 +893,7 @@ GLuint Effect::process_superimpose(double timecode) { } if (texture == nullptr || texture->width() != img.width() || texture->height() != img.height()) { + delete_texture(); texture = new QOpenGLTexture(QOpenGLTexture::Target2D); @@ -903,6 +904,7 @@ GLuint Effect::process_superimpose(double timecode) { texture->allocateStorage(QOpenGLTexture::RGBA, QOpenGLTexture::UInt8); redrew_image = true; + } if (redrew_image) { @@ -1088,10 +1090,8 @@ bool Effect::valueHasChanged(double timecode) { } void Effect::delete_texture() { - if (texture != nullptr) { - delete texture; - texture = nullptr; - } + delete texture; + texture = nullptr; } const EffectMeta* get_meta_from_name(const QString& input) { diff --git a/effects/internal/texteffect.cpp b/effects/internal/texteffect.cpp index ee183f0b3..3fec335ad 100644 --- a/effects/internal/texteffect.cpp +++ b/effects/internal/texteffect.cpp @@ -299,8 +299,6 @@ void TextEffect::redraw(double timecode) { } void TextEffect::shadow_enable(bool e) { - close(); - shadow_color->SetEnabled(e); shadow_angle->SetEnabled(e); shadow_distance->SetEnabled(e); From fbd77791cd6b6130fe319e1291b24d7844adec69 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 21 Mar 2019 00:18:47 +1100 Subject: [PATCH 03/10] implemented #574 --- dialogs/preferencesdialog.cpp | 4 ++-- dialogs/proxydialog.cpp | 3 ++- effects/internal/vsthost.cpp | 3 ++- global/global.cpp | 22 +++++++++++++++++++--- global/global.h | 35 +++++++++++++++++++++++++++++++++++ panels/project.cpp | 4 ++-- project/loadthread.cpp | 2 +- project/sourcescommon.cpp | 3 ++- undo/undo.cpp | 6 +++--- 9 files changed, 68 insertions(+), 14 deletions(-) diff --git a/dialogs/preferencesdialog.cpp b/dialogs/preferencesdialog.cpp index 4717dc7ae..9e366344f 100644 --- a/dialogs/preferencesdialog.cpp +++ b/dialogs/preferencesdialog.cpp @@ -319,8 +319,8 @@ void PreferencesDialog::save() { accept(); if (restart_after_saving) { - // since we already ran can_close_project(), bypass checking again by running setWindowModified(false) - olive::MainWindow->setWindowModified(false); + // since we already ran can_close_project(), bypass checking again by running set_modified(false) + olive::Global->set_modified(false); olive::MainWindow->close(); diff --git a/dialogs/proxydialog.cpp b/dialogs/proxydialog.cpp index bcc0a0745..d86044860 100644 --- a/dialogs/proxydialog.cpp +++ b/dialogs/proxydialog.cpp @@ -31,6 +31,7 @@ #include "project/proxygenerator.h" #include "project/footage.h" #include "ui/mainwindow.h" +#include "global/global.h" ProxyDialog::ProxyDialog(QWidget *parent, const QVector &media) : QDialog(parent), @@ -155,7 +156,7 @@ void ProxyDialog::accept() { olive::proxy_generator.queue(info_list.at(i)); } - olive::MainWindow->setWindowModified(true); + olive::Global->set_modified(true); QDialog::accept(); } diff --git a/effects/internal/vsthost.cpp b/effects/internal/vsthost.cpp index ade778f06..8a07f69dc 100644 --- a/effects/internal/vsthost.cpp +++ b/effects/internal/vsthost.cpp @@ -33,6 +33,7 @@ #include "rendering/audio.h" #include "ui/mainwindow.h" +#include "global/global.h" #include "global/debug.h" #ifdef __linux__ @@ -85,7 +86,7 @@ intptr_t hostCallback(AEffect* effect, int32_t opcode, int32_t index, intptr_t v // but we are aware of it break; case audioMasterEndEdit: // change made - olive::MainWindow->setWindowModified(true); + olive::Global->set_modified(true); break; default: qInfo() << "Plugin requested unhandled opcode" << opcode; diff --git a/global/global.cpp b/global/global.cpp index 16a59c9c6..668586dc9 100644 --- a/global/global.cpp +++ b/global/global.cpp @@ -48,7 +48,9 @@ std::unique_ptr olive::Global; QString olive::ActiveProjectFilename; QString olive::AppName; -OliveGlobal::OliveGlobal() { +OliveGlobal::OliveGlobal() : + changed_since_last_autorecovery(false) +{ // sets current app name QString version_id; @@ -107,6 +109,17 @@ void OliveGlobal::set_rendering_state(bool rendering) { } } +void OliveGlobal::set_modified(bool modified) +{ + olive::MainWindow->setWindowModified(modified); + changed_since_last_autorecovery = modified; +} + +bool OliveGlobal::is_modified() +{ + return olive::MainWindow->isWindowModified(); +} + void OliveGlobal::load_project_on_launch(const QString& s) { olive::ActiveProjectFilename = s; enable_load_project_on_init = true; @@ -219,7 +232,7 @@ bool OliveGlobal::save_project() { } bool OliveGlobal::can_close_project() { - if (olive::MainWindow->isWindowModified()) { + if (is_modified()) { QMessageBox* m = new QMessageBox( QMessageBox::Question, tr("Unsaved Project"), @@ -281,8 +294,11 @@ void OliveGlobal::finished_initialize() { } void OliveGlobal::save_autorecovery_file() { - if (olive::MainWindow->isWindowModified()) { + if (changed_since_last_autorecovery) { panel_project->save_project(true); + + changed_since_last_autorecovery = false; + qInfo() << "Auto-recovery project saved"; } } diff --git a/global/global.h b/global/global.h index 6a9d57617..4c4cc0872 100644 --- a/global/global.h +++ b/global/global.h @@ -92,6 +92,31 @@ public: */ void set_rendering_state(bool rendering); + /** + * @brief Set the application's "modified" state + * + * Primarily controls whether the application prompts the user to save the project upon closing or not. Also + * technically controls whether to create autorecovery files as they'll only be generated if there are unsaved + * changes. + * + * @param modified + * + * TRUE if the project has been modified, FALSE if it has not. + */ + void set_modified(bool modified); + + /** + * @brief Get application's current "modified" state + * + * Currently just a wrapper around MainWindow::isWindowModified(), but use this instead in case it changes. + * This value is used to determine whether the currently open project has unsaved changes. + * + * @return + * + * TRUE if the project has been modified since the last save. + */ + bool is_modified(); + /** * @brief Set a project to load just after launching * @@ -324,6 +349,16 @@ private: */ std::unique_ptr translator; + /** + * @brief Internal variable for whether the project has changed since the last autorecovery + * + * Set by set_modified(), which should be called alongside any change made to the project file and is "unset" when + * an autorecovery file is made. Provides an extra layer of abstraction from the application "modified" state to + * prevents an autorecovery file saving multiple times if the project hasn't actually changed since the last + * autorecovery, but still hasn't been saved into the original file yet. + */ + bool changed_since_last_autorecovery; + private slots: }; diff --git a/panels/project.cpp b/panels/project.cpp index dfacd8488..2ef1ec72e 100644 --- a/panels/project.cpp +++ b/panels/project.cpp @@ -1046,7 +1046,7 @@ void Project::new_project() { olive::Global->set_sequence(nullptr); panel_footage_viewer->set_media(nullptr); clear(); - olive::MainWindow->setWindowModified(false); + olive::Global->set_modified(false); } void Project::load_project(const QString& filename, bool autorecovery, bool clear) { @@ -1314,7 +1314,7 @@ void Project::save_project(bool autorecovery) { if (!autorecovery) { add_recent_project(olive::ActiveProjectFilename); - olive::MainWindow->setWindowModified(false); + olive::Global->set_modified(false); } } diff --git a/project/loadthread.cpp b/project/loadthread.cpp index eca71eb90..052a28406 100644 --- a/project/loadthread.cpp +++ b/project/loadthread.cpp @@ -774,7 +774,7 @@ void LoadThread::success_func() { panel_project->add_recent_project(filename_); } - olive::MainWindow->setWindowModified(autorecovery_ || !clear_); + olive::Global->set_modified(autorecovery_ || !clear_); if (open_seq != nullptr) { olive::Global->set_sequence(open_seq); } diff --git a/project/sourcescommon.cpp b/project/sourcescommon.cpp index 2d6bd9b5f..e516774e0 100644 --- a/project/sourcescommon.cpp +++ b/project/sourcescommon.cpp @@ -39,6 +39,7 @@ #include "project/projectfilter.h" #include "timeline/sequence.h" #include "global/config.h" +#include "global/global.h" #include "dialogs/proxydialog.h" #include "ui/viewerwidget.h" #include "project/proxygenerator.h" @@ -446,5 +447,5 @@ void SourcesCommon::clear_proxies_from_selected() { panel_sequence_viewer->viewer_widget->frame_update(); } - olive::MainWindow->setWindowModified(true); + olive::Global->set_modified(true); } diff --git a/undo/undo.cpp b/undo/undo.cpp index 172755afb..5b34ffa14 100644 --- a/undo/undo.cpp +++ b/undo/undo.cpp @@ -1136,7 +1136,7 @@ void OliveAction::undo() { doUndo(); if (set_window_modified) { - olive::MainWindow->setWindowModified(old_window_modified); + olive::Global->set_modified(old_window_modified); } } @@ -1146,10 +1146,10 @@ void OliveAction::redo() { if (set_window_modified) { // store current modified state - old_window_modified = olive::MainWindow->isWindowModified(); + old_window_modified = olive::Global->is_modified(); // set modified to true - olive::MainWindow->setWindowModified(true); + olive::Global->set_modified(true); } } From 773882222b42ccc4ee38a4db69ce838d2322d693 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 21 Mar 2019 00:36:57 +1100 Subject: [PATCH 04/10] detect failure to open file, fixes #608 --- rendering/cacher.cpp | 44 +++++++++++++++++++++++++++++++++++++------- rendering/cacher.h | 7 +++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/rendering/cacher.cpp b/rendering/cacher.cpp index d7082b52d..8d23fe99b 100644 --- a/rendering/cacher.cpp +++ b/rendering/cacher.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include "project/projectelements.h" @@ -39,6 +40,7 @@ #include "panels/panels.h" #include "global/config.h" #include "global/debug.h" +#include "ui/mainwindow.h" // Enable verbose audio messages - good for debugging reversed audio //#define AUDIOWARNINGS @@ -847,7 +849,12 @@ void Cacher::WakeMainThread() Cacher::Cacher(Clip* c) : clip(c), frame_(nullptr), - pkt(nullptr) + pkt(nullptr), + formatCtx(nullptr), + opts(nullptr), + filter_graph(nullptr), + codecCtx(nullptr), + is_valid_state_(false) {} void Cacher::OpenWorker() { @@ -911,6 +918,7 @@ void Cacher::OpenWorker() { char err[1024]; av_strerror(errCode, err, 1024); qCritical() << "Could not open" << filename << "-" << err; + olive::MainWindow->statusBar()->showMessage(tr("Could not open %1 - %2").arg(filename, err)); return; } @@ -919,6 +927,7 @@ void Cacher::OpenWorker() { char err[1024]; av_strerror(errCode, err, 1024); qCritical() << "Could not open" << filename << "-" << err; + olive::MainWindow->statusBar()->showMessage(tr("Could not open %1 - %2").arg(filename, err)); return; } @@ -1083,6 +1092,8 @@ void Cacher::OpenWorker() { } qInfo() << "Clip opened on track" << clip->track() << "(took" << (QDateTime::currentMSecsSinceEpoch() - time_start) << "ms)"; + + is_valid_state_ = true; } void Cacher::CacheWorker() { @@ -1112,17 +1123,27 @@ void Cacher::CloseWorker() { } if (clip->media() != nullptr && clip->media()->get_type() == MEDIA_TYPE_FOOTAGE) { - avfilter_graph_free(&filter_graph); + if (filter_graph != nullptr) { + avfilter_graph_free(&filter_graph); + filter_graph = nullptr; + } - avcodec_close(codecCtx); - avcodec_free_context(&codecCtx); + if (codecCtx != nullptr) { + avcodec_close(codecCtx); + avcodec_free_context(&codecCtx); + codecCtx = nullptr; + } - av_dict_free(&opts); + if (opts != nullptr) { + av_dict_free(&opts); + } // protection for get_timebase() stream = nullptr; - avformat_close_input(&formatCtx); + if (formatCtx != nullptr) { + avformat_close_input(&formatCtx); + } } clip->reset(); @@ -1144,11 +1165,16 @@ void Cacher::run() { queued_ = false; if (!caching_) { break; - } else { + } else if (is_valid_state_) { CacheWorker(); + } else { + // main thread waits until cacher starts fully, but the cacher can't run, so we just wake it up here + WakeMainThread(); } } + is_valid_state_ = false; + CloseWorker(); clip->state_change_lock.unlock(); @@ -1169,6 +1195,10 @@ void Cacher::Open() void Cacher::Cache(long playhead, bool scrubbing, QVector& nests, int playback_speed) { + if (!is_valid_state_) { + return; + } + if (clip->media_stream() != nullptr && queue_.size() > 0 && clip->media_stream()->infinite_length) { diff --git a/rendering/cacher.h b/rendering/cacher.h index 57fca0e75..2b5f827da 100644 --- a/rendering/cacher.h +++ b/rendering/cacher.h @@ -465,6 +465,13 @@ private: */ bool caching_; + /** + * @brief Internal variable for whether the current Cacher state is valid or not + * + * If there was an error opening the Cacher for any reason, this will be false. + */ + bool is_valid_state_; + /** * @brief Internal function for opening the file handles and decoder * From 7f59bd9860f63327d6675f4232453fed12c6f308 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 21 Mar 2019 10:33:43 +1100 Subject: [PATCH 05/10] fixed #657 --- dialogs/preferencesdialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dialogs/preferencesdialog.cpp b/dialogs/preferencesdialog.cpp index 9e366344f..1cd0566bb 100644 --- a/dialogs/preferencesdialog.cpp +++ b/dialogs/preferencesdialog.cpp @@ -563,7 +563,7 @@ void PreferencesDialog::setup_ui() { QVBoxLayout* behavior_tab_layout = new QVBoxLayout(behavior_tab); - add_default_effects_to_clips = new QCheckBox("Add Default Effects to New Clips"); + add_default_effects_to_clips = new QCheckBox(tr("Add Default Effects to New Clips")); add_default_effects_to_clips->setChecked(olive::CurrentConfig.add_default_effects_to_clips); behavior_tab_layout->addWidget(add_default_effects_to_clips); From 739abdd401d304c6eeacd1ce1d7be5c9b95705df Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 21 Mar 2019 10:33:55 +1100 Subject: [PATCH 06/10] fixed #655 --- ui/timelineheader.cpp | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/ui/timelineheader.cpp b/ui/timelineheader.cpp index 0f1bb5aa7..a1779c85d 100644 --- a/ui/timelineheader.cpp +++ b/ui/timelineheader.cpp @@ -328,11 +328,26 @@ double TimelineHeader::get_zoom() { void TimelineHeader::delete_markers() { if (selected_markers.size() > 0) { + + // Send command to delete selected markers DeleteMarkerAction* dma = new DeleteMarkerAction(viewer->marker_ref); - for (int i=0;imarkers.append(selected_markers.at(i)); - } + dma->markers.append(selected_markers); olive::UndoStack.push(dma); + + // remove any indices for the selected markers that no longer exist + for (int i=0;i= viewer->marker_ref->size()) { + selected_markers.removeAt(i); + i--; + } + } + + // if we removed all the indices, re-select the last marker in the array so something is always selected + // (allows users to hold delete when deleting markers) + if (selected_markers.isEmpty() && !viewer->marker_ref->isEmpty()) { + selected_markers.append(viewer->marker_ref->size() - 1); + } + update_parents(); } } From 359f937b73d71ffdef3b676e2f66191179e033aa Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 21 Mar 2019 10:41:10 +1100 Subject: [PATCH 07/10] fixed #656 --- effects/fields/stringfield.cpp | 3 ++- effects/internal/timecodeeffect.cpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/effects/fields/stringfield.cpp b/effects/fields/stringfield.cpp index 55b59752d..30fa3c71e 100644 --- a/effects/fields/stringfield.cpp +++ b/effects/fields/stringfield.cpp @@ -10,7 +10,8 @@ StringField::StringField(EffectRow* parent, const QString& id, bool rich_text) : EffectField(parent, id, EFFECT_FIELD_STRING), rich_text_(rich_text) { - + // Set default value to an empty string + SetValueAt(0, ""); } QString StringField::GetStringAt(double timecode) diff --git a/effects/internal/timecodeeffect.cpp b/effects/internal/timecodeeffect.cpp index 43b195bc0..4de3e3497 100644 --- a/effects/internal/timecodeeffect.cpp +++ b/effects/internal/timecodeeffect.cpp @@ -83,7 +83,7 @@ TimecodeEffect::TimecodeEffect(Clip* c, const EffectMeta* em) : offset_y_val = new DoubleField(offset_row, "offsety"); EffectRow* prepent_text_row = new EffectRow(this, tr("Prepend")); - prepend_text = new StringField(prepent_text_row, "prepend"); + prepend_text = new StringField(prepent_text_row, "prepend", false); prepend_text->SetColumnSpan(2); } From f126c4ebc75132e7efd8816253d458ab1f0773b1 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 21 Mar 2019 10:46:02 +1100 Subject: [PATCH 08/10] made rich text the default title effect --- ui/timelinewidget.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/timelinewidget.cpp b/ui/timelinewidget.cpp index 2de81a528..dd2eb82da 100644 --- a/ui/timelinewidget.cpp +++ b/ui/timelinewidget.cpp @@ -57,7 +57,6 @@ #include "global/debug.h" #include "effects/effect.h" #include "effects/internal/solideffect.h" -#include "effects/internal/texteffect.h" #define MAX_TEXT_WIDTH 20 #define TRANSITION_BETWEEN_RANGE 40 @@ -1022,7 +1021,7 @@ void TimelineWidget::mouseReleaseEvent(QMouseEvent *event) { switch (panel_timeline->creating_object) { case ADD_OBJ_TITLE: c->set_name(tr("Title")); - c->effects.append(Effect::Create(c.get(), Effect::GetInternalMeta(EFFECT_INTERNAL_TEXT, EFFECT_TYPE_EFFECT))); + c->effects.append(Effect::Create(c.get(), Effect::GetInternalMeta(EFFECT_INTERNAL_RICHTEXT, EFFECT_TYPE_EFFECT))); break; case ADD_OBJ_SOLID: c->set_name(tr("Solid Color")); From be22e5fbfa127c3077166e5d2234dff6240dffb2 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 21 Mar 2019 14:01:10 +1100 Subject: [PATCH 09/10] more documentation and exportthread restructure --- dialogs/aboutdialog.cpp | 3 + dialogs/aboutdialog.h | 15 +- dialogs/actionsearch.cpp | 103 +++++++- dialogs/actionsearch.h | 150 +++++++++-- dialogs/clippropertiesdialog.cpp | 19 +- dialogs/clippropertiesdialog.h | 28 ++ dialogs/debugdialog.h | 40 ++- dialogs/demonotice.cpp | 54 ++-- dialogs/demonotice.h | 15 +- dialogs/exportdialog.cpp | 49 ++-- dialogs/exportdialog.h | 25 +- effects/effect.cpp | 1 - effects/effect.h | 1 - effects/effectfield.cpp | 8 - effects/fields/boolfield.h | 5 + olive.pro | 2 - rendering/cacher.cpp | 2 - rendering/exportthread.cpp | 427 ++++++++++++++++++++----------- rendering/exportthread.h | 37 +-- rendering/renderfunctions.h | 4 + rendering/renderthread.cpp | 2 +- timeline/clip.cpp | 46 ++-- timeline/clip.h | 2 +- ui/checkboxex.cpp | 33 --- ui/checkboxex.h | 35 --- ui/collapsiblewidget.cpp | 4 +- ui/collapsiblewidget.h | 4 +- ui/viewerwidget.cpp | 2 +- undo/undostack.h | 3 + 29 files changed, 743 insertions(+), 376 deletions(-) delete mode 100644 ui/checkboxex.cpp delete mode 100644 ui/checkboxex.h diff --git a/dialogs/aboutdialog.cpp b/dialogs/aboutdialog.cpp index 711c11e73..fcb0cc664 100644 --- a/dialogs/aboutdialog.cpp +++ b/dialogs/aboutdialog.cpp @@ -34,6 +34,7 @@ AboutDialog::AboutDialog(QWidget *parent) : QVBoxLayout* layout = new QVBoxLayout(this); layout->setSpacing(20); + // Construct About text QLabel* label = new QLabel(QString("" "

" @@ -49,6 +50,8 @@ AboutDialog::AboutDialog(QWidget *parent) : "protected by the GNU GPL."), tr("Olive Team is obliged to inform users that Olive source code is " "available for download from its website.")), this); + + // Set text formatting label->setAlignment(Qt::AlignCenter); label->setWordWrap(true); layout->addWidget(label); diff --git a/dialogs/aboutdialog.h b/dialogs/aboutdialog.h index 4f4e49134..f0a5f14bd 100644 --- a/dialogs/aboutdialog.h +++ b/dialogs/aboutdialog.h @@ -23,11 +23,24 @@ #include +/** + * @brief The AboutDialog class + * + * The About dialog (accessible through Help > About). Contains license and version information. + */ class AboutDialog : public QDialog { Q_OBJECT - public: + /** + * @brief AboutDialog Constructor + * + * Creates About dialog. + * + * @param parent + * + * QWidget parent object. Usually this will be MainWindow. + */ explicit AboutDialog(QWidget *parent = nullptr); }; diff --git a/dialogs/actionsearch.cpp b/dialogs/actionsearch.cpp index c112061dd..c64968322 100644 --- a/dialogs/actionsearch.cpp +++ b/dialogs/actionsearch.cpp @@ -30,63 +30,133 @@ ActionSearch::ActionSearch(QWidget *parent) : QDialog(parent) { + // ActionSearch requires a parent widget + Q_ASSERT(parent != nullptr); + + // Set styling (object name is required for CSS specific to this object) setObjectName("ASDiag"); setStyleSheet("#ASDiag{border: 2px solid #808080;}"); + // Size proportionally to the parent (usually MainWindow). resize(parent->width()/3, parent->height()/3); + // Show dialog as a "popup", which will make the dialog close if the user clicks out of it. setWindowFlags(Qt::Popup); QVBoxLayout* layout = new QVBoxLayout(this); + // Construct the main entry text field. ActionSearchEntry* entry_field = new ActionSearchEntry(this); + + // Set the main entry field font size to 1.2x its standard font size. QFont entry_field_font = entry_field->font(); entry_field_font.setPointSize(qRound(entry_field_font.pointSize()*1.2)); entry_field->setFont(entry_field_font); + + // Set placeholder text for the main entry field entry_field->setPlaceholderText(tr("Search for action...")); + + // Connect signals/slots connect(entry_field, SIGNAL(textChanged(const QString&)), this, SLOT(search_update(const QString &))); connect(entry_field, SIGNAL(returnPressed()), this, SLOT(perform_action())); + + // moveSelectionUp() and moveSelectionDown() are emitted when the user pressed up or down on the text field. + // We override it here to select the upper or lower item in the list. connect(entry_field, SIGNAL(moveSelectionUp()), this, SLOT(move_selection_up())); connect(entry_field, SIGNAL(moveSelectionDown()), this, SLOT(move_selection_down())); layout->addWidget(entry_field); + // Construct list of actions list_widget = new ActionSearchList(this); + + // Set list's font to 1.2x its standard font size QFont list_widget_font = list_widget->font(); list_widget_font.setPointSize(qRound(list_widget_font.pointSize()*1.2)); list_widget->setFont(list_widget_font); + layout->addWidget(list_widget); + connect(list_widget, SIGNAL(dbl_click()), this, SLOT(perform_action())); + // Instantly focus on the entry field to allow for fully keyboard operation (if this popup was initiated by keyboard + // shortcut for example). entry_field->setFocus(); } void ActionSearch::search_update(const QString &s, const QString &p, QMenu *parent) { + + // This function is recursive, using the `parent` parameter to loop through a menu's items. It functions in two + // modes - the parent being NULL, meaning it'll get MainWindow's menubar and loop over its menus, and the parent + // referring to a menu at which point it'll loop over its actions (and call itself recursively if it finds any + // submenus). + if (parent == nullptr) { + + // If parent is NULL, we'll pull from the MainWindow's menubar and call this recursively on all of its submenus + // (and their submenus). + + // We'll clear all the current items in the list since if we're here, we're just starting. list_widget->clear(); - QList menus = olive::MainWindow->menuBar()->actions(); + + QList menus = olive::MainWindow->menuBar()->actions(); + + // Loop through all menus from the menubar and run this function on each one. for (int i=0;imenu(); + search_update(s, p, menu); } - if (list_widget->count() > 0) + + // Once we're here, all the recursion/item retrieval is complete. We auto-select the first item for better + // keyboard-exclusive functionality. + if (list_widget->count() > 0) { list_widget->item(0)->setSelected(true); + } + } else { + + // Parent was not NULL, so we loop over the actions in the menu we were given in `parent`. + + // The list shows a '>' delimited hierarchy of the menus in which this action came from. We construct it here by + // adding the current menu's text to the existing hierarchy (passed in `p`). QString menu_text; if (!p.isEmpty()) menu_text += p + " > "; - menu_text += parent->title().replace("&", ""); + menu_text += parent->title().replace("&", ""); // Strip out any &s used in menu action names + + // Loop over the menu's actions QList actions = parent->actions(); for (int i=0;iisSeparator()) { + if (a->menu() != nullptr) { + + // If the action is a menu, run this function recursively on it search_update(s, menu_text, a->menu()); + } else { + + // This is a valid non-separator non-menu action, so check it against the currently entered string. + + // Strip out all &s from the action's name QString comp = a->text().replace("&", ""); + + // See if the action's name contains any of the currently entered string if (comp.contains(s, Qt::CaseInsensitive)) { + + // If so, we add it to the list widget. QListWidgetItem* item = new QListWidgetItem(QString("%1\n(%2)").arg(comp, menu_text), list_widget); + + // Add a pointer to the original QAction in the item's data item->setData(Qt::UserRole+1, reinterpret_cast(a)); + list_widget->addItem(item); + } + } } } @@ -94,16 +164,31 @@ void ActionSearch::search_update(const QString &s, const QString &p, QMenu *pare } void ActionSearch::perform_action() { + + // Loop over all the items in the list and if we find one that's selected, we trigger it. QList selected_items = list_widget->selectedItems(); if (list_widget->count() > 0 && selected_items.size() > 0) { + QListWidgetItem* item = selected_items.at(0); + + // Get QAction pointer from item's data QAction* a = reinterpret_cast(item->data(Qt::UserRole+1).value()); + a->trigger(); + } + + // Close this popup accept(); + } void ActionSearch::move_selection_up() { + + // Here we loop over all the items to find the currently selected one, and then select the one above it. We start + // iterating at 1 (instead of 0) to efficiently ignore the first item (since the selection can't go below the very + // bottom item). + int lim = list_widget->count(); for (int i=1;iitem(i)->isSelected()) { @@ -115,6 +200,11 @@ void ActionSearch::move_selection_up() { } void ActionSearch::move_selection_down() { + + // Here we loop over all the items to find the currently selected one, and then select the one below it. We limit it + // one entry before count() to efficiently ignore the item at the end (since the selection can't go below the very + // bottom item). + int lim = list_widget->count()-1; for (int i=0;iitem(i)->isSelected()) { @@ -128,6 +218,9 @@ void ActionSearch::move_selection_down() { ActionSearchEntry::ActionSearchEntry(QWidget *parent) : QLineEdit(parent) {} void ActionSearchEntry::keyPressEvent(QKeyEvent * event) { + + // Listen for up/down, otherwise pass the key event to the base class. + switch (event->key()) { case Qt::Key_Up: emit moveSelectionUp(); @@ -138,10 +231,14 @@ void ActionSearchEntry::keyPressEvent(QKeyEvent * event) { default: QLineEdit::keyPressEvent(event); } + } ActionSearchList::ActionSearchList(QWidget *parent) : QListWidget(parent) {} void ActionSearchList::mouseDoubleClickEvent(QMouseEvent *) { + + // Indiscriminately emit a signal on any double click emit dbl_click(); + } diff --git a/dialogs/actionsearch.h b/dialogs/actionsearch.h index 5d9632899..e00fb2025 100644 --- a/dialogs/actionsearch.h +++ b/dialogs/actionsearch.h @@ -26,39 +26,145 @@ #include #include -class ActionSearchList : public QListWidget { - Q_OBJECT -public: - ActionSearchList(QWidget* parent); -protected: - void mouseDoubleClickEvent(QMouseEvent *event); -signals: - void dbl_click(); -}; +class ActionSearchList; +/** + * @brief The ActionSearch class + * + * A popup window (accessible through Help > Action Search) that allows users to search for a menu command by typing + * rather than browsing through the menu bar. + */ class ActionSearch : public QDialog { - Q_OBJECT + Q_OBJECT public: - ActionSearch(QWidget* parent = nullptr); + /** + * @brief ActionSearch Constructor + * + * Create ActionSearch popup. + * + * @param parent + * + * QWidget parent. Usually MainWindow. + */ + ActionSearch(QWidget* parent); private slots: - void search_update(const QString& s, const QString &p = nullptr, QMenu *parent = nullptr); - void perform_action(); - void move_selection_up(); - void move_selection_down(); + /** + * @brief Update the list of actions according to a search query + * + * This function adds/removes actions in the action list according to a given search query entered by the user. + * + * To loop over the menubar and all of its menus and submenus, this function will call itself recursively. As such + * some of its parameters do not need to be set externally, as these will be set by the function itself as it calls + * itself. + * + * @param s + * + * The search text. This is the only parameter that should be set externally. + * + * @param p + * + * The current parent hierarchy. In most cases, this should be left as nullptr when called externally. + * search_update() will fill this automatically as it needs while calling itself recursively. + * + * @param parent + * + * The current menu to loop over. In most cases, this should be left as nullptr when called externally. + * search_update() will fill this automatically as it needs while calling itself recursively. + */ + void search_update(const QString& s, const QString &p = nullptr, QMenu *parent = nullptr); + + /** + * @brief Perform the currently selected action + * + * Usually triggered by pressing Enter on the ActionSearchEntry field, this will trigger whatever action is currently + * highlighted and then close this popup. If no entries are highlighted (i.e. the list is empty), no action is + * triggered and the popup closes anyway. + */ + void perform_action(); + + /** + * @brief Move selection up + * + * A slot for pressing up on the ActionSearchEntry field. Moves the selection in the list up once. If the + * selection is already at the top of the list, this is a no-op. + */ + void move_selection_up(); + + /** + * @brief Move selection down + * + * A slot for pressing down on the ActionSearchEntry field. Moves the selection in the list down once. If the + * selection is already at the bottom of the list, this is a no-op. + */ + void move_selection_down(); private: - ActionSearchList* list_widget; + /** + * @brief Main widget that shows the list of commands + */ + ActionSearchList* list_widget; }; -class ActionSearchEntry : public QLineEdit { - Q_OBJECT +/** + * @brief The ActionSearchList class + * + * Simple wrapper around QListWidget that emits a signal when an item is double clicked that ActionSearch connects + * to a slot that triggers the currently selected action. + */ +class ActionSearchList : public QListWidget { + Q_OBJECT public: - ActionSearchEntry(QWidget* parent); + /** + * @brief ActionSearchList Constructor + * @param parent + * + * Usually ActionSearch. + */ + ActionSearchList(QWidget* parent); protected: - void keyPressEvent(QKeyEvent * event); + /** + * @brief Override of QListWidget's double click event that emits a signal. + */ + void mouseDoubleClickEvent(QMouseEvent *); signals: - void moveSelectionUp(); - void moveSelectionDown(); + /** + * @brief Signal emitted when a QListWidget item is double clicked. + */ + void dbl_click(); +}; + +/** + * @brief The ActionSearchEntry class + * + * Simple wrapper around QLineEdit that emits signals when the up or down arrow keys are pressed so that ActionSearch + * can connect them to moving the current selection up or down. + */ +class ActionSearchEntry : public QLineEdit { + Q_OBJECT +public: + /** + * @brief ActionSearchEntry + * @param parent + * + * Usually ActionSearch. + */ + ActionSearchEntry(QWidget* parent); +protected: + /** + * @brief Override of QLineEdit's key press event that listens for up/down key presses. + * @param event + */ + void keyPressEvent(QKeyEvent * event); +signals: + /** + * @brief Emitted when the user presses the up arrow key. + */ + void moveSelectionUp(); + + /** + * @brief Emitted when the user presses the down arrow key. + */ + void moveSelectionDown(); }; #endif // ACTIONSEARCH_H diff --git a/dialogs/clippropertiesdialog.cpp b/dialogs/clippropertiesdialog.cpp index a767e5dc6..545ec7ca6 100644 --- a/dialogs/clippropertiesdialog.cpp +++ b/dialogs/clippropertiesdialog.cpp @@ -94,16 +94,23 @@ void ClipPropertiesDialog::accept() for (int i=0;iname()) { ca->append(new RenameClipCommand(clip, clip_name)); } + // If the user entered a clip duration (and the duration has changed), create a "clip move" command if (!qIsNaN(clip_duration)) { - clip->move(ca, - clip->timeline_in(), - clip->timeline_in() + qRound(clip_duration), - clip->clip_in(), - clip->track()); + long clip_duration_rounded = qRound(clip_duration); + + if (clip->length() != clip_duration_rounded) { + clip->move(ca, + clip->timeline_in(), + clip->timeline_in() + clip_duration_rounded, + clip->clip_in(), + clip->track()); + } + } } diff --git a/dialogs/clippropertiesdialog.h b/dialogs/clippropertiesdialog.h index 8fd830978..dc169a597 100644 --- a/dialogs/clippropertiesdialog.h +++ b/dialogs/clippropertiesdialog.h @@ -7,16 +7,44 @@ #include "timeline/clip.h" #include "ui/labelslider.h" +/** + * @brief The ClipPropertiesDialog class + * + * A dialog for setting Clip properties, accessible by right clicking a Clip and clicking "Properties". + */ class ClipPropertiesDialog : public QDialog { Q_OBJECT public: + /** + * @brief ClipPropertiesDialog Constructor + * @param parent + * + * Parent widget. + * + * @param clips + * + * Array of Clip objects to set the properties of. + */ ClipPropertiesDialog(QWidget* parent, QVector clips); protected: + /** + * @brief Accept override. Saves the current properties to the array of Clips. + */ virtual void accept() override; private: + /** + * @brief Internal clip array (set in the constructor) + */ QVector clips_; + /** + * @brief Widget for setting the Clip names + */ QLineEdit* clip_name_field_; + + /** + * @brief Widget for setting the Clip durations + */ LabelSlider* duration_field_; }; diff --git a/dialogs/debugdialog.h b/dialogs/debugdialog.h index 3f9d84d75..a1b6aab3c 100644 --- a/dialogs/debugdialog.h +++ b/dialogs/debugdialog.h @@ -24,22 +24,52 @@ #include #include +/** + * @brief The DebugDialog class + * + * A dialog to display the current debug output. + */ class DebugDialog : public QDialog { - Q_OBJECT + Q_OBJECT public: - DebugDialog(QWidget* parent = 0); + /** + * @brief DebugDialog Constructor + * @param parent + * + * Parent widget. Usually MainWindow. + */ + DebugDialog(QWidget* parent = nullptr); + + /** + * @brief Retranslate window title + * + * Sets title based on the current translation. + */ void Retranslate(); public slots: - void update_log(); + /** + * @brief Update the visual log with the debug text from get_debug_str() + */ + void update_log(); protected: + /** + * @brief Overrides change event to trigger Retranslate() on a LanguageChange event. + */ virtual void changeEvent(QEvent* e) override; + /** + * @brief Overrides show event to trigger an update of the visual log (the visual log does not update while the + * debug dialog is hidden). + */ virtual void showEvent(QShowEvent* event) override; private: - QTextEdit* textEdit; + /** + * @brief Display widget for the debug dialog. + */ + QTextEdit* textEdit; }; namespace olive { - extern DebugDialog* DebugDialog; +extern DebugDialog* DebugDialog; } #endif // DEBUGDIALOG_H diff --git a/dialogs/demonotice.cpp b/dialogs/demonotice.cpp index 3f2cc9169..e91570131 100644 --- a/dialogs/demonotice.cpp +++ b/dialogs/demonotice.cpp @@ -25,38 +25,38 @@ #include DemoNotice::DemoNotice(QWidget *parent) : - QDialog(parent) + QDialog(parent) { - setWindowTitle(tr("Welcome to Olive!")); + setWindowTitle(tr("Welcome to Olive!")); - QVBoxLayout* vlayout = new QVBoxLayout(this); + QVBoxLayout* vlayout = new QVBoxLayout(this); - QHBoxLayout* layout = new QHBoxLayout(); - layout->setMargin(10); - layout->setSpacing(20); + QHBoxLayout* layout = new QHBoxLayout(); + layout->setMargin(10); + layout->setSpacing(20); - QLabel* icon = new QLabel("" - "

" - "", this); - layout->addWidget(icon); + QLabel* icon = new QLabel("" + "

" + "", this); + layout->addWidget(icon); - QLabel* text = new QLabel("

" - "" - + tr("Welcome to Olive!") - + "

" - + tr("Olive is a free open-source video editor released under the GNU GPL. If you have paid for this software, you have been scammed.") - + "

" - + tr("This software is currently in ALPHA which means it is unstable and very likely to crash, have bugs, and have missing features. We offer no warranty so use at your own risk. Please report any bugs or feature requests at %1").arg("www.olivevideoeditor.org") - + "

" - + tr("Thank you for trying Olive and we hope you enjoy it!") - + "

", this); - text->setWordWrap(true); - layout->addWidget(text); + QLabel* text = new QLabel("

" + "" + + tr("Welcome to Olive!") + + "

" + + tr("Olive is a free open-source video editor released under the GNU GPL. If you have paid for this software, you have been scammed.") + + "

" + + tr("This software is currently in ALPHA which means it is unstable and very likely to crash, have bugs, and have missing features. We offer no warranty so use at your own risk. Please report any bugs or feature requests at %1").arg("www.olivevideoeditor.org") + + "

" + + tr("Thank you for trying Olive and we hope you enjoy it!") + + "

", this); + text->setWordWrap(true); + layout->addWidget(text); - vlayout->addLayout(layout); + vlayout->addLayout(layout); - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok, this); - buttons->setCenterButtons(true); - connect(buttons, SIGNAL(accepted()), this, SLOT(accept())); - vlayout->addWidget(buttons); + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok, this); + buttons->setCenterButtons(true); + connect(buttons, SIGNAL(accepted()), this, SLOT(accept())); + vlayout->addWidget(buttons); } diff --git a/dialogs/demonotice.h b/dialogs/demonotice.h index b50a690fe..a986e51ab 100644 --- a/dialogs/demonotice.h +++ b/dialogs/demonotice.h @@ -23,11 +23,22 @@ #include +/** + * @brief The DemoNotice class + * + * Simple dialog shown on startup to introduce Olive as alpha software (in release builds). + */ class DemoNotice : public QDialog { - Q_OBJECT + Q_OBJECT public: - explicit DemoNotice(QWidget *parent = 0); + /** + * @brief DemoNotice Constructor + * @param parent + * + * QWidget parent. Usually MainWindow. + */ + explicit DemoNotice(QWidget *parent = nullptr); }; #endif // DEMONOTICE_H diff --git a/dialogs/exportdialog.cpp b/dialogs/exportdialog.cpp index be924d870..73dc3ec65 100644 --- a/dialogs/exportdialog.cpp +++ b/dialogs/exportdialog.cpp @@ -20,6 +20,10 @@ #include "exportdialog.h" +extern "C" { +#include +} + #include #include #include @@ -39,10 +43,6 @@ #include "rendering/exportthread.h" #include "ui/mainwindow.h" -extern "C" { -#include -} - enum ExportFormats { FORMAT_3GPP, FORMAT_AIFF, @@ -118,9 +118,6 @@ ExportDialog::ExportDialog(QWidget *parent) : vcodec_params.threads = 0; } -ExportDialog::~ExportDialog() -{} - void ExportDialog::add_codec_to_combobox(QComboBox* box, enum AVCodecID codec) { QString codec_name; @@ -334,21 +331,42 @@ void ExportDialog::format_changed(int index) { } void ExportDialog::render_thread_finished() { - if (progressBar->value() < 100 && !cancelled) { + // Determine if the export succeeded + bool succeeded = (progressBar->value() == 100); + + // If it failed and we didn't cancel it, it must have errored out. Show an error message. + if (!succeeded && !et->WasInterrupted()) { QMessageBox::critical( this, tr("Export Failed"), - tr("Export failed - %1").arg(export_error), + tr("Export failed - %1").arg(et->GetError()), QMessageBox::Ok ); } + + // Clear audio buffer clear_audio_ibuffer(); + + // Re-enable/disable UI widgets based on the rendering state prep_ui_for_render(false); + + // Move OpenGL context back to the sequence viewer panel_sequence_viewer->viewer_widget->makeCurrent(); panel_sequence_viewer->viewer_widget->initializeGL(); + + // Update the application UI update_ui(false); + + // Disconnect cancel button from export thread + disconnect(renderCancel, SIGNAL(clicked(bool)), et, SLOT(Interrupt())); + + // Free the export thread et->deleteLater(); - if (progressBar->value() == 100) accept(); + + // If the export succeeded, close the dialog + if (succeeded) { + accept(); + } } void ExportDialog::prep_ui_for_render(bool r) { @@ -543,7 +561,8 @@ void ExportDialog::export_action() { et = new ExportThread(params, vcodec_params, this); connect(et, SIGNAL(finished()), this, SLOT(render_thread_finished())); - connect(et, SIGNAL(progress_changed(int, qint64)), this, SLOT(update_progress_bar(int, qint64))); + connect(et, SIGNAL(ProgressChanged(int, qint64)), this, SLOT(update_progress_bar(int, qint64))); + connect(renderCancel, SIGNAL(clicked(bool)), et, SLOT(Interrupt())); close_active_clips(olive::ActiveSequence.get()); @@ -553,8 +572,6 @@ void ExportDialog::export_action() { prep_ui_for_render(true); - cancelled = false; - total_export_time_start = QDateTime::currentMSecsSinceEpoch(); et->start(); @@ -587,11 +604,6 @@ void ExportDialog::update_progress_bar(int value, qint64 remaining_ms) { progressBar->setValue(value); } -void ExportDialog::cancel_render() { - et->continueEncode = false; - cancelled = true; -} - void ExportDialog::vcodec_changed(int index) { compressionTypeCombobox->clear(); @@ -755,7 +767,6 @@ void ExportDialog::setup_ui() { renderCancel = new QPushButton(this); renderCancel->setIcon(QIcon(":/icons/error.svg")); renderCancel->setEnabled(false); - connect(renderCancel, SIGNAL(clicked(bool)), this, SLOT(cancel_render())); progressLayout->addWidget(renderCancel); verticalLayout->addLayout(progressLayout); diff --git a/dialogs/exportdialog.h b/dialogs/exportdialog.h index 909912c55..df3f14c21 100644 --- a/dialogs/exportdialog.h +++ b/dialogs/exportdialog.h @@ -30,34 +30,41 @@ #include #include "timeline/sequence.h" - #include "rendering/exportthread.h" +/** + * @brief The ExportDialog class + * + * The dialog to initiate an export. + */ class ExportDialog : public QDialog { Q_OBJECT public: - explicit ExportDialog(QWidget *parent = nullptr); - ~ExportDialog(); - QString export_error; + /** + * @brief ExportDialog Constructor + * + * @param parent + * + * QWidget parent. Usually MainWindow. + */ + explicit ExportDialog(QWidget *parent); private slots: void format_changed(int index); void export_action(); void update_progress_bar(int value, qint64 remaining_ms); - void cancel_render(); void render_thread_finished(); void vcodec_changed(int index); void comp_type_changed(int index); void open_advanced_video_dialog(); private: - QVector format_strings; void setup_ui(); - - ExportThread* et; void prep_ui_for_render(bool r); - bool cancelled; + + QVector format_strings; + ExportThread* et; void add_codec_to_combobox(QComboBox* box, enum AVCodecID codec); diff --git a/effects/effect.cpp b/effects/effect.cpp index 5690000a9..ec17cb84d 100644 --- a/effects/effect.cpp +++ b/effects/effect.cpp @@ -44,7 +44,6 @@ #include "panels/timeline.h" #include "panels/effectcontrols.h" #include "panels/grapheditor.h" -#include "ui/checkboxex.h" #include "global/debug.h" #include "global/path.h" #include "ui/mainwindow.h" diff --git a/effects/effect.h b/effects/effect.h index acf5e255b..b9571ea2f 100644 --- a/effects/effect.h +++ b/effects/effect.h @@ -41,7 +41,6 @@ #include #include "ui/collapsiblewidget.h" -#include "ui/checkboxex.h" #include "effectrow.h" #include "effectgizmo.h" diff --git a/effects/effectfield.cpp b/effects/effectfield.cpp index 86886d3ad..e09723088 100644 --- a/effects/effectfield.cpp +++ b/effects/effectfield.cpp @@ -23,14 +23,6 @@ #include #include -#include "ui/labelslider.h" -#include "ui/colorbutton.h" -#include "ui/texteditex.h" -#include "ui/checkboxex.h" -#include "ui/comboboxex.h" -#include "ui/fontcombobox.h" -#include "ui/embeddedfilechooser.h" - #include "rendering/renderfunctions.h" #include "global/config.h" diff --git a/effects/fields/boolfield.h b/effects/fields/boolfield.h index 5af612530..8a7ce1f6f 100644 --- a/effects/fields/boolfield.h +++ b/effects/fields/boolfield.h @@ -3,6 +3,11 @@ #include "../effectfield.h" +/** + * @brief The BoolField class + * + * An EffectField derivative the uses boolean values (true or false) and uses a checkbox as its visual representation. + */ class BoolField : public EffectField { Q_OBJECT diff --git a/olive.pro b/olive.pro index 95509fe66..3fe5ca515 100644 --- a/olive.pro +++ b/olive.pro @@ -85,7 +85,6 @@ SOURCES += \ ui/colorbutton.cpp \ dialogs/replaceclipmediadialog.cpp \ ui/fontcombobox.cpp \ - ui/checkboxex.cpp \ ui/keyframeview.cpp \ ui/texteditex.cpp \ dialogs/demonotice.cpp \ @@ -210,7 +209,6 @@ HEADERS += \ ui/colorbutton.h \ dialogs/replaceclipmediadialog.h \ ui/fontcombobox.h \ - ui/checkboxex.h \ ui/keyframeview.h \ ui/texteditex.h \ dialogs/demonotice.h \ diff --git a/rendering/cacher.cpp b/rendering/cacher.cpp index 8d23fe99b..1fc51e93b 100644 --- a/rendering/cacher.cpp +++ b/rendering/cacher.cpp @@ -1146,8 +1146,6 @@ void Cacher::CloseWorker() { } } - clip->reset(); - qInfo() << "Clip closed on track" << clip->track(); } diff --git a/rendering/exportthread.cpp b/rendering/exportthread.cpp index 3c50776ed..6e389c607 100644 --- a/rendering/exportthread.cpp +++ b/rendering/exportthread.cpp @@ -44,35 +44,34 @@ extern "C" { #include #include -ExportThread::ExportThread(const ExportParams &iparams, - const VideoCodecParams& ivparams, +ExportThread::ExportThread(const ExportParams ¶ms, + const VideoCodecParams& vparams, QObject *parent) : - QThread(parent) + QThread(parent), + params_(params), + vcodec_params_(vparams), + interrupt_(false), + fmt_ctx(nullptr), + video_stream(nullptr), + vcodec(nullptr), + vcodec_ctx(nullptr), + video_frame(nullptr), + sws_ctx(nullptr), + audio_stream(nullptr), + acodec(nullptr), + audio_frame(nullptr), + swr_frame(nullptr), + acodec_ctx(nullptr), + swr_ctx(nullptr), + vpkt_alloc(false), + apkt_alloc(false), + c_filename(nullptr) { - params = iparams; - vcodec_params = ivparams; - continueEncode = true; - + // Create offscreen surface for rendering while exporting surface.create(); - - fmt_ctx = nullptr; - video_stream = nullptr; - vcodec = nullptr; - vcodec_ctx = nullptr; - video_frame = nullptr; - sws_ctx = nullptr; - audio_stream = nullptr; - acodec = nullptr; - audio_frame = nullptr; - swr_frame = nullptr; - acodec_ctx = nullptr; - swr_ctx = nullptr; - - vpkt_alloc = false; - apkt_alloc = false; } -bool ExportThread::encode(AVFormatContext* ofmt_ctx, AVCodecContext* codec_ctx, AVFrame* frame, AVPacket* packet, AVStream* stream, bool rescale) { +bool ExportThread::Encode(AVFormatContext* ofmt_ctx, AVCodecContext* codec_ctx, AVFrame* frame, AVPacket* packet, AVStream* stream, bool rescale) { ret = avcodec_send_frame(codec_ctx, frame); if (ret < 0) { qCritical() << "Failed to send frame to encoder." << ret; @@ -100,15 +99,15 @@ bool ExportThread::encode(AVFormatContext* ofmt_ctx, AVCodecContext* codec_ctx, return true; } -bool ExportThread::setupVideo() { +bool ExportThread::SetupVideo() { // if video is disabled, no setup necessary - if (!params.video_enabled) return true; + if (!params_.video_enabled) return true; // find video encoder - vcodec = avcodec_find_encoder(static_cast(params.video_codec)); + vcodec = avcodec_find_encoder(static_cast(params_.video_codec)); if (!vcodec) { qCritical() << "Could not find video encoder"; - export_error = tr("could not video encoder for %1").arg(QString::number(params.video_codec)); + export_error = tr("could not video encoder for %1").arg(QString::number(params_.video_codec)); return false; } @@ -131,15 +130,15 @@ bool ExportThread::setupVideo() { } // setup context - vcodec_ctx->codec_id = static_cast(params.video_codec); + vcodec_ctx->codec_id = static_cast(params_.video_codec); vcodec_ctx->codec_type = AVMEDIA_TYPE_VIDEO; - vcodec_ctx->width = params.video_width; - vcodec_ctx->height = params.video_height; + vcodec_ctx->width = params_.video_width; + vcodec_ctx->height = params_.video_height; vcodec_ctx->sample_aspect_ratio = {1, 1}; - vcodec_ctx->pix_fmt = static_cast(vcodec_params.pix_fmt); - vcodec_ctx->framerate = av_d2q(params.video_frame_rate, INT_MAX); - if (params.video_compression_type == COMPRESSION_TYPE_CBR) { - vcodec_ctx->bit_rate = qRound(params.video_bitrate * 1000000); + vcodec_ctx->pix_fmt = static_cast(vcodec_params_.pix_fmt); + vcodec_ctx->framerate = av_d2q(params_.video_frame_rate, INT_MAX); + if (params_.video_compression_type == COMPRESSION_TYPE_CBR) { + vcodec_ctx->bit_rate = qRound(params_.video_bitrate * 1000000); } vcodec_ctx->time_base = av_inv_q(vcodec_ctx->framerate); video_stream->time_base = vcodec_ctx->time_base; @@ -148,27 +147,30 @@ bool ExportThread::setupVideo() { vcodec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; } + // Some codecs require special settings so we set that up here switch (vcodec_ctx->codec_id) { /// H.264 specific settings case AV_CODEC_ID_H264: case AV_CODEC_ID_H265: - switch (params.video_compression_type) { + switch (params_.video_compression_type) { case COMPRESSION_TYPE_CFR: - av_opt_set(vcodec_ctx->priv_data, "crf", QString::number(static_cast(params.video_bitrate)).toUtf8(), AV_OPT_SEARCH_CHILDREN); + av_opt_set(vcodec_ctx->priv_data, "crf", QString::number(static_cast(params_.video_bitrate)).toUtf8(), AV_OPT_SEARCH_CHILDREN); break; } break; } + // Set export to be multithreaded AVDictionary* opts = nullptr; - if (vcodec_params.threads == 0) { + if (vcodec_params_.threads == 0) { av_dict_set(&opts, "threads", "auto", 0); } else { - av_dict_set(&opts, "threads", QString::number(vcodec_params.threads).toUtf8(), 0); + av_dict_set(&opts, "threads", QString::number(vcodec_params_.threads).toUtf8(), 0); } + // Open video encoder ret = avcodec_open2(vcodec_ctx, vcodec, &opts); if (ret < 0) { qCritical() << "Could not open output video encoder." << ret; @@ -176,7 +178,7 @@ bool ExportThread::setupVideo() { return false; } - // copy video encoder parameters to output stream + // Copy video encoder parameters to output stream ret = avcodec_parameters_from_context(video_stream->codecpar, vcodec_ctx); if (ret < 0) { qCritical() << "Could not copy video encoder parameters to output stream." << ret; @@ -184,7 +186,7 @@ bool ExportThread::setupVideo() { return false; } - // create AVFrame + // Create raw AVFrame that will contain the RGBA buffer straight from compositing video_frame = av_frame_alloc(); av_frame_make_writable(video_frame); video_frame->format = AV_PIX_FMT_RGBA; @@ -194,14 +196,15 @@ bool ExportThread::setupVideo() { av_init_packet(&video_pkt); + // Set up conversion context sws_ctx = sws_getContext( olive::ActiveSequence->width, olive::ActiveSequence->height, AV_PIX_FMT_RGBA, - params.video_width, - params.video_height, + params_.video_width, + params_.video_height, vcodec_ctx->pix_fmt, - SWS_FAST_BILINEAR, + SWS_BILINEAR, nullptr, nullptr, nullptr @@ -210,29 +213,31 @@ bool ExportThread::setupVideo() { return true; } -bool ExportThread::setupAudio() { - // if audio is disabled, no setup necessary - if (!params.audio_enabled) return true; +bool ExportThread::SetupAudio() { - // find encoder - acodec = avcodec_find_encoder(static_cast(params.audio_codec)); + // Find encoder for this codec + acodec = avcodec_find_encoder(static_cast(params_.audio_codec)); if (!acodec) { qCritical() << "Could not find audio encoder"; - export_error = tr("could not audio encoder for %1").arg(QString::number(params.audio_codec)); + export_error = tr("could not audio encoder for %1").arg(QString::number(params_.audio_codec)); return false; } - // allocate audio stream + // Allocate audio stream audio_stream = avformat_new_stream(fmt_ctx, acodec); - audio_stream->id = 1; - if (!audio_stream) { + if (audio_stream == nullptr) { qCritical() << "Could not allocate audio stream"; export_error = tr("could not allocate audio stream"); return false; } - // allocate context - // acodec_ctx = audio_stream->codec; + // Set audio stream's ID to 1 + audio_stream->id = 1; + + // set sample rate to use for project + audio_rendering_rate = params_.audio_sampling_rate; + + // Allocate encoding context acodec_ctx = avcodec_alloc_context3(acodec); if (!acodec_ctx) { qCritical() << "Could not find allocate audio encoding context"; @@ -240,27 +245,24 @@ bool ExportThread::setupAudio() { return false; } - // set sample rate to use for project - audio_rendering_rate = params.audio_sampling_rate; - - // setup context - acodec_ctx->codec_id = static_cast(params.audio_codec); + // Set up encoding context + acodec_ctx->codec_id = static_cast(params_.audio_codec); acodec_ctx->codec_type = AVMEDIA_TYPE_AUDIO; - acodec_ctx->sample_rate = params.audio_sampling_rate; + acodec_ctx->sample_rate = params_.audio_sampling_rate; acodec_ctx->channel_layout = AV_CH_LAYOUT_STEREO; // change this to support surround/mono sound in the future (this is what the user sets the output audio to) acodec_ctx->channels = av_get_channel_layout_nb_channels(acodec_ctx->channel_layout); acodec_ctx->sample_fmt = acodec->sample_fmts[0]; - acodec_ctx->bit_rate = params.audio_bitrate * 1000; + acodec_ctx->bit_rate = params_.audio_bitrate * 1000; acodec_ctx->time_base.num = 1; - acodec_ctx->time_base.den = params.audio_sampling_rate; + acodec_ctx->time_base.den = params_.audio_sampling_rate; audio_stream->time_base = acodec_ctx->time_base; if (fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) { acodec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; } - // open encoder + // Open encoder ret = avcodec_open2(acodec_ctx, acodec, nullptr); if (ret < 0) { qCritical() << "Could not open output audio encoder." << ret; @@ -268,7 +270,7 @@ bool ExportThread::setupAudio() { return false; } - // copy params to output stream + // Copy paramters from the codec context (set up above) to the output stream ret = avcodec_parameters_from_context(audio_stream->codecpar, acodec_ctx); if (ret < 0) { qCritical() << "Could not copy audio encoder parameters to output stream." << ret; @@ -300,8 +302,10 @@ bool ExportThread::setupAudio() { audio_frame->nb_samples = 256; } + // TODO change this to support surround/mono sound in the future (this is whatever format they're held in the internal buffer) + audio_frame->channel_layout = AV_CH_LAYOUT_STEREO; + audio_frame->format = AV_SAMPLE_FMT_S16; - audio_frame->channel_layout = AV_CH_LAYOUT_STEREO; // change this to support surround/mono sound in the future (this is whatever format they're held in the internal buffer) audio_frame->channels = av_get_channel_layout_nb_channels(audio_frame->channel_layout); av_frame_make_writable(audio_frame); ret = av_frame_get_buffer(audio_frame, 0); @@ -328,18 +332,25 @@ bool ExportThread::setupAudio() { return true; } -bool ExportThread::setupContainer() { +bool ExportThread::SetupContainer() { + + // Set up output context (using the filename as the format specification) + avformat_alloc_output_context2(&fmt_ctx, nullptr, nullptr, c_filename); - if (!fmt_ctx) { + if (fmt_ctx == nullptr) { + + // Failed to create the output format context. Exit the export and throw an error. + qCritical() << "Could not create output context"; export_error = tr("could not create output format context"); return false; } - //av_dump_format(fmt_ctx, 0, c_filename, 1); - ret = avio_open(&fmt_ctx->pb, c_filename, AVIO_FLAG_WRITE); if (ret < 0) { + + // Failed to get a valid write handle for the exported file. Exit the export and throw an error. + qCritical() << "Could not open output file." << ret; export_error = tr("could not open output file (%1)").arg(QString::number(ret)); return false; @@ -348,60 +359,98 @@ bool ExportThread::setupContainer() { return true; } -void ExportThread::run() { - panel_sequence_viewer->pause(); - panel_sequence_viewer->seek(params.start_frame); - - // copy filename - QByteArray ba = params.filename.toUtf8(); +void ExportThread::Export() +{ + // Copy filename from QString to const char + QByteArray ba = params_.filename.toUtf8(); c_filename = new char[ba.size()+1]; strcpy(c_filename, ba.data()); - continueEncode = setupContainer(); - - if (params.video_enabled && continueEncode) continueEncode = setupVideo(); - - if (params.audio_enabled && continueEncode) continueEncode = setupAudio(); - - if (continueEncode) { - ret = avformat_write_header(fmt_ctx, nullptr); - if (ret < 0) { - qCritical() << "Could not write output file header." << ret; - export_error = tr("could not write output file header (%1)").arg(QString::number(ret)); - continueEncode = false; - } + // Set up file container + if (!SetupContainer()) { + return; } + // If video is enabled, set it up in the container now + if (params_.video_enabled && !SetupVideo()) { + return; + } + + // If audio is enabled, set it up in the container now + if (params_.audio_enabled && !SetupAudio()) { + return; + } + + // Write the container header based on what's been set up above + ret = avformat_write_header(fmt_ctx, nullptr); + if (ret < 0) { + + // FFmpeg failed to write the header, so cancel the export and throw an error + + qCritical() << "Could not write output file header." << ret; + export_error = tr("could not write output file header (%1)").arg(QString::number(ret)); + + return; + } + + // Count audio samples in file (used for calculating PTS) long file_audio_samples = 0; - qint64 start_time, frame_time, avg_time, eta, total_time = 0; + + // Set up timing variables, used for determining rendering ETA + qint64 frame_start_time, frame_time, avg_time, eta, total_time = 0; + + // Frame counters - used for generating encoding statistics (e.g. average frame time, ETA, etc.) long remaining_frames, frame_count = 1; + // Use Sequence Viewer's render thread - TODO separate this into a new render thread for background rendering RenderThread* renderer = panel_sequence_viewer->viewer_widget->get_renderer(); + + // Override connection from RenderThread disconnect(renderer, SIGNAL(ready()), panel_sequence_viewer->viewer_widget, SLOT(queue_repaint())); connect(renderer, SIGNAL(ready()), this, SLOT(wake())); + // Lock mutex (used for synchronization with RenderThread) mutex.lock(); - while (olive::ActiveSequence->playhead <= params.end_frame && continueEncode) { - start_time = QDateTime::currentMSecsSinceEpoch(); + // Loop from now (set to the beginning frame earlier) to the end of the frame + while (olive::ActiveSequence->playhead <= params_.end_frame && !interrupt_) { - if (params.audio_enabled) { - compose_audio(nullptr, olive::ActiveSequence.get(), 1, true); + // Start timing how long this frame will take + frame_start_time = QDateTime::currentMSecsSinceEpoch(); + + // If we're exporting audio, run compose_audio() which will write mixed audio to the internal audio buffer + if (params_.audio_enabled) { + olive::rendering::compose_audio(nullptr, olive::ActiveSequence.get(), 1, true); } - if (params.video_enabled) { + + // If we're exporting video, trigger a render on the RenderThread + if (params_.video_enabled) { do { // TODO optimize by rendering the next frame while encoding the last renderer->start_render(nullptr, olive::ActiveSequence.get(), 1, nullptr, video_frame->data[0], video_frame->linesize[0]/4); + + // Wait for RenderThread to return waitCond.wait(&mutex); - if (!continueEncode) break; + + if (interrupt_) { + return; + } + + // If the RenderThread failed, do another render } while (renderer->did_texture_fail()); - if (!continueEncode) break; + + if (interrupt_) { + return; + } + } - // encode last frame while rendering next frame - double timecode_secs = double(olive::ActiveSequence->playhead - params.start_frame) / olive::ActiveSequence->frame_rate; - if (params.video_enabled) { - // create sws_frame for converting pixel format + // Get the current sequence playhead in seconds (used for timestamp calculations later on) + double timecode_secs = double(olive::ActiveSequence->playhead - params_.start_frame) / olive::ActiveSequence->frame_rate; + + // If we're exporting video, construct an AVFrame in the destination codec's pixel format to convert the raw RGBA + // OpenGL buffer to + if (params_.video_enabled) { // // - I'm not sure why, but we have to alloc/free sws_frame every frame, or it breaks GIF exporting. @@ -411,146 +460,222 @@ void ExportThread::run() { // - Anyway, here we are. // + // Construct destination pixel format frame sws_frame = av_frame_alloc(); sws_frame->format = vcodec_ctx->pix_fmt; - sws_frame->width = params.video_width; - sws_frame->height = params.video_height; + sws_frame->width = params_.video_width; + sws_frame->height = params_.video_height; av_frame_get_buffer(sws_frame, 0); - // convert pixel format to format expected by the encoder + // Convert raw RGBA buffer to format expected by the encoder sws_scale(sws_ctx, video_frame->data, video_frame->linesize, 0, video_frame->height, sws_frame->data, sws_frame->linesize); sws_frame->pts = qRound(timecode_secs/av_q2d(video_stream->time_base)); - // send converted frame to encoder - if (!encode(fmt_ctx, vcodec_ctx, sws_frame, &video_pkt, video_stream, false)) continueEncode = false; + // Send frame to encoder + if (!Encode(fmt_ctx, vcodec_ctx, sws_frame, &video_pkt, video_stream, false)) { + return; + } av_frame_free(&sws_frame); + sws_frame = nullptr; } - if (params.audio_enabled) { - // do we need to encode more audio samples? - while (continueEncode && file_audio_samples <= (timecode_secs*params.audio_sampling_rate)) { + // If we're exporting audio, copy audio from the buffer into an AVFrame for encoding + if (params_.audio_enabled) { - // copy samples from audio buffer to AVFrame + // Check if the count of encoded samples exceeds the current Sequence playhead, in which case we don't need to + // encode any audio at this moment + while (!interrupt_ && file_audio_samples <= (timecode_secs*params_.audio_sampling_rate)) { + + // Copy samples from audio buffer to AVFrame int adjusted_read = audio_ibuffer_read%audio_ibuffer_size; int copylen = qMin(aframe_bytes, audio_ibuffer_size-adjusted_read); memcpy(audio_frame->data[0], audio_ibuffer+adjusted_read, copylen); memset(audio_ibuffer+adjusted_read, 0, copylen); audio_ibuffer_read += copylen; + // If we reached the end of the buffer without reaching the end of the frame, do another copy from the start + // of the buffer if (copylen < aframe_bytes) { - // copy remainder int remainder_len = aframe_bytes-copylen; memcpy(audio_frame->data[0]+copylen, audio_ibuffer, remainder_len); memset(audio_ibuffer, 0, remainder_len); audio_ibuffer_read += remainder_len; } - // convert to export sample format + // Convert raw audio samples to the destination codec's sample format swr_convert_frame(swr_ctx, swr_frame, audio_frame); + // The timestamp is set to the current count of audio samples (since the audio stream's timebase is swr_frame->pts = file_audio_samples; - // send to encoder - if (!encode(fmt_ctx, acodec_ctx, swr_frame, &audio_pkt, audio_stream, true)) continueEncode = false; + // Send frame to encoder + if (!Encode(fmt_ctx, acodec_ctx, swr_frame, &audio_pkt, audio_stream, true)) { + return; + } + // Increment by the frame's number of samples file_audio_samples += swr_frame->nb_samples; } } - // generating encoding statistics (time it took to encode this frame/estimated remaining time) - frame_time = (QDateTime::currentMSecsSinceEpoch()-start_time); + // Generating encoding statistics (e.g. the time it took to encode this frame/estimated remaining time) + frame_time = (QDateTime::currentMSecsSinceEpoch()-frame_start_time); total_time += frame_time; - remaining_frames = (params.end_frame - olive::ActiveSequence->playhead); + remaining_frames = (params_.end_frame - olive::ActiveSequence->playhead); avg_time = (total_time/frame_count); eta = (remaining_frames*avg_time); - emit progress_changed(qRound((double(olive::ActiveSequence->playhead - params.start_frame) / double(params.end_frame - params.start_frame)) * 100.0), eta); + // Emit a signal for the percent of the sequence that's been encoded so far + emit ProgressChanged(qRound((double(olive::ActiveSequence->playhead - params_.start_frame) / double(params_.end_frame - params_.start_frame)) * 100.0), eta); + + // Increment sequence playhead olive::ActiveSequence->playhead++; + + // Increment frame count (used for generating encoding statistics above) frame_count++; } + // Restore original connection from RenderThread disconnect(renderer, SIGNAL(ready()), this, SLOT(wake())); connect(renderer, SIGNAL(ready()), panel_sequence_viewer->viewer_widget, SLOT(queue_repaint())); mutex.unlock(); - if (continueEncode) { - if (params.video_enabled) vpkt_alloc = true; - if (params.audio_enabled) apkt_alloc = true; + if (interrupt_) { + return; } + if (params_.video_enabled) vpkt_alloc = true; + if (params_.audio_enabled) apkt_alloc = true; + olive::Global->set_rendering_state(false); - if (params.audio_enabled && continueEncode) { + // If audio is enabled, flush the rest of the audio out of swresample + if (params_.audio_enabled) { - // flush swresample do { swr_convert_frame(swr_ctx, swr_frame, nullptr); if (swr_frame->nb_samples == 0) break; swr_frame->pts = file_audio_samples; - if (!encode(fmt_ctx, acodec_ctx, swr_frame, &audio_pkt, audio_stream, true)) continueEncode = false; + if (!Encode(fmt_ctx, acodec_ctx, swr_frame, &audio_pkt, audio_stream, true)) { + return; + } file_audio_samples += swr_frame->nb_samples; } while (swr_frame->nb_samples > 0); } - bool continueVideo = true; - bool continueAudio = true; - if (continueEncode) { - // flush remaining packets - while (continueVideo && continueAudio) { - if (continueVideo && params.video_enabled) continueVideo = encode(fmt_ctx, vcodec_ctx, nullptr, &video_pkt, video_stream, false); - if (continueAudio && params.audio_enabled) continueAudio = encode(fmt_ctx, acodec_ctx, nullptr, &audio_pkt, audio_stream, true); - } + if (interrupt_) { + return; + } - ret = av_write_trailer(fmt_ctx); - if (ret < 0) { - qCritical() << "Could not write output file trailer." << ret; - export_error = tr("could not write output file trailer (%1)").arg(QString::number(ret)); - continueEncode = false; - } + bool continueVideo = params_.video_enabled; + bool continueAudio = params_.audio_enabled; - if (continueEncode) { - emit progress_changed(100, 0); + // Flush remaining packets out of video and audio encoders + while (continueVideo && continueAudio) { + if (continueVideo) { + continueVideo = Encode(fmt_ctx, vcodec_ctx, nullptr, &video_pkt, video_stream, false); + } + if (continueAudio) { + continueAudio = Encode(fmt_ctx, acodec_ctx, nullptr, &audio_pkt, audio_stream, true); } } - avio_closep(&fmt_ctx->pb); + // Write container trailer + ret = av_write_trailer(fmt_ctx); + if (ret < 0) { + qCritical() << "Could not write output file trailer." << ret; + export_error = tr("could not write output file trailer (%1)").arg(QString::number(ret)); + return; + } - if (vpkt_alloc) av_packet_unref(&video_pkt); - if (video_frame != nullptr) av_frame_free(&video_frame); - if (vcodec_ctx != nullptr) { - avcodec_close(vcodec_ctx); - avcodec_free_context(&vcodec_ctx); + emit ProgressChanged(100, 0); +} + +void ExportThread::Cleanup() +{ + if (fmt_ctx != nullptr) { + avio_closep(&fmt_ctx->pb); + avformat_free_context(fmt_ctx); } - if (apkt_alloc) av_packet_unref(&audio_pkt); - if (audio_frame != nullptr) av_frame_free(&audio_frame); if (acodec_ctx != nullptr) { avcodec_close(acodec_ctx); avcodec_free_context(&acodec_ctx); } - avformat_free_context(fmt_ctx); + if (audio_frame != nullptr) { + av_frame_free(&audio_frame); + } + + if (apkt_alloc) { + av_packet_unref(&audio_pkt); + } + + if (vcodec_ctx != nullptr) { + avcodec_close(vcodec_ctx); + avcodec_free_context(&vcodec_ctx); + } + + if (video_frame != nullptr) { + av_frame_free(&video_frame); + } + + if (vpkt_alloc) { + av_packet_unref(&video_pkt); + } if (sws_ctx != nullptr) { sws_freeContext(sws_ctx); } + if (swr_ctx != nullptr) { - av_frame_free(&swr_frame); swr_free(&swr_ctx); } + if (swr_frame != nullptr) { + av_frame_free(&swr_frame); + } + + if (sws_frame != nullptr) { + av_frame_free(&sws_frame); + } + delete [] c_filename; } -const QString &ExportThread::getError() { +void ExportThread::run() { + // Ensure sequence isn't currently playing + panel_sequence_viewer->pause(); + + // Seek to the first frame we're exporting + panel_sequence_viewer->seek(params_.start_frame); + + // Run export function (which will return if there's a failure) + Export(); + + // Clean up anything that was allocated in Export() (whether it succeeded or not) + Cleanup(); +} + +const QString &ExportThread::GetError() { return export_error; } +bool ExportThread::WasInterrupted() +{ + return interrupt_; +} + +void ExportThread::Interrupt() +{ + interrupt_ = true; +} + void ExportThread::wake() { mutex.lock(); waitCond.wakeAll(); diff --git a/rendering/exportthread.h b/rendering/exportthread.h index ab30f6da8..e4bc03080 100644 --- a/rendering/exportthread.h +++ b/rendering/exportthread.h @@ -72,27 +72,30 @@ struct VideoCodecParams { class ExportThread : public QThread { Q_OBJECT public: - ExportThread(const ExportParams& iparams, const VideoCodecParams& ivparams, QObject* parent = nullptr); - void run(); + ExportThread(const ExportParams& params, const VideoCodecParams& vparams, QObject* parent = nullptr); + virtual void run() override; - const QString& getError(); + const QString& GetError(); + + bool WasInterrupted(); +signals: + void ProgressChanged(int value, qint64 remaining_ms); +public slots: + void Interrupt(); +private: + bool Encode(AVFormatContext* ofmt_ctx, AVCodecContext* codec_ctx, AVFrame* frame, AVPacket* packet, AVStream* stream, bool rescale); + bool SetupVideo(); + bool SetupAudio(); + bool SetupContainer(); + void Export(); + void Cleanup(); QOffscreenSurface surface; - - bool continueEncode; -signals: - void progress_changed(int value, qint64 remaining_ms); -public slots: - void wake(); -private: - bool encode(AVFormatContext* ofmt_ctx, AVCodecContext* codec_ctx, AVFrame* frame, AVPacket* packet, AVStream* stream, bool rescale); - bool setupVideo(); - bool setupAudio(); - bool setupContainer(); + bool interrupt_; // params imported from dialogs - ExportParams params; - VideoCodecParams vcodec_params; + ExportParams params_; + VideoCodecParams vcodec_params_; AVFormatContext* fmt_ctx; AVStream* video_stream; @@ -121,6 +124,8 @@ private: QWaitCondition waitCond; QString export_error; +private slots: + void wake(); }; #endif // EXPORTTHREAD_H diff --git a/rendering/renderfunctions.h b/rendering/renderfunctions.h index abea07c33..c370b0493 100644 --- a/rendering/renderfunctions.h +++ b/rendering/renderfunctions.h @@ -215,6 +215,8 @@ struct ComposeSequenceParams { GLuint ocio_lut_texture; }; +namespace olive { +namespace rendering { /** * @brief Compose a frame of a given sequence * @@ -263,6 +265,8 @@ GLuint compose_sequence(ComposeSequenceParams ¶ms); * Whether to wait for media to open or simply fail if the media is not yet open. This should usually be **FALSE**. */ void compose_audio(Viewer* viewer, Sequence *seq, int playback_speed, bool wait_for_mutexes); +} +} /** * @brief Rescale a frame number between two frame rates diff --git a/rendering/renderthread.cpp b/rendering/renderthread.cpp index 6d92ea422..1fb88308c 100644 --- a/rendering/renderthread.cpp +++ b/rendering/renderthread.cpp @@ -178,7 +178,7 @@ void RenderThread::paint() { glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); - compose_sequence(params); + olive::rendering::compose_sequence(params); // flush changes ctx->functions()->glFinish(); diff --git a/timeline/clip.cpp b/timeline/clip.cpp index 5b4311de6..743587f79 100644 --- a/timeline/clip.cpp +++ b/timeline/clip.cpp @@ -40,26 +40,23 @@ const int kRGBAComponentCount = 4; Clip::Clip(Sequence* s) : sequence(s), - cacher(this) + cacher(this), + enabled_(true), + clip_in_(0), + timeline_in_(0), + timeline_out_(0), + track_(0), + media_(nullptr), + reverse_(false), + autoscale_(olive::CurrentConfig.autoscale_by_default), + opening_transition(nullptr), + closing_transition(nullptr), + undeletable(false), + replaced(false), + fbo(nullptr), + open_(false), + texture(nullptr) { - enabled_ = true; - clip_in_ = 0; - timeline_in_ = 0; - timeline_out_ = 0; - track_ = 0; - media_ = nullptr; - speed_.value = 1.0; - speed_.maintain_audio_pitch = false; - reverse_ = false; - autoscale_ = olive::CurrentConfig.autoscale_by_default; - opening_transition = nullptr; - closing_transition = nullptr; - undeletable = false; - replaced = false; - fbo = nullptr; - open_ = false; - - reset(); } ClipPtr Clip::copy(Sequence* s) { @@ -197,10 +194,6 @@ void Clip::move(ComboAction* ca, long iin, long iout, long iclip_in, int itrack, } } -void Clip::reset() { - texture = nullptr; -} - void Clip::reset_audio() { if (UsesCacher()) { cacher.ResetAudio(); @@ -619,7 +612,6 @@ bool Clip::Retrieve() const_cast(using_db_1 ? data_buffer_1 : data_buffer_2)); if (data_buffer_1 != frame->data[0]) { - qDebug() << data_buffer_1 << frame->data[0]; delete [] data_buffer_1; delete [] data_buffer_2; } @@ -641,3 +633,9 @@ bool Clip::UsesCacher() { return track() >= 0 || (media() != nullptr && media()->get_type() == MEDIA_TYPE_FOOTAGE); } + +ClipSpeed::ClipSpeed() : + value(1.0), + maintain_audio_pitch(false) +{ +} diff --git a/timeline/clip.h b/timeline/clip.h index 8e6e94914..3f29708f1 100644 --- a/timeline/clip.h +++ b/timeline/clip.h @@ -44,6 +44,7 @@ extern "C" { } struct ClipSpeed { + ClipSpeed(); double value; bool maintain_audio_pitch; }; @@ -115,7 +116,6 @@ public: AVRational time_base(); void reset_audio(); - void reset(); void refresh(); long length(); diff --git a/ui/checkboxex.cpp b/ui/checkboxex.cpp deleted file mode 100644 index 0682e8b52..000000000 --- a/ui/checkboxex.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include "checkboxex.h" - -#include "undo/undostack.h" -#include "undo/undo.h" - -CheckboxEx::CheckboxEx(QWidget* parent) : QCheckBox(parent) { -// connect(this, SIGNAL(clicked(bool)), this, SLOT(checkbox_command())); -} - -void CheckboxEx::checkbox_command() { - CheckboxCommand* c = new CheckboxCommand(this); - olive::UndoStack.push(c); -} diff --git a/ui/checkboxex.h b/ui/checkboxex.h deleted file mode 100644 index eb3db0f01..000000000 --- a/ui/checkboxex.h +++ /dev/null @@ -1,35 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#ifndef CHECKBOXEX_H -#define CHECKBOXEX_H - -#include - -class CheckboxEx : public QCheckBox -{ - Q_OBJECT -public: - CheckboxEx(QWidget* parent = 0); -private slots: - void checkbox_command(); -}; - -#endif // CHECKBOXEX_H diff --git a/ui/collapsiblewidget.cpp b/ui/collapsiblewidget.cpp index e81c3dbc8..21b719018 100644 --- a/ui/collapsiblewidget.cpp +++ b/ui/collapsiblewidget.cpp @@ -30,9 +30,7 @@ #include #include -#include "ui/checkboxex.h" #include "ui/icons.h" - #include "global/debug.h" CollapsibleWidget::CollapsibleWidget(QWidget* parent) : QWidget(parent) { @@ -47,7 +45,7 @@ CollapsibleWidget::CollapsibleWidget(QWidget* parent) : QWidget(parent) { title_bar->setAutoFillBackground(true); title_bar_layout = new QHBoxLayout(title_bar); title_bar_layout->setMargin(5); - enabled_check = new CheckboxEx(title_bar); + enabled_check = new QCheckBox(title_bar); enabled_check->setChecked(true); header = new QLabel(title_bar); collapse_button = new QPushButton(title_bar); diff --git a/ui/collapsiblewidget.h b/ui/collapsiblewidget.h index 8aa67ebb4..3cc25c522 100644 --- a/ui/collapsiblewidget.h +++ b/ui/collapsiblewidget.h @@ -30,8 +30,6 @@ #include #include -#include "ui/checkboxex.h" - class CollapsibleWidgetHeader : public QWidget { Q_OBJECT public: @@ -56,7 +54,7 @@ public: bool IsExpanded(); bool IsSelected(); protected: - CheckboxEx* enabled_check; + QCheckBox* enabled_check; CollapsibleWidgetHeader* title_bar; QWidget* contents; private: diff --git a/ui/viewerwidget.cpp b/ui/viewerwidget.cpp index 523b56c24..ee2de87b3 100644 --- a/ui/viewerwidget.cpp +++ b/ui/viewerwidget.cpp @@ -233,7 +233,7 @@ void ViewerWidget::frame_update() { } // render the audio - compose_audio(viewer, viewer->seq.get(), viewer->get_playback_speed(), viewer->WaitingForPlayWake()); + olive::rendering::compose_audio(viewer, viewer->seq.get(), viewer->get_playback_speed(), viewer->WaitingForPlayWake()); } } diff --git a/undo/undostack.h b/undo/undostack.h index 72086a823..9d42c4583 100644 --- a/undo/undostack.h +++ b/undo/undostack.h @@ -4,6 +4,9 @@ #include namespace olive { +/** + * @brief Global undo stack object + */ extern QUndoStack UndoStack; } From 1da4a315f80e715c5756d5f66f956851e93b9b2c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 21 Mar 2019 14:33:05 +1100 Subject: [PATCH 10/10] more documentation and added edit text button to stringfield --- dialogs/exportdialog.cpp | 10 ++++-- dialogs/exportdialog.h | 17 +++++++++- effects/fields/stringfield.cpp | 6 ++-- rendering/renderfunctions.cpp | 4 +-- ui/texteditex.cpp | 62 +++++++++++++++++++++++++++++----- ui/texteditex.h | 14 +++++++- 6 files changed, 96 insertions(+), 17 deletions(-) diff --git a/dialogs/exportdialog.cpp b/dialogs/exportdialog.cpp index 73dc3ec65..51180a7ea 100644 --- a/dialogs/exportdialog.cpp +++ b/dialogs/exportdialog.cpp @@ -372,10 +372,12 @@ void ExportDialog::render_thread_finished() { void ExportDialog::prep_ui_for_render(bool r) { export_button->setEnabled(!r); cancel_button->setEnabled(!r); + videoGroupbox->setEnabled(!r); + audioGroupbox->setEnabled(!r); renderCancel->setEnabled(r); } -void ExportDialog::export_action() { +void ExportDialog::StartExport() { if (widthSpinbox->value()%2 == 1 || heightSpinbox->value()%2 == 1) { QMessageBox::critical( this, @@ -533,6 +535,7 @@ void ExportDialog::export_action() { } } + // Set up export parameters to send to the ExportThread ExportParams params; params.filename = filename; params.video_enabled = videoGroupbox->isChecked(); @@ -558,12 +561,15 @@ void ExportDialog::export_action() { params.end_frame = qMin(olive::ActiveSequence->workarea_out, params.end_frame); } + // Create export thread et = new ExportThread(params, vcodec_params, this); + // Connect export thread signals/slots connect(et, SIGNAL(finished()), this, SLOT(render_thread_finished())); connect(et, SIGNAL(ProgressChanged(int, qint64)), this, SLOT(update_progress_bar(int, qint64))); connect(renderCancel, SIGNAL(clicked(bool)), et, SLOT(Interrupt())); + // Close all currently open clips close_active_clips(olive::ActiveSequence.get()); olive::Global->set_rendering_state(true); @@ -776,7 +782,7 @@ void ExportDialog::setup_ui() { export_button = new QPushButton(this); export_button->setText("Export"); - connect(export_button, SIGNAL(clicked(bool)), this, SLOT(export_action())); + connect(export_button, SIGNAL(clicked(bool)), this, SLOT(StartExport())); buttonLayout->addWidget(export_button); diff --git a/dialogs/exportdialog.h b/dialogs/exportdialog.h index df3f14c21..b2ee2e14c 100644 --- a/dialogs/exportdialog.h +++ b/dialogs/exportdialog.h @@ -51,8 +51,23 @@ public: explicit ExportDialog(QWidget *parent); private slots: + /** + * @brief Slot for when the user changes the format + * + * Used to populate the available codecs list for this format. + * + * @param index + * + * Current format index (corresponding to enum ExportFormats) + */ void format_changed(int index); - void export_action(); + + /** + * @brief Slot for when the user clicks the Export button + * + * Asks the user for the file to save to. + */ + void StartExport(); void update_progress_bar(int value, qint64 remaining_ms); void render_thread_finished(); void vcodec_changed(int index); diff --git a/effects/fields/stringfield.cpp b/effects/fields/stringfield.cpp index 30fa3c71e..7d59d3391 100644 --- a/effects/fields/stringfield.cpp +++ b/effects/fields/stringfield.cpp @@ -31,9 +31,9 @@ QWidget *StringField::CreateWidget(QWidget *existing) text_edit->setUndoRedoEnabled(true); // the "2" is because the height needs one extra pixel of padding on the top and the bottom - text_edit->setFixedHeight(qCeil(text_edit->fontMetrics().lineSpacing()*olive::CurrentConfig.effect_textbox_lines - + text_edit->document()->documentMargin() - + text_edit->document()->documentMargin() + 2)); + text_edit->setTextHeight(qCeil(text_edit->fontMetrics().lineSpacing()*olive::CurrentConfig.effect_textbox_lines + + text_edit->document()->documentMargin() + + text_edit->document()->documentMargin() + 2)); } else { diff --git a/rendering/renderfunctions.cpp b/rendering/renderfunctions.cpp index 6ab72b70b..c369f8ab9 100644 --- a/rendering/renderfunctions.cpp +++ b/rendering/renderfunctions.cpp @@ -152,7 +152,7 @@ void process_effect(Clip* c, } } -GLuint compose_sequence(ComposeSequenceParams ¶ms) { +GLuint olive::rendering::compose_sequence(ComposeSequenceParams ¶ms) { // qint64 time = QDateTime::currentMSecsSinceEpoch(); GLuint final_fbo = params.main_buffer; @@ -666,7 +666,7 @@ GLuint compose_sequence(ComposeSequenceParams ¶ms) { return 0; } -void compose_audio(Viewer* viewer, Sequence* seq, int playback_speed, bool wait_for_mutexes) { +void olive::rendering::compose_audio(Viewer* viewer, Sequence* seq, int playback_speed, bool wait_for_mutexes) { ComposeSequenceParams params; params.viewer = viewer; params.ctx = nullptr; diff --git a/ui/texteditex.cpp b/ui/texteditex.cpp index 3400217fc..cc6f2c189 100644 --- a/ui/texteditex.cpp +++ b/ui/texteditex.cpp @@ -20,6 +20,7 @@ #include "texteditex.h" +#include #include #include "dialogs/texteditdialog.h" @@ -27,13 +28,58 @@ #include "mainwindow.h" TextEditEx::TextEditEx(QWidget *parent, bool enable_rich_text) : - QTextEdit(parent), + QWidget(parent), enable_rich_text_(enable_rich_text) { - setContextMenuPolicy(Qt::CustomContextMenu); - connect(this, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(text_edit_menu())); + QVBoxLayout* layout = new QVBoxLayout(this); - connect(this, SIGNAL(textChanged()), this, SLOT(queue_text_modified())); + text_editor_ = new QTextEdit(); + connect(text_editor_, SIGNAL(textChanged()), this, SLOT(queue_text_modified())); + layout->addWidget(text_editor_); + + QPushButton* edit_button = new QPushButton(tr("Edit Text")); + layout->addWidget(edit_button); + connect(edit_button, SIGNAL(clicked(bool)), this, SLOT(open_text_edit())); + + /* + text_editor_->setContextMenuPolicy(Qt::CustomContextMenu); + connect(text_editor_, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(text_edit_menu())); + */ +} + +void TextEditEx::setUndoRedoEnabled(bool e) +{ + text_editor_->setUndoRedoEnabled(e); +} + +QTextDocument *TextEditEx::document() +{ + return text_editor_->document(); +} + +QTextCursor TextEditEx::textCursor() +{ + return text_editor_->textCursor(); +} + +void TextEditEx::setTextCursor(const QTextCursor &cursor) +{ + text_editor_->setTextCursor(cursor); +} + +void TextEditEx::setTextHeight(int h) +{ + text_editor_->setFixedHeight(h); +} + +void TextEditEx::setHtml(const QString &text) +{ + text_editor_->setHtml(text); +} + +void TextEditEx::setPlainText(const QString &text) +{ + text_editor_->setPlainText(text); } void TextEditEx::text_edit_menu() { @@ -45,21 +91,21 @@ void TextEditEx::text_edit_menu() { } void TextEditEx::open_text_edit() { - const QString& current_text = (enable_rich_text_) ? toHtml() : toPlainText(); + const QString& current_text = (enable_rich_text_) ? text_editor_->toHtml() : text_editor_->toPlainText(); TextEditDialog ted(olive::MainWindow, current_text, enable_rich_text_); ted.exec(); QString result = ted.get_string(); if (!result.isEmpty()) { if (enable_rich_text_) { - setHtml(result); + text_editor_->setHtml(result); } else { - setPlainText(result); + text_editor_->setPlainText(result); } } } void TextEditEx::queue_text_modified() { - emit textModified(enable_rich_text_ ? toHtml() : toPlainText()); + emit textModified(enable_rich_text_ ? text_editor_->toHtml() : text_editor_->toPlainText()); } diff --git a/ui/texteditex.h b/ui/texteditex.h index 0b4802a30..f9f8700a2 100644 --- a/ui/texteditex.h +++ b/ui/texteditex.h @@ -22,11 +22,21 @@ #define TEXTEDITEX_H #include +#include -class TextEditEx : public QTextEdit { +class TextEditEx : public QWidget { Q_OBJECT public: TextEditEx(QWidget* parent = nullptr, bool enable_rich_text = true); + + void setUndoRedoEnabled(bool e); + QTextDocument* document(); + QTextCursor textCursor(); + void setTextCursor(const QTextCursor &cursor); + void setTextHeight(int h); +public slots: + void setHtml(const QString &text); + void setPlainText(const QString &text); signals: void textModified(const QString& s); private slots: @@ -34,6 +44,8 @@ private slots: void open_text_edit(); void queue_text_modified(); private: + QTextEdit* text_editor_; + bool enable_rich_text_; };