aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_bsplugins/src/BSPluginInfo
diff options
context:
space:
mode:
Diffstat (limited to 'libs/installer_bsplugins/src/BSPluginInfo')
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/AuxConflictModel.cpp153
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/AuxConflictModel.h101
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/AuxTreeModel.cpp83
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/AuxTreeModel.h37
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/PluginInfoDialog.cpp235
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/PluginInfoDialog.h69
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/PluginRecordModel.cpp352
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/PluginRecordModel.h60
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/PluginRecordView.cpp283
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/PluginRecordView.h78
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/RecordFilterProxyModel.cpp138
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/RecordFilterProxyModel.h53
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/RecordStructureModel.cpp167
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/RecordStructureModel.h51
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/plugininfodialog.ui397
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/pluginrecordview.ui149
16 files changed, 2406 insertions, 0 deletions
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/AuxConflictModel.cpp b/libs/installer_bsplugins/src/BSPluginInfo/AuxConflictModel.cpp
new file mode 100644
index 0000000..bd93000
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/AuxConflictModel.cpp
@@ -0,0 +1,153 @@
+#include "AuxConflictModel.h"
+
+#include <utility>
+
+namespace BSPluginInfo
+{
+
+AuxConflictModel::AuxConflictModel(const TESData::PluginList* pluginList,
+ QObject* parent)
+ : QAbstractItemModel(parent), m_PluginList{pluginList}
+{}
+
+void AuxConflictModel::appendItem(const QString& name,
+ QList<TESData::TESFileHandle>&& handles)
+{
+ m_Items.append({name, std::move(handles)});
+}
+
+QModelIndex AuxConflictModel::index(int row, int column,
+ const QModelIndex& parent) const
+{
+ if (parent.isValid()) {
+ return QModelIndex();
+ }
+
+ return createIndex(row, column, row);
+}
+
+QModelIndex AuxConflictModel::parent([[maybe_unused]] const QModelIndex& index) const
+{
+ return QModelIndex();
+}
+
+int AuxConflictModel::rowCount(const QModelIndex& parent) const
+{
+ return !parent.isValid() ? m_Items.length() : 0;
+}
+
+int AuxWinnerModel::columnCount([[maybe_unused]] const QModelIndex& parent) const
+{
+ return COL_COUNT;
+}
+
+int AuxLoserModel::columnCount([[maybe_unused]] const QModelIndex& parent) const
+{
+ return COL_COUNT;
+}
+
+int AuxNonConflictModel::columnCount([[maybe_unused]] const QModelIndex& parent) const
+{
+ return COL_COUNT;
+}
+
+QVariant AuxWinnerModel::data(const QModelIndex& index, int role) const
+{
+ if (role == Qt::DisplayRole) {
+ switch (index.column()) {
+ case COL_NAME:
+ return m_Items[index.row()].name;
+ case COL_LOSERS: {
+ QStringList names;
+ const auto& handles = m_Items[index.row()].sortedHandles;
+ for (int i = 0, count = handles.length() - 1; i < count; ++i) {
+ const auto handle = handles[i];
+ const auto entry = m_PluginList->findEntryByHandle(handle);
+ if (entry) {
+ names.append(QString::fromStdString(entry->name()));
+ }
+ }
+ return names.join(QStringLiteral(", "));
+ }
+ }
+ }
+ return QVariant();
+}
+
+QVariant AuxLoserModel::data(const QModelIndex& index, int role) const
+{
+ if (role == Qt::DisplayRole) {
+ switch (index.column()) {
+ case COL_NAME:
+ return m_Items[index.row()].name;
+ case COL_WINNER: {
+ const auto& handles = m_Items[index.row()].sortedHandles;
+ const auto entry = m_PluginList->findEntryByHandle(handles.last());
+ return entry ? QString::fromStdString(entry->name()) : QString();
+ }
+ }
+ }
+ return QVariant();
+}
+
+QVariant AuxNonConflictModel::data(const QModelIndex& index, int role) const
+{
+ if (role == Qt::DisplayRole) {
+ switch (index.column()) {
+ case COL_NAME:
+ return m_Items[index.row()].name;
+ }
+ }
+ return QVariant();
+}
+
+QVariant AuxWinnerModel::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ if (orientation != Qt::Horizontal || role != Qt::DisplayRole) {
+ return QVariant();
+ }
+
+ switch (section) {
+ case COL_NAME:
+ return tr("File");
+ case COL_LOSERS:
+ return tr("Overwritten Mods");
+ default:
+ return QVariant();
+ }
+}
+
+QVariant AuxLoserModel::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ if (orientation != Qt::Horizontal || role != Qt::DisplayRole) {
+ return QVariant();
+ }
+
+ switch (section) {
+ case COL_NAME:
+ return tr("File");
+ case COL_WINNER:
+ return tr("Providing Mod");
+ default:
+ return QVariant();
+ }
+}
+
+QVariant AuxNonConflictModel::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ if (orientation != Qt::Horizontal || role != Qt::DisplayRole) {
+ return QVariant();
+ }
+
+ switch (section) {
+ case COL_NAME:
+ return tr("File");
+ default:
+ return QVariant();
+ }
+}
+
+} // namespace BSPluginInfo
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/AuxConflictModel.h b/libs/installer_bsplugins/src/BSPluginInfo/AuxConflictModel.h
new file mode 100644
index 0000000..328a39e
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/AuxConflictModel.h
@@ -0,0 +1,101 @@
+#ifndef BSPLUGININFO_AUXCONFLICTMODEL_H
+#define BSPLUGININFO_AUXCONFLICTMODEL_H
+
+#include "TESData/PluginList.h"
+
+#include <QAbstractItemModel>
+
+namespace BSPluginInfo
+{
+
+class AuxConflictModel : public QAbstractItemModel
+{
+ Q_OBJECT
+
+public:
+ struct Item
+ {
+ QString name;
+ QList<TESData::TESFileHandle> sortedHandles;
+ };
+
+ explicit AuxConflictModel(const TESData::PluginList* pluginList,
+ QObject* parent = nullptr);
+
+ void appendItem(const QString& name, QList<TESData::TESFileHandle>&& handles);
+
+ QModelIndex index(int row, int column,
+ const QModelIndex& parent = QModelIndex()) const override;
+ QModelIndex parent(const QModelIndex& index) const override;
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+
+protected:
+ const TESData::PluginList* m_PluginList;
+ QList<Item> m_Items;
+};
+
+class AuxWinnerModel final : public AuxConflictModel
+{
+ Q_OBJECT
+
+public:
+ enum EColumn
+ {
+ COL_NAME,
+ COL_LOSERS,
+
+ COL_COUNT
+ };
+
+ using AuxConflictModel::AuxConflictModel;
+
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role = Qt::DisplayRole) const override;
+};
+
+class AuxLoserModel final : public AuxConflictModel
+{
+ Q_OBJECT
+
+public:
+ enum EColumn
+ {
+ COL_NAME,
+ COL_WINNER,
+
+ COL_COUNT
+ };
+
+ using AuxConflictModel::AuxConflictModel;
+
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role = Qt::DisplayRole) const override;
+};
+
+class AuxNonConflictModel final : public AuxConflictModel
+{
+ Q_OBJECT
+
+public:
+ enum EColumn
+ {
+ COL_NAME,
+
+ COL_COUNT
+ };
+
+ using AuxConflictModel::AuxConflictModel;
+
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role = Qt::DisplayRole) const override;
+};
+
+} // namespace BSPluginInfo
+
+#endif // BSPLUGININFO_AUXCONFLICTMODEL_H
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/AuxTreeModel.cpp b/libs/installer_bsplugins/src/BSPluginInfo/AuxTreeModel.cpp
new file mode 100644
index 0000000..b5df2ab
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/AuxTreeModel.cpp
@@ -0,0 +1,83 @@
+#include "AuxTreeModel.h"
+
+namespace BSPluginInfo
+{
+
+AuxTreeModel::AuxTreeModel(const TESData::AssociatedEntry* entry, QObject* parent)
+ : QAbstractItemModel(parent), m_Entry{entry}
+{}
+
+QModelIndex AuxTreeModel::index(int row, int column, const QModelIndex& parent) const
+{
+ if (row < 0 || column != 0) {
+ return QModelIndex();
+ }
+
+ const auto parentItem = parent.isValid()
+ ? static_cast<const Item*>(parent.internalPointer())
+ : m_Entry->root().get();
+
+ const auto item = parentItem->getByIndex(row).get();
+ return item ? createIndex(row, column, item) : QModelIndex();
+}
+
+QModelIndex AuxTreeModel::parent(const QModelIndex& index) const
+{
+ if (!index.isValid())
+ return QModelIndex();
+
+ const auto item = static_cast<const Item*>(index.internalPointer());
+ const auto parent = item ? item->parent() : nullptr;
+ if (!parent || !parent->parent()) {
+ return QModelIndex();
+ }
+
+ const int row = parent->parent()->indexOf(parent);
+
+ return createIndex(row, 0, parent);
+}
+
+int AuxTreeModel::rowCount(const QModelIndex& parent) const
+{
+ const auto parentItem = parent.isValid()
+ ? static_cast<const Item*>(parent.internalPointer())
+ : m_Entry->root().get();
+
+ return parentItem ? parentItem->numChildren() : 0;
+}
+
+int AuxTreeModel::columnCount([[maybe_unused]] const QModelIndex& parent) const
+{
+ return 1;
+}
+
+QVariant AuxTreeModel::data(const QModelIndex& index, int role) const
+{
+ if (!index.isValid()) {
+ return QVariant();
+ }
+
+ const auto item = static_cast<const Item*>(index.internalPointer());
+ switch (role) {
+ case Qt::DisplayRole:
+ return QString::fromStdString(item->name());
+ }
+
+ return QVariant();
+}
+
+QVariant AuxTreeModel::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ if (orientation != Qt::Horizontal || role != Qt::DisplayRole) {
+ return QVariant();
+ }
+
+ if (section == 0) {
+ return tr("File Name");
+ } else {
+ return QVariant();
+ }
+}
+
+} // namespace BSPluginInfo
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/AuxTreeModel.h b/libs/installer_bsplugins/src/BSPluginInfo/AuxTreeModel.h
new file mode 100644
index 0000000..6663aa8
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/AuxTreeModel.h
@@ -0,0 +1,37 @@
+#ifndef BSPLUGININFO_AUXTREEMODEL_H
+#define BSPLUGININFO_AUXTREEMODEL_H
+
+#include "TESData/AssociatedEntry.h"
+
+#include <QAbstractItemModel>
+
+namespace BSPluginInfo
+{
+
+class AuxTreeModel final : public QAbstractItemModel
+{
+ Q_OBJECT
+
+public:
+ explicit AuxTreeModel(const TESData::AssociatedEntry* entry,
+ QObject* parent = nullptr);
+
+ QModelIndex index(int row, int column,
+ const QModelIndex& parent = QModelIndex()) const override;
+ QModelIndex parent(const QModelIndex& index) const override;
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role = Qt::DisplayRole) const override;
+
+private:
+ using Item = TESData::AuxItem;
+
+ const TESData::AssociatedEntry* m_Entry;
+};
+
+} // namespace BSPluginInfo
+
+#endif // BSPLUGININFO_AUXTREEMODEL_H
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/PluginInfoDialog.cpp b/libs/installer_bsplugins/src/BSPluginInfo/PluginInfoDialog.cpp
new file mode 100644
index 0000000..e011629
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/PluginInfoDialog.cpp
@@ -0,0 +1,235 @@
+#include "PluginInfoDialog.h"
+#include "AuxConflictModel.h"
+#include "GUI/IGeometrySettings.h"
+#include "MOPlugin/Settings.h"
+#include "PluginRecordView.h"
+#include "ui_plugininfodialog.h"
+
+#include <QTreeView>
+#include <QVBoxLayout>
+
+namespace BSPluginInfo
+{
+
+PluginInfoDialog::PluginInfoDialog(MOBase::IOrganizer* organizer,
+ TESData::PluginList* pluginList,
+ const QString& pluginName, QWidget* parent)
+ : QDialog(parent), ui{new Ui::PluginInfoDialog()}, m_PluginList{pluginList},
+ m_PluginName{pluginName}
+{
+ ui->setupUi(this);
+ setWindowTitle(pluginName);
+
+ ui->pluginRecordView->setup(organizer, pluginList, pluginName);
+
+ const auto winnerModel = new AuxWinnerModel(pluginList, ui->winningTree);
+ const auto loserModel = new AuxLoserModel(pluginList, ui->losingTree);
+ const auto nonConflictModel = new AuxNonConflictModel(pluginList, ui->noConflictTree);
+
+ const auto entry = pluginList->findEntryByName(pluginName.toStdString());
+ const auto handle = entry ? entry->handle() : -1;
+
+ if (const auto plugin = pluginList->getPluginByName(pluginName)) {
+ for (const auto& archive : plugin->archives()) {
+ const auto archiveEntry = m_PluginList->findArchive(archive);
+ if (archiveEntry) {
+ m_Archives.append(archive);
+
+ int winningCount = 0;
+ int losingCount = 0;
+ int noConflictCount = 0;
+
+ archiveEntry->forEachMember([&](auto&& member) {
+ QList<TESData::TESFileHandle> handles;
+ std::ranges::copy(member->alternatives, std::back_inserter(handles));
+ std::ranges::sort(handles, std::less<int>(), [&](auto altHandle) {
+ const auto altEntry = pluginList->findEntryByHandle(altHandle);
+ const auto info = altEntry ? pluginList->getPluginByName(
+ QString::fromStdString(altEntry->name()))
+ : nullptr;
+ return info ? info->priority() : -1;
+ });
+
+ if (handles.length() <= 1) {
+ ++noConflictCount;
+ nonConflictModel->appendItem(QString::fromStdString(member->path),
+ std::move(handles));
+ } else if (handles.last() == handle) {
+ ++winningCount;
+ winnerModel->appendItem(QString::fromStdString(member->path),
+ std::move(handles));
+ } else {
+ ++losingCount;
+ loserModel->appendItem(QString::fromStdString(member->path),
+ std::move(handles));
+ }
+ });
+
+ ui->winningCount->display(winningCount);
+ ui->losingCount->display(losingCount);
+ ui->noConflictCount->display(noConflictCount);
+
+ const auto dataTree = new QTreeView(ui->archivesTreeStack);
+
+ const auto model = new AuxTreeModel(archiveEntry, dataTree);
+ const auto proxy = new QSortFilterProxyModel(dataTree);
+ proxy->setRecursiveFilteringEnabled(true);
+ proxy->setSourceModel(model);
+
+ dataTree->setModel(proxy);
+
+ ui->archivesTreeStack->addWidget(dataTree);
+ }
+ }
+ }
+
+ ui->winningTree->setModel(winnerModel);
+ ui->losingTree->setModel(loserModel);
+ ui->noConflictTree->setModel(nonConflictModel);
+
+ m_WinningExpander.set(ui->winningExpander, ui->winningTree, true);
+ m_LosingExpander.set(ui->losingExpander, ui->losingTree, true);
+ m_NoConflictExpander.set(ui->noConflictExpander, ui->noConflictTree, true);
+
+ m_FilterWinning.setEdit(ui->winningLineEdit);
+ m_FilterWinning.setList(ui->winningTree);
+ m_FilterWinning.setUseSourceSort(true);
+
+ m_FilterLosing.setEdit(ui->losingLineEdit);
+ m_FilterLosing.setList(ui->losingTree);
+ m_FilterLosing.setUseSourceSort(true);
+
+ m_FilterNoConflicts.setEdit(ui->noConflictLineEdit);
+ m_FilterNoConflicts.setList(ui->noConflictTree);
+ m_FilterNoConflicts.setUseSourceSort(true);
+
+ const auto p = palette();
+ if (!ui->pluginRecordView->hasData()) {
+ const auto index = ui->tabWidget->indexOf(ui->tabRecords);
+ ui->tabWidget->tabBar()->setTabTextColor(
+ index, p.color(QPalette::Disabled, QPalette::WindowText));
+ }
+ if (m_Archives.isEmpty()) {
+ const auto index = ui->tabWidget->indexOf(ui->tabArchives);
+ ui->tabWidget->tabBar()->setTabTextColor(
+ index, p.color(QPalette::Disabled, QPalette::WindowText));
+ }
+}
+
+PluginInfoDialog::~PluginInfoDialog() noexcept
+{
+ delete ui;
+}
+
+int PluginInfoDialog::exec()
+{
+ const auto settings = Settings::instance();
+ GUI::GeometrySaver gs{*settings, this};
+
+ GUI::StateSaver ssRecordSplitter{*settings, ui->pluginRecordView->splitter()};
+ GUI::StateSaver ssRecordPickHeader{*settings,
+ ui->pluginRecordView->pickRecordView()->header()};
+
+ GUI::StateSaver ssArchiveWinningExpander{*settings, &m_WinningExpander};
+ GUI::StateSaver ssArchiveLosingExpander{*settings, &m_LosingExpander};
+ GUI::StateSaver ssArchiveNoConflictExpander{*settings, &m_NoConflictExpander};
+ GUI::StateSaver ssArchiveWinningHeader{*settings, ui->winningTree->header()};
+ GUI::StateSaver ssArchiveLosingHeader{*settings, ui->losingTree->header()};
+ GUI::StateSaver ssArchiveNoConflictHeader{*settings, ui->noConflictTree->header()};
+
+ const int r = QDialog::exec();
+
+ return r;
+}
+
+void PluginInfoDialog::on_close_clicked()
+{
+ close();
+}
+
+void PluginInfoDialog::on_nextFile_clicked()
+{
+ const auto current = m_PluginList->getPluginByName(m_PluginName);
+ int nextPriority = current ? current->priority() + 1 : 0;
+ if (nextPriority >= m_PluginList->pluginCount()) {
+ nextPriority = 0;
+ }
+
+ const auto next = m_PluginList->getPluginByPriority(nextPriority);
+ if (next) {
+ setCurrent(next->name());
+ }
+}
+
+void PluginInfoDialog::on_previousFile_clicked()
+{
+ const auto current = m_PluginList->getPluginByName(m_PluginName);
+ int previousPriority = current ? current->priority() - 1 : 0;
+ if (previousPriority < 0) {
+ previousPriority = m_PluginList->pluginCount() - 1;
+ }
+
+ const auto previous = m_PluginList->getPluginByPriority(previousPriority);
+ if (previous) {
+ setCurrent(previous->name());
+ }
+}
+
+void PluginInfoDialog::on_previousArchiveButton_clicked()
+{
+ ui->archiveFilterEdit->clear();
+
+ const int currentIndex = ui->archivesTreeStack->currentIndex();
+ const int count = ui->archivesTreeStack->count();
+
+ if (currentIndex == 0) {
+ ui->archivesTreeStack->setCurrentIndex(count - 1);
+ } else {
+ ui->archivesTreeStack->setCurrentIndex(currentIndex - 1);
+ }
+}
+
+void PluginInfoDialog::on_nextArchiveButton_clicked()
+{
+ ui->archiveFilterEdit->clear();
+
+ const int currentIndex = ui->archivesTreeStack->currentIndex();
+ const int count = ui->archivesTreeStack->count();
+
+ if (currentIndex == count - 1) {
+ ui->archivesTreeStack->setCurrentIndex(0);
+ } else {
+ ui->archivesTreeStack->setCurrentIndex(currentIndex + 1);
+ }
+}
+
+void PluginInfoDialog::on_archivesTreeStack_currentChanged(int index)
+{
+ ui->currentArchiveLabel->setText(m_Archives[index]);
+}
+
+void PluginInfoDialog::on_archiveFilterEdit_textChanged(const QString& text)
+{
+ const auto widget = ui->archivesTreeStack->currentWidget();
+ const auto view = qobject_cast<QAbstractItemView*>(widget);
+ if (!view) {
+ return;
+ }
+
+ const auto model = view->model();
+ const auto filterProxy = qobject_cast<QSortFilterProxyModel*>(model);
+ if (!filterProxy) {
+ return;
+ }
+
+ filterProxy->setFilterFixedString(text);
+}
+
+void PluginInfoDialog::setCurrent(const QString& pluginName)
+{
+ m_PluginName = pluginName;
+ setWindowTitle(pluginName);
+ ui->pluginRecordView->setFile(pluginName);
+}
+
+} // namespace BSPluginInfo
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/PluginInfoDialog.h b/libs/installer_bsplugins/src/BSPluginInfo/PluginInfoDialog.h
new file mode 100644
index 0000000..db2fe4a
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/PluginInfoDialog.h
@@ -0,0 +1,69 @@
+#ifndef BSPLUGININFO_PLUGININFODIALOG_H
+#define BSPLUGININFO_PLUGININFODIALOG_H
+
+#include "AuxTreeModel.h"
+#include "TESData/PluginList.h"
+
+#include <expanderwidget.h>
+#include <filterwidget.h>
+#include <imoinfo.h>
+
+#include <QDialog>
+#include <QString>
+
+namespace Ui
+{
+class PluginInfoDialog;
+}
+
+namespace BSPluginInfo
+{
+
+class PluginInfoDialog final : public QDialog
+{
+ Q_OBJECT
+
+public:
+ PluginInfoDialog(MOBase::IOrganizer* organizer, TESData::PluginList* pluginList,
+ const QString& pluginName, QWidget* parent = nullptr);
+
+ PluginInfoDialog(const PluginInfoDialog&) = delete;
+ PluginInfoDialog(PluginInfoDialog&&) = delete;
+
+ ~PluginInfoDialog() noexcept;
+
+ PluginInfoDialog& operator=(const PluginInfoDialog&) = delete;
+ PluginInfoDialog& operator=(PluginInfoDialog&&) = delete;
+
+ int exec() override;
+
+private slots:
+ void on_close_clicked();
+ void on_nextFile_clicked();
+ void on_previousFile_clicked();
+
+ void on_previousArchiveButton_clicked();
+ void on_nextArchiveButton_clicked();
+ void on_archivesTreeStack_currentChanged(int index);
+ void on_archiveFilterEdit_textChanged(const QString& text);
+
+private:
+ void setCurrent(const QString& pluginName);
+
+ Ui::PluginInfoDialog* ui;
+
+ TESData::PluginList* m_PluginList;
+ QString m_PluginName;
+
+ QStringList m_Archives;
+ MOBase::ExpanderWidget m_WinningExpander;
+ MOBase::ExpanderWidget m_LosingExpander;
+ MOBase::ExpanderWidget m_NoConflictExpander;
+ MOBase::FilterWidget m_FilterWinning;
+ MOBase::FilterWidget m_FilterLosing;
+ MOBase::FilterWidget m_FilterNoConflicts;
+};
+
+} // namespace BSPluginInfo
+
+#endif // BSPLUGININFO_PLUGININFODIALOG_H
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/PluginRecordModel.cpp b/libs/installer_bsplugins/src/BSPluginInfo/PluginRecordModel.cpp
new file mode 100644
index 0000000..9bc554e
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/PluginRecordModel.cpp
@@ -0,0 +1,352 @@
+#include "PluginRecordModel.h"
+#include "TESData/BranchConflictParser.h"
+#include "TESData/TypeStringNames.h"
+#include "TESFile/Reader.h"
+
+#include <log.h>
+
+#include <algorithm>
+#include <iterator>
+#include <ranges>
+
+using namespace Qt::Literals::StringLiterals;
+
+namespace BSPluginInfo
+{
+
+PluginRecordModel::PluginRecordModel(MOBase::IOrganizer* organizer,
+ TESData::PluginList* pluginList,
+ const QString& pluginName)
+ : m_PluginName{pluginName}, m_Organizer{organizer}, m_PluginList{pluginList},
+ m_FileEntry{pluginList->findEntryByName(pluginName.toStdString())}
+{
+ if (m_FileEntry) {
+ m_DataRoot = m_FileEntry->dataRoot();
+ }
+}
+
+TESData::RecordPath PluginRecordModel::getPath(const QModelIndex& index) const
+{
+ TESData::RecordPath path;
+
+ std::vector<const Item*> parents;
+ const auto last = static_cast<const Item*>(index.internalPointer());
+ for (auto item = last; item->parent; item = item->parent) {
+ parents.push_back(item);
+ }
+
+ for (const auto& item : std::ranges::reverse_view(parents)) {
+ if (item->group.has_value()) {
+ path.push(item->group.value(), m_FileEntry->files(), m_PluginName.toStdString());
+ }
+ }
+
+ if (last->record) {
+ if (last->group.has_value()) {
+ path.pop();
+ }
+ path.setIdentifier(last->record->identifier(), {&last->record->file(), 1});
+ }
+
+ return path;
+}
+
+QModelIndex PluginRecordModel::index(int row, int column,
+ const QModelIndex& parent) const
+{
+ if (row < 0 || column < 0)
+ return QModelIndex();
+
+ const auto parentItem = parent.isValid()
+ ? static_cast<const Item*>(parent.internalPointer())
+ : m_DataRoot;
+
+ if (!parentItem || row >= parentItem->children.size())
+ return QModelIndex();
+
+ const auto item = parentItem->children.nth(row)->second.get();
+ return createIndex(row, column, item);
+}
+
+QModelIndex PluginRecordModel::parent(const QModelIndex& index) const
+{
+ if (!index.isValid())
+ return QModelIndex();
+
+ const auto item = static_cast<const Item*>(index.internalPointer());
+ if (!item->parent || !item->parent->parent) {
+ return QModelIndex();
+ }
+
+ const auto& siblings = item->parent->parent->children;
+ const int row = static_cast<int>(
+ siblings.index_of(std::ranges::find(siblings, item->parent, [&](auto&& pair) {
+ return pair.second.get();
+ })));
+
+ return createIndex(row, 0, item->parent);
+}
+
+bool PluginRecordModel::hasChildren(const QModelIndex& parent) const
+{
+ if (!parent.isValid()) {
+ return m_DataRoot && !m_DataRoot->children.empty();
+ }
+
+ const auto parentItem = static_cast<const Item*>(parent.internalPointer());
+ return parentItem ? parentItem->group.has_value() : false;
+}
+
+int PluginRecordModel::rowCount(const QModelIndex& parent) const
+{
+ const auto parentItem =
+ parent.isValid() ? static_cast<Item*>(parent.internalPointer()) : m_DataRoot;
+
+ return parentItem ? static_cast<int>(parentItem->children.size()) : 0;
+}
+
+int PluginRecordModel::columnCount([[maybe_unused]] const QModelIndex& parent) const
+{
+ return COL_COUNT;
+}
+
+bool PluginRecordModel::canFetchMore(const QModelIndex& parent) const
+{
+ if (!parent.isValid()) {
+ return false;
+ }
+
+ const auto parentItem = static_cast<const Item*>(parent.internalPointer());
+ return parentItem && parentItem->group.has_value() && parentItem->children.empty();
+}
+
+void PluginRecordModel::fetchMore(const QModelIndex& parent)
+{
+ const auto parentItem =
+ parent.isValid() ? static_cast<Item*>(parent.internalPointer()) : nullptr;
+
+ if (!parentItem || !parentItem->children.empty() || !parentItem->group.has_value()) {
+ return;
+ }
+
+ QList<QString> names;
+ if (parentItem->record) {
+ for (const auto handle : parentItem->record->alternatives()) {
+ const auto entry = m_PluginList->findEntryByHandle(handle);
+ names.append(QString::fromStdString(entry->name()));
+ }
+ } else {
+ names.append(m_PluginName);
+ }
+
+ for (const auto& name : names) {
+ const auto vfsEntry =
+ m_Organizer->virtualFileTree()->find(name, MOBase::FileTreeEntry::FILE);
+
+ const auto filePath = m_Organizer->resolvePath(name);
+
+ try {
+ TESData::BranchConflictParser handler{m_PluginList, name.toStdString(),
+ getPath(parent)};
+ TESFile::Reader<TESData::BranchConflictParser> reader{};
+ reader.parse(std::filesystem::path(filePath.toStdString()), handler);
+ } catch (const std::exception& e) {
+ MOBase::log::error("Error parsing \"{}\": {}", filePath, e.what());
+ }
+ }
+
+ if (parentItem->children.empty()) {
+ beginRemoveRows(parent, 0, 0);
+ parentItem->group = std::nullopt;
+ endRemoveRows();
+ }
+}
+
+QVariant PluginRecordModel::data(const QModelIndex& index, int role) const
+{
+ const auto item = static_cast<const Item*>(index.internalPointer());
+
+ switch (role) {
+ case Qt::DisplayRole:
+ case Qt::EditRole: {
+ switch (index.column()) {
+ case COL_ID: {
+ if (item->record) {
+ if (item->record->hasFormId()) {
+ std::uint32_t formId = item->record->formId() & 0xFFFFFFU;
+
+ const auto plugin =
+ m_PluginList ? m_PluginList->getPluginByName(m_PluginName) : nullptr;
+
+ if (plugin) {
+ const auto file = item->record->file();
+ const auto localIndex = std::distance(
+ std::begin(plugin->masters()),
+ TESFile::find(plugin->masters(), file, &QString::toStdString));
+ formId |= ((localIndex & 0xFF) << 24U);
+ }
+
+ QString str = u"%1"_s.arg(formId, 8, 16, QChar(u'0')).toUpper();
+
+ for (const Item* p = item->parent; p; p = p->parent) {
+ if (p->group && p->group->hasFormType()) {
+ if (item->formType != p->group->formType()) {
+ QString suffix = TESData::getFormName(item->formType).toString();
+ if (suffix.isEmpty()) {
+ suffix = QString::fromLocal8Bit(item->formType.data(),
+ item->formType.size());
+ }
+ str = u"%1 %2"_s.arg(str).arg(suffix);
+ }
+ break;
+ }
+ }
+
+ return str;
+
+ } else if (item->record->hasEditorId()) {
+ return QString::fromStdString(item->record->editorId());
+
+ } else if (item->record->hasTypeId()) {
+ const auto type = item->record->typeId();
+ const auto typestr = QString::fromLocal8Bit(type.data(), type.size());
+ const auto name = TESData::getDefaultObjectName(type);
+ if (!name.isEmpty()) {
+ return u"%1 - %2"_s.arg(typestr).arg(name);
+ } else {
+ return typestr;
+ }
+ }
+ }
+
+ if (item->group.has_value()) {
+ return makeGroupName(item->group.value());
+ }
+
+ return QVariant();
+ }
+
+ case COL_OWNER: {
+ if (item->record && item->record->hasFormId()) {
+ const auto file = item->record->file();
+ return QString::fromStdString(file);
+ }
+ return QVariant();
+ }
+
+ case COL_NAME: {
+ if (item->record && item->record->hasFormId()) {
+ return QString::fromStdString(item->name);
+ }
+ return QVariant();
+ }
+ }
+
+ return QVariant();
+ }
+
+ case Qt::BackgroundRole: {
+ if (!m_PluginList || !m_FileEntry || !item->record) {
+ return QVariant();
+ }
+
+ const auto info = m_PluginList->getPluginByName(m_PluginName);
+ if (!info) {
+ return QVariant();
+ }
+
+ bool isConflicted = false;
+ for (const auto alternative : item->record->alternatives()) {
+ const auto altEntry = m_PluginList->findEntryByHandle(alternative);
+ if (!altEntry || altEntry == m_FileEntry)
+ continue;
+
+ const auto altInfo =
+ m_PluginList->getPluginByName(QString::fromStdString(altEntry->name()));
+ if (!altInfo || !altInfo->enabled())
+ continue;
+
+ isConflicted = true;
+ if (altInfo->priority() > info->priority()) {
+ return QColor(255, 0, 0, 64);
+ }
+ }
+
+ if (isConflicted) {
+ return QColor(0, 255, 0, 64);
+ } else {
+ return QVariant();
+ }
+ }
+
+ case Qt::UserRole: {
+ return QVariant::fromValue(item);
+ }
+ }
+
+ return QVariant();
+}
+
+QString PluginRecordModel::makeGroupName(TESFile::GroupData group)
+{
+ using GroupType = TESFile::GroupType;
+
+ switch (group.type()) {
+ case GroupType::Top: {
+ const auto type = group.formType();
+ const auto typestr = QString::fromLocal8Bit(type.data(), type.size());
+ const auto name = TESData::getFormName(type);
+ if (!name.isEmpty()) {
+ return u"%1 - %2"_s.arg(typestr).arg(name);
+ } else {
+ return typestr;
+ }
+ }
+ case GroupType::WorldChildren:
+ return tr("Children");
+ case GroupType::InteriorCellBlock:
+ return tr("Block %1").arg(group.block());
+ case GroupType::InteriorCellSubBlock:
+ return tr("Sub-Block %1").arg(group.block());
+ case GroupType::ExteriorCellBlock: {
+ const auto [x, y] = group.gridCell();
+ return tr("Block %1, %2").arg(x).arg(y);
+ }
+ case GroupType::ExteriorCellSubBlock: {
+ const auto [x, y] = group.gridCell();
+ return tr("Sub-Block %1, %2").arg(x).arg(y);
+ }
+ case GroupType::CellChildren:
+ return tr("Children");
+ case GroupType::TopicChildren:
+ return tr("Children");
+ case GroupType::CellPersistentChildren:
+ return tr("Persistent");
+ case GroupType::CellTemporaryChildren:
+ return tr("Temporary");
+ case GroupType::CellVisibleDistantChildren:
+ return tr("Visible Distant");
+ default:
+ return tr("Children");
+ }
+}
+
+QVariant PluginRecordModel::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ if (orientation != Qt::Horizontal || role != Qt::DisplayRole)
+ return QVariant();
+
+ switch (section) {
+ case COL_ID:
+ return tr("Form");
+ case COL_OWNER:
+ return tr("Origin");
+ case COL_NAME:
+ return tr("Editor ID");
+ }
+
+ return QVariant();
+}
+
+} // namespace BSPluginInfo
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/PluginRecordModel.h b/libs/installer_bsplugins/src/BSPluginInfo/PluginRecordModel.h
new file mode 100644
index 0000000..b625e2d
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/PluginRecordModel.h
@@ -0,0 +1,60 @@
+#ifndef BSPLUGININFO_PLUGINRECORDMODEL_H
+#define BSPLUGININFO_PLUGINRECORDMODEL_H
+
+#include "TESData/PluginList.h"
+
+#include <imoinfo.h>
+
+#include <QAbstractItemModel>
+
+namespace BSPluginInfo
+{
+
+class PluginRecordModel final : public QAbstractItemModel
+{
+ Q_OBJECT
+
+public:
+ enum EColumn
+ {
+ COL_ID,
+ COL_OWNER,
+ COL_NAME,
+
+ COL_COUNT
+ };
+
+ PluginRecordModel(MOBase::IOrganizer* organizer, TESData::PluginList* pluginList,
+ const QString& pluginName);
+
+ [[nodiscard]] TESData::RecordPath getPath(const QModelIndex& index) const;
+
+ QModelIndex index(int row, int column,
+ const QModelIndex& parent = QModelIndex()) const override;
+ QModelIndex parent(const QModelIndex& index) const override;
+ bool hasChildren(const QModelIndex& parent = QModelIndex()) const override;
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+
+ bool canFetchMore(const QModelIndex& parent) const override;
+ void fetchMore(const QModelIndex& parent) override;
+
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role = Qt::DisplayRole) const override;
+
+private:
+ using Item = TESData::FileEntry::TreeItem;
+
+ static QString makeGroupName(TESFile::GroupData group);
+
+ QString m_PluginName;
+ MOBase::IOrganizer* m_Organizer = nullptr;
+ TESData::PluginList* m_PluginList = nullptr;
+ TESData::FileEntry* m_FileEntry = nullptr;
+ Item* m_DataRoot = nullptr;
+};
+
+} // namespace BSPluginInfo
+
+#endif // BSPLUGININFO_PLUGINRECORDMODEL_H
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/PluginRecordView.cpp b/libs/installer_bsplugins/src/BSPluginInfo/PluginRecordView.cpp
new file mode 100644
index 0000000..01f4464
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/PluginRecordView.cpp
@@ -0,0 +1,283 @@
+#include "PluginRecordView.h"
+#include "MOPlugin/Settings.h"
+#include "ui_pluginrecordview.h"
+
+#include <widgetutility.h>
+
+#include <QMenu>
+
+#include <ranges>
+
+namespace BSPluginInfo
+{
+
+PluginRecordView::PluginRecordView(QWidget* parent)
+ : QWidget(parent), ui{new Ui::PluginRecordView()}
+{
+ ui->setupUi(this);
+
+ MOBase::setCustomizableColumns(ui->pickRecordView);
+
+ // fix issue where view scrolls after moving headers
+ ui->recordStructureView->header()->setAutoScroll(false);
+
+ ui->conflictFilterRow->hide();
+}
+
+void PluginRecordView::setup(MOBase::IOrganizer* organizer,
+ TESData::PluginList* pluginList, const QString& pluginName)
+{
+ m_Organizer = organizer;
+ m_PluginList = pluginList;
+ m_FilterProxy = new RecordFilterProxyModel(pluginList, pluginName);
+ ui->conflictFilterRow->show();
+
+ setFile(pluginName);
+
+ ui->pickRecordView->header()->resizeSection(PluginRecordModel::COL_ID, 222);
+
+ connect(ui->recordStructureView->header(), &QHeaderView::sectionMoved, this,
+ &PluginRecordView::onFileHeaderMoved);
+
+ const bool ignoreMasters =
+ Settings::instance()->get<bool>("ignore_master_conflicts", false);
+
+ ui->ignoreMasterConflicts->setChecked(ignoreMasters);
+}
+
+bool PluginRecordView::hasData() const
+{
+ return m_RecordModel->hasChildren();
+}
+
+QSplitter* PluginRecordView::splitter() const
+{
+ return ui->splitter;
+}
+
+QTreeView* PluginRecordView::pickRecordView() const
+{
+ return ui->pickRecordView;
+}
+
+QTreeView* PluginRecordView::recordStructureView() const
+{
+ return ui->recordStructureView;
+}
+
+void PluginRecordView::setFile(const QString& pluginName)
+{
+ const auto recordModel = m_RecordModel;
+ const auto structureModel = m_StructureModel;
+
+ m_RecordModel = new PluginRecordModel(m_Organizer, m_PluginList, pluginName);
+ m_FilterProxy->setFile(pluginName);
+ m_FilterProxy->setSourceModel(m_RecordModel);
+ ui->pickRecordView->setModel(m_FilterProxy);
+
+ m_StructureModel = nullptr;
+ ui->recordStructureView->setModel(nullptr);
+ ui->recordStructureView->setVisible(false);
+ on_pickRecordView_expanded(QModelIndex());
+
+ connect(ui->pickRecordView->selectionModel(), &QItemSelectionModel::currentChanged,
+ this, &PluginRecordView::onRecordPicked);
+
+ m_ConflictEntry = m_PluginList->findEntryByName(pluginName.toStdString());
+
+ delete recordModel;
+ delete structureModel;
+}
+
+PluginRecordView::~PluginRecordView() noexcept
+{
+ delete ui;
+ delete m_RecordModel;
+ delete m_FilterProxy;
+ delete m_StructureModel;
+}
+
+void PluginRecordView::onRecordPicked(const QModelIndex& current)
+{
+ if (!current.isValid()) {
+ return;
+ }
+
+ const auto oldModel = m_StructureModel;
+ m_StructureModel = nullptr;
+
+ const auto sourceIndex = m_FilterProxy->mapToSource(current);
+ if (m_ConflictEntry) {
+ const auto path = m_RecordModel->getPath(sourceIndex);
+ const auto record = m_ConflictEntry->findRecord(path).get();
+ if (record) {
+ m_StructureModel =
+ new RecordStructureModel(m_PluginList, record, path, m_Organizer);
+ }
+ }
+
+ ui->recordStructureView->setModel(m_StructureModel);
+ ui->recordStructureView->setVisible(m_StructureModel != nullptr);
+ expandStructureConflicts();
+
+ if (oldModel) {
+ delete oldModel;
+ }
+}
+
+void PluginRecordView::onFileHeaderMoved(int logicalIndex, int oldVisualIndex,
+ int newVisualIndex)
+{
+ if (!m_PluginList || m_MovingSection) {
+ return;
+ }
+
+ m_MovingSection = true;
+
+ const auto& file = m_StructureModel->file(logicalIndex - 1);
+ const int index = m_PluginList->getIndex(file);
+ const auto info = m_PluginList->getPlugin(index);
+ if (!info) {
+ return;
+ }
+
+ const auto& otherFile = m_StructureModel->file(newVisualIndex - 1);
+ const auto otherInfo = m_PluginList->getPluginByName(otherFile);
+ if (otherInfo) {
+ int destination = otherInfo->priority();
+ if (newVisualIndex > oldVisualIndex)
+ ++destination;
+
+ if (m_PluginList->canMoveToPriority({index}, destination)) {
+ m_PluginList->moveToPriority({index}, destination);
+ m_RecordModel->emit dataChanged(QModelIndex(), QModelIndex());
+ m_StructureModel->refresh();
+ }
+ }
+
+ ui->recordStructureView->header()->moveSection(newVisualIndex, oldVisualIndex);
+ m_MovingSection = false;
+}
+
+void PluginRecordView::on_pickRecordView_expanded(const QModelIndex& index)
+{
+ const auto model = ui->pickRecordView->model();
+ for (int row = 0, count = model->rowCount(index); row < count; ++row) {
+ const auto child = model->index(row, 0, index);
+
+ using Item = TESData::FileEntry::TreeItem;
+ const auto item = child.data(Qt::UserRole).value<const Item*>();
+ if (!item || !item->record || !item->record->hasFormId()) {
+ ui->pickRecordView->setFirstColumnSpanned(row, index, true);
+ }
+ }
+}
+
+void PluginRecordView::on_pickRecordView_customContextMenuRequested(const QPoint& pos)
+{
+ const auto selectionModel = ui->pickRecordView->selectionModel();
+ const auto selectedRows = selectionModel->selectedRows();
+ using Item = TESData::FileEntry::TreeItem;
+ const auto items =
+ std::ranges::transform_view(selectedRows, [](const QModelIndex& index) {
+ return index.data(Qt::UserRole).value<const Item*>();
+ });
+
+ if (items.empty() || !std::ranges::all_of(items, [](const Item* item) {
+ return item && item->record;
+ })) {
+ return;
+ }
+
+ QMenu menu;
+
+ QAction* ignoreRecord;
+ ignoreRecord = menu.addAction(tr("Ignore Record"), [&] {
+ for (auto&& item : items) {
+ item->record->setIgnored(ignoreRecord->isChecked());
+
+ for (const auto handle : item->record->alternatives()) {
+ const auto entry = m_PluginList->findEntryByHandle(handle);
+ const auto info =
+ m_PluginList->getPluginByName(QString::fromStdString(entry->name()));
+ if (info) {
+ info->invalidateConflicts();
+ }
+ }
+ }
+ });
+
+ ignoreRecord->setCheckable(true);
+ ignoreRecord->setChecked(std::ranges::all_of(items, [](const Item* item) {
+ return item->record->ignored();
+ }));
+
+ const QPoint p = ui->pickRecordView->viewport()->mapToGlobal(pos);
+ menu.exec(p);
+}
+
+void PluginRecordView::on_recordStructureView_expanded(const QModelIndex& index)
+{
+ const auto model = ui->recordStructureView->model();
+ const int rowCount = model->rowCount(index);
+ if (rowCount == 1) {
+ const auto child = model->index(0, 0, index);
+ if (model->hasChildren(child)) {
+ ui->recordStructureView->expand(child);
+ }
+ } else {
+ for (int i = 0; i < rowCount; ++i) {
+ const auto child = model->index(i, 0, index);
+ if (model->hasChildren(child) && child.data().toString().isEmpty()) {
+ ui->recordStructureView->expand(child);
+ }
+ }
+ }
+}
+
+void PluginRecordView::on_filterCombo_currentIndexChanged(int index)
+{
+ switch (index) {
+ case Filter_AllConflicts:
+ m_FilterProxy->setFilterFlags(RecordFilterProxyModel::Filter_AllConflicts);
+ break;
+ case Filter_WinningConflicts:
+ m_FilterProxy->setFilterFlags(RecordFilterProxyModel::Filter_WinningConflicts);
+ break;
+ case Filter_LosingConflicts:
+ m_FilterProxy->setFilterFlags(RecordFilterProxyModel::Filter_LosingConflicts);
+ break;
+ }
+}
+
+void PluginRecordView::on_ignoreMasterConflicts_stateChanged(int state)
+{
+ Settings::instance()->set("ignore_master_conflicts", state == Qt::Checked);
+ m_RecordModel->emit dataChanged(QModelIndex(), QModelIndex());
+}
+
+void PluginRecordView::expandStructureConflicts(const QModelIndex& parent)
+{
+ if (!m_StructureModel) {
+ return;
+ }
+
+ const int count = m_StructureModel->rowCount(parent);
+ for (int i = 0; i < count; ++i) {
+ using Item = TESData::DataItem;
+ const auto idx = m_StructureModel->index(i, 0, parent);
+ const auto item = idx.data(Qt::UserRole).value<const Item*>();
+ const int fileCount = m_StructureModel->columnCount(parent) - 1;
+
+ if (item->numChildren() > 0 && item->isConflicted(fileCount)) {
+ static constexpr int EXPAND_MAX_ROWS = 80;
+
+ if (item->numChildren() <= EXPAND_MAX_ROWS) {
+ ui->recordStructureView->expand(idx);
+ expandStructureConflicts(idx);
+ }
+ }
+ }
+}
+
+} // namespace BSPluginInfo
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/PluginRecordView.h b/libs/installer_bsplugins/src/BSPluginInfo/PluginRecordView.h
new file mode 100644
index 0000000..880cc1a
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/PluginRecordView.h
@@ -0,0 +1,78 @@
+#ifndef BSPLUGININFO_PLUGINRECORDVIEW_H
+#define BSPLUGININFO_PLUGINRECORDVIEW_H
+
+#include "PluginRecordModel.h"
+#include "RecordFilterProxyModel.h"
+#include "RecordStructureModel.h"
+
+#include <QWidget>
+
+class QSplitter;
+class QTreeView;
+namespace Ui
+{
+class PluginRecordView;
+}
+
+namespace BSPluginInfo
+{
+
+class PluginRecordView final : public QWidget
+{
+ Q_OBJECT
+
+public:
+ enum ConflictFilter
+ {
+ Filter_AllConflicts,
+ Filter_WinningConflicts,
+ Filter_LosingConflicts,
+ };
+
+ explicit PluginRecordView(QWidget* parent = nullptr);
+
+ PluginRecordView(const PluginRecordView&) = delete;
+ PluginRecordView(PluginRecordView&&) = delete;
+
+ ~PluginRecordView() noexcept;
+
+ PluginRecordView& operator=(const PluginRecordView&) = delete;
+ PluginRecordView& operator=(PluginRecordView&&) = delete;
+
+ void setup(MOBase::IOrganizer* organizer, TESData::PluginList* pluginList,
+ const QString& pluginName);
+
+ [[nodiscard]] bool hasData() const;
+
+ [[nodiscard]] QSplitter* splitter() const;
+ [[nodiscard]] QTreeView* pickRecordView() const;
+ [[nodiscard]] QTreeView* recordStructureView() const;
+
+ void setFile(const QString& pluginName);
+
+private slots:
+ void onRecordPicked(const QModelIndex& current);
+ void onFileHeaderMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex);
+
+ void on_pickRecordView_expanded(const QModelIndex& index);
+ void on_pickRecordView_customContextMenuRequested(const QPoint& pos);
+ void on_recordStructureView_expanded(const QModelIndex& index);
+ void on_filterCombo_currentIndexChanged(int index);
+ void on_ignoreMasterConflicts_stateChanged(int state);
+
+private:
+ void expandStructureConflicts(const QModelIndex& parent = QModelIndex());
+
+ Ui::PluginRecordView* ui;
+ MOBase::IOrganizer* m_Organizer = nullptr;
+ const TESData::FileEntry* m_ConflictEntry = nullptr;
+ TESData::PluginList* m_PluginList = nullptr;
+ PluginRecordModel* m_RecordModel = nullptr;
+ RecordFilterProxyModel* m_FilterProxy = nullptr;
+ RecordStructureModel* m_StructureModel = nullptr;
+ bool m_MovingSection = false;
+};
+
+} // namespace BSPluginInfo
+
+#endif // BSPLUGININFO_PLUGINRECORDVIEW_H
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/RecordFilterProxyModel.cpp b/libs/installer_bsplugins/src/BSPluginInfo/RecordFilterProxyModel.cpp
new file mode 100644
index 0000000..919e8c9
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/RecordFilterProxyModel.cpp
@@ -0,0 +1,138 @@
+#include "RecordFilterProxyModel.h"
+#include "MOPlugin/Settings.h"
+
+#include <algorithm>
+
+namespace BSPluginInfo
+{
+
+RecordFilterProxyModel::RecordFilterProxyModel(const TESData::PluginList* pluginList,
+ const QString& pluginName)
+ : m_PluginList{pluginList}, m_PluginName{pluginName},
+ m_FilterFlags{Filter_AllConflicts}
+{
+ setRecursiveFilteringEnabled(true);
+}
+
+void RecordFilterProxyModel::setFile(const QString& pluginName)
+{
+ m_PluginName = pluginName;
+}
+
+void RecordFilterProxyModel::setFilterFlags(FilterFlags filterFlags)
+{
+ m_FilterFlags = filterFlags;
+ invalidateRowsFilter();
+}
+
+void RecordFilterProxyModel::setSourceModel(QAbstractItemModel* sourceModel)
+{
+ emit beginResetModel();
+
+ if (const auto oldSource = this->sourceModel()) {
+ disconnect(oldSource, nullptr, this, nullptr);
+ }
+
+ QSortFilterProxyModel::setSourceModel(sourceModel);
+
+ connect(sourceModel, &QAbstractItemModel::dataChanged, this,
+ &RecordFilterProxyModel::onSourceDataChanged);
+ connect(sourceModel, &QAbstractItemModel::rowsRemoved, this,
+ &RecordFilterProxyModel::onSourceRowsRemoved);
+
+ emit endResetModel();
+}
+
+void RecordFilterProxyModel::onSourceDataChanged()
+{
+ if (dynamicSortFilter()) {
+ invalidateRowsFilter();
+ }
+}
+
+void RecordFilterProxyModel::onSourceRowsRemoved(const QModelIndex& parent,
+ [[maybe_unused]] int first,
+ [[maybe_unused]] int last)
+{
+ emit layoutAboutToBeChanged({parent});
+ emit layoutChanged({parent});
+}
+
+bool RecordFilterProxyModel::filterAcceptsRow(int source_row,
+ const QModelIndex& source_parent) const
+{
+ if (!m_PluginList) {
+ return true;
+ }
+
+ using Item = TESData::FileEntry::TreeItem;
+ const auto index = sourceModel()->index(source_row, 0, source_parent);
+
+ const auto item = index.data(Qt::UserRole).value<const Item*>();
+ const auto info = m_PluginList->getPluginByName(m_PluginName);
+ if (!item || !info) {
+ return true;
+ }
+
+ if (!item->record) {
+ return false;
+ }
+
+ if (m_FilterFlags == Filter_AllRecords) {
+ return true;
+ }
+
+ const bool ignoreMasters =
+ Settings::instance()->get<bool>("ignore_master_conflicts", false);
+
+ bool isConflicted = false;
+ bool isLosing = false;
+ for (const auto alternative : item->record->alternatives()) {
+ const auto altEntry = m_PluginList->findEntryByHandle(alternative);
+ if (!altEntry || altEntry->name() == m_PluginName.toStdString())
+ continue;
+
+ const auto altInfo =
+ m_PluginList->getPluginByName(QString::fromStdString(altEntry->name()));
+ if (!altInfo || !altInfo->enabled())
+ continue;
+
+ if (info->priority() > altInfo->priority()) {
+ if (!ignoreMasters ||
+ !info->masters().contains(altInfo->name(), Qt::CaseInsensitive)) {
+ isConflicted = true;
+ }
+ } else {
+ if (!ignoreMasters ||
+ !altInfo->masters().contains(info->name(), Qt::CaseInsensitive)) {
+ isConflicted = true;
+ isLosing = true;
+ break;
+ }
+ }
+ }
+
+ if (isConflicted && !isLosing) {
+ return m_FilterFlags & Filter_WinningConflicts;
+ }
+
+ if (!isConflicted) {
+ return m_FilterFlags & Filter_NoConflicts;
+ } else {
+ if (!isLosing) {
+ return m_FilterFlags & Filter_WinningConflicts;
+ } else {
+ if (m_FilterFlags == Filter_AllConflicts) {
+ return true;
+ }
+
+ if (sourceModel()->canFetchMore(index)) {
+ sourceModel()->fetchMore(index);
+ }
+
+ return m_FilterFlags & Filter_LosingConflicts;
+ }
+ }
+}
+
+} // namespace BSPluginInfo
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/RecordFilterProxyModel.h b/libs/installer_bsplugins/src/BSPluginInfo/RecordFilterProxyModel.h
new file mode 100644
index 0000000..b0313ba
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/RecordFilterProxyModel.h
@@ -0,0 +1,53 @@
+#ifndef BSPLUGININFO_RECORDFILTERPROXYMODEL_H
+#define BSPLUGININFO_RECORDFILTERPROXYMODEL_H
+
+#include "TESData/PluginList.h"
+
+#include <QSortFilterProxyModel>
+
+namespace BSPluginInfo
+{
+
+class RecordFilterProxyModel final : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ enum FilterFlags
+ {
+ Filter_WinningConflicts = 0x1,
+ Filter_LosingConflicts = 0x2,
+ Filter_NoConflicts = 0x4,
+
+ Filter_AllConflicts = Filter_WinningConflicts | Filter_LosingConflicts,
+ Filter_AllRecords = Filter_AllConflicts | Filter_NoConflicts,
+ };
+
+ RecordFilterProxyModel(const TESData::PluginList* pluginList,
+ const QString& pluginName);
+
+ [[nodiscard]] const QString& file() const { return m_PluginName; }
+ void setFile(const QString& pluginName);
+
+ [[nodiscard]] FilterFlags filterFlags() const { return m_FilterFlags; }
+ void setFilterFlags(FilterFlags filterFlags);
+
+ void setSourceModel(QAbstractItemModel* sourceModel) override;
+
+protected:
+ bool filterAcceptsRow(int source_row,
+ const QModelIndex& source_parent) const override;
+
+private slots:
+ void onSourceDataChanged();
+ void onSourceRowsRemoved(const QModelIndex& parent, int first, int last);
+
+private:
+ const TESData::PluginList* m_PluginList;
+ QString m_PluginName;
+ FilterFlags m_FilterFlags;
+};
+
+} // namespace BSPluginInfo
+
+#endif // BSPLUGININFO_RECORDFILTERPROXYMODEL_H
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/RecordStructureModel.cpp b/libs/installer_bsplugins/src/BSPluginInfo/RecordStructureModel.cpp
new file mode 100644
index 0000000..a28d714
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/RecordStructureModel.cpp
@@ -0,0 +1,167 @@
+#include "RecordStructureModel.h"
+#include "TESData/SingleRecordParser.h"
+#include "TESFile/Reader.h"
+
+#include <log.h>
+
+#include <boost/container/flat_map.hpp>
+
+namespace BSPluginInfo
+{
+
+RecordStructureModel::RecordStructureModel(TESData::PluginList* pluginList,
+ TESData::Record* record,
+ const TESData::RecordPath& path,
+ MOBase::IOrganizer* organizer)
+ : m_Organizer{organizer}, m_PluginList{pluginList}, m_Record{record}, m_Path{path},
+ m_Root{std::make_shared<Item>()}
+{
+ refresh();
+}
+
+void RecordStructureModel::refresh()
+{
+ boost::container::flat_map<int, const TESData::FileEntry*> entries;
+ for (const auto handle : m_Record->alternatives()) {
+ const auto entry = m_PluginList->findEntryByHandle(handle);
+ const auto info =
+ m_PluginList->getPluginByName(QString::fromStdString(entry->name()));
+ if (info && info->enabled()) {
+ entries.emplace(info->priority(), entry);
+ }
+ }
+
+ m_Files.resize(entries.size());
+
+ for (int index = static_cast<int>(entries.size()) - 1; index >= 0; --index) {
+ const auto entry = entries.nth(index)->second;
+ const auto name = QString::fromStdString(entry->name());
+ const auto vfsEntry =
+ m_Organizer->virtualFileTree()->find(name, MOBase::FileTreeEntry::FILE);
+
+ const auto filePath = m_Organizer->resolvePath(name);
+ m_Files[index] = QFileInfo(filePath).fileName();
+
+ readFile(m_Path, filePath, index);
+ }
+}
+
+void RecordStructureModel::readFile(const TESData::RecordPath& path,
+ const QString& filePath, int index)
+try {
+ const auto fileName = QFileInfo(filePath).fileName().toStdString();
+ const auto gameName = m_Organizer->managedGame()->gameName();
+ TESData::SingleRecordParser handler(gameName, path, fileName, m_Root.get(), index);
+ TESFile::Reader<TESData::SingleRecordParser> reader{};
+ reader.parse(std::filesystem::path(filePath.toStdWString()), handler);
+} catch (const std::exception& e) {
+ MOBase::log::error("Error parsing \"{}\": {}", filePath, e.what());
+}
+
+QModelIndex RecordStructureModel::index(int row, int column,
+ const QModelIndex& parent) const
+{
+ if (row < 0 || column < 0) {
+ return QModelIndex();
+ }
+
+ const auto parentItem = parent.isValid()
+ ? static_cast<const Item*>(parent.internalPointer())
+ : m_Root.get();
+ if (parentItem && row < parentItem->numChildren()) {
+ const auto childItem = parentItem->childAt(row);
+ return createIndex(row, column, childItem);
+ }
+
+ return QModelIndex();
+}
+
+QModelIndex RecordStructureModel::parent(const QModelIndex& index) const
+{
+ const auto item =
+ index.isValid() ? static_cast<const Item*>(index.internalPointer()) : nullptr;
+ const auto parentItem = item ? item->parent() : nullptr;
+ if (!parentItem) {
+ return QModelIndex();
+ }
+
+ const int row = parentItem->index();
+ return createIndex(row, 0, parentItem);
+}
+
+int RecordStructureModel::rowCount(const QModelIndex& parent) const
+{
+ const auto item = parent.isValid()
+ ? static_cast<const Item*>(parent.internalPointer())
+ : m_Root.get();
+ return item ? item->numChildren() : 0;
+}
+
+int RecordStructureModel::columnCount([[maybe_unused]] const QModelIndex& parent) const
+{
+ return 1 + static_cast<int>(m_Files.size());
+}
+
+QVariant RecordStructureModel::data(const QModelIndex& index, int role) const
+{
+ const auto item =
+ index.isValid() ? static_cast<const Item*>(index.internalPointer()) : nullptr;
+ if (!item) {
+ return QVariant();
+ }
+
+ switch (role) {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ if (index.column() == 0) {
+ return item->rowHeader();
+ } else {
+ return item->displayData(index.column() - 1);
+ }
+
+ case Qt::BackgroundRole: {
+ if (index.column() == 0) {
+ return QVariant();
+ }
+
+ const int count = columnCount() - 1;
+ const int fileIndex = index.column() - 1;
+ if (item->isLosingConflict(fileIndex, count)) {
+ return QColor(255, 0, 0, 64);
+ } else if (item->isOverriding(fileIndex)) {
+ return QColor(0, 255, 0, 64);
+ }
+
+ return QVariant();
+ }
+
+ case Qt::UserRole:
+ return QVariant::fromValue(item);
+ }
+
+ return QVariant();
+}
+
+QVariant RecordStructureModel::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ if (orientation != Qt::Horizontal)
+ return QVariant();
+
+ switch (role) {
+ case Qt::DisplayRole: {
+ if (section == 0) {
+ // Name
+ return QVariant();
+ }
+
+ if (section - 1 < m_Files.length()) {
+ return m_Files[section - 1];
+ }
+ }
+ }
+
+ return QVariant();
+}
+
+} // namespace BSPluginInfo
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/RecordStructureModel.h b/libs/installer_bsplugins/src/BSPluginInfo/RecordStructureModel.h
new file mode 100644
index 0000000..9592b24
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/RecordStructureModel.h
@@ -0,0 +1,51 @@
+#ifndef BSPLUGININFO_RECORDSTRUCTUREMODEL_H
+#define BSPLUGININFO_RECORDSTRUCTUREMODEL_H
+
+#include "TESData/DataItem.h"
+#include "TESData/PluginList.h"
+#include "TESData/RecordPath.h"
+
+#include <QAbstractItemModel>
+#include <QList>
+
+namespace BSPluginInfo
+{
+
+class RecordStructureModel final : public QAbstractItemModel
+{
+ Q_OBJECT
+
+public:
+ RecordStructureModel(TESData::PluginList* pluginList, TESData::Record* record,
+ const TESData::RecordPath& path, MOBase::IOrganizer* organizer);
+
+ void refresh();
+
+ [[nodiscard]] const QString& file(int index) const { return m_Files[index]; }
+
+ QModelIndex index(int row, int column,
+ const QModelIndex& parent = QModelIndex()) const override;
+ QModelIndex parent(const QModelIndex& index) const override;
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role = Qt::DisplayRole) const override;
+
+private:
+ using Item = TESData::DataItem;
+
+ void readFile(const TESData::RecordPath& path, const QString& filePath,
+ int index = 0);
+
+ QList<QString> m_Files;
+ MOBase::IOrganizer* m_Organizer = nullptr;
+ TESData::PluginList* m_PluginList = nullptr;
+ TESData::Record* m_Record = nullptr;
+ TESData::RecordPath m_Path;
+ std::shared_ptr<Item> m_Root;
+};
+
+} // namespace BSPluginInfo
+
+#endif // BSPLUGININFO_RECORDSTRUCTUREMODEL_H
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/plugininfodialog.ui b/libs/installer_bsplugins/src/BSPluginInfo/plugininfodialog.ui
new file mode 100644
index 0000000..1b6fc59
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/plugininfodialog.ui
@@ -0,0 +1,397 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PluginInfoDialog</class>
+ <widget class="QDialog" name="PluginInfoDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>1000</width>
+ <height>612</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>BGS Data</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tabRecords">
+ <attribute name="title">
+ <string>Records</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="BSPluginInfo::PluginRecordView" name="pluginRecordView" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tabArchives">
+ <attribute name="title">
+ <string>Archives</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QTabWidget" name="tabArchivesTabs">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tabConflictsGeneral">
+ <attribute name="title">
+ <string>Conflicts</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,1,0,1,0,1">
+ <item>
+ <layout class="QVBoxLayout" name="winningRoot">
+ <item>
+ <layout class="QHBoxLayout" name="winningHeader" stretch="1,0,0">
+ <item>
+ <widget class="QToolButton" name="winningExpander">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="toolTip">
+ <string>Files that exist in other mods but are overwritten by this mod</string>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">border: none;
+text-align: left;</string>
+ </property>
+ <property name="text">
+ <string>Winning file conflicts:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="MOBase::LineEditClear" name="winningLineEdit">
+ <property name="placeholderText">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLCDNumber" name="winningCount">
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="lineWidth">
+ <number>1</number>
+ </property>
+ <property name="segmentStyle">
+ <enum>QLCDNumber::Flat</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTreeView" name="winningTree">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string>Files that exist in other mods but are overwritten by this mod</string>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="textElideMode">
+ <enum>Qt::ElideLeft</enum>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="losingRoot">
+ <item>
+ <layout class="QHBoxLayout" name="losingHeader" stretch="1,0,0">
+ <item>
+ <widget class="QToolButton" name="losingExpander">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="toolTip">
+ <string>Files that are unused because they are overwritten by other mods</string>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">border: none;
+text-align: left;</string>
+ </property>
+ <property name="text">
+ <string>Losing file conflicts:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="MOBase::LineEditClear" name="losingLineEdit">
+ <property name="placeholderText">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLCDNumber" name="losingCount">
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="segmentStyle">
+ <enum>QLCDNumber::Flat</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTreeView" name="losingTree">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string>Files that are unused because they are overwritten by other mods</string>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="textElideMode">
+ <enum>Qt::ElideLeft</enum>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="noConflictRoot">
+ <item>
+ <layout class="QHBoxLayout" name="noConflictHeader" stretch="1,0,0">
+ <item>
+ <widget class="QToolButton" name="noConflictExpander">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="toolTip">
+ <string>Files that have no conflicts</string>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">border: none;
+text-align: left;</string>
+ </property>
+ <property name="text">
+ <string>The following files have no conflicts:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="MOBase::LineEditClear" name="noConflictLineEdit">
+ <property name="placeholderText">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLCDNumber" name="noConflictCount">
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="segmentStyle">
+ <enum>QLCDNumber::Flat</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTreeView" name="noConflictTree">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string>Files that have no conflicts</string>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="textElideMode">
+ <enum>Qt::ElideLeft</enum>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tabArchivesTree">
+ <attribute name="title">
+ <string>Tree</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0,0">
+ <item>
+ <widget class="QLabel" name="currentArchiveLabel">
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="previousArchiveButton">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="../../../modorganizer/src/resources.qrc">
+ <normaloff>:/MO/gui/previous</normaloff>:/MO/gui/previous</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="nextArchiveButton">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="../../../modorganizer/src/resources.qrc">
+ <normaloff>:/MO/gui/next</normaloff>:/MO/gui/next</iconset>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QStackedWidget" name="archivesTreeStack"/>
+ </item>
+ <item>
+ <widget class="MOBase::LineEditClear" name="archiveFilterEdit">
+ <property name="baseSize">
+ <size>
+ <width>220</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="placeholderText">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="previousFile">
+ <property name="text">
+ <string>Previous</string>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="nextFile">
+ <property name="text">
+ <string>Next</string>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </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>
+ <item>
+ <widget class="QPushButton" name="close">
+ <property name="text">
+ <string>Close</string>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>MOBase::LineEditClear</class>
+ <extends>QLineEdit</extends>
+ <header>lineeditclear.h</header>
+ </customwidget>
+ <customwidget>
+ <class>BSPluginInfo::PluginRecordView</class>
+ <extends>QWidget</extends>
+ <header>BSPluginInfo/PluginRecordView.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <resources>
+ <include location="../../../modorganizer/src/resources.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/pluginrecordview.ui b/libs/installer_bsplugins/src/BSPluginInfo/pluginrecordview.ui
new file mode 100644
index 0000000..02d2ce6
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/pluginrecordview.ui
@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PluginRecordView</class>
+ <widget class="QWidget" name="PluginRecordView">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>709</width>
+ <height>453</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string notr="true">BGS Data</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <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="QSplitter" name="splitter">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <widget class="QTreeView" name="pickRecordView">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>150</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ </widget>
+ <widget class="QTreeView" name="recordStructureView">
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::NoSelection</enum>
+ </property>
+ <property name="verticalScrollMode">
+ <enum>QAbstractItemView::ScrollPerPixel</enum>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerDefaultSectionSize">
+ <number>150</number>
+ </attribute>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="conflictFilterRow" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <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="QComboBox" name="filterCombo">
+ <item>
+ <property name="text">
+ <string>All conflicts</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Winning conflicts</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Losing conflicts</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="ignoreMasterConflicts">
+ <property name="text">
+ <string>Ignore conflicts with masters</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>780</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>