Implement playhead snapping

When Shift is down, playhead snaps to ruler ticks and keyframes.

Task-number: QDS-1068
Change-Id: Ie793c3041c00ff01ec42f6045e1f3c44fc02da2c
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
This commit is contained in:
Mahmoud Badri
2019-10-02 16:53:24 +03:00
parent f866319c16
commit 69719e7b3e
9 changed files with 82 additions and 9 deletions

View File

@@ -159,4 +159,9 @@ void TimelineGraphicsLayout::activate()
m_layout->activate();
}
TimelineRulerSectionItem *TimelineGraphicsLayout::ruler() const
{
return m_rulerItem;
}
} // End namespace QmlDesigner.

View File

@@ -74,6 +74,8 @@ public:
void activate();
TimelineRulerSectionItem *ruler() const;
private:
QGraphicsLinearLayout *m_layout = nullptr;

View File

@@ -171,12 +171,58 @@ void TimelineGraphicsScene::invalidateLayout()
m_layout->invalidate();
}
void TimelineGraphicsScene::updateKeyframePositionsCache()
{
auto kfPos = keyframePositions();
std::sort(kfPos.begin(), kfPos.end());
kfPos.erase(std::unique(kfPos.begin(), kfPos.end()), kfPos.end()); // remove duplicates
m_keyframePositionsCache = kfPos;
}
// snap a frame to nearest keyframe or ruler tick
qreal TimelineGraphicsScene::snap(qreal frame)
{
qreal frameTick = m_layout->ruler()->getFrameTick();
qreal rulerTicksSnapframe = qRound(frame / frameTick) * frameTick;
// get nearest keyframe to the input frame
bool nearestKeyframeFound = false;
qreal nearestKeyframe = 0;
for (int i = 0; i < m_keyframePositionsCache.size(); ++i) {
qreal kf_i = m_keyframePositionsCache[i];
if (kf_i > frame) {
nearestKeyframeFound = true;
nearestKeyframe = kf_i;
if (i > 0) {
qreal kf_p = m_keyframePositionsCache[i - 1]; // previous kf
if (frame - kf_p < kf_i - frame)
nearestKeyframe = kf_p;
}
break;
}
}
if (!nearestKeyframeFound && !m_keyframePositionsCache.empty()) {
// playhead past last keyframe case
nearestKeyframe = m_keyframePositionsCache.last();
nearestKeyframeFound = true;
}
// return nearest snappable keyframe or ruler tick
return nearestKeyframeFound && qAbs(nearestKeyframe - frame)
< qAbs(rulerTicksSnapframe - frame) ? nearestKeyframe
: rulerTicksSnapframe;
}
void TimelineGraphicsScene::setCurrenFrame(const QmlTimeline &timeline, qreal frame)
{
if (timeline.isValid())
if (timeline.isValid()) {
if (QApplication::keyboardModifiers() & Qt::ShiftModifier) // playhead snapping
frame = snap(frame);
m_currentFrameIndicator->setPosition(frame);
else
} else {
m_currentFrameIndicator->setPosition(0);
}
invalidateCurrentValues();
}

View File

@@ -88,6 +88,8 @@ public:
qreal startFrame() const;
qreal endFrame() const;
void updateKeyframePositionsCache();
qreal mapToScene(qreal x) const;
qreal mapFromScene(qreal x) const;
@@ -165,6 +167,8 @@ private:
QList<QGraphicsItem *> itemsAt(const QPointF &pos);
private:
qreal snap(qreal frame);
TimelineWidget *m_parent = nullptr;
TimelineGraphicsLayout *m_layout = nullptr;
@@ -175,6 +179,9 @@ private:
QList<TimelineKeyframeItem *> m_selectedKeyframes;
// sorted, unique cache of keyframes positions, used for snapping
QVector<qreal> m_keyframePositionsCache;
int m_scrollOffset = 0;
};

View File

@@ -79,15 +79,15 @@ void TimelineFrameHandle::setHeight(int height)
setRect(rect().x(), rect().y(), rect().width(), height);
}
void TimelineFrameHandle::setPosition(qreal position)
void TimelineFrameHandle::setPosition(qreal frame)
{
const qreal scenePos = mapFromFrameToScene(position);
const qreal scenePos = mapFromFrameToScene(frame);
QRectF newRect(scenePos - rect().width() / 2, rect().y(), rect().width(), rect().height());
if (!qFuzzyCompare(newRect.x(), rect().x())) {
setRect(newRect);
}
m_position = position;
m_position = frame;
}
void TimelineFrameHandle::setPositionInteractive(const QPointF &position)

View File

@@ -50,7 +50,7 @@ public:
explicit TimelineFrameHandle(TimelineItem *parent = nullptr);
void setHeight(int height);
void setPosition(qreal position);
void setPosition(qreal frame);
void setPositionInteractive(const QPointF &postion) override;
void commitPosition(const QPointF &point) override;
qreal position() const;

View File

@@ -701,8 +701,6 @@ void TimelineRulerSectionItem::paint(QPainter *painter, const QStyleOptionGraphi
void TimelineRulerSectionItem::paintTicks(QPainter *painter)
{
const int totalWidth = size().width() / m_scaling + timelineScene()->scrollOffset() / m_scaling;
QFontMetrics fm(painter->font());
int minSpacingText = fm.horizontalAdvance(QString("X%1X").arg(rulerDuration()));
@@ -734,8 +732,10 @@ void TimelineRulerSectionItem::paintTicks(QPainter *painter)
}
}
int height = size().height();
m_frameTick = qreal(deltaLine);
int height = size().height();
const int totalWidth = (size().width() + timelineScene()->scrollOffset()) / m_scaling;
for (int i = timelineScene()->scrollOffset() / m_scaling; i < totalWidth; ++i) {
if ((i % deltaText) == 0) {
drawCenteredText(painter,
@@ -759,6 +759,11 @@ void TimelineRulerSectionItem::paintTicks(QPainter *painter)
}
}
qreal TimelineRulerSectionItem::getFrameTick() const
{
return m_frameTick;
}
void TimelineRulerSectionItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
TimelineItem::mousePressEvent(event);

View File

@@ -155,6 +155,7 @@ public:
void setRulerScaleFactor(int scaling);
int getRulerScaleFactor() const;
qreal getFrameTick() const;
qreal rulerScaling() const;
qreal rulerDuration() const;
@@ -187,6 +188,7 @@ private:
qreal m_start = 0;
qreal m_end = 0;
qreal m_scaling = 1;
qreal m_frameTick = 1.; // distance between 2 tick steps (in frames) on the ruler at current scale
};
} // namespace QmlDesigner

View File

@@ -170,6 +170,7 @@ void TimelineView::instancePropertyChanged(const QList<QPair<ModelNode, Property
{
QmlTimeline timeline = currentTimeline();
bool updated = false;
bool keyframeChangeFlag = false;
for (const auto &pair : propertyList) {
if (pair.second == "startFrame" || pair.second == "endFrame") {
if (QmlTimeline::isValidQmlTimeline(pair.first)) {
@@ -183,6 +184,11 @@ void TimelineView::instancePropertyChanged(const QList<QPair<ModelNode, Property
m_timelineWidget->graphicsScene()->invalidateCurrentValues();
updated = true;
}
if (!keyframeChangeFlag && pair.second == "frame") {
m_timelineWidget->graphicsScene()->updateKeyframePositionsCache();
keyframeChangeFlag = true;
}
}
}