initial commit
This commit is contained in:
310
client/gui/timeline/TimelineWidget.cpp
Normal file
310
client/gui/timeline/TimelineWidget.cpp
Normal file
@@ -0,0 +1,310 @@
|
||||
#include "timeline/TimelineWidget.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QWheelEvent>
|
||||
|
||||
namespace {
|
||||
|
||||
int clampFrame(int f, int a, int b) {
|
||||
if (a > b) std::swap(a, b);
|
||||
return std::clamp(f, a, b);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TimelineWidget::TimelineWidget(QWidget* parent)
|
||||
: QWidget(parent) {
|
||||
setMouseTracking(true);
|
||||
setMinimumHeight(28);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
}
|
||||
|
||||
void TimelineWidget::setFrameRange(int start, int end) {
|
||||
if (m_start == start && m_end == end) {
|
||||
return;
|
||||
}
|
||||
m_start = start;
|
||||
m_end = end;
|
||||
m_currentFrame = clampFrame(m_currentFrame, m_start, m_end);
|
||||
update();
|
||||
}
|
||||
|
||||
void TimelineWidget::setCurrentFrame(int frame) {
|
||||
setFrameInternal(frame, false);
|
||||
}
|
||||
|
||||
void TimelineWidget::setSelectionRange(int start, int end) {
|
||||
if (start < 0 || end < 0) {
|
||||
m_selStart = -1;
|
||||
m_selEnd = -1;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
m_selStart = clampFrame(std::min(start, end), m_start, m_end);
|
||||
m_selEnd = clampFrame(std::max(start, end), m_start, m_end);
|
||||
update();
|
||||
}
|
||||
|
||||
void TimelineWidget::setKeyframeTracks(const core::Project::Entity* e) {
|
||||
m_locFrames.clear();
|
||||
m_scaleFrames.clear();
|
||||
m_imgFrames.clear();
|
||||
if (!e) {
|
||||
update();
|
||||
return;
|
||||
}
|
||||
m_locFrames.reserve(e->locationKeys.size());
|
||||
for (const auto& k : e->locationKeys) m_locFrames.push_back(k.frame);
|
||||
m_scaleFrames.reserve(e->userScaleKeys.size());
|
||||
for (const auto& k : e->userScaleKeys) m_scaleFrames.push_back(k.frame);
|
||||
m_imgFrames.reserve(e->imageFrames.size());
|
||||
for (const auto& k : e->imageFrames) m_imgFrames.push_back(k.frame);
|
||||
|
||||
auto uniqSort = [](QVector<int>& v) {
|
||||
std::sort(v.begin(), v.end());
|
||||
v.erase(std::unique(v.begin(), v.end()), v.end());
|
||||
};
|
||||
uniqSort(m_locFrames);
|
||||
uniqSort(m_scaleFrames);
|
||||
uniqSort(m_imgFrames);
|
||||
// 轨道变了:若当前选中的关键帧不再存在,则清除
|
||||
auto contains = [](const QVector<int>& v, int f) {
|
||||
return std::binary_search(v.begin(), v.end(), f);
|
||||
};
|
||||
bool ok = true;
|
||||
if (m_selKeyKind == KeyKind::Location) ok = contains(m_locFrames, m_selKeyFrame);
|
||||
if (m_selKeyKind == KeyKind::UserScale) ok = contains(m_scaleFrames, m_selKeyFrame);
|
||||
if (m_selKeyKind == KeyKind::Image) ok = contains(m_imgFrames, m_selKeyFrame);
|
||||
if (!ok) {
|
||||
m_selKeyKind = KeyKind::None;
|
||||
m_selKeyFrame = -1;
|
||||
emit keyframeSelectionChanged(m_selKeyKind, m_selKeyFrame);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
QRect TimelineWidget::trackRect() const {
|
||||
const int pad = 8;
|
||||
const int h = height();
|
||||
return QRect(pad, 0, std::max(1, width() - pad * 2), h);
|
||||
}
|
||||
|
||||
int TimelineWidget::xToFrame(int x) const {
|
||||
const QRect r = trackRect();
|
||||
if (r.width() <= 1) return m_start;
|
||||
const double t = std::clamp((x - r.left()) / double(r.width() - 1), 0.0, 1.0);
|
||||
const int span = std::max(1, m_end - m_start);
|
||||
const int f = m_start + int(std::round(t * span));
|
||||
return clampFrame(f, m_start, m_end);
|
||||
}
|
||||
|
||||
int TimelineWidget::frameToX(int frame) const {
|
||||
const QRect r = trackRect();
|
||||
if (r.width() <= 1) return r.left();
|
||||
const int f = clampFrame(frame, m_start, m_end);
|
||||
const int span = std::max(1, m_end - m_start);
|
||||
const double t = double(f - m_start) / double(span);
|
||||
return r.left() + int(std::round(t * (r.width() - 1)));
|
||||
}
|
||||
|
||||
void TimelineWidget::setFrameInternal(int frame, bool commit) {
|
||||
const int f = clampFrame(frame, m_start, m_end);
|
||||
if (m_currentFrame == f && !commit) {
|
||||
return;
|
||||
}
|
||||
m_currentFrame = f;
|
||||
update();
|
||||
emit frameScrubbed(f);
|
||||
if (commit) {
|
||||
emit frameCommitted(f);
|
||||
}
|
||||
}
|
||||
|
||||
void TimelineWidget::paintEvent(QPaintEvent*) {
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
const QRect r = rect();
|
||||
p.fillRect(r, palette().base());
|
||||
|
||||
const QRect tr = trackRect().adjusted(0, 8, 0, -8);
|
||||
const QColor rail = palette().mid().color();
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(rail);
|
||||
p.drawRoundedRect(tr, 6, 6);
|
||||
|
||||
// selection range
|
||||
if (m_selStart >= 0 && m_selEnd >= 0) {
|
||||
const int x0 = frameToX(m_selStart);
|
||||
const int x1 = frameToX(m_selEnd);
|
||||
QRect sel(QPoint(std::min(x0, x1), tr.top()), QPoint(std::max(x0, x1), tr.bottom()));
|
||||
sel = sel.adjusted(0, 2, 0, -2);
|
||||
QColor c = palette().highlight().color();
|
||||
c.setAlpha(50);
|
||||
p.setBrush(c);
|
||||
p.drawRoundedRect(sel, 4, 4);
|
||||
}
|
||||
|
||||
auto drawDots = [&](const QVector<int>& frames, const QColor& c, int y) {
|
||||
p.setBrush(c);
|
||||
p.setPen(Qt::NoPen);
|
||||
for (int f : frames) {
|
||||
if (f < m_start || f > m_end) continue;
|
||||
const int x = frameToX(f);
|
||||
const bool sel =
|
||||
(m_selKeyFrame == f)
|
||||
&& ((m_selKeyKind == KeyKind::Image && &frames == &m_imgFrames)
|
||||
|| (m_selKeyKind == KeyKind::Location && &frames == &m_locFrames)
|
||||
|| (m_selKeyKind == KeyKind::UserScale && &frames == &m_scaleFrames));
|
||||
if (sel) {
|
||||
p.setPen(QPen(palette().highlight().color(), 2.0));
|
||||
p.setBrush(c);
|
||||
p.drawEllipse(QPointF(x, y), 4.4, 4.4);
|
||||
p.setPen(Qt::NoPen);
|
||||
} else {
|
||||
p.drawEllipse(QPointF(x, y), 2.6, 2.6);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const int yMid = tr.center().y();
|
||||
drawDots(m_imgFrames, QColor(80, 160, 255, 230), yMid - 6);
|
||||
drawDots(m_locFrames, QColor(255, 120, 0, 230), yMid);
|
||||
drawDots(m_scaleFrames, QColor(140, 220, 140, 230), yMid + 6);
|
||||
|
||||
// current frame caret
|
||||
const int cx = frameToX(m_currentFrame);
|
||||
p.setPen(QPen(palette().highlight().color(), 2.0));
|
||||
p.drawLine(QPoint(cx, tr.top() - 6), QPoint(cx, tr.bottom() + 6));
|
||||
}
|
||||
|
||||
static bool hitDot(const QPoint& pos, int dotX, int dotY, int radiusPx) {
|
||||
const int dx = pos.x() - dotX;
|
||||
const int dy = pos.y() - dotY;
|
||||
return (dx * dx + dy * dy) <= (radiusPx * radiusPx);
|
||||
}
|
||||
|
||||
static int findNearestFrameInTrack(const QVector<int>& frames, int frame) {
|
||||
if (frames.isEmpty()) return -1;
|
||||
const auto it = std::lower_bound(frames.begin(), frames.end(), frame);
|
||||
if (it == frames.begin()) return *it;
|
||||
if (it == frames.end()) return frames.back();
|
||||
const int a = *(it - 1);
|
||||
const int b = *it;
|
||||
return (std::abs(frame - a) <= std::abs(b - frame)) ? a : b;
|
||||
}
|
||||
|
||||
static void findIntervalAround(const QVector<int>& allFrames, int frame, int& outA, int& outB) {
|
||||
outA = -1;
|
||||
outB = -1;
|
||||
if (allFrames.size() < 2) return;
|
||||
const auto it = std::upper_bound(allFrames.begin(), allFrames.end(), frame);
|
||||
if (it == allFrames.begin() || it == allFrames.end()) return;
|
||||
outA = *(it - 1);
|
||||
outB = *it;
|
||||
}
|
||||
|
||||
void TimelineWidget::mousePressEvent(QMouseEvent* e) {
|
||||
if (e->button() == Qt::RightButton) {
|
||||
emit contextMenuRequested(mapToGlobal(e->pos()), xToFrame(e->pos().x()));
|
||||
return;
|
||||
}
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
m_pressPos = e->pos();
|
||||
m_moved = false;
|
||||
m_dragging = true;
|
||||
setFrameInternal(xToFrame(e->pos().x()), false);
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
QWidget::mousePressEvent(e);
|
||||
}
|
||||
|
||||
void TimelineWidget::mouseMoveEvent(QMouseEvent* e) {
|
||||
if (m_dragging) {
|
||||
if ((e->pos() - m_pressPos).manhattanLength() > 3) {
|
||||
m_moved = true;
|
||||
}
|
||||
setFrameInternal(xToFrame(e->pos().x()), false);
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
QWidget::mouseMoveEvent(e);
|
||||
}
|
||||
|
||||
void TimelineWidget::mouseReleaseEvent(QMouseEvent* e) {
|
||||
if (m_dragging && e->button() == Qt::LeftButton) {
|
||||
m_dragging = false;
|
||||
const int f = xToFrame(e->pos().x());
|
||||
setFrameInternal(f, true);
|
||||
|
||||
// 点击(非拖拽)时做选中:关键帧或区间
|
||||
if (!m_moved) {
|
||||
const QRect tr = trackRect().adjusted(0, 8, 0, -8);
|
||||
const int yMid = tr.center().y();
|
||||
const int yImg = yMid - 6;
|
||||
const int yLoc = yMid;
|
||||
const int ySc = yMid + 6;
|
||||
const int rad = 7;
|
||||
|
||||
auto trySelectKey = [&](KeyKind kind, const QVector<int>& frames, int laneY) -> bool {
|
||||
const int nearest = findNearestFrameInTrack(frames, f);
|
||||
if (nearest < 0) return false;
|
||||
const int x = frameToX(nearest);
|
||||
if (hitDot(e->pos(), x, laneY, rad)) {
|
||||
m_selKeyKind = kind;
|
||||
m_selKeyFrame = nearest;
|
||||
emit keyframeSelectionChanged(m_selKeyKind, m_selKeyFrame);
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 先尝试命中关键帧(按 lane 优先)
|
||||
if (trySelectKey(KeyKind::Image, m_imgFrames, yImg)
|
||||
|| trySelectKey(KeyKind::Location, m_locFrames, yLoc)
|
||||
|| trySelectKey(KeyKind::UserScale, m_scaleFrames, ySc)) {
|
||||
// 选中关键帧时清掉区间
|
||||
if (m_selStart >= 0 && m_selEnd >= 0) {
|
||||
m_selStart = -1;
|
||||
m_selEnd = -1;
|
||||
emit intervalSelectionChanged(m_selStart, m_selEnd);
|
||||
}
|
||||
} else {
|
||||
// 未命中关键帧:尝试选中由关键帧切分出的区间(使用三轨道的并集)
|
||||
QVector<int> all = m_locFrames;
|
||||
all += m_scaleFrames;
|
||||
all += m_imgFrames;
|
||||
std::sort(all.begin(), all.end());
|
||||
all.erase(std::unique(all.begin(), all.end()), all.end());
|
||||
int a = -1, b = -1;
|
||||
findIntervalAround(all, f, a, b);
|
||||
if (a >= 0 && b >= 0) {
|
||||
setSelectionRange(a, b);
|
||||
emit intervalSelectionChanged(m_selStart, m_selEnd);
|
||||
// 选中区间时清掉关键帧选中
|
||||
if (m_selKeyKind != KeyKind::None) {
|
||||
m_selKeyKind = KeyKind::None;
|
||||
m_selKeyFrame = -1;
|
||||
emit keyframeSelectionChanged(m_selKeyKind, m_selKeyFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
QWidget::mouseReleaseEvent(e);
|
||||
}
|
||||
|
||||
void TimelineWidget::wheelEvent(QWheelEvent* e) {
|
||||
const int delta = (e->angleDelta().y() > 0) ? 1 : -1;
|
||||
setFrameInternal(m_currentFrame + delta, true);
|
||||
e->accept();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user