/*** Olive - Non-Linear Video Editor Copyright (C) 2019 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 "common/timecodefunctions.h" #include "core.h" #include "node/input/media/media.h" #include "project/item/footage/footage.h" #include "tool/tool.h" TimelineView::TimelineView(QWidget *parent) : QGraphicsView(parent), pointer_tool_(this), import_tool_(this), ripple_tool_(this), rolling_tool_(this), razor_tool_(this), slide_tool_(this), slip_tool_(this), hand_tool_(this), zoom_tool_(this), timeline_node_(nullptr), playhead_(0) { setScene(&scene_); setAlignment(Qt::AlignLeft | Qt::AlignTop); setDragMode(RubberBandDrag); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setBackgroundRole(QPalette::Window); connect(&scene_, SIGNAL(changed(const QList&)), this, SLOT(UpdateSceneRect())); // Create playhead line playhead_line_ = new TimelineViewPlayheadItem(); scene_.addItem(playhead_line_); // Set default scale SetScale(1.0); } void TimelineView::AddBlock(Block *block, int track) { switch (block->type()) { case Block::kClip: case Block::kGap: { TimelineViewBlockItem* item = new TimelineViewBlockItem(); // Set up clip with view parameters (clip item will automatically size its rect accordingly) item->SetBlock(block); item->SetY(GetTrackY(track)); item->SetHeight(GetTrackHeight(track)); item->SetScale(scale_); item->SetTrack(track); // Add to list of clip items that can be iterated through block_items_.insert(block, item); // Add item to graphics scene scene_.addItem(item); connect(block, SIGNAL(Refreshed()), this, SLOT(BlockChanged())); break; } case Block::kEnd: // Do nothing break; } } void TimelineView::RemoveBlock(Block *block) { delete block_items_[block]; block_items_.remove(block); } void TimelineView::AddTrack(TrackOutput *track) { foreach (Block* b, track->Blocks()) { AddBlock(b, track->Index()); } } void TimelineView::RemoveTrack(TrackOutput *track) { foreach (Block* b, track->Blocks()) { RemoveBlock(b); } } void TimelineView::SetScale(const double &scale) { scale_ = scale; QMapIterator iterator(block_items_); while (iterator.hasNext()) { iterator.next(); if (iterator.value() != nullptr) { iterator.value()->SetScale(scale_); } } foreach (TimelineViewGhostItem* ghost, ghost_items_) { ghost->SetScale(scale_); } playhead_line_->SetScale(scale_); } void TimelineView::SetTimebase(const rational &timebase) { timebase_ = timebase; timebase_dbl_ = timebase_.toDouble(); playhead_line_->SetTimebase(timebase_); } void TimelineView::Clear() { QMapIterator iterator(block_items_); while (iterator.hasNext()) { iterator.next(); if (iterator.value() != nullptr) { delete iterator.value(); } } block_items_.clear(); } void TimelineView::ConnectTimelineNode(TimelineOutput *node) { if (timeline_node_ != nullptr) { disconnect(timeline_node_, SIGNAL(TimebaseChanged(const rational&)), this, SIGNAL(TimebaseChanged(const rational&))); disconnect(timeline_node_, SIGNAL(TimebaseChanged(const rational&)), this, SLOT(SetTimebase(const rational&))); disconnect(timeline_node_, SIGNAL(TimelineCleared()), this, SLOT(Clear())); disconnect(timeline_node_, SIGNAL(BlockAdded(Block*, int)), this, SLOT(AddBlock(Block*, int))); disconnect(timeline_node_, SIGNAL(BlockRemoved(Block*)), this, SLOT(RemoveBlock(Block*))); disconnect(timeline_node_, SIGNAL(TrackAdded(TrackOutput*)), this, SLOT(AddTrack(TrackOutput*))); disconnect(timeline_node_, SIGNAL(TrackRemoved(TrackOutput*)), this, SLOT(RemoveTrack(TrackOutput*))); Clear(); } timeline_node_ = node; if (timeline_node_ != nullptr) { SetTimebase(timeline_node_->Timebase()); emit TimebaseChanged(timebase_); connect(timeline_node_, SIGNAL(TimebaseChanged(const rational&)), this, SIGNAL(TimebaseChanged(const rational&))); connect(timeline_node_, SIGNAL(TimebaseChanged(const rational&)), this, SLOT(SetTimebase(const rational&))); connect(timeline_node_, SIGNAL(TimelineCleared()), this, SLOT(Clear())); connect(timeline_node_, SIGNAL(BlockAdded(Block*, int)), this, SLOT(AddBlock(Block*, int))); connect(timeline_node_, SIGNAL(BlockRemoved(Block*)), this, SLOT(RemoveBlock(Block*))); connect(timeline_node_, SIGNAL(TrackAdded(TrackOutput*)), this, SLOT(AddTrack(TrackOutput*))); connect(timeline_node_, SIGNAL(TrackRemoved(TrackOutput*)), this, SLOT(RemoveTrack(TrackOutput*))); foreach (TrackOutput* track, timeline_node_->Tracks()) { // Defer to the track to make all the block UI items necessary AddTrack(track); } } } void TimelineView::DisconnectTimelineNode() { ConnectTimelineNode(nullptr); } void TimelineView::SelectAll() { QList all_items = items(); foreach (QGraphicsItem* i, all_items) { i->setSelected(true); } } void TimelineView::DeselectAll() { QList all_items = items(); foreach (QGraphicsItem* i, all_items) { i->setSelected(false); } } void TimelineView::RippleToIn() { RippleEditTo(olive::timeline::kTrimIn, false); } void TimelineView::RippleToOut() { RippleEditTo(olive::timeline::kTrimOut, false); } void TimelineView::EditToIn() { RippleEditTo(olive::timeline::kTrimIn, true); } void TimelineView::EditToOut() { RippleEditTo(olive::timeline::kTrimOut, true); } void TimelineView::GoToPrevCut() { if (timeline_node_ == nullptr) { return; } if (playhead_ == 0) { return; } int64_t closest_cut = 0; foreach (TrackOutput* track, timeline_node_->Tracks()) { int64_t this_track_closest_cut = 0; foreach (Block* block, track->Blocks()) { int64_t block_out_ts = olive::time_to_timestamp(block->out(), timebase_); if (block_out_ts < playhead_) { this_track_closest_cut = block_out_ts; } else { break; } } closest_cut = qMax(closest_cut, this_track_closest_cut); } UserSetTime(closest_cut); } void TimelineView::GoToNextCut() { if (timeline_node_ == nullptr) { return; } int64_t closest_cut = INT64_MAX; foreach (TrackOutput* track, timeline_node_->Tracks()) { int64_t this_track_closest_cut = olive::time_to_timestamp(track->in(), timebase_); if (this_track_closest_cut <= playhead_) { this_track_closest_cut = INT64_MAX; } foreach (Block* block, track->Blocks()) { int64_t block_in_ts = olive::time_to_timestamp(block->in(), timebase_); if (block_in_ts > playhead_) { this_track_closest_cut = block_in_ts; break; } } closest_cut = qMin(closest_cut, this_track_closest_cut); } if (closest_cut < INT64_MAX) { UserSetTime(closest_cut); } } void TimelineView::SetTime(const int64_t time) { playhead_ = time; playhead_line_->SetPlayhead(playhead_); } void TimelineView::mousePressEvent(QMouseEvent *event) { active_tool_ = GetActiveTool(); if (timeline_node_ != nullptr && active_tool_ != nullptr) { active_tool_->MousePress(event); } } void TimelineView::mouseMoveEvent(QMouseEvent *event) { if (timeline_node_ != nullptr && active_tool_ != nullptr) { active_tool_->MouseMove(event); } } void TimelineView::mouseReleaseEvent(QMouseEvent *event) { if (timeline_node_ != nullptr && active_tool_ != nullptr) { active_tool_->MouseRelease(event); } } void TimelineView::dragEnterEvent(QDragEnterEvent *event) { if (timeline_node_ != nullptr) { import_tool_.DragEnter(event); } } void TimelineView::dragMoveEvent(QDragMoveEvent *event) { if (timeline_node_ != nullptr) { import_tool_.DragMove(event); } } void TimelineView::dragLeaveEvent(QDragLeaveEvent *event) { if (timeline_node_ != nullptr) { import_tool_.DragLeave(event); } } void TimelineView::dropEvent(QDropEvent *event) { if (timeline_node_ != nullptr) { import_tool_.DragDrop(event); } } void TimelineView::resizeEvent(QResizeEvent *event) { QGraphicsView::resizeEvent(event); UpdateSceneRect(); } TimelineView::Tool *TimelineView::GetActiveTool() { switch (olive::core.tool()) { case olive::tool::kNone: return nullptr; case olive::tool::kPointer: return &pointer_tool_; case olive::tool::kEdit: return nullptr; // FIXME: Implement case olive::tool::kRipple: return &ripple_tool_; case olive::tool::kRolling: return &rolling_tool_; case olive::tool::kRazor: return &razor_tool_; case olive::tool::kSlip: return &slip_tool_; case olive::tool::kSlide: return &slide_tool_; case olive::tool::kHand: return &hand_tool_; case olive::tool::kZoom: return &zoom_tool_; case olive::tool::kTransition: return nullptr; // FIXME: Implement case olive::tool::kRecord: return nullptr; // FIXME: Implement case olive::tool::kAdd: return nullptr; // FIXME: Implement } return nullptr; } int TimelineView::GetTrackY(int track_index) { int y = 0; for (int i=0;iSetScale(scale_); ghost_items_.append(ghost); scene_.addItem(ghost); } bool TimelineView::HasGhosts() { return !ghost_items_.isEmpty(); } rational TimelineView::SceneToTime(const double &x) { // Adjust screen point by scale and timebase int scaled_x_mvmt = qRound(x / scale_ / timebase_dbl_); // Return a time in the timebase return rational(scaled_x_mvmt * timebase_.numerator(), timebase_.denominator()); } int TimelineView::SceneToTrack(const double &y) { int track = -1; int heights = 0; do { track++; heights += GetTrackHeight(track); } while (y > heights); return track; } void TimelineView::ClearGhosts() { if (!ghost_items_.isEmpty()) { foreach (TimelineViewGhostItem* ghost, ghost_items_) { delete ghost; } ghost_items_.clear(); } } void TimelineView::RippleEditTo(olive::timeline::MovementMode mode, bool insert_gaps) { rational playhead_time = olive::timestamp_to_time(playhead_, timebase_); rational closest_point_to_playhead; if (mode == olive::timeline::kTrimIn) { closest_point_to_playhead = 0; } else { closest_point_to_playhead = RATIONAL_MAX; } foreach (TrackOutput* track, timeline_node_->Tracks()) { Block* b = track->NearestBlockBefore(playhead_time); if (b != nullptr) { if (mode == olive::timeline::kTrimIn) { closest_point_to_playhead = qMax(b->in(), closest_point_to_playhead); } else { closest_point_to_playhead = qMin(b->out(), closest_point_to_playhead); } } } QUndoCommand* command = new QUndoCommand(); if (closest_point_to_playhead == playhead_time) { // Remove one frame only if (mode == olive::timeline::kTrimIn) { playhead_time += timebase_; } else { playhead_time -= timebase_; } } rational in_ripple = qMin(closest_point_to_playhead, playhead_time); rational out_ripple = qMax(closest_point_to_playhead, playhead_time); rational ripple_length = out_ripple - in_ripple; foreach (TrackOutput* track, timeline_node_->Tracks()) { TrackRippleRemoveAreaCommand* ripple_command = new TrackRippleRemoveAreaCommand(track, in_ripple, out_ripple, command); if (insert_gaps) { GapBlock* gap = new GapBlock(); gap->set_length(ripple_length); ripple_command->SetInsert(gap); } } olive::undo_stack.pushIfHasChildren(command); if (mode == olive::timeline::kTrimIn && !insert_gaps) { int64_t new_time = olive::time_to_timestamp(closest_point_to_playhead, timebase_); UserSetTime(new_time); } } void TimelineView::UserSetTime(const int64_t &time) { SetTime(time); emit TimeChanged(time); } void TimelineView::BlockChanged() { TimelineViewRect* rect = block_items_[static_cast(sender())]; if (rect != nullptr) { rect->UpdateRect(); } } void TimelineView::UpdateSceneRect() { QRectF bounding_rect = scene_.itemsBoundingRect(); // Ensure the scene left and top are always 0 bounding_rect.setTopLeft(QPointF(0, 0)); // Ensure the scene height is always AT LEAST the height of the view // The scrollbar appears to have a 1px margin on the top and bottom, hence the -2 int minimum_height = height() - horizontalScrollBar()->height() - 2; if (bounding_rect.height() < minimum_height) { bounding_rect.setHeight(minimum_height); } // Ensure playhead is the correct height playhead_line_->UpdateRect(); // If the scene is already this rect, do nothing if (scene_.sceneRect() == bounding_rect) { return; } scene_.setSceneRect(bounding_rect); }