Über... hinzugefügt, Icon hinzugefügt

This commit is contained in:
2026-06-09 23:15:29 +02:00
parent 2181f254d2
commit 92168ee5dc
19 changed files with 1039 additions and 88 deletions

260
src/videowidget.cpp Normal file
View File

@@ -0,0 +1,260 @@
#include "videowidget.h"
#include <QPainter>
#include <QFileDialog>
#include <QDir>
#include <QDateTime>
#include <QStandardPaths>
#include <QMetaObject>
#include <QResizeEvent>
// Enumerate /dev/video* entries
static QStringList enumerateVideoDevices()
{
QStringList devices;
const QDir dev("/dev");
const auto entries = dev.entryList({"video*"}, QDir::System, QDir::Name);
for (const auto &e : entries)
devices << "/dev/" + e;
return devices;
}
// ── ctor / dtor ───────────────────────────────────────────────────────────────
VideoWidget::VideoWidget(QWidget *parent)
: QWidget(parent)
{
setupUi();
m_thread = new QThread(this);
m_worker = new V4l2Worker();
m_worker->moveToThread(m_thread);
connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater);
connect(m_worker, &V4l2Worker::newFrame, this, &VideoWidget::onNewFrame,
Qt::QueuedConnection);
connect(m_worker, &V4l2Worker::captureStarted, this, [this](const QString &dev, int w, int h, const QString &fmt) {
m_infoLabel->setText(tr("%1 %2×%3 %4").arg(dev).arg(w).arg(h).arg(fmt));
m_running = true;
m_startBtn->setText(tr("■ Stop"));
});
connect(m_worker, &V4l2Worker::captureStopped, this, [this] {
m_running = false;
m_startBtn->setText(tr("▶ Start"));
m_infoLabel->setText(tr("Stopped"));
update();
});
connect(m_worker, &V4l2Worker::errorOccurred, this, [this](const QString &msg) {
m_infoLabel->setText(tr("Error: %1").arg(msg));
m_running = false;
m_startBtn->setText(tr("▶ Start"));
});
m_thread->start();
}
void VideoWidget::shutdown()
{
if (m_shutdownDone)
return;
m_shutdownDone = true;
// stopCapture() sets m_running=false and joins the std::thread internally.
// Must happen before we quit the Qt event thread.
m_worker->stopCapture();
if (m_thread && m_thread->isRunning()) {
m_thread->quit();
m_thread->wait(5000);
if (m_thread->isRunning())
m_thread->terminate();
}
}
VideoWidget::~VideoWidget()
{
shutdown();
}
// ── UI setup ─────────────────────────────────────────────────────────────────
void VideoWidget::setupUi()
{
setMinimumHeight(140);
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(2, 2, 2, 2);
layout->setSpacing(3);
// ── Controls row ──────────────────────────────────────────────────────────
auto *ctrlRow = new QHBoxLayout();
ctrlRow->setSpacing(4);
m_deviceCombo = new QComboBox(this);
m_deviceCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
refreshDeviceList();
auto *refreshBtn = new QPushButton("↻", this);
refreshBtn->setMaximumWidth(26);
refreshBtn->setToolTip(tr("Refresh device list"));
connect(refreshBtn, &QPushButton::clicked, this, &VideoWidget::refreshDeviceList);
m_startBtn = new QPushButton(tr("▶ Start"), this);
m_startBtn->setMaximumWidth(70);
connect(m_startBtn, &QPushButton::clicked, this, &VideoWidget::onStartStop);
m_freezeBtn = new QPushButton(tr("❚❚ Freeze"), this);
m_freezeBtn->setCheckable(true);
m_freezeBtn->setMaximumWidth(80);
connect(m_freezeBtn, &QPushButton::toggled, this, &VideoWidget::onFreeze);
m_screenshotBtn = new QPushButton(tr("📷"), this);
m_screenshotBtn->setMaximumWidth(32);
m_screenshotBtn->setToolTip(tr("Save screenshot (PNG/JPG)"));
connect(m_screenshotBtn, &QPushButton::clicked, this, &VideoWidget::onScreenshot);
ctrlRow->addWidget(m_deviceCombo, 1);
ctrlRow->addWidget(refreshBtn);
ctrlRow->addWidget(m_startBtn);
ctrlRow->addWidget(m_freezeBtn);
ctrlRow->addWidget(m_screenshotBtn);
layout->addLayout(ctrlRow);
// ── Info label ────────────────────────────────────────────────────────────
m_infoLabel = new QLabel(tr("No device started"), this);
m_infoLabel->setStyleSheet("color: gray; font-size: 9px;");
layout->addWidget(m_infoLabel);
// The video frame is drawn directly in paintEvent – no QLabel needed.
// We leave stretch space for it.
layout->addStretch(1);
}
// ── Slots ─────────────────────────────────────────────────────────────────────
void VideoWidget::onStartStop()
{
if (m_running)
stopCapture();
else
startCapture();
}
void VideoWidget::onFreeze(bool frozen)
{
m_frozen = frozen;
m_freezeBtn->setText(frozen ? tr("▶ Live") : tr("❚❚ Freeze"));
}
void VideoWidget::onScreenshot()
{
if (m_frame.isNull())
return;
const QString defaultName =
QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)
+ "/uartscope_"
+ QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss")
+ ".png";
const QString path = QFileDialog::getSaveFileName(
this, tr("Save screenshot"), defaultName,
tr("PNG Image (*.png);;JPEG Image (*.jpg *.jpeg)"));
if (!path.isEmpty())
m_frame.save(path);
}
void VideoWidget::onNewFrame(const QImage &frame)
{
if (m_frozen)
return;
m_frame = frame;
updateScaled();
update(); // trigger paintEvent
}
void VideoWidget::refreshDeviceList()
{
const QString current = m_deviceCombo->currentText();
m_deviceCombo->clear();
const auto devices = enumerateVideoDevices();
for (const auto &d : devices)
m_deviceCombo->addItem(d);
// Restore previous selection if still present
const int idx = m_deviceCombo->findText(current);
if (idx >= 0)
m_deviceCombo->setCurrentIndex(idx);
}
// ── Capture start/stop ────────────────────────────────────────────────────────
void VideoWidget::startCapture()
{
const QString dev = m_deviceCombo->currentText();
if (dev.isEmpty())
return;
m_frame = QImage();
m_scaled = QImage();
// startCapture blocks in the worker thread until stopCapture() is called
QMetaObject::invokeMethod(m_worker, "startCapture",
Qt::QueuedConnection,
Q_ARG(QString, dev),
Q_ARG(int, 1920),
Q_ARG(int, 1080));
}
void VideoWidget::stopCapture()
{
QMetaObject::invokeMethod(m_worker, "stopCapture", Qt::QueuedConnection);
}
// ── Paint ─────────────────────────────────────────────────────────────────────
void VideoWidget::updateScaled()
{
// Video area = widget height minus the two control rows (~55 px)
const QRect videoRect(0, 55, width(), height() - 55);
if (videoRect.width() < 10 || videoRect.height() < 10 || m_frame.isNull())
return;
m_scaled = m_frame.scaled(videoRect.size(), Qt::KeepAspectRatio,
Qt::SmoothTransformation);
}
void VideoWidget::paintEvent(QPaintEvent *)
{
QPainter p(this);
// Background
p.fillRect(rect(), QColor(0x10, 0x10, 0x10));
if (m_scaled.isNull()) {
p.setPen(QColor(0x44, 0x44, 0x44));
p.drawText(rect(), Qt::AlignCenter,
m_running ? tr("Waiting for frames…") : tr("No signal"));
return;
}
// Centre the scaled frame in the video area
const QRect videoRect(0, 55, width(), height() - 55);
const QPoint origin(
videoRect.left() + (videoRect.width() - m_scaled.width()) / 2,
videoRect.top() + (videoRect.height() - m_scaled.height()) / 2);
p.drawImage(origin, m_scaled);
// Frozen overlay
if (m_frozen) {
p.setPen(QColor(0xff, 0xcc, 0x00));
p.setFont(QFont("Sans", 9, QFont::Bold));
p.drawText(videoRect.adjusted(4, 4, -4, -4),
Qt::AlignTop | Qt::AlignRight, tr("FREEZE"));
}
}
void VideoWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
updateScaled();
}