diff --git a/app/dialog/CMakeLists.txt b/app/dialog/CMakeLists.txt
index 09fa3653b..f5683a0e3 100644
--- a/app/dialog/CMakeLists.txt
+++ b/app/dialog/CMakeLists.txt
@@ -18,6 +18,7 @@ add_subdirectory(about)
add_subdirectory(actionsearch)
add_subdirectory(export)
add_subdirectory(footageproperties)
+add_subdirectory(keyframeproperties)
add_subdirectory(preferences)
add_subdirectory(projectproperties)
add_subdirectory(sequence)
diff --git a/app/dialog/keyframeproperties/CMakeLists.txt b/app/dialog/keyframeproperties/CMakeLists.txt
new file mode 100644
index 000000000..fc3b399ec
--- /dev/null
+++ b/app/dialog/keyframeproperties/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/keyframeproperties/keyframeproperties.h
+ dialog/keyframeproperties/keyframeproperties.cpp
+ PARENT_SCOPE
+)
diff --git a/app/dialog/keyframeproperties/keyframeproperties.cpp b/app/dialog/keyframeproperties/keyframeproperties.cpp
new file mode 100644
index 000000000..6507bee66
--- /dev/null
+++ b/app/dialog/keyframeproperties/keyframeproperties.cpp
@@ -0,0 +1,223 @@
+#include "keyframeproperties.h"
+
+#include
+#include
+
+#include "core.h"
+#include "common/timecodefunctions.h"
+#include "widget/keyframeview/keyframeviewundo.h"
+#include "widget/nodeparamview/nodeparamviewundo.h"
+
+KeyframePropertiesDialog::KeyframePropertiesDialog(const QList &keys, const rational &timebase, QWidget *parent) :
+ QDialog(parent),
+ keys_(keys),
+ timebase_(timebase)
+{
+ setWindowTitle(tr("Keyframe Properties"));
+
+ QGridLayout* layout = new QGridLayout(this);
+
+ int row = 0;
+
+ layout->addWidget(new QLabel("Time:"), row, 0);
+
+ time_slider_ = new TimeSlider();
+ time_slider_->SetTimebase(timebase_);
+ layout->addWidget(time_slider_, row, 1);
+
+ row++;
+
+ layout->addWidget(new QLabel("Type:"), row, 0);
+
+ type_select_ = new QComboBox();
+ connect(type_select_, SIGNAL(currentIndexChanged(int)), this, SLOT(KeyTypeChanged(int)));
+ layout->addWidget(type_select_, row, 1);
+
+ row++;
+
+ // Bezier handles
+ bezier_group_ = new QGroupBox();
+
+ QGridLayout* bezier_group_layout = new QGridLayout(bezier_group_);
+
+ bezier_group_layout->addWidget(new QLabel(tr("In:")), 0, 0);
+
+ bezier_in_x_slider_ = new FloatSlider();
+ bezier_group_layout->addWidget(bezier_in_x_slider_, 0, 1);
+
+ bezier_in_y_slider_ = new FloatSlider();
+ bezier_group_layout->addWidget(bezier_in_y_slider_, 0, 2);
+
+ bezier_group_layout->addWidget(new QLabel(tr("Out:")), 1, 0);
+
+ bezier_out_x_slider_ = new FloatSlider();
+ bezier_group_layout->addWidget(bezier_out_x_slider_, 1, 1);
+
+ bezier_out_y_slider_ = new FloatSlider();
+ bezier_group_layout->addWidget(bezier_out_y_slider_, 1, 2);
+
+ layout->addWidget(bezier_group_, row, 0, 1, 2);
+
+ bool all_same_time = true;
+ bool can_set_time = true;
+
+ bool all_same_type = true;
+
+ bool all_same_bezier_in_x = true;
+ bool all_same_bezier_in_y = true;
+ bool all_same_bezier_out_x = true;
+ bool all_same_bezier_out_y = true;
+
+ for (int i=0;i 0) {
+ NodeKeyframePtr prev_key = keys_.at(i-1);
+ NodeKeyframePtr this_key = keys_.at(i);
+
+ // Determine if the keyframes are all the same time or not
+ if (all_same_time) {
+ all_same_time = (prev_key->time() == this_key->time());
+ }
+
+ // Determine if the keyframes are all the same type
+ if (all_same_type) {
+ all_same_type = (prev_key->type() == this_key->type());
+ }
+
+ // Check all four bezier control points
+ if (all_same_bezier_in_x) {
+ all_same_bezier_in_x = (prev_key->bezier_control_in().x() == this_key->bezier_control_in().x());
+ }
+
+ if (all_same_bezier_in_y) {
+ all_same_bezier_in_y = (prev_key->bezier_control_in().y() == this_key->bezier_control_in().y());
+ }
+
+ if (all_same_bezier_out_x) {
+ all_same_bezier_out_x = (prev_key->bezier_control_out().x() == this_key->bezier_control_out().x());
+ }
+
+ if (all_same_bezier_out_y) {
+ all_same_bezier_out_y = (prev_key->bezier_control_out().y() == this_key->bezier_control_out().y());
+ }
+ }
+
+ // Determine if any keyframes are on the same track (in which case we can't set the time)
+ if (can_set_time) {
+ for (int j=0;jtrack() == keys_.at(i)->track());
+ }
+
+ if (!can_set_time) {
+ break;
+ }
+ }
+ }
+
+ if (!all_same_time
+ && !all_same_type
+ && !can_set_time
+ && !all_same_bezier_in_x
+ && !all_same_bezier_in_y
+ && !all_same_bezier_out_x
+ && !all_same_bezier_out_y) {
+ break;
+ }
+ }
+
+ if (all_same_time) {
+ time_slider_->SetValue(Timecode::time_to_timestamp(keys_.first()->time(), timebase_));
+ } else {
+ time_slider_->SetTristate();
+ }
+
+ if (!can_set_time) {
+ time_slider_->setEnabled(false);
+ }
+
+ if (!all_same_type) {
+ // If all keyframes aren't the same type, add an empty item
+ type_select_->addItem(QStringLiteral("--"), -1);
+
+ // Ensure UI updates for the index being 0
+ KeyTypeChanged(0);
+ }
+
+ type_select_->addItem(tr("Linear"), NodeKeyframe::kLinear);
+ type_select_->addItem(tr("Hold"), NodeKeyframe::kHold);
+ type_select_->addItem(tr("Bezier"), NodeKeyframe::kBezier);
+
+ if (all_same_type) {
+ // If all keyframes are the same type, set it here
+ for (int i=0;icount();i++) {
+ if (type_select_->itemData(i).toInt() == keys_.first()->type()) {
+ type_select_->setCurrentIndex(i);
+
+ // Ensure UI updates for this index
+ KeyTypeChanged(i);
+ break;
+ }
+ }
+ }
+
+ SetUpBezierSlider(bezier_in_x_slider_, all_same_bezier_in_x, keys_.first()->bezier_control_in().x());
+ SetUpBezierSlider(bezier_in_y_slider_, all_same_bezier_in_y, keys_.first()->bezier_control_in().y());
+ SetUpBezierSlider(bezier_out_x_slider_, all_same_bezier_out_x, keys_.first()->bezier_control_out().x());
+ SetUpBezierSlider(bezier_out_y_slider_, all_same_bezier_out_y, keys_.first()->bezier_control_out().y());
+
+ row++;
+
+ QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+ buttons->setCenterButtons(true);
+ layout->addWidget(buttons, row, 0, 1, 2);
+ connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
+}
+
+void KeyframePropertiesDialog::accept()
+{
+ QUndoCommand* command = new QUndoCommand();
+
+ rational new_time = Timecode::timestamp_to_time(time_slider_->GetValue(), timebase_);
+ int new_type = type_select_->currentData().toInt();
+
+ foreach (NodeKeyframePtr key, keys_) {
+ if (time_slider_->isEnabled() && !time_slider_->IsTristate()) {
+ new NodeParamSetKeyframeTimeCommand(key, new_time, command);
+ }
+
+ if (new_type > -1) {
+ new KeyframeSetTypeCommand(key, static_cast(new_type), command);
+ }
+
+ if (bezier_group_->isEnabled()) {
+ new KeyframeSetBezierControlPoint(key,
+ NodeKeyframe::kInHandle,
+ QPointF(bezier_in_x_slider_->GetValue(), bezier_in_y_slider_->GetValue()),
+ command);
+
+ new KeyframeSetBezierControlPoint(key,
+ NodeKeyframe::kOutHandle,
+ QPointF(bezier_out_x_slider_->GetValue(), bezier_out_y_slider_->GetValue()),
+ command);
+ }
+ }
+
+ Core::instance()->undo_stack()->pushIfHasChildren(command);
+
+ QDialog::accept();
+}
+
+void KeyframePropertiesDialog::SetUpBezierSlider(FloatSlider *slider, bool all_same, double value)
+{
+ if (all_same) {
+ slider->SetValue(value);
+ } else {
+ slider->SetTristate();
+ }
+}
+
+void KeyframePropertiesDialog::KeyTypeChanged(int index)
+{
+ bezier_group_->setEnabled(type_select_->itemData(index) == NodeKeyframe::kBezier);
+}
diff --git a/app/dialog/keyframeproperties/keyframeproperties.h b/app/dialog/keyframeproperties/keyframeproperties.h
new file mode 100644
index 000000000..415365b5d
--- /dev/null
+++ b/app/dialog/keyframeproperties/keyframeproperties.h
@@ -0,0 +1,47 @@
+#ifndef KEYFRAMEPROPERTIESDIALOG_H
+#define KEYFRAMEPROPERTIESDIALOG_H
+
+#include
+#include
+#include
+
+#include "node/keyframe.h"
+#include "widget/slider/floatslider.h"
+#include "widget/slider/timeslider.h"
+
+class KeyframePropertiesDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ KeyframePropertiesDialog(const QList& keys, const rational& timebase, QWidget* parent = nullptr);
+
+public slots:
+ virtual void accept() override;
+
+private:
+ void SetUpBezierSlider(FloatSlider *slider, bool all_same, double value);
+
+ const QList& keys_;
+
+ rational timebase_;
+
+ TimeSlider* time_slider_;
+
+ QComboBox* type_select_;
+
+ QGroupBox* bezier_group_;
+
+ FloatSlider* bezier_in_x_slider_;
+
+ FloatSlider* bezier_in_y_slider_;
+
+ FloatSlider* bezier_out_x_slider_;
+
+ FloatSlider* bezier_out_y_slider_;
+
+private slots:
+ void KeyTypeChanged(int index);
+
+};
+
+#endif // KEYFRAMEPROPERTIESDIALOG_H