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()));