/*** 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 "effectcontrols.h" #include #include #include #include #include #include #include #include #include #include #include "panels/panels.h" #include "effects/effect.h" #include "effects/transition.h" #include "timeline/clip.h" #include "ui/collapsiblewidget.h" #include "timeline/sequence.h" #include "undo/undo.h" #include "undo/undostack.h" #include "panels/project.h" #include "panels/timeline.h" #include "panels/viewer.h" #include "panels/grapheditor.h" #include "ui/viewerwidget.h" #include "ui/menuhelper.h" #include "ui/icons.h" #include "project/clipboard.h" #include "global/config.h" #include "ui/timelineheader.h" #include "ui/keyframeview.h" #include "ui/resizablescrollbar.h" #include "global/debug.h" #include "ui/menu.h" EffectControls::EffectControls(QWidget *parent) : Panel(parent), zoom(1) { setup_ui(); Retranslate(); Clear(false); headers->viewer = panel_sequence_viewer; headers->snapping = false; effects_area->parent_widget = scrollArea; effects_area->keyframe_area = keyframeView; effects_area->header = headers; keyframeView->header = headers; connect(keyframeView, SIGNAL(wheel_event_signal(QWheelEvent*)), effects_area, SLOT(receive_wheel_event(QWheelEvent*))); connect(horizontalScrollBar, SIGNAL(valueChanged(int)), headers, SLOT(set_scroll(int))); connect(horizontalScrollBar, SIGNAL(resize_move(double)), keyframeView, SLOT(resize_move(double))); connect(horizontalScrollBar, SIGNAL(valueChanged(int)), keyframeView, SLOT(set_x_scroll(int))); connect(verticalScrollBar, SIGNAL(valueChanged(int)), keyframeView, SLOT(set_y_scroll(int))); connect(verticalScrollBar, SIGNAL(valueChanged(int)), scrollArea->verticalScrollBar(), SLOT(setValue(int))); connect(scrollArea->verticalScrollBar(), SIGNAL(valueChanged(int)), verticalScrollBar, SLOT(setValue(int))); } EffectControls::~EffectControls() { Clear(true); } bool EffectControls::keyframe_focus() { return headers->hasFocus() || keyframeView->hasFocus(); } void EffectControls::set_zoom(bool in) { zoom *= (in) ? 2 : 0.5; update_keyframes(); scroll_to_frame(olive::ActiveSequence->playhead); } void EffectControls::menu_select(QAction* q) { ComboAction* ca = new ComboAction(); for (int i=0;itrack() < 0) == (effect_menu_subtype == EFFECT_TYPE_VIDEO)) { const EffectMeta* meta = reinterpret_cast(q->data().value()); if (effect_menu_type == EFFECT_TYPE_TRANSITION) { if (c->opening_transition == nullptr) { ca->append(new AddTransitionCommand(c, nullptr, nullptr, meta, olive::CurrentConfig.default_transition_length)); } if (c->closing_transition == nullptr) { ca->append(new AddTransitionCommand(nullptr, c, nullptr, meta, olive::CurrentConfig.default_transition_length)); } } else { ca->append(new AddEffectCommand(c, nullptr, meta)); } } } olive::UndoStack.push(ca); if (effect_menu_type == EFFECT_TYPE_TRANSITION) { update_ui(true); } else { Reload(); panel_sequence_viewer->viewer_widget->frame_update(); } } void EffectControls::update_keyframes() { for (int i=0;iUpdateFromEffect(); } headers->update_zoom(zoom); keyframeView->update(); } void EffectControls::delete_selected_keyframes() { keyframeView->delete_selected_keyframes(); } void EffectControls::copy(bool del) { bool cleared = false; ComboAction* ca = nullptr; if (del) { ca = new ComboAction(); } for (int i=0;iIsSelected()) { Effect* e = open_effects_.at(i)->GetEffect(); if (e->meta->type == EFFECT_TYPE_EFFECT) { if (!cleared) { clear_clipboard(); cleared = true; clipboard_type = CLIPBOARD_TYPE_EFFECT; } clipboard.append(e->copy(nullptr)); if (del) { DeleteEffect(ca, e); } } } } if (del) { if (ca->hasActions()) { olive::UndoStack.push(ca); } else { delete ca; } } } void EffectControls::scroll_to_frame(long frame) { scroll_to_frame_internal(horizontalScrollBar, frame - keyframeView->visible_in, zoom, keyframeView->width()); } void EffectControls::cut() { copy(true); } void EffectControls::show_effect_menu(int type, int subtype) { effect_menu_type = type; effect_menu_subtype = subtype; effects_loaded.lock(); Menu effects_menu(this); effects_menu.setToolTipsVisible(true); for (int i=0;isetText(em.name); action->setData(reinterpret_cast(&em)); if (!em.tooltip.isEmpty()) { action->setToolTip(em.tooltip); } QMenu* parent = &effects_menu; if (!em.category.isEmpty()) { bool found = false; for (int j=0;jmenu() != nullptr) { if (action->menu()->title() == em.category) { parent = action->menu(); found = true; break; } } } if (!found) { parent = new Menu(&effects_menu); parent->setToolTipsVisible(true); parent->setTitle(em.category); bool found = false; for (int i=0;itext() > em.category) { effects_menu.insertMenu(comp_action, parent); found = true; break; } } if (!found) effects_menu.addMenu(parent); } } bool found = false; for (int i=0;iactions().size();i++) { QAction* comp_action = parent->actions().at(i); if (comp_action->text() > action->text()) { parent->insertAction(comp_action, action); found = true; break; } } if (!found) parent->addAction(action); } } effects_loaded.unlock(); connect(&effects_menu, SIGNAL(triggered(QAction*)), this, SLOT(menu_select(QAction*))); effects_menu.exec(QCursor::pos()); } void EffectControls::Clear(bool clear_cache) { // clear existing clips deselect_all_effects(nullptr); for (int i=0;iSetEffects(open_effects_); vcontainer->setVisible(false); acontainer->setVisible(false); headers->setVisible(false); keyframeView->setEnabled(false); if (clear_cache) { selected_clips_.clear(); } UpdateTitle(); } bool EffectControls::IsEffectSelected(Effect *e) { for (int i=0;iGetEffect() == e && open_effects_.at(i)->IsSelected()) { return true; } } return false; } void EffectControls::deselect_all_effects(QWidget* sender) { for (int i=0;iheader_click(false, false); } } if (panel_sequence_viewer != nullptr) { panel_sequence_viewer->viewer_widget->update(); } } void EffectControls::open_effect(QVBoxLayout* layout, Effect* e) { EffectUI* container = new EffectUI(e); connect(container, SIGNAL(CutRequested()), this, SLOT(cut())); connect(container, SIGNAL(CopyRequested()), this, SLOT(copy())); connect(container, SIGNAL(deselect_others(QWidget*)), this, SLOT(deselect_all_effects(QWidget*))); open_effects_.append(container); layout->addWidget(container); } void EffectControls::UpdateTitle() { if (selected_clips_.isEmpty()) { setWindowTitle(panel_name + tr("(none)")); } else { setWindowTitle(panel_name + selected_clips_.first()->name()); } } void EffectControls::setup_ui() { QWidget* contents = new QWidget(this); QHBoxLayout* hlayout = new QHBoxLayout(contents); hlayout->setSpacing(0); hlayout->setMargin(0); splitter = new QSplitter(); splitter->setOrientation(Qt::Horizontal); splitter->setChildrenCollapsible(false); scrollArea = new QScrollArea(); scrollArea->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding); scrollArea->setFrameShape(QFrame::NoFrame); scrollArea->setFrameShadow(QFrame::Plain); scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); scrollArea->setWidgetResizable(true); QWidget* scrollAreaWidgetContents = new QWidget(); QHBoxLayout* scrollAreaLayout = new QHBoxLayout(scrollAreaWidgetContents); scrollAreaLayout->setSpacing(0); scrollAreaLayout->setMargin(0); effects_area = new EffectsArea(); effects_area->setContextMenuPolicy(Qt::CustomContextMenu); connect(effects_area, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(effects_area_context_menu())); QVBoxLayout* effects_area_layout = new QVBoxLayout(effects_area); effects_area_layout->setSpacing(0); effects_area_layout->setMargin(0); vcontainer = new QWidget(); QVBoxLayout* vcontainerLayout = new QVBoxLayout(vcontainer); vcontainerLayout->setSpacing(0); vcontainerLayout->setMargin(0); QWidget* veHeader = new QWidget(); veHeader->setObjectName(QStringLiteral("veHeader")); veHeader->setStyleSheet(QLatin1String("#veHeader { background: rgba(0, 0, 0, 0.25); }")); QHBoxLayout* veHeaderLayout = new QHBoxLayout(veHeader); veHeaderLayout->setSpacing(0); veHeaderLayout->setMargin(0); QIcon add_effect_icon = olive::icon::CreateIconFromSVG(":/icons/add-effect.svg", false); QIcon add_transition_icon = olive::icon::CreateIconFromSVG(":/icons/add-transition.svg", false); btnAddVideoEffect = new QPushButton(); btnAddVideoEffect->setIcon(add_effect_icon); veHeaderLayout->addWidget(btnAddVideoEffect); connect(btnAddVideoEffect, SIGNAL(clicked(bool)), this, SLOT(video_effect_click())); veHeaderLayout->addStretch(); lblVideoEffects = new QLabel(); QFont font; font.setPointSize(9); lblVideoEffects->setFont(font); lblVideoEffects->setAlignment(Qt::AlignCenter); veHeaderLayout->addWidget(lblVideoEffects); veHeaderLayout->addStretch(); btnAddVideoTransition = new QPushButton(); btnAddVideoTransition->setIcon(add_transition_icon); connect(btnAddVideoTransition, SIGNAL(clicked(bool)), this, SLOT(video_transition_click())); veHeaderLayout->addWidget(btnAddVideoTransition); vcontainerLayout->addWidget(veHeader); video_effect_area = new QWidget(); video_effect_layout = new QVBoxLayout(video_effect_area); video_effect_layout->setSpacing(0); video_effect_layout->setMargin(0); vcontainerLayout->addWidget(video_effect_area); effects_area_layout->addWidget(vcontainer); acontainer = new QWidget(); QVBoxLayout* acontainerLayout = new QVBoxLayout(acontainer); acontainerLayout->setSpacing(0); acontainerLayout->setMargin(0); QWidget* aeHeader = new QWidget(); aeHeader->setObjectName(QStringLiteral("aeHeader")); aeHeader->setStyleSheet(QLatin1String("#aeHeader { background: rgba(0, 0, 0, 0.25); }")); QHBoxLayout* aeHeaderLayout = new QHBoxLayout(aeHeader); aeHeaderLayout->setSpacing(0); aeHeaderLayout->setMargin(0); btnAddAudioEffect = new QPushButton(); btnAddAudioEffect->setIcon(add_effect_icon); connect(btnAddAudioEffect, SIGNAL(clicked(bool)), this, SLOT(audio_effect_click())); aeHeaderLayout->addWidget(btnAddAudioEffect); aeHeaderLayout->addStretch(); lblAudioEffects = new QLabel(); lblAudioEffects->setFont(font); lblAudioEffects->setAlignment(Qt::AlignCenter); aeHeaderLayout->addWidget(lblAudioEffects); aeHeaderLayout->addStretch(); btnAddAudioTransition = new QPushButton(); btnAddAudioTransition->setIcon(add_transition_icon); connect(btnAddAudioTransition, SIGNAL(clicked(bool)), this, SLOT(audio_transition_click())); aeHeaderLayout->addWidget(btnAddAudioTransition); acontainerLayout->addWidget(aeHeader); audio_effect_area = new QWidget(); audio_effect_layout = new QVBoxLayout(audio_effect_area); audio_effect_layout->setSpacing(0); audio_effect_layout->setMargin(0); acontainerLayout->addWidget(audio_effect_area); effects_area_layout->addWidget(acontainer); effects_area_layout->addStretch(); scrollAreaLayout->addWidget(effects_area); scrollArea->setWidget(scrollAreaWidgetContents); splitter->addWidget(scrollArea); QWidget* keyframeArea = new QWidget(); QSizePolicy keyframe_sp; keyframe_sp.setHorizontalPolicy(QSizePolicy::Minimum); keyframe_sp.setVerticalPolicy(QSizePolicy::Preferred); keyframe_sp.setHorizontalStretch(1); keyframeArea->setSizePolicy(keyframe_sp); QVBoxLayout* keyframeAreaLayout = new QVBoxLayout(keyframeArea); keyframeAreaLayout->setSpacing(0); keyframeAreaLayout->setMargin(0); headers = new TimelineHeader(); keyframeAreaLayout->addWidget(headers); QWidget* keyframeCenterWidget = new QWidget(); QHBoxLayout* keyframeCenterLayout = new QHBoxLayout(keyframeCenterWidget); keyframeCenterLayout->setSpacing(0); keyframeCenterLayout->setMargin(0); keyframeView = new KeyframeView(); keyframeCenterLayout->addWidget(keyframeView); verticalScrollBar = new QScrollBar(); verticalScrollBar->setOrientation(Qt::Vertical); keyframeCenterLayout->addWidget(verticalScrollBar); keyframeAreaLayout->addWidget(keyframeCenterWidget); horizontalScrollBar = new ResizableScrollBar(); horizontalScrollBar->setOrientation(Qt::Horizontal); keyframeAreaLayout->addWidget(horizontalScrollBar); splitter->addWidget(keyframeArea); hlayout->addWidget(splitter); setWidget(contents); } void EffectControls::Retranslate() { panel_name = tr("Effects: "); btnAddVideoEffect->setToolTip(tr("Add Video Effect")); lblVideoEffects->setText(tr("VIDEO EFFECTS")); btnAddVideoTransition->setToolTip(tr("Add Video Transition")); btnAddAudioEffect->setToolTip(tr("Add Audio Effect")); lblAudioEffects->setText(tr("AUDIO EFFECTS")); btnAddAudioTransition->setToolTip(tr("Add Audio Transition")); UpdateTitle(); } void EffectControls::LoadLayoutState(const QByteArray &data) { splitter->restoreState(data); } QByteArray EffectControls::SaveLayoutState() { return splitter->saveState(); } void EffectControls::update_scrollbar() { verticalScrollBar->setMaximum(qMax(0, effects_area->height() - keyframeView->height() - headers->height())); verticalScrollBar->setPageStep(verticalScrollBar->height()); } void EffectControls::queue_post_update() { keyframeView->update(); update_scrollbar(); } void EffectControls::effects_area_context_menu() { Menu menu(this); olive::MenuHelper.create_effect_paste_action(&menu); menu.exec(QCursor::pos()); } void EffectControls::DeleteEffect(ComboAction* ca, Effect* effect_ref) { if (effect_ref->meta->type == EFFECT_TYPE_EFFECT) { ca->append(new EffectDeleteCommand(effect_ref)); } else if (effect_ref->meta->type == EFFECT_TYPE_TRANSITION) { // Retrieve shared ptr for this transition Clip* attached_clip = effect_ref->parent_clip; TransitionPtr t = nullptr; if (attached_clip->opening_transition.get() == effect_ref) { t = attached_clip->opening_transition; } else if (attached_clip->closing_transition.get() == effect_ref) { t = attached_clip->closing_transition; } if (t == nullptr) { qWarning() << "Failed to delete transition, couldn't find clip link."; } else { ca->append(new DeleteTransitionCommand(t)); } } } void EffectControls::DeleteSelectedEffects() { ComboAction* ca = new ComboAction(); for (int i=0;iIsSelected()) { DeleteEffect(ca, open_effects_.at(i)->GetEffect()); } } if (ca->hasActions()) { olive::UndoStack.push(ca); update_ui(true); } else { delete ca; } } void EffectControls::Reload() { Clear(false); Load(); } void EffectControls::SetClips() { Clear(true); if (olive::ActiveSequence == nullptr) { selected_clips_.clear(); } else { // replace clip vector selected_clips_ = olive::ActiveSequence->SelectedClips(false); Load(); } } void EffectControls::Load() { bool graph_editor_row_is_still_active = false; // load in new clips for (int i=0;itrack() < 0) { vcontainer->setVisible(true); layout = video_effect_layout; } else { acontainer->setVisible(true); layout = audio_effect_layout; } // Create a list of the effects we'll open QVector effects_to_open; // Determine based on the current selections whether to load all effects or just the transitions bool whole_clip_is_selected = c->IsSelected(); if (whole_clip_is_selected) { for (int j=0;jeffects.size();j++) { effects_to_open.append(c->effects.at(j).get()); } } if (c->opening_transition != nullptr && (whole_clip_is_selected || c->sequence->IsTransitionSelected(c->opening_transition.get()))) { effects_to_open.append(c->opening_transition.get()); } if (c->closing_transition != nullptr && (whole_clip_is_selected || c->sequence->IsTransitionSelected(c->closing_transition.get()))) { effects_to_open.append(c->closing_transition.get()); } for (int j=0;jGetEffect()->meta == effects_to_open.at(j)->meta && !open_effects_.at(k)->IsAttachedToClip(c)) { open_effects_.at(k)->AddAdditionalEffect(effects_to_open.at(j)); already_opened = true; break; } } if (!already_opened) { open_effect(layout, effects_to_open.at(j)); } // Check if one of the open effects contains the row currently active in the graph editor. If not, we'll have // to clear the graph editor later. if (!graph_editor_row_is_still_active) { for (int k=0;krow_count();k++) { EffectRow* row = effects_to_open.at(j)->row(k); if (row == panel_graph_editor->get_row()) { graph_editor_row_is_still_active = true; break; } } } } } keyframeView->SetEffects(open_effects_); if (selected_clips_.size() > 0) { keyframeView->setEnabled(true); headers->setVisible(true); QTimer::singleShot(50, this, SLOT(queue_post_update())); } // If the graph editor's currently active row is not part of the current effects, clear it if (!graph_editor_row_is_still_active) { panel_graph_editor->set_row(nullptr); } UpdateTitle(); update_keyframes(); } void EffectControls::video_effect_click() { show_effect_menu(EFFECT_TYPE_EFFECT, EFFECT_TYPE_VIDEO); } void EffectControls::audio_effect_click() { show_effect_menu(EFFECT_TYPE_EFFECT, EFFECT_TYPE_AUDIO); } void EffectControls::video_transition_click() { show_effect_menu(EFFECT_TYPE_TRANSITION, EFFECT_TYPE_VIDEO); } void EffectControls::audio_transition_click() { show_effect_menu(EFFECT_TYPE_TRANSITION, EFFECT_TYPE_AUDIO); } void EffectControls::resizeEvent(QResizeEvent*) { update_scrollbar(); } bool EffectControls::is_focused() { if (this->hasFocus()) return true; for (int i=0;iIsFocused()) { return true; } } return false; } EffectsArea::EffectsArea(QWidget* parent) : QWidget(parent) {} void EffectsArea::resizeEvent(QResizeEvent *) { parent_widget->setMinimumWidth(sizeHint().width()); } void EffectsArea::receive_wheel_event(QWheelEvent *e) { QApplication::sendEvent(this, e); }