/***
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 "timelineview.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "global/global.h"
#include "panels/panels.h"
#include "project/projectelements.h"
#include "rendering/audio.h"
#include "global/config.h"
#include "global/timing.h"
#include "ui/sourcetable.h"
#include "ui/sourceiconview.h"
#include "undo/undo.h"
#include "undo/undostack.h"
#include "ui/viewerwidget.h"
#include "ui/resizablescrollbar.h"
#include "dialogs/newsequencedialog.h"
#include "mainwindow.h"
#include "ui/rectangleselect.h"
#include "rendering/renderfunctions.h"
#include "ui/cursors.h"
#include "ui/menuhelper.h"
#include "ui/menu.h"
#include "ui/focusfilter.h"
#include "dialogs/clippropertiesdialog.h"
#include "global/debug.h"
#include "nodes/node.h"
#include "effects/internal/solideffect.h"
#include "timeline/track.h"
#include "global/math.h"
#include "project/projectfunctions.h"
#include "ui/waveform.h"
#define MAX_TEXT_WIDTH 20
#define TRANSITION_BETWEEN_RANGE 40
TimelineView::TimelineView(Timeline *parent) :
timeline_(parent),
self_created_sequence(nullptr),
track_list_(nullptr),
scroll(0),
alignment_(olive::timeline::kAlignmentTop),
track_resizing(false)
{
setMouseTracking(true);
setFocusPolicy(Qt::ClickFocus);
setAcceptDrops(true);
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(show_context_menu(const QPoint&)));
tooltip_timer.setInterval(500);
connect(&tooltip_timer, SIGNAL(timeout()), this, SLOT(tooltip_timer_timeout()));
}
void TimelineView::SetAlignment(olive::timeline::Alignment alignment)
{
alignment_ = alignment;
}
void TimelineView::SetTrackList(TrackList *tl)
{
track_list_ = tl;
update();
}
void TimelineView::show_context_menu(const QPoint& pos) {
if (sequence() != nullptr) {
// hack because sometimes right clicking doesn't trigger mouse release event
ParentTimeline()->rect_select_init = false;
ParentTimeline()->rect_select_proc = false;
Menu menu(this);
QAction* undoAction = menu.addAction(tr("&Undo"));
QAction* redoAction = menu.addAction(tr("&Redo"));
connect(undoAction, SIGNAL(triggered(bool)), olive::Global.get(), SLOT(undo()));
connect(redoAction, SIGNAL(triggered(bool)), olive::Global.get(), SLOT(redo()));
undoAction->setEnabled(olive::undo_stack.canUndo());
redoAction->setEnabled(olive::undo_stack.canRedo());
menu.addSeparator();
// collect all the selected clips
QVector selected_clips = sequence()->SelectedClips();
olive::MenuHelper.make_edit_functions_menu(&menu, !selected_clips.isEmpty());
if (selected_clips.isEmpty()) {
// no clips are selected
// determine if we can perform a ripple empty space
ParentTimeline()->cursor_frame = ParentTimeline()->getTimelineFrameFromScreenPoint(pos.x());
ParentTimeline()->cursor_track = getTrackFromScreenPoint(pos.y());
// check if the space the cursor is currently at is empty
if (ParentTimeline()->cursor_track->GetClipFromPoint(ParentTimeline()->cursor_frame) == nullptr) {
QAction* ripple_delete_action = menu.addAction(tr("R&ipple Delete Empty Space"));
connect(ripple_delete_action, SIGNAL(triggered(bool)), ParentTimeline(), SLOT(ripple_delete_empty_space()));
}
QAction* seq_settings = menu.addAction(tr("Sequence Settings"));
connect(seq_settings, SIGNAL(triggered(bool)), this, SLOT(open_sequence_properties()));
}
if (!selected_clips.isEmpty()) {
bool video_clips_are_selected = false;
bool audio_clips_are_selected = false;
for (int i=0;itype() == olive::kTypeVideo) {
video_clips_are_selected = true;
} else {
audio_clips_are_selected = true;
}
}
menu.addSeparator();
menu.addAction(tr("&Speed/Duration"), olive::Global.get(), SLOT(open_speed_dialog()));
if (audio_clips_are_selected) {
menu.addAction(tr("Auto-Cut Silence"), olive::Global.get(), SLOT(open_autocut_silence_dialog()));
}
QAction* autoscaleAction = menu.addAction(tr("Auto-S&cale"), this, SLOT(toggle_autoscale()));
autoscaleAction->setCheckable(true);
// set autoscale to the first selected clip
autoscaleAction->setChecked(selected_clips.at(0)->autoscaled());
olive::MenuHelper.make_clip_functions_menu(&menu);
// check if all selected clips have the same media for a "Reveal In Project"
bool same_media = true;
rc_reveal_media = selected_clips.at(0)->media();
for (int i=1;imedia() != rc_reveal_media) {
same_media = false;
break;
}
}
if (same_media) {
QAction* revealInProjectAction = menu.addAction(tr("&Reveal in Project"));
connect(revealInProjectAction, SIGNAL(triggered(bool)), this, SLOT(reveal_media()));
}
menu.addAction(tr("Properties"), this, SLOT(show_clip_properties()));
}
menu.exec(mapToGlobal(pos));
}
}
void TimelineView::toggle_autoscale() {
QVector selected_clips = sequence()->SelectedClips();
if (!selected_clips.isEmpty()) {
SetClipProperty* action = new SetClipProperty(kSetClipPropertyAutoscale);
for (int i=0;iAddSetting(c, !c->autoscaled());
}
olive::undo_stack.push(action);
}
}
void TimelineView::tooltip_timer_timeout() {
if (tooltip_clip != nullptr) {
QToolTip::showText(QCursor::pos(),
tr("%1\nStart: %2\nEnd: %3\nDuration: %4").arg(
tooltip_clip->name(),
frame_to_timecode(tooltip_clip->timeline_in(), olive::config.timecode_view, sequence()->frame_rate),
frame_to_timecode(tooltip_clip->timeline_out(), olive::config.timecode_view, sequence()->frame_rate),
frame_to_timecode(tooltip_clip->length(), olive::config.timecode_view, sequence()->frame_rate)
));
}
tooltip_timer.stop();
}
void TimelineView::open_sequence_properties() {
QVector sequence_items = olive::project_model.GetAllSequences();
for (int i=0;ito_sequence().get() == sequence()) {
NewSequenceDialog nsd(this, sequence_items.at(i));
nsd.exec();
return;
}
}
QMessageBox::critical(this, tr("Error"), tr("Couldn't locate media wrapper for sequence."));
}
void TimelineView::show_clip_properties()
{
// get list of selected clips
QVector selected_clips = sequence()->SelectedClips();
// if clips are selected, open the clip properties dialog
if (!selected_clips.isEmpty()) {
ClipPropertiesDialog cpd(this, selected_clips);
cpd.exec();
}
}
void TimelineView::dragEnterEvent(QDragEnterEvent *event) {
bool import_init = false;
QVector media_list;
ParentTimeline()->importing_files = false;
for (int i=0;iIsProjectWidget(event->source())) {
QModelIndexList items = panel_project.at(i)->get_current_selected();
media_list.resize(items.size());
for (int i=0;iitem_to_media(items.at(i));
}
import_init = true;
break;
}
}
if (event->source() == panel_footage_viewer) {
if (panel_footage_viewer->seq.get() != sequence()) { // don't allow nesting the same sequence
media_list.append(olive::timeline::MediaImportData(panel_footage_viewer->media,
static_cast(event->mimeData()->text().toInt())));
import_init = true;
}
}
if (olive::config.enable_drag_files_to_timeline && event->mimeData()->hasUrls()) {
QList urls = event->mimeData()->urls();
if (!urls.isEmpty()) {
QStringList file_list;
for (int i=0;i last_imported_media = olive::project_model.GetLastImportedMedia();
for (int i=0;ito_footage();
// waits for media to have a duration
// TODO would be much nicer if this was multithreaded
f->ready_lock.lock();
f->ready_lock.unlock();
if (f->ready) {
media_list.append(last_imported_media.at(i));
}
}
if (media_list.isEmpty()) {
olive::undo_stack.undo();
} else {
import_init = true;
ParentTimeline()->importing_files = true;
}
}
}
if (import_init) {
event->acceptProposedAction();
long entry_point;
Sequence* seq = sequence();
if (seq == nullptr) {
// if no sequence, we're going to create a new one using the clips as a reference
entry_point = 0;
self_created_sequence = olive::project::CreateSequenceFromMedia(media_list);
seq = self_created_sequence.get();
} else {
entry_point = ParentTimeline()->getTimelineFrameFromScreenPoint(event->pos().x());
ParentTimeline()->drag_frame_start = entry_point + getFrameFromScreenPoint(ParentTimeline()->zoom, 50);
ParentTimeline()->drag_track_start = track_list_->First();
}
ParentTimeline()->ghosts = olive::timeline::CreateGhostsFromMedia(seq, entry_point, media_list);
ParentTimeline()->importing = true;
}
}
void TimelineView::dragMoveEvent(QDragMoveEvent *event) {
if (ParentTimeline()->importing) {
event->acceptProposedAction();
if (sequence() != nullptr) {
QPoint pos = event->pos();
ParentTimeline()->scroll_to_frame(ParentTimeline()->getTimelineFrameFromScreenPoint(event->pos().x()));
update_ghosts(pos, event->keyboardModifiers() & Qt::ShiftModifier);
ParentTimeline()->move_insert = ((event->keyboardModifiers() & Qt::ControlModifier) && (olive::timeline::current_tool == olive::timeline::TIMELINE_TOOL_POINTER || ParentTimeline()->importing));
update_ui(false);
}
}
}
void TimelineView::wheelEvent(QWheelEvent *event) {
static_cast(parent())->wheelEvent(event);
}
void TimelineView::dragLeaveEvent(QDragLeaveEvent* event) {
event->accept();
if (ParentTimeline()->importing) {
if (ParentTimeline()->importing_files) {
olive::undo_stack.undo();
}
ParentTimeline()->importing_files = false;
ParentTimeline()->ghosts.clear();
ParentTimeline()->importing = false;
update_ui(false);
}
if (self_created_sequence != nullptr) {
self_created_sequence.reset();
self_created_sequence = nullptr;
}
}
void TimelineView::delete_area_under_ghosts(ComboAction* ca, Sequence* s) {
// delete areas before adding
QVector delete_areas;
for (int i=0;ighosts.size();i++) {
delete_areas.append(ParentTimeline()->ghosts.at(i).ToSelection());
}
s->DeleteAreas(ca, delete_areas, false);
}
void TimelineView::insert_clips(ComboAction* ca, Sequence* s) {
bool ripple_old_point = true;
long earliest_old_point = LONG_MAX;
long latest_old_point = LONG_MIN;
long earliest_new_point = LONG_MAX;
long latest_new_point = LONG_MIN;
QVector ignore_clips;
for (int i=0;ighosts.size();i++) {
const Ghost& g = ParentTimeline()->ghosts.at(i);
earliest_old_point = qMin(earliest_old_point, g.old_in);
latest_old_point = qMax(latest_old_point, g.old_out);
earliest_new_point = qMin(earliest_new_point, g.in);
latest_new_point = qMax(latest_new_point, g.out);
if (g.clip != nullptr) {
ignore_clips.append(g.clip);
} else {
// don't try to close old gap if importing
ripple_old_point = false;
}
}
QVector sequence_clips = s->GetAllClips();
for (int i=0;ighosts.size();j++) {
if (ParentTimeline()->ghosts.at(j).clip == c) {
found = true;
break;
}
}
if (!found) {
if (c->timeline_in() < earliest_new_point && c->timeline_out() > earliest_new_point) {
s->SplitClipAtPositions(ca, c, {earliest_new_point}, true);
}
// determine if we should close the gap the old clips left behind
if (ripple_old_point
&& !((c->timeline_in() < earliest_old_point && c->timeline_out() <= earliest_old_point) || (c->timeline_in() >= latest_old_point && c->timeline_out() > latest_old_point))
&& !ignore_clips.contains(c)) {
ripple_old_point = false;
}
}
}
long ripple_length = (latest_new_point - earliest_new_point);
s->Ripple(ca, earliest_new_point, ripple_length, ignore_clips);
if (ripple_old_point) {
// works for moving later clips earlier but not earlier to later
long second_ripple_length = (earliest_old_point - latest_old_point);
s->Ripple(ca, latest_old_point, second_ripple_length, ignore_clips);
if (earliest_old_point < earliest_new_point) {
for (int i=0;ighosts.size();i++) {
Ghost& g = ParentTimeline()->ghosts[i];
g.in += second_ripple_length;
g.out += second_ripple_length;
}
QVector sequence_selections = s->Selections();
for (int i=0;iSetSelections(sequence_selections);
}
}
}
void TimelineView::dropEvent(QDropEvent* event) {
if (ParentTimeline()->importing && ParentTimeline()->ghosts.size() > 0) {
event->acceptProposedAction();
ComboAction* ca = new ComboAction();
Sequence* s = sequence();
// if we're dropping into nothing, create a new sequences based on the clip being dragged
if (s == nullptr) {
s = self_created_sequence.get();
olive::project_model.CreateSequence(ca, self_created_sequence, true, nullptr);
self_created_sequence = nullptr;
} else if (event->keyboardModifiers() & Qt::ControlModifier) {
insert_clips(ca, s);
} else {
delete_area_under_ghosts(ca, s);
}
s->AddClipsFromGhosts(ca, ParentTimeline()->ghosts);
ParentTimeline()->ghosts.clear();
ParentTimeline()->importing = false;
olive::undo_stack.push(ca);
setFocus();
update_ui(true);
}
}
void TimelineView::mouseDoubleClickEvent(QMouseEvent *event) {
if (sequence() != nullptr) {
if (olive::timeline::current_tool == olive::timeline::TIMELINE_TOOL_EDIT) {
Clip* clip = GetClipAtCursor();
if (clip != nullptr) {
if (!(event->modifiers() & Qt::ShiftModifier)) {
sequence()->ClearSelections();
}
clip->track()->SelectClip(clip);
update_ui(false);
}
} else if (olive::timeline::current_tool == olive::timeline::TIMELINE_TOOL_POINTER) {
Clip* c = GetClipAtCursor();
if (c != nullptr) {
if (c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_SEQUENCE) {
Timeline::OpenSequence(c->media()->to_sequence());
}
}
}
}
}
bool TimelineView::current_tool_shows_cursor() {
return (olive::timeline::current_tool == olive::timeline::TIMELINE_TOOL_EDIT
|| olive::timeline::current_tool == olive::timeline::TIMELINE_TOOL_RAZOR
|| ParentTimeline()->creating);
}
Clip *TimelineView::GetClipAtCursor()
{
if (ParentTimeline()->cursor_track == nullptr) {
return nullptr;
}
return ParentTimeline()->cursor_track->GetClipFromPoint(ParentTimeline()->cursor_frame);
}
QVector