aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_bsplugins/src/BSPluginInfo/PluginRecordView.cpp
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-12 21:04:15 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-12 21:04:15 -0500
commit3949dcfce95af4bd305f258ff5b170d7d50435f6 (patch)
tree4c768be256c40f31794c3311f0a2c22933a6705a /libs/installer_bsplugins/src/BSPluginInfo/PluginRecordView.cpp
parent892070f3dec1dc690983fd862880e37e711c2516 (diff)
VFS perf fixes, icon/stylesheet compat, download filename fix
VFS: - Open backing fd at mo2_open time for read-only handles so mo2_read can splice without re-opening the file on every read call. - Bump RLIMIT_NOFILE at mount time to fit the resulting fd pressure when the game keeps hundreds of BSAs open. - Dedicated node_cache_mutex so resolveByInode / mo2_lookup stop racing unordered_map writes while multiple tree_mutex readers are active. - max_read=1MB + matching conn->max_read and raised max_readahead/max_write so the kernel merges Wine's small sequential reads into bigger FUSE requests. - Per-op wall-clock counters in mo2_init() logs so we can distinguish VFS latency from game-side work. Proton launch: - Write dxvk.conf to the prefix and set DXVK_CONFIG_FILE to force dxvk.enableGraphicsPipelineLibrary=False, avoiding long GPL compile stalls on first run. UI / plugin compat: - Clamp IconDelegate's per-icon width to [8, 16] so narrow content columns don't trigger QIcon::pixmap(0,0) returning null and logging "failed to load icon" every repaint. - Pre-create lowercase symlinks in the stylesheet directory so QSS files authored on Windows resolve url(foo.svg) when on-disk files are Foo.svg. - Flip content icons empty-chessboard.png and facegen.png to 8-bit RGBA so Qt's built-in PNG reader loads them uniformly. - Add mobase.Version.canonicalString shim for legacy Python plugins that still call the old VersionInfo method on appVersion()'s return. - BSPluginList: compare normalized plugin name lists instead of byte hashes so the post-run case-fix refresh stops spuriously firing the "load order changed" dialog. - BSPluginList disabled by default. Download manager: - Parse CDN URL with QUrl + QUrlQuery and read the filename from the response-content-disposition query param (RFC 6266 quoted / unquoted / ext-value forms), not QFileInfo on the raw URL. - When the API or URL gives us a CDN object key (no archive extension), prefer the actual Content-Disposition header from the live response in metaDataChanged and downloadFinished. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'libs/installer_bsplugins/src/BSPluginInfo/PluginRecordView.cpp')
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/PluginRecordView.cpp283
1 files changed, 283 insertions, 0 deletions
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