381 lines
12 KiB
C++
381 lines
12 KiB
C++
/*
|
|
* Copyright (c) 2011-2026 Meltytech, LLC
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "scrubbar.h"
|
|
|
|
#include "mltcontroller.h"
|
|
#include "settings.h"
|
|
|
|
#include <QToolTip>
|
|
#include <QtWidgets>
|
|
|
|
static const int selectionSize = 14; /// the height of the top bar
|
|
#ifndef CLAMP
|
|
#define CLAMP(x, min, max) (((x) < (min)) ? (min) : ((x) > (max)) ? (max) : (x))
|
|
#endif
|
|
|
|
ScrubBar::ScrubBar(QWidget *parent)
|
|
: QWidget(parent)
|
|
, m_head(-1)
|
|
, m_scale(-1)
|
|
, m_fps(25)
|
|
, m_max(1)
|
|
, m_in(-1)
|
|
, m_out(-1)
|
|
, m_margin(14) /// left and right margins
|
|
, m_activeControl(CONTROL_NONE)
|
|
, m_timecodeWidth(0)
|
|
, m_loopStart(-1)
|
|
, m_loopEnd(-1)
|
|
{
|
|
setMouseTracking(true);
|
|
setMinimumHeight(fontMetrics().height() + selectionSize);
|
|
setWhatsThis("https://forum.shotcut.org/t/trimming-clips/49216/1");
|
|
}
|
|
|
|
void ScrubBar::setScale(int maximum)
|
|
{
|
|
if (!m_timecodeWidth) {
|
|
const int fontSize = font().pointSize()
|
|
- (font().pointSize() > 10 ? 2 : (font().pointSize() > 8 ? 1 : 0));
|
|
setFont(QFont(font().family(), fontSize * devicePixelRatioF()));
|
|
m_timecodeWidth = fontMetrics().horizontalAdvance("00:00:00:00") / devicePixelRatioF();
|
|
}
|
|
m_max = maximum;
|
|
/// m_scale is the pixels per frame ratio
|
|
m_scale = m_max > 0 ? (double) (width() - 2 * m_margin) / (double) m_max : -1;
|
|
if (m_scale == 0)
|
|
m_scale = -1;
|
|
m_secondsPerTick = qMax(qRound(double(m_timecodeWidth * 1.8) / m_scale / m_fps), 1);
|
|
if (m_secondsPerTick > 3600)
|
|
// force to a multiple of one hour
|
|
m_secondsPerTick += 3600 - m_secondsPerTick % 3600;
|
|
else if (m_secondsPerTick > 300)
|
|
// force to a multiple of 5 minutes
|
|
m_secondsPerTick += 300 - m_secondsPerTick % 300;
|
|
else if (m_secondsPerTick > 60)
|
|
// force to a multiple of one minute
|
|
m_secondsPerTick += 60 - m_secondsPerTick % 60;
|
|
else if (m_secondsPerTick > 5)
|
|
// force to a multiple of 10 seconds
|
|
m_secondsPerTick += 10 - m_secondsPerTick % 10;
|
|
else if (m_secondsPerTick > 2)
|
|
// force to a multiple of 5 seconds
|
|
m_secondsPerTick += 5 - m_secondsPerTick % 5;
|
|
/// m_interval is the number of pixels per major tick to be labeled with time
|
|
m_interval = qRound(double(m_secondsPerTick) * m_fps * m_scale);
|
|
m_head = -1;
|
|
updatePixmap();
|
|
}
|
|
|
|
void ScrubBar::setFramerate(double fps)
|
|
{
|
|
m_fps = fps;
|
|
}
|
|
|
|
int ScrubBar::position() const
|
|
{
|
|
return m_head;
|
|
}
|
|
|
|
void ScrubBar::setInPoint(int in)
|
|
{
|
|
m_in = qMax(in, -1);
|
|
updatePixmap();
|
|
emit inChanged(in);
|
|
}
|
|
|
|
void ScrubBar::setOutPoint(int out)
|
|
{
|
|
m_out = qMin(out, m_max);
|
|
updatePixmap();
|
|
emit outChanged(out);
|
|
}
|
|
|
|
void ScrubBar::setMarkers(const QList<int> &list)
|
|
{
|
|
m_markers = list;
|
|
updatePixmap();
|
|
}
|
|
|
|
void ScrubBar::setLoopRange(int start, int end)
|
|
{
|
|
m_loopStart = start;
|
|
m_loopEnd = end;
|
|
updatePixmap();
|
|
}
|
|
|
|
void ScrubBar::mousePressEvent(QMouseEvent *event)
|
|
{
|
|
int x = event->position().x() - m_margin;
|
|
int in = m_in * m_scale;
|
|
int out = m_out * m_scale;
|
|
int head = m_head * m_scale;
|
|
int pos = CLAMP(x / m_scale, 0, m_max);
|
|
|
|
if (m_in > -1 && m_out > -1) {
|
|
if (x >= in - 12 && x <= in + 6) {
|
|
m_activeControl = CONTROL_IN;
|
|
setInPoint(pos);
|
|
} else if (x >= out - 6 && x <= out + 12) {
|
|
m_activeControl = CONTROL_OUT;
|
|
setOutPoint(pos);
|
|
}
|
|
}
|
|
if (m_head > -1) {
|
|
if (m_activeControl == CONTROL_NONE) {
|
|
m_activeControl = CONTROL_HEAD;
|
|
m_head = pos;
|
|
const int offset = height() / 2;
|
|
const int x = head;
|
|
const int w = qAbs(x - head);
|
|
update(m_margin + x - offset, 0, w + 2 * offset, height());
|
|
}
|
|
}
|
|
if (m_activeControl >= CONTROL_IN && !Settings.playerPauseAfterSeek())
|
|
emit paused(pos);
|
|
emit seeked(pos);
|
|
}
|
|
|
|
void ScrubBar::mouseReleaseEvent(QMouseEvent *event)
|
|
{
|
|
Q_UNUSED(event)
|
|
m_activeControl = CONTROL_NONE;
|
|
}
|
|
|
|
void ScrubBar::mouseMoveEvent(QMouseEvent *event)
|
|
{
|
|
int x = event->position().x() - m_margin;
|
|
int pos = CLAMP(x / m_scale, 0, m_max);
|
|
|
|
if (event->buttons() & Qt::LeftButton) {
|
|
if (m_activeControl == CONTROL_IN)
|
|
setInPoint(pos);
|
|
else if (m_activeControl == CONTROL_OUT)
|
|
setOutPoint(pos);
|
|
else if (m_activeControl == CONTROL_HEAD) {
|
|
const int head = m_head * m_scale;
|
|
const int offset = height() / 2;
|
|
const int x = head;
|
|
const int w = qAbs(x - head);
|
|
update(m_margin + x - offset, 0, w + 2 * offset, height());
|
|
m_head = pos;
|
|
}
|
|
if (m_activeControl >= CONTROL_IN && !Settings.playerPauseAfterSeek())
|
|
emit paused(pos);
|
|
emit seeked(pos);
|
|
} else if (event->buttons() == Qt::NoButton && MLT.producer()) {
|
|
QString text = QString::fromLatin1(
|
|
MLT.producer()->frames_to_time(pos, Settings.timeFormat()));
|
|
QToolTip::showText(event->globalPosition().toPoint(), text);
|
|
}
|
|
}
|
|
|
|
bool ScrubBar::onSeek(int value)
|
|
{
|
|
if (m_activeControl != CONTROL_HEAD)
|
|
m_head = value;
|
|
int oldPos = m_cursorPosition;
|
|
m_cursorPosition = value * m_scale;
|
|
const int offset = height() / 2;
|
|
const int x = qMin(oldPos, m_cursorPosition);
|
|
const int w = qAbs(oldPos - m_cursorPosition);
|
|
update(m_margin + x - offset, 0, w + 2 * offset, height());
|
|
return true;
|
|
}
|
|
|
|
void ScrubBar::paintEvent(QPaintEvent *e)
|
|
{
|
|
QPen pen(QBrush(palette().text().color()), 2);
|
|
QPainter p(this);
|
|
QRect r = e->rect();
|
|
p.setClipRect(r);
|
|
p.drawPixmap(0, 0, width(), height(), m_pixmap);
|
|
|
|
if (!isEnabled())
|
|
return;
|
|
|
|
// draw pointer
|
|
QPolygon pa(3);
|
|
const int x = selectionSize / 2 - 1;
|
|
int head = m_margin + m_cursorPosition;
|
|
pa.setPoints(3, head - x - 1, 0, head + x, 0, head, x);
|
|
p.setBrush(palette().text().color());
|
|
p.setPen(Qt::NoPen);
|
|
p.drawPolygon(pa);
|
|
p.setPen(pen);
|
|
if (m_head >= 0) {
|
|
head = m_margin + m_head * m_scale;
|
|
p.drawLine(head, 0, head, height() - 1);
|
|
}
|
|
|
|
// draw in point
|
|
if (m_in > -1) {
|
|
const int in = m_margin + m_in * m_scale;
|
|
pa.setPoints(3,
|
|
in - selectionSize / 2,
|
|
0,
|
|
in - selectionSize / 2,
|
|
selectionSize - 1,
|
|
in - 1,
|
|
selectionSize / 2);
|
|
p.setBrush(palette().text().color());
|
|
p.setPen(Qt::NoPen);
|
|
p.drawPolygon(pa);
|
|
p.setPen(pen);
|
|
p.drawLine(in, 0, in, selectionSize - 1);
|
|
}
|
|
|
|
// draw out point
|
|
if (m_out > -1) {
|
|
const int out = m_margin + m_out * m_scale;
|
|
pa.setPoints(3,
|
|
out + selectionSize / 2,
|
|
0,
|
|
out + selectionSize / 2,
|
|
selectionSize - 1,
|
|
out,
|
|
selectionSize / 2);
|
|
p.setBrush(palette().text().color());
|
|
p.setPen(Qt::NoPen);
|
|
p.drawPolygon(pa);
|
|
p.setPen(pen);
|
|
p.drawLine(out, 0, out, selectionSize - 1);
|
|
}
|
|
}
|
|
|
|
void ScrubBar::resizeEvent(QResizeEvent *)
|
|
{
|
|
setScale(m_max);
|
|
}
|
|
|
|
bool ScrubBar::event(QEvent *event)
|
|
{
|
|
QWidget::event(event);
|
|
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
|
|
updatePixmap();
|
|
return false;
|
|
}
|
|
|
|
void ScrubBar::updatePixmap()
|
|
{
|
|
const auto ratio = devicePixelRatioF();
|
|
const int l_width = width() * ratio;
|
|
const int l_height = height() * ratio;
|
|
const int l_margin = m_margin * ratio;
|
|
const int l_selectionSize = selectionSize * ratio;
|
|
const int l_interval = m_interval * ratio;
|
|
const int l_timecodeWidth = m_timecodeWidth * ratio;
|
|
m_pixmap = QPixmap(l_width, l_height);
|
|
m_pixmap.fill(palette().window().color());
|
|
QPainter p(&m_pixmap);
|
|
p.setFont(font());
|
|
const int markerHeight = fontMetrics().ascent() + 2 * ratio;
|
|
QPen pen;
|
|
|
|
if (!isEnabled()) {
|
|
p.fillRect(0, 0, l_width, l_height, palette().window().color());
|
|
p.end();
|
|
update();
|
|
return;
|
|
}
|
|
|
|
// background color
|
|
p.fillRect(l_margin, 0, l_width - 2 * l_margin, l_height, palette().base().color());
|
|
|
|
// selected region
|
|
if (m_in > -1 && m_out > m_in) {
|
|
const int in = m_in * m_scale * ratio;
|
|
const int out = m_out * m_scale * ratio;
|
|
p.fillRect(l_margin + in, 0, out - in, l_selectionSize, Qt::red);
|
|
p.fillRect(l_margin + in + (2 + ratio),
|
|
ratio, // 2 for the in point line
|
|
out - in - 2 * (2 + ratio) - qFloor(0.5 * ratio),
|
|
l_selectionSize - ratio * 2,
|
|
palette().highlight().color());
|
|
}
|
|
|
|
// draw time ticks
|
|
pen.setColor(palette().text().color());
|
|
pen.setWidth(qRound(ratio));
|
|
p.setPen(pen);
|
|
if (l_interval > 2) {
|
|
for (int x = l_margin; x < l_width - l_margin; x += l_interval) {
|
|
p.drawLine(x, l_selectionSize, x, l_height - 1);
|
|
if (x + l_interval / 4 < l_width - l_margin)
|
|
p.drawLine(x + l_interval / 4,
|
|
l_height - 3 * ratio,
|
|
x + l_interval / 4,
|
|
l_height - 1);
|
|
if (x + l_interval / 2 < l_width - l_margin)
|
|
p.drawLine(x + l_interval / 2,
|
|
l_height - 7 * ratio,
|
|
x + l_interval / 2,
|
|
l_height - 1);
|
|
if (x + l_interval * 3 / 4 < l_width - l_margin)
|
|
p.drawLine(x + l_interval * 3 / 4,
|
|
l_height - 3 * ratio,
|
|
x + l_interval * 3 / 4,
|
|
l_height - 1);
|
|
}
|
|
}
|
|
|
|
// draw timecode
|
|
const auto timeFormat = Settings.timeFormat();
|
|
if (l_interval > l_timecodeWidth && MLT.producer()) {
|
|
int x = l_margin;
|
|
for (int i = 0; x < l_width - l_margin - l_timecodeWidth; i++, x += l_interval) {
|
|
int y = l_selectionSize + fontMetrics().ascent() - 2 * ratio;
|
|
int frames = qRound(i * m_fps * m_secondsPerTick);
|
|
p.drawText(x + 2 * ratio,
|
|
y,
|
|
QString(MLT.producer()->frames_to_time(frames, timeFormat)).left(8));
|
|
}
|
|
}
|
|
|
|
// draw markers
|
|
if (m_in < 0 && m_out < 0) {
|
|
int i = 1;
|
|
foreach (int pos, m_markers) {
|
|
const int x = l_margin + pos * m_scale * ratio;
|
|
if (x < 0)
|
|
continue;
|
|
QString s = QString::number(i++);
|
|
int markerWidth = fontMetrics().horizontalAdvance(s) * 1.5;
|
|
p.fillRect(x, 0, 1, l_height, palette().highlight().color());
|
|
p.fillRect(x - markerWidth / 2,
|
|
0,
|
|
markerWidth,
|
|
markerHeight,
|
|
palette().highlight().color());
|
|
p.drawText(x - markerWidth / 3, markerHeight - 2 * ratio, s);
|
|
}
|
|
}
|
|
|
|
// draw loop range
|
|
if (m_loopStart > -1 && m_loopEnd > -1) {
|
|
const int start = m_loopStart * m_scale * ratio;
|
|
const int end = m_loopEnd * m_scale * ratio;
|
|
QColor loopColor = palette().highlight().color();
|
|
loopColor.setAlphaF(0.5);
|
|
p.fillRect(l_margin + start, l_height - 7 * ratio, end - start, l_height * ratio, loopColor);
|
|
}
|
|
|
|
p.end();
|
|
update();
|
|
}
|