diff --git a/app/common/rational.cpp b/app/common/rational.cpp index 059b3057b..677ff9e89 100644 --- a/app/common/rational.cpp +++ b/app/common/rational.cpp @@ -9,6 +9,12 @@ rational::rational(const AVRational &r) : { } +rational rational::fromDouble(const double &flt) +{ + // Use FFmpeg function for the time being + return av_d2q(flt, INT_MAX); +} + //Function: print number to cout void rational::print(ostream &out) const diff --git a/app/common/rational.h b/app/common/rational.h index 519b8fcd1..ba35f45aa 100644 --- a/app/common/rational.h +++ b/app/common/rational.h @@ -55,6 +55,8 @@ public: rational(const AVRational& r); + static rational fromDouble(const double& flt); + //Assignment Operators const rational& operator=(const rational &rhs); const rational& operator+=(const rational &rhs); diff --git a/app/dialog/CMakeLists.txt b/app/dialog/CMakeLists.txt index f1101d230..09fa3653b 100644 --- a/app/dialog/CMakeLists.txt +++ b/app/dialog/CMakeLists.txt @@ -21,6 +21,7 @@ add_subdirectory(footageproperties) add_subdirectory(preferences) add_subdirectory(projectproperties) add_subdirectory(sequence) +add_subdirectory(speedduration) set(OLIVE_SOURCES ${OLIVE_SOURCES} diff --git a/app/dialog/speedduration/CMakeLists.txt b/app/dialog/speedduration/CMakeLists.txt new file mode 100644 index 000000000..ee025e694 --- /dev/null +++ b/app/dialog/speedduration/CMakeLists.txt @@ -0,0 +1,22 @@ +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/speedduration/speedduration.h + dialog/speedduration/speedduration.cpp + PARENT_SCOPE +) diff --git a/app/dialog/speedduration/speedduration.cpp b/app/dialog/speedduration/speedduration.cpp new file mode 100644 index 000000000..a994a526a --- /dev/null +++ b/app/dialog/speedduration/speedduration.cpp @@ -0,0 +1,181 @@ +#include "speedduration.h" + +#include +#include +#include +#include + +#include "common/timecodefunctions.h" +#include "undo/undostack.h" +#include "widget/nodeview/nodeviewundo.h" +#include "widget/timelinewidget/undo/undo.h" + +SpeedDurationDialog::SpeedDurationDialog(const rational& timebase, const QList &clips, QWidget *parent) : + QDialog(parent), + clips_(clips), + timebase_(timebase) +{ + setWindowTitle(tr("Speed/Duration")); + + QGridLayout* layout = new QGridLayout(this); + + int row = 0; + + original_speed_ = clips.first()->speed(); + original_length_ = olive::time_to_timestamp(clips.first()->length(), timebase_); + + // Check all clips after the first to see if they all share the same speed or not + for (int i=1;ispeed())) { + original_speed_ = qSNaN(); + } + + if (original_length_ != olive::time_to_timestamp(clips.at(i)->length(), timebase_)) { + original_length_ = -1; + } + + if (original_length_ == -1 && qIsNaN(original_speed_)) { + break; + } + } + + layout->addWidget(new QLabel(tr("Speed:")), row, 0); + + speed_slider_ = new FloatSlider(); + + // Convert speed to percentage value + speed_slider_->SetValue(original_speed_ * 100.0); + connect(speed_slider_, SIGNAL(ValueChanged(double)), this, SLOT(SpeedChanged())); + layout->addWidget(speed_slider_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Duration:")), row, 0); + + duration_slider_ = new TimeSlider(); + duration_slider_->SetTimebase(timebase_); + duration_slider_->SetValue(original_length_); + layout->addWidget(duration_slider_, row, 1); + + if (!qIsNaN(original_speed_)) { + // Convert internally stored original length value to 1x speed for later calculations + original_length_ = qRound(static_cast(original_length_) * original_speed_); + } + + row++; + + reverse_speed_checkbox_ = new QCheckBox(tr("Reverse Speed")); + layout->addWidget(reverse_speed_checkbox_, row, 0, 1, 2); + + row++; + + maintain_audio_pitch_checkbox_ = new QCheckBox(tr("Maintain Audio Pitch")); + layout->addWidget(maintain_audio_pitch_checkbox_, row, 0, 1, 2); + + row++; + + ripple_clips_checkbox_ = new QCheckBox(tr("Ripple Clips")); + layout->addWidget(ripple_clips_checkbox_, row, 0, 1, 2); + + row++; + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + buttons->setCenterButtons(true); + layout->addWidget(buttons, row, 0, 1, 2); + connect(buttons, SIGNAL(accepted()), this, SLOT(accept())); + connect(buttons, SIGNAL(rejected()), this, SLOT(reject())); +} + +void SpeedDurationDialog::accept() +{ + // FIXME: Support multiple incompatible speeds/lengths + + QUndoCommand* command = new QUndoCommand(); + + rational new_clip_length = olive::timestamp_to_time(duration_slider_->GetValue(), timebase_); + double new_speed = speed_slider_->GetValue() * 0.01; + + foreach (ClipBlock* clip, clips_) { + rational this_clip_new_length = new_clip_length; + + Block* next_block = clip->next(); + + if (this_clip_new_length != clip->length() + && !ripple_clips_checkbox_->isChecked() + && next_block) { + + if (this_clip_new_length > clip->length()) { + + // Check if next clip is a gap, and if so we can take it all up + if (next_block->type() == Block::kGap) { + this_clip_new_length = qMin(next_block->out(), clip->in() + this_clip_new_length); + + // If we're taking up the entire clip, we'll just remove it + if (this_clip_new_length == next_block->out()) { + new TrackRippleRemoveBlockCommand(TrackOutput::TrackFromBlock(next_block), next_block, command); + + // Delete node and its exclusive deps + QList gap_and_its_deps; + gap_and_its_deps.append(next_block); + gap_and_its_deps.append(next_block->GetExclusiveDependencies()); + new NodeRemoveCommand(static_cast(next_block->parent()), gap_and_its_deps, command); + } else { + // Otherwise we can just resize it + new BlockResizeCommand(next_block, next_block->out() - this_clip_new_length, command); + } + + } else { + // Otherwise we can't extend any further + this_clip_new_length = clip->length(); + } + + } else if (this_clip_new_length < clip->length()) { + + // If we're not rippling these clips, we'll need to insert a gap (unless the clip is already at the end) + rational gap_length = clip->length() - this_clip_new_length; + + if (next_block->type() == Block::kGap) { + // If we've already got a gap here, we can just resize it + new BlockResizeCommand(next_block, next_block->length() + gap_length, command); + } else { + // Otherwise we have to create a new gap + GapBlock* gap = new GapBlock(); + gap->set_length(gap_length); + new NodeAddCommand(static_cast(clip->parent()), gap, command); + new TrackInsertBlockBetweenBlocksCommand(TrackOutput::TrackFromBlock(clip), gap, clip, next_block, command); + } + } + } + + if (this_clip_new_length != clip->length()) { + new BlockResizeWithoutMediaOutCommand(clip, this_clip_new_length, command); + } + + int64_t media_length = qRound(static_cast(olive::time_to_timestamp(this_clip_new_length, timebase_)) * new_speed); + new BlockSetMediaOutCommand(clip, + clip->media_in() + olive::timestamp_to_time(media_length, timebase_), + command); + } + + olive::undo_stack.pushIfHasChildren(command); + + QDialog::accept(); +} + +void SpeedDurationDialog::SpeedChanged() +{ + if (original_length_ > -1) { + double new_speed = speed_slider_->GetValue()*0.01; + + if (qIsNull(new_speed)) { + // 0 speed is considered a still image which could be any length and since we can't divide by zero anyway, we + // assume the length will stay the same + duration_slider_->SetValue(original_length_); + } else { + // Otherwise, calculate a length that will show the same amount of "content" with a new length + int64_t new_length = qRound(static_cast(original_length_) / new_speed); + duration_slider_->SetValue(new_length); + } + } +} diff --git a/app/dialog/speedduration/speedduration.h b/app/dialog/speedduration/speedduration.h new file mode 100644 index 000000000..d2496a043 --- /dev/null +++ b/app/dialog/speedduration/speedduration.h @@ -0,0 +1,40 @@ +#ifndef SPEEDDURATIONDIALOG_H +#define SPEEDDURATIONDIALOG_H + +#include +#include +#include + +#include "node/block/clip/clip.h" +#include "widget/slider/floatslider.h" +#include "widget/slider/timeslider.h" + +class SpeedDurationDialog : public QDialog +{ + Q_OBJECT +public: + SpeedDurationDialog(const rational& timebase, const QList& clips, QWidget* parent = nullptr); + +public slots: + virtual void accept() override; + +private: + QList clips_; + + FloatSlider* speed_slider_; + TimeSlider* duration_slider_; + + rational timebase_; + + double original_speed_; + int64_t original_length_; + + QCheckBox* reverse_speed_checkbox_; + QCheckBox* maintain_audio_pitch_checkbox_; + QCheckBox* ripple_clips_checkbox_; + +private slots: + void SpeedChanged(); +}; + +#endif // SPEEDDURATIONDIALOG_H diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index 6d16ff2e0..fde83ff7d 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -159,6 +159,11 @@ rational Block::media_length() const return media_out() - media_in(); } +double Block::speed() const +{ + return media_length().toDouble() / length().toDouble(); +} + const QString &Block::block_name() const { return block_name_; diff --git a/app/node/block/block.h b/app/node/block/block.h index 650f66cff..29eca56e0 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -72,6 +72,7 @@ public: void set_media_out(const rational& media_out); rational media_length() const; + double speed() const; const QString& block_name() const; void set_block_name(const QString& name); diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 9aa7e1b5f..9dcab8a80 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -6,8 +6,10 @@ #include "core.h" #include "common/timecodefunctions.h" +#include "dialog/speedduration/speedduration.h" #include "tool/tool.h" #include "trackview/trackview.h" +#include "widget/menu/menu.h" #include "widget/nodeview/nodeviewundo.h" TimelineWidget::TimelineWidget(QWidget *parent) : @@ -86,6 +88,7 @@ TimelineWidget::TimelineWidget(QWidget *parent) : connect(ruler_, SIGNAL(TimeChanged(const int64_t&)), view, SLOT(SetTime(const int64_t&))); connect(view, SIGNAL(TimeChanged(const int64_t&)), ruler_, SLOT(SetTime(const int64_t&))); connect(view, SIGNAL(TimeChanged(const int64_t&)), this, SIGNAL(TimeChanged(const int64_t&))); + connect(view, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(ShowContextMenu())); connect(horizontal_scroll_, SIGNAL(valueChanged(int)), view->horizontalScrollBar(), SLOT(setValue(int))); connect(view->horizontalScrollBar(), SIGNAL(valueChanged(int)), horizontal_scroll_, SLOT(setValue(int))); @@ -802,6 +805,32 @@ void TimelineWidget::TrackHeightChanged(TrackType type, int index, int height) } } +void TimelineWidget::ShowContextMenu() +{ + Menu menu(this); + + //QList selected = GetSelectedBlocks(); + QAction* speed_duration_action = menu.addAction(tr("Speed/Duration")); + connect(speed_duration_action, SIGNAL(triggered(bool)), this, SLOT(ShowSpeedDurationDialog())); + + menu.exec(QCursor::pos()); +} + +void TimelineWidget::ShowSpeedDurationDialog() +{ + QList selected = GetSelectedBlocks(); + QList selected_clips; + + foreach (TimelineViewBlockItem* item, selected) { + if (item->block()->type() == Block::kClip) { + selected_clips.append(static_cast(item->block())); + } + } + + SpeedDurationDialog speed_diag(timebase(), selected_clips, this); + speed_diag.exec(); +} + void TimelineWidget::AddGhost(TimelineViewGhostItem *ghost) { ghost->SetScale(scale_); diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 23df7e4d4..783385e84 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -415,6 +415,10 @@ private slots: void TrackHeightChanged(TrackType type, int index, int height); + void ShowContextMenu(); + + void ShowSpeedDurationDialog(); + }; #endif // TIMELINEWIDGET_H diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 5ebe3eb12..64ff38f40 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -182,6 +182,10 @@ void TimelineWidget::ImportTool::DragMove(TimelineViewMouseEvent *event) QString tooltip_text = olive::timestamp_to_timecode(earliest_timestamp, parent()->timebase(), olive::CurrentTimecodeDisplay()); + + // Force tooltip to update (otherwise the tooltip won't move as written in the documentation, and could get in the way + // of the cursor) + QToolTip::hideText(); QToolTip::showText(QCursor::pos(), tooltip_text, parent()); diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index 835b705a2..1481c7d03 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -331,6 +331,10 @@ void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_po parent()->timebase(), olive::CurrentTimecodeDisplay(), true); + + // Force tooltip to update (otherwise the tooltip won't move as written in the documentation, and could get in the way + // of the cursor) + QToolTip::hideText(); QToolTip::showText(QCursor::pos(), tooltip_text, parent()); diff --git a/app/widget/timelinewidget/tool/slip.cpp b/app/widget/timelinewidget/tool/slip.cpp index 1c5d14a6a..d00f5501c 100644 --- a/app/widget/timelinewidget/tool/slip.cpp +++ b/app/widget/timelinewidget/tool/slip.cpp @@ -56,6 +56,9 @@ void TimelineWidget::SlipTool::ProcessDrag(const TimelineCoordinate &mouse_pos) parent()->timebase(), olive::CurrentTimecodeDisplay(), true); + // Force tooltip to update (otherwise the tooltip won't move as written in the documentation, and could get in the way + // of the cursor) + QToolTip::hideText(); QToolTip::showText(QCursor::pos(), tooltip_text, parent()); diff --git a/app/widget/timelinewidget/undo/undo.cpp b/app/widget/timelinewidget/undo/undo.cpp index 0d0b973d3..3640416d3 100644 --- a/app/widget/timelinewidget/undo/undo.cpp +++ b/app/widget/timelinewidget/undo/undo.cpp @@ -57,6 +57,24 @@ void BlockResizeCommand::undo() block_->set_length_and_media_out(old_length_); } +BlockResizeWithoutMediaOutCommand::BlockResizeWithoutMediaOutCommand(Block *block, rational new_length, QUndoCommand *parent) : + QUndoCommand(parent), + block_(block), + old_length_(block->length()), + new_length_(new_length) +{ +} + +void BlockResizeWithoutMediaOutCommand::redo() +{ + block_->set_length(new_length_); +} + +void BlockResizeWithoutMediaOutCommand::undo() +{ + block_->set_length(old_length_); +} + BlockResizeWithMediaInCommand::BlockResizeWithMediaInCommand(Block *block, rational new_length, QUndoCommand *parent) : QUndoCommand(parent), block_(block), @@ -103,12 +121,12 @@ BlockSetMediaOutCommand::BlockSetMediaOutCommand(Block *block, rational new_medi void BlockSetMediaOutCommand::redo() { - block_->set_media_in(new_media_out_); + block_->set_media_out(new_media_out_); } void BlockSetMediaOutCommand::undo() { - block_->set_media_in(old_media_out_); + block_->set_media_out(old_media_out_); } TrackRippleRemoveBlockCommand::TrackRippleRemoveBlockCommand(TrackOutput *track, Block *block, QUndoCommand *parent) : diff --git a/app/widget/timelinewidget/undo/undo.h b/app/widget/timelinewidget/undo/undo.h index d5fa4b896..dd01183a8 100644 --- a/app/widget/timelinewidget/undo/undo.h +++ b/app/widget/timelinewidget/undo/undo.h @@ -41,6 +41,19 @@ private: rational new_length_; }; +class BlockResizeWithoutMediaOutCommand : public QUndoCommand { +public: + BlockResizeWithoutMediaOutCommand(Block* block, rational new_length, QUndoCommand* parent = nullptr); + + virtual void redo() override; + virtual void undo() override; + +private: + Block* block_; + rational old_length_; + rational new_length_; +}; + class BlockResizeWithMediaInCommand : public QUndoCommand { public: BlockResizeWithMediaInCommand(Block* block, rational new_length, QUndoCommand* parent = nullptr); diff --git a/app/widget/timelinewidget/view/timelineview.cpp b/app/widget/timelinewidget/view/timelineview.cpp index fdef54aef..26e7c499d 100644 --- a/app/widget/timelinewidget/view/timelineview.cpp +++ b/app/widget/timelinewidget/view/timelineview.cpp @@ -46,6 +46,7 @@ TimelineView::TimelineView(const TrackType &type, Qt::Alignment vertical_alignme setDragMode(NoDrag); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setBackgroundRole(QPalette::Window); + setContextMenuPolicy(Qt::CustomContextMenu); connect(&scene_, SIGNAL(changed(const QList&)), this, SLOT(UpdateSceneRect()));