/*** Olive - Non-Linear Video Editor Copyright (C) 2021 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 "curveview.h" #include #include #include #include #include #include "common/qtutils.h" namespace olive { #define super KeyframeViewBase CurveView::CurveView(QWidget *parent) : KeyframeViewBase(parent) { setAlignment(Qt::AlignLeft | Qt::AlignVCenter); setDragMode(RubberBandDrag); setViewportUpdateMode(FullViewportUpdate); SetYAxisEnabled(true); text_padding_ = QtUtils::QFontMetricsWidth(fontMetrics(), QStringLiteral("i")); minimum_grid_space_ = QtUtils::QFontMetricsWidth(fontMetrics(), QStringLiteral("00000")); connect(scene(), &QGraphicsScene::selectionChanged, this, &CurveView::SelectionChanged); } CurveView::~CurveView() { // Quick way to avoid segfault when QGraphicsScene::selectionChanged is emitted after other members have been destroyed Clear(); } void CurveView::Clear() { KeyframeViewBase::Clear(); foreach (QGraphicsLineItem* line, lines_) { delete line; } lines_.clear(); } void CurveView::ConnectInput(const NodeKeyframeTrackReference& ref) { if (connected_inputs_.contains(ref)) { // Input wasn't connected, do nothing return; } // Add keyframes from track AddKeyframesOfTrack(ref); // Append to the list connected_inputs_.append(ref); } void CurveView::DisconnectInput(const NodeKeyframeTrackReference& ref) { if (!connected_inputs_.contains(ref)) { // Input wasn't connected, do nothing return; } // Remove keyframes belonging to this element and track RemoveKeyframesOfTrack(ref); // Remove from the list connected_inputs_.removeOne(ref); } void CurveView::SelectKeyframesOfInput(const NodeKeyframeTrackReference& ref) { DeselectAll(); for (auto it=item_map().cbegin(); it!=item_map().cend(); it++) { if (it.key()->key_track_ref() == ref) { it.value()->setSelected(true); } } } void CurveView::ZoomToFitInput(const NodeKeyframeTrackReference& ref) { QList keys; for (auto it=item_map().cbegin(); it!=item_map().cend(); it++) { if (it.key()->key_track_ref() == ref) { keys.append(it.key()); } } ZoomToFitInternal(keys); } void CurveView::SetKeyframeTrackColor(const NodeKeyframeTrackReference &ref, const QColor &color) { // Insert color into hashmap keyframe_colors_.insert(ref, color); // Update all keyframes for (auto it=item_map().cbegin(); it!=item_map().cend(); it++) { if (it.key()->key_track_ref() == ref) { it.value()->SetOverrideBrush(color); } } } void CurveView::drawBackground(QPainter *painter, const QRectF &rect) { if (timebase().isNull()) { return; } painter->setRenderHint(QPainter::Antialiasing); QVector lines; double x_interval = timebase().flipped().toDouble(); double y_interval = 100.0; int x_grid_interval, y_grid_interval; painter->setPen(QPen(palette().window().color(), 1)); do { x_grid_interval = qRound(x_interval * GetScale() * timebase_dbl()); x_interval *= 2.0; } while (x_grid_interval < minimum_grid_space_); do { y_grid_interval = qRound(y_interval * GetYScale()); y_interval *= 2.0; } while (y_grid_interval < minimum_grid_space_); int x_start = qCeil(rect.left() / x_grid_interval) * x_grid_interval; int y_start = qCeil(rect.top() / y_grid_interval) * y_grid_interval; QPointF scene_bottom_left = mapToScene(QPoint(0, qRound(rect.height()))); QPointF scene_top_right = mapToScene(QPoint(qRound(rect.width()), 0)); // Add vertical lines for (int i=x_start;i(i) / GetScale() / timebase_dbl()); painter->drawText(i + text_padding_, qRound(scene_bottom_left.y()) - text_padding_, QString::number(value)); lines.append(QLine(i, qRound(rect.top()), i, qRound(rect.bottom()))); } // Add horizontal lines for (int i=y_start;i(i) / GetYScale()); painter->drawText(qRound(scene_bottom_left.x()) + text_padding_, i - text_padding_, QString::number(-value)); lines.append(QLine(qRound(rect.left()), i, qRound(rect.right()), i)); } // Draw grid painter->drawLines(lines); // Draw keyframe lines foreach (const NodeKeyframeTrackReference& ref, connected_inputs_) { Node* node = ref.input().node(); const QString& input = ref.input().input(); if (node->IsInputKeyframing(input, ref.input().element())) { const QVector& tracks = node->GetKeyframeTracks(ref.input()); const NodeKeyframeTrack& track = tracks.at(ref.track()); if (!track.isEmpty()) { painter->setPen(QPen(keyframe_colors_.value(ref), qMax(1, fontMetrics().height() / 4))); // Create a path QPainterPath path; // Draw straight line leading to first keyframe QPointF first_key_pos = item_map().value(track.first())->pos(); path.moveTo(QPointF(scene_bottom_left.x(), first_key_pos.y())); path.lineTo(first_key_pos); // Draw lines between each keyframe for (int i=1;itype() == NodeKeyframe::kHold) { // Draw a hold keyframe (basically a right angle) path.lineTo(after_item->pos().x(), before_item->pos().y()); path.lineTo(after_item->pos().x(), after_item->pos().y()); } else if (before->type() == NodeKeyframe::kBezier && after->type() == NodeKeyframe::kBezier) { // Draw a cubic bezier // Cubic beziers have two control points, so we can just use both QPointF before_control_point = before_item->pos() + ScalePoint(before->valid_bezier_control_out()); QPointF after_control_point = after_item->pos() + ScalePoint(after->valid_bezier_control_in()); path.cubicTo(before_control_point, after_control_point, after_item->pos()); } else if (before->type() == NodeKeyframe::kBezier || after->type() == NodeKeyframe::kBezier) { // Draw a quadratic bezier // Quadratic beziers have a single control point, we just have to determine which it is QPointF key_anchor; QPointF control_point; if (before->type() == NodeKeyframe::kBezier) { key_anchor = before_item->pos(); control_point = before->valid_bezier_control_out(); } else { key_anchor = after_item->pos(); control_point = after->valid_bezier_control_in(); } // Scale control point control_point = key_anchor + ScalePoint(control_point); // Create the path from both keyframes path.quadTo(control_point, after_item->pos()); } else { // Linear to linear path.lineTo(after_item->pos()); } } // Draw straight line leading from end keyframe QPointF last_key_pos = item_map().value(track.last())->pos(); path.lineTo(QPointF(scene_top_right.x(), last_key_pos.y())); painter->drawPath(path); } } } // Draw bezier control point lines if (!bezier_control_points_.isEmpty()) { painter->setPen(QPen(palette().text().color(), 1)); QVector bezier_lines; foreach (BezierControlPointItem* item, bezier_control_points_) { // All BezierControlPointItems should be children of a KeyframeViewItem KeyframeViewItem* par = static_cast(item->parentItem()); bezier_lines.append(QLineF(par->pos(), par->pos() + item->pos())); } painter->drawLines(bezier_lines); } } void CurveView::KeyframeAboutToBeRemoved(NodeKeyframe *key) { disconnect(key, &NodeKeyframe::ValueChanged, this, &CurveView::KeyframeValueChanged); disconnect(key, &NodeKeyframe::TypeChanged, this, &CurveView::KeyframeTypeChanged); } void CurveView::ScaleChangedEvent(const double& scale) { KeyframeViewBase::ScaleChangedEvent(scale); foreach (BezierControlPointItem* item, bezier_control_points_) { item->SetXScale(scale); } } void CurveView::VerticalScaleChangedEvent(double scale) { Q_UNUSED(scale) for (auto iterator=item_map().begin();iterator!=item_map().end();iterator++) { SetItemYFromKeyframeValue(iterator.value()->key(), iterator.value()); } foreach (BezierControlPointItem* item, bezier_control_points_) { item->SetYScale(scale); } } void CurveView::ContextMenuEvent(Menu &m) { m.addSeparator(); // View settings QAction* zoom_fit_action = m.addAction(tr("Zoom to Fit")); connect(zoom_fit_action, &QAction::triggered, this, &CurveView::ZoomToFit); QAction* zoom_fit_selected_action = m.addAction(tr("Zoom to Fit Selected")); connect(zoom_fit_selected_action, &QAction::triggered, this, &CurveView::ZoomToFitSelected); QAction* reset_zoom_action = m.addAction(tr("Reset Zoom")); connect(reset_zoom_action, &QAction::triggered, this, &CurveView::ResetZoom); } void CurveView::ZoomToFitInternal(const QList &keys) { if (keys.isEmpty()) { // Prevent scaling to DBL_MIN/DBL_MAX return; } rational min_time = RATIONAL_MAX; rational max_time = RATIONAL_MIN; double min_val = DBL_MAX; double max_val = DBL_MIN; foreach (NodeKeyframe* key, keys) { rational transformed_time = GetAdjustedTime(key->parent(), GetTimeTarget(), key->time(), false); min_time = qMin(transformed_time, min_time); max_time = qMax(transformed_time, max_time); min_val = qMin(key->value().toDouble(), min_val); max_val = qMax(key->value().toDouble(), max_val); } double time_range = max_time.toDouble() - min_time.toDouble(); double new_x_scale = CalculateScaleFromDimensions(this->width(), time_range); double new_y_scale = CalculateScaleFromDimensions(this->height(), max_val - min_val); emit ScaleChanged(new_x_scale); SetYScale(new_y_scale); QMetaObject::invokeMethod(horizontalScrollBar(), "setValue", Qt::QueuedConnection, Q_ARG(int, TimeToScene(min_time) - CalculatePaddingFromDimensionScale(this->width()))); QMetaObject::invokeMethod(verticalScrollBar(), "setValue", Qt::QueuedConnection, Q_ARG(int, GetItemYFromKeyframeValue(max_val) - CalculatePaddingFromDimensionScale(this->height()))); } qreal CurveView::GetItemYFromKeyframeValue(NodeKeyframe *key) { return GetItemYFromKeyframeValue(key->value().toDouble()); } qreal CurveView::GetItemYFromKeyframeValue(double value) { return -value * GetYScale(); } void CurveView::SetItemYFromKeyframeValue(NodeKeyframe *key, KeyframeViewItem *item) { item->SetOverrideY(GetItemYFromKeyframeValue(key)); } QPointF CurveView::ScalePoint(const QPointF &point) { // Flips Y coordinate because curves are drawn bottom to top return QPointF(point.x() * GetScale(), - point.y() * GetYScale()); } void CurveView::CreateBezierControlPoints(KeyframeViewItem* item) { BezierControlPointItem* bezier_in_pt = new BezierControlPointItem(item->key(), NodeKeyframe::kInHandle, item); bezier_in_pt->SetXScale(GetScale()); bezier_in_pt->SetYScale(GetYScale()); bezier_control_points_.append(bezier_in_pt); connect(bezier_in_pt, &QObject::destroyed, this, &CurveView::BezierControlPointDestroyed, Qt::DirectConnection); BezierControlPointItem* bezier_out_pt = new BezierControlPointItem(item->key(), NodeKeyframe::kOutHandle, item); bezier_out_pt->SetXScale(GetScale()); bezier_out_pt->SetYScale(GetYScale()); bezier_control_points_.append(bezier_out_pt); connect(bezier_out_pt, &QObject::destroyed, this, &CurveView::BezierControlPointDestroyed, Qt::DirectConnection); } void CurveView::KeyframeValueChanged() { NodeKeyframe* key = static_cast(sender()); KeyframeViewItem* item = item_map().value(key); SetItemYFromKeyframeValue(key, item); } void CurveView::KeyframeTypeChanged() { NodeKeyframe* key = static_cast(sender()); KeyframeViewItem* item = item_map().value(key); if (item->isSelected()) { item->setSelected(false); item->setSelected(true); } } void CurveView::SelectionChanged() { // Clear current bezier handles while (!bezier_control_points_.isEmpty()) { delete bezier_control_points_.first(); } QList selected = scene()->selectedItems(); foreach (QGraphicsItem* item, selected) { KeyframeViewItem* this_item = static_cast(item); if (this_item->key()->type() == NodeKeyframe::kBezier) { CreateBezierControlPoints(this_item); } } } void CurveView::BezierControlPointDestroyed() { BezierControlPointItem* item = static_cast(sender()); bezier_control_points_.removeOne(item); } void CurveView::ZoomToFit() { ZoomToFitInternal(item_map().keys()); } void CurveView::ZoomToFitSelected() { QList selected_keys; for (auto it=item_map().cbegin(); it!=item_map().cend(); it++) { if (it.value()->isSelected()) { selected_keys.append(it.key()); } } ZoomToFitInternal(selected_keys); } void CurveView::ResetZoom() { emit ScaleChanged(1.0); SetYScale(1.0); } KeyframeViewItem* CurveView::AddKeyframe(NodeKeyframe* key) { KeyframeViewItem* item = super::AddKeyframe(key); SetItemYFromKeyframeValue(key, item); item->SetOverrideBrush(keyframe_colors_.value(key->key_track_ref())); connect(key, &NodeKeyframe::ValueChanged, this, &CurveView::KeyframeValueChanged); connect(key, &NodeKeyframe::TypeChanged, this, &CurveView::KeyframeTypeChanged); return item; } }