curve/timeline: improved user interface behavior

* Implements "auto-fit" setting for curve view and sets it on open.
* Improves scroll zooming on all TimelineViewBase derivatives.
* Improves code sharing for better maintenance.
This commit is contained in:
itsmattkc
2020-06-23 05:25:34 +10:00
parent e681b87988
commit 8bf653925f
11 changed files with 198 additions and 82 deletions
+53 -33
View File
@@ -20,6 +20,7 @@
#include "curveview.h"
#include <QScrollBar>
#include <QMouseEvent>
#include <QtMath>
@@ -251,42 +252,22 @@ void CurveView::VerticalScaleChangedEvent(double scale)
void CurveView::wheelEvent(QWheelEvent *event)
{
if (WheelEventIsAZoomEvent(event)) {
if (!event->angleDelta().isNull()) {
bool only_vertical = false;
bool only_horizontal = false;
if (event->modifiers() & Qt::ShiftModifier) {
if (event->modifiers() & Qt::AltModifier) {
only_horizontal = true;
} else {
only_vertical = true;
}
}
if (event->angleDelta().x() + event->angleDelta().y() > 0) {
if (!only_vertical) {
emit ScaleChanged(GetScale() * 1.1);
}
if (!only_horizontal) {
SetYScale(GetYScale() * 1.1);
}
} else {
if (!only_vertical) {
emit ScaleChanged(GetScale() * 0.9);
}
if (!only_horizontal) {
SetYScale(GetYScale() *0.9);
}
}
}
} else {
if (!HandleZoomFromScroll(event)) {
KeyframeViewBase::wheelEvent(event);
}
}
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* reset_zoom_action = m.addAction(tr("Reset Zoom"));
}
QList<NodeKeyframe *> CurveView::GetKeyframesSortedByTime(int track)
{
QList<NodeKeyframe *> sorted;
@@ -320,7 +301,12 @@ QList<NodeKeyframe *> CurveView::GetKeyframesSortedByTime(int track)
qreal CurveView::GetItemYFromKeyframeValue(NodeKeyframe *key)
{
return -key->value().toDouble() * GetYScale();
return GetItemYFromKeyframeValue(key->value().toDouble());
}
qreal CurveView::GetItemYFromKeyframeValue(double value)
{
return -value * GetYScale();
}
void CurveView::SetItemYFromKeyframeValue(NodeKeyframe *key, KeyframeViewItem *item)
@@ -402,6 +388,40 @@ void CurveView::BezierControlPointDestroyed()
bezier_control_points_.removeOne(item);
}
void CurveView::ZoomToFit()
{
if (item_map().isEmpty()) {
// Prevent scaling to DBL_MIN/DBL_MAX
return;
}
QMap<NodeKeyframe *, KeyframeViewItem *>::const_iterator i;
rational min_time = RATIONAL_MAX;
rational max_time = RATIONAL_MIN;
double min_val = DBL_MAX;
double max_val = DBL_MIN;
for (i=item_map().constBegin(); i!=item_map().constEnd(); i++) {
min_time = qMin(i.key()->time(), min_time);
max_time = qMax(i.key()->time(), max_time);
min_val = qMin(i.key()->value().toDouble(), min_val);
max_val = qMax(i.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);
horizontalScrollBar()->setValue(TimeToScene(min_time) - CalculatePaddingFromDimensionScale(this->width()));
verticalScrollBar()->setValue(GetItemYFromKeyframeValue(max_val) - CalculatePaddingFromDimensionScale(this->height()));
}
void CurveView::AddKeyframe(NodeKeyframePtr key)
{
KeyframeViewItem* item = AddKeyframeInternal(key);
+6
View File
@@ -30,6 +30,7 @@ OLIVE_NAMESPACE_ENTER
class CurveView : public KeyframeViewBase
{
Q_OBJECT
public:
CurveView(QWidget* parent = nullptr);
@@ -44,6 +45,8 @@ public:
public slots:
void AddKeyframe(NodeKeyframePtr key);
void ZoomToFit();
protected:
virtual void drawBackground(QPainter* painter, const QRectF& rect) override;
@@ -55,10 +58,13 @@ protected:
virtual void wheelEvent(QWheelEvent* event) override;
virtual void ContextMenuEvent(Menu &m) override;
private:
QList<NodeKeyframe*> GetKeyframesSortedByTime(int track);
qreal GetItemYFromKeyframeValue(NodeKeyframe* key);
qreal GetItemYFromKeyframeValue(double value);
void SetItemYFromKeyframeValue(NodeKeyframe* key, KeyframeViewItem* item);
+2
View File
@@ -169,6 +169,8 @@ void CurveWidget::SetInput(NodeInput *input)
}
UpdateInputLabel();
QMetaObject::invokeMethod(view_, "ZoomToFit", Qt::QueuedConnection);
}
const double &CurveWidget::GetVerticalScale()
+13 -31
View File
@@ -35,8 +35,6 @@ OLIVE_NAMESPACE_ENTER
KeyframeViewBase::KeyframeViewBase(QWidget *parent) :
TimelineViewBase(parent),
dragging_bezier_point_(nullptr),
y_axis_enabled_(false),
y_scale_(1.0),
currently_autoselecting_(false)
{
SetDefaultDragMode(RubberBandDrag);
@@ -57,22 +55,6 @@ void KeyframeViewBase::Clear()
item_map_.clear();
}
const double &KeyframeViewBase::GetYScale() const
{
return y_scale_;
}
void KeyframeViewBase::SetYScale(const double &y_scale)
{
y_scale_ = y_scale;
if (y_axis_enabled_) {
VerticalScaleChangedEvent(y_scale_);
viewport()->update();
}
}
void KeyframeViewBase::DeleteSelected()
{
QUndoCommand* command = new QUndoCommand();
@@ -194,7 +176,7 @@ void KeyframeViewBase::mouseMoveEvent(QMouseEvent *event)
keypair.key->key()->set_time(node_time);
if (y_axis_enabled_) {
if (IsYAxisEnabled()) {
keypair.key->key()->set_value(keypair.value - mouse_diff_scaled.y());
}
@@ -257,7 +239,7 @@ void KeyframeViewBase::mouseReleaseEvent(QMouseEvent *event)
command);
// Commit value if we're setting a value
if (y_axis_enabled_) {
if (IsYAxisEnabled()) {
item->key()->set_value(keypair.value);
new NodeParamSetKeyframeValueCommand(item->key(),
keypair.value - mouse_diff_scaled.y(),
@@ -288,10 +270,6 @@ void KeyframeViewBase::ScaleChangedEvent(const double &scale)
}
}
void KeyframeViewBase::VerticalScaleChangedEvent(double)
{
}
const QMap<NodeKeyframe *, KeyframeViewItem *> &KeyframeViewBase::item_map() const
{
return item_map_;
@@ -310,11 +288,6 @@ void KeyframeViewBase::TimeTargetChangedEvent(Node *target)
}
}
void KeyframeViewBase::SetYAxisEnabled(bool e)
{
y_axis_enabled_ = e;
}
void KeyframeViewBase::SetKeyframeTrackVisible(int track, bool visible)
{
if (!visible == hidden_tracks_.contains(track)) {
@@ -336,6 +309,11 @@ void KeyframeViewBase::SetKeyframeTrackVisible(int track, bool visible)
}
}
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);
@@ -433,7 +411,7 @@ void KeyframeViewBase::ProcessBezierDrag(QPointF mouse_diff_scaled, bool include
QPointF KeyframeViewBase::GetScaledCursorPos(const QPoint &cursor_pos)
{
return QPointF(static_cast<double>(cursor_pos.x()) / GetScale(),
static_cast<double>(cursor_pos.y()) / y_scale_);
static_cast<double>(cursor_pos.y()) / GetYScale());
}
void KeyframeViewBase::ShowContextMenu()
@@ -480,7 +458,11 @@ void KeyframeViewBase::ShowContextMenu()
break;
}
}
}
ContextMenuEvent(m);
if (!items.isEmpty()) {
m.addSeparator();
QAction* properties_action = m.addAction(tr("P&roperties"));
@@ -532,7 +514,7 @@ void KeyframeViewBase::ShowKeyframePropertiesDialog()
void KeyframeViewBase::AutoSelectKeyTimeNeighbors()
{
if (currently_autoselecting_ || y_axis_enabled_) {
if (currently_autoselecting_ || IsYAxisEnabled()) {
return;
}
+4 -12
View File
@@ -23,9 +23,10 @@
#include "keyframeviewitem.h"
#include "node/keyframe.h"
#include "widget/timetarget/timetarget.h"
#include "widget/curvewidget/beziercontrolpointitem.h"
#include "widget/menu/menu.h"
#include "widget/timelinewidget/view/timelineviewbase.h"
#include "widget/timetarget/timetarget.h"
OLIVE_NAMESPACE_ENTER
@@ -37,9 +38,6 @@ public:
virtual void Clear();
const double& GetYScale() const;
void SetYScale(const double& y_scale);
void DeleteSelected();
public slots:
@@ -54,18 +52,16 @@ protected:
virtual void ScaleChangedEvent(const double& scale) override;
virtual void VerticalScaleChangedEvent(double scale);
const QMap<NodeKeyframe*, KeyframeViewItem*>& item_map() const;
virtual void KeyframeAboutToBeRemoved(NodeKeyframe* key);
virtual void TimeTargetChangedEvent(Node*) override;
void SetYAxisEnabled(bool e);
void SetKeyframeTrackVisible(int track, bool visible);
virtual void ContextMenuEvent(Menu &m);
private:
rational CalculateNewTimeFromScreen(const rational& old_time, double cursor_diff);
@@ -96,10 +92,6 @@ private:
QVector<KeyframeItemAndTime> selected_keys_;
bool y_axis_enabled_;
double y_scale_;
bool currently_autoselecting_;
QList<int> hidden_tracks_;
@@ -55,6 +55,8 @@ NodeParamView::NodeParamView(QWidget *parent) :
// Set up scroll area layout
param_layout_ = new QVBoxLayout(param_widget_area_);
param_layout_->setSpacing(0);
// KeyframeView is offset by a ruler, so to stay synchronized with it, we should be too
param_layout_->setContentsMargins(0, ruler()->height(), 0, 0);
// Add a stretch to allow empty space at the bottom of the layout
@@ -162,6 +164,8 @@ void NodeParamView::SetNodes(QList<Node *> nodes)
}
}
UpdateItemTime(GetTimestamp());
QMetaObject::invokeMethod(this, "PlaceKeyframesOnView", Qt::QueuedConnection);
}
}
+2 -2
View File
@@ -482,12 +482,12 @@ void TimeBasedWidget::ToggleShowAll()
w = timeline_views_.first()->width();
}
w = w / 10 * 9;
toggle_show_all_old_scale_ = GetScale();
toggle_show_all_old_scroll_ = scrollbar_->value();
SetScale(w / GetConnectedNode()->GetLength().toDouble());
SetScaleFromDimensions(w, GetConnectedNode()->GetLength().toDouble());
scrollbar_->setValue(0);
// Must explicitly do this because SetScale() will automatically set this to false
@@ -27,6 +27,8 @@
OLIVE_NAMESPACE_ENTER
const int TimelineScaledObject::kCalculateDimensionsPadding = 10;
TimelineScaledObject::TimelineScaledObject() :
scale_(1.0),
min_scale_(0),
@@ -112,6 +114,21 @@ void TimelineScaledObject::SetScale(const double& scale)
ScaleChangedEvent(scale_);
}
void TimelineScaledObject::SetScaleFromDimensions(double viewport_width, double content_width)
{
SetScale(CalculateScaleFromDimensions(viewport_width, content_width));
}
double TimelineScaledObject::CalculateScaleFromDimensions(double viewport_sz, double content_sz)
{
return static_cast<double>(viewport_sz / kCalculateDimensionsPadding * (kCalculateDimensionsPadding-1)) / static_cast<double>(content_sz);
}
double TimelineScaledObject::CalculatePaddingFromDimensionScale(double viewport_sz)
{
return (viewport_sz / (kCalculateDimensionsPadding * 2));
}
TimelineScaledWidget::TimelineScaledWidget(QWidget *parent) :
QWidget(parent)
{
@@ -44,6 +44,10 @@ public:
void SetScale(const double& scale);
void SetScaleFromDimensions(double viewport_width, double content_width);
static double CalculateScaleFromDimensions(double viewport_sz, double content_sz);
static double CalculatePaddingFromDimensionScale(double viewport_sz);
protected:
double TimeToScene(const rational& time);
rational SceneToTime(const double &x, bool round = false);
@@ -67,6 +71,8 @@ private:
double max_scale_;
static const int kCalculateDimensionsPadding;
};
class TimelineScaledWidget : public QWidget, public TimelineScaledObject
@@ -40,7 +40,9 @@ TimelineViewBase::TimelineViewBase(QWidget *parent) :
playhead_scene_right_(-1),
dragging_playhead_(false),
snapped_(false),
snap_service_(nullptr)
snap_service_(nullptr),
y_axis_enabled_(false),
y_scale_(1.0)
{
setScene(&scene_);
@@ -80,6 +82,26 @@ void TimelineViewBase::SetSnapService(SnapService *service)
snap_service_ = service;
}
const double &TimelineViewBase::GetYScale() const
{
return y_scale_;
}
void TimelineViewBase::VerticalScaleChangedEvent(double)
{
}
void TimelineViewBase::SetYScale(const double &y_scale)
{
y_scale_ = y_scale;
if (y_axis_enabled_) {
VerticalScaleChangedEvent(y_scale_);
viewport()->update();
}
}
void TimelineViewBase::SetTime(const int64_t time)
{
playhead_ = time;
@@ -252,10 +274,56 @@ bool TimelineViewBase::HandleZoomFromScroll(QWheelEvent *event)
if (WheelEventIsAZoomEvent(event)) {
// If CTRL is held (or a preference is set to swap CTRL behavior), we zoom instead of scrolling
if (!event->angleDelta().isNull()) {
if (event->angleDelta().x() + event->angleDelta().y() > 0) {
emit ScaleChanged(GetScale() * 1.1);
bool only_vertical = false;
bool only_horizontal = false;
// Ctrl+Shift limits to only one axis
// Alt switches between horizontal only (alt held) or vertical only (alt not held)
if (y_axis_enabled_) {
if (event->modifiers() & Qt::ShiftModifier) {
if (event->modifiers() & Qt::AltModifier) {
only_horizontal = true;
} else {
only_vertical = true;
}
}
} else {
emit ScaleChanged(GetScale() * 0.9);
only_horizontal = true;
}
double scale_multiplier;
if (event->angleDelta().x() + event->angleDelta().y() > 0) {
scale_multiplier = 1.1;
} else {
scale_multiplier = 0.9;
}
QPointF cursor_pos;
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
cursor_pos = event->position();
#else
cursor_pos = event->posF();
#endif
if (!only_vertical) {
double new_x_scale = GetScale() * scale_multiplier;
int new_x_scroll = qRound(horizontalScrollBar()->value() / GetScale() * new_x_scale + (cursor_pos.x() - cursor_pos.x() / new_x_scale * GetScale()));
emit ScaleChanged(new_x_scale);
horizontalScrollBar()->setValue(new_x_scroll);
}
if (!only_horizontal) {
double new_y_scale = GetYScale() * scale_multiplier;
int new_y_scroll = qRound(verticalScrollBar()->value() / GetYScale() * new_y_scale + (cursor_pos.y() - cursor_pos.y() / new_y_scale * GetYScale()));
SetYScale(new_y_scale);
verticalScrollBar()->setValue(new_y_scroll);
}
}
@@ -48,6 +48,9 @@ public:
void SetSnapService(SnapService* service);
const double& GetYScale() const;
void SetYScale(const double& y_scale);
public slots:
void SetTime(const int64_t time);
@@ -69,6 +72,8 @@ protected:
virtual void SceneRectUpdateEvent(QRectF&){}
virtual void VerticalScaleChangedEvent(double scale);
bool HandleZoomFromScroll(QWheelEvent* event);
bool WheelEventIsAZoomEvent(QWheelEvent* event);
@@ -81,6 +86,16 @@ protected:
virtual void TimebaseChangedEvent(const rational &) override;
bool IsYAxisEnabled() const
{
return y_axis_enabled_;
}
void SetYAxisEnabled(bool e)
{
y_axis_enabled_ = e;
}
private:
qreal GetPlayheadX();
@@ -102,6 +117,10 @@ private:
SnapService* snap_service_;
bool y_axis_enabled_;
double y_scale_;
private slots:
/**
* @brief Slot called whenever the view resizes or the scene contents change to enforce minimum scene sizes