diff --git a/dialogs/replaceclipmediadialog.cpp b/dialogs/replaceclipmediadialog.cpp new file mode 100644 index 000000000..c6b5d77d0 --- /dev/null +++ b/dialogs/replaceclipmediadialog.cpp @@ -0,0 +1,135 @@ +#include "replaceclipmediadialog.h" + +#include "ui/sourcetable.h" +#include "panels/panels.h" +#include "panels/timeline.h" +#include "panels/project.h" +#include "project/sequence.h" +#include "playback/playback.h" +#include "playback/cacher.h" + +#include +#include +#include +#include +#include +#include +#include + +ReplaceClipMediaDialog::ReplaceClipMediaDialog(QWidget *parent, SourceTable *table, QTreeWidgetItem *old_media) : + QDialog(parent), + source_table(table), + media(old_media) +{ + resize(300, 400); + + QVBoxLayout* layout = new QVBoxLayout(); + + layout->addWidget(new QLabel("Select which media you want to replace this media's clips with:")); + + tree = new QTreeWidget(); + tree->setHeaderHidden(true); + + layout->addWidget(tree); + + use_same_media_in_points = new QCheckBox("Keep the same media in points"); + use_same_media_in_points->setChecked(true); + layout->addWidget(use_same_media_in_points); + + QHBoxLayout* buttons = new QHBoxLayout(); + + buttons->addStretch(); + + QPushButton* replace_button = new QPushButton("Replace"); + connect(replace_button, SIGNAL(clicked(bool)), this, SLOT(replace())); + buttons->addWidget(replace_button); + + QPushButton* cancel_button = new QPushButton("Cancel"); + connect(cancel_button, SIGNAL(clicked(bool)), this, SLOT(close())); + buttons->addWidget(cancel_button); + + buttons->addStretch(); + + layout->addLayout(buttons); + + setLayout(layout); + + copy_tree(NULL, NULL); +} + +void ReplaceClipMediaDialog::replace() { + if (tree->selectedItems().size() != 1) { + QMessageBox::critical(this, "No media selected", "Please select a media to replace with or click 'Cancel'.", QMessageBox::Ok); + } else { + QTreeWidgetItem* selected_item = tree->selectedItems().at(0); + QTreeWidgetItem* new_item = reinterpret_cast(selected_item->data(0, Qt::UserRole + 1).value()); + if (media == new_item) { + QMessageBox::critical(this, "Same media selected", "You selected the same media that you're replacing. Please select a different one or click 'Cancel'.", QMessageBox::Ok); + } else if (get_type_from_tree(new_item) == MEDIA_TYPE_FOLDER) { + QMessageBox::critical(this, "Folder selected", "You cannot replace footage with a folder.", QMessageBox::Ok); + } else { + if (get_type_from_tree(new_item) == MEDIA_TYPE_SEQUENCE && sequence == get_sequence_from_tree(new_item)) { + QMessageBox::critical(this, "Active sequence selected", "You cannot insert a sequence into itself.", QMessageBox::Ok); + } else { + void* old_media = get_media_from_tree(media); + void* new_media = get_media_from_tree(new_item); + + // TODO not undoable yet + bool changed = false; + for (int i=0;iclip_count();i++) { + Clip* c = sequence->get_clip(i); + if (c != NULL && c->media == old_media) { + if (c->open) { + close_clip(c); + c->cacher->wait(); + } + + if (!use_same_media_in_points->isChecked()) { + c->clip_in = 0; + } + + c->media = new_media; + c->refresh(); + changed = true; + } + } + if (changed) { + panel_timeline->redraw_all_clips(true); + } + close(); + } + + } + } +} + +void ReplaceClipMediaDialog::copy_tree(QTreeWidgetItem* parent, QTreeWidgetItem* target) { + QVector items; + + if (parent == NULL) { + for (int i=0;itopLevelItemCount();i++) { + items.append(source_table->topLevelItem(i)); + } + } else { + for (int i=0;ichildCount();i++) { + items.append(parent->child(i)); + } + } + + for (int i=0;isetText(0, original->text(0)); + item->setData(0, Qt::UserRole + 1, reinterpret_cast(original)); + + if (target == NULL) { + tree->addTopLevelItem(item); + } else { + target->addChild(item); + } + + if (original->childCount() > 0) { + copy_tree(original, item); + } + } +} diff --git a/dialogs/replaceclipmediadialog.h b/dialogs/replaceclipmediadialog.h new file mode 100644 index 000000000..8bd60e3cd --- /dev/null +++ b/dialogs/replaceclipmediadialog.h @@ -0,0 +1,25 @@ +#ifndef REPLACECLIPMEDIADIALOG_H +#define REPLACECLIPMEDIADIALOG_H + +#include + +class SourceTable; +class QTreeWidget; +class QTreeWidgetItem; +class QCheckBox; + +class ReplaceClipMediaDialog : public QDialog { + Q_OBJECT +public: + ReplaceClipMediaDialog(QWidget* parent, SourceTable* table, QTreeWidgetItem *old_media); +private slots: + void replace(); +private: + SourceTable* source_table; + QTreeWidget* tree; + QTreeWidgetItem* media; + QCheckBox* use_same_media_in_points; + void copy_tree(QTreeWidgetItem* parent, QTreeWidgetItem *target); +}; + +#endif // REPLACECLIPMEDIADIALOG_H diff --git a/io/media.h b/io/media.h index ac74a30b2..105afa0af 100644 --- a/io/media.h +++ b/io/media.h @@ -28,7 +28,7 @@ struct MediaStream { struct Media { Media(); - ~Media(); + ~Media(); QString url; QString name; diff --git a/mainwindow.cpp b/mainwindow.cpp index f75245885..b16ad2455 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -5,6 +5,8 @@ #include "project/sequence.h" +#include "ui/sourcetable.h" + #include "panels/panels.h" #include "panels/project.h" #include "panels/effectcontrols.h" @@ -590,9 +592,10 @@ void MainWindow::on_actionSlide_Tool_triggered() if (panel_timeline->focused()) panel_timeline->ui->toolSlideButton->click(); } -void MainWindow::on_actionFolder_triggered() -{ - panel_project->new_folder(); +void MainWindow::on_actionFolder_triggered() { + TimelineAction* ta = new TimelineAction(); + ta->add_media(panel_project->new_folder(0), panel_project->get_selected_folder()); + undo_stack.push(ta); } void MainWindow::fileMenu_About_To_Be_Shown() { diff --git a/mainwindow.ui b/mainwindow.ui index f5bdadc25..01eceb3b9 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -24,7 +24,7 @@ 0 0 653 - 17 + 20 @@ -151,7 +151,6 @@ - @@ -420,6 +419,9 @@ Crash + + false + diff --git a/olive.pro b/olive.pro index 682d07435..258974f53 100644 --- a/olive.pro +++ b/olive.pro @@ -1,135 +1,137 @@ -#------------------------------------------------- -# -# Project created by QtCreator 2018-05-11T10:31:59 -# -#------------------------------------------------- - -QT += core gui multimedia opengl - -greaterThan(QT_MAJOR_VERSION, 4): QT += widgets - -TARGET = Olive -TEMPLATE = app - -# The following define makes your compiler emit warnings if you use -# any feature of Qt which has been marked as deprecated (the exact warnings -# depend on your compiler). Please consult the documentation of the -# deprecated API in order to know how to port your code away from it. -DEFINES += QT_DEPRECATED_WARNINGS - -# You can also make your code fail to compile if you use deprecated APIs. -# In order to do so, uncomment the following line. -# You can also select to disable deprecated APIs only up to a certain version of Qt. -#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 - - -SOURCES += \ - main.cpp \ - mainwindow.cpp \ - panels/project.cpp \ - panels/effectcontrols.cpp \ - panels/viewer.cpp \ - panels/timeline.cpp \ - ui/sourcetable.cpp \ - dialogs/aboutdialog.cpp \ - ui/timelinewidget.cpp \ - io/media.cpp \ - project/sequence.cpp \ - project/clip.cpp \ - playback/playback.cpp \ - playback/audio.cpp \ - io/config.cpp \ - dialogs/newsequencedialog.cpp \ - ui/viewerwidget.cpp \ - ui/viewercontainer.cpp \ - dialogs/exportdialog.cpp \ - ui/collapsiblewidget.cpp \ - effects/transformeffect.cpp \ - panels/panels.cpp \ - effects/volumeeffect.cpp \ - effects/paneffect.cpp \ - project/effect.cpp \ - effects/effects.cpp \ - playback/cacher.cpp \ - io/exportthread.cpp \ - ui/timelineheader.cpp \ - io/previewgenerator.cpp \ - ui/labelslider.cpp \ - dialogs/preferencesdialog.cpp \ - effects/transition.cpp \ - effects/crossdissolvetransition.cpp \ - ui/audiomonitor.cpp \ - project/undo.cpp \ - ui/scrollarea.cpp \ - effects/shakeeffect.cpp \ - effects/texteffect.cpp \ - ui/comboboxex.cpp \ - ui/colorbutton.cpp \ - effects/solideffect.cpp - -HEADERS += \ - mainwindow.h \ - panels/project.h \ - panels/effectcontrols.h \ - panels/viewer.h \ - panels/timeline.h \ - ui/sourcetable.h \ - dialogs/aboutdialog.h \ - ui/timelinewidget.h \ - io/media.h \ - project/sequence.h \ - project/clip.h \ - playback/playback.h \ - playback/audio.h \ - effects/effects.h \ - io/config.h \ - dialogs/newsequencedialog.h \ - ui/viewerwidget.h \ - ui/viewercontainer.h \ - dialogs/exportdialog.h \ - ui/collapsiblewidget.h \ - project/effect.h \ - panels/panels.h \ - playback/cacher.h \ - io/exportthread.h \ - ui/timelinetools.h \ - ui/timelineheader.h \ - io/previewgenerator.h \ - ui/labelslider.h \ - dialogs/preferencesdialog.h \ - effects/transition.h \ - ui/audiomonitor.h \ - project/undo.h \ - ui/scrollarea.h \ - ui/comboboxex.h \ - ui/colorbutton.h - -FORMS += \ - mainwindow.ui \ - panels/project.ui \ - panels/effectcontrols.ui \ - panels/viewer.ui \ - panels/timeline.ui \ - dialogs/aboutdialog.ui \ - dialogs/newsequencedialog.ui \ - dialogs/exportdialog.ui \ - dialogs/preferencesdialog.ui - -win32 { - LIBS += -L../ffmpeg/lib -lopengl32 - INCLUDEPATH = ../ffmpeg/include - RC_FILE = icons/win.rc - LIBS += -lavutil -lavformat -lavcodec -lswscale -lswresample -} - -mac { - LIBS += -L/usr/local/lib -lavutil -lavformat -lavcodec -lswscale -lswresample - INCLUDEPATH = /usr/local/include -} - -linux { - LIBS += -lavutil -lavformat -lavcodec -lswscale -lswresample -} - -RESOURCES += \ - icons/icons.qrc +#------------------------------------------------- +# +# Project created by QtCreator 2018-05-11T10:31:59 +# +#------------------------------------------------- + +QT += core gui multimedia opengl + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +TARGET = Olive +TEMPLATE = app + +# The following define makes your compiler emit warnings if you use +# any feature of Qt which has been marked as deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if you use deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + + +SOURCES += \ + main.cpp \ + mainwindow.cpp \ + panels/project.cpp \ + panels/effectcontrols.cpp \ + panels/viewer.cpp \ + panels/timeline.cpp \ + ui/sourcetable.cpp \ + dialogs/aboutdialog.cpp \ + ui/timelinewidget.cpp \ + io/media.cpp \ + project/sequence.cpp \ + project/clip.cpp \ + playback/playback.cpp \ + playback/audio.cpp \ + io/config.cpp \ + dialogs/newsequencedialog.cpp \ + ui/viewerwidget.cpp \ + ui/viewercontainer.cpp \ + dialogs/exportdialog.cpp \ + ui/collapsiblewidget.cpp \ + effects/transformeffect.cpp \ + panels/panels.cpp \ + effects/volumeeffect.cpp \ + effects/paneffect.cpp \ + project/effect.cpp \ + effects/effects.cpp \ + playback/cacher.cpp \ + io/exportthread.cpp \ + ui/timelineheader.cpp \ + io/previewgenerator.cpp \ + ui/labelslider.cpp \ + dialogs/preferencesdialog.cpp \ + effects/transition.cpp \ + effects/crossdissolvetransition.cpp \ + ui/audiomonitor.cpp \ + project/undo.cpp \ + ui/scrollarea.cpp \ + effects/shakeeffect.cpp \ + effects/texteffect.cpp \ + ui/comboboxex.cpp \ + ui/colorbutton.cpp \ + effects/solideffect.cpp \ + dialogs/replaceclipmediadialog.cpp + +HEADERS += \ + mainwindow.h \ + panels/project.h \ + panels/effectcontrols.h \ + panels/viewer.h \ + panels/timeline.h \ + ui/sourcetable.h \ + dialogs/aboutdialog.h \ + ui/timelinewidget.h \ + io/media.h \ + project/sequence.h \ + project/clip.h \ + playback/playback.h \ + playback/audio.h \ + effects/effects.h \ + io/config.h \ + dialogs/newsequencedialog.h \ + ui/viewerwidget.h \ + ui/viewercontainer.h \ + dialogs/exportdialog.h \ + ui/collapsiblewidget.h \ + project/effect.h \ + panels/panels.h \ + playback/cacher.h \ + io/exportthread.h \ + ui/timelinetools.h \ + ui/timelineheader.h \ + io/previewgenerator.h \ + ui/labelslider.h \ + dialogs/preferencesdialog.h \ + effects/transition.h \ + ui/audiomonitor.h \ + project/undo.h \ + ui/scrollarea.h \ + ui/comboboxex.h \ + ui/colorbutton.h \ + dialogs/replaceclipmediadialog.h + +FORMS += \ + mainwindow.ui \ + panels/project.ui \ + panels/effectcontrols.ui \ + panels/viewer.ui \ + panels/timeline.ui \ + dialogs/aboutdialog.ui \ + dialogs/newsequencedialog.ui \ + dialogs/exportdialog.ui \ + dialogs/preferencesdialog.ui + +win32 { + LIBS += -L../ffmpeg/lib -lopengl32 + INCLUDEPATH = ../ffmpeg/include + RC_FILE = icons/win.rc + LIBS += -lavutil -lavformat -lavcodec -lswscale -lswresample +} + +mac { + LIBS += -L/usr/local/lib -lavutil -lavformat -lavcodec -lswscale -lswresample + INCLUDEPATH = /usr/local/include +} + +linux { + LIBS += -lavutil -lavformat -lavcodec -lswscale -lswresample +} + +RESOURCES += \ + icons/icons.qrc diff --git a/panels/project.cpp b/panels/project.cpp index 183e075cd..19d19baab 100644 --- a/panels/project.cpp +++ b/panels/project.cpp @@ -15,6 +15,8 @@ #include "project/undo.h" #include "mainwindow.h" #include "io/config.h" +#include "playback/cacher.h" +#include "dialogs/replaceclipmediadialog.h" #include #include @@ -81,7 +83,7 @@ void Project::rename_media(QTreeWidgetItem* item, int column) { int type = get_type_from_tree(item); QString n = item->text(column); switch (type) { - case MEDIA_TYPE_FOOTAGE: get_media_from_tree(item)->name = n; break; + case MEDIA_TYPE_FOOTAGE: get_footage_from_tree(item)->name = n; break; case MEDIA_TYPE_SEQUENCE: get_sequence_from_tree(item)->name = n; break; } } @@ -104,6 +106,71 @@ void Project::duplicate_selected() { } } +void Project::replace_selected_file() { + QList selected_items = ui->treeWidget->selectedItems(); + if (selected_items.size() == 1) { + QTreeWidgetItem* item = selected_items.at(0); + if (get_type_from_tree(item) == MEDIA_TYPE_FOOTAGE) { + replace_media(item, 0); + } + } +} + +void Project::replace_media(QTreeWidgetItem* item, QString filename) { + Media* m = get_footage_from_tree(item); + + QStringList files; + if (filename.isEmpty()) { + QString browse_file = QFileDialog::getOpenFileName(this, "Replace '" + item->text(0) + "'", "", "All Files (*)"); + if (!browse_file.isEmpty()) files.append(browse_file); + } else { + files.append(filename); + } + + if (files.size() > 0) { + // close any clips currently using this media + QVector all_sequences = list_all_project_sequences(); + for (int i=0;iclip_count();j++) { + Clip* c = s->get_clip(j); + if (c != NULL && c->media == m && c->open) { + close_clip(c); + c->cacher->wait(); + } + } + } + + process_file_list(NULL, files, NULL, item); + + for (int i=0;iclip_count();j++) { + Clip* c = s->get_clip(j); + if (c != NULL && c->media == m) { + c->refresh(); + } + } + } + + panel_timeline->redraw_all_clips(true); + } +} + +void Project::replace_clip_media() { + if (sequence == NULL) { + QMessageBox::critical(this, "No active sequence", "No sequence is active, please open the sequence you want to replace clips from.", QMessageBox::Ok); + } else if (ui->treeWidget->selectedItems().size() == 1) { + QTreeWidgetItem* item = ui->treeWidget->selectedItems().at(0); + if (get_type_from_tree(item) == MEDIA_TYPE_SEQUENCE && sequence == get_sequence_from_tree(item)) { + QMessageBox::critical(this, "Active sequence selected", "You cannot insert a sequence into itself, so no clips of this media would be in this sequence.", QMessageBox::Ok); + } else { + ReplaceClipMediaDialog dialog(this, ui->treeWidget, item); + dialog.exec(); + } + } +} + void Project::new_sequence(TimelineAction *ta, Sequence *s, bool open, QTreeWidgetItem* parent) { QTreeWidgetItem* item = new_item(); item->setText(0, s->name); @@ -134,17 +201,16 @@ void Project::start_preview_generator(QTreeWidgetItem* item, Media* media) { pg->start(QThread::LowPriority); } -QTreeWidgetItem* Project::import_file(QString file, QString imported_filename) { - QTreeWidgetItem* item = new_item(); - Media* m = new Media(); +QString Project::get_file_name_from_path(const QString& path) { + return path.mid(path.lastIndexOf('/')+1); +} + +void Project::import_file(QTreeWidgetItem* item, Media* m, QString file, QString imported_filename) { m->url = file; - m->name = imported_filename.mid(file.lastIndexOf('/')+1); - set_media_of_tree(item, m); + m->name = get_file_name_from_path(imported_filename); // generate waveform/thumbnail in another thread - start_preview_generator(item, m); - - return item; + start_preview_generator(item, m); } QTreeWidgetItem* Project::new_item() { @@ -157,17 +223,15 @@ bool Project::is_focused() { return ui->treeWidget->hasFocus(); } -QTreeWidgetItem* Project::new_folder() { +QTreeWidgetItem* Project::new_folder(QString name) { QTreeWidgetItem* item = new_item(); item->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); - item->setText(0, "New Folder"); + item->setText(0, (name.isEmpty()) ? "New Folder" : name); set_item_to_folder(item); - ui->treeWidget->addTopLevelItem(item); - project_changed = true; return item; } -void Project::get_media_from_table(QList items, QList& list, int search_type) { +void Project::get_all_media_from_table(QList items, QList& list, int search_type) { for (int i=0;i items, QListchildCount();j++) { children.append(item->child(j)); } - get_media_from_table(children, list, search_type); + get_all_media_from_table(children, list, search_type); } else if (search_type == type) { list.append(item); } @@ -196,13 +260,13 @@ void Project::delete_selected_media() { for (int i=0;itreeWidget->topLevelItemCount();i++) { all_top_level_items.append(ui->treeWidget->topLevelItem(i)); } - get_media_from_table(all_top_level_items, sequence_items, MEDIA_TYPE_SEQUENCE); // find all sequences in project + get_all_media_from_table(all_top_level_items, sequence_items, MEDIA_TYPE_SEQUENCE); // find all sequences in project if (sequence_items.size() > 0) { QList media_items; - get_media_from_table(items, media_items, MEDIA_TYPE_FOOTAGE); + get_all_media_from_table(items, media_items, MEDIA_TYPE_FOOTAGE); for (int i=0;i image_sequence_urls; QVector image_sequence_importassequence; QStringList image_sequence_formats = config.img_seq_formats.split("|"); - TimelineAction* ta = new TimelineAction(); + TimelineAction* ta; + if (!recursive) ta = new TimelineAction(); + for (int i=0;i file.lastIndexOf('/')) { - // image_sequence_formats - found = false; - QString ext = file.mid(lastcharindex+1); - for (int j=0;jadd_media(folder, parent); + } else { + parent->addChild(folder); + } - // Check if there are files with the same filename but just different numbers - if (QFileInfo::exists(QString(file.left(digit_test) + QString("%1").arg(file_number-1, digit_count, 10, QChar('0')) + file.mid(lastcharindex))) - || QFileInfo::exists(QString(file.left(digit_test) + QString("%1").arg(file_number+1, digit_count, 10, QChar('0')) + file.mid(lastcharindex)))) { - is_img_sequence = true; - } + imported = true; + } else { + QString file(files.at(i)); + bool skip = false; - if (is_img_sequence) { - // get the URL that we would pass to FFmpeg to force it to read the image as a sequence - QString new_filename = file.left(digit_test) + "%" + QString("%1").arg(digit_count, 2, 10, QChar('0')) + "d" + file.mid(lastcharindex); + /* Heuristic to determine whether file is part of an image sequence */ - // add image sequence url to a vector in case the user imported several files that - // we're interpreting as a possible sequence - found = false; - for (int i=0;i file.lastIndexOf('/')) { + // image_sequence_formats + found = false; + QString ext = file.mid(lastcharindex+1); + for (int j=0;jadd_media(import_file(file, files.at(i))); - imported = true; - project_changed = true; - } - } - if (imported) { - undo_stack.push(ta); - } else { - delete ta; + if (found && file[lastcharindex-1].isDigit()) { + bool is_img_sequence = false; + + // how many digits are in the filename? + int digit_count = 0; + int digit_test = lastcharindex-1; + while (file[digit_test].isDigit()) { + digit_count++; + digit_test--; + } + + // retrieve number from filename + digit_test++; + int file_number = file.mid(digit_test, digit_count).toInt(); + + // Check if there are files with the same filename but just different numbers + if (QFileInfo::exists(QString(file.left(digit_test) + QString("%1").arg(file_number-1, digit_count, 10, QChar('0')) + file.mid(lastcharindex))) + || QFileInfo::exists(QString(file.left(digit_test) + QString("%1").arg(file_number+1, digit_count, 10, QChar('0')) + file.mid(lastcharindex)))) { + is_img_sequence = true; + } + + if (is_img_sequence) { + // get the URL that we would pass to FFmpeg to force it to read the image as a sequence + QString new_filename = file.left(digit_test) + "%" + QString("%1").arg(digit_count, 2, 10, QChar('0')) + "d" + file.mid(lastcharindex); + + // add image sequence url to a vector in case the user imported several files that + // we're interpreting as a possible sequence + found = false; + for (int i=0;ireset(); + } else { + item = new_item(); + m = new Media(); + } + + import_file(item, m, file, files.at(i)); + set_footage_of_tree(item, m); + + if (recursive) { + parent->addChild(item); + } else { + ta->add_media(item, parent); + } + + imported = true; + } + } } + if (!recursive) { + if (imported) { + undo_stack.push(ta); + } else { + delete ta; + } + } +} + +QTreeWidgetItem* Project::get_selected_folder() { + // if one item is selected and it's a folder, return it + QList selected_items = panel_project->source_table->selectedItems(); + if (selected_items.size() == 1 && get_type_from_tree(selected_items.at(0)) == MEDIA_TYPE_FOLDER) { + return selected_items.at(0); + } + return NULL; } void Project::import_dialog() { - QStringList files = QFileDialog::getOpenFileNames(this, "Import media...", "", "All Files (*)"); - process_file_list(files); + QFileDialog fd(this, "Import media...", "", "All Files (*)"); + fd.setFileMode(QFileDialog::ExistingFiles); + + if (fd.exec()) { + QStringList files = fd.selectedFiles(); + process_file_list(NULL, files, get_selected_folder(), NULL); + } } void set_item_to_folder(QTreeWidgetItem* item) { item->setData(0, Qt::UserRole + 1, MEDIA_TYPE_FOLDER); } -Media* get_media_from_tree(QTreeWidgetItem* item) { +void* get_media_from_tree(QTreeWidgetItem* item) { + int type = get_type_from_tree(item); + switch (type) { + case MEDIA_TYPE_FOOTAGE: return get_footage_from_tree(item); + case MEDIA_TYPE_SEQUENCE: return get_sequence_from_tree(item); + default: qDebug() << "[ERROR] Invalid media type when retrieving media"; + } + return NULL; +} + +Media* get_footage_from_tree(QTreeWidgetItem* item) { return reinterpret_cast(item->data(0, Qt::UserRole + 2).value()); } -void set_media_of_tree(QTreeWidgetItem* item, Media* media) { +void set_footage_of_tree(QTreeWidgetItem* item, Media* media) { item->setText(0, media->name); item->setData(0, Qt::UserRole + 1, MEDIA_TYPE_FOOTAGE); item->setData(0, Qt::UserRole + 2, QVariant::fromValue(reinterpret_cast(media))); @@ -426,18 +562,35 @@ int get_type_from_tree(QTreeWidgetItem* item) { } void Project::delete_media(QTreeWidgetItem* item) { - int type = get_type_from_tree(item); - switch (type) { - case MEDIA_TYPE_FOOTAGE: - delete get_media_from_tree(item); - break; - case MEDIA_TYPE_SEQUENCE: - Sequence* s = get_sequence_from_tree(item); - delete s; - break; - } + delete get_media_from_tree(item); +} - project_changed = true; +void Project::delete_clips_using_selected_media() { + if (sequence == NULL) { + QMessageBox::critical(this, "No active sequence", "No sequence is active, please open the sequence you want to delete clips from.", QMessageBox::Ok); + } else { + TimelineAction* ta = new TimelineAction(); + bool deleted = false; + for (int i=0;iclip_count();i++) { + Clip* c = sequence->get_clip(i); + if (c != NULL) { + QList items = source_table->selectedItems(); + for (int j=0;jmedia == m) { + ta->delete_clip(sequence, i); + deleted = true; + } + } + } + } + if (deleted) { + undo_stack.push(ta); + panel_timeline->redraw_all_clips(true); + } else { + delete ta; + } + } } void Project::clear() { @@ -516,7 +669,7 @@ bool Project::load_worker(QFile& f, QXmlStreamReader& stream, int type) { switch (type) { case MEDIA_TYPE_FOLDER: { - QTreeWidgetItem* folder = new_folder(); + QTreeWidgetItem* folder = new_folder(0); for (int j=0;jtreeWidget->addTopLevelItem(item); @@ -823,10 +976,11 @@ void Project::load_project() { for (int i=0;idata(0, Qt::UserRole + 4).toInt(); - if (parent > 0) { - ui->treeWidget->takeTopLevelItem(ui->treeWidget->indexOfTopLevelItem(folder)); + if (parent > 0) { find_loaded_folder_by_id(parent)->addChild(folder); - } + } else { + ui->treeWidget->addTopLevelItem(folder); + } } cont = load_worker(file, stream, MEDIA_TYPE_FOOTAGE); @@ -897,7 +1051,7 @@ void Project::save_folder(QXmlStreamWriter& stream, QTreeWidgetItem* parent, int } else { int folder = root ? 0 : parent->data(0, Qt::UserRole + 3).toInt(); if (type == MEDIA_TYPE_FOOTAGE) { - Media* m = get_media_from_tree(item); + Media* m = get_footage_from_tree(item); m->save_id = media_id; stream.writeStartElement("footage"); stream.writeAttribute("id", QString::number(media_id)); diff --git a/panels/project.h b/panels/project.h index 8d4d202fb..f10514fd3 100644 --- a/panels/project.h +++ b/panels/project.h @@ -36,8 +36,9 @@ extern QStringList recent_projects; extern QString recent_proj_file; int get_type_from_tree(QTreeWidgetItem* item); -Media* get_media_from_tree(QTreeWidgetItem* item); -void set_media_of_tree(QTreeWidgetItem* item, Media* media); +void* get_media_from_tree(QTreeWidgetItem* item); +Media* get_footage_from_tree(QTreeWidgetItem* item); +void set_footage_of_tree(QTreeWidgetItem* item, Media* media); Sequence* get_sequence_from_tree(QTreeWidgetItem* item); void set_sequence_of_tree(QTreeWidgetItem* item, Sequence* sequence); void set_item_to_folder(QTreeWidgetItem* item); @@ -51,17 +52,19 @@ public: ~Project(); bool is_focused(); void clear(); - QTreeWidgetItem* import_file(QString url, QString imported_filename); + void import_file(QTreeWidgetItem* item, Media* m, QString url, QString imported_filename); void new_sequence(TimelineAction* ta, Sequence* s, bool open, QTreeWidgetItem* parent); QString get_next_sequence_name(); void delete_media(QTreeWidgetItem* item); - void process_file_list(QStringList& files); + void process_file_list(bool recursive, QStringList& files, QTreeWidgetItem *parent, QTreeWidgetItem* replace); + void replace_media(QTreeWidgetItem* item, QString filename); + QTreeWidgetItem* get_selected_folder(); void new_project(); void load_project(); void save_project(bool autorecovery); - QTreeWidgetItem* new_folder(); + QTreeWidgetItem* new_folder(QString name); void save_recent_projects(); @@ -69,9 +72,12 @@ public: SourceTable* source_table; public slots: - void import_dialog(); + void import_dialog(); void delete_selected_media(); void duplicate_selected(); + void delete_clips_using_selected_media(); + void replace_selected_file(); + void replace_clip_media(); private: Ui::Project *ui; QTreeWidgetItem* new_item(); @@ -89,9 +95,10 @@ private: QVector loaded_sequences; QTreeWidgetItem* find_loaded_folder_by_id(int id); void add_recent_project(QString url); - void get_media_from_table(QList items, QList& list, int type); + void get_all_media_from_table(QList items, QList& list, int type); void start_preview_generator(QTreeWidgetItem* item, Media* media); void list_all_sequences_worker(QVector* list, QTreeWidgetItem* parent); + QString get_file_name_from_path(const QString &path); private slots: void rename_media(QTreeWidgetItem* item, int column); void clear_recent_projects(); diff --git a/panels/timeline.cpp b/panels/timeline.cpp index c6c85e7ae..2e8be1ce2 100644 --- a/panels/timeline.cpp +++ b/panels/timeline.cpp @@ -264,8 +264,7 @@ void Timeline::repaint_timeline() { } void Timeline::redraw_all_clips(bool changed) { - if (changed) { - project_changed = true; + if (changed) { if (!playing) reset_all_audio(); panel_viewer->viewer_widget->update(); } diff --git a/playback/cacher.cpp b/playback/cacher.cpp index 4ac960e83..96fbb4e3c 100644 --- a/playback/cacher.cpp +++ b/playback/cacher.cpp @@ -182,7 +182,6 @@ void reset_cache(Clip* c, long target_frame) { // seeks to nearest keyframe (target_frame represents internal clip frame) av_seek_frame(c->formatCtx, ms->file_index, (int64_t) qFloor(clip_frame_to_seconds(c, target_frame) / timebase), AVSEEK_FLAG_BACKWARD); - qDebug() << target_frame; // play up to the frame we actually want long retrieved_frame = 0; diff --git a/playback/playback.cpp b/playback/playback.cpp index 578e6099c..9f29586a3 100644 --- a/playback/playback.cpp +++ b/playback/playback.cpp @@ -246,7 +246,7 @@ void retrieve_next_frame_raw_data(Clip* c, AVFrame* output) { } bool is_clip_active(Clip* c, long playhead) { - return c->timeline_in < playhead + ceil(c->sequence->frame_rate) && c->timeline_out > playhead && c->enabled; + return c->timeline_in < playhead + ceil(c->sequence->frame_rate) && c->timeline_out > playhead && c->enabled; } void set_sequence(Sequence* s) { diff --git a/project/undo.cpp b/project/undo.cpp index 35bbf7f16..9953572b5 100644 --- a/project/undo.cpp +++ b/project/undo.cpp @@ -31,7 +31,8 @@ TimelineAction::TimelineAction() : done(false), change_seq(false), ripple_enabled(false), - change_in_out(false) + change_in_out(false), + old_project_changed(project_changed) {} TimelineAction::~TimelineAction() { @@ -129,8 +130,9 @@ void TimelineAction::delete_clip(Sequence* s, int clip) { new_action(s, TA_DELETE, clip, 0, 0); } -void TimelineAction::add_media(QTreeWidgetItem* item) { +void TimelineAction::add_media(QTreeWidgetItem* item, QTreeWidgetItem *parent) { media_to_add.append(item); + media_to_add_parents.append(parent); } void TimelineAction::delete_media(QTreeWidgetItem* item) { @@ -242,20 +244,20 @@ void TimelineAction::undo() { } // delete added clips - if (clips_to_add.size() > 0) { - int delete_count = 0; - for (int i=0;idestroy_clip(added_indexes.at(i)-delete_count, true); - delete_count++; - } - } + int delete_count = 0; + for (int i=0;idestroy_clip(added_indexes.at(i)-delete_count, true); + delete_count++; + } // remove any added media - if (media_to_add.size() > 0) { - for (int i=0;isource_table->takeTopLevelItem(panel_project->source_table->indexOfTopLevelItem(media_to_add.at(i))); - } - } + for (int i=0;isource_table->takeTopLevelItem(panel_project->source_table->indexOfTopLevelItem(media_to_add.at(i))); + } else { + media_to_add_parents.at(i)->removeChild(media_to_add.at(i)); + } + } // un-change workarea if (change_in_out) { @@ -269,6 +271,8 @@ void TimelineAction::undo() { set_sequence(old_seq); } + project_changed = old_project_changed; + done = false; } @@ -411,34 +415,34 @@ void TimelineAction::redo() { } // delete media - if (deleted_media.size() > 0) { - for (int i=0;iparent()); + for (int i=0;iparent()); - // if we're deleting the open sequence, close it - if (get_type_from_tree(item) == MEDIA_TYPE_SEQUENCE && - get_sequence_from_tree(item) == sequence && - !change_seq) { - old_seq = sequence; - new_seq = NULL; - change_seq = true; - } + // if we're deleting the open sequence, close it + if (get_type_from_tree(item) == MEDIA_TYPE_SEQUENCE && + get_sequence_from_tree(item) == sequence && + !change_seq) { + old_seq = sequence; + new_seq = NULL; + change_seq = true; + } - if (item->parent() == NULL) { - panel_project->source_table->takeTopLevelItem(panel_project->source_table->indexOfTopLevelItem(item)); - } else { - item->parent()->removeChild(item); - } - } - } + if (item->parent() == NULL) { + panel_project->source_table->takeTopLevelItem(panel_project->source_table->indexOfTopLevelItem(item)); + } else { + item->parent()->removeChild(item); + } + } // add any new media - if (media_to_add.size() > 0) { - for (int i=0;isource_table->addTopLevelItem(media_to_add.at(i)); - } - } + for (int i=0;isource_table->addTopLevelItem(media_to_add.at(i)); + } else { + media_to_add_parents.at(i)->addChild(media_to_add.at(i)); + } + } // change sequence workarea if (change_in_out) { @@ -453,6 +457,8 @@ void TimelineAction::redo() { set_sequence(new_seq); } + project_changed = true; + done = true; } diff --git a/project/undo.h b/project/undo.h index b66fdf075..12deb4131 100644 --- a/project/undo.h +++ b/project/undo.h @@ -28,7 +28,7 @@ public: void set_track(Sequence* s, int clip, int value); void increase_track(Sequence* s, int clip, int value); void delete_clip(Sequence* s, int clip); - void add_media(QTreeWidgetItem* item); + void add_media(QTreeWidgetItem* item, QTreeWidgetItem* parent); void delete_media(QTreeWidgetItem* item); void ripple(Sequence* s, long point, long length); void ripple(Sequence* s, long point, long length, QVector &ignore); @@ -68,6 +68,7 @@ private: QVector deleted_media_parents; QVector media_to_add; + QVector media_to_add_parents; Sequence* ripple_sequence; bool ripple_enabled; @@ -87,6 +88,8 @@ private: void new_action(Sequence* s, int action, int clip, long old_val, long new_val); void offset_links(QVector& clips, int offset); + + bool old_project_changed; }; class LinkCommand : public QUndoCommand { diff --git a/ui/sourcetable.cpp b/ui/sourcetable.cpp index c1748a004..7b71e7f61 100644 --- a/ui/sourcetable.cpp +++ b/ui/sourcetable.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include SourceTable::SourceTable(QWidget* parent) : QTreeWidget(parent) { @@ -34,24 +36,41 @@ void SourceTable::show_context_menu(const QPoint& pos) { } else { if (selectedItems().size() == 1) { // replace footage - if (get_type_from_tree(selectedItems().at(0)) == MEDIA_TYPE_FOOTAGE) { - menu.addAction("Replace/Relink Media"); + int type = get_type_from_tree(selectedItems().at(0)); + if (type == MEDIA_TYPE_FOOTAGE) { + QAction* replace_action = menu.addAction("Replace/Relink Media"); + connect(replace_action, SIGNAL(triggered(bool)), panel_project, SLOT(replace_selected_file())); } - menu.addAction("Replace Clips Using This Media"); + if (type != MEDIA_TYPE_FOLDER) { + QAction* replace_clip_media = menu.addAction("Replace Clips Using This Media"); + connect(replace_clip_media, SIGNAL(triggered(bool)), panel_project, SLOT(replace_clip_media())); + } } // duplicate item bool all_sequences = true; + bool all_footage = true; for (int i=0;imimeData(); + QTreeWidgetItem* drop_item = itemAt(event->pos()); if (mimeData->hasUrls()) { // drag files in from outside QList urls = mimeData->urls(); - if (!urls.isEmpty()) { + if (!urls.isEmpty()) { QStringList paths; - for (int i=0;iprocess_file_list(paths); + bool replace = false; + if (urls.size() == 1 + && drop_item != NULL + && get_type_from_tree(drop_item) == MEDIA_TYPE_FOOTAGE + && !QFileInfo(paths.at(0)).isDir() + && QMessageBox::question(this, "Replace Media", "You dropped a file onto '" + drop_item->text(0) + "'. Would you like to replace it with the dropped file?", QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { + replace = true; + panel_project->replace_media(drop_item, paths.at(0)); + } + if (!replace) { + QTreeWidgetItem* parent = NULL; + if (drop_item != NULL && get_type_from_tree(drop_item) == MEDIA_TYPE_FOLDER) { + parent = drop_item; + } + panel_project->process_file_list(NULL, paths, parent, NULL); + } } event->acceptProposedAction(); } else { // dragging files within project QVector move_items; - QTreeWidgetItem* drop_item = itemAt(event->pos()); // if we dragged to the root OR dragged to a folder if (drop_item == NULL || (drop_item != NULL && get_type_from_tree(drop_item) == MEDIA_TYPE_FOLDER)) { QList selected_items = selectedItems(); diff --git a/ui/timelinewidget.cpp b/ui/timelinewidget.cpp index 8c4381b82..a5c5bbd25 100644 --- a/ui/timelinewidget.cpp +++ b/ui/timelinewidget.cpp @@ -79,7 +79,7 @@ void TimelineWidget::dragEnterEvent(QDragEnterEvent *event) { switch (get_type_from_tree(item)) { case MEDIA_TYPE_FOOTAGE: { - Media* m = get_media_from_tree(item); + Media* m = get_footage_from_tree(item); if (m->ready) { if (!got_video_values) { for (int j=0;jvideo_tracks.size();j++) { @@ -140,7 +140,7 @@ void TimelineWidget::dragEnterEvent(QDragEnterEvent *event) { switch (item_type) { case MEDIA_TYPE_FOOTAGE: - m = get_media_from_tree(item); + m = get_footage_from_tree(item); media = m; can_import = m->ready; break; @@ -815,12 +815,12 @@ void TimelineWidget::update_ghosts(QPoint& mouse_pos) { validator = g.ghost_length + frame_diff; if (validator < 1) frame_diff += (1 - validator); - // prevent clip length exceeding media length - if (c->media_type == MEDIA_TYPE_SEQUENCE + // prevent clip length exceeding media length + if (c->media_type == MEDIA_TYPE_SEQUENCE || (c->media_type == MEDIA_TYPE_FOOTAGE && !static_cast(c->media)->get_stream_from_file_index(c->media_stream)->infinite_length)) { validator = g.old_clip_in + g.ghost_length + frame_diff; if (validator > g.media_length) frame_diff -= validator - g.media_length; - } + } } // ripple ops @@ -1433,30 +1433,22 @@ void TimelineWidget::redraw_clips() { } if (clip->media_type == MEDIA_TYPE_FOOTAGE) { + bool draw_checkerboard = false; + QRect checkerboard_rect = clip_rect; Media* m = static_cast(clip->media); MediaStream* ms = m->get_stream_from_file_index(clip->media_stream); if (ms == NULL) { - // draw "error lines" if media stream is missing - clip_painter.setPen(QPen(QColor(64, 64, 64), 2)); - int limit = clip_rect.width(); - int clip_height = clip_rect.bottom()-clip_rect.top(); - for (int j=-clip_height;j clip_rect.right()) { - lines_end_y -= (clip_rect.right() - lines_end_x); - lines_end_x = clip_rect.right(); - } - clip_painter.drawLine(lines_start_x, lines_start_y, lines_end_x, lines_end_y); - } + draw_checkerboard = true; } else if (ms->preview_done) { // draw thumbnail/waveform + long media_length = m->get_length_in_frames(clip->sequence->frame_rate); + int waveform_limit = qMin(clip_rect.width(), panel_timeline->getScreenPointFromFrame(media_length - clip->clip_in)); + + if (waveform_limit < clip_rect.width()) { + draw_checkerboard = true; + checkerboard_rect.setLeft(clip_rect.left() + waveform_limit); + } + if (clip->track < 0) { int thumb_y = clip_painter.fontMetrics().height()+CLIP_TEXT_PADDING+CLIP_TEXT_PADDING; int thumb_height = clip_rect.height()-thumb_y; @@ -1465,33 +1457,47 @@ void TimelineWidget::redraw_clips() { QRect thumb_rect(thumb_x, clip_rect.y()+thumb_y, thumb_width, thumb_height); clip_painter.drawImage(thumb_rect, ms->video_preview); } - } else if (clip_rect.height() > TRACK_MIN_HEIGHT) { + } else if (clip_rect.height() > TRACK_MIN_HEIGHT) { int divider = ms->audio_channels*2; clip_painter.setPen(QColor(80, 80, 80)); int channel_height = clip_rect.height()/ms->audio_channels; - long media_length = m->get_length_in_frames(clip->sequence->frame_rate); - for (int i=0;iclip_in + ((double) i/panel_timeline->zoom))/media_length) * ms->audio_preview.size())/divider)*divider; - for (int j=0;jaudio_channels;j++) { - int mid = clip_rect.top()+channel_height*j+(channel_height/2); - int offset = waveform_index+(j*2); + for (int j=0;jaudio_channels;j++) { + int mid = clip_rect.top()+channel_height*j+(channel_height/2); + int offset = waveform_index+(j*2); - qint8 min = (double)ms->audio_preview.at(offset) / 128.0 * (channel_height/2); - qint8 max = (double)ms->audio_preview.at(offset+1) / 128.0 * (channel_height/2); - clip_painter.drawLine(clip_rect.left()+i, mid+min, clip_rect.left()+i, mid+max); - - /*if (offset >= 0 && offset < ms->audio_preview.size()) { - - } else { - QMessageBox::critical(this, "AAAAAAAAAAAAA", "Here's that terrible, no-good bug again!!! I've stepped around it but plz report the following to Matt:\n\n" + QString::number(offset) + " vs " + QString::number(clip->media_stream->audio_preview.size()), QMessageBox::Ok); - break; - }*/ - } - } + qint8 min = (double)ms->audio_preview.at(offset) / 128.0 * (channel_height/2); + qint8 max = (double)ms->audio_preview.at(offset+1) / 128.0 * (channel_height/2); + clip_painter.drawLine(clip_rect.left()+i, mid+min, clip_rect.left()+i, mid+max); + } + } } } + if (draw_checkerboard) { + // draw "error lines" if media stream is missing + clip_painter.setPen(QPen(QColor(64, 64, 64), 2)); + int limit = checkerboard_rect.width(); + int clip_height = checkerboard_rect.bottom()-checkerboard_rect.top(); + for (int j=-clip_height;j checkerboard_rect.right()) { + lines_end_y -= (checkerboard_rect.right() - lines_end_x); + lines_end_x = checkerboard_rect.right(); + } + clip_painter.drawLine(lines_start_x, lines_start_y, lines_end_x, lines_end_y); + } + } } // top left bevel diff --git a/ui/viewerwidget.cpp b/ui/viewerwidget.cpp index 8058a96e5..59723471a 100644 --- a/ui/viewerwidget.cpp +++ b/ui/viewerwidget.cpp @@ -110,19 +110,22 @@ void ViewerWidget::compose_sequence(Clip* nest, bool render_audio) { case MEDIA_TYPE_FOOTAGE: { Media* m = static_cast(c->media); - if (m->ready - && m->get_stream_from_file_index(c->media_stream) != NULL - && is_clip_active(c, playhead)) { - // if thread is already working, we don't want to touch this, - // but we also don't want to hang the UI thread - if (!c->open) { - open_clip(c, multithreaded); - } + if (m->ready) { + if (m->get_stream_from_file_index(c->media_stream) != NULL + && is_clip_active(c, playhead)) { + // if thread is already working, we don't want to touch this, + // but we also don't want to hang the UI thread + if (!c->open) { + open_clip(c, multithreaded); + } - clip_is_active = true; - } else if (c->open) { - close_clip(c); - } + clip_is_active = true; + } else if (c->open) { + close_clip(c); + } + } else { + texture_failed = true; + } } break; case MEDIA_TYPE_SEQUENCE: