Files
hfut-bishe/client/gui/timeline/TimelineWidget.cpp

465 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "timeline/TimelineWidget.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(QStringLiteral("片段时间轴(固定 0-600):左键拖动播放头;滚轮:逐帧"));
}
void TimelineWidget::resizeEvent(QResizeEvent* e) {
QWidget::resizeEvent(e);
update();
}
void TimelineWidget::setFrameRange(int start, int end) {
(void)start;
(void)end;
update();
}
void TimelineWidget::setCurrentFrame(int frame) {
setFrameInternal(frame, false);
}
void TimelineWidget::setCurrentFrameProgrammatic(int frame) {
const int f = std::clamp(frame, kStart, kEnd - 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, kStart, kEnd - 1);
m_selEnd = std::clamp(hi, m_selStart, kEnd - 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);
}
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);
}
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, kEnd - kStart));
return double(contentLeft()) + (frame - double(kStart)) * 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, kEnd - kStart));
return double(kStart) + 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, kStart, kEnd - 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(kStart);
const int visMin = kStart;
const int visMax = kEnd;
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, kEnd - kStart));
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();
}