From 3070a60ab7e721e1c2766c7bf85373ed0cd9be6a Mon Sep 17 00:00:00 2001 From: Jonathan Feenstra <26406078+JonathanFeenstra@users.noreply.github.com> Date: Wed, 6 May 2026 19:51:17 +0200 Subject: Don't apply LOOT-sorted load order until user clicks "Apply" (#2382) * Add sorted plugin list to LOOT dialog (markdown and button) * Prefix plugins with checkboxes to show whether they're enabled --- src/loot.cpp | 78 +++++++-- src/loot.h | 8 +- src/lootdialog.cpp | 56 +++++- src/lootdialog.h | 5 + src/lootdialog.ui | 455 +++++++++++++++++++++++++------------------------ src/pluginlistview.cpp | 25 +-- 6 files changed, 372 insertions(+), 255 deletions(-) diff --git a/src/loot.cpp b/src/loot.cpp index 2c52aaa1..a888b577 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -9,8 +9,9 @@ using namespace MOBase; using namespace json; -static QString LootReportPath = QDir::temp().absoluteFilePath("lootreport.json"); -static const DWORD PipeTimeout = 500; +static QString LootReportPath = QDir::temp().absoluteFilePath("lootreport.json"); +static QString SortedPluginListPath = QDir::temp().absoluteFilePath("loadorder.txt"); +static const DWORD PipeTimeout = 500; class AsyncPipe { @@ -423,11 +424,13 @@ Loot::~Loot() } deleteReportFile(); + deleteSortedLoadOrder(); } bool Loot::start(QWidget* parent, bool didUpdateMasterList) { deleteReportFile(); + deleteSortedLoadOrder(); log::debug("starting loot"); @@ -476,6 +479,8 @@ bool Loot::spawnLootcli(QWidget* parent, bool didUpdateMasterList, << "--out" << QString("\"%1\"").arg(LootReportPath) + << "--pluginListOutputPath" << QString("\"%1\"").arg(SortedPluginListPath) + << "--language" << m_core.settings().interface().language(); if (didUpdateMasterList) { @@ -514,11 +519,16 @@ bool Loot::result() const return m_result; } -const QString& Loot::outPath() const +const QString& Loot::reportPath() const { return LootReportPath; } +const QString& Loot::sortedPluginListPath() const +{ + return SortedPluginListPath; +} + const Loot::Report& Loot::report() const { return m_report; @@ -676,7 +686,7 @@ Loot::Report Loot::createReport() const r.warnings = m_warnings; if (m_result) { - processOutputFile(r); + processReport(r); } return r; @@ -695,21 +705,33 @@ void Loot::deleteReportFile() } } -void Loot::processOutputFile(Report& r) const +void Loot::deleteSortedLoadOrder() +{ + if (QFile::exists(SortedPluginListPath)) { + log::debug("deleting temporary sorted plugin list '{}'", SortedPluginListPath); + const auto r = shell::Delete(QFileInfo(SortedPluginListPath)); + if (!r) { + log::error("failed to remove temporary sorted plugin list '{}': {}", + SortedPluginListPath, r.toString()); + } + } +} + +void Loot::processReport(Report& r) const { log::debug("parsing json output file at '{}'", LootReportPath); - QFile outFile(LootReportPath); - if (!outFile.open(QIODevice::ReadOnly)) { + QFile reportFile(LootReportPath); + if (!reportFile.open(QIODevice::ReadOnly)) { emit log(MOBase::log::Error, QString("failed to open file, %1 (error %2)") - .arg(outFile.errorString()) - .arg(outFile.error())); + .arg(reportFile.errorString()) + .arg(reportFile.error())); return; } QJsonParseError e; - const QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll(), &e); + const QJsonDocument doc = QJsonDocument::fromJson(reportFile.readAll(), &e); if (doc.isNull()) { emit log(MOBase::log::Error, QString("invalid json, %1 (error %2)").arg(e.errorString()).arg(e.error)); @@ -745,6 +767,42 @@ std::vector Loot::reportPlugins(const QJsonArray& plugins) const return v; } +const QString Loot::getSortedPluginListMarkdown() const +{ + if (!m_result) { + return {}; + } + + log::debug("parsing sorted plugin list at '{}'", SortedPluginListPath); + + QFile pluginListFile(SortedPluginListPath); + if (!pluginListFile.open(QIODevice::ReadOnly)) { + emit log(MOBase::log::Error, QString("failed to open file, %1 (error %2)") + .arg(pluginListFile.errorString()) + .arg(pluginListFile.error())); + return {}; + } + + // skip "# This file was automatically generated by Mod Organizer" + pluginListFile.readLine(); + + QString markdown = "### " + tr("Sorted plugins") + "\n
" + + tr("Show") + "\n\n"; + + const auto pluginList = m_core.pluginList(); + while (!pluginListFile.atEnd()) { + const QString pluginName = QString::fromUtf8(pluginListFile.readLine().trimmed()); + const bool active = pluginList->isEnabled(pluginName); + const QString prefix = active ? " - [x] " : " - [ ] "; + markdown += prefix + pluginName + "\n"; + } + + markdown += "
\n"; + + pluginListFile.close(); + return markdown; +} + Loot::Plugin Loot::reportPlugin(const QJsonObject& plugin) const { Plugin p; diff --git a/src/loot.h b/src/loot.h index 5442a722..8f88c045 100644 --- a/src/loot.h +++ b/src/loot.h @@ -91,8 +91,10 @@ public: void cancel(); bool result() const; - const QString& outPath() const; + const QString& reportPath() const; + const QString& sortedPluginListPath() const; const Report& report() const; + const QString getSortedPluginListMarkdown() const; const std::vector& errors() const; const std::vector& warnings() const; @@ -123,9 +125,11 @@ private: void processMessage(const lootcli::Message& m); Report createReport() const; - void processOutputFile(Report& r) const; + void processReport(Report& r) const; void deleteReportFile(); + void deleteSortedLoadOrder(); + Message reportMessage(const QJsonObject& message) const; std::vector reportPlugins(const QJsonArray& plugins) const; Loot::Plugin reportPlugin(const QJsonObject& plugin) const; diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp index 6e31b8a6..e1e88843 100644 --- a/src/lootdialog.cpp +++ b/src/lootdialog.cpp @@ -146,9 +146,15 @@ void LootDialog::cancel() } } +void LootDialog::openSortedPluginList() +{ + const auto& path = m_loot.sortedPluginListPath(); + shell::Open(path); +} + void LootDialog::openReport() { - const auto path = m_loot.outPath(); + const auto& path = m_loot.reportPath(); shell::Open(path); } @@ -209,7 +215,11 @@ void LootDialog::createUI() } m_expander.set(ui->details, ui->detailsPanel); + ui->openPluginList->setEnabled(false); ui->openJsonReport->setEnabled(false); + connect(ui->openPluginList, &QPushButton::clicked, [&] { + openSortedPluginList(); + }); connect(ui->openJsonReport, &QPushButton::clicked, [&] { openReport(); }); @@ -253,8 +263,9 @@ void LootDialog::onFinished() showReport(); + ui->openPluginList->setEnabled(true); ui->openJsonReport->setEnabled(true); - ui->buttons->setStandardButtons(QDialogButtonBox::Close); + ui->buttons->setStandardButtons(QDialogButtonBox::Apply | QDialogButtonBox::Cancel); // if loot failed, the Done progress won't be received; this makes sure // the progress bar is stopped @@ -284,5 +295,44 @@ void LootDialog::showReport() } } - m_report.setText(lootReport.toMarkdown()); + const auto markdown = + m_loot.getSortedPluginListMarkdown() + "\n" + lootReport.toMarkdown(); + + m_report.setText(markdown); +} + +void LootDialog::applySortedLoadOrder() +{ + const auto& sortedPluginListPath = m_loot.sortedPluginListPath(); + if (!QFile::exists(sortedPluginListPath)) { + log::error("sorted plugin list '{}' does not exist, cannot apply sorted load order", + sortedPluginListPath); + return; + } + + const auto& pluginListPath = m_core.profilePath() + "/loadorder.txt"; + log::debug("moving sorted load order from '{}' to '{}'", sortedPluginListPath, + pluginListPath); + + const auto r = shellMove(sortedPluginListPath, pluginListPath, true, this); + if (!r) { + const auto e = GetLastError(); + log::error("failed to move sorted plugin list from '{}' to '{}': {}", + sortedPluginListPath, pluginListPath, formatSystemMessage(e)); + } +} + +void LootDialog::on_buttons_clicked(QAbstractButton* b) +{ + const auto role = ui->buttons->buttonRole(b); + switch (role) { + case QDialogButtonBox::ApplyRole: + ui->buttons->setEnabled(false); + applySortedLoadOrder(); + close(); + break; + case QDialogButtonBox::RejectRole: + reject(); + break; + } } diff --git a/src/lootdialog.h b/src/lootdialog.h index f632b9c1..b107e5be 100644 --- a/src/lootdialog.h +++ b/src/lootdialog.h @@ -54,6 +54,7 @@ public: void addOutput(const QString& s); bool result() const; void cancel(); + void openSortedPluginList(); void openReport(); int exec() override; @@ -75,6 +76,10 @@ private: void onFinished(); void log(MOBase::log::Levels lv, const QString& s); void showReport(); + void applySortedLoadOrder(); + +private slots: + void on_buttons_clicked(QAbstractButton* b); }; #endif // MODORGANIZER_LOOTDIALOG_H diff --git a/src/lootdialog.ui b/src/lootdialog.ui index e366ce41..2f5f11c3 100644 --- a/src/lootdialog.ui +++ b/src/lootdialog.ui @@ -1,241 +1,248 @@ - LootDialog - - - - 0 - 0 - 457 - 600 - - - - LOOT - - - - - - - 0 - - - 0 - - - 0 - - - 0 - + LootDialog + + + + 0 + 0 + 457 + 600 + + + + LOOT + + - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Progress - - - - - - - 24 - - - false - - - - - - - QFrame::StyledPanel - - - QFrame::Sunken - - + + - 0 + 0 - 0 + 0 - 0 + 0 - 0 + 0 - - - - about:blank - - - + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Progress + + + + + + + 24 + + + false + + + + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + about:blank + + + + + + + + + - - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Details - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Open JSON report - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Details + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Open sorted plugin list + + + + + + + Open JSON report + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Close - - - - - - - - QWebEngineView - QWidget -
QtWebEngineWidgets/QWebEngineView
-
-
- - - - buttons - accepted() - LootDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttons - rejected() - LootDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - + + + + + QWebEngineView + QWidget +
QtWebEngineWidgets/QWebEngineView
+
+
+ + + + buttons + accepted() + LootDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttons + rejected() + LootDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + +
diff --git a/src/pluginlistview.cpp b/src/pluginlistview.cpp index f8990a94..c7dc1507 100644 --- a/src/pluginlistview.cpp +++ b/src/pluginlistview.cpp @@ -181,23 +181,16 @@ void PluginListView::onSortButtonClicked() { const bool offline = m_core->settings().network().offlineMode(); - auto r = QMessageBox::No; - if (offline) { - r = QMessageBox::question(topLevelWidget(), tr("Sorting plugins"), - tr("Are you sure you want to sort your plugins list?") + - "\r\n\r\n" + - tr("Note: You are currently in offline mode and LOOT " - "will not update the master list."), - QMessageBox::Yes | QMessageBox::No); - } else { - r = QMessageBox::question(topLevelWidget(), tr("Sorting plugins"), - tr("Are you sure you want to sort your plugins list?"), - QMessageBox::Yes | QMessageBox::No); - } - - if (r != QMessageBox::Yes) { - return; + const auto r = QMessageBox::question( + topLevelWidget(), tr("Sorting plugins"), + tr("Are you sure you want to sort your plugins list?") + "\r\n\r\n" + + tr("Note: You are currently in offline mode and LOOT " + "will not update the master list."), + QMessageBox::Yes | QMessageBox::No); + if (r != QMessageBox::Yes) { + return; + } } m_core->savePluginList(); -- cgit v1.3.1