Improve stability of the curve editor

- Clear the CurveEditor when deleting a timeline
- Prevent value-axis computation with invalid values
- Prevent invalid values when animation curves contain one keyframe only
- Set the dirty flag when deleting a keyframe in order to properly
  delete it
- Fix autoscrolling for the timeline editor

Fixes: QDS-4115
Fixes: QDS-4081
Fixes: QDS-4080
Fixes: QDS-3251
Change-Id: I3bc8406ac57f30b16bccc2e1c164a84502de7750
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Knud Dollereder
2021-04-20 15:11:14 +02:00
parent 357e70279c
commit d5daa1d0bb
9 changed files with 66 additions and 53 deletions

View File

@@ -93,6 +93,11 @@ AnimationCurve::AnimationCurve(const QEasingCurve &easing, const QPointF &start,
analyze(); analyze();
} }
bool AnimationCurve::isEmpty() const
{
return m_frames.empty();
}
bool AnimationCurve::isValid() const bool AnimationCurve::isValid() const
{ {
return m_frames.size() >= 2; return m_frames.size() >= 2;
@@ -352,45 +357,43 @@ void AnimationCurve::insert(double time)
void AnimationCurve::analyze() void AnimationCurve::analyze()
{ {
if (isValid()) { m_minY = std::numeric_limits<double>::max();
m_minY = std::numeric_limits<double>::max(); m_maxY = std::numeric_limits<double>::lowest();
m_maxY = std::numeric_limits<double>::lowest();
auto byTime = [](const auto &a, const auto &b) { auto byTime = [](const auto &a, const auto &b) {
return a.position().x() < b.position().x(); return a.position().x() < b.position().x();
}; };
std::sort(m_frames.begin(), m_frames.end(), byTime); std::sort(m_frames.begin(), m_frames.end(), byTime);
for (auto e : extrema()) { for (auto e : extrema()) {
if (m_minY > e.y()) if (m_minY > e.y())
m_minY = e.y(); m_minY = e.y();
if (m_maxY < e.y()) if (m_maxY < e.y())
m_maxY = e.y(); m_maxY = e.y();
}
for (auto &frame : qAsConst(m_frames)) {
if (frame.position().y() < m_minY)
m_minY = frame.position().y();
if (frame.position().y() > m_maxY)
m_maxY = frame.position().y();
if (frame.hasLeftHandle()) {
if (frame.leftHandle().y() < m_minY)
m_minY = frame.leftHandle().y();
if (frame.leftHandle().y() > m_maxY)
m_maxY = frame.leftHandle().y();
} }
for (auto &frame : qAsConst(m_frames)) { if (frame.hasRightHandle()) {
if (frame.position().y() < m_minY) if (frame.rightHandle().y() < m_minY)
m_minY = frame.position().y(); m_minY = frame.rightHandle().y();
if (frame.position().y() > m_maxY) if (frame.rightHandle().y() > m_maxY)
m_maxY = frame.position().y(); m_maxY = frame.rightHandle().y();
if (frame.hasLeftHandle()) {
if (frame.leftHandle().y() < m_minY)
m_minY = frame.leftHandle().y();
if (frame.leftHandle().y() > m_maxY)
m_maxY = frame.leftHandle().y();
}
if (frame.hasRightHandle()) {
if (frame.rightHandle().y() < m_minY)
m_minY = frame.rightHandle().y();
if (frame.rightHandle().y() > m_maxY)
m_maxY = frame.rightHandle().y();
}
} }
} }
} }

View File

@@ -46,6 +46,8 @@ public:
AnimationCurve(const QEasingCurve &easing, const QPointF &start, const QPointF &end); AnimationCurve(const QEasingCurve &easing, const QPointF &start, const QPointF &end);
bool isEmpty() const;
bool isValid() const; bool isValid() const;
bool isFromData() const; bool isFromData() const;

View File

@@ -255,7 +255,7 @@ TreeItem *CurveEditorModel::createTopLevelItem(const QmlDesigner::QmlTimeline &t
for (auto &&grp : timeline.keyframeGroupsForTarget(node)) { for (auto &&grp : timeline.keyframeGroupsForTarget(node)) {
if (grp.isValid()) { if (grp.isValid()) {
AnimationCurve curve = createAnimationCurve(grp); AnimationCurve curve = createAnimationCurve(grp);
if (curve.isValid()) { if (!curve.isEmpty()) {
QString name = QString::fromUtf8(grp.propertyName()); QString name = QString::fromUtf8(grp.propertyName());
auto propertyItem = new PropertyTreeItem(name, curve, typeFrom(grp)); auto propertyItem = new PropertyTreeItem(name, curve, typeFrom(grp));

View File

@@ -101,6 +101,9 @@ void CurveEditorView::nodeRemoved(const ModelNode &removedNode,
ModelNode parent = parentProperty.parentModelNode(); ModelNode parent = parentProperty.parentModelNode();
if (dirtyfiesView(parent)) if (dirtyfiesView(parent))
updateKeyframes(); updateKeyframes();
if (!activeTimeline().isValid())
m_model->reset({});
} }
void CurveEditorView::nodeReparented(const ModelNode &node, void CurveEditorView::nodeReparented(const ModelNode &node,

View File

@@ -292,7 +292,7 @@ std::vector<AnimationCurve> CurveItem::curves() const
} }
} }
if (tmp.size() >= 2) if (!tmp.empty())
out.push_back(AnimationCurve(tmp)); out.push_back(AnimationCurve(tmp));
return out; return out;
@@ -372,6 +372,9 @@ void CurveItem::restore()
currItem->setInterpolation(segment.interpolation()); currItem->setInterpolation(segment.interpolation());
prevItem = currItem; prevItem = currItem;
} }
// It is now the last item.
prevItem->setRightHandle(QPointF());
} }
void CurveItem::setDirty(bool dirty) void CurveItem::setDirty(bool dirty)
@@ -404,12 +407,12 @@ void CurveItem::setCurve(const AnimationCurve &curve)
item->setLocked(locked()); item->setLocked(locked());
item->setComponentTransform(m_transform); item->setComponentTransform(m_transform);
m_keyframes.push_back(item); m_keyframes.push_back(item);
QObject::connect(item, &KeyframeItem::redrawCurve, this, &CurveItem::emitCurveChanged); QObject::connect(item, &KeyframeItem::redrawCurve, this, &CurveItem::markDirty);
QObject::connect(item, &KeyframeItem::keyframeMoved, this, &CurveItem::keyframeMoved); QObject::connect(item, &KeyframeItem::keyframeMoved, this, &CurveItem::keyframeMoved);
QObject::connect(item, &KeyframeItem::handleMoved, this, &CurveItem::handleMoved); QObject::connect(item, &KeyframeItem::handleMoved, this, &CurveItem::handleMoved);
} }
emitCurveChanged(); markDirty();
} }
QRectF CurveItem::setComponentTransform(const QTransform &transform) QRectF CurveItem::setComponentTransform(const QTransform &transform)
@@ -499,10 +502,12 @@ void CurveItem::deleteSelectedKeyframes()
m_keyframes.erase(iter, m_keyframes.end()); m_keyframes.erase(iter, m_keyframes.end());
restore(); restore();
emitCurveChanged(); markDirty();
emit curveChanged(id(), curve());
} }
void CurveItem::emitCurveChanged() void CurveItem::markDirty()
{ {
setDirty(true); setDirty(true);
update(); update();

View File

@@ -134,7 +134,7 @@ public:
void deleteSelectedKeyframes(); void deleteSelectedKeyframes();
private: private:
void emitCurveChanged(); void markDirty();
unsigned int m_id; unsigned int m_id;

View File

@@ -216,6 +216,7 @@ void GraphicsScene::reset()
void GraphicsScene::deleteSelectedKeyframes() void GraphicsScene::deleteSelectedKeyframes()
{ {
m_dirty = true;
for (auto *curve : qAsConst(m_curves)) for (auto *curve : qAsConst(m_curves))
curve->deleteSelectedKeyframes(); curve->deleteSelectedKeyframes();
} }

View File

@@ -72,8 +72,8 @@ GraphicsView::GraphicsView(CurveEditorModel *model, QWidget *parent)
connect(&m_dialog, &CurveEditorStyleDialog::styleChanged, this, &GraphicsView::setStyle); connect(&m_dialog, &CurveEditorStyleDialog::styleChanged, this, &GraphicsView::setStyle);
auto itemSlot = [this](unsigned int id, const AnimationCurve &curve) { auto itemSlot = [this](unsigned int id, const AnimationCurve &curve) {
applyZoom(m_zoomX, m_zoomY);
m_model->setCurve(id, curve); m_model->setCurve(id, curve);
applyZoom(m_zoomX, m_zoomY);
}; };
connect(m_scene, &GraphicsScene::curveChanged, itemSlot); connect(m_scene, &GraphicsScene::curveChanged, itemSlot);
@@ -671,11 +671,15 @@ void GraphicsView::drawValueScale(QPainter *painter, const QRectF &rect)
painter->drawText(textRect, Qt::AlignCenter, valueText); painter->drawText(textRect, Qt::AlignCenter, valueText);
}; };
double density = 1. / (static_cast<double>(fm.height()) * m_style.labelDensityY); double min = minimumValue();
Axis axis = Axis::compute(minimumValue(), maximumValue(), rect.height(), density); double max = maximumValue();
const double eps = 1.0e-10; if (std::isfinite(min) && std::isfinite(max) && rect.isValid()) {
for (double i = axis.lmin; i <= axis.lmax + eps; i += axis.lstep) double density = 1. / (static_cast<double>(fm.height()) * m_style.labelDensityY);
paintLabeledTick(i); Axis axis = Axis::compute(min, max, rect.height(), density);
const double eps = 1.0e-10;
for (double i = axis.lmin; i <= axis.lmax + eps; i += axis.lstep)
paintLabeledTick(i);
}
painter->restore(); painter->restore();
} }

View File

@@ -242,13 +242,8 @@ void TimelineFrameHandle::scrollOutOfBoundsMax()
{ {
const double width = abstractScrollGraphicsScene()->width(); const double width = abstractScrollGraphicsScene()->width();
if (QApplication::mouseButtons() == Qt::LeftButton) { if (QApplication::mouseButtons() == Qt::LeftButton) {
const double frameWidth = abstractScrollGraphicsScene()->rulerScaling(); abstractScrollGraphicsScene()->setScrollOffset(computeScrollSpeed());
const double upperThreshold = width - frameWidth; abstractScrollGraphicsScene()->invalidateScrollbar();
if (rect().center().x() > upperThreshold) {
abstractScrollGraphicsScene()->setScrollOffset(computeScrollSpeed());
abstractScrollGraphicsScene()->invalidateScrollbar();
}
callSetClampedXPosition(width - (rect().width() / 2) - 1); callSetClampedXPosition(width - (rect().width() / 2) - 1);
m_timer.start(); m_timer.start();