#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 "effects/effects.h" #include "panels/timeline.h" #include "project/sequence.h" #include "project/effect.h" #include "effects/transition.h" #include "io/previewgenerator.h" #include "project/undo.h" #include "mainwindow.h" #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include #include } #define MAXIMUM_RECENT_PROJECTS 10 QString autorecovery_filename; bool project_changed = false; 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() { int n = 1; bool found = true; QString name; while (found) { found = false; name = "Sequence "; 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; } 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_SEQUENCE: get_sequence_from_tree(item)->name = n; break; } } void Project::duplicate_selected() { QList items = ui->treeWidget->selectedItems(); bool duped = false; TimelineAction* ta = new TimelineAction(); for (int j=0;jcopy(), false, i->parent()); duped = true; } } if (duped) { undo_stack.push(ta); } else { delete ta; } } void Project::new_sequence(TimelineAction *ta, Sequence *s, bool open, QTreeWidgetItem* parent) { QTreeWidgetItem* item = new_item(); item->setText(0, s->name); set_sequence_of_tree(item, s); if (ta != NULL) { ta->new_sequence(item, parent); if (open) ta->change_sequence(s); } else { if (parent == NULL) { ui->treeWidget->addTopLevelItem(item); } else { parent->addChild(item); } if (open) set_sequence(s); } project_changed = true; } void Project::start_preview_generator(QTreeWidgetItem* item, Media* media) { // set up throbber animation MediaThrobber* throbber = new MediaThrobber(item); PreviewGenerator* pg = new PreviewGenerator(item, media); connect(pg, SIGNAL(set_icon(int)), throbber, SLOT(stop(int))); connect(pg, SIGNAL(finished()), pg, SLOT(deleteLater())); pg->start(QThread::LowPriority); } QTreeWidgetItem* Project::import_file(QString file) { QTreeWidgetItem* item = new_item(); Media* m = new Media(); m->url = file; m->name = file.mid(file.lastIndexOf('/')+1); set_media_of_tree(item, m); // generate waveform/thumbnail in another thread start_preview_generator(item, m); return item; } QTreeWidgetItem* Project::new_item() { QTreeWidgetItem* item = new QTreeWidgetItem(); item->setFlags(item->flags() | Qt::ItemIsEditable); return item; } bool Project::is_focused() { return ui->treeWidget->hasFocus(); } QTreeWidgetItem* Project::new_folder() { QTreeWidgetItem* item = new_item(); item->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); item->setText(0, "New Folder"); 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) { for (int i=0;i children; for (int j=0;jchildCount();j++) { children.append(item->child(j)); } get_media_from_table(children, list, search_type); } else if (search_type == type) { list.append(item); } } } void Project::delete_selected_media() { TimelineAction* ta = new TimelineAction(); 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_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); for (int i=0;iclip_count();k++) { Clip* c = s->get_clip(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->clip_count(); } else if (confirm.clickedButton() == abort_button) { // break out of loop i = media_items.size(); j = sequence_items.size(); k = s->clip_count(); remove = false; } } if (confirm_delete) { ta->delete_clip(s, k); } } } } } } // remove if (remove) { // remove media and parents for (int m=0;mdelete_media(items.at(i)); } undo_stack.push(ta); // redraw clips if (redraw) { panel_timeline->redraw_all_clips(true); } project_changed = true; } else { delete ta; } } void Project::process_file_list(QStringList& files) { bool imported = false; TimelineAction* ta = new TimelineAction(); for (int i=0;iadd_media(import_file(file)); imported = true; project_changed = true; } if (imported) { undo_stack.push(ta); } else { delete ta; } } void Project::import_dialog() { QStringList files = QFileDialog::getOpenFileNames(this, "Import media...", "", "All Files (*.*)"); process_file_list(files); } void set_item_to_folder(QTreeWidgetItem* item) { item->setData(0, Qt::UserRole + 1, MEDIA_TYPE_FOLDER); } Media* get_media_from_tree(QTreeWidgetItem* item) { return reinterpret_cast(item->data(0, Qt::UserRole + 2).value()); } void set_media_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()); } void set_sequence_of_tree(QTreeWidgetItem* item, Sequence* sequence) { item->setData(0, Qt::UserRole + 1, MEDIA_TYPE_SEQUENCE); item->setData(0, Qt::UserRole + 2, QVariant::fromValue(reinterpret_cast(sequence))); } 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); 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; } project_changed = true; } void Project::clear() { while (ui->treeWidget->topLevelItemCount() > 0) { QTreeWidgetItem* item = ui->treeWidget->topLevelItem(0); delete_media(item); delete item; } } void Project::new_project() { // clear existing project clear(); project_changed = false; set_sequence(NULL); } 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 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; } while (!stream.atEnd()) { stream.readNextStartElement(); if (stream.name() == root_search) { if (type == LOAD_TYPE_VERSION) { if (stream.readElementText() != 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) { error_str = "Incompatible project version."; return false; } } } 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(); 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: { QString name; QString url; int id; int folder; for (int j=0;jurl = url; m->name = name; m->save_id = id; while (!(stream.name() == child_search && stream.isEndElement())) { stream.readNext(); if (stream.isStartElement()) { if (stream.name() == "video") { MediaStream* ms = new MediaStream(); for (int j=0;jfile_index = attr.value().toInt(); } else if (attr.name() == "width") { ms->video_width = attr.value().toInt(); } else if (attr.name() == "height") { ms->video_height = attr.value().toInt(); } else if (attr.name() == "framerate") { ms->video_frame_rate = attr.value().toDouble(); } else if (attr.name() == "infinite") { ms->infinite_length = (attr.value().toInt() == 1); } } m->video_tracks.append(ms); } else if (stream.name() == "audio") { MediaStream* ms = new MediaStream(); for (int j=0;jfile_index = attr.value().toInt(); } else if (attr.name() == "channels") { ms->audio_channels = attr.value().toInt(); } else if (attr.name() == "layout") { ms->audio_layout = attr.value().toInt(); } else if (attr.name() == "frequency") { ms->audio_frequency = attr.value().toInt(); } } m->audio_tracks.append(ms); } } } set_media_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 start_preview_generator(item, m); loaded_media.append(m); } break; case MEDIA_TYPE_SEQUENCE: { QTreeWidgetItem* parent = NULL; Sequence* s = new Sequence(); bool open_sequence = false; // 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() == "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().toFloat(); } 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_sequence = true; } else if (attr.name() == "workareaIn") { s->using_workarea = true; 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() == "clip" && stream.isStartElement()) { // int media_id, stream_id; Clip* c = new Clip(s); for (int j=0;jname = attr.value().toString(); } 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() == "media") { media_id = attr.value().toInt(); } else if (attr.name() == "stream") { stream_id = attr.value().toInt(); } } // set media and media stream for (int j=0;jsave_id == media_id) { c->media = m; c->media_stream = stream_id; break; } } // load links and effects while (!(stream.name() == "clip" && stream.isEndElement()) && !stream.atEnd()) { stream.readNext(); if (stream.name() == "linked" && stream.isStartElement()) { 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; } } } } } if (stream.name() == "effects" && stream.isStartElement()) { while (!(stream.name() == "effects" && stream.isEndElement()) && !stream.atEnd()) { stream.readNext(); if (stream.name() == "effect" && stream.isStartElement()) { int effect_id = -1; for (int j=0;jload(&stream); c->effects.append(e); } } } } } s->add_clip(c); } } // correct links and clip IDs for (int i=0;iclip_count();i++) { // correct links Clip* correct_clip = s->get_clip(i); for (int j=0;jlinked.size();j++) { bool found = false; for (int k=0;kclip_count();k++) { if (s->get_clip(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); if (open_sequence) set_sequence(s); } break; } } } } break; } } return true; } void Project::load_project() { new_project(); QFile file(project_url); if (!file.open(QIODevice::ReadOnly)) { qDebug() << "[ERROR] Could not open file"; return; } QXmlStreamReader stream(&file); bool cont = false; error_str.clear(); // temp variables for loading loaded_folders.clear(); loaded_media.clear(); // find project file version cont = load_worker(file, stream, LOAD_TYPE_VERSION); // 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) { ui->treeWidget->takeTopLevelItem(ui->treeWidget->indexOfTopLevelItem(folder)); find_loaded_folder_by_id(parent)->addChild(folder); } } cont = load_worker(file, stream, MEDIA_TYPE_FOOTAGE); } // load sequences if (cont) { cont = load_worker(file, stream, MEDIA_TYPE_SEQUENCE); } if (!cont) { QMessageBox::critical(this, "Project Load Error", "Error loading project: " + error_str, QMessageBox::Ok); } else if (stream.hasError()) { qDebug() << "[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) { panel_timeline->redraw_all_clips(false); project_changed = false; } else { new_project(); } add_recent_project(project_url); file.close(); } void Project::save_folder(QXmlStreamWriter& stream, QTreeWidgetItem* parent, int type) { bool root = (parent == NULL); int len = root ? ui->treeWidget->topLevelItemCount() : parent->childCount(); for (int i=0;itreeWidget->topLevelItem(i) : parent->child(i); int item_type = get_type_from_tree(item); if (item_type == MEDIA_TYPE_FOLDER) { if (type == SAVE_SET_FOLDER_IDS) { item->setData(0, Qt::UserRole + 3, folder_id); // saves a temporary ID for matching in the project file folder_id++; } else if (type == MEDIA_TYPE_FOLDER) { // 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); } else if (type == item_type) { int folder = root ? 0 : parent->data(0, Qt::UserRole + 3).toInt(); if (type == MEDIA_TYPE_FOOTAGE) { Media* m = get_media_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", m->url); stream.writeAttribute("duration", QString::number(m->length)); 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)); 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); stream.writeStartElement("sequence"); 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)); stream.writeAttribute("afreq", QString::number(s->audio_frequency)); stream.writeAttribute("alayout", QString::number(s->audio_layout)); if (s == sequence) { stream.writeAttribute("open", "1"); } if (s->using_workarea) { stream.writeAttribute("workareaIn", QString::number(s->workarea_in)); stream.writeAttribute("workareaOut", QString::number(s->workarea_out)); } for (int j=0;jclip_count();j++) { Clip* c = s->get_clip(j); if (c != NULL) { stream.writeStartElement("clip"); // clip stream.writeAttribute("id", QString::number(j)); 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)); if (c->opening_transition != NULL) { stream.writeAttribute("opening", QString::number(c->opening_transition->id)); } if (c->closing_transition != NULL) { stream.writeAttribute("closing", QString::number(c->closing_transition->id)); } 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("media", QString::number(c->media->save_id)); stream.writeAttribute("stream", QString::number(c->media_stream)); 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 stream.writeStartElement("effects"); // effects for (int k=0;keffects.size();k++) { stream.writeStartElement("effect"); // effect Effect* e = c->effects.at(k); stream.writeAttribute("id", QString::number(e->id)); e->save(&stream); stream.writeEndElement(); // effect } stream.writeEndElement(); // effects stream.writeEndElement(); // clip } } stream.writeEndElement(); } } } } void Project::save_project(bool autorecovery) { folder_id = 1; media_id = 0; QFile file(autorecovery ? autorecovery_filename : project_url); if (!file.open(QIODevice::WriteOnly/* | QIODevice::Text*/)) { qDebug() << "[ERROR] Could not open file"; return; } QXmlStreamWriter stream(&file); stream.setAutoFormatting(true); stream.writeStartDocument(); // doc stream.writeStartElement("project"); // project stream.writeTextElement("version", SAVE_VERSION); save_folder(stream, NULL, SAVE_SET_FOLDER_IDS); stream.writeStartElement("folders"); // folders save_folder(stream, NULL, MEDIA_TYPE_FOLDER); stream.writeEndElement(); // folders stream.writeStartElement("media"); // media save_folder(stream, NULL, MEDIA_TYPE_FOOTAGE); stream.writeEndElement(); // media stream.writeStartElement("sequences"); // sequences save_folder(stream, NULL, MEDIA_TYPE_SEQUENCE); stream.writeEndElement();// sequences stream.writeEndElement(); // project stream.writeEndDocument(); // doc file.close(); if (!autorecovery) { add_recent_project(project_url); project_changed = 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 { qDebug() << "[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(); } #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) { 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_ERROR: item->setIcon(0, QIcon::fromTheme("dialog-error")); break; } panel_project->source_table->viewport()->update(); deleteLater(); }