/***
Olive - Non-Linear Video Editor
Copyright (C) 2020 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 .
***/
#include "keyframeviewbase.h"
#include
#include
#include
#include "dialog/keyframeproperties/keyframeproperties.h"
#include "keyframeviewundo.h"
#include "node/node.h"
#include "widget/menu/menu.h"
#include "widget/menu/menushared.h"
#include "widget/nodeparamview/nodeparamviewundo.h"
namespace olive {
KeyframeViewBase::KeyframeViewBase(QWidget *parent) :
TimeBasedView(parent),
dragging_bezier_point_(nullptr),
currently_autoselecting_(false)
{
SetDefaultDragMode(RubberBandDrag);
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &KeyframeViewBase::customContextMenuRequested, this, &KeyframeViewBase::ShowContextMenu);
connect(scene(), &QGraphicsScene::selectionChanged, this, &KeyframeViewBase::AutoSelectKeyTimeNeighbors);
}
void KeyframeViewBase::Clear()
{
QMap::iterator iterator;
for (iterator=item_map_.begin();iterator!=item_map_.end();iterator++) {
delete iterator.value();
}
item_map_.clear();
}
void KeyframeViewBase::DeleteSelected()
{
QUndoCommand* command = new QUndoCommand();
QMap::const_iterator i;
for (i=item_map_.constBegin(); i!=item_map_.constEnd(); i++) {
if (i.value()->isSelected()) {
new NodeParamRemoveKeyframeCommand(i.key(), command);
}
}
Core::instance()->undo_stack()->pushIfHasChildren(command);
}
void KeyframeViewBase::RemoveKeyframesOfNode(Node *n)
{
foreach (NodeInput* i, n->inputs()) {
RemoveKeyframesOfInput(i);
}
}
void KeyframeViewBase::RemoveKeyframesOfInput(NodeInput *input)
{
for (int i=-1; iArraySize(); i++) {
foreach (const NodeKeyframeTrack& track, input->GetKeyframeTracks(i)) {
foreach (NodeKeyframe* key, track) {
RemoveKeyframe(key);
}
}
}
}
void KeyframeViewBase::SelectAll()
{
for (auto it=item_map_.cbegin(); it!=item_map_.cend(); it++) {
it.value()->setSelected(true);
}
}
void KeyframeViewBase::DeselectAll()
{
for (auto it=item_map_.cbegin(); it!=item_map_.cend(); it++) {
it.value()->setSelected(false);
}
}
void KeyframeViewBase::RemoveKeyframe(NodeKeyframe* key)
{
KeyframeAboutToBeRemoved(key);
delete item_map_.take(key);
}
KeyframeViewItem *KeyframeViewBase::AddKeyframeInternal(NodeKeyframe* key)
{
KeyframeViewItem* item = item_map_.value(key);
if (!item) {
item = new KeyframeViewItem(key);
item->SetTimeTarget(GetTimeTarget());
item->SetScale(GetScale());
item_map_.insert(key, item);
scene()->addItem(item);
}
return item;
}
void KeyframeViewBase::mousePressEvent(QMouseEvent *event)
{
QGraphicsItem* item_under_cursor = itemAt(event->pos());
if (HandPress(event) || (!item_under_cursor && PlayheadPress(event))) {
return;
}
active_tool_ = Core::instance()->tool();
if (event->button() == Qt::LeftButton) {
QGraphicsView::mousePressEvent(event);
if (active_tool_ == Tool::kPointer) {
if (item_under_cursor) {
drag_start_ = event->pos();
// Determine what type of item is under the cursor
dragging_bezier_point_ = dynamic_cast(item_under_cursor);
if (dragging_bezier_point_) {
dragging_bezier_point_start_ = dragging_bezier_point_->GetCorrespondingKeyframeHandle();
dragging_bezier_point_opposing_start_ = dragging_bezier_point_->key()->bezier_control(NodeKeyframe::get_opposing_bezier_type(dragging_bezier_point_->mode()));
} else {
QList selected_items = scene()->selectedItems();
selected_keys_.resize(selected_items.size());
initial_drag_item_ = static_cast(item_under_cursor);
for (int i=0;i(selected_items.at(i));
selected_keys_.replace(i, {key,
key->x(),
GetAdjustedTime(key->key()->parent()->parent(), GetTimeTarget(), key->key()->time(), false),
key->key()->value().toDouble()});
}
}
}
}
}
}
void KeyframeViewBase::mouseMoveEvent(QMouseEvent *event)
{
if (HandMove(event) || PlayheadMove(event)) {
return;
}
if (event->buttons() & Qt::LeftButton) {
QGraphicsView::mouseMoveEvent(event);
if (active_tool_ == Tool::kPointer) {
// Calculate cursor difference and scale it
QPointF mouse_diff_scaled = GetScaledCursorPos(event->pos() - drag_start_);
if (dragging_bezier_point_) {
ProcessBezierDrag(mouse_diff_scaled,
!(event->modifiers() & Qt::ControlModifier),
false);
} else if (!selected_keys_.isEmpty()) {
foreach (const KeyframeItemAndTime& keypair, selected_keys_) {
//NodeInput* input_parent = keypair.key->key()->parent();
//input_parent->blockSignals(true);
rational node_time = GetAdjustedTime(GetTimeTarget(),
keypair.key->key()->parent()->parent(),
CalculateNewTimeFromScreen(keypair.time, mouse_diff_scaled.x()),
true);
keypair.key->key()->set_time(node_time);
if (IsYAxisEnabled()) {
keypair.key->key()->set_value(keypair.value - mouse_diff_scaled.y());
}
// We emit a custom value changed signal while the keyframe is being dragged so only the currently viewed
// frame gets rendered in this time
//input_parent->blockSignals(false);
//input_parent->parentNode()->InvalidateVisible(input_parent, input_parent);
}
// Show information about this keyframe
QString tip = Timecode::time_to_timecode(initial_drag_item_->key()->time(), timebase(),
Core::instance()->GetTimecodeDisplay(), false);
if (IsYAxisEnabled()) {
bool ok;
double num_value = initial_drag_item_->key()->value().toDouble(&ok);
if (ok) {
tip.append('\n');
tip.append(QString::number(num_value));
}
}
QToolTip::hideText();
QToolTip::showText(QCursor::pos(), tip);
}
}
}
}
void KeyframeViewBase::mouseReleaseEvent(QMouseEvent *event)
{
if (HandRelease(event) || PlayheadRelease(event)) {
return;
}
if (event->button() == Qt::LeftButton) {
QGraphicsView::mouseReleaseEvent(event);
if (active_tool_ == Tool::kPointer) {
QPoint mouse_diff = event->pos() - drag_start_;
QPointF mouse_diff_scaled = GetScaledCursorPos(mouse_diff);
if (!mouse_diff.isNull()) {
if (dragging_bezier_point_) {
ProcessBezierDrag(mouse_diff_scaled,
!(event->modifiers() & Qt::ControlModifier),
true);
dragging_bezier_point_ = nullptr;
} else if (!selected_keys_.isEmpty()) {
QUndoCommand* command = new QUndoCommand();
foreach (const KeyframeItemAndTime& keypair, selected_keys_) {
KeyframeViewItem* item = keypair.key;
keypair.key->key()->parent()->blockSignals(true);
// Calculate the new time for this keyframe
rational node_time = GetAdjustedTime(GetTimeTarget(),
keypair.key->key()->parent()->parent(),
CalculateNewTimeFromScreen(keypair.time, mouse_diff_scaled.x()),
true);
// Commit movement
// Since we overrode the cache signalling while dragging, we simulate here precisely the change that
// occurred by first setting the keyframe to its original position, and then letting the input handle
// the signalling once the undo command is pushed.
item->key()->set_time(keypair.time);
new NodeParamSetKeyframeTimeCommand(item->key(),
node_time,
keypair.time,
command);
// Commit value if we're setting a value
if (IsYAxisEnabled()) {
item->key()->set_value(keypair.value);
new NodeParamSetKeyframeValueCommand(item->key(),
keypair.value - mouse_diff_scaled.y(),
keypair.value,
command);
}
keypair.key->key()->parent()->blockSignals(false);
}
Core::instance()->undo_stack()->push(command);
}
}
selected_keys_.clear();
}
}
}
void KeyframeViewBase::ScaleChangedEvent(const double &scale)
{
TimeBasedView::ScaleChangedEvent(scale);
QMap::const_iterator iterator;
for (iterator=item_map_.begin();iterator!=item_map_.end();iterator++) {
iterator.value()->SetScale(scale);
}
}
const QMap &KeyframeViewBase::item_map() const
{
return item_map_;
}
void KeyframeViewBase::KeyframeAboutToBeRemoved(NodeKeyframe *)
{
}
void KeyframeViewBase::TimeTargetChangedEvent(Node *target)
{
QMap::const_iterator i;
for (i=item_map_.begin();i!=item_map_.end();i++) {
i.value()->SetTimeTarget(target);
}
}
void KeyframeViewBase::ContextMenuEvent(Menu& m)
{
Q_UNUSED(m)
}
rational KeyframeViewBase::CalculateNewTimeFromScreen(const rational &old_time, double cursor_diff)
{
return rational::fromDouble(old_time.toDouble() + cursor_diff);
}
QPointF KeyframeViewBase::GenerateBezierControlPosition(const NodeKeyframe::BezierType mode, const QPointF &start_point, const QPointF &scaled_cursor_diff)
{
QPointF new_bezier_pos = start_point;
new_bezier_pos += scaled_cursor_diff;
// LIMIT bezier handles from overlapping each other
if (mode == NodeKeyframe::kInHandle) {
if (new_bezier_pos.x() > 0) {
new_bezier_pos.setX(0);
}
} else {
if (new_bezier_pos.x() < 0) {
new_bezier_pos.setX(0);
}
}
return new_bezier_pos;
}
void KeyframeViewBase::ProcessBezierDrag(QPointF mouse_diff_scaled, bool include_opposing, bool undoable)
{
// Flip the mouse Y because bezier control points are drawn bottom to top, not top to bottom
mouse_diff_scaled.setY(-mouse_diff_scaled.y());
QPointF new_bezier_pos = GenerateBezierControlPosition(dragging_bezier_point_->mode(),
dragging_bezier_point_start_,
mouse_diff_scaled);
// If the user is NOT holding control, we set the other handle to the exact negative of this handle
QPointF new_opposing_pos;
NodeKeyframe::BezierType opposing_type = NodeKeyframe::get_opposing_bezier_type(dragging_bezier_point_->mode());
if (include_opposing) {
new_opposing_pos = GenerateBezierControlPosition(opposing_type,
dragging_bezier_point_opposing_start_,
-mouse_diff_scaled);
} else {
new_opposing_pos = dragging_bezier_point_opposing_start_;
}
//NodeInput* input_parent = dragging_bezier_point_->key()->parent();
if (undoable) {
QUndoCommand* command = new QUndoCommand();
// Similar to the code in MouseRelease, we manipulated the signalling earlier and need to set the keys back to their
// original position to allow the input to signal correctly when the undo command is pushed.
//input_parent->blockSignals(true);
dragging_bezier_point_->key()->set_bezier_control(dragging_bezier_point_->mode(),
dragging_bezier_point_start_);
new KeyframeSetBezierControlPoint(dragging_bezier_point_->key(),
dragging_bezier_point_->mode(),
new_bezier_pos,
dragging_bezier_point_start_,
command);
if (include_opposing) {
dragging_bezier_point_->key()->set_bezier_control(opposing_type,
dragging_bezier_point_opposing_start_);
new KeyframeSetBezierControlPoint(dragging_bezier_point_->key(),
opposing_type,
new_opposing_pos,
dragging_bezier_point_opposing_start_,
command);
}
//input_parent->blockSignals(false);
Core::instance()->undo_stack()->push(command);
} else {
//input_parent->blockSignals(true);
dragging_bezier_point_->key()->set_bezier_control(dragging_bezier_point_->mode(),
new_bezier_pos);
dragging_bezier_point_->key()->set_bezier_control(opposing_type,
new_opposing_pos);
//input_parent->blockSignals(false);
//input_parent->parentNode()->InvalidateVisible(input_parent, input_parent);
}
}
QPointF KeyframeViewBase::GetScaledCursorPos(const QPoint &cursor_pos)
{
return QPointF(static_cast(cursor_pos.x()) / GetScale(),
static_cast(cursor_pos.y()) / GetYScale());
}
void KeyframeViewBase::ShowContextMenu()
{
Menu m;
MenuShared::instance()->AddItemsForEditMenu(&m, false);
QAction* linear_key_action = nullptr;
QAction* bezier_key_action = nullptr;
QAction* hold_key_action = nullptr;
QList items = scene()->selectedItems();
if (!items.isEmpty()) {
bool all_keys_are_same_type = true;
NodeKeyframe::Type type = static_cast(items.first())->key()->type();
for (int i=1;i(items.at(i));
KeyframeViewItem* prev_item = static_cast(items.at(i-1));
if (key_item->key()->type() != prev_item->key()->type()) {
all_keys_are_same_type = false;
break;
}
}
m.addSeparator();
linear_key_action = m.addAction(tr("Linear"));
bezier_key_action = m.addAction(tr("Bezier"));
hold_key_action = m.addAction(tr("Hold"));
if (all_keys_are_same_type) {
switch (type) {
case NodeKeyframe::kLinear:
linear_key_action->setChecked(true);
break;
case NodeKeyframe::kBezier:
bezier_key_action->setChecked(true);
break;
case NodeKeyframe::kHold:
hold_key_action->setChecked(true);
break;
}
}
}
ContextMenuEvent(m);
if (!items.isEmpty()) {
m.addSeparator();
QAction* properties_action = m.addAction(tr("P&roperties"));
connect(properties_action, &QAction::triggered, this, &KeyframeViewBase::ShowKeyframePropertiesDialog);
}
QAction* selected = m.exec(QCursor::pos());
// Process keyframe type changes
if (!items.isEmpty()) {
if (selected == linear_key_action
|| selected == bezier_key_action
|| selected == hold_key_action) {
NodeKeyframe::Type new_type;
if (selected == hold_key_action) {
new_type = NodeKeyframe::kHold;
} else if (selected == bezier_key_action) {
new_type = NodeKeyframe::kBezier;
} else {
new_type = NodeKeyframe::kLinear;
}
QUndoCommand* command = new QUndoCommand();
foreach (QGraphicsItem* item, items) {
new KeyframeSetTypeCommand(static_cast(item)->key(),
new_type,
command);
}
Core::instance()->undo_stack()->pushIfHasChildren(command);
}
}
}
void KeyframeViewBase::ShowKeyframePropertiesDialog()
{
QList items = scene()->selectedItems();
QVector keys;
foreach (QGraphicsItem* item, items) {
keys.append(static_cast(item)->key());
}
if (!keys.isEmpty()) {
KeyframePropertiesDialog kd(keys, timebase(), this);
kd.exec();
}
}
void KeyframeViewBase::AutoSelectKeyTimeNeighbors()
{
if (currently_autoselecting_ || IsYAxisEnabled()) {
return;
}
// Prevents infinite loop
currently_autoselecting_ = true;
QList selected_items = scene()->selectedItems();
foreach (QGraphicsItem* g, selected_items) {
KeyframeViewItem* key_item = static_cast(g);
rational key_time = key_item->key()->time();
QVector keys = key_item->key()->parent()->GetKeyframesAtTime(key_time, key_item->key()->element());
foreach (NodeKeyframe* k, keys) {
if (k == key_item->key()) {
continue;
}
// Ensure this key is not already selected
KeyframeViewItem* item = item_map_.value(k);
item->setSelected(true);
}
}
currently_autoselecting_ = false;
}
}