diff --git a/app/node/input.cpp b/app/node/input.cpp
index 5698d5ad2..e7bbf73fc 100644
--- a/app/node/input.cpp
+++ b/app/node/input.cpp
@@ -27,6 +27,7 @@
NodeInput::NodeInput(const QString& id) :
NodeParam(id),
+ keyframable_(true),
keyframing_(false),
dependent_(true),
has_minimum_(false),
@@ -241,6 +242,16 @@ void NodeInput::set_is_keyframing(bool k)
keyframing_ = k;
}
+bool NodeInput::is_keyframable() const
+{
+ return keyframable_;
+}
+
+void NodeInput::set_is_keyframable(bool k)
+{
+ keyframable_ = k;
+}
+
const QVariant &NodeInput::minimum() const
{
return minimum_;
diff --git a/app/node/input.h b/app/node/input.h
index 60dafc870..35b967f5a 100644
--- a/app/node/input.h
+++ b/app/node/input.h
@@ -98,6 +98,16 @@ public:
*/
void set_is_keyframing(bool k);
+ /**
+ * @brief Return whether this input can be keyframed or not
+ */
+ bool is_keyframable() const;
+
+ /**
+ * @brief Set whether this input can be keyframed or not
+ */
+ void set_is_keyframable(bool k);
+
const QVariant& minimum() const;
bool has_minimum() const;
void set_minimum(const QVariant& min);
@@ -122,6 +132,11 @@ private:
*/
DataType data_type_;
+ /**
+ * @brief Internal keyframable value
+ */
+ bool keyframable_;
+
/**
* @brief Internal keyframe array
*
diff --git a/app/ui/icons/icons.cpp b/app/ui/icons/icons.cpp
index 90ef7dc40..e69221ab6 100644
--- a/app/ui/icons/icons.cpp
+++ b/app/ui/icons/icons.cpp
@@ -69,6 +69,8 @@ QIcon olive::icon::Record;
QIcon olive::icon::Add;
QIcon olive::icon::Error;
QIcon olive::icon::DirUp;
+QIcon olive::icon::Clock;
+QIcon olive::icon::Diamond;
void olive::icon::LoadAll(const QString& theme)
{
@@ -116,6 +118,8 @@ void olive::icon::LoadAll(const QString& theme)
Add = Create(theme, "add-button");
Error = Create(theme, "error");
DirUp = Create(theme, "dirup");
+ Clock = Create(theme, "clock");
+ Diamond = Create(theme, "diamond");
}
QIcon olive::icon::Create(const QString& theme, const QString &name)
diff --git a/app/ui/icons/icons.h b/app/ui/icons/icons.h
index 4ecf5a621..56c62b93e 100644
--- a/app/ui/icons/icons.h
+++ b/app/ui/icons/icons.h
@@ -76,6 +76,8 @@ extern QIcon Record;
extern QIcon Add;
extern QIcon Error;
extern QIcon DirUp;
+extern QIcon Clock;
+extern QIcon Diamond;
/**
* @brief Create an icon object loaded from file
diff --git a/app/widget/CMakeLists.txt b/app/widget/CMakeLists.txt
index da75b0919..1de01521b 100644
--- a/app/widget/CMakeLists.txt
+++ b/app/widget/CMakeLists.txt
@@ -20,6 +20,7 @@ add_subdirectory(columnedgridlayout)
add_subdirectory(flowlayout)
add_subdirectory(focusablelineedit)
add_subdirectory(footagecombobox)
+add_subdirectory(keyframeview)
add_subdirectory(menu)
add_subdirectory(nodeview)
add_subdirectory(nodeparamview)
diff --git a/app/widget/keyframeview/CMakeLists.txt b/app/widget/keyframeview/CMakeLists.txt
new file mode 100644
index 000000000..e27cdfb53
--- /dev/null
+++ b/app/widget/keyframeview/CMakeLists.txt
@@ -0,0 +1,24 @@
+# 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}
+ widget/keyframeview/keyframeview.h
+ widget/keyframeview/keyframeview.cpp
+ widget/keyframeview/keyframeviewitem.h
+ widget/keyframeview/keyframeviewitem.cpp
+ PARENT_SCOPE
+)
diff --git a/app/widget/keyframeview/keyframeview.cpp b/app/widget/keyframeview/keyframeview.cpp
new file mode 100644
index 000000000..8c75bc107
--- /dev/null
+++ b/app/widget/keyframeview/keyframeview.cpp
@@ -0,0 +1,27 @@
+#include "keyframeview.h"
+
+#include
+
+KeyframeView::KeyframeView(QWidget *parent) :
+ QWidget(parent)
+{
+ QVBoxLayout* layout = new QVBoxLayout(this);
+ layout->setSpacing(0);
+ layout->setMargin(0);
+
+ ruler_ = new TimeRuler();
+ ruler_->SetTextVisible(true);
+ layout->addWidget(ruler_);
+
+ view_ = new QGraphicsView();
+ view_->setScene(&scene_);
+ view_->setBackgroundRole(QPalette::Base);
+ view_->setAlignment(Qt::AlignLeft | Qt::AlignTop);
+ view_->setDragMode(QGraphicsView::RubberBandDrag);
+ layout->addWidget(view_);
+}
+
+void KeyframeView::SetTimebase(const rational &timebase)
+{
+ ruler_->SetTimebase(timebase);
+}
diff --git a/app/widget/keyframeview/keyframeview.h b/app/widget/keyframeview/keyframeview.h
new file mode 100644
index 000000000..9e9b7ca8e
--- /dev/null
+++ b/app/widget/keyframeview/keyframeview.h
@@ -0,0 +1,26 @@
+#ifndef KEYFRAMEVIEW_H
+#define KEYFRAMEVIEW_H
+
+#include
+
+#include "common/rational.h"
+#include "keyframeviewitem.h"
+#include "widget/timeruler/timeruler.h"
+
+class KeyframeView : public QWidget
+{
+public:
+ KeyframeView(QWidget* parent = nullptr);
+
+ void AddKeyframe(const rational& time, int y);
+
+ void SetTimebase(const rational& timebase);
+
+private:
+ TimeRuler* ruler_;
+ QGraphicsView* view_;
+
+ QGraphicsScene scene_;
+};
+
+#endif // KEYFRAMEVIEW_H
diff --git a/app/widget/keyframeview/keyframeviewitem.cpp b/app/widget/keyframeview/keyframeviewitem.cpp
new file mode 100644
index 000000000..5f724337f
--- /dev/null
+++ b/app/widget/keyframeview/keyframeviewitem.cpp
@@ -0,0 +1,83 @@
+#include "keyframeviewitem.h"
+
+#include
+#include
+#include
+#include
+
+#include "common/qtversionabstraction.h"
+
+KeyframeViewItem::KeyframeViewItem(QGraphicsItem *parent) :
+ QGraphicsRectItem(parent),
+ key_(nullptr),
+ scale_(1.0)
+{
+ keyframe_size_ = QFontMetricsWidth(qApp->fontMetrics(), "Oi");
+ setFlag(QGraphicsItem::ItemIsSelectable);
+ setFlag(QGraphicsItem::ItemIsMovable);
+}
+
+void KeyframeViewItem::SetKeyframe(NodeKeyframe *key)
+{
+ key_ = key;
+ UpdateRect();
+}
+
+void KeyframeViewItem::SetVerticalCenter(int middle)
+{
+ middle_ = middle;
+ UpdateRect();
+}
+
+void KeyframeViewItem::SetScale(double scale)
+{
+ scale_ = scale;
+ UpdateRect();
+}
+
+void KeyframeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
+{
+ if (!key_) {
+ return;
+ }
+
+ painter->setPen(Qt::black);
+
+ if (option->state & QStyle::State_Selected) {
+ painter->setBrush(widget->palette().highlight());
+ } else {
+ painter->setBrush(widget->palette().text());
+ }
+
+ switch (key_->type()) {
+ case NodeKeyframe::kLinear:
+ {
+ QPointF points[] = {
+ QPointF(rect().center().x(), rect().top()),
+ QPointF(rect().right(), rect().center().y()),
+ QPointF(rect().center().x(), rect().bottom()),
+ QPointF(rect().left(), rect().center().y())
+ };
+
+ painter->drawPolygon(points, 4);
+ break;
+ }
+ case NodeKeyframe::kBezier:
+ painter->drawEllipse(rect());
+ break;
+ case NodeKeyframe::kHold:
+ painter->drawRect(rect());
+ break;
+ }
+}
+
+void KeyframeViewItem::UpdateRect()
+{
+ if (!key_) {
+ return;
+ }
+
+ double x_center = key_->time().toDouble() * scale_;
+
+ setRect(x_center - keyframe_size_/2, middle_ - keyframe_size_/2, keyframe_size_, keyframe_size_);
+}
diff --git a/app/widget/keyframeview/keyframeviewitem.h b/app/widget/keyframeview/keyframeviewitem.h
new file mode 100644
index 000000000..9c724d4e9
--- /dev/null
+++ b/app/widget/keyframeview/keyframeviewitem.h
@@ -0,0 +1,34 @@
+#ifndef KEYFRAMEVIEWITEM_H
+#define KEYFRAMEVIEWITEM_H
+
+#include
+
+#include "node/keyframe.h"
+
+class KeyframeViewItem : public QGraphicsRectItem
+{
+public:
+ KeyframeViewItem(QGraphicsItem *parent = nullptr);
+
+ void SetKeyframe(NodeKeyframe* key);
+
+ void SetVerticalCenter(int middle);
+
+ void SetScale(double scale);
+
+protected:
+ virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override;
+
+private:
+ void UpdateRect();
+
+ NodeKeyframe* key_;
+
+ double scale_;
+
+ int middle_;
+
+ int keyframe_size_;
+};
+
+#endif // KEYFRAMEVIEWITEM_H
diff --git a/app/widget/nodeparamview/CMakeLists.txt b/app/widget/nodeparamview/CMakeLists.txt
index a9ab4577b..087779a3d 100644
--- a/app/widget/nodeparamview/CMakeLists.txt
+++ b/app/widget/nodeparamview/CMakeLists.txt
@@ -20,6 +20,10 @@ set(OLIVE_SOURCES
widget/nodeparamview/nodeparamview.cpp
widget/nodeparamview/nodeparamviewitem.h
widget/nodeparamview/nodeparamviewitem.cpp
+ widget/nodeparamview/nodeparamviewkeyframecontrol.h
+ widget/nodeparamview/nodeparamviewkeyframecontrol.cpp
+ widget/nodeparamview/nodeparamviewundo.h
+ widget/nodeparamview/nodeparamviewundo.cpp
widget/nodeparamview/nodeparamviewwidgetbridge.h
widget/nodeparamview/nodeparamviewwidgetbridge.cpp
PARENT_SCOPE
diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp
index 559d525d7..d5a4f2ea1 100644
--- a/app/widget/nodeparamview/nodeparamview.cpp
+++ b/app/widget/nodeparamview/nodeparamview.cpp
@@ -21,6 +21,7 @@
#include "nodeparamview.h"
#include
+#include
NodeParamView::NodeParamView(QWidget *parent) :
QWidget(parent)
@@ -30,10 +31,13 @@ NodeParamView::NodeParamView(QWidget *parent) :
widget_layout->setSpacing(0);
widget_layout->setMargin(0);
+ QSplitter* splitter = new QSplitter(Qt::Horizontal);
+ widget_layout->addWidget(splitter);
+
// Set up scroll area for params
QScrollArea* scroll_area = new QScrollArea();
scroll_area->setWidgetResizable(true);
- widget_layout->addWidget(scroll_area);
+ splitter->addWidget(scroll_area);
// Param widget
QWidget* param_widget_area = new QWidget();
@@ -46,6 +50,13 @@ NodeParamView::NodeParamView(QWidget *parent) :
// Add a stretch to allow empty space at the bottom of the layout
param_layout_->addStretch();
+
+ // Set up keyframe view
+ keyframe_view_ = new KeyframeView();
+ splitter->addWidget(keyframe_view_);
+
+ // Disable collapsing param view (but collapsing keyframe view is permitted)
+ splitter->setCollapsible(0, false);
}
void NodeParamView::SetNodes(QList nodes)
diff --git a/app/widget/nodeparamview/nodeparamview.h b/app/widget/nodeparamview/nodeparamview.h
index 45a94a810..6c4adf0ab 100644
--- a/app/widget/nodeparamview/nodeparamview.h
+++ b/app/widget/nodeparamview/nodeparamview.h
@@ -26,6 +26,7 @@
#include "node/node.h"
#include "nodeparamviewitem.h"
+#include "widget/keyframeview/keyframeview.h"
class NodeParamView : public QWidget
{
@@ -38,6 +39,8 @@ public:
private:
QVBoxLayout* param_layout_;
+ KeyframeView* keyframe_view_;
+
QList nodes_;
QList items_;
diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp
index 741df6177..64ac571cc 100644
--- a/app/widget/nodeparamview/nodeparamviewitem.cpp
+++ b/app/widget/nodeparamview/nodeparamviewitem.cpp
@@ -23,10 +23,14 @@
#include
#include
#include
+#include
#include
+#include "nodeparamviewkeyframecontrol.h"
+#include "nodeparamviewundo.h"
#include "project/item/sequence/sequence.h"
#include "ui/icons/icons.h"
+#include "undo/undostack.h"
NodeParamViewItem::NodeParamViewItem(QWidget *parent) :
QWidget(parent)
@@ -110,6 +114,7 @@ void NodeParamViewItem::SetupUI()
foreach (NodeParam* param, first_node->parameters()) {
// This widget only needs to show input parameters
if (param->type() == NodeParam::kInput) {
+ NodeInput* input = static_cast(param);
// Add descriptor label
QLabel* param_label = new QLabel();
@@ -119,7 +124,7 @@ void NodeParamViewItem::SetupUI()
// Create a widget/input bridge for this input
NodeParamViewWidgetBridge* bridge = new NodeParamViewWidgetBridge(this);
- bridge->AddInput(static_cast(param));
+ bridge->AddInput(input);
bridges_.append(bridge);
// Add widgets for this parameter ot the layout
@@ -128,6 +133,15 @@ void NodeParamViewItem::SetupUI()
content_layout_->addWidget(widgets_for_param.at(i), row_count, i + 1);
}
+ // Add keyframe control to this layout if parameter is keyframable
+ if (input->is_keyframable()) {
+ // Hacky but effective way to make sure this widget is always as far right as possible
+ int control_column = 10;
+
+ NodeParamViewKeyframeControl* key_control = new NodeParamViewKeyframeControl(input);
+ content_layout_->addWidget(key_control, row_count, control_column);
+ }
+
row_count++;
}
}
@@ -181,6 +195,49 @@ void NodeParamViewItem::SetExpanded(bool e)
}
}
+void NodeParamViewItem::KeyframeEnableChanged(bool e)
+{
+ NodeParamViewKeyframeControl* control = static_cast(sender());
+ NodeInput* input = control->GetConnectedInput();
+
+ if (e == input->is_keyframing()) {
+ // No-op
+ return;
+ }
+
+ if (e) {
+ QUndoCommand* command = new QUndoCommand();
+
+ // Enable keyframing
+ new NodeParamSetKeyframing(input, true, command);
+
+ // FIXME: Create a keyframe at this time
+
+ olive::undo_stack.push(command);
+ } else {
+ // Confirm the user wants to clear all keyframes
+ if (QMessageBox::warning(this,
+ tr("Warning"),
+ tr("Are you sure you want to disable keyframing on this value? This will clear all existing keyframes."),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+
+ // Disable keyframing
+ QUndoCommand* command = new QUndoCommand();
+
+ // FIXME: Delete all keyframes
+
+ // Disable keyframing
+ new NodeParamSetKeyframing(input, false, command);
+
+ olive::undo_stack.push(command);
+
+ } else {
+ // Disable action has effectively been ignored
+ control->SetKeyframeEnabled(true);
+ }
+ }
+}
+
NodeParamViewItemTitleBar::NodeParamViewItemTitleBar(QWidget *parent) :
QWidget(parent)
{
diff --git a/app/widget/nodeparamview/nodeparamviewitem.h b/app/widget/nodeparamview/nodeparamviewitem.h
index b327b8945..42923c0d9 100644
--- a/app/widget/nodeparamview/nodeparamviewitem.h
+++ b/app/widget/nodeparamview/nodeparamviewitem.h
@@ -78,6 +78,8 @@ private:
private slots:
void SetExpanded(bool e);
+
+ void KeyframeEnableChanged(bool e);
};
#endif // NODEPARAMVIEWITEM_H
diff --git a/app/widget/nodeparamview/nodeparamviewkeyframecontrol.cpp b/app/widget/nodeparamview/nodeparamviewkeyframecontrol.cpp
new file mode 100644
index 000000000..e533e5b3b
--- /dev/null
+++ b/app/widget/nodeparamview/nodeparamviewkeyframecontrol.cpp
@@ -0,0 +1,72 @@
+#include "nodeparamviewkeyframecontrol.h"
+
+#include
+
+#include "ui/icons/icons.h"
+
+NodeParamViewKeyframeControl::NodeParamViewKeyframeControl(NodeInput* input, QWidget *parent) :
+ QWidget(parent),
+ input_(input)
+{
+ QHBoxLayout* layout = new QHBoxLayout(this);
+ layout->setMargin(0);
+ layout->setSpacing(0);
+
+ // Automatically right aligns all buttons
+ layout->addStretch();
+
+ prev_key_btn_ = CreateNewToolButton(olive::icon::TriLeft);
+ prev_key_btn_->setIconSize(prev_key_btn_->iconSize() / 2);
+ layout->addWidget(prev_key_btn_);
+
+ toggle_key_btn_ = CreateNewToolButton(olive::icon::Diamond);
+ toggle_key_btn_->setCheckable(true);
+ toggle_key_btn_->setIconSize(toggle_key_btn_->iconSize() / 2);
+ layout->addWidget(toggle_key_btn_);
+
+ next_key_btn_ = CreateNewToolButton(olive::icon::TriRight);
+ next_key_btn_->setIconSize(next_key_btn_->iconSize() / 2);
+ layout->addWidget(next_key_btn_);
+
+ enable_key_btn_ = CreateNewToolButton(olive::icon::Clock);
+ enable_key_btn_->setCheckable(true);
+ enable_key_btn_->setIconSize(enable_key_btn_->iconSize() / 4 * 3);
+ layout->addWidget(enable_key_btn_);
+
+ connect(enable_key_btn_, SIGNAL(toggled(bool)), this, SLOT(ShowButtonsFromKeyframeEnable(bool)));
+ connect(enable_key_btn_, SIGNAL(toggled(bool)), this, SIGNAL(KeyframeEnableChanged(bool)));
+
+ // Pick up keyframing value
+ ShowButtonsFromKeyframeEnable(input_->is_keyframing());
+}
+
+NodeInput *NodeParamViewKeyframeControl::GetConnectedInput() const
+{
+ return input_;
+}
+
+void NodeParamViewKeyframeControl::SetKeyframeEnabled(bool e)
+{
+ // Suppress KeyframeEnableChanged() signal from this object
+ blockSignals(true);
+
+ enable_key_btn_->setChecked(e);
+
+ blockSignals(false);
+}
+
+QPushButton *NodeParamViewKeyframeControl::CreateNewToolButton(const QIcon& icon) const
+{
+ QPushButton* btn = new QPushButton();
+ btn->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+ btn->setIcon(icon);
+
+ return btn;
+}
+
+void NodeParamViewKeyframeControl::ShowButtonsFromKeyframeEnable(bool e)
+{
+ prev_key_btn_->setVisible(e);
+ toggle_key_btn_->setVisible(e);
+ next_key_btn_->setVisible(e);
+}
diff --git a/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h b/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h
new file mode 100644
index 000000000..9a71ed4b8
--- /dev/null
+++ b/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h
@@ -0,0 +1,37 @@
+#ifndef NODEPARAMVIEWKEYFRAMECONTROL_H
+#define NODEPARAMVIEWKEYFRAMECONTROL_H
+
+#include
+#include
+
+#include "node/input.h"
+
+class NodeParamViewKeyframeControl : public QWidget
+{
+ Q_OBJECT
+public:
+ NodeParamViewKeyframeControl(NodeInput* input, QWidget* parent = nullptr);
+
+ NodeInput* GetConnectedInput() const;
+
+public slots:
+ void SetKeyframeEnabled(bool e);
+
+signals:
+ void KeyframeEnableChanged(bool);
+
+private:
+ QPushButton* CreateNewToolButton(const QIcon &icon) const;
+
+ QPushButton* prev_key_btn_;
+ QPushButton* toggle_key_btn_;
+ QPushButton* next_key_btn_;
+ QPushButton* enable_key_btn_;
+
+ NodeInput* input_;
+
+private slots:
+ void ShowButtonsFromKeyframeEnable(bool e);
+};
+
+#endif // NODEPARAMVIEWKEYFRAMECONTROL_H
diff --git a/app/widget/nodeparamview/nodeparamviewundo.cpp b/app/widget/nodeparamview/nodeparamviewundo.cpp
new file mode 100644
index 000000000..008608fc7
--- /dev/null
+++ b/app/widget/nodeparamview/nodeparamviewundo.cpp
@@ -0,0 +1,19 @@
+#include "nodeparamviewundo.h"
+
+NodeParamSetKeyframing::NodeParamSetKeyframing(NodeInput *input, bool setting, QUndoCommand *parent) :
+ QUndoCommand(parent),
+ input_(input),
+ setting_(setting)
+{
+ Q_ASSERT(setting != input_->is_keyframing());
+}
+
+void NodeParamSetKeyframing::redo()
+{
+ input_->set_is_keyframing(setting_);
+}
+
+void NodeParamSetKeyframing::undo()
+{
+ input_->set_is_keyframing(!setting_);
+}
diff --git a/app/widget/nodeparamview/nodeparamviewundo.h b/app/widget/nodeparamview/nodeparamviewundo.h
new file mode 100644
index 000000000..e13b82b66
--- /dev/null
+++ b/app/widget/nodeparamview/nodeparamviewundo.h
@@ -0,0 +1,20 @@
+#ifndef NODEPARAMVIEWUNDO_H
+#define NODEPARAMVIEWUNDO_H
+
+#include
+
+#include "node/input.h"
+
+class NodeParamSetKeyframing : public QUndoCommand {
+public:
+ NodeParamSetKeyframing(NodeInput* input, bool setting, QUndoCommand* parent = nullptr);
+
+ virtual void redo() override;
+ virtual void undo() override;
+
+private:
+ NodeInput* input_;
+ bool setting_;
+};
+
+#endif // NODEPARAMVIEWUNDO_H