#include "timeline/TimelineWidget.h" #include #include #include #include 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& 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& 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& 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& 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 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& 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 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(); }