Files
oak-editor/app/widget/timelinewidget/timelinewidget.cpp
T
Mike-Solar 0969e2adc5 Add diagnostics for proxy toggle synchronization
Add temporary qDebug logging to Footage, ProjectCopier,
RenderWorkerPool, and TimelineWidget to trace why Use Proxy cannot be
 toggled and why proxy footage may still be used after disabling.
2026-07-13 10:19:30 +08:00

2851 lines
76 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 <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
namespace {
struct SourceSyncClip {
ClipBlock *clip = nullptr;
AudioSynchronizer::SourceClip source;
rational source_head;
};
struct WaveformSyncClip {
ClipBlock *clip = nullptr;
const AudioWaveformCache *waveform = nullptr;
TimeRange media_range;
int sample_rate = 0;
};
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;
}
bool GetWaveformSyncClip(Block *block, WaveformSyncClip *out)
{
ClipBlock *clip = dynamic_cast<ClipBlock *>(block);
if (!clip || !clip->waveform()) {
return false;
}
const TimeRange media_range = clip->media_range();
if (media_range.length().isNull()) {
return false;
}
const AudioWaveformCache *waveform = clip->waveform();
if (waveform->GetParameters().sample_rate() <= 0 ||
!waveform->GetInvalidatedRanges(media_range).isEmpty()) {
return false;
}
out->clip = clip;
out->waveform = waveform;
out->media_range = media_range;
out->sample_rate = waveform->GetParameters().sample_rate();
return true;
}
QVector<WaveformSyncClip> GetSelectedWaveformSyncClips(
const QVector<Block *> &blocks)
{
QVector<WaveformSyncClip> clips;
for (Block *block : blocks) {
WaveformSyncClip sync_clip;
if (GetWaveformSyncClip(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;
}
QVector<double> ExtractWaveformCacheEnvelope(const WaveformSyncClip &clip,
int sample_rate,
size_t window_samples)
{
QVector<double> envelope;
if (sample_rate <= 0 || !window_samples) {
return envelope;
}
const rational window_time(static_cast<int>(window_samples), sample_rate);
for (rational t = clip.media_range.in(); t < clip.media_range.out();
t += window_time) {
const rational length = qMin(window_time, clip.media_range.out() - t);
const AudioVisualWaveform::Sample summary =
clip.waveform->GetSummaryFromTime(t, length);
double peak = 0.0;
for (const AudioVisualWaveform::SamplePerChannel &channel : summary) {
const double channel_peak =
std::max(std::abs(static_cast<double>(channel.min)),
std::abs(static_cast<double>(channel.max)));
peak = std::max(peak, channel_peak);
}
envelope.append(peak);
}
return envelope;
}
} // 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(selected_blocks_);
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(selected_blocks_);
if (sync_clips.size() < 2) {
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);
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);
if (!offset.valid) {
continue;
}
const AudioSynchronizer::Placement placement =
AudioSynchronizer::PlaceByWaveformOffset(
reference.clip->in(), offset.offset_samples, sample_rate);
if (placement.valid && placement.timeline_in >= 0) {
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 Waveform"));
}
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);
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_);
}
}