#include "timeline/TimelineWidget.h" #include #include #include #include #include #include #include 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& v) { std::sort(v.begin(), v.end()); v.erase(std::unique(v.begin(), v.end()), v.end()); } static bool containsFrame(const QVector& v, int f) { return std::binary_search(v.begin(), v.end(), f); } void TimelineWidget::setKeyframeTracks(const QVector& locFrames, const QVector& scaleFrames, const QVector& imgFrames, const QVector& 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& locFrames, const QVector& 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 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 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(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& 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& 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& 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 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(); }