/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
***/
#include "timelineview.h"
#include
#include
#include
#include
#include
#include
#include "config/config.h"
#include "common/qtutils.h"
#include "common/timecodefunctions.h"
#include "node/project/footage/footage.h"
#include "panel/panelmanager.h"
#include "panel/timeline/timeline.h"
#include "ui/colorcoding.h"
#include "widget/timelinewidget/timelinewidget.h"
namespace olive {
#define super TimeBasedView
TimelineView::TimelineView(Qt::Alignment vertical_alignment, QWidget *parent) :
super(parent),
selections_(nullptr),
ghosts_(nullptr),
show_beam_cursor_(false),
connected_track_list_(nullptr),
show_thumbnails_(true),
show_waveforms_(true),
transition_overlay_out_(nullptr),
transition_overlay_in_(nullptr)
{
Q_ASSERT(vertical_alignment == Qt::AlignTop || vertical_alignment == Qt::AlignBottom);
setAlignment(Qt::AlignLeft | vertical_alignment);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
setBackgroundRole(QPalette::Window);
setContextMenuPolicy(Qt::CustomContextMenu);
viewport()->setMouseTracking(true);
}
void TimelineView::mousePressEvent(QMouseEvent *event)
{
// If we click on marker, jump to that point in the timeline
QPointF scene_pos = mapToScene(event->pos());
for (auto it=clip_marker_rects_.cbegin(); it!=clip_marker_rects_.cend(); it++) {
if (it.value().contains(scene_pos)) {
QObject *p = this->parent();
while (p) {
if (TimelineWidget *timeline = dynamic_cast(p)) {
timeline->SetTime(it.key()->time().in());
break;
}
p = p->parent();
}
}
}
TimelineViewMouseEvent timeline_event = CreateMouseEvent(event);
if (HandPress(event)
|| (!GetItemAtScenePos(timeline_event.GetFrame(), timeline_event.GetTrack().index()) && Core::instance()->tool() != Tool::kAdd && PlayheadPress(event))) {
// Let the parent handle this
return;
}
if (dragMode() != GetDefaultDragMode()) {
// Use default behavior when hand dragging for instance
super::mousePressEvent(event);
return;
}
emit MousePressed(&timeline_event);
}
void TimelineView::mouseMoveEvent(QMouseEvent *event)
{
TimelineViewMouseEvent timeline_event = CreateMouseEvent(event);
if (HandMove(event) || PlayheadMove(event)) {
// Let the parent handle this
return;
}
if (dragMode() != GetDefaultDragMode()) {
super::mouseMoveEvent(event);
return;
}
if (event->buttons() == Qt::NoButton) {
Block* b = GetItemAtScenePos(timeline_event.GetFrame(), timeline_event.GetTrack().index());
if (b) {
setToolTip(tr("In: %1\nOut: %2\nDuration: %3").arg(
Timecode::time_to_timecode(b->in(), timebase(), Core::instance()->GetTimecodeDisplay()),
Timecode::time_to_timecode(b->out(), timebase(), Core::instance()->GetTimecodeDisplay()),
Timecode::time_to_timecode(b->length(), timebase(), Core::instance()->GetTimecodeDisplay())
));
} else {
setToolTip(QString());
}
}
emit MouseMoved(&timeline_event);
}
void TimelineView::mouseReleaseEvent(QMouseEvent *event)
{
if (HandRelease(event) || PlayheadRelease(event)) {
// Let the parent handle this
return;
}
if (dragMode() != GetDefaultDragMode()) {
super::mouseReleaseEvent(event);
return;
}
TimelineViewMouseEvent timeline_event = CreateMouseEvent(event);
emit MouseReleased(&timeline_event);
}
void TimelineView::mouseDoubleClickEvent(QMouseEvent *event)
{
TimelineViewMouseEvent timeline_event = CreateMouseEvent(event);
emit MouseDoubleClicked(&timeline_event);
}
void TimelineView::wheelEvent(QWheelEvent *event)
{
if (WheelEventIsAZoomEvent(event)) {
super::wheelEvent(event);
} else {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
QPoint angle_delta = event->angleDelta();
if (OLIVE_CONFIG("InvertTimelineScrollAxes").toBool() // Check if config is set to invert timeline axes
&& event->source() != Qt::MouseEventSynthesizedBySystem) { // Never flip axes on Apple trackpads though
angle_delta = QPoint(angle_delta.y(), angle_delta.x());
}
QWheelEvent e(
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
event->position(),
event->globalPosition(),
#else
event->pos(),
event->globalPos(),
#endif
event->pixelDelta(),
angle_delta,
event->buttons(),
event->modifiers(),
event->phase(),
event->inverted(),
event->source()
);
#else
Qt::Orientation orientation = event->orientation();
if (OLIVE_CONFIG("InvertTimelineScrollAxes").toBool()) {
orientation = (orientation == Qt::Horizontal) ? Qt::Vertical : Qt::Horizontal;
}
QWheelEvent e(
event->pos(),
event->globalPos(),
event->pixelDelta(),
event->angleDelta(),
event->delta(),
orientation,
event->buttons(),
event->modifiers()
);
#endif
super::wheelEvent(&e);
}
}
void TimelineView::dragEnterEvent(QDragEnterEvent *event)
{
TimelineViewMouseEvent timeline_event = CreateMouseEvent(event->pos(), Qt::NoButton, event->keyboardModifiers());
timeline_event.SetMimeData(event->mimeData());
timeline_event.SetEvent(event);
emit DragEntered(&timeline_event);
}
void TimelineView::dragMoveEvent(QDragMoveEvent *event)
{
TimelineViewMouseEvent timeline_event = CreateMouseEvent(event->pos(), Qt::NoButton, event->keyboardModifiers());
timeline_event.SetMimeData(event->mimeData());
timeline_event.SetEvent(event);
emit DragMoved(&timeline_event);
}
void TimelineView::dragLeaveEvent(QDragLeaveEvent *event)
{
emit DragLeft(event);
}
void TimelineView::dropEvent(QDropEvent *event)
{
TimelineViewMouseEvent timeline_event = CreateMouseEvent(event->pos(), Qt::NoButton, event->keyboardModifiers());
timeline_event.SetMimeData(event->mimeData());
timeline_event.SetEvent(event);
emit DragDropped(&timeline_event);
}
void TimelineView::drawBackground(QPainter *painter, const QRectF &rect)
{
if (!connected_track_list_) {
return;
}
painter->setPen(palette().base().color());
int line_y = 0;
foreach (Track* track, connected_track_list_->GetTracks()) {
line_y += track->GetTrackHeightInPixels();
// One px gap between tracks
line_y++;
int this_line_y;
if (alignment() & Qt::AlignTop) {
this_line_y = line_y;
} else {
this_line_y = -line_y;
}
painter->drawLine(qRound(rect.left()), this_line_y, qRound(rect.right()), this_line_y);
}
}
void TimelineView::drawForeground(QPainter *painter, const QRectF &rect)
{
if (!connected_track_list_) {
return;
}
// Draw block backgrounds
DrawBlocks(painter, false);
// Draw selections
if (selections_ && !selections_->isEmpty()) {
painter->setPen(Qt::NoPen);
painter->setBrush(QColor(0, 0, 0, 64));
for (auto it=selections_->cbegin(); it!=selections_->cend(); it++) {
if (it.key().type() == connected_track_list_->type()) {
int track_index = it.key().index();
foreach (const TimeRange& range, it.value()) {
painter->drawRect(TimeToScene(range.in()),
GetTrackY(track_index),
TimeToScene(range.length()),
GetTrackHeight(track_index));
}
}
}
}
// Draw block foregrounds
DrawBlocks(painter, true);
// Draw ghosts
if (ghosts_ && !ghosts_->isEmpty()) {
foreach (TimelineViewGhostItem* ghost, (*ghosts_)) {
if (ghost->GetTrack().type() == connected_track_list_->type()
&& !ghost->IsInvisible()) {
int track_index = ghost->GetAdjustedTrack().index();
Block *attached = Node::ValueToPtr(ghost->GetData(TimelineViewGhostItem::kAttachedBlock));
if (attached && OLIVE_CONFIG("ShowClipWhileDragging").toBool()) {
int adj_track = ghost->GetAdjustedTrack().index();
qreal track_top = GetTrackY(adj_track);
qreal track_height = GetTrackHeight(adj_track);
qreal old_opacity = painter->opacity();
painter->setOpacity(0.5);
rational in = ghost->GetAdjustedIn(), out = ghost->GetAdjustedOut(), media_in = ghost->GetAdjustedMediaIn();
DrawBlock(painter, false, attached, track_top, track_height, in, out, media_in);
DrawBlock(painter, true, attached, track_top, track_height, in, out, media_in);
painter->setOpacity(old_opacity);
}
painter->setPen(QPen(Qt::yellow, 2));
painter->setBrush(Qt::NoBrush);
painter->drawRect(TimeToScene(ghost->GetAdjustedIn()),
GetTrackY(track_index),
TimeToScene(ghost->GetAdjustedLength()),
GetTrackHeight(track_index));
}
}
}
// Draw beam cursor
if (show_beam_cursor_
&& cursor_coord_.GetTrack().type() == connected_track_list_->type()) {
painter->setPen(Qt::gray);
double cursor_x = TimeToScene(cursor_coord_.GetFrame());
int track_index = cursor_coord_.GetTrack().index();
int track_y = GetTrackY(track_index);
painter->drawLine(cursor_x,
track_y,
cursor_x,
track_y + GetTrackHeight(track_index));
}
// Draw recording overlay
if (recording_overlay_ && recording_coord_.GetTrack().type() == connected_track_list_->type()) {
painter->setPen(QPen(Qt::red, 2));
painter->setBrush(QColor(255, 128, 128));
int x = TimeToScene(recording_coord_.GetFrame());
painter->drawRect(x, GetTrackY(recording_coord_.GetTrack().index()),
TimeToScene(GetTime()) - x, GetTrackHeight(recording_coord_.GetTrack().index()));
}
// Draw standard TimelineViewBase things (such as playhead)
super::drawForeground(painter, rect);
}
void TimelineView::ToolChangedEvent(Tool::Item tool)
{
switch (tool) {
case Tool::kRazor:
setCursor(Qt::SplitHCursor);
break;
case Tool::kEdit:
setCursor(Qt::IBeamCursor);
break;
case Tool::kAdd:
case Tool::kTransition:
case Tool::kZoom:
case Tool::kRecord :
setCursor(Qt::CrossCursor);
break;
case Tool::kTrackSelect:
setCursor(Qt::SizeHorCursor); // FIXME: Not the ideal cursor
break;
default:
unsetCursor();
}
// Hide/show cursor if necessary
if (show_beam_cursor_) {
show_beam_cursor_ = false;
viewport()->update();
}
}
void TimelineView::SceneRectUpdateEvent(QRectF &rect)
{
if (alignment() & Qt::AlignTop) {
rect.setTop(0);
rect.setBottom(GetHeightOfAllTracks() + height() / 2);
} else if (alignment() & Qt::AlignBottom) {
rect.setBottom(0);
rect.setTop(GetHeightOfAllTracks() - height() / 2);
}
}
Track::Type TimelineView::ConnectedTrackType()
{
if (connected_track_list_) {
return connected_track_list_->type();
}
return Track::kNone;
}
TimelineCoordinate TimelineView::ScreenToCoordinate(const QPoint& pt)
{
return SceneToCoordinate(mapToScene(pt));
}
TimelineCoordinate TimelineView::SceneToCoordinate(const QPointF& pt)
{
return TimelineCoordinate(SceneToTime(pt.x()), Track::Reference(ConnectedTrackType(), SceneToTrack(pt.y())));
}
TimelineViewMouseEvent TimelineView::CreateMouseEvent(QMouseEvent *event)
{
return CreateMouseEvent(event->pos(), event->button(), event->modifiers());
}
TimelineViewMouseEvent TimelineView::CreateMouseEvent(const QPoint& pos, Qt::MouseButton button, Qt::KeyboardModifiers modifiers)
{
QPointF scene_pt = mapToScene(pos);
return TimelineViewMouseEvent(scene_pt.x(),
GetScale(),
timebase(),
Track::Reference(ConnectedTrackType(), SceneToTrack(scene_pt.y())),
button,
modifiers);
}
void TimelineView::DrawBlocks(QPainter *painter, bool foreground)
{
rational start_time = SceneToTime(GetTimelineLeftBound());
rational end_time = SceneToTime(GetTimelineRightBound());
foreach (Track* track, connected_track_list_->GetTracks()) {
// Get first visible block in this track
Block* block = track->NearestBlockBeforeOrAt(start_time);
qreal track_top = GetTrackY(track->Index());
qreal track_height = GetTrackHeight(track->Index());
while (block) {
DrawBlock(painter, foreground, block, track_top, track_height);
if (block->out() >= end_time) {
// Rest of the clips are offscreen, can break loop now
break;
}
block = block->next();
}
}
}
void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, qreal block_top, qreal block_height, const rational &in, const rational &out, const rational &media_in)
{
if (dynamic_cast(block) || dynamic_cast(block)) {
qreal block_in = TimeToScene(in);
qreal block_left = qMax(GetTimelineLeftBound(), block_in);
qreal block_right = qMin(GetTimelineRightBound(), TimeToScene(out)) - 1;
QRectF r(block_left,
block_top,
block_right - block_left,
block_height);
QColor shadow_color = block->color().toQColor().darker();
QFontMetrics fm = fontMetrics();
int text_height = fm.height();
int text_padding = text_height/4; // This ties into the track minimum height being 1.5
int text_total_height = text_height + text_padding + text_padding;
if (foreground) {
painter->setBrush(Qt::NoBrush);
QString using_label = block->GetLabel().isEmpty() ? block->Name() : block->GetLabel();
QRectF text_rect = r.adjusted(text_padding, text_padding, -text_padding, -text_padding);
painter->setPen(block->is_enabled() ? ColorCoding::GetUISelectorColor(block->color()) : Qt::lightGray);
painter->drawText(text_rect, Qt::AlignLeft | Qt::AlignTop, using_label);
if (block->HasLinks()) {
int text_width = qMin(qRound(text_rect.width()),
QtUtils::QFontMetricsWidth(fm, using_label));
int underline_y = text_rect.y() + text_height;
painter->drawLine(text_rect.x(), underline_y, text_width + text_rect.x(), underline_y);
}
qreal line_bottom = block_top+block_height-1;
painter->setPen(Qt::white);
painter->drawLine(block_left, block_top, block_right, block_top);
painter->drawLine(block_left, block_top, block_left, line_bottom);
painter->setPen(shadow_color);
painter->drawLine(block_left, line_bottom, block_right, line_bottom);
painter->drawLine(block_right, line_bottom, block_right, block_top);
} else {
painter->setPen(Qt::NoPen);
painter->setBrush(block->is_enabled() ? block->brush(block_top, block_top + block_height) : Qt::gray);
painter->drawRect(r);
if (ClipBlock *clip = dynamic_cast(block)) {
QRect preview_rect = r.adjusted(0, text_total_height, 0, 0).toRect();
// Draw clip thumbnails
if (clip->GetTrackType() == Track::kVideo && show_thumbnails_ && preview_rect.height() > r.height()/3) {
if (const FrameHashCache *thumbs = clip->thumbnails()) {
QRect thumb_rect;
painter->setClipRect(preview_rect);
painter->setRenderHint(QPainter::SmoothPixmapTransform);
for (int i=preview_rect.left(); iparent()->GetVideoParams().frame_rate_as_time_base()) + media_in;
QString thumbnail = thumbs->GetValidCacheFilename(time_here);
if (thumbnail.isEmpty()) {
// Jump ahead to next frame, ensuring that frame width > 0 for optimization
if (thumb_rect.width() == 0 && clip->track() && clip->track()->sequence()) {
Sequence *s = clip->track()->sequence();
int width = s->GetVideoParams().width();
int height = s->GetVideoParams().height();
if (height > 0) { // Prevent divide by zero/invalid params
double scale = double(preview_rect.height())/double(height);
thumb_rect.setWidth(width * scale);
}
}
} else {
QImage img;
if (img.load(thumbnail, "jpg")) {
double scale = double(preview_rect.height())/double(img.height());
thumb_rect = QRect(i, preview_rect.top(), img.width() * scale, preview_rect.height());
painter->drawImage(thumb_rect, img);
}
}
}
painter->setClipping(false);
}
}
// Draw waveform
if (clip->GetTrackType() == Track::kAudio && show_waveforms_) {
if (const AudioWaveformCache *wave = clip->waveform()) {
rational waveform_start = SceneToTime(block_left - block_in, GetScale(), connected_track_list_->parent()->GetAudioParams().sample_rate_as_time_base()) + media_in;
painter->setPen(shadow_color);
wave->Draw(painter, preview_rect, this->GetScale(), waveform_start);
}
}
// Draw zebra stripes and markers
if (clip->connected_viewer()) {
if (!clip->connected_viewer()->GetLength().isNull()) {
if (clip->media_in() < 0) {
// Draw stripes for sections of clip < 0
qreal zebra_right = TimeToScene(-clip->media_in());
if (zebra_right > GetTimelineLeftBound()) {
DrawZebraStripes(painter, QRectF(block_left, block_top, zebra_right, block_height));
}
}
if (clip->length() + clip->media_in() > clip->connected_viewer()->GetLength()) {
// Draw stripes for sections for clip > clip length
qreal zebra_left = TimeToScene(clip->out() - (clip->media_in() + clip->length() - clip->connected_viewer()->GetLength()));
if (zebra_left < GetTimelineRightBound()) {
DrawZebraStripes(painter, QRectF(zebra_left, block_top, block_right - zebra_left, block_height));
}
}
}
TimelineMarkerList *marker_list = clip->connected_viewer()->GetMarkers();
if (!marker_list->empty()) {
clip_marker_rects_.clear();
for (auto it=marker_list->cbegin(); it!=marker_list->cend(); it++) {
TimelineMarker *marker = *it;
// Make sure marker is within In/Out points of the clip
if (marker->time().in() >= clip->media_in() && marker->time().out() <= clip->media_in() + clip->length()) {
QPoint marker_pt(TimeToScene(clip->in() - clip->media_in() + marker->time().in()), block_top + block_height);
painter->setClipRect(r);
QRect marker_rect = marker->Draw(painter, marker_pt, GetScale(), false);
clip_marker_rects_.insert(marker, marker_rect);
painter->setClipping(false);
}
}
}
}
if (const FrameHashCache *cache = clip->connected_video_cache()) {
if (cache->HasValidatedRanges()) {
QRect cache_rect = r.adjusted(0, r.height() - PlaybackCache::GetCacheIndicatorHeight(), 0, 0).toRect();
cache->Draw(painter, clip->media_in(), GetScale(), cache_rect);
}
}
}
// For transitions, show lines representing a transition
if (TransitionBlock* transition = dynamic_cast(block)) {
QVector lines;
if (transition->connected_in_block()) {
lines.append(QLineF(r.bottomLeft(), r.topRight()));
}
if (transition->connected_out_block()) {
lines.append(QLineF(r.topLeft(), r.bottomRight()));
}
painter->setPen(shadow_color);
painter->drawLines(lines);
}
if (transition_overlay_out_ == block || transition_overlay_in_ == block) {
QRectF transition_overlay_rect = r;
qreal transition_overlay_width = TimeToScene(block->length()) * 0.5;
if (transition_overlay_out_ && transition_overlay_in_) {
// This is a dual transition, use the smallest width
Block *other_block = (transition_overlay_out_ == block) ? transition_overlay_in_ : transition_overlay_out_;
qreal other_width = TimeToScene(other_block->length()) * 0.5;
transition_overlay_width = qMin(transition_overlay_width, other_width);
}
if (transition_overlay_out_ == block) {
transition_overlay_rect.setLeft(transition_overlay_rect.right() - transition_overlay_width);
} else {
transition_overlay_rect.setRight(transition_overlay_rect.left() + transition_overlay_width);
}
painter->setPen(Qt::NoPen);
painter->setBrush(QColor(0, 0, 0, 64));
painter->drawRect(transition_overlay_rect);
}
}
}
}
void TimelineView::DrawZebraStripes(QPainter *painter, const QRectF &r)
{
int zebra_interval = fontMetrics().height();
painter->setPen(QPen(QColor(0, 0, 0, 128), zebra_interval/4));
painter->setBrush(Qt::NoBrush);
QVector lines;
lines.reserve(qCeil(r.width() / zebra_interval));
qreal left = r.left() - r.height();
qreal right = r.right() + r.height();
for (qreal i=left; isetClipRect(r);
painter->drawLines(lines);
painter->setClipping(false);
}
int TimelineView::GetHeightOfAllTracks() const
{
if (connected_track_list_) {
if (alignment() & Qt::AlignTop) {
return GetTrackY(connected_track_list_->GetTrackCount());
} else {
return GetTrackY(connected_track_list_->GetTrackCount() - 1);
}
} else {
return 0;
}
}
qreal TimelineView::GetTimelineLeftBound() const
{
return horizontalScrollBar()->value();
}
qreal TimelineView::GetTimelineRightBound() const
{
return GetTimelineLeftBound() + viewport()->width();
}
int TimelineView::GetTrackY(int track_index) const
{
if (!connected_track_list_ || !connected_track_list_->GetTrackCount()) {
return 0;
}
int y = 0;
if (alignment() & Qt::AlignBottom) {
track_index++;
}
for (int i=0;i= connected_track_list_->GetTrackCount()) {
return Track::GetDefaultTrackHeightInPixels();
}
return connected_track_list_->GetTrackAt(track_index)->GetTrackHeightInPixels();
}
QPoint TimelineView::GetScrollCoordinates() const
{
return QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value());
}
void TimelineView::SetScrollCoordinates(const QPoint &pt)
{
horizontalScrollBar()->setValue(pt.x());
verticalScrollBar()->setValue(pt.y());
}
void TimelineView::ConnectTrackList(TrackList *list)
{
if (connected_track_list_) {
disconnect(connected_track_list_, &TrackList::TrackListChanged, this, &TimelineView::TrackListChanged);
disconnect(connected_track_list_, &TrackList::TrackHeightChanged, this, &TimelineView::TrackListChanged);
}
connected_track_list_ = list;
if (connected_track_list_) {
connect(connected_track_list_, &TrackList::TrackListChanged, this, &TimelineView::TrackListChanged);
connect(connected_track_list_, &TrackList::TrackHeightChanged, this, &TimelineView::TrackListChanged);
}
}
void TimelineView::SetBeamCursor(const TimelineCoordinate &coord)
{
if (!connected_track_list_) {
return;
}
bool update_required = coord.GetTrack().type() == connected_track_list_->type()
|| cursor_coord_.GetTrack().type() == connected_track_list_->type();
show_beam_cursor_ = true;
cursor_coord_ = coord;
if (update_required) {
viewport()->update();
}
}
void TimelineView::SetTransitionOverlay(ClipBlock *out, ClipBlock *in)
{
if (transition_overlay_out_ != out || transition_overlay_in_ != in) {
Track::Type type = Track::kNone;
if (out) {
type = out->track()->type();
} else if (in) {
type = in->track()->type();
}
if (type == this->connected_track_list_->type()) {
transition_overlay_out_ = out;
transition_overlay_in_ = in;
} else {
transition_overlay_out_ = nullptr;
transition_overlay_in_ = nullptr;
}
viewport()->update();
}
}
void TimelineView::EnableRecordingOverlay(const TimelineCoordinate &coord)
{
recording_overlay_ = true;
recording_coord_ = coord;
viewport()->update();
}
void TimelineView::DisableRecordingOverlay()
{
recording_overlay_ = false;
viewport()->update();
}
int TimelineView::SceneToTrack(double y)
{
int track = -1;
int heights = 0;
if (alignment() & Qt::AlignBottom) {
y = -y;
}
do {
track++;
heights += GetTrackHeight(track);
} while (y > heights);
return track;
}
Block *TimelineView::GetItemAtScenePos(const rational &time, int track_index) const
{
if (connected_track_list_) {
Track* track = connected_track_list_->GetTrackAt(track_index);
if (track) {
foreach (Block* b, track->Blocks()) {
if (b->in() <= time && b->out() > time) {
return b;
}
}
}
}
return nullptr;
}
void TimelineView::TrackListChanged()
{
UpdateSceneRect();
viewport()->update();
}
}