/***
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 "effects/effect.h"
#include "effects/internal/solideffect.h"
#include "timeline/track.h"
#include "global/math.h"
#include "project/projectfunctions.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() == Track::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 = sequence()->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) {
sequence()->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);
sequence()->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);
sequence()->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 = sequence()->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);
}
void TimelineView::mousePressEvent(QMouseEvent *event) {
if (sequence() != nullptr) {
int effective_tool = olive::timeline::current_tool;
// some user actions will override which tool we'll be using
if (event->button() == Qt::MiddleButton) {
effective_tool = olive::timeline::TIMELINE_TOOL_HAND;
ParentTimeline()->creating = false;
} else if (event->button() == Qt::RightButton) {
effective_tool = olive::timeline::TIMELINE_TOOL_MENU;
ParentTimeline()->creating = false;
}
// ensure cursor_frame and cursor_track are up to date
mouseMoveEvent(event);
// store current cursor positions
ParentTimeline()->drag_x_start = event->pos().x();
ParentTimeline()->drag_y_start = event->pos().y();
// store current frame/tracks as the values to start dragging from
ParentTimeline()->drag_frame_start = ParentTimeline()->cursor_frame;
ParentTimeline()->drag_track_start = ParentTimeline()->cursor_track;
// get the clip the user is currently hovering over, priority to trim_target set from mouseMoveEvent
Clip* hovered_clip = ParentTimeline()->trim_target == nullptr ?
GetClipAtCursor()
: ParentTimeline()->trim_target;
bool shift = (event->modifiers() & Qt::ShiftModifier);
bool alt = (event->modifiers() & Qt::AltModifier);
// Normal behavior is to reset selections to zero when clicking, but if Shift is held, we add selections
// to the existing selections. `selection_offset` is the index to change selections from (and we don't touch
// any prior to that)
if (shift) {
ParentTimeline()->selection_cache = sequence()->Selections();
} else {
ParentTimeline()->selection_cache.clear();
}
// if the user is creating an object
if (ParentTimeline()->creating) {
Track::Type create_type = Track::kTypeVideo;
switch (ParentTimeline()->creating_object) {
case olive::timeline::ADD_OBJ_TITLE:
case olive::timeline::ADD_OBJ_SOLID:
case olive::timeline::ADD_OBJ_BARS:
break;
case olive::timeline::ADD_OBJ_TONE:
case olive::timeline::ADD_OBJ_NOISE:
case olive::timeline::ADD_OBJ_AUDIO:
create_type = Track::kTypeAudio;
break;
}
// if the track the user clicked is correct for the type of object we're adding
if (track_list_->type() == create_type) {
Ghost g;
g.in = g.old_in = g.out = g.old_out = ParentTimeline()->drag_frame_start;
g.track = ParentTimeline()->drag_track_start;
if (g.track == nullptr) {
g.track = track_list_->Last();
ParentTimeline()->drag_track_start = track_list_->Last();
g.track_movement = getTrackIndexFromScreenPoint(event->pos().y()) - g.track->Index();
}
g.trim_type = olive::timeline::TRIM_OUT;
ParentTimeline()->ghosts.append(g);
ParentTimeline()->moving_init = true;
ParentTimeline()->moving_proc = true;
}
} else {
// pass through tools to determine what action we'll be starting
switch (effective_tool) {
// many tools share pointer-esque behavior
case olive::timeline::TIMELINE_TOOL_POINTER:
case olive::timeline::TIMELINE_TOOL_RIPPLE:
case olive::timeline::TIMELINE_TOOL_SLIP:
case olive::timeline::TIMELINE_TOOL_ROLLING:
case olive::timeline::TIMELINE_TOOL_SLIDE:
case olive::timeline::TIMELINE_TOOL_MENU:
{
if (track_resizing && effective_tool != olive::timeline::TIMELINE_TOOL_MENU) {
// if the cursor is currently hovering over a track, init track resizing
ParentTimeline()->moving_init = true;
} else {
// check if we're currently hovering over a clip or not
if (hovered_clip != nullptr) {
if (hovered_clip->IsSelected()) {
if (shift) {
// if the user clicks a selected clip while holding shift, deselect the clip
hovered_clip->track()->DeselectArea(hovered_clip->timeline_in(), hovered_clip->timeline_out());
// if the user isn't holding alt, also deselect all of its links as well
if (!alt) {
for (int i=0;ilinked.size();i++) {
Clip* link = hovered_clip->linked.at(i);
link->track()->DeselectArea(link->timeline_in(), link->timeline_out());
}
}
} else if (olive::timeline::current_tool == olive::timeline::TIMELINE_TOOL_POINTER
&& ParentTimeline()->transition_select != kTransitionNone) {
// if the clip was selected by then the user clicked a transition, de-select the clip and its links
// and select the transition only
hovered_clip->track()->DeselectArea(hovered_clip->timeline_in(), hovered_clip->timeline_out());
for (int i=0;ilinked.size();i++) {
Clip* link = hovered_clip->linked.at(i);
link->track()->DeselectArea(link->timeline_in(), link->timeline_out());
}
long s_in, s_out;
// select the transition only
if (ParentTimeline()->transition_select == kTransitionOpening
&& hovered_clip->opening_transition != nullptr) {
s_in = hovered_clip->timeline_in();
if (hovered_clip->opening_transition->secondary_clip != nullptr) {
s_in -= hovered_clip->opening_transition->get_true_length();
}
s_out = hovered_clip->timeline_in() + hovered_clip->opening_transition->get_true_length();
} else if (ParentTimeline()->transition_select == kTransitionClosing
&& hovered_clip->closing_transition != nullptr) {
s_in = hovered_clip->timeline_out() - hovered_clip->closing_transition->get_true_length();
s_out = hovered_clip->timeline_out();
if (hovered_clip->closing_transition->secondary_clip != nullptr) {
s_out += hovered_clip->closing_transition->get_true_length();
}
}
hovered_clip->track()->SelectArea(s_in, s_out);
}
} else {
// if the clip is not already selected
// if shift is NOT down, we change clear all current selections
if (!shift) {
sequence()->ClearSelections();
}
long s_in = hovered_clip->timeline_in();
long s_out = hovered_clip->timeline_out();
// if user is using the pointer tool, they may be trying to select a transition
// check if the use is hovering over a transition
if (olive::timeline::current_tool == olive::timeline::TIMELINE_TOOL_POINTER) {
if (ParentTimeline()->transition_select == kTransitionOpening) {
// move the selection to only select the transitoin
s_out = hovered_clip->timeline_in() + hovered_clip->opening_transition->get_true_length();
// if the transition is a "shared" transition, adjust the selection to select both sides
if (hovered_clip->opening_transition->secondary_clip != nullptr) {
s_in -= hovered_clip->opening_transition->get_true_length();
}
} else if (ParentTimeline()->transition_select == kTransitionClosing) {
// move the selection to only select the transitoin
s_in = hovered_clip->timeline_out() - hovered_clip->closing_transition->get_true_length();
// if the transition is a "shared" transition, adjust the selection to select both sides
if (hovered_clip->closing_transition->secondary_clip != nullptr) {
s_out += hovered_clip->closing_transition->get_true_length();
}
}
}
// add the selection to the array
hovered_clip->track()->SelectArea(s_in, s_out);
// if the config is set to also seek with selections, do so now
if (olive::config.select_also_seeks) {
panel_sequence_viewer->seek(hovered_clip->timeline_in());
}
// if alt is not down, select links (provided we're not selecting transitions)
if (!alt && ParentTimeline()->transition_select == kTransitionNone) {
for (int i=0;ilinked.size();i++) {
Clip* link = hovered_clip->linked.at(i);
// check if the clip is already selected
if (!link->IsSelected()) {
link->track()->SelectClip(link);
}
}
}
}
// authorize the starting of a move action if the mouse moves after this
if (effective_tool != olive::timeline::TIMELINE_TOOL_MENU) {
ParentTimeline()->moving_init = true;
}
} else {
// if the user did not click a clip at all, we start a rectangle selection
if (!shift) {
sequence()->ClearSelections();
}
ParentTimeline()->rect_select_init = true;
}
// update everything
update_ui(false);
}
}
break;
case olive::timeline::TIMELINE_TOOL_HAND:
// initiate moving with the hand tool
ParentTimeline()->hand_moving = true;
break;
case olive::timeline::TIMELINE_TOOL_EDIT:
// if the config is set to seek with the edit tool, do so now
if (olive::config.edit_tool_also_seeks) {
panel_sequence_viewer->seek(ParentTimeline()->drag_frame_start);
}
// initiate selecting
ParentTimeline()->selecting = true;
break;
case olive::timeline::TIMELINE_TOOL_RAZOR:
{
// initiate razor tool
ParentTimeline()->splitting = true;
// add this track as a track being split by the razor
ParentTimeline()->split_tracks.append(ParentTimeline()->drag_track_start);
update_ui(false);
}
break;
case olive::timeline::TIMELINE_TOOL_TRANSITION:
{
// if there is a clip to run the transition tool on, initiate the transition tool
if (ParentTimeline()->transition_tool_open_clip != nullptr
|| ParentTimeline()->transition_tool_close_clip != nullptr) {
ParentTimeline()->transition_tool_init = true;
}
}
break;
}
}
}
}
void make_room_for_transition(ComboAction* ca,
Clip* c,
int type,
long transition_start,
long transition_end,
bool delete_old_transitions,
long timeline_in = -1,
long timeline_out = -1) {
// it's possible to specify other in/out points for the clip, but default behavior is to use the ones existing
if (timeline_in < 0) {
timeline_in = c->timeline_in();
}
if (timeline_out < 0) {
timeline_out = c->timeline_out();
}
// make room for transition
if (type == kTransitionOpening) {
if (delete_old_transitions && c->opening_transition != nullptr) {
ca->append(new DeleteTransitionCommand(c->opening_transition));
}
if (c->closing_transition != nullptr) {
if (transition_end >= c->timeline_out()) {
ca->append(new DeleteTransitionCommand(c->closing_transition));
} else if (transition_end > c->timeline_out() - c->closing_transition->get_true_length()) {
ca->append(new ModifyTransitionCommand(c->closing_transition, c->timeline_out() - transition_end));
}
}
} else {
if (delete_old_transitions && c->closing_transition != nullptr) {
ca->append(new DeleteTransitionCommand(c->closing_transition));
}
if (c->opening_transition != nullptr) {
if (transition_start <= c->timeline_in()) {
ca->append(new DeleteTransitionCommand(c->opening_transition));
} else if (transition_start < c->timeline_in() + c->opening_transition->get_true_length()) {
ca->append(new ModifyTransitionCommand(c->opening_transition, transition_start - c->timeline_in()));
}
}
}
}
void TimelineView::VerifyTransitionsAfterCreating(ComboAction* ca, Clip* open, Clip* close, long transition_start, long transition_end) {
// in case the user made the transition larger than the clips, we're going to delete everything under
// the transition ghost and extend the clips to the transition's coordinates as necessary
if (open == nullptr && close == nullptr) {
qWarning() << "VerifyTransitionsAfterCreating() called with two null clips";
return;
}
// determine whether this is a "shared" transition between to clips or not
bool shared_transition = (open != nullptr && close != nullptr);
Track* track = nullptr;
// first we set the clips to "undeletable" so they aren't affected by delete_areas_and_relink()
if (open != nullptr) {
open->undeletable = true;
track = open->track();
}
if (close != nullptr) {
close->undeletable = true;
track = close->track();
}
// set the area to delete to the transition's coordinates and clear it
QVector areas;
areas.append(Selection(transition_start, transition_end, track));
sequence()->DeleteAreas(ca, areas, false);
// set the clips back to undeletable now that we're done
if (open != nullptr) {
open->undeletable = false;
}
if (close != nullptr) {
close->undeletable = false;
}
// loop through both kinds of transition
for (int t=kTransitionOpening;t<=kTransitionClosing;t++) {
Clip* clip_ref = (t == kTransitionOpening) ? open : close;
// if we have an opening transition:
if (clip_ref != nullptr) {
// make_room_for_transition will adjust the opposite transition to make space for this one,
// for example if the user makes an opening transition that overlaps the closing transition, it'll resize
// or even delete the closing transition if necessary (and vice versa)
make_room_for_transition(ca, clip_ref, t, transition_start, transition_end, true);
// check if the transition coordinates require the clip to be resized
if (transition_start < clip_ref->timeline_in() || transition_end > clip_ref->timeline_out()) {
long new_in, new_out;
if (t == kTransitionOpening) {
// if the transition is shared, it doesn't matter if the transition extend beyond the in point since
// that'll be "absorbed" by the other clip
new_in = (shared_transition) ? open->timeline_in() : qMin(transition_start, open->timeline_in());
new_out = qMax(transition_end, open->timeline_out());
} else {
new_in = qMin(transition_start, close->timeline_in());
// if the transition is shared, it doesn't matter if the transition extend beyond the out point since
// that'll be "absorbed" by the other clip
new_out = (shared_transition) ? close->timeline_out() : qMax(transition_end, close->timeline_out());
}
clip_ref->Move(ca,
new_in,
new_out,
clip_ref->clip_in() - (clip_ref->timeline_in() - new_in),
clip_ref->track());
}
}
}
}
int TimelineView::GetTotalAreaHeight()
{
// start by adding a track height worth of padding
int panel_height = olive::timeline::kTrackDefaultHeight;
for (int i=0;iTrackCount();i++) {
panel_height += track_list_->TrackAt(i)->height();
}
return panel_height;
}
void TimelineView::mouseReleaseEvent(QMouseEvent *event) {
QToolTip::hideText();
if (sequence() != nullptr) {
bool alt = (event->modifiers() & Qt::AltModifier);
bool shift = (event->modifiers() & Qt::ShiftModifier);
bool ctrl = (event->modifiers() & Qt::ControlModifier);
if (event->button() == Qt::LeftButton) {
ComboAction* ca = new ComboAction();
bool push_undo = false;
if (ParentTimeline()->creating) {
if (ParentTimeline()->ghosts.size() > 0) {
const Ghost& g = ParentTimeline()->ghosts.at(0);
if (ParentTimeline()->creating_object == olive::timeline::ADD_OBJ_AUDIO) {
olive::MainWindow->statusBar()->clearMessage();
panel_sequence_viewer->cue_recording(qMin(g.in, g.out), qMax(g.in, g.out), g.track);
ParentTimeline()->creating = false;
} else if (g.in != g.out) {
ClipPtr c = std::make_shared(g.track->Sibling(g.track_movement));
c->set_media(nullptr, 0);
c->set_timeline_in(qMin(g.in, g.out));
c->set_timeline_out(qMax(g.in, g.out));
c->set_clip_in(0);
c->set_color(192, 192, 64);
if (ctrl) {
insert_clips(ca, sequence());
} else {
sequence()->DeleteAreas(ca, {c->ToSelection()}, false);
}
QVector add;
add.append(c);
ca->append(new AddClipCommand(add));
if (c->type() == Track::kTypeVideo && olive::config.add_default_effects_to_clips) {
// default video effects (before custom effects)
c->effects.append(Effect::Create(c.get(), Effect::GetInternalMeta(EFFECT_INTERNAL_TRANSFORM, EFFECT_TYPE_EFFECT)));
}
switch (ParentTimeline()->creating_object) {
case olive::timeline::ADD_OBJ_TITLE:
c->set_name(tr("Title"));
c->effects.append(Effect::Create(c.get(), Effect::GetInternalMeta(EFFECT_INTERNAL_RICHTEXT, EFFECT_TYPE_EFFECT)));
break;
case olive::timeline::ADD_OBJ_SOLID:
c->set_name(tr("Solid Color"));
c->effects.append(Effect::Create(c.get(), Effect::GetInternalMeta(EFFECT_INTERNAL_SOLID, EFFECT_TYPE_EFFECT)));
break;
case olive::timeline::ADD_OBJ_BARS:
{
c->set_name(tr("Bars"));
EffectPtr e = Effect::Create(c.get(), Effect::GetInternalMeta(EFFECT_INTERNAL_SOLID, EFFECT_TYPE_EFFECT));
// Auto-select bars
SolidEffect* solid_effect = static_cast(e.get());
solid_effect->SetType(SolidEffect::SOLID_TYPE_BARS);
c->effects.append(e);
}
break;
case olive::timeline::ADD_OBJ_TONE:
c->set_name(tr("Tone"));
c->effects.append(Effect::Create(c.get(), Effect::GetInternalMeta(EFFECT_INTERNAL_TONE, EFFECT_TYPE_EFFECT)));
break;
case olive::timeline::ADD_OBJ_NOISE:
c->set_name(tr("Noise"));
c->effects.append(Effect::Create(c.get(), Effect::GetInternalMeta(EFFECT_INTERNAL_NOISE, EFFECT_TYPE_EFFECT)));
break;
default:
break;
}
if (c->type() == Track::kTypeAudio && olive::config.add_default_effects_to_clips) {
// default audio effects (after custom effects)
c->effects.append(Effect::Create(c.get(), Effect::GetInternalMeta(EFFECT_INTERNAL_VOLUME, EFFECT_TYPE_EFFECT)));
c->effects.append(Effect::Create(c.get(), Effect::GetInternalMeta(EFFECT_INTERNAL_PAN, EFFECT_TYPE_EFFECT)));
}
push_undo = true;
if (!shift) {
ParentTimeline()->creating = false;
}
}
}
} else if (ParentTimeline()->moving_proc) {
// see if any clips actually moved, otherwise we don't need to do any processing
// (perhaps this could be moved further up to cover more actions?)
bool process_moving = false;
for (int i=0;ighosts.size();i++) {
const Ghost& g = ParentTimeline()->ghosts.at(i);
if (g.in != g.old_in
|| g.out != g.old_out
|| g.clip_in != g.old_clip_in
|| g.track_movement != 0) {
process_moving = true;
break;
}
}
if (process_moving) {
const Ghost& first_ghost = ParentTimeline()->ghosts.at(0);
// start a ripple movement
if (olive::timeline::current_tool == olive::timeline::TIMELINE_TOOL_RIPPLE) {
// ripple_length becomes the length/number of frames we trimmed
// ripple_point is the "axis" around which we move all the clips, any clips after it get moved
long ripple_length;
long ripple_point = LONG_MAX;
if (ParentTimeline()->trim_type == olive::timeline::TRIM_IN) {
// it's assumed that all the ghosts rippled by the same length, so we just take the difference of the
// first ghost here
ripple_length = first_ghost.old_in - first_ghost.in;
// for in trimming movements we also move the selections forward (unnecessary for out trimming since
// the selected clips more or less stay in the same place)
/*
for (int i=0;iselections.size();i++) {
sequence()->selections[i].in += ripple_length;
sequence()->selections[i].out += ripple_length;
}
*/
} else {
// use the out points for length if the user trimmed the out point
ripple_length = first_ghost.old_out - ParentTimeline()->ghosts.at(0).out;
}
// build a list of "ignore clips" that won't get affected by ripple_clips() below
QVector ignore_clips;
for (int i=0;ighosts.size();i++) {
const Ghost& g = ParentTimeline()->ghosts.at(i);
// for the same reason that we pushed selections forward above, for in trimming,
// we push the ghosts forward here
if (ParentTimeline()->trim_type == olive::timeline::TRIM_IN) {
ignore_clips.append(g.clip);
ParentTimeline()->ghosts[i].in += ripple_length;
ParentTimeline()->ghosts[i].out += ripple_length;
}
// find the earliest ripple point
long comp_point = (ParentTimeline()->trim_type == olive::timeline::TRIM_IN) ? g.old_in : g.old_out;
ripple_point = qMin(ripple_point, comp_point);
}
// if this was out trimming, flip the direction of the ripple
if (ParentTimeline()->trim_type == olive::timeline::TRIM_OUT) ripple_length = -ripple_length;
// finally, ripple everything
sequence()->Ripple(ca, ripple_point, ripple_length, ignore_clips);
}
if (olive::timeline::current_tool == olive::timeline::TIMELINE_TOOL_POINTER
&& (event->modifiers() & Qt::AltModifier)
&& ParentTimeline()->trim_target == nullptr) {
// if the user was holding alt (and not trimming), we duplicate clips rather than move them
QVector old_clips;
QVector new_clips;
QVector delete_areas;
for (int i=0;ighosts.size();i++) {
const Ghost& g = ParentTimeline()->ghosts.at(i);
if (g.old_in != g.in || g.old_out != g.out || g.track_movement != 0 || g.clip_in != g.old_clip_in) {
// create copy of clip
ClipPtr c = g.clip->copy(g.track->Sibling(g.track_movement));
c->set_timeline_in(g.in);
c->set_timeline_out(g.out);
delete_areas.append(g.ToSelection());
old_clips.append(g.clip);
new_clips.append(c);
}
}
if (new_clips.size() > 0) {
// delete anything under the new clips
sequence()->DeleteAreas(ca, delete_areas, false);
// relink duplicated clips
olive::timeline::RelinkClips(old_clips, new_clips);
// add them
ca->append(new AddClipCommand(new_clips));
}
} else {
// if we're not holding alt, this will just be a move
// if the user is holding ctrl, perform an insert rather than an overwrite
if (olive::timeline::current_tool == olive::timeline::TIMELINE_TOOL_POINTER && ctrl) {
insert_clips(ca, sequence());
} else if (olive::timeline::current_tool == olive::timeline::TIMELINE_TOOL_POINTER || olive::timeline::current_tool == olive::timeline::TIMELINE_TOOL_SLIDE) {
// if the user is not holding ctrl, we start standard clip movement
// delete everything under the new clips
QVector delete_areas;
for (int i=0;ighosts.size();i++) {
// step 1 - set clips that are moving to "undeletable" (to avoid step 2 deleting any part of them)
const Ghost& g = ParentTimeline()->ghosts.at(i);
// set clip to undeletable so it's unaffected by delete_areas_and_relink() below
g.clip->undeletable = true;
// if the user was moving a transition make sure they're undeletable too
if (g.transition != nullptr) {
g.transition->parent_clip->undeletable = true;
if (g.transition->secondary_clip != nullptr) {
g.transition->secondary_clip->undeletable = true;
}
}
// set area to delete
delete_areas.append(g.ToSelection());
}
sequence()->DeleteAreas(ca, delete_areas, false);
// clean up, i.e. make everything not undeletable again
for (int i=0;ighosts.size();i++) {
const Ghost& g = ParentTimeline()->ghosts.at(i);
g.clip->undeletable = false;
if (g.transition != nullptr) {
g.transition->parent_clip->undeletable = false;
if (g.transition->secondary_clip != nullptr) {
g.transition->secondary_clip->undeletable = false;
}
}
}
}
// finally, perform actual movement of clips
for (int i=0;ighosts.size();i++) {
Ghost& g = ParentTimeline()->ghosts[i];
Clip* c = g.clip;
if (g.transition == nullptr) {
// if this was a clip rather than a transition
c->Move(ca,
(g.in - g.old_in),
(g.out - g.old_out),
(g.clip_in - g.old_clip_in),
g.track->Sibling(g.track_movement),
false,
true);
} else {
// if the user was moving a transition
bool is_opening_transition = (g.transition == c->opening_transition);
long new_transition_length = g.out - g.in;
if (g.transition->secondary_clip != nullptr) new_transition_length >>= 1;
ca->append(
new ModifyTransitionCommand(is_opening_transition ? c->opening_transition : c->closing_transition,
new_transition_length)
);
long clip_length = c->length();
if (g.transition->secondary_clip != nullptr) {
// if this is a shared transition
if (g.in != g.old_in && g.trim_type == olive::timeline::TRIM_NONE) {
long movement = g.in - g.old_in;
// check if the transition is going to extend the out point (opening clip)
long timeline_out_movement = 0;
if (g.out > g.transition->parent_clip->timeline_out()) {
timeline_out_movement = g.out - g.transition->parent_clip->timeline_out();
}
// check if the transition is going to extend the in point (closing clip)
long timeline_in_movement = 0;
if (g.in < g.transition->secondary_clip->timeline_in()) {
timeline_in_movement = g.in - g.transition->secondary_clip->timeline_in();
}
g.transition->parent_clip->Move(ca, movement, timeline_out_movement, movement, g.transition->parent_clip->track(), false, true);
g.transition->secondary_clip->Move(ca, timeline_in_movement, movement, timeline_in_movement, g.transition->secondary_clip->track(), false, true);
make_room_for_transition(ca, g.transition->parent_clip, kTransitionOpening, g.in, g.out, false);
make_room_for_transition(ca, g.transition->secondary_clip, kTransitionClosing, g.in, g.out, false);
}
} else if (is_opening_transition) {
if (g.in != g.old_in) {
// if transition is going to make the clip bigger, make the clip bigger
// check if the transition is going to extend the out point
long timeline_out_movement = 0;
if (g.out > g.transition->parent_clip->timeline_out()) {
timeline_out_movement = g.out - g.transition->parent_clip->timeline_out();
}
c->Move(ca,
(g.in - g.old_in),
timeline_out_movement,
(g.clip_in - g.old_clip_in),
g.track,
false,
true);
clip_length -= (g.in - g.old_in);
}
make_room_for_transition(ca, c, kTransitionOpening, g.in, g.out, false);
} else {
if (g.out != g.old_out) {
// check if the transition is going to extend the in point
long timeline_in_movement = 0;
if (g.in < g.transition->parent_clip->timeline_in()) {
timeline_in_movement = g.in - g.transition->parent_clip->timeline_in();
}
// if transition is going to make the clip bigger, make the clip bigger
c->Move(ca, timeline_in_movement, (g.out - g.old_out), timeline_in_movement, c->track(), false, true);
clip_length += (g.out - g.old_out);
}
make_room_for_transition(ca, c, kTransitionClosing, g.in, g.out, false);
}
}
}
// time to verify the transitions of moved clips
for (int i=0;ighosts.size();i++) {
const Ghost& g = ParentTimeline()->ghosts.at(i);
// only applies to moving clips, transitions are verified above instead
if (g.transition == nullptr) {
Clip* c = g.clip;
long new_clip_length = g.out - g.in;
// using a for loop between constants to repeat the same steps for the opening and closing transitions
for (int t=kTransitionOpening;t<=kTransitionClosing;t++) {
TransitionPtr transition = (t == kTransitionOpening) ? c->opening_transition : c->closing_transition;
// check the whether the clip has a transition here
if (transition != nullptr) {
// if the new clip size exceeds the opening transition's length, resize the transition
if (new_clip_length < transition->get_true_length()) {
ca->append(new ModifyTransitionCommand(transition, new_clip_length));
}
// check if the transition is a shared transition (it'll never have a secondary clip if it isn't)
if (transition->secondary_clip != nullptr) {
// check if the transition's "edge" is going to move
if ((t == kTransitionOpening && g.in != g.old_in)
|| (t == kTransitionClosing && g.out != g.old_out)
|| (g.track_movement != 0)) {
// if we're here, this clip shares its opening transition as the closing transition of another
// clip (or vice versa), and the in point is moving, so we may have to account for this
// the other clip sharing this transition may be moving as well, meaning we don't have to do
// anything
bool split = true;
// loop through ghosts to find out
// for a shared transition, the secondary_clip will always be the closing transition side and
// the parent_clip will always be the opening transition side
Clip* search_clip = (t == kTransitionOpening)
? transition->secondary_clip : transition->parent_clip;
for (int j=0;jghosts.size();j++) {
const Ghost& other_clip_ghost = ParentTimeline()->ghosts.at(j);
if (other_clip_ghost.clip == search_clip) {
// we found the other clip in the current ghosts/selections
// see if it's destination edge will be equal to this ghost's edge (in which case the
// transition doesn't need to change)
//
// also only do this if j is less than i, because it only needs to happen once and chances are
// the other clip already
bool edges_still_touch = (other_clip_ghost.track_movement == g.track_movement);
if (edges_still_touch) {
if (t == kTransitionOpening) {
edges_still_touch = (other_clip_ghost.out == g.in);
} else {
edges_still_touch = (other_clip_ghost.in == g.out);
}
}
if (edges_still_touch || j < i) {
split = false;
}
break;
}
}
if (split) {
// separate shared transition into one transition for each clip
if (t == kTransitionOpening) {
// set transition to single-clip mode
ca->append(new SetPointer(reinterpret_cast(&transition->secondary_clip),
nullptr));
// create duplicate transition for other clip
ca->append(new AddTransitionCommand(nullptr,
transition->secondary_clip,
transition,
nullptr,
0));
} else {
// set transition to single-clip mode
ca->append(new SetPointer(reinterpret_cast(&transition->secondary_clip),
nullptr));
// that transition will now attach to the other clip, so we duplicate it for this one
// create duplicate transition for this clip
ca->append(new AddTransitionCommand(nullptr,
transition->secondary_clip,
transition,
nullptr,
0));
}
}
}
}
}
}
}
}
}
// move selections to match new ghosts
QVector new_selections;
for (int i=0;ighosts.size();i++) {
new_selections.append(ParentTimeline()->ghosts.at(i).ToSelection());
}
ca->append(new SetSelectionsCommand(sequence(), sequence()->Selections(), new_selections));
push_undo = true;
}
} else if (ParentTimeline()->selecting || ParentTimeline()->rect_select_proc) {
} else if (ParentTimeline()->transition_tool_proc) {
const Ghost& g = ParentTimeline()->ghosts.at(0);
// if the transition is greater than 0 length (if it is 0, we make nothing)
if (g.in != g.out) {
// get transition coordinates on the timeline
long transition_start = qMin(g.in, g.out);
long transition_end = qMax(g.in, g.out);
// get clip references from tool's cached data
Clip* open = ParentTimeline()->transition_tool_open_clip;
Clip* close = ParentTimeline()->transition_tool_close_clip;
// if it's shared, the transition length is halved (one half for each clip will result in the full length)
long transition_length = transition_end - transition_start;
if (open != nullptr && close != nullptr) {
transition_length /= 2;
}
VerifyTransitionsAfterCreating(ca, open, close, transition_start, transition_end);
// finally, add the transition to these clips
ca->append(new AddTransitionCommand(open,
close,
nullptr,
ParentTimeline()->transition_tool_meta,
transition_length));
push_undo = true;
}
} else if (ParentTimeline()->splitting) {
bool split = false;
for (int i=0;isplit_tracks.size();i++) {
Clip* split_index = ParentTimeline()->split_tracks.at(i)->GetClipFromPoint(ParentTimeline()->drag_frame_start);
if (split_index != nullptr
&& sequence()->SplitClipAtPositions(ca, split_index, {ParentTimeline()->drag_frame_start}, !alt)) {
split = true;
}
}
if (split) {
push_undo = true;
}
}
// remove duplicate selections
sequence()->TidySelections();
/*
if (selection_command != nullptr) {
selection_command->new_data = sequence()->selections;
ca->append(selection_command);
selection_command = nullptr;
push_undo = true;
}
*/
if (push_undo) {
olive::undo_stack.push(ca);
} else {
delete ca;
}
// destroy all ghosts
ParentTimeline()->ghosts.clear();
// clear split tracks
ParentTimeline()->split_tracks.clear();
ParentTimeline()->selecting = false;
ParentTimeline()->moving_proc = false;
ParentTimeline()->moving_init = false;
ParentTimeline()->splitting = false;
olive::timeline::snapped = false;
ParentTimeline()->rect_select_init = false;
ParentTimeline()->rect_select_proc = false;
ParentTimeline()->transition_tool_init = false;
ParentTimeline()->transition_tool_proc = false;
pre_clips.clear();
post_clips.clear();
update_ui(true);
}
ParentTimeline()->hand_moving = false;
}
}
void TimelineView::init_ghosts() {
for (int i=0;ighosts.size();i++) {
Ghost& g = ParentTimeline()->ghosts[i];
Clip* c = g.clip;
g.track = c->track();
g.clip_in = g.old_clip_in = c->clip_in();
if (olive::timeline::current_tool == olive::timeline::TIMELINE_TOOL_SLIP) {
g.clip_in = g.old_clip_in = c->clip_in(true);
g.in = g.old_in = c->timeline_in(true);
g.out = g.old_out = c->timeline_out(true);
g.ghost_length = g.old_out - g.old_in;
} else if (g.transition == nullptr) {
// this ghost is for a clip
g.in = g.old_in = c->timeline_in();
g.out = g.old_out = c->timeline_out();
g.ghost_length = g.old_out - g.old_in;
} else if (g.transition == c->opening_transition) {
g.in = g.old_in = c->timeline_in(true);
g.ghost_length = c->opening_transition->get_length();
g.out = g.old_out = g.in + g.ghost_length;
} else if (g.transition == c->closing_transition) {
g.out = g.old_out = c->timeline_out(true);
g.ghost_length = c->closing_transition->get_length();
g.in = g.old_in = g.out - g.ghost_length;
g.clip_in = g.old_clip_in = c->clip_in() + c->length() - c->closing_transition->get_true_length();
}
// used for trim ops
g.media_length = c->media_length();
}
/*
for (int i=0;iselections.size();i++) {
Selection& s = sequence()->selections[i];
s.old_in = s.in;
s.old_out = s.out;
s.old_track = s.track;
}
*/
}
void validate_transitions(Clip* c, int transition_type, long& frame_diff) {
long validator;
if (transition_type == kTransitionOpening) {
// prevent from going below 0 on the timeline
validator = c->timeline_in() + frame_diff;
if (validator < 0) frame_diff -= validator;
// prevent from going below 0 for the media
validator = c->clip_in() + frame_diff;
if (validator < 0) frame_diff -= validator;
// prevent transition from exceeding media length
validator -= c->media_length();
if (validator > 0) frame_diff -= validator;
} else {
// prevent from going below 0 on the timeline
validator = c->timeline_out() + frame_diff;
if (validator < 0) frame_diff -= validator;
// prevent from going below 0 for the media
validator = c->clip_in() + c->length() + frame_diff;
if (validator < 0) frame_diff -= validator;
// prevent transition from exceeding media length
validator -= c->media_length();
if (validator > 0) frame_diff -= validator;
}
}
void TimelineView::update_ghosts(const QPoint& mouse_pos, bool lock_frame) {
int effective_tool = olive::timeline::current_tool;
if (ParentTimeline()->importing || ParentTimeline()->creating) effective_tool = olive::timeline::TIMELINE_TOOL_POINTER;
long frame_diff = (lock_frame) ? 0 : ParentTimeline()->getTimelineFrameFromScreenPoint(mouse_pos.x()) - ParentTimeline()->drag_frame_start;
long validator;
long earliest_in_point = LONG_MAX;
int track_diff = getTrackIndexFromScreenPoint(mouse_pos.y()) - ParentTimeline()->drag_track_start->Index();
// first try to snap
long fm;
if (effective_tool != olive::timeline::TIMELINE_TOOL_SLIP) {
// slipping doesn't move the clips so we don't bother snapping for it
for (int i=0;ighosts.size();i++) {
const Ghost& g = ParentTimeline()->ghosts.at(i);
// snap ghost's in point
if ((olive::timeline::current_tool != olive::timeline::TIMELINE_TOOL_TRANSITION && ParentTimeline()->trim_target == nullptr)
|| g.trim_type == olive::timeline::TRIM_IN
|| ParentTimeline()->transition_tool_open_clip != nullptr) {
fm = g.old_in + frame_diff;
if (sequence()->SnapPoint(&fm, ParentTimeline()->zoom, true, true, true)) {
frame_diff = fm - g.old_in;
break;
}
}
// snap ghost's out point
if ((olive::timeline::current_tool != olive::timeline::TIMELINE_TOOL_TRANSITION && ParentTimeline()->trim_target == nullptr)
|| g.trim_type == olive::timeline::TRIM_OUT
|| ParentTimeline()->transition_tool_close_clip != nullptr) {
fm = g.old_out + frame_diff;
if (sequence()->SnapPoint(&fm, ParentTimeline()->zoom, true, true, true)) {
frame_diff = fm - g.old_out;
break;
}
}
// if the ghost is attached to a clip, snap its markers too
if (ParentTimeline()->trim_target == nullptr
&& g.clip != nullptr
&& olive::timeline::current_tool != olive::timeline::TIMELINE_TOOL_TRANSITION) {
Clip* c = g.clip;
for (int j=0;jget_markers().size();j++) {
long marker_real_time = c->get_markers().at(j).frame + c->timeline_in() - c->clip_in();
fm = marker_real_time + frame_diff;
if (sequence()->SnapPoint(&fm, ParentTimeline()->zoom, true, true, true)) {
frame_diff = fm - marker_real_time;
break;
}
}
}
}
}
bool clips_are_movable = (effective_tool == olive::timeline::TIMELINE_TOOL_POINTER || effective_tool == olive::timeline::TIMELINE_TOOL_SLIDE);
// validate ghosts
long temp_frame_diff = frame_diff; // cache to see if we change it (thus cancelling any snap)
for (int i=0;ighosts.size();i++) {
const Ghost& g = ParentTimeline()->ghosts.at(i);
Clip* c = nullptr;
if (g.clip != nullptr) {
c = g.clip;
}
const FootageStream* ms = nullptr;
if (g.clip != nullptr && c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_FOOTAGE) {
ms = c->media_stream();
}
// validate ghosts for trimming
if (ParentTimeline()->creating) {
// i feel like we might need something here but we haven't so far?
} else if (effective_tool == olive::timeline::TIMELINE_TOOL_SLIP) {
if ((c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_SEQUENCE)
|| (ms != nullptr && !ms->infinite_length)) {
// prevent slip moving a clip below 0 clip_in
validator = g.old_clip_in - frame_diff;
if (validator < 0) frame_diff += validator;
// prevent slip moving clip beyond media length
validator += g.ghost_length;
if (validator > g.media_length) frame_diff += validator - g.media_length;
}
} else if (g.trim_type != olive::timeline::TRIM_NONE) {
if (g.trim_type == olive::timeline::TRIM_IN) {
// prevent clip/transition length from being less than 1 frame long
validator = g.ghost_length - frame_diff;
if (validator < 1) frame_diff -= (1 - validator);
// prevent timeline in from going below 0
if (effective_tool != olive::timeline::TIMELINE_TOOL_RIPPLE) {
validator = g.old_in + frame_diff;
if (validator < 0) frame_diff -= validator;
}
// prevent clip_in from going below 0
if ((c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_SEQUENCE)
|| (ms != nullptr && !ms->infinite_length)) {
validator = g.old_clip_in + frame_diff;
if (validator < 0) frame_diff -= validator;
}
} else {
// prevent clip length from being less than 1 frame long
validator = g.ghost_length + frame_diff;
if (validator < 1) frame_diff += (1 - validator);
// prevent clip length exceeding media length
if ((c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_SEQUENCE)
|| (ms != nullptr && !ms->infinite_length)) {
validator = g.old_clip_in + g.ghost_length + frame_diff;
if (validator > g.media_length) frame_diff -= validator - g.media_length;
}
}
// prevent dual transition from going below 0 on the primary or media length on the secondary
if (g.transition != nullptr && g.transition->secondary_clip != nullptr) {
Clip* otc = g.transition->parent_clip;
Clip* ctc = g.transition->secondary_clip;
if (g.trim_type == olive::timeline::TRIM_IN) {
frame_diff -= g.transition->get_true_length();
} else {
frame_diff += g.transition->get_true_length();
}
validate_transitions(otc, kTransitionOpening, frame_diff);
validate_transitions(ctc, kTransitionClosing, frame_diff);
frame_diff = -frame_diff;
validate_transitions(otc, kTransitionOpening, frame_diff);
validate_transitions(ctc, kTransitionClosing, frame_diff);
frame_diff = -frame_diff;
if (g.trim_type == olive::timeline::TRIM_IN) {
frame_diff += g.transition->get_true_length();
} else {
frame_diff -= g.transition->get_true_length();
}
}
// ripple ops
if (effective_tool == olive::timeline::TIMELINE_TOOL_RIPPLE) {
for (int j=0;jtrim_type == olive::timeline::TRIM_IN) {
validator = post->timeline_in() - frame_diff;
if (validator < 0) frame_diff += validator;
}
// prevent any post-clips colliding with pre-clips
for (int k=0;ktrack() == post->track()) {
if (ParentTimeline()->trim_type == olive::timeline::TRIM_IN) {
validator = post->timeline_in() - frame_diff - pre->timeline_out();
if (validator < 0) frame_diff += validator;
} else {
validator = post->timeline_in() + frame_diff - pre->timeline_out();
if (validator < 0) frame_diff -= validator;
}
}
}
}
}
} else if (clips_are_movable) { // validate ghosts for moving
// prevent clips from moving below 0 on the timeline
validator = g.old_in + frame_diff;
if (validator < 0) frame_diff -= validator;
if (g.transition != nullptr) {
if (g.transition->secondary_clip != nullptr) {
// prevent dual transitions from going below 0 on the primary or above media length on the secondary
validator = g.transition->parent_clip->clip_in(true) + frame_diff;
if (validator < 0) frame_diff -= validator;
validator = g.transition->secondary_clip->timeline_out(true) - g.transition->secondary_clip->timeline_in(true) - g.transition->get_length() + g.transition->secondary_clip->clip_in(true) + frame_diff;
if (validator < 0) frame_diff -= validator;
validator = g.transition->parent_clip->clip_in() + frame_diff - g.transition->parent_clip->media_length() + g.transition->get_true_length();
if (validator > 0) frame_diff -= validator;
validator = g.transition->secondary_clip->timeline_out(true) - g.transition->secondary_clip->timeline_in(true) + g.transition->secondary_clip->clip_in(true) + frame_diff - g.transition->secondary_clip->media_length();
if (validator > 0) frame_diff -= validator;
} else {
// prevent clip_in from going below 0
if ((c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_SEQUENCE)
|| (ms != nullptr && !ms->infinite_length)) {
validator = g.old_clip_in + frame_diff;
if (validator < 0) frame_diff -= validator;
}
// prevent clip length exceeding media length
if ((c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_SEQUENCE)
|| (ms != nullptr && !ms->infinite_length)) {
validator = g.old_clip_in + g.ghost_length + frame_diff;
if (validator > g.media_length) frame_diff -= validator - g.media_length;
}
}
}
// Prevent any clips from going below the "zeroeth" track
if (ParentTimeline()->importing || g.track->type() == track_list_->type()) {
int track_validator = g.track->Index() + track_diff;
if (track_validator < 0) {
track_diff -= track_validator;
}
}
} else if (effective_tool == olive::timeline::TIMELINE_TOOL_TRANSITION) {
if (ParentTimeline()->transition_tool_open_clip == nullptr
|| ParentTimeline()->transition_tool_close_clip == nullptr) {
validate_transitions(c, g.media_stream, frame_diff);
} else {
// open transition clip
Clip* otc = ParentTimeline()->transition_tool_open_clip;
// close transition clip
Clip* ctc = ParentTimeline()->transition_tool_close_clip;
if (g.media_stream == kTransitionClosing) {
// swap
Clip* temp = otc;
otc = ctc;
ctc = temp;
}
// always gets a positive frame_diff
validate_transitions(otc, kTransitionOpening, frame_diff);
validate_transitions(ctc, kTransitionClosing, frame_diff);
// always gets a negative frame_diff
frame_diff = -frame_diff;
validate_transitions(otc, kTransitionOpening, frame_diff);
validate_transitions(ctc, kTransitionClosing, frame_diff);
frame_diff = -frame_diff;
}
}
}
// if the above validation changed the frame movement, it's unlikely we're still snapped
if (temp_frame_diff != frame_diff) {
olive::timeline::snapped = false;
}
// apply changes to ghosts
for (int i=0;ighosts.size();i++) {
Ghost& g = ParentTimeline()->ghosts[i];
if (effective_tool == olive::timeline::TIMELINE_TOOL_SLIP) {
g.clip_in = g.old_clip_in - frame_diff;
} else if (g.trim_type != olive::timeline::TRIM_NONE) {
long ghost_diff = frame_diff;
// prevent trimming clips from overlapping each other
for (int j=0;jghosts.size();j++) {
const Ghost& comp = ParentTimeline()->ghosts.at(j);
if (i != j && g.track == comp.track) {
long validator;
if (g.trim_type == olive::timeline::TRIM_IN && comp.out < g.out) {
validator = (g.old_in + ghost_diff) - comp.out;
if (validator < 0) ghost_diff -= validator;
} else if (comp.in > g.in) {
validator = (g.old_out + ghost_diff) - comp.in;
if (validator > 0) ghost_diff -= validator;
}
}
}
// apply changes
if (g.transition != nullptr && g.transition->secondary_clip != nullptr) {
if (g.trim_type == olive::timeline::TRIM_IN) ghost_diff = -ghost_diff;
g.in = g.old_in - ghost_diff;
g.out = g.old_out + ghost_diff;
} else if (g.trim_type == olive::timeline::TRIM_IN) {
g.in = g.old_in + ghost_diff;
g.clip_in = g.old_clip_in + ghost_diff;
} else {
g.out = g.old_out + ghost_diff;
}
} else if (clips_are_movable) {
g.in = g.old_in + frame_diff;
g.out = g.old_out + frame_diff;
if (g.transition != nullptr
&& g.transition == g.clip->opening_transition) {
g.clip_in = g.old_clip_in + frame_diff;
}
if (ParentTimeline()->importing) {
g.track_movement = getTrackIndexFromScreenPoint(mouse_pos.y());
} else if (g.track->type() == track_list_->type() && g.transition == nullptr) {
g.track_movement = track_diff;
}
} else if (effective_tool == olive::timeline::TIMELINE_TOOL_TRANSITION) {
if (ParentTimeline()->transition_tool_open_clip != nullptr
&& ParentTimeline()->transition_tool_close_clip != nullptr) {
g.in = g.old_in - frame_diff;
g.out = g.old_out + frame_diff;
} else if (ParentTimeline()->transition_tool_open_clip == g.clip) {
g.out = g.old_out + frame_diff;
} else {
g.in = g.old_in + frame_diff;
}
}
earliest_in_point = qMin(earliest_in_point, g.in);
}
// apply changes to selections
/*
if (effective_tool != olive::timeline::TIMELINE_TOOL_SLIP && !ParentTimeline()->importing && !ParentTimeline()->creating) {
for (int i=0;iselections.size();i++) {
Selection& s = sequence()->selections[i];
if (ParentTimeline()->trim_target > -1) {
if (ParentTimeline()->trim_type == olive::timeline::TRIM_IN) {
s.in = s.old_in + frame_diff;
} else {
s.out = s.old_out + frame_diff;
}
} else if (clips_are_movable) {
for (int i=0;iselections.size();i++) {
Selection& s = sequence()->selections[i];
s.in = s.old_in + frame_diff;
s.out = s.old_out + frame_diff;
s.track = s.old_track;
if (ParentTimeline()->importing) {
int abs_track_diff = abs(track_diff);
if (s.old_track < 0) {
s.track -= abs_track_diff;
} else {
s.track += abs_track_diff;
}
} else {
if (same_sign(s.track, ParentTimeline()->drag_track_start)) s.track += track_diff;
}
}
}
}
}
*/
if (ParentTimeline()->importing) {
QToolTip::showText(mapToGlobal(mouse_pos), frame_to_timecode(earliest_in_point, olive::config.timecode_view, sequence()->frame_rate));
} else {
QString tip = ((frame_diff < 0) ? "-" : "+") + frame_to_timecode(qAbs(frame_diff), olive::config.timecode_view, sequence()->frame_rate);
if (ParentTimeline()->trim_target != nullptr) {
// find which clip is being moved
const Ghost* g = nullptr;
for (int i=0;ighosts.size();i++) {
if (ParentTimeline()->ghosts.at(i).clip == ParentTimeline()->trim_target) {
g = &ParentTimeline()->ghosts.at(i);
break;
}
}
if (g != nullptr) {
tip += " " + tr("Duration:") + " ";
long len = (g->old_out-g->old_in);
if (ParentTimeline()->trim_type == olive::timeline::TRIM_IN) {
len -= frame_diff;
} else {
len += frame_diff;
}
tip += frame_to_timecode(len, olive::config.timecode_view, sequence()->frame_rate);
}
}
QToolTip::showText(mapToGlobal(mouse_pos), tip);
}
}
void TimelineView::mouseMoveEvent(QMouseEvent *event) {
// interrupt any potential tooltip about to show
tooltip_timer.stop();
if (sequence() != nullptr) {
bool alt = (event->modifiers() & Qt::AltModifier);
// store current frame/track corresponding to the cursor
ParentTimeline()->cursor_frame = ParentTimeline()->getTimelineFrameFromScreenPoint(event->pos().x());
ParentTimeline()->cursor_track = getTrackFromScreenPoint(event->pos().y());
// if holding the mouse button down, let's scroll to that location
if (event->buttons() != 0 && olive::timeline::current_tool != olive::timeline::TIMELINE_TOOL_HAND) {
ParentTimeline()->scroll_to_frame(ParentTimeline()->cursor_frame);
}
// determine if the action should be "inserting" rather than "overwriting"
// Default behavior is to replace/overwrite clips under any clips we're dropping over them. Inserting will
// split and move existing clips at the drop point to make space for the drop
ParentTimeline()->move_insert = ((event->modifiers() & Qt::ControlModifier)
&& (olive::timeline::current_tool == olive::timeline::TIMELINE_TOOL_POINTER
|| ParentTimeline()->importing
|| ParentTimeline()->creating));
// if we're not currently resizing already, default track resizing to false (we'll set it to true later if
// the user is still hovering over a track line)
if (!ParentTimeline()->moving_init) {
track_resizing = false;
}
// if the current tool uses an on-screen visible cursor, we snap the cursor to the timeline
if (current_tool_shows_cursor()) {
sequence()->SnapPoint(&ParentTimeline()->cursor_frame,
ParentTimeline()->zoom,
// only snap to the playhead if the edit tool doesn't force the playhead to
// follow it (or if we're not selecting since that means the playhead is
// static at the moment)
!olive::config.edit_tool_also_seeks || !ParentTimeline()->selecting,
true,
true);
}
if (ParentTimeline()->selecting) {
QVector selections = ParentTimeline()->selection_cache;
if (ParentTimeline()->drag_track_start != nullptr || ParentTimeline()->cursor_track != nullptr) {
long selection_in = qMin(ParentTimeline()->drag_frame_start, ParentTimeline()->cursor_frame);
long selection_out = qMax(ParentTimeline()->drag_frame_start, ParentTimeline()->cursor_frame);
int selection_top = mapToGlobal(QPoint(0, ParentTimeline()->drag_y_start)).y();
int selection_bottom = mapToGlobal(event->pos()).y();
QVector