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