/***
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 "timeline.h"
#include
#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 "ui/timelineview.h"
#include "ui/icons.h"
#include "ui/viewerwidget.h"
#include "rendering/audio.h"
#include "rendering/cacher.h"
#include "rendering/renderfunctions.h"
#include "global/config.h"
#include "global/clipboard.h"
#include "global/math.h"
#include "ui/timelineheader.h"
#include "ui/resizablescrollbar.h"
#include "ui/audiomonitor.h"
#include "ui/flowlayout.h"
#include "ui/cursors.h"
#include "ui/mainwindow.h"
#include "undo/undostack.h"
#include "global/debug.h"
#include "global/timing.h"
#include "ui/menu.h"
Timeline::Timeline(QWidget *parent) :
Panel(parent),
cursor_frame(0),
cursor_track(nullptr),
zoom(1.0),
zoom_just_changed(false),
showing_all(false),
selecting(false),
rect_select_init(false),
rect_select_proc(false),
moving_init(false),
moving_proc(false),
move_insert(false),
trim_target(nullptr),
trim_type(olive::timeline::TRIM_NONE),
splitting(false),
importing(false),
importing_files(false),
creating(false),
transition_tool_init(false),
transition_tool_proc(false),
transition_tool_open_clip(nullptr),
transition_tool_close_clip(nullptr),
hand_moving(false),
block_repaints(false),
scroll(0),
sequence_(nullptr)
{
setup_ui();
headers->viewer = panel_sequence_viewer;
video_area->SetAlignment(olive::timeline::kAlignmentBottom);
tool_buttons.append(toolArrowButton);
tool_buttons.append(toolEditButton);
tool_buttons.append(toolRippleButton);
tool_buttons.append(toolRazorButton);
tool_buttons.append(toolSlipButton);
tool_buttons.append(toolSlideButton);
tool_buttons.append(toolTransitionButton);
tool_buttons.append(toolHandButton);
tool_button_group = new QButtonGroup(this);
tool_button_group->addButton(toolArrowButton);
tool_button_group->addButton(toolEditButton);
tool_button_group->addButton(toolRippleButton);
tool_button_group->addButton(toolRazorButton);
tool_button_group->addButton(toolSlipButton);
tool_button_group->addButton(toolSlideButton);
tool_button_group->addButton(toolTransitionButton);
tool_button_group->addButton(toolHandButton);
toolArrowButton->click();
connect(horizontalScrollBar, SIGNAL(valueChanged(int)), this, SLOT(setScroll(int)));
connect(horizontalScrollBar, SIGNAL(resize_move(double)), this, SLOT(resize_move(double)));
connect(this, SIGNAL(SequenceChanged(SequencePtr)), panel_sequence_viewer, SLOT(set_sequence(SequencePtr)));
connect(this, SIGNAL(visibilityChanged(bool)), this, SLOT(visibility_changed_slot(bool)));
update_sequence();
Retranslate();
}
Timeline *Timeline::GetTopTimeline()
{
for (int i=0;iisVisible()) {
return panel_timeline.at(i);
}
}
return nullptr;
}
SequencePtr Timeline::GetTopSequence()
{
Timeline* top_timeline = GetTopTimeline();
if (top_timeline != nullptr) {
return top_timeline->sequence_;
}
return nullptr;
}
void Timeline::OpenSequence(SequencePtr s)
{
Q_ASSERT(s != nullptr);
for (int i=0;isequence_ == s) {
t->raise();
return;
} else if (t->sequence_ == nullptr) {
t->SetSequence(s);
t->raise();
return;
}
}
Timeline* t = new Timeline(olive::MainWindow);
olive::MainWindow->tabifyDockWidget(panel_timeline.last(), t);
t->SetSequence(s);
t->show();
t->raise();
panel_timeline.append(t);
}
void Timeline::CloseSequence(Sequence *s)
{
Q_ASSERT(s != nullptr);
// Don't respond to a null sequence
if (s == nullptr) {
return;
}
// If there's only one Timeline object left, just set it to nullptr without destroying it
if (panel_timeline.size() == 1) {
panel_timeline.first()->SetSequence(nullptr);
return;
}
// If there are multiple, kill the Timeline object that has the specified sequence
for (int i=0;isequence_.get() == s) {
delete t;
panel_timeline.removeAt(i);
i--;
}
}
}
void Timeline::CloseAll()
{
while (panel_timeline.size() > 1) {
delete panel_timeline.last();
panel_timeline.removeLast();
}
panel_timeline.first()->SetSequence(nullptr);
}
bool Timeline::IsImporting()
{
for (int i=0;iimporting) {
return true;
}
}
return false;
}
void Timeline::SetSequence(SequencePtr sequence)
{
if (sequence_ == sequence) {
return;
}
sequence_ = sequence;
update_sequence();
video_area->SetTrackList(sequence_.get(), Track::kTypeVideo);
audio_area->SetTrackList(sequence_.get(), Track::kTypeAudio);
repaint_timeline();
emit SequenceChanged(sequence_);
}
void Timeline::Retranslate() {
toolArrowButton->setToolTip(tr("Pointer Tool") + " (V)");
toolEditButton->setToolTip(tr("Edit Tool") + " (X)");
toolRippleButton->setToolTip(tr("Ripple Tool") + " (B)");
toolRazorButton->setToolTip(tr("Razor Tool") + " (C)");
toolSlipButton->setToolTip(tr("Slip Tool") + " (Y)");
toolSlideButton->setToolTip(tr("Slide Tool") + " (U)");
toolHandButton->setToolTip(tr("Hand Tool") + " (H)");
toolTransitionButton->setToolTip(tr("Transition Tool") + " (T)");
snappingButton->setToolTip(tr("Snapping") + " (S)");
zoomInButton->setToolTip(tr("Zoom In") + " (=)");
zoomOutButton->setToolTip(tr("Zoom Out") + " (-)");
recordButton->setToolTip(tr("Record audio"));
addButton->setToolTip(tr("Add title, solid, bars, etc."));
UpdateTitle();
}
void Timeline::toggle_show_all() {
if (sequence_ != nullptr) {
showing_all = !showing_all;
if (showing_all) {
old_zoom = zoom;
set_zoom_value(double(timeline_area->width() - 200) / double(sequence_->GetEndFrame()));
} else {
set_zoom_value(old_zoom);
}
}
}
void Timeline::add_transition() {
ComboAction* ca = new ComboAction();
bool adding = false;
QVector selected_clips = sequence_->SelectedClips();
for (int i=0;itype() == Track::kTypeVideo) ? TRANSITION_INTERNAL_CROSSDISSOLVE
: TRANSITION_INTERNAL_LINEARFADE;
if (c->opening_transition == nullptr) {
ca->append(new AddTransitionCommand(c,
nullptr,
nullptr,
Effect::GetInternalMeta(transition_to_add, EFFECT_TYPE_TRANSITION),
olive::config.default_transition_length));
adding = true;
}
if (c->closing_transition == nullptr) {
ca->append(new AddTransitionCommand(nullptr,
c,
nullptr,
Effect::GetInternalMeta(transition_to_add, EFFECT_TYPE_TRANSITION),
olive::config.default_transition_length));
adding = true;
}
}
if (adding) {
olive::undo_stack.push(ca);
} else {
delete ca;
}
update_ui(true);
}
void Timeline::nest() {
if (sequence_ != nullptr) {
// get selected clips
QVector selected_clips = sequence_->SelectedClips();
// nest them
if (!selected_clips.isEmpty()) {
// get earliest point in selected clips
long earliest_point = LONG_MAX;
for (int i=0;itimeline_in(), earliest_point);
}
ComboAction* ca = new ComboAction();
// create "nest" sequence with the same attributes as the current sequence
SequencePtr s = std::make_shared();
s->name = olive::project_model.GetNextSequenceName(tr("Nested Sequence"));
s->width = sequence_->width;
s->height = sequence_->height;
s->frame_rate = sequence_->frame_rate;
s->audio_frequency = sequence_->audio_frequency;
s->audio_layout = sequence_->audio_layout;
QVector new_clips;
// copy all selected clips to the nest
for (int i=0;iappend(new DeleteClipAction(c));
// copy to new
Track* track = s->GetTrackList(c->type())->TrackAt(c->track()->Index());
ClipPtr copy = selected_clips.at(i)->copy(track);
copy->set_timeline_in(copy->timeline_in() - earliest_point);
copy->set_timeline_out(copy->timeline_out() - earliest_point);
track->AddClip(copy);
new_clips.append(copy);
}
// relink clips in new nested sequences
olive::timeline::RelinkClips(selected_clips, new_clips);
// add sequence to project
MediaPtr m = olive::project_model.CreateSequence(ca, s, false, nullptr);
// add nested sequence to active sequence
QVector media_list;
media_list.append(m.get());
olive::timeline::CreateGhostsFromMedia(sequence_.get(), earliest_point, media_list);
// ensure ghosts won't overlap anything
QVector all_sequence_clips = sequence_->GetAllClips();
for (int j=0;jtrack() == g.track
&& !((c->timeline_in() < g.in
&& c->timeline_out() < g.in)
|| (c->timeline_in() > g.out
&& c->timeline_out() > g.out))) {
// There's a clip occupied by the space taken up by this ghost. Move up a track, and seek again.
g.track = g.track->Next();
// Restart entire loop again
j = -1;
break;
}
}
}
}
sequence_->AddClipsFromGhosts(ca, ghosts);
panel_graph_editor->set_row(nullptr);
panel_effect_controls->Clear(true);
sequence_->ClearSelections();
olive::undo_stack.push(ca);
update_ui(true);
}
}
}
void Timeline::update_sequence() {
bool null_sequence = (sequence_ == nullptr);
for (int i=0;isetEnabled(!null_sequence);
}
snappingButton->setEnabled(!null_sequence);
zoomInButton->setEnabled(!null_sequence);
zoomOutButton->setEnabled(!null_sequence);
recordButton->setEnabled(!null_sequence);
addButton->setEnabled(!null_sequence);
headers->setEnabled(!null_sequence);
UpdateTitle();
}
bool Timeline::focused() {
return (sequence_ != nullptr && (headers->hasFocus() || video_area->hasFocus() || audio_area->hasFocus()));
}
void Timeline::repaint_timeline() {
if (!block_repaints) {
bool draw = true;
if (sequence_ != nullptr
&& !horizontalScrollBar->isSliderDown()
&& !horizontalScrollBar->is_resizing()
&& panel_sequence_viewer->playing
&& !zoom_just_changed) {
// auto scroll
if (olive::config.autoscroll == olive::AUTOSCROLL_PAGE_SCROLL) {
int playhead_x = getTimelineScreenPointFromFrame(sequence_->playhead);
if (playhead_x < 0 || playhead_x > (editAreas->width())) {
horizontalScrollBar->setValue(getScreenPointFromFrame(zoom, sequence_->playhead));
draw = false;
}
} else if (olive::config.autoscroll == olive::AUTOSCROLL_SMOOTH_SCROLL) {
if (center_scroll_to_playhead(horizontalScrollBar, zoom, sequence_->playhead)) {
draw = false;
}
}
}
if (draw) {
headers->update();
video_area->update();
audio_area->update();
if (sequence_ != nullptr
&& !zoom_just_changed) {
set_sb_max();
}
}
zoom_just_changed = false;
}
}
void Timeline::select_all() {
if (sequence_ != nullptr) {
sequence_->SelectAll();
repaint_timeline();
}
}
void Timeline::scroll_to_frame(long frame) {
scroll_to_frame_internal(horizontalScrollBar, frame, zoom, timeline_area->width());
}
void Timeline::resizeEvent(QResizeEvent *) {
// adjust maximum scrollbar
if (sequence_ != nullptr) set_sb_max();
// resize tool button widget to its contents
QList tool_button_children = tool_button_widget->findChildren();
int horizontal_spacing = static_cast(tool_button_widget->layout())->horizontalSpacing();
int vertical_spacing = static_cast(tool_button_widget->layout())->verticalSpacing();
int total_area = tool_button_widget->height();
int button_count = tool_button_children.size();
int button_height = tool_button_children.at(0)->sizeHint().height() + vertical_spacing;
int cols = 0;
int col_height;
if (button_height < total_area) {
do {
cols++;
col_height = (qCeil(double(button_count)/double(cols))*button_height)-vertical_spacing;
} while (col_height > total_area);
} else {
cols = button_count;
}
tool_button_widget->setFixedWidth((tool_button_children.at(0)->sizeHint().width())*cols + horizontal_spacing*(cols-1) + 1);
}
void Timeline::toggle_enable_on_selected_clips() {
if (sequence_ != nullptr) {
// get currently selected clips
QVector selected_clips = sequence_->SelectedClips();
if (!selected_clips.isEmpty()) {
// if clips are selected, create an undoable action
SetClipProperty* set_action = new SetClipProperty(kSetClipPropertyEnabled);
// add each selected clip to the action
for (int i=0;iAddSetting(c, !c->enabled());
}
// push the action
olive::undo_stack.push(set_action);
update_ui(false);
}
}
}
void Timeline::set_zoom_value(double v) {
// set zoom value
zoom = v;
// update header zoom to match
headers->update_zoom(zoom);
// set flag that zoom has just changed to prevent auto-scrolling since we change the scroll below
zoom_just_changed = true;
// set scrollbar to center the playhead
if (sequence_ != nullptr) {
// update scrollbar maximum value for new zoom
set_sb_max();
if (!horizontalScrollBar->is_resizing()) {
center_scroll_to_playhead(horizontalScrollBar, zoom, sequence_->playhead);
}
}
// repaint the timeline for the new zoom/location
repaint_timeline();
}
void Timeline::multiply_zoom(double m) {
showing_all = false;
set_zoom_value(zoom * m);
}
void Timeline::zoom_in() {
multiply_zoom(2.0);
}
void Timeline::zoom_out() {
multiply_zoom(0.5);
}
void Timeline::ChangeTrackHeightUniformly(int diff) {
if (sequence_ != nullptr) {
sequence_->ChangeTrackHeightsRelatively(diff);
}
// update the timeline
repaint_timeline();
}
void Timeline::IncreaseTrackHeight() {
ChangeTrackHeightUniformly(olive::timeline::kTrackHeightIncrement);
}
void Timeline::DecreaseTrackHeight() {
ChangeTrackHeightUniformly(-olive::timeline::kTrackHeightIncrement);
}
void Timeline::snapping_clicked(bool checked) {
olive::timeline::snapping = checked;
}
/*
bool Timeline::split_clip_and_relink(ComboAction *ca, int clip, long frame, bool relink) {
Clip* c = sequence_->clips.at(clip).get();
if (c != nullptr) {
QVector pre_clips;
QVector post_clips;
ClipPtr post = split_clip(ca, true, clip, frame);
if (post == nullptr) {
return false;
} else {
post_clips.append(post);
// if alt is not down, split clips links too
if (relink) {
pre_clips.append(clip);
bool original_clip_is_selected = c->IsSelected();
// find linked clips of old clip
for (int i=0;ilinked.size();i++) {
int l = c->linked.at(i);
Clip* link = sequence_->clips.at(l).get();
if ((original_clip_is_selected && link->IsSelected()) || !original_clip_is_selected) {
ClipPtr s = split_clip(ca, true, l, frame);
if (s != nullptr) {
pre_clips.append(l);
post_clips.append(s);
}
}
}
relink_clips_using_ids(pre_clips, post_clips);
}
ca->append(new AddClipCommand(sequence_.get(), post_clips));
return true;
}
}
return false;
}
*/
void Timeline::copy(bool del) {
if (sequence_ != nullptr) {
sequence_->AddSelectionsToClipboard(del);
}
}
void Timeline::ripple_delete() {
if (sequence_ != nullptr) {
QVector selections = sequence_->Selections();
if (!selections.isEmpty()) {
ComboAction* ca = new ComboAction();
sequence_->DeleteAreas(ca, selections, true, true);
olive::undo_stack.push(ca);
repaint_timeline();
} else if (olive::config.hover_focus && get_focused_panel() == this) {
ripple_delete_empty_space();
}
}
}
void Timeline::ripple_delete_empty_space()
{
if (sequence_ != nullptr) {
ComboAction* ca = new ComboAction();
sequence_->RippleDeleteEmptySpace(ca, cursor_track, cursor_frame);
olive::undo_stack.push(ca);
repaint_timeline();
}
}
void Timeline::set_marker() {
// determine if any clips are selected, and if so add markers to clips rather than the sequence
QVector selected_clips = sequence_->SelectedClips();
if (selected_clips.isEmpty()) {
Marker::SetOnSequence(sequence_.get());
} else {
// Remove any clips that don't contain the playhead
for (int i=0;itimeline_out() < sequence_->playhead
|| c->timeline_in() > sequence_->playhead) {
selected_clips.removeAt(i);
i--;
}
}
// Check if we removed them all
if (selected_clips.isEmpty()) {
return;
}
// If not, let's create markers on them
Marker::SetOnClips(selected_clips);
}
}
void Timeline::delete_inout() {
if (sequence_ != nullptr) {
sequence_->DeleteInToOut(false);
}
}
void Timeline::ripple_delete_inout() {
if (sequence_ != nullptr) {
sequence_->DeleteInToOut(true);
}
}
void Timeline::ripple_to_in_point() {
if (sequence_ != nullptr) {
sequence_->EditToPoint(true, true);
}
}
void Timeline::ripple_to_out_point() {
if (sequence_ != nullptr) {
sequence_->EditToPoint(false, true);
}
}
void Timeline::edit_to_in_point() {
if (sequence_ != nullptr) {
sequence_->EditToPoint(true, false);
}
}
void Timeline::edit_to_out_point() {
if (sequence_ != nullptr) {
sequence_->EditToPoint(false, false);
}
}
void Timeline::deselect() {
if (sequence_ != nullptr) {
sequence_->ClearSelections();
repaint_timeline();
}
}
void Timeline::split_at_playhead()
{
if (sequence_ != nullptr) {
sequence_->Split();
repaint_timeline();
}
}
long getFrameFromScreenPoint(double zoom, int x) {
long f = qRound(double(x) / zoom);
if (f < 0) {
return 0;
}
return f;
}
int getScreenPointFromFrame(double zoom, long frame) {
return qRound(double(frame)*zoom);
}
long Timeline::getTimelineFrameFromScreenPoint(int x) {
return getFrameFromScreenPoint(zoom, x + scroll);
}
QVector