新增根据帧数控制可见性
This commit is contained in:
@@ -1,16 +1,33 @@
|
||||
#include "timeline/TimelineWidget.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include <QFontMetrics>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QResizeEvent>
|
||||
#include <QWheelEvent>
|
||||
|
||||
namespace {
|
||||
|
||||
int clampFrame(int f, int a, int b) {
|
||||
if (a > b) std::swap(a, b);
|
||||
return std::clamp(f, a, b);
|
||||
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
|
||||
@@ -18,17 +35,20 @@ int clampFrame(int f, int a, int b) {
|
||||
TimelineWidget::TimelineWidget(QWidget* parent)
|
||||
: QWidget(parent) {
|
||||
setMouseTracking(true);
|
||||
setMinimumHeight(28);
|
||||
// 单行紧凑:标尺 + 轨道(帧号画在播放头处,随坐标轴滚动)
|
||||
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) {
|
||||
if (m_start == start && m_end == end) {
|
||||
return;
|
||||
}
|
||||
m_start = start;
|
||||
m_end = end;
|
||||
m_currentFrame = clampFrame(m_currentFrame, m_start, m_end);
|
||||
(void)start;
|
||||
(void)end;
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -36,6 +56,15 @@ 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;
|
||||
@@ -43,41 +72,40 @@ void TimelineWidget::setSelectionRange(int start, int end) {
|
||||
update();
|
||||
return;
|
||||
}
|
||||
m_selStart = clampFrame(std::min(start, end), m_start, m_end);
|
||||
m_selEnd = clampFrame(std::max(start, end), m_start, m_end);
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
static void uniqSort(QVector<int>& v) {
|
||||
std::sort(v.begin(), v.end());
|
||||
v.erase(std::unique(v.begin(), v.end()), v.end());
|
||||
}
|
||||
|
||||
auto 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);
|
||||
// 轨道变了:若当前选中的关键帧不再存在,则清除
|
||||
auto contains = [](const QVector<int>& v, int f) {
|
||||
return std::binary_search(v.begin(), v.end(), f);
|
||||
};
|
||||
uniqSort(m_visFrames);
|
||||
|
||||
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 (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;
|
||||
@@ -86,33 +114,67 @@ void TimelineWidget::setKeyframeTracks(const core::Project::Entity* e) {
|
||||
update();
|
||||
}
|
||||
|
||||
QRect TimelineWidget::trackRect() const {
|
||||
const int pad = 8;
|
||||
const int h = height();
|
||||
return QRect(pad, 0, std::max(1, width() - pad * 2), h);
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
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 {
|
||||
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)));
|
||||
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 = clampFrame(frame, m_start, m_end);
|
||||
if (m_currentFrame == f && !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;
|
||||
@@ -127,64 +189,164 @@ void TimelineWidget::paintEvent(QPaintEvent*) {
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
const QRect r = rect();
|
||||
p.fillRect(r, palette().base());
|
||||
p.fillRect(rect(), 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);
|
||||
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;
|
||||
|
||||
// selection range
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// —— 关键帧切分背景(仅轨道):按可见范围切分,不因 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);
|
||||
};
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
// current frame caret
|
||||
// —— 标尺底与刻度文字(与帧一一对应,随 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(palette().highlight().color(), 2.0));
|
||||
p.drawLine(QPoint(cx, tr.top() - 6), QPoint(cx, tr.bottom() + 6));
|
||||
}
|
||||
p.setPen(QPen(QColor(220, 55, 55, 250), 2.0));
|
||||
p.drawLine(QPoint(cx, cr.top()), QPoint(cx, cr.bottom()));
|
||||
|
||||
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);
|
||||
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) {
|
||||
@@ -241,44 +403,36 @@ void TimelineWidget::mouseReleaseEvent(QMouseEvent* e) {
|
||||
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;
|
||||
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 laneY) -> bool {
|
||||
auto trySelectKey = [&](KeyKind kind, const QVector<int>& frames, int xOff) -> 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;
|
||||
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;
|
||||
};
|
||||
|
||||
// 先尝试命中关键帧(按 lane 优先)
|
||||
if (trySelectKey(KeyKind::Image, m_imgFrames, yImg)
|
||||
|| trySelectKey(KeyKind::Location, m_locFrames, yLoc)
|
||||
|| trySelectKey(KeyKind::UserScale, m_scaleFrames, ySc)) {
|
||||
// 选中关键帧时清掉区间
|
||||
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;
|
||||
@@ -286,7 +440,6 @@ void TimelineWidget::mouseReleaseEvent(QMouseEvent* e) {
|
||||
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;
|
||||
@@ -303,8 +456,9 @@ void TimelineWidget::mouseReleaseEvent(QMouseEvent* e) {
|
||||
}
|
||||
|
||||
void TimelineWidget::wheelEvent(QWheelEvent* e) {
|
||||
const int delta = (e->angleDelta().y() > 0) ? 1 : -1;
|
||||
setFrameInternal(m_currentFrame + delta, true);
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/domain/Project.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QResizeEvent;
|
||||
|
||||
class TimelineWidget final : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TimelineWidget(QWidget* parent = nullptr);
|
||||
|
||||
// 兼容旧接口:NLA/片段系统下时间轴始终固定为 0..600(local frame)。
|
||||
void setFrameRange(int start, int end);
|
||||
void setCurrentFrame(int frame);
|
||||
/// 由主窗口同步工程帧时调用:不发射 frameScrubbed,避免与拖动/刷新打架造成数字闪烁
|
||||
void setCurrentFrameProgrammatic(int frame);
|
||||
int currentFrame() const { return m_currentFrame; }
|
||||
|
||||
void setSelectionRange(int start, int end); // -1,-1 清除
|
||||
int selectionStart() const { return m_selStart; }
|
||||
int selectionEnd() const { return m_selEnd; }
|
||||
|
||||
// 只显示“当前选中实体”的关键帧标记
|
||||
void setKeyframeTracks(const core::Project::Entity* entityOrNull);
|
||||
// 轨道数据直接由上层提供(通常来自当前条带引用的 clip)。
|
||||
void setKeyframeTracks(const QVector<int>& locFrames,
|
||||
const QVector<int>& scaleFrames,
|
||||
const QVector<int>& imgFrames,
|
||||
const QVector<int>& visFrames);
|
||||
void setToolKeyframeTracks(const QVector<int>& locFrames,
|
||||
const QVector<int>& visFrames);
|
||||
|
||||
enum class KeyKind { None, Location, UserScale, Image };
|
||||
enum class KeyKind { None, Location, UserScale, Image, Visibility };
|
||||
KeyKind selectedKeyKind() const { return m_selKeyKind; }
|
||||
int selectedKeyFrame() const { return m_selKeyFrame; }
|
||||
bool hasSelectedKeyframe() const { return m_selKeyKind != KeyKind::None && m_selKeyFrame >= 0; }
|
||||
|
||||
signals:
|
||||
void frameScrubbed(int frame); // 拖动中实时触发(用于实时预览)
|
||||
void frameCommitted(int frame); // 松手/点击确认(用于较重的刷新)
|
||||
void frameScrubbed(int frame);
|
||||
void frameCommitted(int frame);
|
||||
void contextMenuRequested(const QPoint& globalPos, int frame);
|
||||
void keyframeSelectionChanged(KeyKind kind, int frame);
|
||||
void intervalSelectionChanged(int start, int end);
|
||||
@@ -38,18 +46,26 @@ protected:
|
||||
void mouseMoveEvent(QMouseEvent*) override;
|
||||
void mouseReleaseEvent(QMouseEvent*) override;
|
||||
void wheelEvent(QWheelEvent*) override;
|
||||
void resizeEvent(QResizeEvent* e) override;
|
||||
|
||||
private:
|
||||
int xToFrame(int x) const;
|
||||
int contentLeft() const { return 6; }
|
||||
int contentWidth() const { return std::max(1, width() - 12); }
|
||||
QRect contentRect() const;
|
||||
|
||||
double frameToXf(double frame) const;
|
||||
int frameToX(int frame) const;
|
||||
QRect trackRect() const;
|
||||
double xToFramef(int x) const;
|
||||
int xToFrame(int x) const;
|
||||
|
||||
QRect rulerRect() const;
|
||||
QRect keyAreaRect() const;
|
||||
|
||||
void setFrameInternal(int frame, bool commit);
|
||||
|
||||
private:
|
||||
int m_start = 0;
|
||||
int m_end = 600;
|
||||
int m_currentFrame = 0;
|
||||
static constexpr int kStart = 0;
|
||||
static constexpr int kEnd = 600; // exclusive for mapping, inclusive for UI labels
|
||||
int m_currentFrame = 0; // local frame: 0..599
|
||||
|
||||
int m_selStart = -1;
|
||||
int m_selEnd = -1;
|
||||
@@ -58,12 +74,13 @@ private:
|
||||
QPoint m_pressPos;
|
||||
bool m_moved = false;
|
||||
|
||||
// snapshot(避免频繁遍历 workspace)
|
||||
static constexpr int kRulerHeight = 14;
|
||||
|
||||
QVector<int> m_locFrames;
|
||||
QVector<int> m_scaleFrames;
|
||||
QVector<int> m_imgFrames;
|
||||
QVector<int> m_visFrames;
|
||||
|
||||
KeyKind m_selKeyKind = KeyKind::None;
|
||||
int m_selKeyFrame = -1;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user