From d63c3feb9fc2476c135e22aff821e260faf72b89 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Sat, 16 May 2026 13:46:06 -0500 Subject: ui: inline trash icon on backup-restore dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User feedback: the only way to clean up old backups was to hand-edit the profile directory, since the "Choose backup to restore" dialog exposed select-or-cancel and no delete affordance. Add an addChoice overload on SelectionDialog that takes a per-row delete callback. Each row is rendered as a horizontal layout — QCommandLinkButton (stretched) plus a small auto-raised QToolButton with a trash icon (user-trash → edit-delete → SP_TrashIcon fallback chain). Clicking the trash prompts for confirmation, invokes the callback, and hides the row on success; the QCommandLink remains live for restore. numChoices() now counts visible rows under scrollAreaWidgetContents so the "no backups" check stays correct as rows disappear. MainWindow::queryRestore wires the callback to delete the matching Plugins.txt., loadorder.txt., and lockedorder.txt. triplet via QFile::remove. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/src/mainwindow.cpp | 38 +++++++++++++-- src/src/selectiondialog.cpp | 109 +++++++++++++++++++++++++++++++++++++++++++- src/src/selectiondialog.h | 12 +++++ 3 files changed, 154 insertions(+), 5 deletions(-) diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp index 6a663ce..ffdd4bb 100644 --- a/src/src/mainwindow.cpp +++ b/src/src/mainwindow.cpp @@ -4003,20 +4003,50 @@ QString MainWindow::queryRestore(const QString& filePath) pluginFileInfo.fileName() + "\\.([A-Za-z]{6})")); QRegularExpression const exp3( QRegularExpression::anchoredPattern(pluginFileInfo.fileName() + "\\.(.*)")); + // Each restore button gets a sibling trash button that deletes the backup + // companion files (Plugins.txt., loadorder.txt., lockedorder.txt.). + const QString loadOrderName = + m_OrganizerCore.currentProfile()->getLoadOrderFileName(); + const QString lockedName = + m_OrganizerCore.currentProfile()->getLockedOrderFileName(); + auto deleteBackupSet = [filePath, loadOrderName, lockedName]( + const QString& suffix) -> bool { + bool ok = true; + for (const QString& base : {filePath, loadOrderName, lockedName}) { + const QString candidate = base + "." + suffix; + if (QFile::exists(candidate) && !QFile::remove(candidate)) { + MOBase::log::warn("failed to delete backup file '{}'", + candidate.toStdString()); + ok = false; + } + } + return ok; + }; for (const QFileInfo& info : boost::adaptors::reverse(files)) { auto match = exp.match(info.fileName()); auto match2 = exp2.match(info.fileName()); auto match3 = exp3.match(info.fileName()); if (match.hasMatch()) { QDateTime const time = QDateTime::fromString(match.captured(1), PATTERN_BACKUP_DATE); - dialog.addChoice(time.toString(), "", match.captured(1)); + const QString suffix = match.captured(1); + dialog.addChoice(time.toString(), "", suffix, + [deleteBackupSet, suffix]() { + return deleteBackupSet(suffix); + }); } else if (match2.hasMatch()) { - dialog.addChoice(match2.captured(1), + const QString suffix = match2.captured(1); + dialog.addChoice(suffix, tr("This file might be left over following a crash or power " "loss event. Check its contents before restoring."), - match2.captured(1)); + suffix, [deleteBackupSet, suffix]() { + return deleteBackupSet(suffix); + }); } else if (match3.hasMatch()) { - dialog.addChoice(match3.captured(1), "", match3.captured(1)); + const QString suffix = match3.captured(1); + dialog.addChoice(suffix, "", suffix, + [deleteBackupSet, suffix]() { + return deleteBackupSet(suffix); + }); } } diff --git a/src/src/selectiondialog.cpp b/src/src/selectiondialog.cpp index 25fc765..f61fae1 100644 --- a/src/src/selectiondialog.cpp +++ b/src/src/selectiondialog.cpp @@ -21,6 +21,12 @@ along with Mod Organizer. If not, see . #include "ui_selectiondialog.h" #include +#include +#include +#include +#include +#include +#include SelectionDialog::SelectionDialog(const QString& description, QWidget* parent, const QSize& iconSize) @@ -65,9 +71,110 @@ void SelectionDialog::addChoice(const QIcon& icon, const QString& buttonText, m_ValidateByData = true; } +void SelectionDialog::addChoice(const QString& buttonText, + const QString& description, + const QVariant& data, + std::function onDelete) +{ + // Custom row: [QCommandLinkButton (stretch)] [trash QToolButton]. Added + // directly to the same vertical layout the button box sits in, so it + // visually matches the rows produced by the standard addChoice(). + auto* row = new QWidget(ui->scrollAreaWidgetContents); + auto* layout = new QHBoxLayout(row); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(4); + + auto* choice = new QCommandLinkButton(buttonText, description, row); + if (m_IconSize.isValid()) { + choice->setIconSize(m_IconSize); + } + choice->setProperty("data", data); + + auto* trash = new QToolButton(row); + trash->setAutoRaise(true); + QIcon trashIcon = QIcon::fromTheme(QStringLiteral("user-trash")); + if (trashIcon.isNull()) { + trashIcon = QIcon::fromTheme(QStringLiteral("edit-delete")); + } + if (trashIcon.isNull()) { + trashIcon = style()->standardIcon(QStyle::SP_TrashIcon); + } + trash->setIcon(trashIcon); + trash->setToolTip(tr("Delete this backup")); + trash->setAccessibleName(tr("Delete backup '%1'").arg(buttonText)); + + layout->addWidget(choice, /*stretch=*/1); + layout->addWidget(trash, /*stretch=*/0); + + // Insert ABOVE the (empty) buttonBox so visual order matches insertion + // order regardless of how many standard rows have been added. + auto* parentLayout = + qobject_cast(ui->scrollAreaWidgetContents->layout()); + if (parentLayout) { + const int idx = parentLayout->indexOf(ui->buttonBox); + if (idx >= 0) { + parentLayout->insertWidget(idx, row); + } else { + parentLayout->addWidget(row); + } + } + + if (data.isValid()) { + m_ValidateByData = true; + } + + connect(choice, &QAbstractButton::clicked, this, [this, choice]() { + m_Choice = choice; + if (!m_ValidateByData || m_Choice->property("data").isValid()) { + accept(); + } else { + reject(); + } + }); + + connect(trash, &QToolButton::clicked, this, + [this, row, choice, buttonText, onDelete]() { + const auto ans = QMessageBox::question( + this, tr("Delete backup?"), + tr("Delete backup '%1'? This cannot be undone.").arg(buttonText), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (ans != QMessageBox::Yes) { + return; + } + if (!onDelete) { + row->setEnabled(false); + return; + } + if (onDelete()) { + if (m_Choice == choice) { + m_Choice = nullptr; + } + row->setVisible(false); + row->deleteLater(); + } else { + QMessageBox::warning( + this, tr("Delete failed"), + tr("Failed to delete backup '%1'. Check the log for details.") + .arg(buttonText)); + } + }); +} + int SelectionDialog::numChoices() const { - return ui->buttonBox->findChildren(QString()).count(); + // Count standard rows (inside buttonBox) plus delete-enabled rows (whose + // QCommandLinkButton lives in a custom row widget under + // scrollAreaWidgetContents). Visibility check excludes rows the user has + // already deleted via the trash button. + int n = 0; + const auto buttons = + ui->scrollAreaWidgetContents->findChildren(); + for (const auto* b : buttons) { + if (b->isVisibleTo(ui->scrollAreaWidgetContents)) { + ++n; + } + } + return n; } QVariant SelectionDialog::getChoiceData() diff --git a/src/src/selectiondialog.h b/src/src/selectiondialog.h index b04086f..0c01077 100644 --- a/src/src/selectiondialog.h +++ b/src/src/selectiondialog.h @@ -23,6 +23,8 @@ along with Mod Organizer. If not, see . #include #include +#include + namespace Ui { class SelectionDialog; @@ -53,6 +55,16 @@ public: void addChoice(const QIcon& icon, const QString& buttonText, const QString& description, const QVariant& data); + /** + * @brief add a choice with an inline delete button. The trash icon prompts + * for confirmation, then invokes |onDelete|; if the callback returns true, + * the row is removed from the dialog (so the user can keep deleting other + * entries without dismissing the dialog). + */ + void addChoice(const QString& buttonText, const QString& description, + const QVariant& data, + std::function onDelete); + int numChoices() const; QVariant getChoiceData(); -- cgit v1.3.1