Files
hfut-bishe/client/gui/timeline/TimelineWidget.cpp
T
2026-05-14 13:30:06 +08:00

511 lines
18 KiB
C++

#include "timeline/TimelineWidget.h"
#include "timeline/TimelineEditorModel.h"
#include <algorithm>
#include <cmath>
#include <QFontMetrics>
#include <QMouseEvent>
#include <QPainter>
#include <QResizeEvent>
#include <QWheelEvent>
namespace {
static int pickMajorStep(double pixelsPerFrame) {
// 主刻度间距(帧):保证屏幕上大约 ≥ 48px
const double targetPx = 48.0;
const double raw = targetPx / std::max(pixelsPerFrame, 1e-6);
static const int cand[] = {1, 2, 5, 10, 15, 20, 25, 50, 75, 100, 150, 200, 250, 500, 1000, 2000, 5000};
for (int c : cand) {
if (c >= raw) return c;
}
return cand[sizeof(cand) / sizeof(cand[0]) - 1];
}
static int pickMinorStep(int major) {
if (major >= 100) return major / 5;
if (major >= 20) return major / 4;
if (major >= 10) return major / 5;
if (major >= 5) return 1;
return 1;
}
} // namespace
TimelineWidget::TimelineWidget(QWidget* parent)
: QWidget(parent) {
setMouseTracking(true);
// 单行紧凑:标尺 + 轨道(帧号画在播放头处,随坐标轴滚动)
setMinimumHeight(kRulerHeight + 18 + 6);
setFocusPolicy(Qt::StrongFocus);
setToolTip({});
}
void TimelineWidget::setModel(TimelineEditorModel* model) {
m_model = model;
}
void TimelineWidget::resizeEvent(QResizeEvent* e) {
QWidget::resizeEvent(e);
update();
}
void TimelineWidget::setFrameRange(int start, int end) {
m_start = std::max(0, start);
m_end = std::max(m_start + 1, end);
m_currentFrame = std::clamp(m_currentFrame, m_start, m_end - 1);
setSelectionRange(m_selStart, m_selEnd);
update();
}
void TimelineWidget::setCurrentFrame(int frame) {
setFrameInternal(frame, false);
}
void TimelineWidget::setCurrentFrameProgrammatic(int frame) {
const int f = std::clamp(frame, m_start, m_end - 1);
if (m_currentFrame == f) {
return;
}
m_currentFrame = f;
update();
}
void TimelineWidget::setSelectionRange(int start, int end) {
if (start < 0 || end < 0) {
m_selStart = -1;
m_selEnd = -1;
update();
return;
}
const int lo = std::min(start, end);
const int hi = std::max(start, end);
m_selStart = std::clamp(lo, m_start, m_end - 1);
m_selEnd = std::clamp(hi, m_selStart, m_end - 1);
update();
}
static void uniqSort(QVector<int>& v) {
std::sort(v.begin(), v.end());
v.erase(std::unique(v.begin(), v.end()), v.end());
}
static bool containsFrame(const QVector<int>& v, int f) {
return std::binary_search(v.begin(), v.end(), f);
}
void TimelineWidget::setKeyframeTracks(const QVector<int>& locFrames,
const QVector<int>& scaleFrames,
const QVector<int>& imgFrames,
const QVector<int>& visFrames) {
m_locFrames = locFrames;
m_scaleFrames = scaleFrames;
m_imgFrames = imgFrames;
m_visFrames = visFrames;
uniqSort(m_locFrames);
uniqSort(m_scaleFrames);
uniqSort(m_imgFrames);
uniqSort(m_visFrames);
bool ok = true;
if (m_selKeyKind == KeyKind::Location) ok = containsFrame(m_locFrames, m_selKeyFrame);
if (m_selKeyKind == KeyKind::UserScale) ok = containsFrame(m_scaleFrames, m_selKeyFrame);
if (m_selKeyKind == KeyKind::Image) ok = containsFrame(m_imgFrames, m_selKeyFrame);
if (m_selKeyKind == KeyKind::Visibility) ok = containsFrame(m_visFrames, m_selKeyFrame);
if (!ok) {
m_selKeyKind = KeyKind::None;
m_selKeyFrame = -1;
emit keyframeSelectionChanged(m_selKeyKind, m_selKeyFrame);
}
if (m_model) {
QVector<TimelineEditorModel::TrackView> tracks;
tracks.reserve(4);
tracks.push_back({TimelineEditorModel::TrackKind::Image, QStringLiteral("图像"), QColor(70, 130, 240), m_imgFrames});
tracks.push_back({TimelineEditorModel::TrackKind::Location, QStringLiteral("位置"), QColor(240, 110, 40), m_locFrames});
tracks.push_back({TimelineEditorModel::TrackKind::UserScale, QStringLiteral("缩放"), QColor(80, 190, 90), m_scaleFrames});
tracks.push_back({TimelineEditorModel::TrackKind::Visibility, QStringLiteral("显隐"), QColor(160, 100, 230), m_visFrames});
m_model->setTracks(std::move(tracks));
if (m_selKeyKind == KeyKind::None) {
m_model->clearSelection();
} else {
TimelineEditorModel::TrackKind kind = TimelineEditorModel::TrackKind::Location;
switch (m_selKeyKind) {
case KeyKind::Location: kind = TimelineEditorModel::TrackKind::Location; break;
case KeyKind::UserScale: kind = TimelineEditorModel::TrackKind::UserScale; break;
case KeyKind::Image: kind = TimelineEditorModel::TrackKind::Image; break;
case KeyKind::Visibility: kind = TimelineEditorModel::TrackKind::Visibility; break;
default: break;
}
m_model->setSelectedKey({kind, m_selKeyFrame});
}
m_model->setInterval(m_selStart, m_selEnd);
}
update();
}
void TimelineWidget::setToolKeyframeTracks(const QVector<int>& locFrames,
const QVector<int>& visFrames) {
m_locFrames = locFrames;
m_scaleFrames.clear();
m_imgFrames.clear();
m_visFrames = visFrames;
uniqSort(m_locFrames);
uniqSort(m_visFrames);
bool ok = true;
if (m_selKeyKind == KeyKind::Location) ok = containsFrame(m_locFrames, m_selKeyFrame);
if (m_selKeyKind == KeyKind::Visibility) ok = containsFrame(m_visFrames, m_selKeyFrame);
if (!ok) {
m_selKeyKind = KeyKind::None;
m_selKeyFrame = -1;
emit keyframeSelectionChanged(m_selKeyKind, m_selKeyFrame);
}
if (m_model) {
QVector<TimelineEditorModel::TrackView> tracks;
tracks.reserve(2);
tracks.push_back({TimelineEditorModel::TrackKind::Location, QStringLiteral("位置"), QColor(240, 110, 40), m_locFrames});
tracks.push_back({TimelineEditorModel::TrackKind::Visibility, QStringLiteral("显隐"), QColor(160, 100, 230), m_visFrames});
m_model->setTracks(std::move(tracks));
if (m_selKeyKind == KeyKind::None) {
m_model->clearSelection();
} else {
TimelineEditorModel::TrackKind kind = TimelineEditorModel::TrackKind::Location;
if (m_selKeyKind == KeyKind::Visibility) kind = TimelineEditorModel::TrackKind::Visibility;
m_model->setSelectedKey({kind, m_selKeyFrame});
}
m_model->setInterval(m_selStart, m_selEnd);
}
update();
}
QRect TimelineWidget::contentRect() const {
return QRect(contentLeft(), 3, contentWidth(), std::max(24, height() - 6));
}
QRect TimelineWidget::rulerRect() const {
const QRect c = contentRect();
return QRect(c.left(), c.top(), c.width(), kRulerHeight);
}
QRect TimelineWidget::keyAreaRect() const {
const QRect c = contentRect();
const QRect r = rulerRect();
const int top = r.bottom();
return QRect(c.left(), top, c.width(), std::max(1, c.bottom() - top));
}
double TimelineWidget::frameToXf(double frame) const {
const double pxf = double(contentWidth()) / double(std::max(1, m_end - m_start));
return double(contentLeft()) + (frame - double(m_start)) * pxf;
}
int TimelineWidget::frameToX(int frame) const {
return int(std::lround(frameToXf(double(frame))));
}
double TimelineWidget::xToFramef(int x) const {
const double pxf = double(contentWidth()) / double(std::max(1, m_end - m_start));
return double(m_start) + double(x - contentLeft()) / std::max(pxf, 1e-9);
}
int TimelineWidget::xToFrame(int x) const {
return int(std::lround(xToFramef(x)));
}
void TimelineWidget::setFrameInternal(int frame, bool commit) {
const int f = std::clamp(frame, m_start, m_end - 1);
// 松手时若帧未变:只发 committed,禁止再发 scrubbed,否则主窗口会双次求值/刷新导致帧号与红线闪烁
if (m_currentFrame == f) {
if (commit) {
emit frameCommitted(f);
}
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);
p.fillRect(rect(), palette().base());
const QRect cr = contentRect();
const QRect kr = keyAreaRect();
const QRect rr = rulerRect();
const double fLeft = double(m_start);
const int visMin = m_start;
const int visMax = m_end;
auto frameVisible = [&](int fr) { return fr >= visMin && fr <= visMax; };
// —— 区间:贯穿标尺+轨道,贴在时间坐标上 ——
if (m_selStart >= 0 && m_selEnd >= 0) {
const int x0 = frameToX(m_selStart);
const int x1 = frameToX(m_selEnd);
const int xa = std::min(x0, x1);
const int xb = std::max(x0, x1);
QRect sel(xa, cr.top(), xb - xa, cr.height());
sel = sel.intersected(cr);
if (sel.width() > 0) {
QColor c = palette().highlight().color();
c.setAlpha(72);
p.setPen(Qt::NoPen);
p.setBrush(c);
p.drawRoundedRect(sel, 3, 3);
p.setPen(QPen(palette().highlight().color(), 1));
p.setBrush(Qt::NoBrush);
p.drawRoundedRect(sel, 3, 3);
}
}
// —— 关键帧切分背景(仅轨道):按可见范围切分,不因 m_end 隐藏尾部关键帧 ——
QVector<int> allK;
allK.reserve(m_locFrames.size() + m_scaleFrames.size() + m_imgFrames.size() + m_visFrames.size());
allK += m_locFrames;
allK += m_scaleFrames;
allK += m_imgFrames;
allK += m_visFrames;
std::sort(allK.begin(), allK.end());
allK.erase(std::unique(allK.begin(), allK.end()), allK.end());
const int v0 = visMin;
const int v1 = visMax;
QVector<int> cuts;
cuts.reserve(allK.size() + 4);
cuts.push_back(v0);
for (int k : allK) {
if (k > v0 && k < v1) {
cuts.push_back(k);
}
}
cuts.push_back(v1);
std::sort(cuts.begin(), cuts.end());
cuts.erase(std::unique(cuts.begin(), cuts.end()), cuts.end());
for (int i = 0; i + 1 < cuts.size(); ++i) {
const int a = cuts[i];
const int b = cuts[i + 1];
if (a >= b) continue;
const int x0 = frameToX(a);
const int x1 = frameToX(b);
if (x1 < kr.left() || x0 > kr.right()) continue;
QRect seg(std::max(x0, kr.left()), kr.top(), std::min(x1, kr.right()) - std::max(x0, kr.left()), kr.height());
if (seg.width() <= 0) continue;
QColor c = (i % 2) ? QColor(255, 255, 255, 28) : QColor(0, 0, 0, 12);
p.setPen(Qt::NoPen);
p.setBrush(c);
p.drawRect(seg);
}
const QColor rail = palette().mid().color();
p.setPen(Qt::NoPen);
p.setBrush(rail);
p.drawRoundedRect(kr, 4, 4);
// —— 关键帧:竖线贯穿标尺+轨道(位置钉在帧坐标上,随平移/缩放移动)——
auto drawKeyLine = [&](int fr, const QColor& col, KeyKind kind, int xOffPx) {
if (!frameVisible(fr)) return;
const int x = frameToX(fr) + xOffPx;
if (x < cr.left() - 4 || x > cr.right() + 4) return;
const bool sel = (m_selKeyFrame == fr && m_selKeyKind == kind);
QPen pen(col, sel ? 3.2 : 2.0);
pen.setCapStyle(Qt::FlatCap);
p.setPen(pen);
p.drawLine(x, cr.top() + 1, x, cr.bottom() - 1);
const int yb = kr.bottom() - 4;
QPolygonF dia;
dia << QPointF(x, yb - 5) << QPointF(x + 4, yb) << QPointF(x, yb + 4) << QPointF(x - 4, yb);
p.setPen(Qt::NoPen);
p.setBrush(col);
p.drawPolygon(dia);
};
for (int fr : m_imgFrames) drawKeyLine(fr, QColor(70, 130, 240), KeyKind::Image, -3);
for (int fr : m_locFrames) drawKeyLine(fr, QColor(240, 110, 40), KeyKind::Location, -1);
for (int fr : m_scaleFrames) drawKeyLine(fr, QColor(80, 190, 90), KeyKind::UserScale, 1);
for (int fr : m_visFrames) drawKeyLine(fr, QColor(160, 100, 230), KeyKind::Visibility, 3);
// —— 标尺底与刻度文字(与帧一一对应,随 view 滚动)——
p.setPen(Qt::NoPen);
p.setBrush(palette().alternateBase());
p.drawRoundedRect(rr, 3, 3);
const double pxf = double(contentWidth()) / double(std::max(1, m_end - m_start));
const int major = pickMajorStep(pxf);
const int minor = pickMinorStep(major);
QPen minorPen(QColor(60, 60, 60, 100));
minorPen.setWidth(1);
QPen majorPen(QColor(35, 35, 35, 170));
majorPen.setWidth(1);
QFont rulerFont = font();
rulerFont.setPointSize(std::max(7, rulerFont.pointSize() - 1));
p.setFont(rulerFont);
for (int f = static_cast<int>(std::floor(fLeft / minor) * minor); f <= visMax + minor; f += minor) {
if (f < visMin - minor) continue;
const double xf = frameToXf(f);
if (xf < rr.left() - 2 || xf > rr.right() + 2) continue;
const int xi = int(std::lround(xf));
const bool isMajor = (major > 0) && (f % major == 0);
p.setPen(isMajor ? majorPen : minorPen);
const int tickH = isMajor ? 8 : 4;
p.drawLine(xi, rr.bottom() - tickH, xi, rr.bottom());
if (isMajor && pxf > 0.28) {
p.setPen(QColor(25, 25, 25, 235));
const QString txt = QString::number(f);
const int tw = QFontMetrics(rulerFont).horizontalAdvance(txt);
int tx = xi - tw / 2;
tx = std::clamp(tx, rr.left() + 1, rr.right() - tw - 1);
p.drawText(tx, rr.top() + QFontMetrics(rulerFont).ascent() + 1, txt);
}
}
// 播放头 + 帧号(钉在轴上,与红线同一 x)
const int cx = frameToX(m_currentFrame);
p.setPen(QPen(QColor(220, 55, 55, 250), 2.0));
p.drawLine(QPoint(cx, cr.top()), QPoint(cx, cr.bottom()));
QFont bf = font();
bf.setBold(true);
p.setFont(bf);
QFontMetrics fm(bf);
const QString ft = QString::number(m_currentFrame);
const int tw = fm.horizontalAdvance(ft);
const int ph = fm.height() + 3;
const int pw = tw + 10;
int px = cx - pw / 2;
if (pw <= cr.width() - 4) {
px = std::clamp(px, cr.left() + 2, cr.right() - pw - 2);
} else {
px = cr.left() + 2;
}
const int py = cr.top() + 1;
p.setPen(QPen(QColor(180, 40, 40), 1));
p.setBrush(QColor(255, 245, 245, 245));
p.drawRoundedRect(px, py, pw, ph, 3, 3);
p.setPen(QColor(20, 20, 20));
p.drawText(px + 5, py + fm.ascent() + 1, ft);
}
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 cr = contentRect();
const int mx = e->pos().x();
const int my = e->pos().y();
auto trySelectKey = [&](KeyKind kind, const QVector<int>& frames, int xOff) -> bool {
const int nearest = findNearestFrameInTrack(frames, f);
if (nearest < 0) return false;
const int xk = frameToX(nearest) + xOff;
if (std::abs(mx - xk) > 9) return false;
if (my < cr.top() || my > cr.bottom()) return false;
m_selKeyKind = kind;
m_selKeyFrame = nearest;
emit keyframeSelectionChanged(m_selKeyKind, m_selKeyFrame);
update();
return true;
};
if (trySelectKey(KeyKind::Visibility, m_visFrames, 3) || trySelectKey(KeyKind::Image, m_imgFrames, -3)
|| trySelectKey(KeyKind::Location, m_locFrames, -1) || trySelectKey(KeyKind::UserScale, m_scaleFrames, 1)) {
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;
all += m_visFrames;
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 double steps = e->angleDelta().y() / 120.0;
const int delta = (steps > 0) ? 1 : -1;
// 滚轮只走 scrubbed:避免每格同时 scrub+committed 造成双次 refresh 与帧号闪烁
setFrameInternal(m_currentFrame + delta, false);
e->accept();
}