#include "project.h" #include "ui_project.h" #include "io/media.h" #include "panels/panels.h" #include "panels/timeline.h" #include "panels/viewer.h" #include "playback/playback.h" #include "project/effect.h" #include "project/transition.h" #include "panels/timeline.h" #include "project/sequence.h" #include "project/clip.h" #include "io/previewgenerator.h" #include "project/undo.h" #include "mainwindow.h" #include "io/config.h" #include "playback/cacher.h" #include "dialogs/replaceclipmediadialog.h" #include "panels/effectcontrols.h" #include "dialogs/newsequencedialog.h" #include "dialogs/mediapropertiesdialog.h" #include "dialogs/loaddialog.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include #include } #define MAXIMUM_RECENT_PROJECTS 10 QString autorecovery_filename; QString project_url = ""; QStringList recent_projects; QString recent_proj_file; Project::Project(QWidget *parent) : QDockWidget(parent), ui(new Ui::Project) { ui->setupUi(this); source_table = ui->treeWidget; connect(ui->treeWidget, SIGNAL(itemChanged(QTreeWidgetItem*,int)), this, SLOT(rename_media(QTreeWidgetItem*,int))); } Project::~Project() { delete ui; } QString Project::get_next_sequence_name(QString start) { if (start.isEmpty()) start = "Sequence"; int n = 1; bool found = true; QString name; while (found) { found = false; name = start + " "; if (n < 10) { name += "0"; } name += QString::number(n); for (int i=0;itreeWidget->topLevelItemCount();i++) { if (QString::compare(ui->treeWidget->topLevelItem(i)->text(0), name, Qt::CaseInsensitive) == 0) { found = true; n++; break; } } } return name; } Sequence* create_sequence_from_media(QVector& media_list, QVector& type_list) { Sequence* s = new Sequence(); s->name = panel_project->get_next_sequence_name(); // shitty hardcoded default values s->width = 1920; s->height = 1080; s->frame_rate = 29.97; s->audio_frequency = 48000; s->audio_layout = 3; bool got_video_values = false; bool got_audio_values = false; for (int i=0;i(media_list.at(i)); if (m->ready) { if (!got_video_values) { for (int j=0;jvideo_tracks.size();j++) { MediaStream* ms = m->video_tracks.at(j); s->width = ms->video_width; s->height = ms->video_height; if (ms->video_frame_rate != 0) { s->frame_rate = ms->video_frame_rate; if (ms->video_interlacing != VIDEO_PROGRESSIVE) s->frame_rate *= 2; // only break with a decent frame rate, otherwise there may be a better candidate got_video_values = true; break; } } } if (!got_audio_values) { for (int j=0;jaudio_tracks.size();j++) { MediaStream* ms = m->audio_tracks.at(j); s->audio_frequency = ms->audio_frequency; got_audio_values = true; break; } } } } break; case MEDIA_TYPE_SEQUENCE: { Sequence* seq = static_cast(media_list.at(i)); s->width = seq->width; s->height = seq->height; s->frame_rate = seq->frame_rate; s->audio_frequency = seq->audio_frequency; s->audio_layout = seq->audio_layout; got_video_values = true; got_audio_values = true; } break; } if (got_video_values && got_audio_values) break; } return s; } void Project::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_footage_from_tree(item)->name = n; break; case MEDIA_TYPE_SEQUENCE: get_sequence_from_tree(item)->name = n; break; } } void Project::duplicate_selected() { QList items = ui->treeWidget->selectedItems(); bool duped = false; ComboAction* ca = new ComboAction(); for (int j=0;jcopy(), false, i->parent()); duped = true; } } if (duped) { undo_stack.push(ca); } else { delete ca; } } 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) { if (filename.isEmpty()) { filename = QFileDialog::getOpenFileName(this, "Replace '" + item->text(0) + "'", "", "All Files (*)"); } if (!filename.isEmpty()) { ReplaceMediaCommand* rmc = new ReplaceMediaCommand(item, filename); undo_stack.push(rmc); } } 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::open_properties() { if (ui->treeWidget->selectedItems().size() == 1) { QTreeWidgetItem* item = ui->treeWidget->selectedItems().at(0); switch (get_type_from_tree(item)) { case MEDIA_TYPE_FOOTAGE: { MediaPropertiesDialog mpd(this, item, get_footage_from_tree(item)); mpd.exec(); } break; case MEDIA_TYPE_SEQUENCE: { NewSequenceDialog nsd(this); Sequence* s = get_sequence_from_tree(item); nsd.existing_sequence = s; nsd.existing_item = item; nsd.exec(); } break; default: { // fall back to renaming QString new_name = QInputDialog::getText(this, "Rename '" + item->text(0) + "'", "Enter new name:", QLineEdit::Normal, item->text(0)); if (!new_name.isEmpty()) { MediaRename* mr = new MediaRename(); mr->from = item->text(0); mr->item = item; mr->to = new_name; undo_stack.push(mr); } } } } } void Project::new_sequence(ComboAction *ca, Sequence *s, bool open, QTreeWidgetItem* parent) { QTreeWidgetItem* item = new_item(); item->setText(0, s->name); set_sequence_of_tree(item, s); if (ca != NULL) { ca->append(new NewSequenceCommand(item, parent)); if (open) ca->append(new ChangeSequenceAction(s)); } else { if (parent == NULL) { ui->treeWidget->addTopLevelItem(item); } else { parent->addChild(item); } if (open) set_sequence(s); } } void Project::start_preview_generator(QTreeWidgetItem* item, Media* media, bool replacing) { // set up throbber animation MediaThrobber* throbber = new MediaThrobber(item); item->setData(0, Qt::UserRole + 5, reinterpret_cast(throbber)); PreviewGenerator* pg = new PreviewGenerator(item, media, replacing); media->preview_gen = pg; connect(pg, SIGNAL(set_icon(int, bool)), throbber, SLOT(stop(int, bool))); pg->start(QThread::LowPriority); } QString Project::get_file_name_from_path(const QString& path) { return path.mid(path.lastIndexOf('/')+1); } QTreeWidgetItem* Project::new_item() { QTreeWidgetItem* item = new QTreeWidgetItem(); item->setData(0, Qt::UserRole + 5, 0); item->setFlags(item->flags() | Qt::ItemIsEditable); return item; } bool Project::is_focused() { return ui->treeWidget->hasFocus(); } QTreeWidgetItem* Project::new_folder(QString name) { QTreeWidgetItem* item = new_item(); item->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); item->setText(0, (name.isEmpty()) ? "New Folder" : name); item->setIcon(0, QIcon(":/icons/folder.png")); set_item_to_folder(item); return item; } void Project::get_all_media_from_table(QList items, QList& list, int search_type) { for (int i=0;i children; for (int j=0;jchildCount();j++) { children.append(item->child(j)); } get_all_media_from_table(children, list, search_type); } else if (search_type == type || search_type == -1) { list.append(item); } } } bool delete_clips_in_clipboard_with_media(ComboAction* ca, Media* m) { int delete_count = 0; for (int i=0;iclip_clipboard.size();i++) { Clip* c = panel_timeline->clip_clipboard.at(i); if (c->media == m) { ca->append(new RemoveClipsFromClipboard(i-delete_count)); delete_count++; } } return (delete_count > 0); } void Project::delete_selected_media() { ComboAction* ca = new ComboAction(); QList items = ui->treeWidget->selectedItems(); bool remove = true; bool redraw = false; // check if media is in use QVector parents; QList sequence_items; QList all_top_level_items; for (int i=0;itreeWidget->topLevelItemCount();i++) { all_top_level_items.append(ui->treeWidget->topLevelItem(i)); } 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_all_media_from_table(items, media_items, MEDIA_TYPE_FOOTAGE); for (int i=0;iclips.size();k++) { Clip* c = s->clips.at(k); if (c != NULL && c->media == media) { if (!confirm_delete) { // we found a reference, so we know we'll need to ask if the user wants to delete it QMessageBox confirm(this); confirm.setWindowTitle("Delete media in use?"); confirm.setText("The media '" + media->name + "' is currently used in '" + s->name + "'. Deleting it will remove all instances in the sequence. Are you sure you want to do this?"); QAbstractButton* yes_button = confirm.addButton(QMessageBox::Yes); QAbstractButton* skip_button = NULL; if (items.size() > 1) skip_button = confirm.addButton("Skip", QMessageBox::NoRole); QAbstractButton* abort_button = confirm.addButton(QMessageBox::Cancel); confirm.exec(); if (confirm.clickedButton() == yes_button) { // remove all clips referencing this media confirm_delete = true; redraw = true; } else if (confirm.clickedButton() == skip_button) { // remove media item and any folders containing it from the remove list QTreeWidgetItem* parent = item; while (parent != NULL) { parents.append(parent); // re-add item's siblings for (int m=0;mchildCount();m++) { QTreeWidgetItem* child = parent->child(m); bool found = false; for (int n=0;nparent(); } j = sequence_items.size(); k = s->clips.size(); } else if (confirm.clickedButton() == abort_button) { // break out of loop i = media_items.size(); j = sequence_items.size(); k = s->clips.size(); remove = false; } } if (confirm_delete) { ca->append(new DeleteClipAction(s, k)); } } } } if (confirm_delete) { delete_clips_in_clipboard_with_media(ca, media); } } } // remove if (remove) { // remove media and parents for (int m=0;mappend(new DeleteMediaCommand(items.at(i))); if (get_type_from_tree(items.at(i)) == MEDIA_TYPE_SEQUENCE) { redraw = true; Sequence* s = get_sequence_from_tree(items.at(i)); if (s == sequence) { ca->append(new ChangeSequenceAction(NULL)); } if (s == panel_footage_viewer->seq) { panel_footage_viewer->set_media(MEDIA_TYPE_SEQUENCE, NULL); } } else if (get_type_from_tree(items.at(i)) == MEDIA_TYPE_FOOTAGE) { if (panel_footage_viewer->seq != NULL) { for (int j=0;jseq->clips.size();j++) { Clip* c = panel_footage_viewer->seq->clips.at(j); if (c != NULL) { if (c->media == get_media_from_tree(items.at(i))) { panel_footage_viewer->set_media(MEDIA_TYPE_SEQUENCE, NULL); } break; } } } } } undo_stack.push(ca); // redraw clips if (redraw) { update_ui(true); } } else { delete ca; } } void Project::process_file_list(bool recursive, QStringList& files, QTreeWidgetItem* parent, QTreeWidgetItem* replace) { bool imported = false; QVector image_sequence_urls; QVector image_sequence_importassequence; QStringList image_sequence_formats = config.img_seq_formats.split("|"); if (!recursive) last_imported_media.clear(); bool create_undo_action = (!recursive && replace == NULL); ComboAction* ca; if (create_undo_action) ca = new ComboAction(); for (int i=0;iappend(new AddMediaCommand(folder, parent)); } else { parent->addChild(folder); } imported = true; } else if (!files.at(i).isEmpty()) { QString file(files.at(i)); bool skip = false; /* Heuristic to determine whether file is part of an image sequence */ // check file extension (assume it's not a int lastcharindex = file.lastIndexOf("."); bool found = true; if (lastcharindex != -1 && lastcharindex > file.lastIndexOf('/')) { // image_sequence_formats found = false; QString ext = file.mid(lastcharindex+1); for (int j=0;jreset(); } else { item = new_item(); m = new Media(); } m->using_inout = false; m->url = file; m->name = get_file_name_from_path(files.at(i)); // generate waveform/thumbnail in another thread start_preview_generator(item, m, replace != NULL); set_footage_of_tree(item, m); last_imported_media.append(m); if (replace == NULL) { if (create_undo_action) { ca->append(new AddMediaCommand(item, parent)); } else { parent->addChild(item); } } imported = true; } } } if (create_undo_action) { if (imported) { undo_stack.push(ca); } else { delete ca; } } } 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; } bool Project::reveal_media(void *media, QTreeWidgetItem* parent) { int count = (parent == NULL) ? ui->treeWidget->topLevelItemCount() : parent->childCount(); for (int i=0;itreeWidget->topLevelItem(i) : parent->child(i); if (get_type_from_tree(item) == MEDIA_TYPE_FOLDER) { if (reveal_media(media, item)) return true; } else if (get_media_from_tree(item) == media) { // expand all folders leading to this media QTreeWidgetItem* hierarchy = item->parent(); while (hierarchy != NULL) { hierarchy->setExpanded(true); hierarchy = hierarchy->parent(); } // select item item->setSelected(true); return true; } } return false; } void Project::import_dialog() { QFileDialog fd(this, "Import media...", "", "All Files (*)"); fd.setFileMode(QFileDialog::ExistingFiles); if (fd.exec()) { QStringList files = fd.selectedFiles(); process_file_list(false, files, get_selected_folder(), NULL); } } void set_item_to_folder(QTreeWidgetItem* item) { item->setData(0, Qt::UserRole + 1, MEDIA_TYPE_FOLDER); } void* get_media_from_tree(QTreeWidgetItem* item) { return reinterpret_cast(item->data(0, Qt::UserRole + 2).value()); /*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: dout << "[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_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))); } Sequence* get_sequence_from_tree(QTreeWidgetItem* item) { return reinterpret_cast(item->data(0, Qt::UserRole + 2).value()); } QString get_channel_layout_name(int channels, int layout) { switch (channels) { case 0: return "Invalid"; break; case 1: return "Mono"; break; case 2: return "Stereo"; break; default: { char buf[50]; av_get_channel_layout_string(buf, sizeof(buf), channels, layout); return QString(buf); } } } void set_sequence_of_tree(QTreeWidgetItem* item, Sequence* s) { item->setData(0, Qt::UserRole + 1, MEDIA_TYPE_SEQUENCE); item->setData(0, Qt::UserRole + 2, QVariant::fromValue(reinterpret_cast(s))); item->setToolTip(0, "Name: " + s->name + "\nVideo Dimensions: " + QString::number(s->width) + "x" + QString::number(s->height) + "\nFrame Rate: " + QString::number(s->frame_rate) + "\nAudio Frequency: " + QString::number(s->audio_frequency) + "\nAudio Layout: " + get_channel_layout_name(av_get_channel_layout_nb_channels(s->audio_layout), s->audio_layout)); item->setIcon(0, QIcon(":/icons/sequence.png")); } int get_type_from_tree(QTreeWidgetItem* item) { return item->data(0, Qt::UserRole + 1).toInt(); } void Project::delete_media(QTreeWidgetItem* item) { int type = get_type_from_tree(item); void* media = get_media_from_tree(item); switch (type) { case MEDIA_TYPE_FOOTAGE: delete static_cast(media); break; case MEDIA_TYPE_SEQUENCE: delete static_cast(media); break; } } 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 { ComboAction* ca = new ComboAction(); bool deleted = false; QList items = source_table->selectedItems(); for (int i=0;iclips.size();i++) { Clip* c = sequence->clips.at(i); if (c != NULL) { for (int j=0;jmedia == m) { ca->append(new DeleteClipAction(sequence, i)); deleted = true; } } } } for (int j=0;jclear_effects(true); // delete sequences first because it's important to close all the clips before deleting the media QVector sequences = list_all_project_sequences(); for (int i=0;itreeWidget->topLevelItemCount() > 0) { QTreeWidgetItem* item = ui->treeWidget->topLevelItem(0); if (get_type_from_tree(item) != MEDIA_TYPE_SEQUENCE) delete_media(item); // already deleted if (item->data(0, Qt::UserRole + 5) != 0) delete reinterpret_cast(item->data(0, Qt::UserRole + 5).value()); delete item; } } void Project::new_project() { // clear existing project set_sequence(NULL); clear(); mainWindow->setWindowModified(false); } QTreeWidgetItem* Project::find_loaded_folder_by_id(int id) { for (int j=0;jdata(0, Qt::UserRole + 3).toInt() == id) { return parent_item; } } return NULL; } bool Project::load_worker(QFile& f, QXmlStreamReader& stream, int type) { f.seek(0); stream.setDevice(stream.device()); QString root_search; QString child_search; switch (type) { case LOAD_TYPE_VERSION: root_search = "version"; break; case LOAD_TYPE_URL: root_search = "url"; break; case MEDIA_TYPE_FOLDER: root_search = "folders"; child_search = "folder"; break; case MEDIA_TYPE_FOOTAGE: root_search = "media"; child_search = "footage"; break; case MEDIA_TYPE_SEQUENCE: root_search = "sequences"; child_search = "sequence"; break; } show_err = true; while (!stream.atEnd()) { stream.readNextStartElement(); if (stream.name() == root_search) { if (type == LOAD_TYPE_VERSION) { int proj_version = stream.readElementText().toInt(); if (proj_version < MIN_SAVE_VERSION && proj_version > SAVE_VERSION) { if (QMessageBox::warning(this, "Version Mismatch", "This project was saved in a different version of Olive and may not be fully compatible with this version. Would you like to attempt loading it anyway?", QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { show_err = false; return false; } } } else if (type == LOAD_TYPE_URL) { internal_proj_url = stream.readElementText(); internal_proj_dir = QFileInfo(internal_proj_url).absoluteDir(); } else { while (!(stream.name() == root_search && stream.isEndElement())) { stream.readNext(); if (stream.name() == child_search && stream.isStartElement()) { switch (type) { case MEDIA_TYPE_FOLDER: { QTreeWidgetItem* folder = new_folder(0); for (int j=0;jsetData(0, Qt::UserRole + 3, attr.value().toInt()); } else if (attr.name() == "name") { folder->setText(0, attr.value().toString()); } else if (attr.name() == "parent") { folder->setData(0, Qt::UserRole + 4, attr.value().toInt()); } } loaded_folders.append(folder); } break; case MEDIA_TYPE_FOOTAGE: { int folder = 0; QTreeWidgetItem* item = new_item(); Media* m = new Media(); m->using_inout = false; for (int j=0;jsave_id = attr.value().toInt(); } else if (attr.name() == "folder") { folder = attr.value().toInt(); } else if (attr.name() == "name") { m->name = attr.value().toString(); } else if (attr.name() == "url") { m->url = attr.value().toString(); if (!QFileInfo::exists(m->url)) { // if path is not absolute QString proj_dir_test = proj_dir.absoluteFilePath(m->url); QString internal_proj_dir_test = internal_proj_dir.absoluteFilePath(m->url); if (QFileInfo::exists(proj_dir_test)) { // if path is relative to the project's current dir m->url = proj_dir_test; dout << "[INFO] Matched" << attr.value().toString() << "relative to project's current directory"; } else if (QFileInfo::exists(internal_proj_dir_test)) { // if path is relative to the last directory the project was saved in m->url = internal_proj_dir_test; dout << "[INFO] Matched" << attr.value().toString() << "relative to project's internal directory"; } else { dout << "[INFO] Failed to match" << attr.value().toString() << "to file"; } } else { dout << "[INFO] Matched" << attr.value().toString() << "with absolute path"; } } else if (attr.name() == "duration") { m->length = attr.value().toLongLong(); } else if (attr.name() == "using_inout") { m->using_inout = (attr.value() == "1"); } else if (attr.name() == "in") { m->in = attr.value().toLong(); } else if (attr.name() == "out") { m->out = attr.value().toLong(); } } set_footage_of_tree(item, m); if (folder == 0) { ui->treeWidget->addTopLevelItem(item); } else { find_loaded_folder_by_id(folder)->addChild(item); } // analyze media to see if it's the same loaded_media.append(m); loaded_media_items.append(item); } break; case MEDIA_TYPE_SEQUENCE: { QTreeWidgetItem* parent = NULL; Sequence* s = new Sequence(); // load attributes about sequence for (int j=0;jname = attr.value().toString(); } else if (attr.name() == "folder") { int folder = attr.value().toInt(); if (folder > 0) parent = find_loaded_folder_by_id(folder); } else if (attr.name() == "id") { s->save_id = attr.value().toInt(); } else if (attr.name() == "width") { s->width = attr.value().toInt(); } else if (attr.name() == "height") { s->height = attr.value().toInt(); } else if (attr.name() == "framerate") { s->frame_rate = attr.value().toDouble(); } else if (attr.name() == "afreq") { s->audio_frequency = attr.value().toInt(); } else if (attr.name() == "alayout") { s->audio_layout = attr.value().toInt(); } else if (attr.name() == "open") { open_seq = s; } else if (attr.name() == "workarea") { s->using_workarea = (attr.value() == "1"); } else if (attr.name() == "workareaIn") { s->workarea_in = attr.value().toLong(); } else if (attr.name() == "workareaOut") { s->workarea_out = attr.value().toLong(); } } // load all clips and clip information while (!(stream.name() == child_search && stream.isEndElement()) && !stream.atEnd()) { stream.readNextStartElement(); if (stream.name() == "marker" && stream.isStartElement()) { Marker m; for (int j=0;jmarkers.append(m); } else if (stream.name() == "clip" && stream.isStartElement()) { int media_id, stream_id; Clip* c = new Clip(s); // backwards compatibility code c->autoscale = false; c->media = NULL; for (int j=0;jname = attr.value().toString(); } else if (attr.name() == "enabled") { c->enabled = (attr.value() == "1"); } else if (attr.name() == "id") { c->load_id = attr.value().toInt(); } else if (attr.name() == "clipin") { c->clip_in = attr.value().toLong(); } else if (attr.name() == "in") { c->timeline_in = attr.value().toLong(); } else if (attr.name() == "out") { c->timeline_out = attr.value().toLong(); } else if (attr.name() == "track") { c->track = attr.value().toInt(); } else if (attr.name() == "r") { c->color_r = attr.value().toInt(); } else if (attr.name() == "g") { c->color_g = attr.value().toInt(); } else if (attr.name() == "b") { c->color_b = attr.value().toInt(); } else if (attr.name() == "autoscale") { c->autoscale = (attr.value() == "1"); } else if (attr.name() == "type") { c->media_type = attr.value().toInt(); } else if (attr.name() == "media") { c->media_type = MEDIA_TYPE_FOOTAGE; media_id = attr.value().toInt(); } else if (attr.name() == "stream") { stream_id = attr.value().toInt(); } else if (attr.name() == "speed") { c->speed = attr.value().toDouble(); } else if (attr.name() == "maintainpitch") { c->maintain_audio_pitch = (attr.value() == "1"); } else if (attr.name() == "reverse") { c->reverse = (attr.value() == "1"); } else if (attr.name() == "sequence") { c->media_type = MEDIA_TYPE_SEQUENCE; // since we haven't finished loading sequences, we defer linking this until later c->media = NULL; c->media_stream = attr.value().toInt(); loaded_clips.append(c); } } // set media and media stream switch (c->media_type) { case MEDIA_TYPE_FOOTAGE: if (media_id == 0) { c->media = NULL; } else { for (int j=0;jsave_id == media_id) { c->media = m; c->media_stream = stream_id; break; } } } break; } // load links and effects while (!(stream.name() == "clip" && stream.isEndElement()) && !stream.atEnd()) { stream.readNext(); if (stream.isStartElement()) { if (stream.name() == "linked") { while (!(stream.name() == "linked" && stream.isEndElement()) && !stream.atEnd()) { stream.readNext(); if (stream.name() == "link" && stream.isStartElement()) { for (int k=0;klinked.append(link_attr.value().toInt()); break; } } } } } else if (stream.isStartElement() && (stream.name() == "effect" || stream.name() == "opening" || stream.name() == "closing")) { int effect_id = -1; QString effect_name; bool effect_enabled = true; long effect_pre_length = -1; long effect_post_length = -1; for (int j=0;jtrack < 0) ? "Transform" : "Volume"; break; case 1: effect_name = (c->track < 0) ? "Shake" : "Pan"; break; case 2: effect_name = (c->track < 0) ? "Text" : "Noise"; break; case 3: effect_name = (c->track < 0) ? "Solid" : "Tone"; break; case 4: effect_name = "Invert"; break; case 5: effect_name = "Chroma Key"; break; case 6: effect_name = "Gaussian Blur"; break; case 7: effect_name = "Crop"; break; case 8: effect_name = "Flip"; break; case 9: effect_name = "Box Blur"; break; case 10: effect_name = "Wave"; break; case 11: effect_name = "Temperature"; break; } } // wait for effects to be loaded effects_loaded.lock(); const EffectMeta* meta = NULL; // find effect with this name if (!effect_name.isEmpty()) { QVector& effect_list = (c->track < 0) ? video_effects : audio_effects; for (int j=0;jsequence->transitions.at(transition_index); if (effect_pre_length > -1) t->length = effect_pre_length; if (effect_post_length > -1) t->length2 = effect_post_length; t->set_enabled(effect_enabled); t->load(stream); if (tag == "opening") { c->opening_transition = transition_index; } else { c->closing_transition = transition_index; } } else { Effect* e = create_effect(c, meta); e->set_enabled(effect_enabled); e->load(stream); c->effects.append(e); } } } } } s->clips.append(c); } } // correct links and clip IDs for (int i=0;iclips.size();i++) { // correct links Clip* correct_clip = s->clips.at(i); for (int j=0;jlinked.size();j++) { bool found = false; for (int k=0;kclips.size();k++) { if (s->clips.at(k)->load_id == correct_clip->linked.at(j)) { correct_clip->linked[j] = k; found = true; break; } } if (!found) { correct_clip->linked.removeAt(j); j--; if (QMessageBox::warning(this, "Invalid Clip Link", "This project contains an invalid clip link. It may be corrupt. Would you like to continue loading it?", QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { delete s; return false; } } } } new_sequence(NULL, s, false, parent); loaded_sequences.append(s); } break; } } } } break; } } return true; } void Project::load_project(bool autorecovery) { new_project(); /*LoadDialog ld; ld.exec();*/ QFile file(project_url); if (!file.open(QIODevice::ReadOnly)) { dout << "[ERROR] Could not open file"; return; } /* set up directories to search for media * most of the time, these will be the same but in * case the project file has moved without the footage, * we check both */ proj_dir = QFileInfo(project_url).absoluteDir(); internal_proj_dir = QFileInfo(project_url).absoluteDir(); internal_proj_url = project_url; QXmlStreamReader stream(&file); bool cont = false; error_str.clear(); show_err = true; // temp variables for loading loaded_folders.clear(); loaded_media.clear(); loaded_media_items.clear(); loaded_clips.clear(); loaded_sequences.clear(); open_seq = NULL; // get "element" count int element_count = 0; while (!stream.atEnd()) { stream.readNextStartElement(); if (stream.name() == "folder" || stream.name() == "footage" || stream.name() == "sequence" || stream.name() == "clip" || stream.name() == "effect") { element_count++; } } // find project file version cont = load_worker(file, stream, LOAD_TYPE_VERSION); // find project's internal URL cont = load_worker(file, stream, LOAD_TYPE_URL); if (autorecovery) { QString orig_filename = internal_proj_url; int insert_index = internal_proj_url.lastIndexOf(".ove", -1, Qt::CaseInsensitive); if (insert_index == -1) insert_index = internal_proj_url.length(); int counter = 1; while (QFileInfo::exists(orig_filename)) { orig_filename = internal_proj_url; QString recover_text = "recovered"; if (counter > 1) { recover_text += " " + QString::number(counter); } orig_filename.insert(insert_index, " (" + recover_text + ")"); counter++; } mainWindow->updateTitle(orig_filename); } // load folders first if (cont) { cont = load_worker(file, stream, MEDIA_TYPE_FOLDER); } // load media if (cont) { // since folders loaded correctly, organize them appropriately for (int i=0;idata(0, Qt::UserRole + 4).toInt(); if (parent > 0) { find_loaded_folder_by_id(parent)->addChild(folder); } else { ui->treeWidget->addTopLevelItem(folder); } } cont = load_worker(file, stream, MEDIA_TYPE_FOOTAGE); } // load sequences if (cont) { cont = load_worker(file, stream, MEDIA_TYPE_SEQUENCE); } // attach nested sequence clips to their sequences for (int i=0;imedia_stream == loaded_sequences.at(j)->save_id) { loaded_clips.at(i)->media = loaded_sequences.at(j); loaded_clips.at(i)->refresh(); break; } } } if (!cont) { if (show_err) QMessageBox::critical(this, "Project Load Error", "Error loading project: " + error_str, QMessageBox::Ok); } else if (stream.hasError()) { dout << "[ERROR] Error parsing XML." << stream.errorString(); QMessageBox::critical(this, "XML Parsing Error", "Couldn't load '" + project_url + "'. " + stream.errorString(), QMessageBox::Ok); cont = false; } if (cont) { if (open_seq != NULL) set_sequence(open_seq); update_ui(false); mainWindow->setWindowModified(autorecovery); for (int i=0;itreeWidget->topLevelItemCount() : parent->childCount(); for (int i=0;itreeWidget->topLevelItem(i) : parent->child(i); int item_type = get_type_from_tree(item); if (type == item_type) { if (item_type == MEDIA_TYPE_FOLDER) { if (set_ids_only) { item->setData(0, Qt::UserRole + 3, folder_id); // saves a temporary ID for matching in the project file folder_id++; } else { // if we're saving folders, save the folder stream.writeStartElement("folder"); stream.writeAttribute("name", item->text(0)); stream.writeAttribute("id", QString::number(item->data(0, Qt::UserRole + 3).toInt())); if (item->parent() == NULL) { stream.writeAttribute("parent", "0"); } else { stream.writeAttribute("parent", QString::number(item->parent()->data(0, Qt::UserRole + 3).toInt())); } stream.writeEndElement(); } // save_folder(stream, item, type, set_ids_only); } else { int folder = root ? 0 : parent->data(0, Qt::UserRole + 3).toInt(); if (type == MEDIA_TYPE_FOOTAGE) { Media* m = get_footage_from_tree(item); m->save_id = media_id; stream.writeStartElement("footage"); stream.writeAttribute("id", QString::number(media_id)); stream.writeAttribute("folder", QString::number(folder)); stream.writeAttribute("name", m->name); stream.writeAttribute("url", proj_dir.relativeFilePath(m->url)); stream.writeAttribute("duration", QString::number(m->length)); stream.writeAttribute("using_inout", QString::number(m->using_inout)); stream.writeAttribute("in", QString::number(m->in)); stream.writeAttribute("out", QString::number(m->out)); for (int j=0;jvideo_tracks.size();j++) { MediaStream* ms = m->video_tracks.at(j); stream.writeStartElement("video"); stream.writeAttribute("id", QString::number(ms->file_index)); stream.writeAttribute("width", QString::number(ms->video_width)); stream.writeAttribute("height", QString::number(ms->video_height)); stream.writeAttribute("framerate", QString::number(ms->video_frame_rate, 'f', 10)); stream.writeAttribute("infinite", QString::number(ms->infinite_length)); stream.writeEndElement(); } for (int j=0;jaudio_tracks.size();j++) { MediaStream* ms = m->audio_tracks.at(j); stream.writeStartElement("audio"); stream.writeAttribute("id", QString::number(ms->file_index)); stream.writeAttribute("channels", QString::number(ms->audio_channels)); stream.writeAttribute("layout", QString::number(ms->audio_layout)); stream.writeAttribute("frequency", QString::number(ms->audio_frequency)); stream.writeEndElement(); } stream.writeEndElement(); media_id++; } else if (type == MEDIA_TYPE_SEQUENCE) { Sequence* s = get_sequence_from_tree(item); if (set_ids_only) { s->save_id = sequence_id; sequence_id++; } else { stream.writeStartElement("sequence"); stream.writeAttribute("id", QString::number(s->save_id)); stream.writeAttribute("folder", QString::number(folder)); stream.writeAttribute("name", s->name); stream.writeAttribute("width", QString::number(s->width)); stream.writeAttribute("height", QString::number(s->height)); stream.writeAttribute("framerate", QString::number(s->frame_rate, 'f', 10)); stream.writeAttribute("afreq", QString::number(s->audio_frequency)); stream.writeAttribute("alayout", QString::number(s->audio_layout)); if (s == sequence) { stream.writeAttribute("open", "1"); } stream.writeAttribute("workarea", QString::number(s->using_workarea)); stream.writeAttribute("workareaIn", QString::number(s->workarea_in)); stream.writeAttribute("workareaOut", QString::number(s->workarea_out)); for (int j=0;jclips.size();j++) { Clip* c = s->clips.at(j); if (c != NULL) { stream.writeStartElement("clip"); // clip stream.writeAttribute("id", QString::number(j)); stream.writeAttribute("enabled", QString::number(c->enabled)); stream.writeAttribute("name", c->name); stream.writeAttribute("clipin", QString::number(c->clip_in)); stream.writeAttribute("in", QString::number(c->timeline_in)); stream.writeAttribute("out", QString::number(c->timeline_out)); stream.writeAttribute("track", QString::number(c->track)); stream.writeAttribute("r", QString::number(c->color_r)); stream.writeAttribute("g", QString::number(c->color_g)); stream.writeAttribute("b", QString::number(c->color_b)); stream.writeAttribute("autoscale", QString::number(c->autoscale)); stream.writeAttribute("speed", QString::number(c->speed, 'f', 10)); stream.writeAttribute("maintainpitch", QString::number(c->maintain_audio_pitch)); stream.writeAttribute("reverse", QString::number(c->reverse)); stream.writeAttribute("type", QString::number(c->media_type)); switch (c->media_type) { case MEDIA_TYPE_FOOTAGE: stream.writeAttribute("media", QString::number(static_cast(c->media)->save_id)); stream.writeAttribute("stream", QString::number(c->media_stream)); break; case MEDIA_TYPE_SEQUENCE: stream.writeAttribute("sequence", QString::number(static_cast(c->media)->save_id)); break; } stream.writeStartElement("linked"); // linked for (int k=0;klinked.size();k++) { stream.writeStartElement("link"); // link stream.writeAttribute("id", QString::number(c->linked.at(k))); stream.writeEndElement(); // link } stream.writeEndElement(); // linked for (int k=0;keffects.size();k++) { stream.writeStartElement("effect"); // effect c->effects.at(k)->save(stream); stream.writeEndElement(); // effect } if (c->get_opening_transition() != NULL) { stream.writeStartElement("opening"); c->get_opening_transition()->save(stream); stream.writeEndElement(); // opening } if (c->get_closing_transition() != NULL) { stream.writeStartElement("closing"); // closing c->get_closing_transition()->save(stream); stream.writeEndElement(); // closing } stream.writeEndElement(); // clip } } for (int j=0;jmarkers.size();j++) { stream.writeStartElement("marker"); stream.writeAttribute("frame", QString::number(s->markers.at(j).frame)); stream.writeAttribute("name", s->markers.at(j).name); stream.writeEndElement(); } stream.writeEndElement(); } } } } if (item_type == MEDIA_TYPE_FOLDER) { save_folder(stream, item, type, set_ids_only); } } } void Project::save_project(bool autorecovery) { folder_id = 1; media_id = 1; sequence_id = 1; QFile file(autorecovery ? autorecovery_filename : project_url); if (!file.open(QIODevice::WriteOnly/* | QIODevice::Text*/)) { dout << "[ERROR] Could not open file"; return; } QXmlStreamWriter stream(&file); stream.setAutoFormatting(true); stream.writeStartDocument(); // doc stream.writeStartElement("project"); // project stream.writeTextElement("version", QString::number(SAVE_VERSION)); stream.writeTextElement("url", project_url); proj_dir = QFileInfo(project_url).absoluteDir(); save_folder(stream, NULL, MEDIA_TYPE_FOLDER, true); stream.writeStartElement("folders"); // folders save_folder(stream, NULL, MEDIA_TYPE_FOLDER, false); stream.writeEndElement(); // folders stream.writeStartElement("media"); // media save_folder(stream, NULL, MEDIA_TYPE_FOOTAGE, false); stream.writeEndElement(); // media save_folder(stream, NULL, MEDIA_TYPE_SEQUENCE, true); stream.writeStartElement("sequences"); // sequences save_folder(stream, NULL, MEDIA_TYPE_SEQUENCE, false); stream.writeEndElement();// sequences stream.writeEndElement(); // project stream.writeEndDocument(); // doc file.close(); if (!autorecovery) { add_recent_project(project_url); mainWindow->setWindowModified(false); } } void Project::save_recent_projects() { // save to file QFile f(recent_proj_file); if (f.open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) { QTextStream out(&f); for (int i=0;i 0) { out << "\n"; } out << recent_projects.at(i); } f.close(); } else { dout << "[WARNING] Could not save recent projects"; } } void Project::clear_recent_projects() { recent_projects.clear(); save_recent_projects(); } void Project::add_recent_project(QString url) { bool found = false; for (int i=0;i MAXIMUM_RECENT_PROJECTS) { recent_projects.removeLast(); } } save_recent_projects(); } void Project::list_all_sequences_worker(QVector* list, QTreeWidgetItem* parent) { int len = (parent == NULL) ? ui->treeWidget->topLevelItemCount() : parent->childCount(); for (int i=0;itreeWidget->topLevelItem(i) : parent->child(i); if (get_type_from_tree(item) == MEDIA_TYPE_SEQUENCE) { list->append(get_sequence_from_tree(item)); } else if (get_type_from_tree(item) == MEDIA_TYPE_FOLDER) { list_all_sequences_worker(list, item); } } } QVector Project::list_all_project_sequences() { QVector list; list_all_sequences_worker(&list, NULL); return list; } #define THROBBER_LIMIT 20 #define THROBBER_SIZE 50 MediaThrobber::MediaThrobber(QTreeWidgetItem *i) : pixmap(":/icons/throbber.png"), animation(0), item(i) { // set up throbber animation_update(); animator.setInterval(20); connect(&animator, SIGNAL(timeout()), this, SLOT(animation_update())); animator.start(); } void MediaThrobber::animation_update() { if (animation == THROBBER_LIMIT) { animation = 0; } item->setIcon(0, QIcon(pixmap.copy(THROBBER_SIZE*animation, 0, THROBBER_SIZE, THROBBER_SIZE))); animation++; } void MediaThrobber::stop(int icon_type, bool replace) { animator.stop(); switch (icon_type) { case ICON_TYPE_VIDEO: item->setIcon(0, QIcon(":/icons/videosource.png")); break; case ICON_TYPE_AUDIO: item->setIcon(0, QIcon(":/icons/audiosource.png")); break; case ICON_TYPE_IMAGE: item->setIcon(0, QIcon(":/icons/imagesource.png")); break; case ICON_TYPE_ERROR: item->setIcon(0, QIcon::fromTheme("dialog-error")); break; } // refresh all clips QVector sequences = panel_project->list_all_project_sequences(); for (int i=0;iclips.size();j++) { Clip* c = s->clips.at(j); if (c != NULL) { c->refresh(); } } } // redraw clips update_ui(replace); panel_project->source_table->viewport()->update(); item->setData(0, Qt::UserRole + 5, 0); deleteLater(); } QString get_interlacing_name(int interlacing) { switch (interlacing) { case VIDEO_PROGRESSIVE: return "None (Progressive)"; case VIDEO_TOP_FIELD_FIRST: return "Top Field First"; case VIDEO_BOTTOM_FIELD_FIRST: return "Bottom Field First"; default: return "Invalid"; } } void update_footage_tooltip(QTreeWidgetItem *item, Media *media, QString error) { QString tooltip = "Name: " + media->name + "\nFilename: " + media->url + "\n"; if (error.isEmpty()) { if (media->video_tracks.size() > 0) { tooltip += "Video Dimensions: "; for (int i=0;ivideo_tracks.size();i++) { if (i > 0) { tooltip += ", "; } tooltip += QString::number(media->video_tracks.at(i)->video_width) + "x" + QString::number(media->video_tracks.at(i)->video_height); } tooltip += "\n"; if (!media->video_tracks.at(0)->infinite_length) { tooltip += "Frame Rate: "; for (int i=0;ivideo_tracks.size();i++) { if (i > 0) { tooltip += ", "; } if (media->video_tracks.at(i)->video_interlacing == VIDEO_PROGRESSIVE) { tooltip += QString::number(media->video_tracks.at(i)->video_frame_rate); } else { tooltip += QString::number(media->video_tracks.at(i)->video_frame_rate * 2); tooltip += " fields (" + QString::number(media->video_tracks.at(i)->video_frame_rate) + " frames)"; } } tooltip += "\n"; } tooltip += "Interlacing: "; for (int i=0;ivideo_tracks.size();i++) { if (i > 0) { tooltip += ", "; } tooltip += get_interlacing_name(media->video_tracks.at(i)->video_interlacing); } } if (media->audio_tracks.size() > 0) { tooltip += "\n"; tooltip += "Audio Frequency: "; for (int i=0;iaudio_tracks.size();i++) { if (i > 0) { tooltip += ", "; } tooltip += QString::number(media->audio_tracks.at(i)->audio_frequency); } tooltip += "\n"; tooltip += "Audio Channels: "; for (int i=0;iaudio_tracks.size();i++) { if (i > 0) { tooltip += ", "; } tooltip += get_channel_layout_name(media->audio_tracks.at(i)->audio_channels, media->audio_tracks.at(i)->audio_layout); } // tooltip += "\n"; } } else { tooltip = error; } item->setToolTip(0, tooltip); }