diff options
Diffstat (limited to 'libs/installer_bsplugins/src/BSPluginInfo')
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> |
