/*** Olive - Non-Linear Video Editor Copyright (C) 2019 Olive Team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***/ #include "project.h" extern "C" { #include #include } #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "global/global.h" #include "panels.h" #include "rendering/renderfunctions.h" #include "project/previewgenerator.h" #include "undo/undo.h" #include "undo/undostack.h" #include "ui/mainwindow.h" #include "global/config.h" #include "rendering/cacher.h" #include "dialogs/replaceclipmediadialog.h" #include "panels/effectcontrols.h" #include "dialogs/mediapropertiesdialog.h" #include "dialogs/newsequencedialog.h" #include "dialogs/loaddialog.h" #include "global/clipboard.h" #include "ui/sourcetable.h" #include "ui/sourceiconview.h" #include "ui/icons.h" #include "ui/menuhelper.h" #include "ui/mediaiconservice.h" #include "project/sourcescommon.h" #include "project/projectfilter.h" #include "project/projectfunctions.h" #include "global/debug.h" #include "ui/menu.h" Project::Project(QWidget *parent) : Panel(parent), sorter(this), sources_common(this, sorter) { QWidget* dockWidgetContents = new QWidget(this); QVBoxLayout* verticalLayout = new QVBoxLayout(dockWidgetContents); verticalLayout->setMargin(0); verticalLayout->setSpacing(0); setWidget(dockWidgetContents); ConnectFilterToModel(); // optional toolbar toolbar_widget = new QWidget(); toolbar_widget->setVisible(olive::config.show_project_toolbar); toolbar_widget->setObjectName("project_toolbar"); QHBoxLayout* toolbar = new QHBoxLayout(toolbar_widget); toolbar->setMargin(0); toolbar->setSpacing(0); QPushButton* toolbar_new = new QPushButton(); toolbar_new->setIcon(olive::icon::CreateIconFromSVG(QStringLiteral(":/icons/add-button.svg"))); toolbar_new->setToolTip(tr("New")); connect(toolbar_new, SIGNAL(clicked(bool)), this, SLOT(make_new_menu())); toolbar->addWidget(toolbar_new); QPushButton* toolbar_open = new QPushButton(); toolbar_open->setIcon(olive::icon::CreateIconFromSVG(QStringLiteral(":/icons/open.svg"))); toolbar_open->setToolTip(tr("Open Project")); connect(toolbar_open, SIGNAL(clicked(bool)), olive::Global.get(), SLOT(OpenProject())); toolbar->addWidget(toolbar_open); QPushButton* toolbar_save = new QPushButton(); toolbar_save->setIcon(olive::icon::CreateIconFromSVG(QStringLiteral(":/icons/save.svg"))); toolbar_save->setToolTip(tr("Save Project")); connect(toolbar_save, SIGNAL(clicked(bool)), olive::Global.get(), SLOT(save_project())); toolbar->addWidget(toolbar_save); QPushButton* toolbar_undo = new QPushButton(); toolbar_undo->setIcon(olive::icon::CreateIconFromSVG(QStringLiteral(":/icons/undo.svg"))); toolbar_undo->setToolTip(tr("Undo")); connect(toolbar_undo, SIGNAL(clicked(bool)), olive::Global.get(), SLOT(undo())); toolbar->addWidget(toolbar_undo); QPushButton* toolbar_redo = new QPushButton(); toolbar_redo->setIcon(olive::icon::CreateIconFromSVG(QStringLiteral(":/icons/redo.svg"))); toolbar_redo->setToolTip(tr("Redo")); connect(toolbar_redo, SIGNAL(clicked(bool)), olive::Global.get(), SLOT(redo())); toolbar->addWidget(toolbar_redo); toolbar_search = new QLineEdit(); toolbar_search->setClearButtonEnabled(true); connect(toolbar_search, SIGNAL(textChanged(QString)), &sorter, SLOT(update_search_filter(const QString&))); toolbar->addWidget(toolbar_search); QPushButton* toolbar_tree_view = new QPushButton(); toolbar_tree_view->setIcon(olive::icon::CreateIconFromSVG(QStringLiteral(":/icons/treeview.svg"))); toolbar_tree_view->setToolTip(tr("Tree View")); connect(toolbar_tree_view, SIGNAL(clicked(bool)), this, SLOT(set_tree_view())); toolbar->addWidget(toolbar_tree_view); QPushButton* toolbar_icon_view = new QPushButton(); toolbar_icon_view->setIcon(olive::icon::CreateIconFromSVG(QStringLiteral(":/icons/iconview.svg"))); toolbar_icon_view->setToolTip(tr("Icon View")); connect(toolbar_icon_view, SIGNAL(clicked(bool)), this, SLOT(set_icon_view())); toolbar->addWidget(toolbar_icon_view); QPushButton* toolbar_list_view = new QPushButton(); toolbar_list_view->setIcon(olive::icon::CreateIconFromSVG(QStringLiteral(":/icons/listview.svg"))); toolbar_list_view->setToolTip(tr("List View")); connect(toolbar_list_view, SIGNAL(clicked(bool)), this, SLOT(set_list_view())); toolbar->addWidget(toolbar_list_view); verticalLayout->addWidget(toolbar_widget); // tree view tree_view = new SourceTable(sources_common); tree_view->project_parent = this; tree_view->setModel(&sorter); verticalLayout->addWidget(tree_view); // Set the first column width // I'm not sure if there's a better way to do this, default behavior seems to have all columns fixed width // and let the last column fill up the remainder when really the opposite would be preferable (having the // first column fill up the majority of the space). Anyway, this will probably do for now. tree_view->setColumnWidth(0, tree_view->width()/2); // icon view icon_view_container = new QWidget(); QVBoxLayout* icon_view_container_layout = new QVBoxLayout(icon_view_container); icon_view_container_layout->setMargin(0); icon_view_container_layout->setSpacing(0); QHBoxLayout* icon_view_controls = new QHBoxLayout(); icon_view_controls->setMargin(0); icon_view_controls->setSpacing(0); directory_up = new QPushButton(); directory_up->setIcon(olive::icon::CreateIconFromSVG(QStringLiteral(":/icons/dirup.svg"))); directory_up->setEnabled(false); icon_view_controls->addWidget(directory_up); icon_view_controls->addStretch(); icon_size_slider = new QSlider(Qt::Horizontal); icon_size_slider->setMinimum(16); icon_size_slider->setMaximum(256); icon_view_controls->addWidget(icon_size_slider); connect(icon_size_slider, SIGNAL(valueChanged(int)), this, SLOT(set_icon_view_size(int))); icon_view_container_layout->addLayout(icon_view_controls); icon_view = new SourceIconView(sources_common); icon_view->project_parent = this; icon_view->setModel(&sorter); icon_view->setGridSize(QSize(100, 100)); icon_view->setViewMode(QListView::IconMode); icon_view->setUniformItemSizes(true); icon_view_container_layout->addWidget(icon_view); icon_size_slider->setValue(icon_view->gridSize().height()); verticalLayout->addWidget(icon_view_container); connect(directory_up, SIGNAL(clicked(bool)), this, SLOT(go_up_dir())); connect(icon_view, SIGNAL(changed_root()), this, SLOT(set_up_dir_enabled())); connect(olive::media_icon_service.get(), SIGNAL(IconChanged()), icon_view->viewport(), SLOT(update())); connect(olive::media_icon_service.get(), SIGNAL(IconChanged()), tree_view->viewport(), SLOT(update())); update_view_type(); Retranslate(); } void Project::ConnectFilterToModel() { sorter.setSourceModel(&olive::project_model); } void Project::DisconnectFilterToModel() { sorter.setSourceModel(nullptr); } void Project::Retranslate() { toolbar_search->setPlaceholderText(tr("Search media, markers, etc.")); setWindowTitle(tr("Project")); } void Project::duplicate_selected() { QModelIndexList items = get_current_selected(); bool duped = false; ComboAction* ca = new ComboAction(); for (int j=0;jget_type() == MEDIA_TYPE_SEQUENCE) { olive::project_model.CreateSequence(ca, i->to_sequence()->copy(), false, item_to_media(items.at(j).parent())); duped = true; } } if (duped) { olive::undo_stack.push(ca); } else { delete ca; } } void Project::replace_selected_file() { QModelIndexList selected_items = get_current_selected(); if (selected_items.size() == 1) { MediaPtr item = item_to_media_ptr(selected_items.at(0)); if (item->get_type() == MEDIA_TYPE_FOOTAGE) { sources_common.replace_media(item, nullptr); } } } void Project::replace_clip_media() { Sequence* top_sequence = Timeline::GetTopSequence().get(); if (top_sequence == nullptr) { QMessageBox::critical(this, tr("No active sequence"), tr("No sequence is active, please open the sequence you want to replace clips from."), QMessageBox::Ok); } else { QModelIndexList selected_items = get_current_selected(); if (selected_items.size() == 1) { Media* item = item_to_media(selected_items.at(0)); if (item->get_type() == MEDIA_TYPE_SEQUENCE && top_sequence == item->to_sequence().get()) { QMessageBox::critical(this, tr("Active sequence selected"), tr("You cannot insert a sequence into itself, so no clips of this media would be in this sequence."), QMessageBox::Ok); } else { ReplaceClipMediaDialog dialog(this, item); dialog.exec(); } } } } void Project::open_properties() { QModelIndexList selected_items = get_current_selected(); if (selected_items.size() == 1) { Media* item = item_to_media(selected_items.at(0)); switch (item->get_type()) { case MEDIA_TYPE_FOOTAGE: { MediaPropertiesDialog mpd(this, item); mpd.exec(); } break; case MEDIA_TYPE_SEQUENCE: { NewSequenceDialog nsd(this, item); nsd.exec(); } break; default: { // fall back to renaming QString new_name = QInputDialog::getText(this, tr("Rename '%1'").arg(item->get_name()), tr("Enter new name:"), QLineEdit::Normal, item->get_name()); if (!new_name.isEmpty()) { MediaRename* mr = new MediaRename(item, new_name); olive::undo_stack.push(mr); } } } } } void Project::new_folder() { MediaPtr m = olive::project::CreateFolder(nullptr); olive::undo_stack.push(new AddMediaCommand(m, get_selected_folder())); QModelIndex index = olive::project_model.create_index(m->row(), 0, m.get()); switch (olive::config.project_view_type) { case olive::PROJECT_VIEW_TREE: tree_view->edit(sorter.mapFromSource(index)); break; case olive::PROJECT_VIEW_ICON: icon_view->edit(sorter.mapFromSource(index)); break; } } bool Project::focused() { return tree_view->hasFocus() || icon_view->hasFocus(); } Media* Project::item_to_media(const QModelIndex &index) { return static_cast(sorter.mapToSource(index).internalPointer()); } MediaPtr Project::item_to_media_ptr(const QModelIndex &index) { Media* raw_ptr = item_to_media(index); if (raw_ptr == nullptr) { return nullptr; } return raw_ptr->parentItem()->get_shared_ptr(raw_ptr); } bool Project::IsToolbarVisible() { return toolbar_widget->isVisible(); } void Project::SetToolbarVisible(bool visible) { toolbar_widget->setVisible(visible); } bool Project::IsProjectWidget(QObject *child) { return (child == tree_view || child == icon_view); } void Project::delete_selected_media() { ComboAction* ca = new ComboAction(); QModelIndexList selected_items = get_current_selected(); QList items; for (int i=0;i parents; QVector all_sequences = olive::project_model.GetAllSequences(); if (all_sequences.size() > 0) { QVector all_footage = olive::project_model.GetAllFootage(); for (int i=0;ito_footage(); bool confirm_delete = false; for (int j=0;jto_sequence().get(); QVector sequence_clips = s->GetAllClips(); for (int k=0;kmedia() == item) { 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(tr("Delete media in use?")); confirm.setText(tr("The media '%1' is currently used in '%2'. Deleting it will remove all instances in the sequence. Are you sure you want to do this?").arg(media->name, s->name())); QAbstractButton* yes_button = confirm.addButton(QMessageBox::Yes); QAbstractButton* skip_button = nullptr; if (items.size() > 1) skip_button = confirm.addButton(tr("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 Media* parent = item; while (parent != nullptr) { parents.append(parent); // re-add item's siblings for (int m=0;mchildCount();m++) { Media* child = parent->child(m); bool found = false; for (int n=0;nparentItem(); } j = all_sequences.size(); k = sequence_clips.size(); } else if (confirm.clickedButton() == abort_button) { // break out of loop i = all_footage.size(); j = all_sequences.size(); k = sequence_clips.size(); remove = false; } } if (confirm_delete) { ca->append(new DeleteClipAction(c)); } } } } if (confirm_delete) { olive::clipboard.DeleteClipsWithMedia(ca, item); } } } // remove if (remove) { panel_graph_editor->set_row(nullptr); panel_effect_controls->Clear(true); Sequence* top_sequence = Timeline::GetTopSequence().get(); if (top_sequence != nullptr) { top_sequence->ClearSelections(); } // remove media and parents for (int m=0;mappend(new DeleteMediaCommand(items.at(i)->parentItem()->get_shared_ptr(items.at(i)))); if (items.at(i)->get_type() == MEDIA_TYPE_SEQUENCE) { redraw = true; Sequence* s = items.at(i)->to_sequence().get(); Timeline::CloseSequence(s); if (s == panel_footage_viewer->seq.get()) { panel_footage_viewer->set_media(nullptr); } } else if (items.at(i)->get_type() == MEDIA_TYPE_FOOTAGE) { if (panel_footage_viewer->media == items.at(i)) { panel_footage_viewer->set_media(nullptr); } } } olive::undo_stack.push(ca); // redraw clips if (redraw) { update_ui(true); } } else { delete ca; } } Media* Project::get_selected_folder() { // if one item is selected and it's a folder, return it QModelIndexList selected_items = get_current_selected(); if (selected_items.size() == 1) { Media* m = item_to_media(selected_items.at(0)); if (m->get_type() == MEDIA_TYPE_FOLDER) return m; } return nullptr; } bool Project::reveal_media(Media *media, QModelIndex parent) { for (int i=0;iget_type() == MEDIA_TYPE_FOLDER) { // if this item is a folder, recursively run this function to search it too if (reveal_media(media, item)) return true; } else if (m == media) { // if m == media, then we found the media object we were looking for // get sorter proxy item (the item that's "visible") QModelIndex sorted_index = sorter.mapFromSource(item); // retrieve its parent item QModelIndex hierarchy = sorted_index.parent(); if (olive::config.project_view_type == olive::PROJECT_VIEW_TREE) { // if we're in tree view, expand every folder in the hierarchy containing the media while (hierarchy.isValid()) { tree_view->setExpanded(hierarchy, true); hierarchy = hierarchy.parent(); } // select item (requires a QItemSelection object to select the whole row) QItemSelection row_select( sorter.index(sorted_index.row(), 0, sorted_index.parent()), sorter.index(sorted_index.row(), sorter.columnCount()-1, sorted_index.parent()) ); tree_view->selectionModel()->select(row_select, QItemSelectionModel::Select); } else if (olive::config.project_view_type == olive::PROJECT_VIEW_ICON) { // if we're in icon view, we just "browse" to the parent folder icon_view->setRootIndex(hierarchy); // select item in this folder icon_view->selectionModel()->select(sorted_index, QItemSelectionModel::Select); // update the "up" button state set_up_dir_enabled(); } return true; } } return false; } void Project::delete_clips_using_selected_media() { Sequence* top_sequence = Timeline::GetTopSequence().get(); if (top_sequence == nullptr) { QMessageBox::critical(this, tr("No active sequence"), tr("No sequence is active, please open the sequence you want to delete clips from."), QMessageBox::Ok); } else { QModelIndexList items = get_current_selected(); QVector media; media.resize(items.size()); for (int i=0;iDeleteClipsUsingMedia(media); } } void Project::update_view_type() { tree_view->setVisible(olive::config.project_view_type == olive::PROJECT_VIEW_TREE); icon_view_container->setVisible(olive::config.project_view_type == olive::PROJECT_VIEW_ICON || olive::config.project_view_type == olive::PROJECT_VIEW_LIST); switch (olive::config.project_view_type) { case olive::PROJECT_VIEW_TREE: sources_common.view = tree_view; break; case olive::PROJECT_VIEW_ICON: case olive::PROJECT_VIEW_LIST: icon_view->setViewMode(olive::config.project_view_type == olive::PROJECT_VIEW_ICON ? QListView::IconMode : QListView::ListMode); // update list/grid size since they use this value slightly differently set_icon_view_size(icon_size_slider->value()); sources_common.view = icon_view; break; } } void Project::set_icon_view() { olive::config.project_view_type = olive::PROJECT_VIEW_ICON; update_view_type(); } void Project::set_list_view() { olive::config.project_view_type = olive::PROJECT_VIEW_LIST; update_view_type(); } void Project::set_tree_view() { olive::config.project_view_type = olive::PROJECT_VIEW_TREE; update_view_type(); } void Project::set_icon_view_size(int s) { if (icon_view->viewMode() == QListView::IconMode) { icon_view->setGridSize(QSize(s, s)); } else { icon_view->setGridSize(QSize()); icon_view->setIconSize(QSize(s, s)); } } void Project::set_up_dir_enabled() { directory_up->setEnabled(icon_view->rootIndex().isValid()); } void Project::go_up_dir() { icon_view->setRootIndex(icon_view->rootIndex().parent()); set_up_dir_enabled(); } void Project::make_new_menu() { Menu new_menu(this); olive::MenuHelper.make_new_menu(&new_menu); new_menu.exec(QCursor::pos()); } QModelIndexList Project::get_current_selected() { if (olive::config.project_view_type == olive::PROJECT_VIEW_TREE) { return tree_view->selectionModel()->selectedRows(); } return icon_view->selectionModel()->selectedIndexes(); }