// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2026 Dany Thinnes #include "mainwindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef APP_VERSION #define APP_VERSION "0.2.0" #endif namespace { constexpr int RolePath = Qt::UserRole; constexpr int RoleIsDir = Qt::UserRole + 1; constexpr int RoleUp = Qt::UserRole + 2; QString humanSize(qint64 s) { if (s < 1024) return QString::number(s) + QStringLiteral(" B"); return QString::number(s / 1024.0, 'f', 1) + QStringLiteral(" KiB"); } QString parentOf(const QString &path) { const int i = path.lastIndexOf('/'); return i < 0 ? QString() : path.left(i); } // Einfaches Diskettensymbol als Platzhalter-Logo (kann spaeter durch ein PNG ersetzt werden) QPixmap makeLogo(int size) { QPixmap pm(size, size); pm.fill(Qt::transparent); QPainter p(&pm); p.setRenderHint(QPainter::Antialiasing); p.scale(size / 72.0, size / 72.0); p.setPen(Qt::NoPen); QPainterPath body; body.addRoundedRect(QRectF(6, 6, 60, 60), 5, 5); QPainterPath cut; cut.moveTo(48, 6); cut.lineTo(66, 24); cut.lineTo(66, 6); cut.closeSubpath(); p.setBrush(QColor("#56b6c2")); p.drawPath(body.subtracted(cut)); p.setBrush(QColor("#1e1e1e")); p.drawRect(QRectF(20, 6, 26, 18)); p.setBrush(QColor("#d4d4d4")); p.drawRect(QRectF(37, 9, 6, 12)); p.drawRoundedRect(QRectF(14, 38, 44, 24), 2, 2); p.setBrush(QColor("#56b6c2")); p.drawRect(QRectF(19, 44, 34, 2)); p.drawRect(QRectF(19, 50, 26, 2)); return pm; } bool runUdisks(const QStringList &args, QString &output) { QProcess p; p.start(QStringLiteral("udisksctl"), args); if (!p.waitForStarted(5000)) { output = QStringLiteral("udisksctl konnte nicht gestartet werden (Paket udisks2 installiert?)"); return false; } p.waitForFinished(30000); output = (QString::fromUtf8(p.readAllStandardOutput()) + QString::fromUtf8(p.readAllStandardError())).trimmed(); return p.exitStatus() == QProcess::NormalExit && p.exitCode() == 0; } } // namespace MainWindow::MainWindow() { setWindowTitle(QStringLiteral("ADF Manager")); setWindowIcon(QIcon(makeLogo(64))); resize(1200, 700); QSettings s; m_tool.setProgram(s.value("xdftool", "xdftool").toString()); // ---- Menue ---- QMenu *fileMenu = menuBar()->addMenu(tr("&Datei")); fileMenu->addAction(tr("ADF &öffnen …"), QKeySequence::Open, this, &MainWindow::openAdf); fileMenu->addAction(tr("&Neu laden"), QKeySequence::Refresh, this, &MainWindow::reload); m_ejectAct = fileMenu->addAction(tr("ADF-Medium &auswerfen"), this, &MainWindow::ejectDevice); m_ejectAct->setEnabled(false); fileMenu->addSeparator(); fileMenu->addAction(tr("xdftool-&Pfad …"), this, &MainWindow::setToolPath); fileMenu->addSeparator(); fileMenu->addAction(tr("&Beenden"), QKeySequence::Quit, this, &QWidget::close); QMenu *helpMenu = menuBar()->addMenu(tr("&Hilfe")); helpMenu->addAction(tr("Über ADF Manager"), this, &MainWindow::showAbout); helpMenu->addAction(tr("Über &Qt"), this, [] { QApplication::aboutQt(); }); // ---- Linke Seite: ADF ---- m_adfLabel = new QLabel(tr("Keine ADF geöffnet")); auto *btnOpen = new QPushButton(tr("ADF öffnen …")); connect(btnOpen, &QPushButton::clicked, this, &MainWindow::openAdf); m_ejectBtn = new QPushButton(tr("⏏ Auswerfen")); m_ejectBtn->setEnabled(false); m_ejectBtn->setToolTip(tr("Nur aktiv, wenn die ADF auf einer SD-Karte / einem Wechseldatenträger liegt")); connect(m_ejectBtn, &QPushButton::clicked, this, &MainWindow::ejectDevice); m_cwdLabel = new QLabel; m_adfTree = new QTreeWidget; m_adfTree->setColumnCount(3); m_adfTree->setHeaderLabels({ tr("Name"), tr("Größe"), tr("Typ") }); m_adfTree->setSelectionMode(QAbstractItemView::ExtendedSelection); m_adfTree->setAlternatingRowColors(true); m_adfTree->setRootIsDecorated(false); m_adfTree->setUniformRowHeights(true); m_adfTree->header()->setSectionResizeMode(QHeaderView::Interactive); m_adfTree->header()->setStretchLastSection(false); m_adfTree->setColumnWidth(0, 320); m_adfTree->setColumnWidth(1, 90); m_adfTree->setColumnWidth(2, 80); connect(m_adfTree, &QTreeWidget::itemActivated, this, &MainWindow::adfActivated); auto *btnDelete = new QPushButton(tr("Löschen")); auto *btnRename = new QPushButton(tr("Umbenennen")); auto *btnMkdir = new QPushButton(tr("Neuer Ordner")); connect(btnDelete, &QPushButton::clicked, this, &MainWindow::deleteSelected); connect(btnRename, &QPushButton::clicked, this, &MainWindow::renameSelected); connect(btnMkdir, &QPushButton::clicked, this, &MainWindow::makeDir); auto *leftTop = new QHBoxLayout; leftTop->addWidget(btnOpen); leftTop->addWidget(m_adfLabel, 1); leftTop->addWidget(m_ejectBtn); auto *leftBottom = new QHBoxLayout; leftBottom->addWidget(btnDelete); leftBottom->addWidget(btnRename); leftBottom->addWidget(btnMkdir); leftBottom->addStretch(1); auto *left = new QWidget; auto *leftLay = new QVBoxLayout(left); leftLay->addLayout(leftTop); leftLay->addWidget(m_cwdLabel); leftLay->addWidget(m_adfTree, 1); leftLay->addLayout(leftBottom); // ---- Mitte: Kopier-Buttons ---- auto *btnToPc = new QPushButton(tr("→\nAuf PC\nkopieren")); auto *btnToAdf = new QPushButton(tr("←\nIn ADF\nkopieren")); btnToPc->setMinimumHeight(70); btnToAdf->setMinimumHeight(70); connect(btnToPc, &QPushButton::clicked, this, &MainWindow::adfToPc); connect(btnToAdf, &QPushButton::clicked, this, &MainWindow::pcToAdf); auto *mid = new QWidget; auto *midLay = new QVBoxLayout(mid); midLay->addStretch(1); midLay->addWidget(btnToPc); midLay->addWidget(btnToAdf); midLay->addStretch(1); mid->setFixedWidth(100); // ---- Rechte Seite: PC-Ordner ---- m_localEdit = new QLineEdit; m_localEdit->setReadOnly(true); auto *btnChoose = new QPushButton(tr("Ordner wählen …")); auto *btnUp = new QPushButton(tr("↑")); btnUp->setToolTip(tr("Eine Ebene höher")); btnUp->setFixedWidth(36); connect(btnChoose, &QPushButton::clicked, this, &MainWindow::chooseLocalDir); connect(btnUp, &QPushButton::clicked, this, &MainWindow::localUp); const bool showHidden = s.value("showHidden", false).toBool(); m_localModel = new QFileSystemModel(this); m_localModel->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | (showHidden ? QDir::Hidden : QDir::Filters())); m_localView = new QTreeView; m_localView->setModel(m_localModel); m_localView->setSelectionMode(QAbstractItemView::ExtendedSelection); m_localView->setRootIsDecorated(false); m_localView->setSortingEnabled(true); m_localView->sortByColumn(0, Qt::AscendingOrder); m_localView->setAlternatingRowColors(true); m_localView->header()->setSectionResizeMode(QHeaderView::Interactive); m_localView->header()->setStretchLastSection(false); m_localView->setColumnWidth(0, 320); m_localView->setColumnWidth(1, 90); m_localView->setColumnWidth(2, 90); m_localView->setColumnWidth(3, 140); connect(m_localView, &QTreeView::doubleClicked, this, &MainWindow::localActivated); auto *chkHidden = new QCheckBox(tr("Versteckte Dateien anzeigen")); chkHidden->setChecked(showHidden); connect(chkHidden, &QCheckBox::toggled, this, [this](bool on) { m_localModel->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | (on ? QDir::Hidden : QDir::Filters())); QSettings().setValue("showHidden", on); }); auto *rightTop = new QHBoxLayout; rightTop->addWidget(btnChoose); rightTop->addWidget(btnUp); rightTop->addWidget(m_localEdit, 1); auto *right = new QWidget; auto *rightLay = new QVBoxLayout(right); rightLay->addLayout(rightTop); rightLay->addWidget(chkHidden); rightLay->addWidget(m_localView, 1); // ---- Zusammenbau ---- auto *top = new QWidget; auto *topLay = new QHBoxLayout(top); topLay->setContentsMargins(0, 0, 0, 0); topLay->addWidget(left, 1); topLay->addWidget(mid); topLay->addWidget(right, 1); m_log = new QPlainTextEdit; m_log->setReadOnly(true); m_log->setMaximumBlockCount(2000); auto *split = new QSplitter(Qt::Vertical); split->addWidget(top); split->addWidget(m_log); split->setStretchFactor(0, 5); split->setStretchFactor(1, 1); setCentralWidget(split); setLocalDir(s.value("localDir", QDir::homePath()).toString()); updatePathLabel(); // Fenstergeometrie und Spaltenbreiten wiederherstellen if (s.contains("geometry")) restoreGeometry(s.value("geometry").toByteArray()); if (s.contains("adfHeader")) m_adfTree->header()->restoreState(s.value("adfHeader").toByteArray()); if (s.contains("localHeader")) m_localView->header()->restoreState(s.value("localHeader").toByteArray()); } void MainWindow::closeEvent(QCloseEvent *e) { QSettings s; s.setValue("geometry", saveGeometry()); s.setValue("adfHeader", m_adfTree->header()->saveState()); s.setValue("localHeader", m_localView->header()->saveState()); QMainWindow::closeEvent(e); } void MainWindow::log(const QString &text) { m_log->appendPlainText(text); } bool MainWindow::haveAdf() { if (m_adf.isEmpty()) { QMessageBox::information(this, tr("ADF Manager"), tr("Bitte zuerst eine ADF öffnen.")); return false; } return true; } // ------------------------------------------------------------------ ADF laden void MainWindow::openAdf() { QSettings s; const QString file = QFileDialog::getOpenFileName( this, tr("ADF auswählen"), s.value("adfDir", QDir::homePath()).toString(), tr("Amiga-Disk-Images (*.adf *.ADF *.adz);;Alle Dateien (*)")); if (file.isEmpty()) return; s.setValue("adfDir", QFileInfo(file).absolutePath()); loadAdf(file); } void MainWindow::loadAdf(const QString &file) { m_adf = file; m_cwd.clear(); m_adfLabel->setText(QFileInfo(file).fileName()); setWindowTitle(QStringLiteral("ADF Manager – %1").arg(QFileInfo(file).fileName())); updateEjectState(); reload(); } void MainWindow::reload() { if (m_adf.isEmpty()) return; QApplication::setOverrideCursor(Qt::WaitCursor); QList entries; const XdfTool::Result r = m_tool.list(m_adf, entries); QApplication::restoreOverrideCursor(); log(QStringLiteral("$ ") + r.commandLine); if (!r.ok) { log(r.err.isEmpty() ? r.out : r.err); QMessageBox::warning(this, tr("xdftool"), tr("Inhalt konnte nicht gelesen werden:\n%1").arg(r.err.isEmpty() ? r.out : r.err)); return; } if (entries.isEmpty()) log(tr("(keine Einträge erkannt – Rohausgabe:)\n") + r.out); m_entries = entries; m_known.clear(); m_dirs.clear(); for (const AdfEntry &e : m_entries) { m_known.insert(e.path.toLower()); if (e.isDir) m_dirs.insert(e.path.toLower()); } // Aktuellen Ordner beibehalten, falls er noch existiert while (!m_cwd.isEmpty() && !m_dirs.contains(m_cwd.toLower())) m_cwd = parentOf(m_cwd); showDir(); } // Zeigt den Inhalt des aktuellen ADF-Ordners (m_cwd) void MainWindow::showDir() { m_adfTree->clear(); if (!m_cwd.isEmpty()) { auto *up = new QTreeWidgetItem(m_adfTree); up->setText(0, QStringLiteral("..")); up->setText(2, tr("Ordner")); up->setIcon(0, style()->standardIcon(QStyle::SP_FileDialogToParent)); up->setData(0, RoleUp, true); up->setData(0, RoleIsDir, true); up->setData(0, RolePath, parentOf(m_cwd)); up->setFlags(Qt::ItemIsEnabled); // nicht auswaehlbar, aber aktivierbar } QList children; for (const AdfEntry &e : m_entries) { if (parentOf(e.path) == m_cwd) children << e; } std::sort(children.begin(), children.end(), [](const AdfEntry &a, const AdfEntry &b) { if (a.isDir != b.isDir) return a.isDir; return a.name.compare(b.name, Qt::CaseInsensitive) < 0; }); for (const AdfEntry &e : children) { auto *item = new QTreeWidgetItem(m_adfTree); item->setText(0, e.name); item->setToolTip(0, e.name); item->setText(1, e.isDir ? QString() : humanSize(e.size)); item->setText(2, e.isDir ? tr("Ordner") : tr("Datei")); item->setTextAlignment(1, Qt::AlignRight | Qt::AlignVCenter); item->setIcon(0, style()->standardIcon(e.isDir ? QStyle::SP_DirIcon : QStyle::SP_FileIcon)); item->setData(0, RolePath, e.path); item->setData(0, RoleIsDir, e.isDir); item->setData(0, RoleUp, false); } updatePathLabel(); } void MainWindow::updatePathLabel() { if (m_adf.isEmpty()) m_cwdLabel->setText(tr("Ziel/Quelle in der ADF: –")); else m_cwdLabel->setText(tr("Aktueller ADF-Ordner: /%1 (Doppelklick öffnet Ordner)").arg(m_cwd.toHtmlEscaped())); } void MainWindow::adfActivated(QTreeWidgetItem *item, int) { if (!item || !item->data(0, RoleIsDir).toBool()) return; m_cwd = item->data(0, RolePath).toString(); showDir(); } QList MainWindow::selectedEntries() const { QList result; for (QTreeWidgetItem *item : m_adfTree->selectedItems()) { if (!item->data(0, RoleUp).toBool()) result << item; } return result; } // ------------------------------------------------------------------ xdftool ausfuehren bool MainWindow::runBatch(const QList &cmds) { QApplication::setOverrideCursor(Qt::WaitCursor); const XdfTool::Result r = m_tool.runBatch(m_adf, cmds); QApplication::restoreOverrideCursor(); log(QStringLiteral("$ ") + r.commandLine); if (!r.out.trimmed().isEmpty()) log(r.out.trimmed()); if (!r.err.trimmed().isEmpty()) log(r.err.trimmed()); if (!r.ok) { QMessageBox::warning(this, tr("xdftool"), tr("xdftool meldet einen Fehler:\n%1").arg(r.err.isEmpty() ? r.out : r.err)); } return r.ok; } // ------------------------------------------------------------------ ADF -> PC void MainWindow::adfToPc() { if (!haveAdf()) return; const QList items = selectedEntries(); if (items.isEmpty()) { QMessageBox::information(this, tr("ADF Manager"), tr("Links nichts ausgewählt.")); return; } QList cmds; for (QTreeWidgetItem *item : items) { const QString path = item->data(0, RolePath).toString(); cmds << QStringList{ QStringLiteral("read"), path, QDir(m_localDir).filePath(item->text(0)) }; } runBatch(cmds); } // ------------------------------------------------------------------ PC -> ADF void MainWindow::pcToAdf() { if (!haveAdf()) return; const QModelIndexList rows = m_localView->selectionModel()->selectedRows(0); if (rows.isEmpty()) { QMessageBox::information(this, tr("ADF Manager"), tr("Rechts nichts ausgewählt.")); return; } // Ziel ist immer der links angezeigte ADF-Ordner const QString dir = m_cwd; QList cmds; QStringList overwritten; for (const QModelIndex &idx : rows) { const QString local = m_localModel->filePath(idx); const QString name = QFileInfo(local).fileName(); const QString ami = dir.isEmpty() ? name : dir + QLatin1Char('/') + name; if (m_known.contains(ami.toLower())) { overwritten << ami; cmds << QStringList{ QStringLiteral("delete"), ami, QStringLiteral("all") }; } cmds << QStringList{ QStringLiteral("write"), local, ami }; } if (!overwritten.isEmpty()) { const auto answer = QMessageBox::question( this, tr("Vorhandene Einträge ersetzen"), tr("Diese Einträge existieren bereits in der ADF und werden vorher gelöscht:\n\n%1\n\nFortfahren?") .arg(overwritten.join('\n'))); if (answer != QMessageBox::Yes) return; } runBatch(cmds); reload(); } // ------------------------------------------------------------------ Loeschen / Umbenennen / Ordner void MainWindow::deleteSelected() { if (!haveAdf()) return; const QList items = selectedEntries(); if (items.isEmpty()) return; QStringList names; QList cmds; for (QTreeWidgetItem *item : items) { const QString path = item->data(0, RolePath).toString(); names << path; cmds << QStringList{ QStringLiteral("delete"), path, QStringLiteral("all") }; } if (QMessageBox::question(this, tr("Löschen"), tr("Wirklich aus der ADF löschen?\n\n%1").arg(names.join('\n'))) != QMessageBox::Yes) return; runBatch(cmds); reload(); } void MainWindow::renameSelected() { if (!haveAdf()) return; const QList items = selectedEntries(); if (items.size() != 1) { QMessageBox::information(this, tr("Umbenennen"), tr("Bitte genau einen Eintrag auswählen.")); return; } QTreeWidgetItem *item = items.first(); bool ok = false; const QString newName = QInputDialog::getText(this, tr("Umbenennen"), tr("Neuer Name:"), QLineEdit::Normal, item->text(0), &ok); if (!ok || newName.isEmpty() || newName == item->text(0)) return; const QString oldPath = item->data(0, RolePath).toString(); const QString parent = parentOf(oldPath); const QString newPath = parent.isEmpty() ? newName : parent + QLatin1Char('/') + newName; runBatch({ QStringList{ QStringLiteral("rename"), oldPath, newPath } }); reload(); } void MainWindow::makeDir() { if (!haveAdf()) return; bool ok = false; const QString name = QInputDialog::getText(this, tr("Neuer Ordner"), tr("Name:"), QLineEdit::Normal, QString(), &ok); if (!ok || name.isEmpty()) return; const QString path = m_cwd.isEmpty() ? name : m_cwd + QLatin1Char('/') + name; runBatch({ QStringList{ QStringLiteral("makedir"), path } }); reload(); } // ------------------------------------------------------------------ PC-Seite void MainWindow::setLocalDir(const QString &dir) { if (!QFileInfo(dir).isDir()) return; m_localDir = QDir(dir).absolutePath(); m_localEdit->setText(m_localDir); m_localModel->setRootPath(m_localDir); m_localView->setRootIndex(m_localModel->index(m_localDir)); QSettings().setValue("localDir", m_localDir); } void MainWindow::chooseLocalDir() { const QString dir = QFileDialog::getExistingDirectory(this, tr("Ordner auf dem PC"), m_localDir); if (!dir.isEmpty()) setLocalDir(dir); } void MainWindow::localUp() { QDir d(m_localDir); if (d.cdUp()) setLocalDir(d.absolutePath()); } void MainWindow::localActivated(const QModelIndex &idx) { if (m_localModel->isDir(idx)) setLocalDir(m_localModel->filePath(idx)); } void MainWindow::setToolPath() { bool ok = false; const QString p = QInputDialog::getText(this, tr("xdftool"), tr("Programm / Pfad zu xdftool:"), QLineEdit::Normal, m_tool.program(), &ok); if (ok && !p.isEmpty()) { m_tool.setProgram(p); QSettings().setValue("xdftool", p); } } // ------------------------------------------------------------------ Auswerfen void MainWindow::updateEjectState() { m_removable = m_adf.isEmpty() ? RemovableInfo() : detectRemovable(m_adf); m_ejectBtn->setEnabled(m_removable.removable); m_ejectAct->setEnabled(m_removable.removable); if (m_removable.removable) { const QString tip = tr("%1 auswerfen (%2)") .arg(m_removable.label.isEmpty() ? m_removable.partition : m_removable.label, m_removable.partition); m_ejectBtn->setToolTip(tip); log(tr("ADF liegt auf Wechseldatenträger: %1 (%2)").arg(m_removable.partition, m_removable.mountPoint)); } else { m_ejectBtn->setToolTip(tr("Nur aktiv, wenn die ADF auf einer SD-Karte / einem Wechseldatenträger liegt")); } } void MainWindow::ejectDevice() { if (!m_removable.removable) return; const RemovableInfo dev = m_removable; // PC-Ansicht verlassen, falls sie auf dem Medium steht if (m_localDir == dev.mountPoint || m_localDir.startsWith(dev.mountPoint + QLatin1Char('/'))) setLocalDir(QDir::homePath()); QApplication::setOverrideCursor(Qt::WaitCursor); QString out; const bool unmounted = runUdisks({ QStringLiteral("unmount"), QStringLiteral("-b"), dev.partition }, out); log(QStringLiteral("$ udisksctl unmount -b %1\n%2").arg(dev.partition, out)); bool poweredOff = false; QString out2; if (unmounted) { poweredOff = runUdisks({ QStringLiteral("power-off"), QStringLiteral("-b"), dev.disk }, out2); log(QStringLiteral("$ udisksctl power-off -b %1\n%2").arg(dev.disk, out2)); } QApplication::restoreOverrideCursor(); if (!unmounted) { QMessageBox::warning(this, tr("Auswerfen"), tr("Das Medium konnte nicht ausgehängt werden:\n%1").arg(out)); return; } // ADF-Ansicht leeren, die Datei ist nicht mehr erreichbar m_adf.clear(); m_entries.clear(); m_known.clear(); m_dirs.clear(); m_cwd.clear(); m_adfTree->clear(); m_adfLabel->setText(tr("Keine ADF geöffnet")); setWindowTitle(QStringLiteral("ADF Manager")); updatePathLabel(); updateEjectState(); if (poweredOff) { QMessageBox::information(this, tr("Auswerfen"), tr("Das Medium wurde ausgehängt und kann entfernt werden.")); } else { QMessageBox::information(this, tr("Auswerfen"), tr("Das Medium wurde ausgehängt.\nAbschalten war nicht möglich:\n%1").arg(out2)); } } // ------------------------------------------------------------------ Ueber-Dialog void MainWindow::showAbout() { auto *dlg = new QDialog(this); dlg->setWindowTitle(tr("Über ADF Manager")); dlg->setFixedWidth(400); auto *root = new QVBoxLayout(dlg); root->setContentsMargins(0, 0, 0, 0); root->setSpacing(0); auto *header = new QWidget(dlg); header->setFixedHeight(110); header->setStyleSheet("background-color: #1e1e1e;"); auto *headerLayout = new QHBoxLayout(header); headerLayout->setContentsMargins(20, 14, 24, 14); headerLayout->setSpacing(16); auto *logoLabel = new QLabel(header); logoLabel->setPixmap(makeLogo(72)); logoLabel->setStyleSheet("background: transparent;"); logoLabel->setFixedSize(72, 72); auto *titleBlock = new QVBoxLayout(); titleBlock->setSpacing(4); auto *titleLabel = new QLabel("ADF Manager", header); titleLabel->setStyleSheet( "color: #ffffff; font-size: 26px; font-weight: bold; background: transparent;"); auto *subtitleLabel = new QLabel(tr("Frontend für Amiga-Disk-Images (xdftool)"), header); subtitleLabel->setStyleSheet( "color: #56b6c2; font-size: 11px; background: transparent;"); titleBlock->addWidget(titleLabel); titleBlock->addWidget(subtitleLabel); headerLayout->addWidget(logoLabel); headerLayout->addLayout(titleBlock); root->addWidget(header); auto *body = new QWidget(dlg); body->setStyleSheet("background-color: #252526;"); auto *grid = new QGridLayout(body); grid->setContentsMargins(24, 20, 24, 20); grid->setVerticalSpacing(10); grid->setHorizontalSpacing(16); grid->setColumnMinimumWidth(0, 90); const QString labelStyle = "color: #888888; font-weight: bold; background: transparent;"; const QString valueStyle = "color: #d4d4d4; background: transparent;"; auto addRow = [&](int row, const QString &label, const QString &value) { auto *l = new QLabel(label, body); auto *v = new QLabel(value, body); l->setStyleSheet(labelStyle); v->setStyleSheet(valueStyle); l->setAlignment(Qt::AlignRight | Qt::AlignVCenter); grid->addWidget(l, row, 0); grid->addWidget(v, row, 1); }; addRow(0, tr("Version"), APP_VERSION); addRow(1, tr("Entwickler"), "Dany Thinnes"); addRow(2, tr("Projekt"), "Projekt Hirnfrei"); addRow(3, tr("Framework"), QString("Qt %1").arg(QT_VERSION_STR)); addRow(4, tr("Sprache"), "C++17"); root->addWidget(body, 1); auto *footer = new QWidget(dlg); footer->setStyleSheet("background-color: #252526; border-top: 1px solid #3c3c3c;"); auto *footerLayout = new QHBoxLayout(footer); footerLayout->setContentsMargins(16, 10, 16, 10); auto *closeBtn = new QPushButton(tr("Schließen"), footer); closeBtn->setDefault(true); closeBtn->setMinimumWidth(90); connect(closeBtn, &QPushButton::clicked, dlg, &QDialog::accept); footerLayout->addStretch(); footerLayout->addWidget(closeBtn); root->addWidget(footer); dlg->exec(); dlg->deleteLater(); }