Files
oak-editor/app/widget/timelinewidget/timelinewidget.cpp
T
Mike-Solar b4b52abbd4 Remove noisy graph-snapshot logs and add waveform-sync diagnostics
- Strip the dense RenderWorkerPool debug messages about cached/stale graph
  snapshots and cleanup to reduce log spam.
- Add qDebug logging and status-bar feedback to
  TimelineWidget::SynchronizeSelectedClipsByWaveform() so we can see why the
  sync action does nothing (e.g. not enough cached clips or no usable offset).
2026-07-13 18:05:02 +08:00

2818 lines
75 KiB
C++

/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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 <http://www.gnu.org/licenses/>.
***/
#include "timelinewidget.h"
#include "timelinewidgetwaveformsync.h"
#include <algorithm>
#include <cfloat>
#include <cmath>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QSplitter>
#include <QUrl>
#include <QVBoxLayout>
#include <QtMath>
#include "audio/audiosynchronizer.h"
#include "audio/audiowaveformsync.h"
#include "codec/proxymanager.h"
#include "core.h"
#include "common/range.h"
#include "dialog/sequence/sequence.h"
#include "dialog/speedduration/speeddurationdialog.h"
#include "node/block/transition/transition.h"
#include "node/nodeundo.h"
#include "node/project/footage/footage.h"
#include "node/project/serializer/serializer.h"
#include "render/audiowaveformcache.h"
#include "task/project/import/import.h"
#include "timeline/timelineundogeneral.h"
#include "timeline/timelineundopointer.h"
#include "timeline/timelineundoripple.h"
#include "timeline/timelineundoworkarea.h"
#include "tool/add.h"
#include "tool/beam.h"
#include "tool/edit.h"
#include "tool/pointer.h"
#include "tool/razor.h"
#include "tool/record.h"
#include "tool/ripple.h"
#include "tool/rolling.h"
#include "tool/slide.h"
#include "tool/slip.h"
#include "tool/trackselect.h"
#include "tool/transition.h"
#include "tool/zoom.h"
#include "tool/tool.h"
#include "trackview/trackview.h"
#include "widget/menu/menu.h"
#include "widget/menu/menushared.h"
#include "widget/nodeparamview/nodeparamview.h"
#include "widget/timeruler/timeruler.h"
namespace olive
{
#define super TimeBasedWidget
using namespace TimelineWaveformSync;
namespace
{
struct SourceSyncClip {
ClipBlock *clip = nullptr;
AudioSynchronizer::SourceClip source;
rational source_head;
};
bool GetSourceSyncClip(Block *block, SourceSyncClip *out)
{
ClipBlock *clip = dynamic_cast<ClipBlock *>(block);
if (!clip) {
return false;
}
Footage *footage = dynamic_cast<Footage *>(clip->connected_viewer());
if (!footage || !footage->HasSourceStartTime()) {
return false;
}
out->clip = clip;
out->source.source_start_time = footage->source_start_time();
out->source.media_in = clip->media_in();
out->source.has_source_start_time = true;
out->source_head = out->source.source_start_time + out->source.media_in;
return true;
}
QVector<SourceSyncClip>
GetSelectedSourceSyncClips(const QVector<Block *> &blocks)
{
QVector<SourceSyncClip> clips;
for (Block *block : blocks) {
SourceSyncClip sync_clip;
if (GetSourceSyncClip(block, &sync_clip)) {
clips.append(sync_clip);
}
}
return clips;
}
QVector<Footage *> GetSelectedProxyFootage(const QVector<Block *> &blocks)
{
QVector<Footage *> footage;
for (Block *block : blocks) {
ClipBlock *clip = dynamic_cast<ClipBlock *>(block);
if (!clip) {
continue;
}
Footage *candidate = dynamic_cast<Footage *>(clip->connected_viewer());
if (!candidate || !candidate->GetFirstEnabledVideoStream().is_valid() ||
footage.contains(candidate)) {
continue;
}
footage.append(candidate);
}
return footage;
}
} // namespace
TimelineWidget::TimelineWidget(QWidget *parent)
: super(true, true, parent)
, rubberband_(QRubberBand::Rectangle, this)
, active_tool_(nullptr)
, use_audio_time_units_(false)
, subtitle_show_command_(nullptr)
, subtitle_tentative_track_(nullptr)
{
QVBoxLayout *vert_layout = new QVBoxLayout(this);
vert_layout->setSpacing(0);
vert_layout->setContentsMargins(0, 0, 0, 0);
QHBoxLayout *ruler_and_time_layout = new QHBoxLayout();
vert_layout->addLayout(ruler_and_time_layout);
timecode_label_ = new RationalSlider();
timecode_label_->SetAlignment(Qt::AlignCenter);
timecode_label_->SetDisplayType(RationalSlider::kTime);
timecode_label_->setVisible(false);
timecode_label_->SetMinimum(0);
ruler_and_time_layout->addWidget(timecode_label_);
ruler_and_time_layout->addWidget(ruler());
ruler()->setFocusPolicy(Qt::TabFocus);
QWidget::setTabOrder(ruler(), timecode_label_);
// Create list of TimelineViews - these MUST correspond to the ViewType enum
view_splitter_ = new QSplitter(Qt::Vertical);
vert_layout->addWidget(view_splitter_);
// Video view
views_.append(AddTimelineAndTrackView(Qt::AlignBottom));
// Audio view
views_.append(AddTimelineAndTrackView(Qt::AlignTop));
// Subtitle view
views_.append(AddTimelineAndTrackView(Qt::AlignTop));
// Create tools
tools_.resize(olive::Tool::kCount);
tools_.fill(nullptr);
tools_.replace(olive::Tool::kPointer, new PointerTool(this));
tools_.replace(olive::Tool::kTrackSelect, new TrackSelectTool(this));
tools_.replace(olive::Tool::kEdit, new EditTool(this));
tools_.replace(olive::Tool::kRipple, new RippleTool(this));
tools_.replace(olive::Tool::kRolling, new RollingTool(this));
tools_.replace(olive::Tool::kRazor, new RazorTool(this));
tools_.replace(olive::Tool::kSlip, new SlipTool(this));
tools_.replace(olive::Tool::kSlide, new SlideTool(this));
tools_.replace(olive::Tool::kZoom, new ZoomTool(this));
tools_.replace(olive::Tool::kTransition, new TransitionTool(this));
tools_.replace(olive::Tool::kRecord, new RecordTool(this));
tools_.replace(olive::Tool::kAdd, new AddTool(this));
import_tool_ = new ImportTool(this);
// We add this to the list to make deleting all tools easier, it should never get accessed through the list under
// normal circumstances (but *technically* its index would be Tool::kCount)
tools_.append(import_tool_);
// Global scrollbar
connect(views_.first()->view()->horizontalScrollBar(),
&QScrollBar::rangeChanged, scrollbar(), &QScrollBar::setRange);
vert_layout->addWidget(scrollbar());
foreach (TimelineAndTrackView *tview, views_) {
TimelineView *view = tview->view();
view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
view->SetSnapService(this);
view->SetSelectionList(&selections_);
view->SetGhostList(&ghost_items_);
view_splitter_->addWidget(tview);
ConnectTimelineView(view);
connect(view, &TimelineView::customContextMenuRequested, this,
&TimelineWidget::ShowContextMenu);
connect(view, &TimelineView::MousePressed, this,
&TimelineWidget::ViewMousePressed);
connect(view, &TimelineView::MouseMoved, this,
&TimelineWidget::ViewMouseMoved);
connect(view, &TimelineView::MouseReleased, this,
&TimelineWidget::ViewMouseReleased);
connect(view, &TimelineView::MouseDoubleClicked, this,
&TimelineWidget::ViewMouseDoubleClicked);
connect(view, &TimelineView::DragEntered, this,
&TimelineWidget::ViewDragEntered);
connect(view, &TimelineView::DragMoved, this,
&TimelineWidget::ViewDragMoved);
connect(view, &TimelineView::DragLeft, this,
&TimelineWidget::ViewDragLeft);
connect(view, &TimelineView::DragDropped, this,
&TimelineWidget::ViewDragDropped);
connect(tview->splitter(), &QSplitter::splitterMoved, this,
&TimelineWidget::UpdateHorizontalSplitters);
}
// Split viewer 50/50
QList<int> view_sizes;
view_sizes.reserve(views_.size());
view_sizes.append(height() / 2); // Video
view_sizes.append(height() / 2); // Audio
view_sizes.append(0); // Subtitle (hidden by default)
view_splitter_->setSizes(view_sizes);
// Video and audio are not collapsible, subtitle is
view_splitter_->setCollapsible(Track::kVideo, false);
view_splitter_->setCollapsible(Track::kAudio, false);
view_splitter_->setCollapsible(Track::kSubtitle, true);
// FIXME: Magic number
SetScale(90.0);
SetAutoSetTimebase(false);
connect(Core::instance(), &Core::ToolChanged, this,
&TimelineWidget::ToolChanged);
connect(Core::instance(), &Core::AddableObjectChanged, this,
&TimelineWidget::AddableObjectChanged);
signal_block_change_timer_ = new QTimer(this);
signal_block_change_timer_->setInterval(1);
signal_block_change_timer_->setSingleShot(true);
connect(signal_block_change_timer_, &QTimer::timeout, this, [this] {
signal_block_change_timer_->stop();
if (OLIVE_CONFIG("SelectAlsoSeeks").toBool()) {
rational start = RATIONAL_MAX;
for (Block *b : selected_blocks_) {
start = std::min(start, b->in());
}
if (start != RATIONAL_MAX) {
GetConnectedNode()->SetPlayhead(start);
}
}
emit BlockSelectionChanged(selected_blocks_);
});
}
TimelineWidget::~TimelineWidget()
{
// Ensure no blocks are selected before any child widgets are destroyed (prevents corrupt ViewSelectionChanged() signal)
ConnectViewerNode(nullptr);
Clear();
qDeleteAll(tools_);
delete subtitle_show_command_;
}
void TimelineWidget::Clear()
{
// Emit that we've deselected any selected blocks
SignalDeselectedAllBlocks();
// Set null timebase
SetTimebase(0);
}
void TimelineWidget::TimebaseChangedEvent(const rational &timebase)
{
super::TimebaseChangedEvent(timebase);
timecode_label_->SetTimebase(timebase);
timecode_label_->setVisible(!timebase.isNull());
UpdateViewTimebases();
}
void TimelineWidget::resizeEvent(QResizeEvent *event)
{
super::resizeEvent(event);
// Update timecode label size
UpdateTimecodeWidthFromSplitters(views_.first()->splitter());
}
void TimelineWidget::TimeChangedEvent(const rational &t)
{
if (OLIVE_CONFIG("SeekAlsoSelects").toBool()) {
TimelineWidgetSelections sels;
QVector<Block *> new_blocks;
for (auto it = sequence()->GetTracks().cbegin();
it != sequence()->GetTracks().cend(); it++) {
Track *track = *it;
if (track->IsLocked()) {
continue;
}
Block *b = track->VisibleBlockAtTime(sequence()->GetPlayhead());
if (!b || dynamic_cast<GapBlock *>(b)) {
continue;
}
new_blocks.push_back(b);
sels[track->ToReference()].insert(b->range());
}
if (selected_blocks_ != new_blocks) {
selected_blocks_ = new_blocks;
SetSelections(sels, false);
SignalBlockSelectionChange();
}
}
}
void TimelineWidget::ScaleChangedEvent(const double &scale)
{
super::ScaleChangedEvent(scale);
foreach (TimelineAndTrackView *view, views_) {
view->view()->SetScale(scale);
}
if (rubberband_.isVisible()) {
QMetaObject::invokeMethod(this, &TimelineWidget::ForceUpdateRubberBand,
Qt::QueuedConnection);
}
}
void TimelineWidget::ConnectNodeEvent(ViewerOutput *n)
{
Sequence *s = static_cast<Sequence *>(n);
connect(s, &Sequence::TrackAdded, this, &TimelineWidget::AddTrack);
connect(s, &Sequence::TrackRemoved, this, &TimelineWidget::RemoveTrack);
connect(s, &Sequence::FrameRateChanged, this,
&TimelineWidget::FrameRateChanged);
connect(s, &Sequence::SampleRateChanged, this,
&TimelineWidget::SampleRateChanged);
connect(timecode_label_, &RationalSlider::ValueChanged, s,
&Sequence::SetPlayhead);
connect(s, &Sequence::PlayheadChanged, timecode_label_,
&RationalSlider::SetValue);
timecode_label_->SetValue(s->GetPlayhead());
ruler()->SetPlaybackCache(n->video_frame_cache());
SetTimebase(n->GetVideoParams().frame_rate_as_time_base());
for (int i = 0; i < views_.size(); i++) {
Track::Type track_type = static_cast<Track::Type>(i);
TimelineView *view = views_.at(i)->view();
TrackList *track_list = s->track_list(track_type);
TrackView *track_view = views_.at(i)->track_view();
track_view->ConnectTrackList(track_list);
view->ConnectTrackList(track_list);
// Defer to the track to make all the block UI items necessary
const QVector<Track *> tracks = s->track_list(track_type)->GetTracks();
foreach (Track *track, tracks) {
AddTrack(track);
}
}
}
void TimelineWidget::DisconnectNodeEvent(ViewerOutput *n)
{
Sequence *s = static_cast<Sequence *>(n);
disconnect(s, &Sequence::TrackAdded, this, &TimelineWidget::AddTrack);
disconnect(s, &Sequence::TrackRemoved, this, &TimelineWidget::RemoveTrack);
disconnect(s, &Sequence::FrameRateChanged, this,
&TimelineWidget::FrameRateChanged);
disconnect(s, &Sequence::SampleRateChanged, this,
&TimelineWidget::SampleRateChanged);
disconnect(timecode_label_, &RationalSlider::ValueChanged, s,
&Sequence::SetPlayhead);
DeselectAll();
foreach (Track *track, s->GetTracks()) {
RemoveTrack(track);
}
ruler()->SetPlaybackCache(nullptr);
SetTimebase(0);
Clear();
foreach (TimelineAndTrackView *tview, views_) {
tview->track_view()->DisconnectTrackList();
tview->view()->ConnectTrackList(nullptr);
}
}
void TimelineWidget::SendCatchUpScrollEvent()
{
super::SendCatchUpScrollEvent();
if (rubberband_.isVisible()) {
this->ForceUpdateRubberBand();
}
}
void TimelineWidget::SelectAll()
{
QVector<Block *> newly_selected_blocks;
foreach (Block *block, added_blocks_) {
if (!selected_blocks_.contains(block)) {
newly_selected_blocks.append(block);
AddSelection(block);
}
}
SignalSelectedBlocks(newly_selected_blocks, false);
}
void TimelineWidget::DeselectAll()
{
// Clear selections
selections_.clear();
// Update all viewports
UpdateViewports();
// Clear list and emit signal
SignalDeselectedAllBlocks();
}
void TimelineWidget::RippleToIn()
{
RippleTo(Timeline::kTrimIn);
}
void TimelineWidget::RippleToOut()
{
RippleTo(Timeline::kTrimOut);
}
void TimelineWidget::EditToIn()
{
EditTo(Timeline::kTrimIn);
}
void TimelineWidget::EditToOut()
{
EditTo(Timeline::kTrimOut);
}
void TimelineWidget::SplitAtPlayhead()
{
if (!GetConnectedNode()) {
return;
}
const rational &playhead_time = GetConnectedNode()->GetPlayhead();
QVector<Block *> selected_blocks = GetSelectedBlocks();
// Prioritize blocks that are selected and overlap the playhead
QVector<Block *> blocks_to_split;
QVector<bool> block_is_selected;
bool some_blocks_are_selected = false;
// Get all blocks at the playhead
foreach (Track *track, sequence()->GetTracks()) {
if (track->IsLocked()) {
continue;
}
Block *b = track->BlockContainingTime(playhead_time);
if (dynamic_cast<ClipBlock *>(b)) {
bool selected = false;
// See if this block is selected
foreach (Block *item, selected_blocks) {
if (item == b) {
some_blocks_are_selected = true;
selected = true;
break;
}
}
blocks_to_split.append(b);
block_is_selected.append(selected);
}
}
// If some blocks are selected, we prioritize those and don't split the blocks that aren't
if (some_blocks_are_selected) {
for (int i = 0; i < block_is_selected.size(); i++) {
if (!block_is_selected.at(i)) {
blocks_to_split.removeAt(i);
block_is_selected.removeAt(i);
i--;
}
}
}
if (!blocks_to_split.isEmpty()) {
Core::instance()->undo_stack()->push(
new BlockSplitPreservingLinksCommand(blocks_to_split,
{ playhead_time }),
tr("Split Clips At Playhead"));
}
}
void TimelineWidget::ReplaceBlocksWithGaps(const QVector<Block *> &blocks,
bool remove_from_graph,
MultiUndoCommand *command,
bool handle_transitions)
{
foreach (Block *b, blocks) {
if (dynamic_cast<GapBlock *>(b)) {
// No point in replacing a gap with a gap, and TrackReplaceBlockWithGapCommand will clear
// up any extraneous gaps
continue;
}
Track *original_track = b->track();
command->add_child(new TrackReplaceBlockWithGapCommand(
original_track, b, handle_transitions));
if (remove_from_graph) {
command->add_child(
new NodeRemoveWithExclusiveDependenciesAndDisconnect(b));
}
}
}
void TimelineWidget::DeleteSelected(bool ripple)
{
if (ruler()->HasItemsSelected()) {
ruler()->DeleteSelected();
return;
}
QVector<Block *> selected_list = GetSelectedBlocks();
// No-op if nothing is selected
if (selected_list.isEmpty()) {
return;
}
QVector<Block *> clips_to_delete;
QVector<TransitionBlock *> transitions_to_delete;
bool all_gaps = true;
foreach (Block *b, selected_list) {
if (!dynamic_cast<GapBlock *>(b)) {
all_gaps = false;
}
if (dynamic_cast<ClipBlock *>(b)) {
clips_to_delete.append(b);
} else if (dynamic_cast<TransitionBlock *>(b)) {
transitions_to_delete.append(static_cast<TransitionBlock *>(b));
}
}
if (all_gaps) {
ripple = true;
}
MultiUndoCommand *command = new MultiUndoCommand();
// Remove all selections
command->add_child(new SetSelectionsCommand(
this, TimelineWidgetSelections(), GetSelections()));
// For transitions, remove them but extend their attached blocks to fill their place
foreach (TransitionBlock *transition, transitions_to_delete) {
TransitionRemoveCommand *trc =
new TransitionRemoveCommand(transition, true);
// Perform the transition removal now so that replacing blocks with gaps below won't get confused
trc->redo_now();
command->add_child(trc);
}
// Replace clips with gaps (effectively deleting them)
ReplaceBlocksWithGaps(clips_to_delete, true, command, false);
// Insert ripple command now that it's all cleaned up gaps
TimelineRippleDeleteGapsAtRegionsCommand *ripple_command = nullptr;
rational new_playhead = RATIONAL_MAX;
if (ripple) {
TimelineRippleDeleteGapsAtRegionsCommand::RangeList range_list;
foreach (Block *b, selected_list) {
range_list.append({ b->track(), b->range() });
new_playhead = qMin(new_playhead, b->in());
}
ripple_command = new TimelineRippleDeleteGapsAtRegionsCommand(
sequence(), range_list);
command->add_child(ripple_command);
}
Core::instance()->undo_stack()->push(command, tr("Deleted Clips"));
// Ensures any current drag operations are cancelled
ClearGhosts();
if (ripple_command && ripple_command->HasCommands() &&
new_playhead != RATIONAL_MAX) {
GetConnectedNode()->SetPlayhead(new_playhead);
}
}
void TimelineWidget::IncreaseTrackHeight()
{
if (!GetConnectedNode()) {
return;
}
// Increase the height of each track by one "unit"
foreach (Track *t, sequence()->GetTracks()) {
t->SetTrackHeight(t->GetTrackHeight() + Track::kTrackHeightInterval);
}
}
void TimelineWidget::DecreaseTrackHeight()
{
if (!GetConnectedNode()) {
return;
}
// Decrease the height of each track by one "unit"
foreach (Track *t, sequence()->GetTracks()) {
t->SetTrackHeight(
qMax(t->GetTrackHeight() - Track::kTrackHeightInterval,
Track::kTrackHeightMinimum));
}
}
void TimelineWidget::InsertFootageAtPlayhead(
const QVector<ViewerOutput *> &footage)
{
auto command = new MultiUndoCommand();
import_tool_->PlaceAt(footage, GetConnectedNode()->GetPlayhead(), true,
command, 0, true);
Core::instance()->undo_stack()->push(command,
tr("Inserted Footage At Playhead"));
}
void TimelineWidget::OverwriteFootageAtPlayhead(
const QVector<ViewerOutput *> &footage)
{
auto command = new MultiUndoCommand();
import_tool_->PlaceAt(footage, GetConnectedNode()->GetPlayhead(), false,
command, 0, true);
Core::instance()->undo_stack()->push(command,
tr("Overwrote Footage At Playhead"));
}
void TimelineWidget::ToggleLinksOnSelected()
{
QVector<Node *> blocks;
bool link = true;
foreach (Block *item, GetSelectedBlocks()) {
// Only clips can be linked
if (!dynamic_cast<ClipBlock *>(item)) {
continue;
}
// Prioritize unlinking, if any block has links, assume we're unlinking
if (link && item->HasLinks()) {
link = false;
}
blocks.append(item);
}
if (blocks.isEmpty()) {
return;
}
Core::instance()->undo_stack()->push(new NodeLinkManyCommand(blocks, link),
tr("Linked Clips"));
}
void TimelineWidget::AddDefaultTransitionsToSelected()
{
QVector<ClipBlock *> blocks;
foreach (Block *item, GetSelectedBlocks()) {
// Only clips can be linked
if (ClipBlock *clip = dynamic_cast<ClipBlock *>(item)) {
blocks.append(clip);
}
}
if (!blocks.isEmpty()) {
Core::instance()->undo_stack()->push(
new TimelineAddDefaultTransitionCommand(blocks, timebase()),
tr("Added Default Transitions"));
}
}
bool TimelineWidget::CopySelected(bool cut)
{
if (super::CopySelected(cut)) {
return true;
}
if (!GetConnectedNode() || selected_blocks_.isEmpty()) {
return false;
}
QVector<Node *> selected_nodes;
foreach (Block *block, selected_blocks_) {
selected_nodes.append(block);
QVector<Node *> deps = block->GetDependencies();
foreach (Node *d, deps) {
if (!selected_nodes.contains(d)) {
selected_nodes.append(d);
}
}
}
ProjectSerializer::SaveData sdata(ProjectSerializer::kOnlyClips);
sdata.SetOnlySerializeNodesAndResolveGroups(selected_nodes);
// Cache the earliest in point so all copied clips have a "relative" in point that can be pasted anywhere
rational earliest_in = RATIONAL_MAX;
ProjectSerializer::SerializedProperties properties;
foreach (Block *block, selected_blocks_) {
earliest_in = qMin(earliest_in, block->in());
}
foreach (Block *block, selected_blocks_) {
properties[block][QStringLiteral("in")] =
QString::fromStdString((block->in() - earliest_in).toString());
properties[block][QStringLiteral("track")] =
block->track()->ToReference().ToString();
}
sdata.SetProperties(properties);
ProjectSerializer::Copy(sdata);
if (cut) {
DeleteSelected();
}
return true;
}
bool TimelineWidget::Paste()
{
// TimeRuler gets first chance (markers, etc.)
if (super::Paste()) {
return true;
}
// Ensure we have a connected node
if (!GetConnectedNode()) {
return false;
}
// Attempt regular clip pasting
if (PasteInternal(false)) {
return true;
}
// Give last chance to NodeParamView
return NodeParamView::Paste(
this, std::bind(&TimelineWidget::GenerateExistingPasteMap, this,
std::placeholders::_1));
}
void TimelineWidget::PasteInsert()
{
PasteInternal(true);
}
void TimelineWidget::DeleteInToOut(bool ripple)
{
if (!GetConnectedNode() || !GetConnectedNode()->GetWorkArea()->enabled()) {
return;
}
MultiUndoCommand *command = new MultiUndoCommand();
if (ripple) {
command->add_child(new TimelineRippleRemoveAreaCommand(
sequence(), GetConnectedNode()->GetWorkArea()->in(),
GetConnectedNode()->GetWorkArea()->out()));
} else {
QVector<Track *> unlocked_tracks = sequence()->GetUnlockedTracks();
foreach (Track *track, unlocked_tracks) {
GapBlock *gap = new GapBlock();
gap->set_length_and_media_out(
GetConnectedNode()->GetWorkArea()->length());
command->add_child(new NodeAddCommand(
static_cast<Project *>(track->parent()), gap));
command->add_child(new TrackPlaceBlockCommand(
sequence()->track_list(track->type()), track->Index(), gap,
GetConnectedNode()->GetWorkArea()->in()));
}
}
// Clear workarea after this
command->add_child(new WorkareaSetEnabledCommand(
GetConnectedNode()->project(), GetConnectedNode()->GetWorkArea(),
false));
if (ripple) {
GetConnectedNode()->SetPlayhead(
GetConnectedNode()->GetWorkArea()->in());
}
Core::instance()->undo_stack()->push(command, tr("Deleted In To Out"));
}
void TimelineWidget::ToggleSelectedEnabled()
{
QVector<Block *> items = GetSelectedBlocks();
if (items.isEmpty()) {
return;
}
MultiUndoCommand *command = new MultiUndoCommand();
foreach (Block *i, items) {
command->add_child(new BlockEnableDisableCommand(i, !i->is_enabled()));
}
Core::instance()->undo_stack()->push(command, tr("Toggled Clips Enabled"));
}
void TimelineWidget::SetColorLabel(int index)
{
MultiUndoCommand *command = new MultiUndoCommand();
foreach (Block *b, selected_blocks_) {
command->add_child(new NodeOverrideColorCommand(b, index));
}
Core::instance()->undo_stack()->push(
command, tr("Set Colors of %1 Clips").arg(selected_blocks_.size()));
}
void TimelineWidget::NudgeLeft()
{
if (GetConnectedNode()) {
NudgeInternal(-timebase());
}
}
void TimelineWidget::NudgeRight()
{
if (GetConnectedNode()) {
NudgeInternal(timebase());
}
}
void TimelineWidget::MoveInToPlayhead()
{
MoveToPlayheadInternal(false);
}
void TimelineWidget::MoveOutToPlayhead()
{
MoveToPlayheadInternal(true);
}
void TimelineWidget::ShowSpeedDurationDialogForSelectedClips()
{
QVector<ClipBlock *> clips;
foreach (Block *b, selected_blocks_) {
ClipBlock *c = dynamic_cast<ClipBlock *>(b);
if (c) {
clips.append(c);
}
}
if (!clips.isEmpty()) {
SpeedDurationDialog sdd(clips, timebase(), this);
sdd.exec();
}
}
void TimelineWidget::SynchronizeSelectedClipsBySourceTime()
{
if (!GetConnectedNode()) {
return;
}
const QVector<SourceSyncClip> sync_clips =
GetSelectedSourceSyncClips(GetSelectedBlocks());
if (sync_clips.size() < 2) {
return;
}
SourceSyncClip reference = sync_clips.first();
rational anchor_timeline_in = sync_clips.first().clip->in();
for (const SourceSyncClip &sync_clip : sync_clips) {
if (sync_clip.source_head < reference.source_head) {
reference = sync_clip;
}
if (sync_clip.clip->in() < anchor_timeline_in) {
anchor_timeline_in = sync_clip.clip->in();
}
}
struct SyncPlacement {
ClipBlock *clip = nullptr;
rational timeline_in;
};
QVector<SyncPlacement> placements;
for (const SourceSyncClip &sync_clip : sync_clips) {
const AudioSynchronizer::Placement placement =
AudioSynchronizer::PlaceBySourceTime(
reference.source, sync_clip.source, anchor_timeline_in);
if (placement.valid) {
placements.append({ sync_clip.clip, placement.timeline_in });
}
}
if (placements.size() < 2) {
return;
}
MultiUndoCommand *command = new MultiUndoCommand();
for (const SyncPlacement &placement : placements) {
command->add_child(new TrackReplaceBlockWithGapCommand(
placement.clip->track(), placement.clip, false));
}
TimelineWidgetSelections new_selections;
for (const SyncPlacement &placement : placements) {
command->add_child(new TrackPlaceBlockCommand(
sequence()->track_list(placement.clip->track()->type()),
placement.clip->track()->Index(), placement.clip,
placement.timeline_in));
new_selections[placement.clip->track()->ToReference()].insert(
TimeRange(placement.timeline_in,
placement.timeline_in + placement.clip->length()));
}
command->add_child(
new SetSelectionsCommand(this, new_selections, GetSelections()));
Core::instance()->undo_stack()->push(
command, tr("Synchronize Clips by Source Time"));
}
void TimelineWidget::SynchronizeSelectedClipsByWaveform()
{
if (!GetConnectedNode()) {
return;
}
const QVector<WaveformSyncClip> sync_clips =
GetSelectedWaveformSyncClips(GetSelectedBlocks());
qDebug() << "TimelineWidget::SynchronizeSelectedClipsByWaveform:"
<< sync_clips.size() << "sync clip(s) selected";
if (sync_clips.size() < 2) {
Core::instance()->ShowStatusBarMessage(
tr("Select at least 2 clips with cached waveforms to sync by waveform"));
return;
}
WaveformSyncClip reference = sync_clips.first();
for (const WaveformSyncClip &sync_clip : sync_clips) {
if (sync_clip.clip->in() < reference.clip->in()) {
reference = sync_clip;
}
}
const int sample_rate = reference.sample_rate;
const size_t window_samples =
static_cast<size_t>(std::max(1, sample_rate / 20));
const int64_t max_offset_samples =
static_cast<int64_t>(sample_rate) * 10 * 60;
const int64_t max_offset_windows =
max_offset_samples / static_cast<int64_t>(window_samples);
const QVector<double> reference_envelope =
ExtractWaveformCacheEnvelope(reference, sample_rate, window_samples);
qDebug() << "TimelineWidget::SynchronizeSelectedClipsByWaveform: sample_rate="
<< sample_rate << "window_samples=" << window_samples
<< "max_offset_windows=" << max_offset_windows
<< "reference_envelope_size=" << reference_envelope.size();
struct SyncPlacement {
ClipBlock *clip = nullptr;
rational timeline_in;
};
QVector<SyncPlacement> placements;
placements.append({ reference.clip, reference.clip->in() });
for (const WaveformSyncClip &sync_clip : sync_clips) {
if (sync_clip.clip == reference.clip) {
continue;
}
const QVector<double> candidate_envelope = ExtractWaveformCacheEnvelope(
sync_clip, sample_rate, window_samples);
const AudioWaveformSync::OffsetResult offset =
AudioWaveformSync::EstimateEnvelopeOffset(reference_envelope,
candidate_envelope,
window_samples,
max_offset_windows);
qDebug() << "TimelineWidget::SynchronizeSelectedClipsByWaveform: candidate"
<< sync_clip.clip << "envelope_size="
<< candidate_envelope.size() << "offset_valid="
<< offset.valid << "offset_samples=" << offset.offset_samples
<< "confidence=" << offset.confidence;
if (!offset.valid) {
continue;
}
const AudioSynchronizer::Placement placement =
AudioSynchronizer::PlaceByWaveformOffset(
reference.clip->in(), offset.offset_samples, sample_rate);
qDebug() << "TimelineWidget::SynchronizeSelectedClipsByWaveform: placement"
<< "valid=" << placement.valid << "timeline_in="
<< placement.timeline_in.toDouble();
if (placement.valid && placement.timeline_in >= 0) {
placements.append({ sync_clip.clip, placement.timeline_in });
}
}
if (placements.size() < 2) {
qDebug() << "TimelineWidget::SynchronizeSelectedClipsByWaveform: no usable"
<< "offsets found";
Core::instance()->ShowStatusBarMessage(
tr("Could not find a usable waveform offset for the selected clips"));
return;
}
MultiUndoCommand *command = new MultiUndoCommand();
for (const SyncPlacement &placement : placements) {
command->add_child(new TrackReplaceBlockWithGapCommand(
placement.clip->track(), placement.clip, false));
}
TimelineWidgetSelections new_selections;
for (const SyncPlacement &placement : placements) {
command->add_child(new TrackPlaceBlockCommand(
sequence()->track_list(placement.clip->track()->type()),
placement.clip->track()->Index(), placement.clip,
placement.timeline_in));
new_selections[placement.clip->track()->ToReference()].insert(
TimeRange(placement.timeline_in,
placement.timeline_in + placement.clip->length()));
}
command->add_child(
new SetSelectionsCommand(this, new_selections, GetSelections()));
Core::instance()->undo_stack()->push(command,
tr("Synchronize Clips by Waveform"));
Core::instance()->ShowStatusBarMessage(
tr("Synchronized %1 clip(s) by waveform").arg(placements.size()));
}
void TimelineWidget::GenerateProxiesForSelectedClips()
{
if (!ProxyManager::instance() || !sequence()) {
qWarning()
<< "GenerateProxiesForSelectedClips: ProxyManager or sequence unavailable";
return;
}
const QVector<Footage *> footage =
GetSelectedProxyFootage(selected_blocks_);
qDebug() << "GenerateProxiesForSelectedClips: starting proxy generation for"
<< footage.size() << "footage item(s)";
for (Footage *item : footage) {
const VideoParams video = item->GetFirstEnabledVideoStream();
if (!video.is_valid()) {
qWarning()
<< "GenerateProxiesForSelectedClips: skipping item with no valid video stream"
<< item->filename();
continue;
}
ProxyManager::ProxyParams params;
params.width = OLIVE_CONFIG("ProxyWidth").value<int>();
params.height = OLIVE_CONFIG("ProxyHeight").value<int>();
params.crf = OLIVE_CONFIG("ProxyCRF").value<int>();
params.preset = OLIVE_CONFIG("ProxyPreset").toString();
const ProxyManager::Proxy proxy =
ProxyManager::instance()->GetOrStartProxy(
item->project()->cache_path(), item->filename(),
video.stream_index(), params);
qDebug() << "GenerateProxiesForSelectedClips: proxy state="
<< ProxyManager::ProxyStateToString(proxy.state)
<< "file=" << proxy.filename
<< "cache=" << item->project()->cache_path();
item->SetProxy(proxy.filename, proxy.state, video.stream_index(),
params.version, true);
item->InvalidateAll(Footage::kFilenameInput);
}
}
void TimelineWidget::SetSelectedClipsProxyEnabled(bool enabled)
{
const QVector<Footage *> footage =
GetSelectedProxyFootage(selected_blocks_);
qDebug() << "TimelineWidget::SetSelectedClipsProxyEnabled:" << enabled
<< "footage count=" << footage.size();
for (Footage *item : footage) {
if (item->proxy_path().isEmpty()) {
qDebug()
<< " skipping item with empty proxy path" << item->filename();
continue;
}
item->set_proxy_enabled(enabled);
item->InvalidateAll(Footage::kFilenameInput);
}
}
void TimelineWidget::RevealProxyForSelectedClips()
{
const QVector<Footage *> footage =
GetSelectedProxyFootage(selected_blocks_);
for (Footage *item : footage) {
if (item->proxy_path().isEmpty()) {
continue;
}
#if defined(Q_OS_WINDOWS)
QStringList args;
args << "/select," << QDir::toNativeSeparators(item->proxy_path());
QProcess::startDetached(QStringLiteral("explorer"), args);
#elif defined(Q_OS_MAC)
QStringList args;
args << "-e";
args << "tell application \"Finder\"";
args << "-e";
args << "activate";
args << "-e";
args << "select POSIX file \"" + item->proxy_path() + "\"";
args << "-e";
args << "end tell";
QProcess::startDetached(QStringLiteral("osascript"), args);
#else
QDesktopServices::openUrl(QUrl::fromLocalFile(
QFileInfo(item->proxy_path()).dir().absolutePath()));
#endif
return;
}
}
void TimelineWidget::DeleteProxiesForSelectedClips()
{
const QVector<Footage *> footage =
GetSelectedProxyFootage(selected_blocks_);
for (Footage *item : footage) {
if (item->proxy_path().isEmpty()) {
continue;
}
QFile::remove(item->proxy_path());
QFile::remove(
ProxyManager::GetWorkingProxyFilename(item->proxy_path()));
item->ClearProxy();
item->InvalidateAll(Footage::kFilenameInput);
}
}
void TimelineWidget::RecordingCallback(const QString &filename,
const TimeRange &time,
const Track::Reference &track)
{
ProjectImportTask task(GetConnectedNode()->project()->root(), { filename });
task.Start();
auto subimport_command = task.GetCommand();
if (task.GetImportedFootage().empty()) {
qCritical() << "Failed to import recorded audio file" << filename;
delete subimport_command;
} else {
subimport_command->redo_now();
auto import_command = new MultiUndoCommand();
import_command->add_child(subimport_command);
import_tool_->PlaceAt({ task.GetImportedFootage().front() }, time.in(),
false, import_command, track.index());
Core::instance()->undo_stack()->push(import_command,
tr("Recorded Audio Clip"));
}
}
void TimelineWidget::EnableRecordingOverlay(const TimelineCoordinate &coord)
{
foreach (TimelineAndTrackView *tview, views_) {
tview->view()->EnableRecordingOverlay(coord);
}
}
void TimelineWidget::DisableRecordingOverlay()
{
foreach (TimelineAndTrackView *tview, views_) {
tview->view()->DisableRecordingOverlay();
}
}
void TimelineWidget::AddTentativeSubtitleTrack()
{
if (!subtitle_show_command_) {
// Determine if we need to do anything
QList<int> sz = view_splitter_->sizes();
bool should_adjust_splitter = (sz[Track::kSubtitle] == 0);
bool should_add_sub_track =
(sequence() &&
sequence()->track_list(Track::kSubtitle)->GetTrackCount() == 0);
if (should_adjust_splitter || should_add_sub_track) {
// Create command
subtitle_show_command_ = new MultiUndoCommand();
if (should_adjust_splitter) {
sz[Track::kSubtitle] = height() / Track::kCount;
subtitle_show_command_->add_child(
new SetSplitterSizesCommand(view_splitter_, sz));
}
if (should_add_sub_track) {
TimelineAddTrackCommand *track_add_cmd =
new TimelineAddTrackCommand(
sequence()->track_list(Track::kSubtitle));
subtitle_tentative_track_ = track_add_cmd->track();
subtitle_show_command_->add_child(track_add_cmd);
}
subtitle_show_command_->redo_now();
}
}
}
void TimelineWidget::NestSelectedClips()
{
if (!GetConnectedNode()) {
return;
}
QVector<Block *> blocks = this->selected_blocks_;
if (blocks.empty()) {
return;
}
QVector<Track::Reference> tracks(blocks.size());
QVector<TimeRange> times(blocks.size());
QVector<int> track_offset(Track::kCount, INT_MAX);
rational start_time = RATIONAL_MAX;
rational end_time = RATIONAL_MIN;
for (int i = 0; i < blocks.size(); i++) {
Block *b = blocks.at(i);
Track::Reference tf = b->track()->ToReference();
;
tracks[i] = tf;
times[i] = b->range();
int &to = track_offset[tf.type()];
to = std::min(to, tf.index());
start_time = std::min(start_time, b->in());
end_time = std::max(end_time, b->out());
}
auto move_to_nest_command = new MultiUndoCommand();
// Remove blocks from this sequence
ReplaceBlocksWithGaps(blocks, false, move_to_nest_command);
// Create new sequence
Project *project = this->GetConnectedNode()->project();
Sequence *nest =
Core::CreateNewSequenceForProject(tr("Nested Sequence %1"), project);
nest->SetVideoParams(GetConnectedNode()->GetVideoParams());
nest->SetAudioParams(GetConnectedNode()->GetAudioParams());
move_to_nest_command->add_child(new NodeAddCommand(project, nest));
// Add to same folder
move_to_nest_command->add_child(
new FolderAddChild(this->GetConnectedNode()->folder(), nest));
// Place blocks in new sequence
for (int i = 0; i < blocks.size(); i++) {
Block *b = blocks.at(i);
const TimeRange &range = times.at(i);
Track::Reference track = tracks.at(i);
move_to_nest_command->add_child(new TrackPlaceBlockCommand(
nest->track_list(track.type()),
track.index() - track_offset.at(track.type()), b,
range.in() - start_time));
}
// Do this command now, because we later do checks and actions that rely on these having been done
move_to_nest_command->redo_now();
auto meta_command = new MultiUndoCommand();
meta_command->add_child(move_to_nest_command);
// Find first free track index
bool empty = false;
int index = -1;
while (!empty) {
index++;
empty = true;
for (int i = 0; i < Track::kCount; i++) {
if (track_offset.at(i) == INT_MAX) {
// No clips on this track
continue;
}
TrackList *list =
sequence()->track_list(static_cast<Track::Type>(i));
if (index < list->GetTrackCount() &&
!list->GetTrackAt(index)->IsRangeFree(
TimeRange(start_time, end_time))) {
empty = false;
break;
}
}
}
// Place new sequence in this sequence
import_tool_->PlaceAt({ nest }, start_time, false, meta_command, index);
Core::instance()->undo_stack()->push(meta_command, tr("Nested Clips"));
}
void TimelineWidget::ClearTentativeSubtitleTrack()
{
if (subtitle_show_command_) {
subtitle_show_command_->undo_now();
delete subtitle_show_command_;
subtitle_show_command_ = nullptr;
subtitle_tentative_track_ = nullptr;
}
}
void TimelineWidget::InsertGapsAt(const rational &earliest_point,
const rational &insert_length,
MultiUndoCommand *command)
{
for (int i = 0; i < Track::kCount; i++) {
command->add_child(new TrackListInsertGaps(
sequence()->track_list(static_cast<Track::Type>(i)), earliest_point,
insert_length));
}
}
Track *TimelineWidget::GetTrackFromReference(const Track::Reference &ref) const
{
return sequence()->track_list(ref.type())->GetTrackAt(ref.index());
}
int TimelineWidget::GetTrackY(const Track::Reference &ref)
{
return views_.at(ref.type())->view()->GetTrackY(ref.index());
}
int TimelineWidget::GetTrackHeight(const Track::Reference &ref)
{
return views_.at(ref.type())->view()->GetTrackHeight(ref.index());
}
void TimelineWidget::CenterOn(qreal scene_pos)
{
scrollbar()->setValue(qRound(scene_pos - scrollbar()->width() / 2));
}
void TimelineWidget::ClearGhosts()
{
if (!ghost_items_.isEmpty()) {
foreach (TimelineViewGhostItem *ghost, ghost_items_) {
delete ghost;
}
ghost_items_.clear();
}
HideSnaps();
}
TimelineTool *TimelineWidget::GetActiveTool()
{
return tools_.at(Core::instance()->tool());
}
void TimelineWidget::ViewMousePressed(TimelineViewMouseEvent *event)
{
active_tool_ = GetActiveTool();
if (GetConnectedNode() && active_tool_ != nullptr) {
active_tool_->MousePress(event);
UpdateViewports();
}
if (event->GetButton() != Qt::LeftButton) {
// Suspend tool immediately if the cursor isn't the primary button
active_tool_->MouseRelease(event);
UpdateViewports();
active_tool_ = nullptr;
}
}
void TimelineWidget::ViewMouseMoved(TimelineViewMouseEvent *event)
{
if (GetConnectedNode()) {
if (active_tool_) {
active_tool_->MouseMove(event);
UpdateViewports();
SetCatchUpScrollValue(event->GetScreenPos().x());
} else {
// Mouse is not down, attempt a hover event
TimelineTool *hover_tool = GetActiveTool();
if (hover_tool) {
hover_tool->HoverMove(event);
}
}
}
}
void TimelineWidget::ViewMouseReleased(TimelineViewMouseEvent *event)
{
StopCatchUpScrollTimer();
if (active_tool_) {
if (GetConnectedNode()) {
active_tool_->MouseRelease(event);
UpdateViewports();
}
active_tool_ = nullptr;
}
}
void TimelineWidget::ViewMouseDoubleClicked(TimelineViewMouseEvent *event)
{
// kHand tool will return nullptr
if (!GetActiveTool()) {
// Only kHand should return a nullptr
Q_ASSERT(Core::instance()->tool() == olive::Tool::kHand);
return;
}
if (GetConnectedNode()) {
GetActiveTool()->MouseDoubleClick(event);
UpdateViewports();
}
}
void TimelineWidget::ViewDragEntered(TimelineViewMouseEvent *event)
{
import_tool_->DragEnter(event);
UpdateViewports();
}
void TimelineWidget::ViewDragMoved(TimelineViewMouseEvent *event)
{
import_tool_->DragMove(event);
UpdateViewports();
SetCatchUpScrollValue(event->GetScreenPos().x());
}
void TimelineWidget::ViewDragLeft(QDragLeaveEvent *event)
{
StopCatchUpScrollTimer();
import_tool_->DragLeave(event);
UpdateViewports();
}
void TimelineWidget::ViewDragDropped(TimelineViewMouseEvent *event)
{
StopCatchUpScrollTimer();
import_tool_->DragDrop(event);
UpdateViewports();
}
void TimelineWidget::AddBlock(Block *block)
{
// Set up clip with view parameters (clip item will automatically size its rect accordingly)
if (!added_blocks_.contains(block)) {
connect(block, &Block::LinksChanged, this,
&TimelineWidget::BlockUpdated);
connect(block, &Block::LabelChanged, this,
&TimelineWidget::BlockUpdated);
connect(block, &Block::ColorChanged, this,
&TimelineWidget::BlockUpdated);
connect(block, &Block::EnabledChanged, this,
&TimelineWidget::BlockUpdated);
connect(block, &Block::PreviewChanged, this,
&TimelineWidget::BlockUpdated);
added_blocks_.append(block);
if (selections_[block->track()->ToReference()].contains(
block->range()) &&
!selected_blocks_.contains(block)) {
selected_blocks_.append(block);
}
}
}
void TimelineWidget::RemoveBlock(Block *block)
{
// Disconnect all signals
disconnect(block, &Block::LinksChanged, this,
&TimelineWidget::BlockUpdated);
disconnect(block, &Block::LabelChanged, this,
&TimelineWidget::BlockUpdated);
disconnect(block, &Block::ColorChanged, this,
&TimelineWidget::BlockUpdated);
disconnect(block, &Block::EnabledChanged, this,
&TimelineWidget::BlockUpdated);
disconnect(block, &Block::PreviewChanged, this,
&TimelineWidget::BlockUpdated);
// Take item from map
added_blocks_.removeOne(block);
// If selected, deselect it
int select_index = selected_blocks_.indexOf(block);
if (select_index > -1) {
selected_blocks_.removeAt(select_index);
RemoveSelection(block);
SignalBlockSelectionChange();
}
}
void TimelineWidget::AddTrack(Track *track)
{
foreach (Block *b, track->Blocks()) {
AddBlock(b);
}
connect(track, &Track::IndexChanged, this, &TimelineWidget::TrackUpdated);
connect(track, &Track::IndexChanged, this,
&TimelineWidget::TrackIndexChanged);
connect(track, &Track::BlocksRefreshed, this,
&TimelineWidget::TrackUpdated);
connect(track, &Track::TrackHeightChanged, this,
&TimelineWidget::TrackUpdated);
connect(track, &Track::BlockAdded, this, &TimelineWidget::AddBlock);
connect(track, &Track::BlockRemoved, this, &TimelineWidget::RemoveBlock);
}
void TimelineWidget::RemoveTrack(Track *track)
{
disconnect(track, &Track::IndexChanged, this,
&TimelineWidget::TrackUpdated);
disconnect(track, &Track::IndexChanged, this,
&TimelineWidget::TrackIndexChanged);
disconnect(track, &Track::BlocksRefreshed, this,
&TimelineWidget::TrackUpdated);
disconnect(track, &Track::TrackHeightChanged, this,
&TimelineWidget::TrackUpdated);
disconnect(track, &Track::BlockAdded, this, &TimelineWidget::AddBlock);
disconnect(track, &Track::BlockRemoved, this, &TimelineWidget::RemoveBlock);
RemoveSelection(TimeRange(0, RATIONAL_MAX), track->ToReference());
foreach (Block *b, track->Blocks()) {
RemoveBlock(b);
}
}
void TimelineWidget::TrackUpdated()
{
UpdateViewports(static_cast<Track *>(sender())->type());
}
void TimelineWidget::BlockUpdated()
{
UpdateViewports(static_cast<Block *>(sender())->track()->type());
}
void TimelineWidget::UpdateHorizontalSplitters()
{
QSplitter *sender_splitter = static_cast<QSplitter *>(sender());
foreach (TimelineAndTrackView *tview, views_) {
QSplitter *recv_splitter = tview->splitter();
if (recv_splitter != sender_splitter) {
recv_splitter->blockSignals(true);
recv_splitter->setSizes(sender_splitter->sizes());
recv_splitter->blockSignals(false);
}
}
UpdateTimecodeWidthFromSplitters(sender_splitter);
}
void TimelineWidget::UpdateTimecodeWidthFromSplitters(QSplitter *s)
{
timecode_label_->setFixedWidth(s->sizes().first() + s->handleWidth());
}
void TimelineWidget::ShowContextMenu()
{
Menu menu(this);
QVector<Block *> selected = GetSelectedBlocks();
if (!selected.isEmpty()) {
MenuShared::instance()->AddItemsForEditMenu(&menu, true);
menu.addSeparator();
MenuShared::instance()->AddColorCodingMenu(&menu);
menu.addSeparator();
QAction *sync_by_source_time =
menu.addAction(tr("Synchronize by Source Time"));
sync_by_source_time->setEnabled(
GetSelectedSourceSyncClips(selected).size() >= 2);
connect(sync_by_source_time, &QAction::triggered, this,
&TimelineWidget::SynchronizeSelectedClipsBySourceTime);
QAction *sync_by_waveform =
menu.addAction(tr("Synchronize by Waveform"));
sync_by_waveform->setEnabled(
GetSelectedWaveformSyncClips(selected).size() >= 2);
sync_by_waveform->setShortcut(
QKeySequence(QStringLiteral("Ctrl+Shift+W")));
sync_by_waveform->setShortcutContext(Qt::WidgetShortcut);
this->addAction(sync_by_waveform);
connect(sync_by_waveform, &QAction::triggered, this,
&TimelineWidget::SynchronizeSelectedClipsByWaveform);
menu.addSeparator();
if (ClipBlock *clip = dynamic_cast<ClipBlock *>(selected.first())) {
{
Menu *cache_menu = new Menu(tr("Cache"), &menu);
menu.addMenu(cache_menu);
QAction *autocache_action =
cache_menu->addAction(tr("Auto-Cache"));
autocache_action->setCheckable(true);
autocache_action->setChecked(clip->IsAutocaching());
connect(autocache_action, &QAction::triggered, this,
&TimelineWidget::SetSelectedClipsAutocaching);
cache_menu->addSeparator();
auto cache_clip = cache_menu->addAction(tr("Cache All"));
connect(cache_clip, &QAction::triggered, this,
&TimelineWidget::CacheClips);
auto cache_inout = cache_menu->addAction(tr("Cache In/Out"));
connect(cache_inout, &QAction::triggered, this,
&TimelineWidget::CacheClipsInOut);
auto cache_discard = cache_menu->addAction(tr("Discard"));
connect(cache_discard, &QAction::triggered, this,
&TimelineWidget::CacheDiscard);
}
{
const QVector<Footage *> proxy_footage =
GetSelectedProxyFootage(selected);
Menu *proxy_menu = new Menu(tr("Proxy"), &menu);
menu.addMenu(proxy_menu);
QAction *generate_proxy =
proxy_menu->addAction(tr("Generate Proxy"));
generate_proxy->setEnabled(!proxy_footage.isEmpty());
connect(generate_proxy, &QAction::triggered, this,
&TimelineWidget::GenerateProxiesForSelectedClips);
QAction *use_proxy = proxy_menu->addAction(tr("Use Proxy"));
use_proxy->setCheckable(true);
use_proxy->setEnabled(!proxy_footage.isEmpty());
use_proxy->setChecked(
!proxy_footage.isEmpty() &&
std::all_of(proxy_footage.cbegin(), proxy_footage.cend(),
[](const Footage *footage) {
return footage->proxy_enabled();
}));
connect(use_proxy, &QAction::triggered, this,
&TimelineWidget::SetSelectedClipsProxyEnabled);
QAction *reveal_proxy =
proxy_menu->addAction(tr("Reveal Proxy"));
reveal_proxy->setEnabled(
std::any_of(proxy_footage.cbegin(), proxy_footage.cend(),
[](const Footage *footage) {
return !footage->proxy_path().isEmpty();
}));
connect(reveal_proxy, &QAction::triggered, this,
&TimelineWidget::RevealProxyForSelectedClips);
QAction *delete_proxy =
proxy_menu->addAction(tr("Delete Proxy"));
delete_proxy->setEnabled(
std::any_of(proxy_footage.cbegin(), proxy_footage.cend(),
[](const Footage *footage) {
return !footage->proxy_path().isEmpty();
}));
connect(delete_proxy, &QAction::triggered, this,
&TimelineWidget::DeleteProxiesForSelectedClips);
}
if (clip->connected_viewer()) {
QAction *reveal_in_footage_viewer =
menu.addAction(tr("Reveal in Footage Viewer"));
reveal_in_footage_viewer->setData(
reinterpret_cast<quintptr>(clip->connected_viewer()));
reveal_in_footage_viewer->setProperty(
"range", QVariant::fromValue(clip->media_range()));
connect(reveal_in_footage_viewer, &QAction::triggered, this,
&TimelineWidget::RevealInFootageViewer);
QAction *reveal_in_project =
menu.addAction(tr("Reveal in Project"));
reveal_in_project->setData(
reinterpret_cast<quintptr>(clip->connected_viewer()));
connect(reveal_in_project, &QAction::triggered, this,
&TimelineWidget::RevealInProject);
if (Sequence *sequence =
dynamic_cast<Sequence *>(clip->connected_viewer())) {
QAction *multicam_enabled = menu.addAction(tr("Multi-Cam"));
multicam_enabled->setCheckable(true);
MultiCamNode *mcn = nullptr;
auto paths = clip->FindWaysNodeArrivesHere(sequence);
for (const NodeInput &i : paths) {
if ((mcn = dynamic_cast<MultiCamNode *>(i.node()))) {
break;
}
}
multicam_enabled->setChecked(mcn);
connect(multicam_enabled, &QAction::triggered, this,
&TimelineWidget::MulticamEnabledTriggered);
}
}
}
menu.addSeparator();
QAction *properties_action = menu.addAction(tr("Properties"));
connect(properties_action, &QAction::triggered, this,
&TimelineWidget::ShowSpeedDurationDialogForSelectedClips);
}
if (selected.isEmpty()) {
QAction *toggle_audio_units =
menu.addAction(tr("Use Audio Time Units"));
toggle_audio_units->setCheckable(true);
toggle_audio_units->setChecked(use_audio_time_units_);
connect(toggle_audio_units, &QAction::triggered, this,
&TimelineWidget::SetUseAudioTimeUnits);
{
Menu *thumbnail_menu = new Menu(tr("Show Thumbnails"), &menu);
menu.addMenu(thumbnail_menu);
thumbnail_menu->AddActionWithData(
tr("Disabled"), Timeline::kThumbnailOff,
OLIVE_CONFIG("TimelineThumbnailMode"));
thumbnail_menu->AddActionWithData(
tr("Only At In Points"), Timeline::kThumbnailInOut,
OLIVE_CONFIG("TimelineThumbnailMode"));
thumbnail_menu->AddActionWithData(
tr("Enabled"), Timeline::kThumbnailOn,
OLIVE_CONFIG("TimelineThumbnailMode"));
connect(thumbnail_menu, &Menu::triggered, this,
&TimelineWidget::SetViewThumbnailsEnabled);
}
QAction *show_waveforms = menu.addAction(tr("Show Waveforms"));
show_waveforms->setCheckable(true);
show_waveforms->setChecked(
OLIVE_CONFIG("TimelineWaveformMode").toInt() ==
Timeline::kWaveformsEnabled);
connect(show_waveforms, &QAction::triggered, this,
&TimelineWidget::SetViewWaveformsEnabled);
menu.addSeparator();
QAction *properties_action = menu.addAction(tr("Properties"));
connect(properties_action, &QAction::triggered, this,
&TimelineWidget::ShowSequenceDialog);
}
menu.exec(QCursor::pos());
}
void TimelineWidget::DeferredScrollAction()
{
scrollbar()->setValue(deferred_scroll_value_);
}
void TimelineWidget::ShowSequenceDialog()
{
if (!GetConnectedNode()) {
return;
}
SequenceDialog sd(sequence(), SequenceDialog::kExisting, this);
sd.exec();
}
void TimelineWidget::SetUseAudioTimeUnits(bool use)
{
use_audio_time_units_ = use;
// Update timebases
UpdateViewTimebases();
}
void TimelineWidget::ToolChanged()
{
HideSnaps();
SetViewBeamCursor(TimelineCoordinate(0, Track::kNone, -1));
SetViewTransitionOverlay(nullptr, nullptr);
AddableObjectChanged();
}
void TimelineWidget::AddableObjectChanged()
{
// Special cast for subtitle adding - ensure section is visible
if (Core::instance()->tool() == Tool::kAdd &&
Core::instance()->GetSelectedAddableObject() ==
Tool::kAddableSubtitle) {
AddTentativeSubtitleTrack();
} else {
ClearTentativeSubtitleTrack();
}
}
void TimelineWidget::SetViewWaveformsEnabled(bool e)
{
OLIVE_CONFIG("TimelineWaveformMode") = e ? Timeline::kWaveformsEnabled :
Timeline::kWaveformsDisabled;
UpdateViewports();
}
void TimelineWidget::SetViewThumbnailsEnabled(QAction *action)
{
OLIVE_CONFIG("TimelineThumbnailMode") = action->data();
UpdateViewports();
}
void TimelineWidget::FrameRateChanged()
{
SetTimebase(GetConnectedNode()->GetVideoParams().frame_rate_as_time_base());
}
void TimelineWidget::SampleRateChanged()
{
UpdateViewTimebases();
}
void TimelineWidget::TrackIndexChanged(int old, int now)
{
Track *track = static_cast<Track *>(sender());
Track::Reference old_ref(track->type(), old);
Track::Reference new_ref(track->type(), now);
auto track_selections = selections_.take(old_ref);
if (!track_selections.isEmpty()) {
selections_.insert(new_ref, track_selections);
}
}
void TimelineWidget::SignalBlockSelectionChange()
{
signal_block_change_timer_->stop();
signal_block_change_timer_->start();
}
void TimelineWidget::RevealInFootageViewer()
{
QAction *a = static_cast<QAction *>(sender());
ViewerOutput *item_to_reveal =
reinterpret_cast<ViewerOutput *>(a->data().value<quintptr>());
TimeRange r = a->property("range").value<TimeRange>();
emit RevealViewerInFootageViewer(item_to_reveal, r);
}
void TimelineWidget::RevealInProject()
{
QAction *a = static_cast<QAction *>(sender());
ViewerOutput *item_to_reveal =
reinterpret_cast<ViewerOutput *>(a->data().value<quintptr>());
emit RevealViewerInProject(item_to_reveal);
}
void TimelineWidget::RenameSelectedBlocks()
{
MultiUndoCommand *command = new MultiUndoCommand();
QVector<Node *> nodes(selected_blocks_.size());
for (int i = 0; i < nodes.size(); i++) {
nodes[i] = selected_blocks_[i];
}
Core::instance()->LabelNodes(nodes);
Core::instance()->undo_stack()->push(
command, tr("Renamed %1 Clip(s)").arg(nodes.size()));
}
void TimelineWidget::TrackAboutToBeDeleted(Track *track)
{
if (track == subtitle_tentative_track_) {
// User is deleting the tentative subtitle track. Technically they shouldn't do this, but they
// might if they misinterpret it as permanent. If so, we handle it cleanly by pushing our
// command as if the action really were permanent.
Core::instance()->undo_stack()->push(TakeSubtitleSectionCommand(),
tr("Created Subtitle Track"));
}
}
void TimelineWidget::SetSelectedClipsAutocaching(bool e)
{
MultiUndoCommand *command = new MultiUndoCommand();
for (Block *b : selected_blocks_) {
if (ClipBlock *clip = dynamic_cast<ClipBlock *>(b)) {
command->add_child(new NodeParamSetStandardValueCommand(
NodeKeyframeTrackReference(
NodeInput(clip, ClipBlock::kAutoCacheInput)),
e));
}
}
Core::instance()->undo_stack()->push(
command, e ? tr("Enabled Auto-Caching On %1 Clip(s)")
.arg(selected_blocks_.size()) :
tr("Disabled Auto-Caching On %1 Clip(s)")
.arg(selected_blocks_.size()));
}
void TimelineWidget::CacheClips()
{
for (Block *b : selected_blocks_) {
if (ClipBlock *clip = dynamic_cast<ClipBlock *>(b)) {
clip->RequestInvalidatedFromConnected(true);
}
}
}
void TimelineWidget::CacheClipsInOut()
{
if (!this->sequence() || !this->sequence()->GetWorkArea()->enabled()) {
return;
}
TimeTargetObject tto;
tto.SetTimeTarget(this->sequence());
const TimeRange &r = this->sequence()->GetWorkArea()->range();
for (Block *b : qAsConst(selected_blocks_)) {
if (ClipBlock *clip = dynamic_cast<ClipBlock *>(b)) {
if (Node *connected = clip->GetConnectedOutput(clip->kBufferIn)) {
TimeRange adjusted =
tto.GetAdjustedTime(this->sequence(), connected, r,
Node::kTransformTowardsInput);
clip->RequestInvalidatedFromConnected(true, adjusted);
}
}
}
}
void TimelineWidget::CacheDiscard()
{
if (QMessageBox::question(
this, tr("Discard Cache"),
tr("This will discard all cache for this clip. "
"If the clip has auto-cache enabled, it will be recached immediately. "
"This cannot be undone.\n\n"
"Do you wish to continue?"),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
for (Block *b : selected_blocks_) {
if (ClipBlock *clip = dynamic_cast<ClipBlock *>(b)) {
clip->DiscardCache();
}
}
}
}
void TimelineWidget::MulticamEnabledTriggered(bool e)
{
MultiUndoCommand *command = new MultiUndoCommand();
for (Block *b : qAsConst(selected_blocks_)) {
if (ClipBlock *c = dynamic_cast<ClipBlock *>(b)) {
if (Sequence *s = dynamic_cast<Sequence *>(c->connected_viewer())) {
if (e) {
// Adding multicams
// Create multicam node and add it to the graph
MultiCamNode *n = new MultiCamNode();
n->SetSequenceType(c->GetTrackType());
command->add_child(new NodeAddCommand(s->parent(), n));
// For each output the sequence has to this clip, disconnect it and
// connect to the multicam instead
QVector<NodeInput> inputs = c->FindWaysNodeArrivesHere(s);
for (const NodeInput &i : inputs) {
command->add_child(new NodeEdgeRemoveCommand(s, i));
command->add_child(new NodeEdgeAddCommand(n, i));
}
command->add_child(new NodeEdgeAddCommand(
s, NodeInput(n, n->kSequenceInput)));
// Move sequence node one unit back, and place multicam in sequence's spot
QPointF sequence_pos = c->GetNodePositionInContext(s);
command->add_child(new NodeSetPositionCommand(
s, c, sequence_pos - QPointF(1, 0)));
command->add_child(
new NodeSetPositionCommand(n, c, sequence_pos));
} else {
// Removing multicams
// Locate first multicam that specifically ends up at this clip
QVector<NodeInput> inputs = c->FindWaysNodeArrivesHere(s);
for (const NodeInput &i : inputs) {
if (MultiCamNode *mcn =
dynamic_cast<MultiCamNode *>(i.node())) {
for (auto it = mcn->output_connections().cbegin();
it != mcn->output_connections().cend(); it++) {
command->add_child(new NodeEdgeRemoveCommand(
it->first, it->second));
command->add_child(
new NodeEdgeAddCommand(s, it->second));
}
command->add_child(
new NodeRemoveAndDisconnectCommand(mcn));
}
}
}
}
}
}
Core::instance()->undo_stack()->push(
command,
e ? tr("Multi-Cam Enabled On %1 Clip(s)").arg(selected_blocks_.size()) :
tr("Multi-Cam Disabled On %1 Clip(s)").arg(selected_blocks_.size()));
}
void TimelineWidget::ForceUpdateRubberBand()
{
if (rubberband_.isVisible()) {
this->MoveRubberBandSelect(rubberband_enable_selecting_,
rubberband_select_links_);
}
}
void TimelineWidget::AddGhost(TimelineViewGhostItem *ghost)
{
ghost_items_.append(ghost);
UpdateViewports(ghost->GetTrack().type());
}
void TimelineWidget::UpdateViewTimebases()
{
for (int i = 0; i < views_.size(); i++) {
TimelineAndTrackView *view = views_.at(i);
if (GetConnectedNode() && use_audio_time_units_ && i == Track::kAudio) {
view->view()->SetTimebase(
GetConnectedNode()->GetAudioParams().sample_rate_as_time_base());
} else {
view->view()->SetTimebase(timebase());
}
}
}
void TimelineWidget::NudgeInternal(rational amount)
{
if (!selected_blocks_.isEmpty()) {
// Validate
foreach (Block *b, selected_blocks_) {
if (b->in() + amount < 0) {
amount = -b->in();
}
}
if (amount.isNull()) {
return;
}
MultiUndoCommand *command = new MultiUndoCommand();
foreach (Block *b, selected_blocks_) {
command->add_child(
new TrackReplaceBlockWithGapCommand(b->track(), b, false));
}
foreach (Block *b, selected_blocks_) {
command->add_child(new TrackPlaceBlockCommand(
sequence()->track_list(b->track()->type()), b->track()->Index(),
b, b->in() + amount));
}
// Nudge selections
TimelineWidgetSelections new_sel = GetSelections();
new_sel.ShiftTime(amount);
command->add_child(new TimelineWidget::SetSelectionsCommand(
this, new_sel, GetSelections()));
Core::instance()->undo_stack()->push(command, tr("Nudged Clips"));
}
}
void TimelineWidget::MoveToPlayheadInternal(bool out)
{
if (GetConnectedNode() && !selected_blocks_.isEmpty()) {
MultiUndoCommand *command = new MultiUndoCommand();
// Remove each block from the graph
QHash<Track *, rational> earliest_pts;
foreach (Block *b, selected_blocks_) {
command->add_child(
new TrackReplaceBlockWithGapCommand(b->track(), b, false));
rational r = earliest_pts.value(b->track(),
out ? RATIONAL_MIN : RATIONAL_MAX);
rational compare = out ? b->out() : b->in();
if ((compare < r) == !out) {
earliest_pts.insert(b->track(), compare);
}
}
foreach (Block *b, selected_blocks_) {
rational shift_amt = GetConnectedNode()->GetPlayhead() -
earliest_pts.value(b->track());
rational new_in = b->in() + shift_amt;
bool can_shift = true;
if (new_in < 0) {
// Handle clips threatening to go below 0
rational new_out = new_in + b->length();
if (new_out <= 0) {
can_shift = false;
} else {
command->add_child(
new BlockResizeWithMediaInCommand(b, new_out));
new_in = 0;
}
}
if (can_shift) {
command->add_child(new TrackPlaceBlockCommand(
sequence()->track_list(b->track()->type()),
b->track()->Index(), b, new_in));
}
}
// Shift selections
TimelineWidgetSelections new_sel = GetSelections();
for (auto it = new_sel.begin(); it != new_sel.end(); it++) {
rational track_adj =
GetConnectedNode()->GetPlayhead() -
earliest_pts.value(GetTrackFromReference(it.key()),
GetConnectedNode()->GetPlayhead());
if (!track_adj.isNull()) {
it.value().shift(track_adj);
}
}
command->add_child(
new SetSelectionsCommand(this, new_sel, GetSelections()));
Core::instance()->undo_stack()->push(command,
tr("Moved Clip(s) To Point"));
}
}
void TimelineWidget::SetViewBeamCursor(const TimelineCoordinate &coord)
{
foreach (TimelineAndTrackView *tview, views_) {
tview->view()->SetBeamCursor(coord);
}
}
void TimelineWidget::SetViewTransitionOverlay(ClipBlock *out, ClipBlock *in)
{
foreach (TimelineAndTrackView *tview, views_) {
tview->view()->SetTransitionOverlay(out, in);
}
}
void TimelineWidget::SetBlockLinksSelected(ClipBlock *block, bool selected)
{
foreach (Block *link, block->block_links()) {
if (selected) {
AddSelection(link);
} else {
RemoveSelection(link);
}
}
}
void TimelineWidget::QueueScroll(int value)
{
// (using a hacky singleShot so the scroll occurs after the scene and its scrollbars have updated)
deferred_scroll_value_ = value;
QTimer::singleShot(0, this, &TimelineWidget::DeferredScrollAction);
}
TimelineView *TimelineWidget::GetFirstTimelineView()
{
return views_.first()->view();
}
rational TimelineWidget::GetTimebaseForTrackType(Track::Type type)
{
return views_.at(type)->view()->timebase();
}
const QRect &TimelineWidget::GetRubberBandGeometry() const
{
return rubberband_.geometry();
}
void TimelineWidget::SignalSelectedBlocks(QVector<Block *> input, bool filter)
{
if (input.isEmpty()) {
return;
}
if (filter) {
// If filtering, remove all the blocks that are already selected
for (int i = 0; i < input.size(); i++) {
Block *b = input.at(i);
if (selected_blocks_.contains(b)) {
input.removeAt(i);
i--;
}
}
}
selected_blocks_.append(input);
SignalBlockSelectionChange();
}
void TimelineWidget::SignalDeselectedBlocks(
const QVector<Block *> &deselected_blocks)
{
if (deselected_blocks.isEmpty()) {
return;
}
foreach (Block *b, deselected_blocks) {
selected_blocks_.removeOne(b);
}
SignalBlockSelectionChange();
}
void TimelineWidget::SignalDeselectedAllBlocks()
{
if (!selected_blocks_.isEmpty()) {
selected_blocks_.clear();
SignalBlockSelectionChange();
}
}
QVector<Timeline::EditToInfo>
TimelineWidget::GetEditToInfo(const rational &playhead_time,
Timeline::MovementMode mode)
{
// Get list of unlocked tracks
QVector<Track *> tracks = sequence()->GetUnlockedTracks();
// Create list to cache nearest times and the blocks at this point
QVector<Timeline::EditToInfo> info_list(tracks.size());
for (int i = 0; i < tracks.size(); i++) {
Timeline::EditToInfo &info = info_list[i];
Track *track = tracks.at(i);
info.track = track;
Block *b;
// Determine what block is at this time (for "trim in", we want to catch blocks that start at
// the time, for "trim out" we don't)
if (mode == Timeline::kTrimIn) {
b = track->NearestBlockBeforeOrAt(playhead_time);
} else {
b = track->NearestBlockBefore(playhead_time);
}
// If we have a block here, cache how close it is to the track
if (b) {
rational this_track_closest_point;
if (mode == Timeline::kTrimIn) {
this_track_closest_point = b->in();
} else {
this_track_closest_point = b->out();
}
info.nearest_time = this_track_closest_point;
}
info.nearest_block = b;
}
return info_list;
}
void TimelineWidget::RippleTo(Timeline::MovementMode mode)
{
if (!GetConnectedNode()) {
return;
}
rational playhead_time = GetConnectedNode()->GetPlayhead();
QVector<Timeline::EditToInfo> tracks = GetEditToInfo(playhead_time, mode);
if (tracks.isEmpty()) {
return;
}
// Find each track's nearest point and determine the overall timeline's nearest point
rational closest_point_to_playhead =
(mode == Timeline::kTrimIn) ? RATIONAL_MIN : RATIONAL_MAX;
foreach (const Timeline::EditToInfo &info, tracks) {
if (info.nearest_block) {
if (mode == Timeline::kTrimIn) {
closest_point_to_playhead =
qMax(info.nearest_time, closest_point_to_playhead);
} else {
closest_point_to_playhead =
qMin(info.nearest_time, closest_point_to_playhead);
}
}
}
if (closest_point_to_playhead == RATIONAL_MIN ||
closest_point_to_playhead == RATIONAL_MAX) {
// Assume no blocks will be acted upon
return;
}
// If we're not inserting gaps and the edit point is right on the nearest in point, we enter a
// single-frame mode where we remove one frame only
if (closest_point_to_playhead == playhead_time) {
if (mode == Timeline::kTrimIn) {
playhead_time += timebase();
} else {
playhead_time -= timebase();
}
}
// For standard rippling, we can cache here the region that will be rippled out
rational in_ripple = qMin(closest_point_to_playhead, playhead_time);
rational out_ripple = qMax(closest_point_to_playhead, playhead_time);
TimelineRippleRemoveAreaCommand *c =
new TimelineRippleRemoveAreaCommand(sequence(), in_ripple, out_ripple);
Core::instance()->undo_stack()->push(c, tr("Rippled Clip(s) To Point"));
// If we rippled, ump to where new cut is if applicable
if (mode == Timeline::kTrimIn) {
GetConnectedNode()->SetPlayhead(closest_point_to_playhead);
} else if (mode == Timeline::kTrimOut &&
closest_point_to_playhead == GetConnectedNode()->GetPlayhead()) {
GetConnectedNode()->SetPlayhead(playhead_time);
}
}
void TimelineWidget::EditTo(Timeline::MovementMode mode)
{
const rational playhead_time = GetConnectedNode()->GetPlayhead();
// Get list of unlocked tracks
QVector<Timeline::EditToInfo> tracks = GetEditToInfo(playhead_time, mode);
if (tracks.isEmpty()) {
return;
}
MultiUndoCommand *command = new MultiUndoCommand();
foreach (const Timeline::EditToInfo &info, tracks) {
if (info.nearest_block &&
!dynamic_cast<GapBlock *>(info.nearest_block) &&
info.nearest_time != playhead_time) {
rational new_len;
if (mode == Timeline::kTrimIn) {
new_len = playhead_time - info.nearest_time;
} else {
new_len = info.nearest_time - playhead_time;
}
new_len = info.nearest_block->length() - new_len;
command->add_child(new BlockTrimCommand(
info.track, info.nearest_block, new_len, mode));
}
}
Core::instance()->undo_stack()->push(command, tr("Cut Clip(s) To Point"));
}
void TimelineWidget::UpdateViewports(const Track::Type &type)
{
if (type == Track::kNone) {
foreach (TimelineAndTrackView *tview, views_) {
tview->view()->viewport()->update();
}
} else {
views_.at(type)->view()->viewport()->update();
}
}
bool TimelineWidget::PasteInternal(bool insert)
{
if (!GetConnectedNode()) {
return false;
}
ProjectSerializer::Result res = ProjectSerializer::Paste(
ProjectSerializer::kOnlyClips, GetConnectedNode()->project());
if (res.GetLoadData().nodes.isEmpty()) {
return false;
}
MultiUndoCommand *command = new MultiUndoCommand();
Project *project = GetConnectedNode()->project();
foreach (Node *n, res.GetLoadData().nodes) {
command->add_child(new NodeAddCommand(project, n));
if (n->IsItem() && !n->folder()) {
command->add_child(new FolderAddChild(project->root(), n));
}
}
for (auto it = res.GetLoadData().promised_connections.cbegin();
it != res.GetLoadData().promised_connections.cend(); it++) {
auto oc = *it;
command->add_child(new NodeEdgeAddCommand(oc.first, oc.second));
}
rational paste_start = GetConnectedNode()->GetPlayhead();
if (insert) {
rational paste_end = paste_start;
for (auto it = res.GetLoadData().properties.cbegin();
it != res.GetLoadData().properties.cend(); it++) {
rational length = static_cast<Block *>(it.key())->length();
rational in = rational::fromString(
it.value()[QStringLiteral("in")].toStdString());
paste_end = qMax(paste_end, paste_start + in + length);
}
if (paste_end != paste_start) {
InsertGapsAt(paste_start, paste_end - paste_start, command);
}
}
for (auto it = res.GetLoadData().properties.cbegin();
it != res.GetLoadData().properties.cend(); it++) {
Block *block = static_cast<Block *>(it.key());
rational in = rational::fromString(
it.value()[QStringLiteral("in")].toStdString());
Track::Reference track =
Track::Reference::FromString(it.value()[QStringLiteral("track")]);
command->add_child(
new TrackPlaceBlockCommand(sequence()->track_list(track.type()),
track.index(), block, paste_start + in));
}
Core::instance()->undo_stack()->push(
command,
tr("Pasted %1 Clip(s)").arg(res.GetLoadData().properties.size()));
return true;
}
TimelineAndTrackView *
TimelineWidget::AddTimelineAndTrackView(Qt::Alignment alignment)
{
TimelineAndTrackView *v = new TimelineAndTrackView(alignment);
connect(v->track_view(), &TrackView::AboutToDeleteTrack, this,
&TimelineWidget::TrackAboutToBeDeleted);
return v;
}
QHash<Node *, Node *>
TimelineWidget::GenerateExistingPasteMap(const ProjectSerializer::Result &r)
{
QHash<Node *, Node *> m;
for (Node *n : r.GetLoadData().nodes) {
for (Block *b : qAsConst(this->selected_blocks_)) {
for (auto it = b->GetContextPositions().cbegin();
it != b->GetContextPositions().cend(); it++) {
if (it.key()->id() == n->id() && !m.contains(it.key())) {
m.insert(it.key(), n);
break;
}
}
}
}
return m;
}
QByteArray TimelineWidget::SaveSplitterState() const
{
return view_splitter_->saveState();
}
void TimelineWidget::RestoreSplitterState(const QByteArray &state)
{
view_splitter_->restoreState(state);
}
void TimelineWidget::StartRubberBandSelect(const QPoint &global_cursor_start)
{
// Store scene positions for each view
rubberband_scene_pos_.resize(views_.size());
for (int i = 0; i < rubberband_scene_pos_.size(); i++) {
TimelineView *v = views_.at(i)->view();
rubberband_scene_pos_[i] = v->UnscalePoint(
v->mapToScene(v->mapFromGlobal(global_cursor_start)));
}
rubberband_.show();
// We don't touch any blocks that are already selected. If you want these to be deselected by
// default, call DeselectAll() before calling StartRubberBandSelect()
rubberband_old_selections_ = selections_;
}
void TimelineWidget::MoveRubberBandSelect(bool enable_selecting,
bool select_links)
{
QPoint rubberband_now = QCursor::pos();
TimelineView *fv = views_.first()->view();
const QPointF &rubberband_scene_start = rubberband_scene_pos_.at(0);
QPointF rubberband_now_scaled =
fv->UnscalePoint(fv->mapToScene(fv->mapFromGlobal(rubberband_now)));
QPoint rubberband_local_start = fv->mapTo(
this, fv->mapFromScene(fv->ScalePoint(rubberband_scene_start)));
QPoint rubberband_local_now = fv->mapTo(
this, fv->mapFromScene(fv->ScalePoint(rubberband_now_scaled)));
rubberband_.setGeometry(
QRect(rubberband_local_start, rubberband_local_now).normalized());
rubberband_enable_selecting_ = enable_selecting;
rubberband_select_links_ = select_links;
if (!enable_selecting) {
return;
}
// Get current items in rubberband
QVector<Block *> items_in_rubberband;
for (int i = 0; i < views_.size(); i++) {
TimelineView *v = views_.at(i)->view();
QRectF r = QRectF(v->ScalePoint(rubberband_scene_pos_.at(i)),
v->mapToScene(v->mapFromGlobal(rubberband_now)))
.normalized();
items_in_rubberband.append(v->GetItemsAtSceneRect(r));
}
// Reset selection to whatever it was before
SetSelections(rubberband_old_selections_, false);
// Add any blocks in rubberband
rubberband_now_selected_.clear();
foreach (Block *b, items_in_rubberband) {
if (dynamic_cast<GapBlock *>(b)) {
continue;
}
Track *t = b->track();
if (t->IsLocked()) {
continue;
}
if (!rubberband_now_selected_.contains(b)) {
AddSelection(b);
rubberband_now_selected_.append(b);
}
ClipBlock *c = dynamic_cast<ClipBlock *>(b);
if (c && select_links) {
foreach (Block *link, c->block_links()) {
if (!rubberband_now_selected_.contains(link)) {
AddSelection(link);
rubberband_now_selected_.append(link);
}
}
}
}
}
void TimelineWidget::EndRubberBandSelect()
{
rubberband_.hide();
// Emit any blocks that were newly selected
SignalSelectedBlocks(rubberband_now_selected_);
rubberband_now_selected_.clear();
rubberband_old_selections_.clear();
}
void TimelineWidget::AddSelection(const TimeRange &time,
const Track::Reference &track)
{
selections_[track].insert(time);
UpdateViewports(track.type());
}
void TimelineWidget::AddSelection(Block *item)
{
if (item->track()) {
AddSelection(item->range(), item->track()->ToReference());
}
}
void TimelineWidget::RemoveSelection(const TimeRange &time,
const Track::Reference &track)
{
selections_[track].remove(time);
UpdateViewports(track.type());
}
void TimelineWidget::RemoveSelection(Block *item)
{
if (item->track()) {
RemoveSelection(item->range(), item->track()->ToReference());
}
}
void TimelineWidget::SetSelections(const TimelineWidgetSelections &s,
bool process_block_changes)
{
if (selections_ == s) {
return;
}
if (!GetConnectedNode()) {
return;
}
if (process_block_changes) {
QVector<Block *> deselected;
QVector<Block *> selected;
foreach (Block *b, selected_blocks_) {
if (!s[b->track()->ToReference()].contains(b->range())) {
deselected.append(b);
}
}
// NOTE: This loop could do with some optimization
for (auto it = s.cbegin(); it != s.cend(); it++) {
Track *track = GetTrackFromReference(it.key());
if (track) {
const TimeRangeList &ranges = it.value();
foreach (Block *b, track->Blocks()) {
if (!selected_blocks_.contains(b) &&
ranges.contains(b->range())) {
selected.append(b);
}
}
}
}
SignalDeselectedBlocks(deselected);
SignalSelectedBlocks(selected);
}
selections_ = s;
UpdateViewports();
}
Block *TimelineWidget::GetItemAtScenePos(const TimelineCoordinate &coord)
{
return views_.at(coord.GetTrack().type())
->view()
->GetItemAtScenePos(coord.GetFrame(), coord.GetTrack().index());
}
void TimelineWidget::SetSplitterSizesCommand::redo()
{
old_sizes_ = splitter_->sizes();
splitter_->setSizes(new_sizes_);
}
void TimelineWidget::SetSplitterSizesCommand::undo()
{
splitter_->setSizes(old_sizes_);
}
}