diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-16 13:46:06 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-16 13:46:06 -0500 |
| commit | d63c3feb9fc2476c135e22aff821e260faf72b89 (patch) | |
| tree | ef071fbf191b83716fab6a9e5e316322391c2372 | |
| parent | e403a7a9e1a71dde52fe21e1dd2215c0981a3fbc (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.cpp | 38 | ||||
| -rw-r--r-- | src/src/selectiondialog.cpp | 109 | ||||
| -rw-r--r-- | 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.<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();
|
