summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJonathan Feenstra <26406078+JonathanFeenstra@users.noreply.github.com>2026-05-06 19:51:17 +0200
committerGitHub <noreply@github.com>2026-05-06 12:51:17 -0500
commit3070a60ab7e721e1c2766c7bf85373ed0cd9be6a (patch)
treefd029df1bf00c8f31d6cc00497188cfd29784e6d /src
parent6d02ef2b751bd55d2a2718c2b31cec1c0ce55479 (diff)
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
Diffstat (limited to 'src')
-rw-r--r--src/loot.cpp78
-rw-r--r--src/loot.h8
-rw-r--r--src/lootdialog.cpp56
-rw-r--r--src/lootdialog.h5
-rw-r--r--src/lootdialog.ui455
-rw-r--r--src/pluginlistview.cpp25
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::Plugin> 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<details><summary>" +
+ tr("Show") + "</summary>\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 += "</details>\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<QString>& errors() const;
const std::vector<QString>& 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<Plugin> 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 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
- <class>LootDialog</class>
- <widget class="QDialog" name="LootDialog">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>457</width>
- <height>600</height>
- </rect>
- </property>
- <property name="windowTitle">
- <string>LOOT</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <widget class="QWidget" name="widget" native="true">
- <layout class="QVBoxLayout" name="verticalLayout_5" stretch="1,0">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
+ <class>LootDialog</class>
+ <widget class="QDialog" name="LootDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>457</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>LOOT</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
<item>
- <widget class="QWidget" name="widget_2" native="true">
- <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,1">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="QLabel" name="progressText">
- <property name="text">
- <string>Progress</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QProgressBar" name="progressBar">
- <property name="value">
- <number>24</number>
- </property>
- <property name="textVisible">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QFrame" name="frame">
- <property name="frameShape">
- <enum>QFrame::StyledPanel</enum>
- </property>
- <property name="frameShadow">
- <enum>QFrame::Sunken</enum>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout_6">
+ <widget class="QWidget" name="widget" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_5" stretch="1,0">
<property name="leftMargin">
- <number>0</number>
+ <number>0</number>
</property>
<property name="topMargin">
- <number>0</number>
+ <number>0</number>
</property>
<property name="rightMargin">
- <number>0</number>
+ <number>0</number>
</property>
<property name="bottomMargin">
- <number>0</number>
+ <number>0</number>
</property>
<item>
- <widget class="QWebEngineView" name="report">
- <property name="url">
- <url>
- <string>about:blank</string>
- </url>
- </property>
- </widget>
+ <widget class="QWidget" name="widget_2" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,1">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="progressText">
+ <property name="text">
+ <string>Progress</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progressBar">
+ <property name="value">
+ <number>24</number>
+ </property>
+ <property name="textVisible">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QFrame" name="frame">
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWebEngineView" name="report">
+ <property name="url">
+ <url>
+ <string>about:blank</string>
+ </url>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</item>
- </layout>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <widget class="QWidget" name="widget_3" native="true">
- <layout class="QVBoxLayout" name="verticalLayout_4">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="QToolButton" name="details">
- <property name="text">
- <string>Details</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QWidget" name="detailsPanel" native="true">
- <layout class="QVBoxLayout" name="verticalLayout_3">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
<item>
- <widget class="QWidget" name="widget_5" native="true">
- <layout class="QHBoxLayout" name="horizontalLayout">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="QPushButton" name="openJsonReport">
- <property name="text">
- <string>Open JSON report</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </widget>
+ <widget class="QWidget" name="widget_3" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QToolButton" name="details">
+ <property name="text">
+ <string>Details</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="detailsPanel" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWidget" name="widget_5" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="openPluginList">
+ <property name="text">
+ <string>Open sorted plugin list</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="openJsonReport">
+ <property name="text">
+ <string>Open JSON report</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPlainTextEdit" name="output"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</item>
- <item>
- <widget class="QPlainTextEdit" name="output"/>
- </item>
- </layout>
- </widget>
- </item>
- </layout>
- </widget>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttons">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Close</set>
+ </property>
+ </widget>
</item>
- </layout>
- </widget>
- </item>
- <item>
- <widget class="QDialogButtonBox" name="buttons">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="standardButtons">
- <set>QDialogButtonBox::Close</set>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- <customwidgets>
- <customwidget>
- <class>QWebEngineView</class>
- <extends>QWidget</extends>
- <header location="global">QtWebEngineWidgets/QWebEngineView</header>
- </customwidget>
- </customwidgets>
- <resources/>
- <connections>
- <connection>
- <sender>buttons</sender>
- <signal>accepted()</signal>
- <receiver>LootDialog</receiver>
- <slot>accept()</slot>
- <hints>
- <hint type="sourcelabel">
- <x>248</x>
- <y>254</y>
- </hint>
- <hint type="destinationlabel">
- <x>157</x>
- <y>274</y>
- </hint>
- </hints>
- </connection>
- <connection>
- <sender>buttons</sender>
- <signal>rejected()</signal>
- <receiver>LootDialog</receiver>
- <slot>reject()</slot>
- <hints>
- <hint type="sourcelabel">
- <x>316</x>
- <y>260</y>
- </hint>
- <hint type="destinationlabel">
- <x>286</x>
- <y>274</y>
- </hint>
- </hints>
- </connection>
- </connections>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>QWebEngineView</class>
+ <extends>QWidget</extends>
+ <header location="global">QtWebEngineWidgets/QWebEngineView</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttons</sender>
+ <signal>accepted()</signal>
+ <receiver>LootDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttons</sender>
+ <signal>rejected()</signal>
+ <receiver>LootDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
</ui>
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();