/***
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 "timelinewidgetwaveformsync.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "audio/audiosynchronizer.h"
#include "audio/audiowaveformsync.h"
#include "codec/proxymanager.h"
#include "core.h"
#include "common/range.h"
#include "dialog/proxy/proxydialog.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 timeline_waveform_sync;
namespace
{
struct SourceSyncClip {
ClipBlock *clip = nullptr;
AudioSynchronizer::SourceClip source;
Rational source_head;
};
bool get_source_sync_clip(Block *block, SourceSyncClip *out)
{
ClipBlock *clip = dynamic_cast(block);
if (!clip) {
return false;
}
Footage *footage = dynamic_cast(clip->connected_viewer());
if (!footage || !footage->has_source_start_time()) {
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
get_selected_source_sync_clips(const QVector &blocks)
{
QVector clips;
for (Block *block : blocks) {
SourceSyncClip sync_clip;
if (get_source_sync_clip(block, &sync_clip)) {
clips.append(sync_clip);
}
}
return clips;
}
QVector get_selected_proxy_footage(const QVector &blocks)
{
QVector footage;
for (Block *block : blocks) {
ClipBlock *clip = dynamic_cast(block);
if (!clip) {
continue;
}
Footage *candidate = dynamic_cast(clip->connected_viewer());
if (!candidate || !candidate->get_first_enabled_video_stream().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_->set_alignment(Qt::AlignCenter);
timecode_label_->set_display_type(RationalSlider::k_time);
timecode_label_->setVisible(false);
timecode_label_->set_minimum(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(add_timeline_and_track_view(Qt::AlignBottom));
// Audio view
views_.append(add_timeline_and_track_view(Qt::AlignTop));
// Subtitle view
views_.append(add_timeline_and_track_view(Qt::AlignTop));
// Create tools
tools_.resize(olive::Tool::k_count);
tools_.fill(nullptr);
tools_.replace(olive::Tool::k_pointer, new PointerTool(this));
tools_.replace(olive::Tool::k_track_select, new TrackSelectTool(this));
tools_.replace(olive::Tool::k_edit, new EditTool(this));
tools_.replace(olive::Tool::k_ripple, new RippleTool(this));
tools_.replace(olive::Tool::k_rolling, new RollingTool(this));
tools_.replace(olive::Tool::k_razor, new RazorTool(this));
tools_.replace(olive::Tool::k_slip, new SlipTool(this));
tools_.replace(olive::Tool::k_slide, new SlideTool(this));
tools_.replace(olive::Tool::k_zoom, new ZoomTool(this));
tools_.replace(olive::Tool::k_transition, new TransitionTool(this));
tools_.replace(olive::Tool::k_record, new RecordTool(this));
tools_.replace(olive::Tool::k_add, 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->set_snap_service(this);
view->set_selection_list(&selections_);
view->set_ghost_list(&ghost_items_);
view_splitter_->addWidget(tview);
connect_timeline_view(view);
connect(view, &TimelineView::customContextMenuRequested, this,
&TimelineWidget::show_context_menu);
connect(view, &TimelineView::mouse_pressed, this,
&TimelineWidget::view_mouse_pressed);
connect(view, &TimelineView::mouse_moved, this,
&TimelineWidget::view_mouse_moved);
connect(view, &TimelineView::mouse_released, this,
&TimelineWidget::view_mouse_released);
connect(view, &TimelineView::mouse_double_clicked, this,
&TimelineWidget::view_mouse_double_clicked);
connect(view, &TimelineView::drag_entered, this,
&TimelineWidget::view_drag_entered);
connect(view, &TimelineView::drag_moved, this,
&TimelineWidget::view_drag_moved);
connect(view, &TimelineView::drag_left, this,
&TimelineWidget::view_drag_left);
connect(view, &TimelineView::drag_dropped, this,
&TimelineWidget::view_drag_dropped);
connect(tview->splitter(), &QSplitter::splitterMoved, this,
&TimelineWidget::update_horizontal_splitters);
}
// 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::k_video, false);
view_splitter_->setCollapsible(Track::k_audio, false);
view_splitter_->setCollapsible(Track::k_subtitle, true);
// FIXME: Magic number
SetScale(90.0);
set_auto_set_timebase(false);
connect(Core::instance(), &Core::tool_changed, this,
&TimelineWidget::tool_changed);
connect(Core::instance(), &Core::addable_object_changed, this,
&TimelineWidget::addable_object_changed);
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 (OAK_CONFIG("SelectAlsoSeeks").toBool()) {
Rational start = RATIONAL_MAX;
for (Block *b : selected_blocks_) {
start = std::min(start, b->in());
}
if (start != RATIONAL_MAX) {
get_connected_node()->set_playhead(start);
}
}
emit block_selection_changed(selected_blocks_);
});
}
TimelineWidget::~TimelineWidget()
{
// Ensure no blocks are selected before any child widgets are destroyed (prevents corrupt ViewSelectionChanged() signal)
connect_viewer_node(nullptr);
clear();
qDeleteAll(tools_);
delete subtitle_show_command_;
}
void TimelineWidget::clear()
{
// Emit that we've deselected any selected blocks
signal_deselected_all_blocks();
// Set null timebase
SetTimebase(0);
}
void TimelineWidget::TimebaseChangedEvent(const Rational &timebase)
{
super::TimebaseChangedEvent(timebase);
timecode_label_->set_timebase(timebase);
timecode_label_->setVisible(!timebase.isNull());
update_view_timebases();
}
void TimelineWidget::resizeEvent(QResizeEvent *event)
{
super::resizeEvent(event);
// Update timecode label size
update_timecode_width_from_splitters(views_.first()->splitter());
}
void TimelineWidget::TimeChangedEvent(const Rational &t)
{
if (OAK_CONFIG("SeekAlsoSelects").toBool()) {
TimelineWidgetSelections sels;
QVector new_blocks;
for (auto it = sequence()->get_tracks().cbegin();
it != sequence()->get_tracks().cend(); it++) {
Track *track = *it;
if (track->is_locked()) {
continue;
}
Block *b = track->visible_block_at_time(sequence()->get_playhead());
if (!b || dynamic_cast(b)) {
continue;
}
new_blocks.push_back(b);
sels[track->to_reference()].insert(b->range());
}
if (selected_blocks_ != new_blocks) {
selected_blocks_ = new_blocks;
set_selections(sels, false);
signal_block_selection_change();
}
}
}
void TimelineWidget::ScaleChangedEvent(const double &scale)
{
super::ScaleChangedEvent(scale);
foreach (TimelineAndTrackView *view, views_) {
view->view()->set_scale(scale);
}
if (rubberband_.isVisible()) {
QMetaObject::invokeMethod(this, &TimelineWidget::force_update_rubber_band,
Qt::QueuedConnection);
}
}
void TimelineWidget::ConnectNodeEvent(ViewerOutput *n)
{
Sequence *s = static_cast(n);
connect(s, &Sequence::track_added, this, &TimelineWidget::add_track);
connect(s, &Sequence::track_removed, this, &TimelineWidget::remove_track);
connect(s, &Sequence::frame_rate_changed, this,
&TimelineWidget::frame_rate_changed);
connect(s, &Sequence::sample_rate_changed, this,
&TimelineWidget::sample_rate_changed);
connect(timecode_label_, &RationalSlider::value_changed, s,
&Sequence::set_playhead);
connect(s, &Sequence::playhead_changed, timecode_label_,
&RationalSlider::set_value);
timecode_label_->set_value(s->get_playhead());
ruler()->set_playback_cache(n->video_frame_cache());
SetTimebase(n->get_video_params().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->connect_track_list(track_list);
view->connect_track_list(track_list);
// Defer to the track to make all the block UI items necessary
const QVector