543 lines
18 KiB
C++
543 lines
18 KiB
C++
#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 "
|
||
"<b>S</b> Glättung "
|
||
"<b>C</b> Retro "
|
||
"<b>X</b>/<b>+</b>/<b>-</b> Schärfen "
|
||
"<b>,</b>/<b>.</b> Helligkeit<br>"
|
||
"<b>1</b>/<b>2</b> HW-Kontrast "
|
||
"<b>3</b>/<b>4</b> HW-Sättigung "
|
||
"<b>R</b> Roh-Diagnose<br>"
|
||
"<b>V</b> Vergleich "
|
||
"<b>P</b> Screenshot "
|
||
"<b>D</b> Frame dumpen "
|
||
"<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;
|
||
}
|