/***
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 .
***/
#include "timelinewidget.h"
#include
#include
#include
#include
#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/serializer/serializer.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
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 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 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(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(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(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