Bild weiter verbessert, Icon und .desktop hinzugefügt
This commit is contained in:
@@ -422,6 +422,30 @@ void CaptureThread::adjustHardwareControl(quint32 id, const char* settingsKey,
|
||||
}
|
||||
}
|
||||
|
||||
void CaptureThread::setHardwareValues(int brightness, int contrast, int saturation)
|
||||
{
|
||||
if (m_fd == -1) return;
|
||||
|
||||
auto set = [&](quint32 id, int value, int restoreValue) {
|
||||
v4l2_queryctrl qc{};
|
||||
qc.id = id;
|
||||
if (xioctl(m_fd, VIDIOC_QUERYCTRL, &qc) == -1 || (qc.flags & V4L2_CTRL_FLAG_DISABLED))
|
||||
return;
|
||||
|
||||
const int target = value >= 0 ? value : restoreValue;
|
||||
if (target < 0) return; // weder Profilwert noch Ausgangswert bekannt
|
||||
|
||||
v4l2_control ctrl{};
|
||||
ctrl.id = id;
|
||||
ctrl.value = qBound(qc.minimum, target, qc.maximum);
|
||||
xioctl(m_fd, VIDIOC_S_CTRL, &ctrl);
|
||||
};
|
||||
|
||||
set(V4L2_CID_BRIGHTNESS, brightness, m_restoreBrightness);
|
||||
set(V4L2_CID_CONTRAST, contrast, m_restoreContrast);
|
||||
set(V4L2_CID_SATURATION, saturation, m_restoreSaturation);
|
||||
}
|
||||
|
||||
void CaptureThread::adjustHardwareBrightness(int delta)
|
||||
{
|
||||
adjustHardwareControl(V4L2_CID_BRIGHTNESS, "hardware/brightness", "Hardware-Helligkeit", delta);
|
||||
|
||||
@@ -27,6 +27,9 @@ public:
|
||||
void adjustHardwareBrightness(int delta);
|
||||
void adjustHardwareContrast(int delta);
|
||||
void adjustHardwareSaturation(int delta);
|
||||
// Setzt die Hardware-Regler absolut (für Profile). -1 = zurück auf den
|
||||
// beim Start gemerkten Ausgangswert des Chips.
|
||||
void setHardwareValues(int brightness, int contrast, int saturation);
|
||||
|
||||
int captureWidth() const { return m_width; }
|
||||
int captureHeight() const { return m_height; }
|
||||
|
||||
@@ -55,6 +55,22 @@ DeviceDialog::DeviceDialog(QWidget* parent)
|
||||
form->addRow("Audio-Gerät (ALSA):", m_audioCombo);
|
||||
form->addRow("Ziel-Framerate:", m_fpsCombo);
|
||||
|
||||
// Profile: gespeicherte Kombination aus Glättung/Retro/Schärfen/HW-Reglern
|
||||
m_profileCombo = new QComboBox(this);
|
||||
m_profileSaveBtn = new QPushButton("Speichern …", this);
|
||||
m_profileDeleteBtn = new QPushButton("Löschen", this);
|
||||
m_profileSaveBtn->setAutoDefault(false);
|
||||
m_profileDeleteBtn->setAutoDefault(false);
|
||||
m_profileCombo->setToolTip(
|
||||
"Gespeicherte Bild-Einstellungen (Glättung, Retro, Schärfen,\n"
|
||||
"Hardware-Regler) für eine Quelle, z. B. PlayStation oder PS3.\n"
|
||||
"Im laufenden Betrieb mit [F1]..[F9] oder über das Menü \"Profile\" umschaltbar.");
|
||||
auto* profileRow = new QHBoxLayout;
|
||||
profileRow->addWidget(m_profileCombo, 1);
|
||||
profileRow->addWidget(m_profileSaveBtn);
|
||||
profileRow->addWidget(m_profileDeleteBtn);
|
||||
form->addRow("Profil:", profileRow);
|
||||
|
||||
m_smoothCheck = new QCheckBox("Bildglättung aktivieren (empfohlen)", this);
|
||||
m_smoothCheck->setChecked(true);
|
||||
m_smoothCheck->setToolTip(
|
||||
@@ -143,6 +159,24 @@ DeviceDialog::DeviceDialog(QWidget* parent)
|
||||
connect(btnOk, &QPushButton::clicked, this, &QDialog::accept);
|
||||
connect(btnClose, &QPushButton::clicked, this, &QDialog::reject);
|
||||
|
||||
// Profile: Auswahl lädt die Werte in die Felder; manuelle Änderungen
|
||||
// führen die Auswahl nach (passendes Profil oder "kein Profil").
|
||||
populateProfiles(QString());
|
||||
connect(m_profileCombo, QOverload<int>::of(&QComboBox::activated), this, [this](int index) {
|
||||
loadProfileIntoControls(m_profileCombo->itemData(index).toString());
|
||||
});
|
||||
connect(m_profileSaveBtn, &QPushButton::clicked, this, &DeviceDialog::saveProfile);
|
||||
connect(m_profileDeleteBtn, &QPushButton::clicked, this, &DeviceDialog::deleteProfile);
|
||||
|
||||
auto onControlChanged = [this]() { refreshProfileSelection(); };
|
||||
connect(m_smoothCheck, &QCheckBox::toggled, this, onControlChanged);
|
||||
connect(m_retroCheck, &QCheckBox::toggled, this, onControlChanged);
|
||||
connect(m_sharpenSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, onControlChanged);
|
||||
connect(m_hwBrightnessSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, onControlChanged);
|
||||
connect(m_hwContrastSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, onControlChanged);
|
||||
connect(m_hwSaturationSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, onControlChanged);
|
||||
refreshProfileSelection();
|
||||
|
||||
auto* main = new QVBoxLayout(this);
|
||||
main->addLayout(form);
|
||||
main->addSpacing(8);
|
||||
@@ -171,6 +205,77 @@ void DeviceDialog::setPreselectedHwBrightness(int value) { m_hwBrightnessSpin->s
|
||||
void DeviceDialog::setPreselectedHwContrast(int value) { m_hwContrastSpin->setValue(value); }
|
||||
void DeviceDialog::setPreselectedHwSaturation(int value) { m_hwSaturationSpin->setValue(value); }
|
||||
|
||||
Profile DeviceDialog::currentProfile() const
|
||||
{
|
||||
Profile p;
|
||||
p.smooth = m_smoothCheck->isChecked();
|
||||
p.retro = m_retroCheck->isChecked();
|
||||
p.sharpen = static_cast<float>(m_sharpenSpin->value());
|
||||
p.hwBrightness = m_hwBrightnessSpin->value();
|
||||
p.hwContrast = m_hwContrastSpin->value();
|
||||
p.hwSaturation = m_hwSaturationSpin->value();
|
||||
return p;
|
||||
}
|
||||
|
||||
void DeviceDialog::populateProfiles(const QString& select)
|
||||
{
|
||||
m_profileCombo->clear();
|
||||
m_profileCombo->addItem("– kein Profil –", QString());
|
||||
for (const QString& name : Profiles::names())
|
||||
m_profileCombo->addItem(name, name);
|
||||
selectByData(m_profileCombo, select);
|
||||
m_profileDeleteBtn->setEnabled(m_profileCombo->currentIndex() > 0);
|
||||
}
|
||||
|
||||
void DeviceDialog::refreshProfileSelection()
|
||||
{
|
||||
if (m_loadingProfile) return;
|
||||
|
||||
const Profile cur = currentProfile();
|
||||
|
||||
// Aktuell gewähltes Profil bevorzugen, solange es noch passt
|
||||
// (relevant, wenn zwei Profile identische Werte haben).
|
||||
QString match = m_profileCombo->currentData().toString();
|
||||
if (match.isEmpty() || !Profiles::exists(match) || !Profiles::load(match).matches(cur))
|
||||
match = Profiles::findMatching(cur);
|
||||
|
||||
selectByData(m_profileCombo, match);
|
||||
m_profileDeleteBtn->setEnabled(m_profileCombo->currentIndex() > 0);
|
||||
}
|
||||
|
||||
void DeviceDialog::loadProfileIntoControls(const QString& name)
|
||||
{
|
||||
m_profileDeleteBtn->setEnabled(!name.isEmpty());
|
||||
if (name.isEmpty() || !Profiles::exists(name))
|
||||
return;
|
||||
|
||||
const Profile p = Profiles::load(name);
|
||||
|
||||
m_loadingProfile = true;
|
||||
m_smoothCheck->setChecked(p.smooth);
|
||||
m_retroCheck->setChecked(p.retro);
|
||||
m_sharpenSpin->setValue(p.sharpen);
|
||||
m_hwBrightnessSpin->setValue(p.hwBrightness);
|
||||
m_hwContrastSpin->setValue(p.hwContrast);
|
||||
m_hwSaturationSpin->setValue(p.hwSaturation);
|
||||
m_loadingProfile = false;
|
||||
}
|
||||
|
||||
void DeviceDialog::saveProfile()
|
||||
{
|
||||
QString saved;
|
||||
if (Profiles::saveWithPrompt(this, currentProfile(), &saved))
|
||||
populateProfiles(saved);
|
||||
}
|
||||
|
||||
void DeviceDialog::deleteProfile()
|
||||
{
|
||||
const QString name = m_profileCombo->currentData().toString();
|
||||
if (name.isEmpty()) return;
|
||||
if (Profiles::removeWithConfirm(this, name))
|
||||
populateProfiles(QString());
|
||||
}
|
||||
|
||||
void DeviceDialog::selectByData(QComboBox* combo, const QString& data)
|
||||
{
|
||||
for (int i = 0; i < combo->count(); ++i)
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
#include "Profile.h"
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QComboBox;
|
||||
class QPushButton;
|
||||
class QCheckBox;
|
||||
class QDoubleSpinBox;
|
||||
class QSpinBox;
|
||||
@@ -37,10 +40,20 @@ public:
|
||||
void setPreselectedHwContrast(int value); // -1 = unverändert
|
||||
void setPreselectedHwSaturation(int value); // -1 = unverändert
|
||||
|
||||
// Profile: aktueller Stand der Bild-Einstellungen im Dialog, und
|
||||
// Profil-Auswahl passend zu den aktuellen Werten setzen (nach den
|
||||
// setPreselected*-Aufrufen verwenden).
|
||||
Profile currentProfile() const;
|
||||
void refreshProfileSelection();
|
||||
|
||||
private:
|
||||
void populateVideoDevices();
|
||||
void populateAudioDevices();
|
||||
void selectByData(QComboBox* combo, const QString& data);
|
||||
void populateProfiles(const QString& select);
|
||||
void loadProfileIntoControls(const QString& name);
|
||||
void saveProfile();
|
||||
void deleteProfile();
|
||||
|
||||
QComboBox* m_videoCombo = nullptr;
|
||||
QComboBox* m_audioCombo = nullptr;
|
||||
@@ -51,4 +64,8 @@ private:
|
||||
QSpinBox* m_hwBrightnessSpin = nullptr;
|
||||
QSpinBox* m_hwContrastSpin = nullptr;
|
||||
QSpinBox* m_hwSaturationSpin = nullptr;
|
||||
QComboBox* m_profileCombo = nullptr;
|
||||
QPushButton* m_profileSaveBtn = nullptr;
|
||||
QPushButton* m_profileDeleteBtn = nullptr;
|
||||
bool m_loadingProfile = false;
|
||||
};
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include <QDir>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "Profile.h"
|
||||
|
||||
MainWindow::MainWindow(const QString& videoDevice,
|
||||
const QString& audioDevice,
|
||||
int targetFps,
|
||||
@@ -139,6 +141,12 @@ void MainWindow::setupMenuBar()
|
||||
QAction* screenshotAct = viewMenu->addAction("Screenshot speichern\tP");
|
||||
connect(screenshotAct, &QAction::triggered, this, &MainWindow::saveScreenshot);
|
||||
|
||||
// Profile werden beim Aufklappen frisch aufgebaut (Häkchen = passt zum
|
||||
// aktuellen Stand), Umschalten auch per F1..F9.
|
||||
m_profileMenu = menuBar()->addMenu("&Profile");
|
||||
connect(m_profileMenu, &QMenu::aboutToShow, this, &MainWindow::rebuildProfileMenu);
|
||||
rebuildProfileMenu();
|
||||
|
||||
QMenu* helpMenu = menuBar()->addMenu("&Hilfe");
|
||||
|
||||
QAction* aboutAct = helpMenu->addAction("Über HDMI Viewer");
|
||||
@@ -146,6 +154,103 @@ void MainWindow::setupMenuBar()
|
||||
connect(aboutAct, &QAction::triggered, this, &MainWindow::showAbout);
|
||||
}
|
||||
|
||||
// ── Profile ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Profile MainWindow::currentProfile() const
|
||||
{
|
||||
// Hardware-Werte stehen laut QSettings immer aktuell (Tasten + Dialog
|
||||
// schreiben sie sofort), die Anzeige-Werte kommen direkt vom Widget.
|
||||
Profile p = Profile::fromSettings();
|
||||
p.smooth = m_video->smoothModeEnabled();
|
||||
p.retro = m_video->retroModeEnabled();
|
||||
p.sharpen = m_video->sharpenAmount();
|
||||
return p;
|
||||
}
|
||||
|
||||
void MainWindow::rebuildProfileMenu()
|
||||
{
|
||||
if (!m_profileMenu) return;
|
||||
m_profileMenu->clear();
|
||||
|
||||
const QStringList names = Profiles::names();
|
||||
const Profile current = currentProfile();
|
||||
|
||||
for (int i = 0; i < names.size(); ++i) {
|
||||
const QString name = names.at(i);
|
||||
QString label = name;
|
||||
label.replace("&", "&&");
|
||||
if (i < 9)
|
||||
label += QString("\tF%1").arg(i + 1);
|
||||
|
||||
QAction* act = m_profileMenu->addAction(label);
|
||||
act->setCheckable(true);
|
||||
act->setChecked(Profiles::load(name).matches(current));
|
||||
connect(act, &QAction::triggered, this, [this, name]() { applyProfile(name); });
|
||||
}
|
||||
|
||||
if (names.isEmpty()) {
|
||||
QAction* none = m_profileMenu->addAction("(noch keine Profile gespeichert)");
|
||||
none->setEnabled(false);
|
||||
}
|
||||
|
||||
m_profileMenu->addSeparator();
|
||||
|
||||
QAction* saveAct = m_profileMenu->addAction("Aktuelle Einstellungen als Profil speichern …");
|
||||
connect(saveAct, &QAction::triggered, this, &MainWindow::saveCurrentAsProfile);
|
||||
|
||||
QMenu* delMenu = m_profileMenu->addMenu("Profil löschen");
|
||||
delMenu->setEnabled(!names.isEmpty());
|
||||
for (const QString& name : names) {
|
||||
QString label = name;
|
||||
label.replace("&", "&&");
|
||||
QAction* act = delMenu->addAction(label);
|
||||
connect(act, &QAction::triggered, this, [this, name]() { deleteProfile(name); });
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::applyProfile(const QString& name)
|
||||
{
|
||||
if (!Profiles::exists(name)) return;
|
||||
const Profile p = Profiles::load(name);
|
||||
|
||||
m_video->setSmoothModeEnabled(p.smooth);
|
||||
m_video->setRetroModeEnabled(p.retro);
|
||||
|
||||
const float sharpen = qBound(VideoWidget::kSharpenMin, p.sharpen, VideoWidget::kSharpenMax);
|
||||
m_video->setSharpenAmount(sharpen);
|
||||
if (sharpen > 0.0f)
|
||||
m_lastSharpenAmount = sharpen;
|
||||
|
||||
if (m_smoothAction) m_smoothAction->setChecked(p.smooth);
|
||||
if (m_retroAction) m_retroAction->setChecked(p.retro);
|
||||
if (m_sharpenAction) m_sharpenAction->setChecked(sharpen > 0.0f);
|
||||
|
||||
// Als aktuellen Stand persistieren (nächster Start übernimmt ihn) ...
|
||||
p.applyToSettings();
|
||||
|
||||
// ... und die Hardware-Regler am Grabber sofort umstellen.
|
||||
if (m_capture)
|
||||
m_capture->setHardwareValues(p.hwBrightness, p.hwContrast, p.hwSaturation);
|
||||
|
||||
statusBar()->showMessage("Profil geladen: " + name, 3000);
|
||||
showOverlayMessage("Profil: " + name);
|
||||
}
|
||||
|
||||
void MainWindow::saveCurrentAsProfile()
|
||||
{
|
||||
QString saved;
|
||||
if (Profiles::saveWithPrompt(this, currentProfile(), &saved)) {
|
||||
statusBar()->showMessage("Profil gespeichert: " + saved, 3000);
|
||||
showOverlayMessage("Profil gespeichert: " + saved);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::deleteProfile(const QString& name)
|
||||
{
|
||||
if (Profiles::removeWithConfirm(this, name))
|
||||
statusBar()->showMessage("Profil gelöscht: " + name, 3000);
|
||||
}
|
||||
|
||||
void MainWindow::showOverlayMessage(const QString& text)
|
||||
{
|
||||
if (!m_overlayLabel) return;
|
||||
@@ -249,7 +354,19 @@ 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";
|
||||
// Auch die Hardware-Regler am Grabber-Chip umgehen: An = zurück auf die
|
||||
// beim Start gemerkten Ausgangswerte des Chips (-1), Aus = gespeicherte
|
||||
// Nutzerwerte (QSettings bzw. aktives Profil) wieder setzen.
|
||||
if (m_capture) {
|
||||
if (enabled) {
|
||||
m_capture->setHardwareValues(-1, -1, -1);
|
||||
} else {
|
||||
const Profile p = Profile::fromSettings();
|
||||
m_capture->setHardwareValues(p.hwBrightness, p.hwContrast, p.hwSaturation);
|
||||
}
|
||||
}
|
||||
|
||||
const QString msg = enabled ? "Roh-Modus: an (alle Filter und HW-Regler aus)" : "Roh-Modus: aus";
|
||||
statusBar()->showMessage(msg, 2000);
|
||||
showOverlayMessage(msg);
|
||||
}
|
||||
@@ -287,7 +404,7 @@ void MainWindow::showAbout()
|
||||
{
|
||||
auto* dlg = new QDialog(this);
|
||||
dlg->setWindowTitle("Über HDMI Viewer");
|
||||
dlg->setFixedSize(460, 470);
|
||||
dlg->setFixedSize(460, 490);
|
||||
dlg->setModal(true);
|
||||
|
||||
auto* root = new QVBoxLayout(dlg);
|
||||
@@ -336,7 +453,7 @@ void MainWindow::showAbout()
|
||||
grid->addWidget(v, row, 1);
|
||||
};
|
||||
|
||||
addRow(0, "Version", "1.0");
|
||||
addRow(0, "Version", "1.1.0");
|
||||
addRow(1, "Entwickler", "Dany Thinnes");
|
||||
addRow(2, "Projekt", "Projekt Hirnfrei");
|
||||
addRow(3, "Framework", QString("Qt %1").arg(QT_VERSION_STR));
|
||||
@@ -360,6 +477,7 @@ void MainWindow::showAbout()
|
||||
"<b>1</b>/<b>2</b> HW-Kontrast "
|
||||
"<b>3</b>/<b>4</b> HW-Sättigung "
|
||||
"<b>R</b> Roh-Diagnose<br>"
|
||||
"<b>F1</b>–<b>F9</b> Profile<br>"
|
||||
"<b>V</b> Vergleich "
|
||||
"<b>P</b> Screenshot "
|
||||
"<b>D</b> Frame dumpen "
|
||||
@@ -404,6 +522,17 @@ void MainWindow::stopAll()
|
||||
|
||||
void MainWindow::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
// F1..F9: Profil 1..9 (Reihenfolge wie im Profile-Menü, alphabetisch)
|
||||
if (event->key() >= Qt::Key_F1 && event->key() <= Qt::Key_F9) {
|
||||
const QStringList names = Profiles::names();
|
||||
const int idx = event->key() - Qt::Key_F1;
|
||||
if (idx < names.size())
|
||||
applyProfile(names.at(idx));
|
||||
else
|
||||
showOverlayMessage(QString("Kein Profil auf F%1").arg(idx + 1));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event->key()) {
|
||||
case Qt::Key_F:
|
||||
toggleFullscreen();
|
||||
@@ -494,7 +623,7 @@ void MainWindow::updateStatusBar()
|
||||
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 [,/.] HW-Helligkeit [1/2] HW-Kontrast [3/4] HW-Sättigung [R] Roh [V] Vergleich [P] Screenshot [D] Frame dumpen [Esc] Einstellungen")
|
||||
info += QString("Video: %1 Audio: %2 [F] Vollbild [S] Glättung [C] Retro [X/+/-] Schärfen [,/.] HW-Helligkeit [1/2] HW-Kontrast [3/4] HW-Sättigung [R] Roh [F1-F9] Profile [V] Vergleich [P] Screenshot [D] Frame dumpen [Esc] Einstellungen")
|
||||
.arg(m_videoDevice,
|
||||
m_audioDevice.isEmpty() ? QStringLiteral("–") : m_audioDevice);
|
||||
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
#include <QMainWindow>
|
||||
#include <QString>
|
||||
|
||||
#include "Profile.h"
|
||||
|
||||
class VideoWidget;
|
||||
class CaptureThread;
|
||||
class AudioThread;
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QAction;
|
||||
class QLabel;
|
||||
class QMenu;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
@@ -57,6 +60,14 @@ private:
|
||||
void applySharpen(float value);
|
||||
void showOverlayMessage(const QString& text);
|
||||
|
||||
// Profile
|
||||
Profile currentProfile() const;
|
||||
void rebuildProfileMenu();
|
||||
void applyProfile(const QString& name);
|
||||
void saveCurrentAsProfile();
|
||||
void deleteProfile(const QString& name);
|
||||
|
||||
QMenu* m_profileMenu = nullptr;
|
||||
VideoWidget* m_video = nullptr;
|
||||
CaptureThread* m_capture = nullptr;
|
||||
AudioThread* m_audio = nullptr;
|
||||
|
||||
168
src/Profile.cpp
Normal file
168
src/Profile.cpp
Normal file
@@ -0,0 +1,168 @@
|
||||
#include "Profile.h"
|
||||
|
||||
#include <QSettings>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QtGlobal>
|
||||
|
||||
// ── Profile ──────────────────────────────────────────────────────────────────
|
||||
|
||||
bool Profile::matches(const Profile& o) const
|
||||
{
|
||||
return smooth == o.smooth
|
||||
&& retro == o.retro
|
||||
&& qAbs(sharpen - o.sharpen) < 0.05f // Spinbox hat 1 Nachkommastelle
|
||||
&& hwBrightness == o.hwBrightness
|
||||
&& hwContrast == o.hwContrast
|
||||
&& hwSaturation == o.hwSaturation;
|
||||
}
|
||||
|
||||
Profile Profile::fromSettings()
|
||||
{
|
||||
QSettings s;
|
||||
Profile p;
|
||||
p.smooth = s.value("display/smooth", true).toBool();
|
||||
p.retro = s.value("display/retro", false).toBool();
|
||||
p.sharpen = s.value("display/sharpen", 0.0).toFloat();
|
||||
p.hwBrightness = s.contains("hardware/brightness") ? s.value("hardware/brightness").toInt() : -1;
|
||||
p.hwContrast = s.contains("hardware/contrast") ? s.value("hardware/contrast").toInt() : -1;
|
||||
p.hwSaturation = s.contains("hardware/saturation") ? s.value("hardware/saturation").toInt() : -1;
|
||||
return p;
|
||||
}
|
||||
|
||||
void Profile::applyToSettings() const
|
||||
{
|
||||
QSettings s;
|
||||
s.setValue("display/smooth", smooth);
|
||||
s.setValue("display/retro", retro);
|
||||
s.setValue("display/sharpen", static_cast<double>(sharpen));
|
||||
if (sharpen > 0.0f)
|
||||
s.setValue("display/sharpen_last", static_cast<double>(sharpen));
|
||||
|
||||
auto setOrRemove = [&](const char* key, int value) {
|
||||
if (value >= 0) s.setValue(key, value);
|
||||
else s.remove(key);
|
||||
};
|
||||
setOrRemove("hardware/brightness", hwBrightness);
|
||||
setOrRemove("hardware/contrast", hwContrast);
|
||||
setOrRemove("hardware/saturation", hwSaturation);
|
||||
}
|
||||
|
||||
// ── Profiles ─────────────────────────────────────────────────────────────────
|
||||
|
||||
namespace Profiles
|
||||
{
|
||||
|
||||
QStringList names()
|
||||
{
|
||||
QSettings s;
|
||||
s.beginGroup("profiles");
|
||||
QStringList list = s.childGroups();
|
||||
s.endGroup();
|
||||
list.sort(Qt::CaseInsensitive);
|
||||
return list;
|
||||
}
|
||||
|
||||
bool exists(const QString& name)
|
||||
{
|
||||
return isValidName(name) && names().contains(name);
|
||||
}
|
||||
|
||||
Profile load(const QString& name)
|
||||
{
|
||||
QSettings s;
|
||||
s.beginGroup("profiles/" + name);
|
||||
Profile p;
|
||||
p.smooth = s.value("smooth", true).toBool();
|
||||
p.retro = s.value("retro", false).toBool();
|
||||
p.sharpen = s.value("sharpen", 0.0).toFloat();
|
||||
p.hwBrightness = s.value("hwBrightness", -1).toInt();
|
||||
p.hwContrast = s.value("hwContrast", -1).toInt();
|
||||
p.hwSaturation = s.value("hwSaturation", -1).toInt();
|
||||
s.endGroup();
|
||||
return p;
|
||||
}
|
||||
|
||||
void save(const QString& name, const Profile& p)
|
||||
{
|
||||
if (!isValidName(name)) return;
|
||||
QSettings s;
|
||||
s.beginGroup("profiles/" + name);
|
||||
s.setValue("smooth", p.smooth);
|
||||
s.setValue("retro", p.retro);
|
||||
s.setValue("sharpen", static_cast<double>(p.sharpen));
|
||||
s.setValue("hwBrightness", p.hwBrightness);
|
||||
s.setValue("hwContrast", p.hwContrast);
|
||||
s.setValue("hwSaturation", p.hwSaturation);
|
||||
s.endGroup();
|
||||
s.sync();
|
||||
}
|
||||
|
||||
bool remove(const QString& name)
|
||||
{
|
||||
if (!exists(name)) return false;
|
||||
QSettings s;
|
||||
s.remove("profiles/" + name);
|
||||
s.sync();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isValidName(const QString& name)
|
||||
{
|
||||
// '/' und '\' wären für QSettings Gruppentrenner
|
||||
return !name.trimmed().isEmpty()
|
||||
&& name == name.trimmed()
|
||||
&& !name.contains('/')
|
||||
&& !name.contains('\\');
|
||||
}
|
||||
|
||||
QString findMatching(const Profile& profile)
|
||||
{
|
||||
for (const QString& n : names())
|
||||
if (load(n).matches(profile))
|
||||
return n;
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool saveWithPrompt(QWidget* parent, const Profile& profile, QString* savedName)
|
||||
{
|
||||
const QStringList existing = names();
|
||||
|
||||
bool ok = false;
|
||||
const QString input = QInputDialog::getItem(
|
||||
parent, "Profil speichern",
|
||||
"Name des Profils (neuen Namen eintippen\noder vorhandenes zum Überschreiben wählen):",
|
||||
existing, 0, true, &ok);
|
||||
if (!ok) return false;
|
||||
|
||||
const QString name = input.trimmed();
|
||||
if (!isValidName(name)) {
|
||||
QMessageBox::warning(parent, "Profil speichern",
|
||||
"Ungültiger Name. Der Name darf nicht leer sein und weder '/' noch '\\' enthalten.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (existing.contains(name)) {
|
||||
const auto answer = QMessageBox::question(
|
||||
parent, "Profil überschreiben",
|
||||
QString("Das Profil „%1“ existiert bereits. Überschreiben?").arg(name),
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||
if (answer != QMessageBox::Yes) return false;
|
||||
}
|
||||
|
||||
save(name, profile);
|
||||
if (savedName) *savedName = name;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool removeWithConfirm(QWidget* parent, const QString& name)
|
||||
{
|
||||
const auto answer = QMessageBox::question(
|
||||
parent, "Profil löschen",
|
||||
QString("Profil „%1“ wirklich löschen?").arg(name),
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||
if (answer != QMessageBox::Yes) return false;
|
||||
return remove(name);
|
||||
}
|
||||
|
||||
} // namespace Profiles
|
||||
49
src/Profile.h
Normal file
49
src/Profile.h
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QWidget;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
// Ein Profil bündelt alle Bildeinstellungen, die sich pro Konsole/Quelle
|
||||
// unterscheiden (z. B. PlayStation: aufhellen + Filter, PS3: alles neutral).
|
||||
// Hardware-Werte: -1 = "unverändert" (Regler am Grabber-Chip nicht anfassen
|
||||
// bzw. auf den Ausgangswert des Chips zurücksetzen).
|
||||
struct Profile
|
||||
{
|
||||
bool smooth = true;
|
||||
bool retro = false;
|
||||
float sharpen = 0.0f; // 0 = aus
|
||||
int hwBrightness = -1;
|
||||
int hwContrast = -1;
|
||||
int hwSaturation = -1;
|
||||
|
||||
bool matches(const Profile& other) const;
|
||||
|
||||
// Aktueller Stand laut QSettings (display/* und hardware/*).
|
||||
static Profile fromSettings();
|
||||
|
||||
// Schreibt das Profil als aktuellen Stand nach display/* und hardware/*.
|
||||
void applyToSettings() const;
|
||||
};
|
||||
|
||||
// Profilverwaltung (gespeichert in QSettings unter profiles/<Name>/...).
|
||||
namespace Profiles
|
||||
{
|
||||
QStringList names(); // alphabetisch sortiert
|
||||
bool exists(const QString& name);
|
||||
Profile load(const QString& name);
|
||||
void save(const QString& name, const Profile& profile);
|
||||
bool remove(const QString& name);
|
||||
bool isValidName(const QString& name);
|
||||
QString findMatching(const Profile& profile); // "" falls keins passt
|
||||
|
||||
// Dialog: Namen eingeben bzw. vorhandenes Profil wählen (Überschreiben
|
||||
// wird nachgefragt) und speichern. true = gespeichert.
|
||||
bool saveWithPrompt(QWidget* parent, const Profile& profile, QString* savedName = nullptr);
|
||||
|
||||
// Löschen nach Rückfrage. true = gelöscht.
|
||||
bool removeWithConfirm(QWidget* parent, const QString& name);
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QOpenGLTexture>
|
||||
#include <QPainter>
|
||||
#include <QVector2D>
|
||||
#include <QDebug>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -64,6 +65,7 @@ uniform float u_sharpen;
|
||||
uniform float u_gamma;
|
||||
uniform int u_raw;
|
||||
uniform int u_compare;
|
||||
uniform int u_debug;
|
||||
|
||||
vec3 rgb2ycbcr(vec3 c)
|
||||
{
|
||||
@@ -159,6 +161,12 @@ void main()
|
||||
{
|
||||
vec2 uv = v_tex;
|
||||
|
||||
// Diagnose (nur per HDMIVIEWER_DEBUG=shaderwhite): Shader gibt reines Weiss aus.
|
||||
if (u_debug == 1) {
|
||||
fragColor = vec4(1.0, 1.0, 1.0, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Diagnose: komplette Umgehung aller Verarbeitung, 1:1 wie im Speicher.
|
||||
if (u_raw == 1) {
|
||||
fragColor = vec4(texture(u_tex, uv).rgb, 1.0);
|
||||
@@ -338,6 +346,14 @@ void VideoWidget::initializeGL()
|
||||
initializeOpenGLFunctions();
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
// Diagnose-Ausgabe einmalig im Terminal: welche GL-Umgebung/Pixelformat aktiv ist.
|
||||
qDebug().noquote() << "GL_RENDERER:" << reinterpret_cast<const char*>(glGetString(GL_RENDERER))
|
||||
<< " GL_VERSION:" << reinterpret_cast<const char*>(glGetString(GL_VERSION));
|
||||
qDebug().noquote() << QString("Surface: R%1 G%2 B%3 A%4 samples=%5")
|
||||
.arg(format().redBufferSize()).arg(format().greenBufferSize())
|
||||
.arg(format().blueBufferSize()).arg(format().alphaBufferSize())
|
||||
.arg(format().samples());
|
||||
|
||||
m_program = new QOpenGLShaderProgram(this);
|
||||
m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, kVertexShaderSrc);
|
||||
m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, kFragmentShaderSrc);
|
||||
@@ -403,11 +419,81 @@ void VideoWidget::paintGL()
|
||||
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
// Diagnose-Schalter per Umgebungsvariable (im Normalbetrieb ohne Wirkung):
|
||||
// HDMIVIEWER_DEBUG=clearwhite -> Framebuffer nur mit Weiss loeschen, kein Shader
|
||||
// HDMIVIEWER_DEBUG=shaderwhite -> Shader wird benutzt, gibt aber reines Weiss aus
|
||||
static const QByteArray dbg = qgetenv("HDMIVIEWER_DEBUG");
|
||||
if (dbg == "clearwhite") {
|
||||
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_hasFrame || !m_texture) {
|
||||
drawNoSignal();
|
||||
return;
|
||||
}
|
||||
|
||||
// Diagnose (HDMIVIEWER_DEBUG=state oder =reset): GL-Zustand direkt VOR dem Zeichnen
|
||||
// einmalig ins Terminal schreiben (nach einem moeglichen QPainter in drawNoSignal).
|
||||
if (dbg == "state" || dbg == "reset") {
|
||||
static bool dumped = false;
|
||||
if (!dumped) {
|
||||
dumped = true;
|
||||
auto en = [this](GLenum cap) { return glIsEnabled(cap) ? 1 : 0; };
|
||||
GLint bsr = 0, bdr = 0, bsa = 0, bda = 0, beq = 0, fbo = 0, enc = 0, ctype = 0;
|
||||
GLfloat bc[4] = {0, 0, 0, 0};
|
||||
GLboolean cm[4] = {0, 0, 0, 0};
|
||||
glGetIntegerv(0x80C9, &bsr); // GL_BLEND_SRC_RGB
|
||||
glGetIntegerv(0x80C8, &bdr); // GL_BLEND_DST_RGB
|
||||
glGetIntegerv(0x80CB, &bsa); // GL_BLEND_SRC_ALPHA
|
||||
glGetIntegerv(0x80CA, &bda); // GL_BLEND_DST_ALPHA
|
||||
glGetIntegerv(0x8009, &beq); // GL_BLEND_EQUATION_RGB
|
||||
glGetIntegerv(0x8CA6, &fbo); // GL_DRAW_FRAMEBUFFER_BINDING
|
||||
glGetFloatv(0x8005, bc); // GL_BLEND_COLOR
|
||||
glGetBooleanv(0x0C23, cm); // GL_COLOR_WRITEMASK
|
||||
glGetFramebufferAttachmentParameteriv(0x8CA9, 0x8CE0, 0x8210, &enc); // COLOR_ENCODING
|
||||
glGetFramebufferAttachmentParameteriv(0x8CA9, 0x8CE0, 0x8211, &ctype); // COMPONENT_TYPE
|
||||
const QStringList st = {
|
||||
QString("BLEND=%1").arg(en(0x0BE2)),
|
||||
QString("blendRGB src=0x%1 dst=0x%2").arg(bsr, 0, 16).arg(bdr, 0, 16),
|
||||
QString("blendA src=0x%1 dst=0x%2").arg(bsa, 0, 16).arg(bda, 0, 16),
|
||||
QString("eq=0x%1").arg(beq, 0, 16),
|
||||
QString("blendColor=%1,%2,%3,%4").arg(bc[0]).arg(bc[1]).arg(bc[2]).arg(bc[3]),
|
||||
QString("colorMask=%1%2%3%4").arg(int(cm[0])).arg(int(cm[1])).arg(int(cm[2])).arg(int(cm[3])),
|
||||
QString("FB_SRGB=%1").arg(en(0x8DB9)),
|
||||
QString("LOGIC_OP=%1").arg(en(0x0BF2)),
|
||||
QString("A2C=%1").arg(en(0x809E)),
|
||||
QString("DITHER=%1").arg(en(0x0BD0)),
|
||||
QString("MULTISAMPLE=%1").arg(en(0x809D)),
|
||||
QString("SCISSOR=%1").arg(en(0x0C11)),
|
||||
QString("DEPTH=%1").arg(en(0x0B71)),
|
||||
QString("STENCIL=%1").arg(en(0x0B90)),
|
||||
QString("CULL=%1").arg(en(0x0B44)),
|
||||
QString("drawFBO=%1").arg(fbo),
|
||||
QString("attachEncoding=0x%1 (0x8c40=sRGB, 0x2601=linear)").arg(enc, 0, 16),
|
||||
QString("attachType=0x%1 (0x8c17=UNORM, 0x8f9c=SNORM, 0x1406=FLOAT)").arg(ctype, 0, 16)
|
||||
};
|
||||
qDebug().noquote() << "GL-State vor dem Zeichnen:" << st.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
// Fester Zustands-Reset vor dem Zeichnen: drawNoSignal() nutzt QPainter, der GL-Zustand
|
||||
// (u. a. Blending) zuruecklaesst. Ohne diesen Reset war das Bild live/im Screenshot
|
||||
// exakt auf die Haelfte abgedunkelt (Weiss 255 -> 128), auch im Roh-Modus.
|
||||
{
|
||||
glDisable(0x0BE2); // GL_BLEND
|
||||
glDisable(0x0B71); // GL_DEPTH_TEST
|
||||
glDisable(0x0B90); // GL_STENCIL_TEST
|
||||
glDisable(0x0C11); // GL_SCISSOR_TEST
|
||||
glDisable(0x0B44); // GL_CULL_FACE
|
||||
glDisable(0x8DB9); // GL_FRAMEBUFFER_SRGB
|
||||
glDisable(0x0BF2); // GL_COLOR_LOGIC_OP
|
||||
glDisable(0x809E); // GL_SAMPLE_ALPHA_TO_COVERAGE
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
}
|
||||
|
||||
const float viewW = static_cast<float>(width());
|
||||
const float viewH = static_cast<float>(height());
|
||||
const float imgW = static_cast<float>(m_imageSize.width());
|
||||
@@ -441,6 +527,7 @@ void VideoWidget::paintGL()
|
||||
m_program->setUniformValue("u_gamma", m_brightness);
|
||||
m_program->setUniformValue("u_raw", m_rawEnabled ? 1 : 0);
|
||||
m_program->setUniformValue("u_compare", m_compareEnabled ? 1 : 0);
|
||||
m_program->setUniformValue("u_debug", dbg == "shaderwhite" ? 1 : 0);
|
||||
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <QSettings>
|
||||
#include <QMessageBox>
|
||||
#include <QSurfaceFormat>
|
||||
#include <QIcon>
|
||||
|
||||
#include "DeviceDialog.h"
|
||||
#include "MainWindow.h"
|
||||
@@ -18,9 +19,13 @@ int main(int argc, char* argv[])
|
||||
|
||||
QApplication app(argc, argv);
|
||||
app.setApplicationName("HDMIViewer");
|
||||
app.setApplicationVersion("1.0");
|
||||
app.setApplicationVersion("1.1.0");
|
||||
app.setOrganizationName("hdmi-viewer");
|
||||
|
||||
// Fenster-/Taskleisten-Icon: installiertes Theme-Icon bevorzugen, sonst das
|
||||
// eingebettete SVG (z. B. beim Start direkt aus dem build-Ordner).
|
||||
app.setWindowIcon(QIcon::fromTheme("hdmi-viewer", QIcon(":/icons/hdmi-viewer.svg")));
|
||||
|
||||
QSettings settings;
|
||||
|
||||
while (true) {
|
||||
@@ -34,6 +39,7 @@ int main(int argc, char* argv[])
|
||||
dlg.setPreselectedHwBrightness(settings.value("hardware/brightness", -1).toInt());
|
||||
dlg.setPreselectedHwContrast(settings.value("hardware/contrast", -1).toInt());
|
||||
dlg.setPreselectedHwSaturation(settings.value("hardware/saturation", -1).toInt());
|
||||
dlg.refreshProfileSelection(); // zeigt das Profil, das zu den Werten passt
|
||||
|
||||
if (dlg.exec() != QDialog::Accepted)
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user