Files
oak-editor/app/widget/timebased/timebasedwidget.cpp
T
itsmattkc d7c5ac4b44 implement entire project in nodes
Project items are now represented directly in nodes opening up more versatility and possibilities. This is the first iteration of this and will be buggy. Need to test thoroughly.
2021-02-15 21:31:57 +11:00

675 lines
17 KiB
C++

/***
Olive - Non-Linear Video Editor
Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
***/
#include "timebasedwidget.h"
#include <QInputDialog>
#include "common/autoscroll.h"
#include "common/timecodefunctions.h"
#include "config/config.h"
#include "core.h"
#include "project/item/sequence/sequence.h"
#include "widget/timelinewidget/timelineundo.h"
namespace olive {
TimeBasedWidget::TimeBasedWidget(bool ruler_text_visible, bool ruler_cache_status_visible, QWidget *parent) :
TimelineScaledWidget(parent),
viewer_node_(nullptr),
auto_max_scrollbar_(false),
points_(nullptr),
toggle_show_all_(false),
auto_set_timebase_(true)
{
ruler_ = new TimeRuler(ruler_text_visible, ruler_cache_status_visible, this);
connect(ruler_, &TimeRuler::TimeChanged, this, &TimeBasedWidget::SetTimeAndSignal);
scrollbar_ = new ResizableTimelineScrollBar(Qt::Horizontal, this);
connect(scrollbar_, &ResizableScrollBar::ResizeBegan, this, &TimeBasedWidget::ScrollBarResizeBegan);
connect(scrollbar_, &ResizableScrollBar::ResizeMoved, this, &TimeBasedWidget::ScrollBarResizeMoved);
PassWheelEventsToScrollBar(ruler_);
}
void TimeBasedWidget::SetScaleAndCenterOnPlayhead(const double &scale)
{
SetScale(scale);
// Zoom towards the playhead
// (using a hacky singleShot so the scroll occurs after the scene and its scrollbars have updated)
QTimer::singleShot(0, this, &TimeBasedWidget::CenterScrollOnPlayhead);
}
rational TimeBasedWidget::GetTime() const
{
return Timecode::timestamp_to_time(ruler()->GetTime(), timebase());
}
const int64_t &TimeBasedWidget::GetTimestamp() const
{
return ruler_->GetTime();
}
Sequence *TimeBasedWidget::GetConnectedNode() const
{
return viewer_node_;
}
void TimeBasedWidget::ConnectViewerNode(Sequence *node)
{
if (viewer_node_ == node) {
return;
}
if (viewer_node_) {
DisconnectNodeInternal(viewer_node_);
disconnect(viewer_node_, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll);
disconnect(viewer_node_, &ViewerOutput::TimebaseChanged, this, &TimeBasedWidget::SetTimebase);
if (auto_set_timebase_) {
SetTimebase(rational());
}
points_ = nullptr;
ruler()->ConnectTimelinePoints(nullptr);
scrollbar_->ConnectTimelinePoints(nullptr);
}
viewer_node_ = node;
ConnectedNodeChanged(viewer_node_);
if (viewer_node_) {
connect(viewer_node_, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll);
if ((points_ = ConnectTimelinePoints())) {
ruler()->ConnectTimelinePoints(points_);
scrollbar_->ConnectTimelinePoints(points_);
}
if (auto_set_timebase_) {
if (!viewer_node_->video_params().time_base().isNull()) {
SetTimebase(viewer_node_->video_params().time_base());
} else if (viewer_node_->audio_params().sample_rate() > 0) {
SetTimebase(viewer_node_->audio_params().time_base());
} else {
SetTimebase(rational());
}
connect(viewer_node_, &ViewerOutput::TimebaseChanged, this, &TimeBasedWidget::SetTimebase);
}
ConnectNodeInternal(viewer_node_);
}
UpdateMaximumScroll();
}
void TimeBasedWidget::UpdateMaximumScroll()
{
rational length = (viewer_node_) ? viewer_node_->GetLength() : 0;
if (auto_max_scrollbar_) {
scrollbar_->setMaximum(qMax(0, qCeil(TimeToScene(length)) - width()));
}
foreach (TimeBasedView* base, timeline_views_) {
base->SetEndTime(length);
}
}
void TimeBasedWidget::ScrollBarResizeBegan(int current_bar_width, bool top_handle)
{
QScrollBar* bar = static_cast<QScrollBar*>(sender());
scrollbar_start_width_ = current_bar_width;
scrollbar_start_value_ = bar->value();
scrollbar_start_scale_ = GetScale();
scrollbar_top_handle_ = top_handle;
}
void TimeBasedWidget::ScrollBarResizeMoved(int movement)
{
ResizableScrollBar* bar = static_cast<ResizableScrollBar*>(sender());
// Negate movement for the top handle
if (scrollbar_top_handle_) {
movement = -movement;
}
// The user wants the bar to be this size
int proposed_size = scrollbar_start_width_ + movement;
double ratio = double(scrollbar_start_width_) / double(proposed_size);
if (ratio > 0) {
SetScale(scrollbar_start_scale_ * ratio);
if (scrollbar_top_handle_) {
int viewable_area;
if (timeline_views_.isEmpty()) {
viewable_area = width();
} else {
viewable_area = timeline_views_.first()->width();
}
bar->setValue((scrollbar_start_value_ + viewable_area) * ratio - viewable_area);
} else {
bar->setValue(scrollbar_start_value_ * ratio);
}
}
}
void TimeBasedWidget::PageScrollToPlayhead()
{
PageScrollInternal(qRound(TimeToScene(GetTime())), true);
}
void TimeBasedWidget::CatchUpScrollToPlayhead()
{
CatchUpScrollToPoint(qRound(TimeToScene(GetTime())));
}
void TimeBasedWidget::CatchUpScrollToPoint(int point)
{
PageScrollInternal(point, false);
}
TimeRuler *TimeBasedWidget::ruler() const
{
return ruler_;
}
ResizableTimelineScrollBar *TimeBasedWidget::scrollbar() const
{
return scrollbar_;
}
void TimeBasedWidget::TimebaseChangedEvent(const rational &timebase)
{
TimelineScaledWidget::TimebaseChangedEvent(timebase);
ruler_->SetTimebase(timebase);
scrollbar_->SetTimebase(timebase);
emit TimebaseChanged(timebase);
}
void TimeBasedWidget::ScaleChangedEvent(const double &scale)
{
TimelineScaledWidget::ScaleChangedEvent(scale);
ruler_->SetScale(scale);
scrollbar_->SetScale(scale);
UpdateMaximumScroll();
toggle_show_all_ = false;
}
void TimeBasedWidget::SetAutoMaxScrollBar(bool e)
{
auto_max_scrollbar_ = e;
}
void TimeBasedWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
// Update horizontal scrollbar's page step to the width of the panel
scrollbar()->setPageStep(scrollbar()->width());
UpdateMaximumScroll();
}
TimelinePoints *TimeBasedWidget::ConnectTimelinePoints()
{
return viewer_node_->timeline_points();
}
Project *TimeBasedWidget::GetTimelinePointsProject()
{
return viewer_node_->project();
}
TimelinePoints *TimeBasedWidget::GetConnectedTimelinePoints() const
{
return points_;
}
void TimeBasedWidget::ConnectTimelineView(TimeBasedView *base, bool connect_time_change_event)
{
if (connect_time_change_event) {
connect(base, &TimeBasedView::TimeChanged, this, &TimeBasedWidget::SetTimestamp);
connect(base, &TimeBasedView::TimeChanged, this, &TimeBasedWidget::TimeChanged);
}
timeline_views_.append(base);
}
void TimeBasedWidget::PassWheelEventsToScrollBar(QObject *object)
{
wheel_passthrough_objects_.append(object);
object->installEventFilter(this);
}
void TimeBasedWidget::SetTimestamp(int64_t timestamp)
{
if (GetTime() != timestamp) {
if (UserIsDraggingPlayhead()) {
// If the user is dragging the playhead, we will simply nudge over and not use autoscroll rules.
QMetaObject::invokeMethod(this, "CatchUpScrollToPlayhead", Qt::QueuedConnection);
} else {
// Otherwise, assume we jumped to this out of nowhere and must now autoscroll
switch (static_cast<AutoScroll::Method>(Config::Current()["Autoscroll"].toInt())) {
case AutoScroll::kNone:
// Do nothing
break;
case AutoScroll::kPage:
QMetaObject::invokeMethod(this, "PageScrollToPlayhead", Qt::QueuedConnection);
break;
case AutoScroll::kSmooth:
QMetaObject::invokeMethod(this, "CenterScrollOnPlayhead", Qt::QueuedConnection);
break;
}
}
ruler_->SetTime(timestamp);
TimeChangedEvent(timestamp);
}
}
void TimeBasedWidget::SetTimebase(const rational &timebase)
{
TimelineScaledWidget::SetTimebase(timebase);
}
void TimeBasedWidget::SetScale(const double &scale)
{
// Simple QObject slot wrapper around TimelineScaledWidget::SetScale()
TimelineScaledWidget::SetScale(scale);
}
void TimeBasedWidget::ZoomIn()
{
SetScaleAndCenterOnPlayhead(GetScale() * 2);
}
void TimeBasedWidget::ZoomOut()
{
SetScaleAndCenterOnPlayhead(GetScale() * 0.5);
}
void TimeBasedWidget::GoToPrevCut()
{
// Cuts are only possible in sequences
Sequence* sequence = dynamic_cast<Sequence*>(viewer_node_);
if (!sequence) {
return;
}
if (GetTimestamp() == 0) {
return;
}
int64_t closest_cut = 0;
foreach (Track* track, sequence->GetTracks()) {
int64_t this_track_closest_cut = 0;
foreach (Block* block, track->Blocks()) {
int64_t block_out_ts = Timecode::time_to_timestamp(block->out(), timebase());
if (block_out_ts < GetTimestamp()) {
this_track_closest_cut = block_out_ts;
} else {
break;
}
}
closest_cut = qMax(closest_cut, this_track_closest_cut);
}
SetTimeAndSignal(closest_cut);
}
void TimeBasedWidget::GoToNextCut()
{
// Cuts are only possible in sequences
Sequence* sequence = dynamic_cast<Sequence*>(viewer_node_);
if (!sequence) {
return;
}
int64_t closest_cut = INT64_MAX;
foreach (Track* track, sequence->GetTracks()) {
int64_t this_track_closest_cut = Timecode::time_to_timestamp(track->track_length(), timebase());
if (this_track_closest_cut <= GetTimestamp()) {
this_track_closest_cut = INT64_MAX;
}
foreach (Block* block, track->Blocks()) {
int64_t block_in_ts = Timecode::time_to_timestamp(block->in(), timebase());
if (block_in_ts > GetTimestamp()) {
this_track_closest_cut = block_in_ts;
break;
}
}
closest_cut = qMin(closest_cut, this_track_closest_cut);
}
if (closest_cut < INT64_MAX) {
SetTimeAndSignal(closest_cut);
}
}
void TimeBasedWidget::GoToStart()
{
if (viewer_node_) {
SetTimeAndSignal(0);
}
}
void TimeBasedWidget::PrevFrame()
{
if (viewer_node_) {
SetTimeAndSignal(qMax(static_cast<int64_t>(0), ruler()->GetTime() - 1));
}
}
void TimeBasedWidget::NextFrame()
{
if (viewer_node_) {
SetTimeAndSignal(ruler()->GetTime() + 1);
}
}
void TimeBasedWidget::GoToEnd()
{
if (viewer_node_) {
SetTimeAndSignal(Timecode::time_to_timestamp(viewer_node_->GetLength(), timebase()));
}
}
void TimeBasedWidget::SetTimeAndSignal(const int64_t &t)
{
SetTimestamp(t);
emit TimeChanged(t);
}
void TimeBasedWidget::CenterScrollOnPlayhead()
{
scrollbar_->setValue(qRound(TimeToScene(Timecode::timestamp_to_time(ruler_->GetTime(), timebase()))) - scrollbar_->width()/2);
}
void TimeBasedWidget::SetAutoSetTimebase(bool e)
{
auto_set_timebase_ = e;
}
void TimeBasedWidget::SetPoint(Timeline::MovementMode m, const rational& time)
{
if (!points_) {
return;
}
MultiUndoCommand* command = new MultiUndoCommand();
// Enable workarea if it isn't already enabled
if (!points_->workarea()->enabled()) {
command->add_child(new WorkareaSetEnabledCommand(GetTimelinePointsProject(), points_, true));
}
// Determine our new range
rational in_point, out_point;
if (m == Timeline::kTrimIn) {
in_point = time;
if (!points_->workarea()->enabled() || points_->workarea()->out() < in_point) {
out_point = TimelineWorkArea::kResetOut;
} else {
out_point = points_->workarea()->out();
}
} else {
out_point = time;
if (!points_->workarea()->enabled() || points_->workarea()->in() > out_point) {
in_point = TimelineWorkArea::kResetIn;
} else {
in_point = points_->workarea()->in();
}
}
// Set workarea
command->add_child(new WorkareaSetRangeCommand(GetTimelinePointsProject(), points_, TimeRange(in_point, out_point)));
Core::instance()->undo_stack()->push(command);
}
void TimeBasedWidget::ResetPoint(Timeline::MovementMode m)
{
if (!points_ || !points_->workarea()->enabled()) {
return;
}
TimeRange r = points_->workarea()->range();
if (m == Timeline::kTrimIn) {
r.set_in(TimelineWorkArea::kResetIn);
} else {
r.set_out(TimelineWorkArea::kResetOut);
}
Core::instance()->undo_stack()->push(new WorkareaSetRangeCommand(GetTimelinePointsProject(), points_, r));
}
void TimeBasedWidget::PageScrollInternal(QScrollBar *bar, int maximum, int screen_position, bool whole_page_scroll)
{
int viewport_padding = maximum / 16;
if (whole_page_scroll) {
if (screen_position < bar->value()) {
// Anchor the playhead to the RIGHT of where we scroll to
bar->setValue(screen_position - maximum + viewport_padding);
} else if (screen_position > bar->value() + maximum) {
// Anchor the playhead to the LEFT of where we scroll to
bar->setValue(screen_position - viewport_padding);
}
} else {
// Just jump in increments
if (screen_position < bar->value() + viewport_padding) {
bar->setValue(bar->value() - viewport_padding);
} else if (screen_position > bar->value() + maximum - viewport_padding) {
bar->setValue(bar->value() + viewport_padding);
}
}
}
void TimeBasedWidget::PageScrollInternal(int screen_position, bool whole_page_scroll)
{
PageScrollInternal(scrollbar(), ruler()->width(), screen_position, whole_page_scroll);
}
bool TimeBasedWidget::UserIsDraggingPlayhead() const
{
if (ruler_->IsDraggingPlayhead()) {
return true;
}
foreach (TimeBasedView* view, timeline_views_) {
if (view->IsDraggingPlayhead()) {
return true;
}
}
return false;
}
void TimeBasedWidget::SetInAtPlayhead()
{
SetPoint(Timeline::kTrimIn, GetTime());
}
void TimeBasedWidget::SetOutAtPlayhead()
{
SetPoint(Timeline::kTrimOut, GetTime());
}
void TimeBasedWidget::ResetIn()
{
ResetPoint(Timeline::kTrimIn);
}
void TimeBasedWidget::ResetOut()
{
ResetPoint(Timeline::kTrimOut);
}
void TimeBasedWidget::ClearInOutPoints()
{
if (!points_) {
return;
}
Core::instance()->undo_stack()->push(new WorkareaSetEnabledCommand(GetTimelinePointsProject(), points_, false));
}
void TimeBasedWidget::SetMarker()
{
if (!points_) {
return;
}
bool ok;
QString marker_name;
if (Config::Current()[QStringLiteral("SetNameWithMarker")].toBool()) {
marker_name = QInputDialog::getText(this, tr("Set Marker"), tr("Marker name:"), QLineEdit::Normal, QString(), &ok);
} else {
ok = true;
}
if (ok) {
Core::instance()->undo_stack()->push(new MarkerAddCommand(GetConnectedNode()->project(),
points_->markers(), TimeRange(GetTime(), GetTime()), marker_name));
}
}
void TimeBasedWidget::ToggleShowAll()
{
if (!GetConnectedNode()) {
return;
}
if (toggle_show_all_) {
SetScale(toggle_show_all_old_scale_);
scrollbar_->setValue(toggle_show_all_old_scroll_);
// Don't have to set toggle_show_all_ because SetScale() will automatically set it to false
} else {
int w;
if (timeline_views_.isEmpty()) {
w = width();
} else {
w = timeline_views_.first()->width();
}
toggle_show_all_old_scale_ = GetScale();
toggle_show_all_old_scroll_ = scrollbar_->value();
SetScaleFromDimensions(w, GetConnectedNode()->GetLength().toDouble());
scrollbar_->setValue(0);
// Must explicitly do this because SetScale() will automatically set this to false
toggle_show_all_ = true;
}
}
void TimeBasedWidget::GoToIn()
{
if (viewer_node_) {
if (points_ && points_->workarea()->enabled()) {
SetTimeAndSignal(Timecode::time_to_timestamp(points_->workarea()->in(), timebase()));
} else {
GoToStart();
}
}
}
void TimeBasedWidget::GoToOut()
{
if (viewer_node_) {
if (points_ && points_->workarea()->enabled()) {
SetTimeAndSignal(Timecode::time_to_timestamp(points_->workarea()->out(), timebase()));
} else {
GoToEnd();
}
}
}
TimeBasedWidget::MarkerAddCommand::MarkerAddCommand(Project *project, TimelineMarkerList *marker_list, const TimeRange &range, const QString &name) :
project_(project),
marker_list_(marker_list),
range_(range),
name_(name)
{
}
Project *TimeBasedWidget::MarkerAddCommand::GetRelevantProject() const
{
return project_;
}
void TimeBasedWidget::MarkerAddCommand::redo()
{
added_marker_ = marker_list_->AddMarker(range_, name_);
}
void TimeBasedWidget::MarkerAddCommand::undo()
{
marker_list_->RemoveMarker(added_marker_);
}
bool TimeBasedWidget::eventFilter(QObject *object, QEvent *event)
{
if (wheel_passthrough_objects_.contains(object) && event->type() == QEvent::Wheel) {
QCoreApplication::sendEvent(scrollbar(), event);
}
return false;
}
}