Files
hdmiviewer/src/MainWindow.cpp

543 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "MainWindow.h"
#include "VideoWidget.h"
#include "CaptureThread.h"
#include "AudioThread.h"
#include <QKeyEvent>
#include <QCloseEvent>
#include <QStatusBar>
#include <QMenuBar>
#include <QMenu>
#include <QAction>
#include <QSettings>
#include <QDialog>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QFrame>
#include <QTimer>
#include <QDir>
#include <QDateTime>
MainWindow::MainWindow(const QString& videoDevice,
const QString& audioDevice,
int targetFps,
bool smoothModeEnabled,
bool retroModeEnabled,
float brightness,
float sharpenAmount,
float lastSharpenAmount,
QWidget* parent)
: QMainWindow(parent)
, m_lastSharpenAmount(lastSharpenAmount)
, m_videoDevice(videoDevice)
, m_audioDevice(audioDevice)
{
setWindowTitle("HDMI Viewer");
resize(1280, 720);
m_video = new VideoWidget(this);
m_video->setSmoothModeEnabled(smoothModeEnabled);
m_video->setRetroModeEnabled(retroModeEnabled);
m_video->setSharpenAmount(sharpenAmount);
m_video->setBrightness(brightness);
setCentralWidget(m_video);
// Kurzzeitige Einblendung unten links (funktioniert auch im Vollbild,
// wo Menü- und Statusleiste ausgeblendet sind).
m_overlayLabel = new QLabel(m_video);
m_overlayLabel->setStyleSheet(
"background-color: rgba(20, 20, 20, 200); color: white;"
"padding: 6px 12px; border-radius: 4px; font-size: 13px;");
m_overlayLabel->hide();
setupMenuBar();
updateStatusBar();
// ── Capture ──────────────────────────────────────────────────────────────
m_capture = new CaptureThread(videoDevice, targetFps, this);
connect(m_capture, &CaptureThread::frameReady,
m_video, &VideoWidget::setFrame,
Qt::QueuedConnection);
connect(m_capture, &CaptureThread::errorOccurred,
this, &MainWindow::onError,
Qt::QueuedConnection);
connect(m_capture, &CaptureThread::captureInfo,
this, &MainWindow::onCaptureInfo,
Qt::QueuedConnection);
connect(m_capture, &CaptureThread::frameDumped,
this, &MainWindow::onFrameDumped,
Qt::QueuedConnection);
connect(m_capture, &CaptureThread::hardwareControlChanged,
this, &MainWindow::onHardwareControlChanged,
Qt::QueuedConnection);
m_capture->start();
// ── Audio ─────────────────────────────────────────────────────────────────
if (!audioDevice.isEmpty()) {
m_audio = new AudioThread(audioDevice, this);
connect(m_audio, &AudioThread::errorOccurred,
this, &MainWindow::onError,
Qt::QueuedConnection);
m_audio->start();
}
}
MainWindow::~MainWindow()
{
stopAll();
}
void MainWindow::setupMenuBar()
{
QMenu* viewMenu = menuBar()->addMenu("&Anzeige");
m_smoothAction = viewMenu->addAction("Bildglättung\tS");
m_smoothAction->setCheckable(true);
m_smoothAction->setChecked(m_video->smoothModeEnabled());
connect(m_smoothAction, &QAction::triggered, this, &MainWindow::toggleSmoothMode);
m_retroAction = viewMenu->addAction("Retro-Optik (Scanlines/Maske)\tC");
m_retroAction->setCheckable(true);
m_retroAction->setChecked(m_video->retroModeEnabled());
connect(m_retroAction, &QAction::triggered, this, &MainWindow::toggleRetroMode);
m_sharpenAction = viewMenu->addAction("Nachschärfen\tX");
m_sharpenAction->setCheckable(true);
m_sharpenAction->setChecked(m_video->sharpenAmount() > 0.0f);
connect(m_sharpenAction, &QAction::triggered, this, &MainWindow::toggleSharpen);
QAction* sharpenUpAct = viewMenu->addAction("Schärfen +\t+");
connect(sharpenUpAct, &QAction::triggered, this, &MainWindow::increaseSharpen);
QAction* sharpenDownAct = viewMenu->addAction("Schärfen -\t-");
connect(sharpenDownAct, &QAction::triggered, this, &MainWindow::decreaseSharpen);
viewMenu->addSeparator();
QAction* brighterAct = viewMenu->addAction("Heller\t.");
connect(brighterAct, &QAction::triggered, this, &MainWindow::increaseBrightness);
QAction* darkerAct = viewMenu->addAction("Dunkler\t,");
connect(darkerAct, &QAction::triggered, this, &MainWindow::decreaseBrightness);
viewMenu->addSeparator();
QAction* rawAct = viewMenu->addAction("Roh-Modus (Diagnose)\tR");
rawAct->setCheckable(true);
rawAct->setChecked(m_video->rawEnabled());
connect(rawAct, &QAction::triggered, this, &MainWindow::toggleRaw);
QAction* compareAct = viewMenu->addAction("Live-Vergleich (roh/bearbeitet)\tV");
compareAct->setCheckable(true);
compareAct->setChecked(m_video->compareEnabled());
connect(compareAct, &QAction::triggered, this, &MainWindow::toggleCompare);
viewMenu->addSeparator();
QAction* screenshotAct = viewMenu->addAction("Screenshot speichern\tP");
connect(screenshotAct, &QAction::triggered, this, &MainWindow::saveScreenshot);
QMenu* helpMenu = menuBar()->addMenu("&Hilfe");
QAction* aboutAct = helpMenu->addAction("Über HDMI Viewer");
aboutAct->setMenuRole(QAction::AboutRole);
connect(aboutAct, &QAction::triggered, this, &MainWindow::showAbout);
}
void MainWindow::showOverlayMessage(const QString& text)
{
if (!m_overlayLabel) return;
m_overlayLabel->setText(text);
m_overlayLabel->adjustSize();
const int margin = 16;
m_overlayLabel->move(margin, m_video->height() - m_overlayLabel->height() - margin);
m_overlayLabel->show();
m_overlayLabel->raise();
QTimer::singleShot(1500, m_overlayLabel, &QLabel::hide);
}
void MainWindow::toggleSmoothMode()
{
const bool enabled = !m_video->smoothModeEnabled();
m_video->setSmoothModeEnabled(enabled);
if (m_smoothAction) m_smoothAction->setChecked(enabled);
QSettings settings;
settings.setValue("display/smooth", enabled);
statusBar()->showMessage(
enabled ? "Bildglättung aktiviert" : "Bildglättung deaktiviert", 2000);
showOverlayMessage(enabled ? "Bildglättung: an" : "Bildglättung: aus");
}
void MainWindow::toggleRetroMode()
{
const bool enabled = !m_video->retroModeEnabled();
m_video->setRetroModeEnabled(enabled);
if (m_retroAction) m_retroAction->setChecked(enabled);
QSettings settings;
settings.setValue("display/retro", enabled);
statusBar()->showMessage(
enabled ? "Retro-Optik aktiviert" : "Retro-Optik deaktiviert", 2000);
showOverlayMessage(enabled ? "Retro-Optik: an" : "Retro-Optik: aus");
}
void MainWindow::toggleSharpen()
{
// Quick-Toggle: aus (0) ↔ zuletzt benutzter Wert (nicht der feste
// Standardwert!) merkt sich also z. B. 7.0, nicht immer 2.5.
if (m_video->sharpenAmount() > 0.0f) {
m_lastSharpenAmount = m_video->sharpenAmount();
applySharpen(0.0f);
} else {
applySharpen(m_lastSharpenAmount > 0.0f ? m_lastSharpenAmount : VideoWidget::kSharpenDefault);
}
}
void MainWindow::increaseSharpen()
{
applySharpen(m_video->sharpenAmount() + VideoWidget::kSharpenStep);
}
void MainWindow::decreaseSharpen()
{
applySharpen(m_video->sharpenAmount() - VideoWidget::kSharpenStep);
}
void MainWindow::applySharpen(float value)
{
value = qBound(VideoWidget::kSharpenMin, value, VideoWidget::kSharpenMax);
m_video->setSharpenAmount(value);
if (m_sharpenAction) m_sharpenAction->setChecked(value > 0.0f);
QSettings settings;
settings.setValue("display/sharpen", static_cast<double>(value));
if (value > 0.0f) {
m_lastSharpenAmount = value;
settings.setValue("display/sharpen_last", static_cast<double>(value));
}
statusBar()->showMessage(QString("Nachschärfen: %1").arg(value, 0, 'f', 1), 2000);
showOverlayMessage(QString("Nachschärfen: %1").arg(value, 0, 'f', 1));
}
void MainWindow::increaseBrightness()
{
applyBrightness(m_video->brightness() + VideoWidget::kBrightnessStep);
}
void MainWindow::decreaseBrightness()
{
applyBrightness(m_video->brightness() - VideoWidget::kBrightnessStep);
}
void MainWindow::toggleRaw()
{
const bool enabled = !m_video->rawEnabled();
m_video->setRawEnabled(enabled);
const QString msg = enabled ? "Roh-Modus: an (alle Effekte umgangen)" : "Roh-Modus: aus";
statusBar()->showMessage(msg, 2000);
showOverlayMessage(msg);
}
void MainWindow::toggleCompare()
{
const bool enabled = !m_video->compareEnabled();
m_video->setCompareEnabled(enabled);
const QString msg = enabled ? "Vergleich: an (links roh, rechts bearbeitet)" : "Vergleich: aus";
statusBar()->showMessage(msg, 2000);
showOverlayMessage(msg);
}
void MainWindow::saveScreenshot()
{
const QImage img = m_video->grabProcessedFrame();
if (img.isNull()) {
statusBar()->showMessage("Screenshot fehlgeschlagen (kein Bild)", 3000);
return;
}
const QString path = QDir::homePath() + "/hdmiviewer_screenshot_" +
QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss") + ".png";
if (img.save(path, "PNG")) {
statusBar()->showMessage("Screenshot gespeichert: " + path, 5000);
showOverlayMessage("Screenshot gespeichert");
} else {
statusBar()->showMessage("Screenshot konnte nicht gespeichert werden: " + path, 5000);
}
}
void MainWindow::applyBrightness(float value)
{
value = qBound(VideoWidget::kBrightnessMin, value, VideoWidget::kBrightnessMax);
m_video->setBrightness(value);
QSettings settings;
settings.setValue("display/brightness", static_cast<double>(value));
statusBar()->showMessage(
QString("Helligkeit: %1 %").arg(qRound(value * 100.0f)), 2000);
showOverlayMessage(QString("Helligkeit: %1 %").arg(qRound(value * 100.0f)));
}
void MainWindow::showAbout()
{
auto* dlg = new QDialog(this);
dlg->setWindowTitle("Über HDMI Viewer");
dlg->setFixedSize(460, 470);
dlg->setModal(true);
auto* root = new QVBoxLayout(dlg);
root->setSpacing(0);
root->setContentsMargins(0, 0, 0, 0);
// ── Header-Bereich ────────────────────────────────────────────────────────
auto* header = new QWidget(dlg);
header->setFixedHeight(90);
header->setStyleSheet("background-color: #1e1e2e;");
auto* headerLayout = new QVBoxLayout(header);
headerLayout->setContentsMargins(24, 16, 24, 16);
auto* titleLabel = new QLabel("HDMI Viewer", header);
titleLabel->setStyleSheet("font-size: 22px; font-weight: bold; color: #cdd6f4;");
auto* subtitleLabel = new QLabel("Live-Vorschau für HDMI-Grabber", header);
subtitleLabel->setStyleSheet("font-size: 11px; color: #6c7086;");
headerLayout->addWidget(titleLabel);
headerLayout->addWidget(subtitleLabel);
// ── Trennlinie ────────────────────────────────────────────────────────────
auto* sep = new QFrame(dlg);
sep->setFrameShape(QFrame::HLine);
sep->setStyleSheet("color: #313244;");
// ── Info-Bereich ──────────────────────────────────────────────────────────
auto* body = new QWidget(dlg);
auto* grid = new QGridLayout(body);
grid->setContentsMargins(24, 20, 24, 8);
grid->setVerticalSpacing(10);
grid->setHorizontalSpacing(16);
grid->setColumnMinimumWidth(0, 100);
auto addRow = [&](int row, const QString& label, const QString& value) {
auto* l = new QLabel(label, body);
l->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
l->setStyleSheet("font-weight: bold; color: #a6adc8;");
auto* v = new QLabel(value, body);
v->setStyleSheet("color: #cdd6f4;");
v->setTextInteractionFlags(Qt::TextSelectableByMouse);
grid->addWidget(l, row, 0);
grid->addWidget(v, row, 1);
};
addRow(0, "Version", "1.4.0");
addRow(1, "Entwickler", "Dany Thinnes");
addRow(2, "Projekt", "Projekt Hirnfrei");
addRow(3, "Framework", QString("Qt %1").arg(QT_VERSION_STR));
addRow(4, "Sprache", "C++17");
addRow(5, "Video-API", "V4L2 · MJPEG / YUYV / NV12");
addRow(6, "Audio-API", "ALSA");
addRow(7, "Rendering", "OpenGL 3.3 · Bildglättung / Retro-Optik");
// ── Tastenkürzel-Hinweis ──────────────────────────────────────────────────
auto* sep2 = new QFrame(body);
sep2->setFrameShape(QFrame::HLine);
sep2->setStyleSheet("color: #313244;");
grid->addWidget(sep2, 8, 0, 1, 2);
auto* keysLabel = new QLabel(
"<small><b>F</b> Vollbild &nbsp;&nbsp; "
"<b>S</b> Glättung &nbsp;&nbsp; "
"<b>C</b> Retro &nbsp;&nbsp; "
"<b>X</b>/<b>+</b>/<b>-</b> Schärfen &nbsp;&nbsp; "
"<b>,</b>/<b>.</b> Helligkeit<br>"
"<b>1</b>/<b>2</b> HW-Kontrast &nbsp;&nbsp; "
"<b>3</b>/<b>4</b> HW-Sättigung &nbsp;&nbsp; "
"<b>R</b> Roh-Diagnose<br>"
"<b>V</b> Vergleich &nbsp;&nbsp; "
"<b>P</b> Screenshot &nbsp;&nbsp; "
"<b>D</b> Frame dumpen &nbsp;&nbsp; "
"<b>Esc</b> Zurück</small>", body);
keysLabel->setStyleSheet("color: #6c7086;");
keysLabel->setAlignment(Qt::AlignCenter);
grid->addWidget(keysLabel, 9, 0, 1, 2);
// ── Schließen-Button ──────────────────────────────────────────────────────
auto* btnRow = new QHBoxLayout;
btnRow->setContentsMargins(24, 8, 24, 20);
auto* closeBtn = new QPushButton("Schließen", dlg);
closeBtn->setDefault(true);
closeBtn->setFixedWidth(100);
btnRow->addStretch();
btnRow->addWidget(closeBtn);
connect(closeBtn, &QPushButton::clicked, dlg, &QDialog::accept);
root->addWidget(header);
root->addWidget(sep);
root->addWidget(body, 1);
root->addLayout(btnRow);
dlg->exec();
dlg->deleteLater();
}
void MainWindow::stopAll()
{
if (m_capture) {
m_capture->stopAndWait();
delete m_capture;
m_capture = nullptr;
}
if (m_audio) {
m_audio->stop();
m_audio->wait();
delete m_audio;
m_audio = nullptr;
}
}
void MainWindow::keyPressEvent(QKeyEvent* event)
{
switch (event->key()) {
case Qt::Key_F:
toggleFullscreen();
break;
case Qt::Key_Escape:
if (m_fullscreen) {
toggleFullscreen();
} else {
m_wantsReopen = true;
close();
}
break;
case Qt::Key_S:
toggleSmoothMode();
break;
case Qt::Key_C:
toggleRetroMode();
break;
case Qt::Key_X:
toggleSharpen();
break;
case Qt::Key_Plus:
case Qt::Key_Equal: // '+' liegt auf vielen Layouts ohne Shift auf dieser Taste
increaseSharpen();
break;
case Qt::Key_Minus:
decreaseSharpen();
break;
case Qt::Key_Period:
increaseBrightness();
break;
case Qt::Key_Comma:
decreaseBrightness();
break;
case Qt::Key_R:
toggleRaw();
break;
case Qt::Key_D:
if (m_capture) m_capture->requestFrameDump();
break;
case Qt::Key_V:
toggleCompare();
break;
case Qt::Key_P:
saveScreenshot();
break;
case Qt::Key_1:
if (m_capture) m_capture->adjustHardwareContrast(-5);
break;
case Qt::Key_2:
if (m_capture) m_capture->adjustHardwareContrast(5);
break;
case Qt::Key_3:
if (m_capture) m_capture->adjustHardwareSaturation(-5);
break;
case Qt::Key_4:
if (m_capture) m_capture->adjustHardwareSaturation(5);
break;
default:
QMainWindow::keyPressEvent(event);
}
}
void MainWindow::closeEvent(QCloseEvent* event)
{
stopAll();
event->accept();
}
void MainWindow::toggleFullscreen()
{
if (m_fullscreen) {
showNormal();
menuBar()->show();
statusBar()->show();
} else {
showFullScreen();
menuBar()->hide();
statusBar()->hide();
}
m_fullscreen = !m_fullscreen;
}
void MainWindow::updateStatusBar()
{
QString info;
if (m_capWidth > 0)
info = QString("%1×%2 @ %3fps %4 ")
.arg(m_capWidth).arg(m_capHeight).arg(m_capFps).arg(m_capFmt);
info += QString("Video: %1 Audio: %2 [F] Vollbild [S] Glättung [C] Retro [X/+/-] Schärfen [,/.] Helligkeit [1/2] HW-Kontrast [3/4] HW-Sättigung [R] Roh [V] Vergleich [P] Screenshot [D] Frame dumpen [Esc] Einstellungen")
.arg(m_videoDevice,
m_audioDevice.isEmpty() ? QStringLiteral("") : m_audioDevice);
statusBar()->showMessage(info);
}
void MainWindow::onCaptureInfo(int width, int height, int fps, const QString& format)
{
m_capWidth = width;
m_capHeight = height;
m_capFps = fps;
m_capFmt = format;
updateStatusBar();
setWindowTitle(QString("HDMI Viewer %1×%2 @ %3fps %4")
.arg(width).arg(height).arg(fps).arg(format));
}
void MainWindow::onFrameDumped(const QString& path)
{
const QString msg = "Frame gespeichert: " + path;
statusBar()->showMessage(msg, 5000);
showOverlayMessage("Frame gespeichert (siehe Home-Ordner)");
}
void MainWindow::onHardwareControlChanged(const QString& label, int value, int minVal, int maxVal)
{
const QString msg = QString("%1: %2 (%3..%4)").arg(label).arg(value).arg(minVal).arg(maxVal);
statusBar()->showMessage(msg, 2000);
showOverlayMessage(msg);
}
void MainWindow::onError(const QString& msg)
{
statusBar()->showMessage("" + msg, 8000);
qWarning() << "[Error]" << msg;
}