aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-05-16 13:46:06 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-05-16 13:46:06 -0500
commitd63c3feb9fc2476c135e22aff821e260faf72b89 (patch)
treeef071fbf191b83716fab6a9e5e316322391c2372
parente403a7a9e1a71dde52fe21e1dd2215c0981a3fbc (diff)
ui: inline trash icon on backup-restore dialog
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.<ts>, loadorder.txt.<ts>, and lockedorder.txt.<ts> triplet via QFile::remove. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
-rw-r--r--src/src/mainwindow.cpp38
-rw-r--r--src/src/selectiondialog.cpp109
-rw-r--r--src/src/selectiondialog.h12
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.<ts>, loadorder.txt.<ts>, lockedorder.txt.<ts>).
+ 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 <http://www.gnu.org/licenses/>.
#include "ui_selectiondialog.h"
#include <QCommandLinkButton>
+#include <QHBoxLayout>
+#include <QIcon>
+#include <QMessageBox>
+#include <QStyle>
+#include <QToolButton>
+#include <QWidget>
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<bool()> 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<QVBoxLayout*>(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<QCommandLinkButton*>(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<QCommandLinkButton*>();
+ 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 <http://www.gnu.org/licenses/>.
#include <QAbstractButton>
#include <QDialog>
+#include <functional>
+
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<bool()> onDelete);
+
int numChoices() const;
QVariant getChoiceData();