chatgui: нативный move/resize (Wayland) + фикс замирания видео при перемотке
- окно: startSystemMove/startSystemResize вместо ручного move/setGeometry (Wayland игнорирует позицию окна) — драг и resize за левый/верхний край работают - пропорции: снэп к videoAspect после нативного resize (дебаунс 150мс) - перемотка: backpressure по m_buffered (декод не убегает вперёд и воркер живёт для seek), seek через av_seek_frame(-1, us), пропуск кадров с AV_NOPTS_VALUE - SeekSlider: клик по шкале = прыжок к позиции - крестик закрытия справа вверху; курсор контролов — стрелка - test_video_engine: проверка seek
This commit is contained in:
1 parent
f2b34459c3
commit
645591bb64
5 files changed
+139
-122
No files matched your search
@@ -15,6 +15,7 @@
|
||||
#include <QApplication>
|
||||
#include <QScreen>
|
||||
#include <QStyle>
|
||||
#include <QWindow>
|
||||
|
||||
/* ─── VideoRenderWidget ─── */
|
||||
|
||||
@@ -51,13 +52,25 @@ static QString fmtTime(qint64 ms) {
|
||||
return QString("%1:%2").arg(s / 60).arg(s % 60, 2, 10, QChar('0'));
|
||||
}
|
||||
|
||||
static QPoint mouseGlobalPos(const QMouseEvent* e) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
return e->globalPosition().toPoint();
|
||||
#else
|
||||
return e->globalPos();
|
||||
#endif
|
||||
}
|
||||
/* Слайдер перемотки: клик по любому месту шкалы = прыжок к позиции (драг кружка сохраняется). */
|
||||
class SeekSlider : public QSlider {
|
||||
public:
|
||||
using QSlider::QSlider;
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent* e) override {
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
setSliderDown(true);
|
||||
int v = QStyle::sliderValueFromPosition(minimum(), maximum(),
|
||||
(int)e->position().x(), width());
|
||||
setValue(v);
|
||||
emit sliderPressed();
|
||||
e->accept();
|
||||
} else {
|
||||
QSlider::mousePressEvent(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
VideoPlayerWindow::VideoPlayerWindow(QWidget* parent)
|
||||
: QWidget(parent, Qt::Window | Qt::FramelessWindowHint)
|
||||
@@ -74,6 +87,7 @@ VideoPlayerWindow::VideoPlayerWindow(QWidget* parent)
|
||||
|
||||
m_controls = new QWidget(this);
|
||||
m_controls->setObjectName("videoControls");
|
||||
m_controls->setCursor(Qt::ArrowCursor);
|
||||
m_controls->installEventFilter(this);
|
||||
m_controls->setStyleSheet(
|
||||
"#videoControls { background-color: rgba(0,0,0,0.65); }"
|
||||
@@ -86,9 +100,7 @@ VideoPlayerWindow::VideoPlayerWindow(QWidget* parent)
|
||||
|
||||
m_playBtn = new QPushButton(QStringLiteral("⏸"), m_controls);
|
||||
m_playBtn->setToolTip(QStringLiteral("Пауза"));
|
||||
m_closeBtn = new QPushButton(QStringLiteral("✕"), m_controls);
|
||||
m_closeBtn->setToolTip(QStringLiteral("Закрыть"));
|
||||
m_slider = new QSlider(Qt::Horizontal, m_controls);
|
||||
m_slider = new SeekSlider(Qt::Horizontal, m_controls);
|
||||
m_slider->setRange(0, 0);
|
||||
m_timeLabel = new QLabel(QStringLiteral("0:00 / 0:00"), m_controls);
|
||||
|
||||
@@ -98,13 +110,26 @@ VideoPlayerWindow::VideoPlayerWindow(QWidget* parent)
|
||||
lay->addWidget(m_playBtn);
|
||||
lay->addWidget(m_slider, 1);
|
||||
lay->addWidget(m_timeLabel);
|
||||
lay->addWidget(m_closeBtn);
|
||||
|
||||
/* крестик закрытия — отдельно, справа вверху */
|
||||
m_closeBtn = new QPushButton(QStringLiteral("✕"), this);
|
||||
m_closeBtn->setToolTip(QStringLiteral("Закрыть"));
|
||||
m_closeBtn->setCursor(Qt::ArrowCursor);
|
||||
m_closeBtn->setStyleSheet(
|
||||
"QPushButton { background: rgba(0,0,0,0.5); color: white; border: none; "
|
||||
"border-radius: 4px; font-size: 14px; }"
|
||||
"QPushButton:hover { background: rgba(255,255,255,0.25); }");
|
||||
|
||||
m_hideTimer = new QTimer(this);
|
||||
m_hideTimer->setSingleShot(true);
|
||||
m_hideTimer->setInterval(2500);
|
||||
connect(m_hideTimer, &QTimer::timeout, this, &VideoPlayerWindow::hideControls);
|
||||
|
||||
m_snapTimer = new QTimer(this);
|
||||
m_snapTimer->setSingleShot(true);
|
||||
m_snapTimer->setInterval(150);
|
||||
connect(m_snapTimer, &QTimer::timeout, this, &VideoPlayerWindow::snapToAspect);
|
||||
|
||||
connect(m_playBtn, &QPushButton::clicked, this, &VideoPlayerWindow::onPlayPause);
|
||||
connect(m_closeBtn, &QPushButton::clicked, this, &VideoPlayerWindow::onClose);
|
||||
connect(m_slider, &QSlider::sliderPressed, this, &VideoPlayerWindow::onSliderPressed);
|
||||
@@ -136,6 +161,7 @@ void VideoPlayerWindow::onEngineOpened(int width, int height, qint64 durationMs,
|
||||
Q_UNUSED(hasAudio);
|
||||
m_slider->setRange(0, (int)durationMs);
|
||||
updateTimeLabels(0);
|
||||
m_videoAspect = (height > 0) ? (double)width / height : 0.0;
|
||||
|
||||
QScreen* scr = QApplication::primaryScreen();
|
||||
QRect ag = scr ? scr->availableGeometry() : QRect(0, 0, 1280, 720);
|
||||
@@ -226,6 +252,7 @@ void VideoPlayerWindow::toggleFullscreen() {
|
||||
|
||||
void VideoPlayerWindow::setControlsVisible(bool visible) {
|
||||
m_controls->setVisible(visible);
|
||||
m_closeBtn->setVisible(visible);
|
||||
}
|
||||
|
||||
void VideoPlayerWindow::showControls() {
|
||||
@@ -285,11 +312,10 @@ void VideoPlayerWindow::mousePressEvent(QMouseEvent* e) {
|
||||
}
|
||||
ResizeMode m = hitTestResize(e->pos());
|
||||
if (m != ResizeNone) {
|
||||
beginResize(m, mouseGlobalPos(e));
|
||||
m_resizeEdges = edgesForMode(m);
|
||||
if (windowHandle()) windowHandle()->startSystemResize(m_resizeEdges);
|
||||
} else {
|
||||
m_dragging = true;
|
||||
m_dragStart = mouseGlobalPos(e);
|
||||
m_windowStart = frameGeometry().topLeft();
|
||||
if (windowHandle()) windowHandle()->startSystemMove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,51 +325,25 @@ void VideoPlayerWindow::mouseMoveEvent(QMouseEvent* e) {
|
||||
showControls();
|
||||
return;
|
||||
}
|
||||
if (m_resizeMode != ResizeNone && (e->buttons() & Qt::LeftButton)) {
|
||||
applyResize(mouseGlobalPos(e));
|
||||
return;
|
||||
}
|
||||
if (m_dragging && (e->buttons() & Qt::LeftButton)) {
|
||||
move(m_windowStart + (mouseGlobalPos(e) - m_dragStart));
|
||||
return;
|
||||
}
|
||||
if (!(e->buttons() & Qt::LeftButton))
|
||||
updateResizeCursor(e->pos());
|
||||
}
|
||||
|
||||
void VideoPlayerWindow::mouseReleaseEvent(QMouseEvent* e) {
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
m_dragging = false;
|
||||
m_resizeMode = ResizeNone;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoPlayerWindow::mouseDoubleClickEvent(QMouseEvent* e) {
|
||||
if (e->button() == Qt::LeftButton) toggleFullscreen();
|
||||
}
|
||||
|
||||
bool VideoPlayerWindow::eventFilter(QObject* obj, QEvent* ev) {
|
||||
/* Резрез за нижний край: панель контролов перекрывает нижнюю кромку окна,
|
||||
* поэтому маршрутизируем события из её пустых полей в логику resize. */
|
||||
if (obj == m_controls && !m_fullscreen) {
|
||||
QEvent::Type t = ev->type();
|
||||
if (t == QEvent::MouseButtonPress || t == QEvent::MouseMove || t == QEvent::MouseButtonRelease) {
|
||||
QMouseEvent* me = static_cast<QMouseEvent*>(ev);
|
||||
* поэтому маршрутизируем press из её пустых полей в нативный resize. */
|
||||
if (obj == m_controls && !m_fullscreen && ev->type() == QEvent::MouseButtonPress) {
|
||||
QMouseEvent* me = static_cast<QMouseEvent*>(ev);
|
||||
if (me->button() == Qt::LeftButton) {
|
||||
QPoint winPos = m_controls->mapTo(this, me->pos());
|
||||
ResizeMode m = hitTestResize(winPos);
|
||||
if (m == ResizeNone) return QWidget::eventFilter(obj, ev);
|
||||
|
||||
QPoint gp = mouseGlobalPos(me);
|
||||
if (t == QEvent::MouseButtonPress && me->button() == Qt::LeftButton) {
|
||||
beginResize(m, gp);
|
||||
return true;
|
||||
}
|
||||
if (t == QEvent::MouseMove && m_resizeMode != ResizeNone && (me->buttons() & Qt::LeftButton)) {
|
||||
applyResize(gp);
|
||||
return true;
|
||||
}
|
||||
if (t == QEvent::MouseButtonRelease && me->button() == Qt::LeftButton) {
|
||||
m_resizeMode = ResizeNone;
|
||||
if (m != ResizeNone) {
|
||||
m_resizeEdges = edgesForMode(m);
|
||||
if (windowHandle()) windowHandle()->startSystemResize(m_resizeEdges);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -363,6 +363,10 @@ void VideoPlayerWindow::resizeEvent(QResizeEvent* e) {
|
||||
int ch = 48;
|
||||
m_controls->setGeometry(0, height() - ch, width(), ch);
|
||||
m_controls->raise();
|
||||
m_closeBtn->setGeometry(width() - 40, 8, 32, 32);
|
||||
m_closeBtn->raise();
|
||||
/* во время нативного resize снэпаем пропорцию после окончания жеста */
|
||||
if (m_resizeEdges != 0) m_snapTimer->start();
|
||||
}
|
||||
|
||||
VideoPlayerWindow::ResizeMode VideoPlayerWindow::hitTestResize(const QPoint& pos) const {
|
||||
@@ -393,47 +397,33 @@ void VideoPlayerWindow::updateResizeCursor(const QPoint& pos) {
|
||||
}
|
||||
}
|
||||
|
||||
void VideoPlayerWindow::beginResize(ResizeMode m, const QPoint& globalPos) {
|
||||
m_resizeMode = m;
|
||||
m_resizeStartGeom = geometry();
|
||||
m_resizeStartGlobal = globalPos;
|
||||
Qt::Edges VideoPlayerWindow::edgesForMode(ResizeMode m) {
|
||||
switch (m) {
|
||||
case ResizeLeft: return Qt::LeftEdge;
|
||||
case ResizeRight: return Qt::RightEdge;
|
||||
case ResizeTop: return Qt::TopEdge;
|
||||
case ResizeBottom: return Qt::BottomEdge;
|
||||
case ResizeTopLeft: return Qt::TopEdge | Qt::LeftEdge;
|
||||
case ResizeTopRight: return Qt::TopEdge | Qt::RightEdge;
|
||||
case ResizeBottomLeft: return Qt::BottomEdge | Qt::LeftEdge;
|
||||
case ResizeBottomRight: return Qt::BottomEdge | Qt::RightEdge;
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
|
||||
void VideoPlayerWindow::applyResize(const QPoint& globalPos) {
|
||||
const int kMinW = 160;
|
||||
QRect g = m_resizeStartGeom;
|
||||
double aspect = (g.height() > 0) ? (double)g.width() / g.height() : 1.0;
|
||||
int minH = qMax(1, qRound(kMinW / aspect));
|
||||
int dx = globalPos.x() - m_resizeStartGlobal.x();
|
||||
int dy = globalPos.y() - m_resizeStartGlobal.y();
|
||||
void VideoPlayerWindow::snapToAspect() {
|
||||
if (m_videoAspect <= 0.0 || m_resizeEdges == 0) return;
|
||||
Qt::Edges edges = m_resizeEdges;
|
||||
m_resizeEdges = {}; /* сброс до resize(), чтобы снэп не ре-триггерил сам себя */
|
||||
int w = width(), h = height();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
int newW = g.width(), newH = g.height();
|
||||
bool widthDriven = true;
|
||||
switch (m_resizeMode) {
|
||||
case ResizeRight: case ResizeTopRight: case ResizeBottomRight: newW = g.width() + dx; break;
|
||||
case ResizeLeft: case ResizeTopLeft: case ResizeBottomLeft: newW = g.width() - dx; break;
|
||||
case ResizeBottom: newH = g.height() + dy; widthDriven = false; break;
|
||||
case ResizeTop: newH = g.height() - dy; widthDriven = false; break;
|
||||
default: return;
|
||||
bool verticalOnly = !(edges & (Qt::LeftEdge | Qt::RightEdge));
|
||||
if (verticalOnly) {
|
||||
int newW = qRound(h * m_videoAspect);
|
||||
if (qAbs(newW - w) > 1) resize(newW, h);
|
||||
} else {
|
||||
int newH = qRound(w / m_videoAspect);
|
||||
if (qAbs(newH - h) > 1) resize(w, newH);
|
||||
}
|
||||
if (widthDriven) newH = qRound(newW / aspect);
|
||||
else newW = qRound(newH * aspect);
|
||||
|
||||
if (newW < kMinW) { newW = kMinW; newH = minH; }
|
||||
if (newH < minH) { newH = minH; newW = kMinW; }
|
||||
|
||||
int cx = g.center().x(), cy = g.center().y();
|
||||
int left = g.left(), top = g.top();
|
||||
switch (m_resizeMode) {
|
||||
case ResizeRight: left = g.left(); top = cy - newH / 2; break;
|
||||
case ResizeLeft: left = g.right() - newW + 1; top = cy - newH / 2; break;
|
||||
case ResizeBottom: left = cx - newW / 2; top = g.top(); break;
|
||||
case ResizeTop: left = cx - newW / 2; top = g.bottom() - newH + 1; break;
|
||||
case ResizeTopLeft: left = g.right() - newW + 1; top = g.bottom() - newH + 1; break;
|
||||
case ResizeTopRight: left = g.left(); top = g.bottom() - newH + 1; break;
|
||||
case ResizeBottomLeft: left = g.right() - newW + 1; top = g.top(); break;
|
||||
case ResizeBottomRight: left = g.left(); top = g.top(); break;
|
||||
default: return;
|
||||
}
|
||||
setGeometry(left, top, newW, newH);
|
||||
}
|
||||
Reference in new issue
Block a user