diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-12 21:04:15 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-12 21:04:15 -0500 |
| commit | 3949dcfce95af4bd305f258ff5b170d7d50435f6 (patch) | |
| tree | 4c768be256c40f31794c3311f0a2c22933a6705a /libs/installer_bsplugins/src/BSPluginList/PluginListView.cpp | |
| parent | 892070f3dec1dc690983fd862880e37e711c2516 (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/BSPluginList/PluginListView.cpp')
| -rw-r--r-- | libs/installer_bsplugins/src/BSPluginList/PluginListView.cpp | 395 |
1 files changed, 395 insertions, 0 deletions
diff --git a/libs/installer_bsplugins/src/BSPluginList/PluginListView.cpp b/libs/installer_bsplugins/src/BSPluginList/PluginListView.cpp new file mode 100644 index 0000000..077adf9 --- /dev/null +++ b/libs/installer_bsplugins/src/BSPluginList/PluginListView.cpp @@ -0,0 +1,395 @@ +#include "PluginListView.h" + +#include "ConflictIconDelegate.h" +#include "FlagIconDelegate.h" +#include "GUI/CopyEventFilter.h" +#include "MOPlugin/Settings.h" +#include "PluginGroupProxyModel.h" +#include "PluginListModel.h" +#include "PluginListStyledItemDelegate.h" +#include "PluginListViewMarkingScrollBar.h" +#include "PluginSortFilterProxyModel.h" +#include "TESData/PluginList.h" + +#include <widgetutility.h> + +#include <QHeaderView> +#include <QSortFilterProxyModel> + +#include <stdexcept> + +namespace BSPluginList +{ + +PluginListView::PluginListView(QWidget* parent) : QTreeView(parent) +{ + setVerticalScrollBar(new PluginListViewMarkingScrollBar(this)); + MOBase::setCustomizableColumns(this); + setItemDelegate(new PluginListStyledItemDelegate(this)); + installEventFilter(new GUI::CopyEventFilter(this)); +} + +void PluginListView::setup() +{ + setItemDelegateForColumn(PluginListModel::COL_CONFLICTS, + new ConflictIconDelegate(this)); + setItemDelegateForColumn(PluginListModel::COL_FLAGS, new FlagIconDelegate(this)); + + header()->resizeSection(PluginListModel::COL_NAME, 332); + header()->resizeSection(PluginListModel::COL_CONFLICTS, 71); + header()->resizeSection(PluginListModel::COL_FLAGS, 60); + header()->resizeSection(PluginListModel::COL_PRIORITY, 62); + header()->resizeSection(PluginListModel::COL_MODINDEX, 79); + header()->setSectionResizeMode(0, QHeaderView::Stretch); + + connect(this, &QTreeView::collapsed, this, &PluginListView::updateOverwriteMarkers); + connect(this, &QTreeView::expanded, this, &PluginListView::updateOverwriteMarkers); + connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, + &PluginListView::updateOverwriteMarkers); +} + +void PluginListView::setModel(QAbstractItemModel* model) +{ + for (auto nextModel = model; nextModel;) { + const auto proxyModel = qobject_cast<QAbstractProxyModel*>(nextModel); + if (proxyModel) { + if (const auto groupProxy = qobject_cast<PluginGroupProxyModel*>(proxyModel)) { + connect(groupProxy, &PluginGroupProxyModel::groupRenameRequested, this, + &PluginListView::onGroupRenameRequested); + } else if (const auto sortProxy = + qobject_cast<PluginSortFilterProxyModel*>(proxyModel)) { + m_SortProxy = sortProxy; + } + nextModel = proxyModel->sourceModel(); + } else { + if (const auto pluginModel = qobject_cast<PluginListModel*>(nextModel)) { + m_PluginModel = pluginModel; + } else { + throw std::logic_error("PluginListView's model should be a PluginListModel"); + } + nextModel = nullptr; + } + } + + QTreeView::setModel(model); +} + +QRect PluginListView::visualRect(const QModelIndex& index) const +{ + QRect rect = QTreeView::visualRect(index); + if (index.column() == 0) { + if (index.isValid() && !index.model()->hasChildren(index) || + !index.data().isValid()) { + rect.adjust(-indentation(), 0, 0, 0); + } + } + return rect; +} + +QColor PluginListView::markerColor(const QModelIndex& index) const +{ + bool ok; + const uint pluginIndex = index.data(PluginListModel::IndexRole).toUInt(&ok); + if (ok) { + const bool highlight = m_Markers.highlight.contains(pluginIndex); + const bool overriding = m_Markers.overriding.contains(pluginIndex); + const bool overridden = m_Markers.overridden.contains(pluginIndex); + const bool overwritingAux = m_Markers.overwritingAux.contains(pluginIndex); + const bool overwrittenAux = m_Markers.overwrittenAux.contains(pluginIndex); + + // the color logic looks backwards but this is what the mod list does + if (highlight) { + return Settings::instance()->containedColor(); + } else if (overridden) { + return Settings::instance()->overwritingLooseFilesColor(); + } else if (overriding) { + return Settings::instance()->overwrittenLooseFilesColor(); + } else if (overwrittenAux) { + return Settings::instance()->overwritingArchiveFilesColor(); + } else if (overwritingAux) { + return Settings::instance()->overwrittenArchiveFilesColor(); + } + } + + const auto rowIndex = index.siblingAtColumn(0); + if (model()->hasChildren(rowIndex) && !isExpanded(rowIndex)) { + std::vector<QColor> colors; + for (int i = 0; i < model()->rowCount(rowIndex); ++i) { + const auto childIndex = model()->index(i, index.column(), rowIndex); + const auto childColor = markerColor(childIndex); + if (childColor.isValid()) { + colors.push_back(childColor); + } + } + + if (colors.empty()) { + return QColor(); + } + + int r = 0, g = 0, b = 0, a = 0; + for (const auto& color : colors) { + r += color.red(); + g += color.green(); + b += color.blue(); + a += color.alpha(); + } + + return QColor( + static_cast<int>(r / colors.size()), static_cast<int>(g / colors.size()), + static_cast<int>(b / colors.size()), static_cast<int>(a / colors.size())); + } + + return QColor(); +} + +uint PluginListView::fileFlags(const QModelIndex& index) const +{ + uint flags = index.data(PluginListModel::FlagsIconRole).toUInt(); + + if (model()->hasChildren(index) && !isExpanded(index.siblingAtColumn(0))) { + for (int i = 0, count = model()->rowCount(index); i < count; ++i) { + const auto child = model()->index(i, 0, index); + flags |= child.data(PluginListModel::FlagsIconRole).toUInt(); + } + } + + return flags; +} + +uint PluginListView::conflictFlags(const QModelIndex& index) const +{ + uint flags = index.data(PluginListModel::ConflictsIconRole).toUInt(); + + if (model()->hasChildren(index) && !isExpanded(index.siblingAtColumn(0))) { + for (int i = 0, count = model()->rowCount(index); i < count; ++i) { + const auto child = model()->index(i, 0, index); + flags |= child.data(PluginListModel::ConflictsIconRole).toUInt(); + } + } + + return flags; +} + +static void visitRows(const QAbstractItemModel* model, + std::function<void(const QModelIndex&)> visit, + const QModelIndex& root = QModelIndex()) +{ + if (root.isValid()) { + visit(root); + } + + for (int i = 0, count = model->rowCount(root); i < count; ++i) { + const auto idx = model->index(i, 0, root); + visitRows(model, visit, idx); + } +} + +void PluginListView::setHighlightedOrigins(const QStringList& origins) +{ + m_Markers.highlight.clear(); + visitRows(model(), [this, &origins](auto&& idx) { + const auto origin = idx.data(PluginListModel::OriginRole).toString(); + if (origins.contains(origin)) { + m_Markers.highlight.insert(idx.data(PluginListModel::IndexRole).toUInt()); + } + }); + + viewport()->update(); + verticalScrollBar()->repaint(); +} + +void PluginListView::clearOverwriteMarkers() +{ + m_Markers.overriding.clear(); + m_Markers.overridden.clear(); + m_Markers.overwritingAux.clear(); + m_Markers.overwrittenAux.clear(); +} + +void PluginListView::updateOverwriteMarkers() +{ + QModelIndexList indexes = selectionModel()->selectedRows(); + for (const auto& idx : selectionModel()->selectedRows()) { + if (model()->hasChildren(idx) && !isExpanded(idx)) { + for (int i = 0, count = model()->rowCount(idx); i < count; ++i) { + indexes.append(model()->index(i, idx.column(), idx)); + } + } + } + + const auto insert = [](auto& dest, const auto& from) { + for (const QVariant& elem : from) { + dest.insert(elem.toUInt()); + } + }; + + clearOverwriteMarkers(); + for (const auto& idx : indexes) { + insert(m_Markers.overriding, + model()->data(idx, PluginListModel::OverridingRole).toList()); + insert(m_Markers.overridden, + model()->data(idx, PluginListModel::OverriddenRole).toList()); + insert(m_Markers.overwritingAux, + model()->data(idx, PluginListModel::OverwritingAuxRole).toList()); + insert(m_Markers.overwrittenAux, + model()->data(idx, PluginListModel::OverwrittenAuxRole).toList()); + } + + viewport()->update(); + verticalScrollBar()->repaint(); +} + +bool PluginListView::event(QEvent* event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + + if (keyEvent->modifiers() == Qt::ControlModifier && + (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)) { + + if (selectionModel()->hasSelection() && + selectionModel()->selectedRows().count() == 1) { + + QModelIndex idx = selectionModel()->currentIndex(); + emit openOriginExplorer(idx); + return true; + } + } + + bool sorted = false; + if (const auto proxy = m_SortProxy) { + sorted = proxy->sortColumn() == PluginListModel::COL_PRIORITY; + } + + if (sorted && keyEvent->modifiers() == Qt::ControlModifier && + (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down)) { + return moveSelection(keyEvent->key()); + } else if (keyEvent->key() == Qt::Key_Space) { + return toggleSelectionState(); + } + } + + return QTreeView::event(event); +} + +void PluginListView::dragMoveEvent(QDragMoveEvent* event) +{ + // HACK: dropping below an expanded item sends the same event as dropping below its + // children, so set an additional flag to signal this + if (const auto m = qobject_cast<PluginGroupProxyModel*>(model())) { + m->setDroppingBelowExpandedItem(dropIndicatorPosition() == + QAbstractItemView::BelowItem && + isExpanded(indexAt(event->position().toPoint()))); + } + + QTreeView::dragMoveEvent(event); +} + +void PluginListView::paintEvent(QPaintEvent* event) +{ + if (m_FirstPaint) { + header()->setSectionResizeMode(0, QHeaderView::Interactive); + header()->setStretchLastSection(true); + m_FirstPaint = false; + } + + QTreeView::paintEvent(event); +} + +bool PluginListView::moveSelection(int key) +{ + if (m_PluginModel == nullptr) { + return false; + } + + const auto sourceRows = + indexViewToModel(selectionModel()->selectedRows(), m_PluginModel, true); + + const auto sortOrder = m_SortProxy ? m_SortProxy->sortOrder() : Qt::DescendingOrder; + const int offset = key == Qt::Key_Up && sortOrder == Qt::AscendingOrder ? -1 : 1; + + m_PluginModel->shiftPluginsPriority(sourceRows, offset); + + return true; +} + +bool PluginListView::toggleSelectionState() +{ + if (m_PluginModel == nullptr) { + return false; + } + + const auto sourceRows = + indexViewToModel(selectionModel()->selectedRows(), m_PluginModel, false); + + m_PluginModel->toggleState(sourceRows); + + return true; +} + +QModelIndex PluginListView::indexViewToModel(const QModelIndex& index, + const QAbstractItemModel* model) const +{ + if (index.model() == model) { + return index; + } else if (const auto* const proxy = + qobject_cast<const QAbstractProxyModel*>(index.model())) { + return indexViewToModel(proxy->mapToSource(index), model); + } else { + return QModelIndex(); + } +} + +QModelIndexList PluginListView::indexViewToModel(const QModelIndexList& indices, + const QAbstractItemModel* model, + bool includeChildren) const +{ + QModelIndexList result; + + for (const auto& idx : indices) { + const auto modelIdx = indexViewToModel(idx, model); + if (modelIdx.isValid()) { + result.append(modelIdx); + } + + if (includeChildren) { + for (int row = 0, count = idx.model()->rowCount(idx); row < count; ++row) { + const auto childIdx = idx.model()->index(row, 0, idx); + const auto modelChildIdx = indexViewToModel(childIdx, model); + if (modelChildIdx.isValid()) { + result.append(modelChildIdx); + } + } + } + } + + return result; +} + +void PluginListView::onGroupRenameRequested(const QModelIndex& index, + const QString& name) +{ + if (!index.model()->hasChildren(index)) { + return; + } + + QModelIndexList sourceRows; + for (int row = 0, count = index.model()->rowCount(index); row < count; ++row) { + const auto childIndex = index.model()->index(row, 0, index); + sourceRows.append(indexViewToModel(childIndex, m_PluginModel)); + } + + const auto persistentIndex = QPersistentModelIndex(index.model()->index(0, 0, index)); + const bool expanded = isExpanded(index); + + m_PluginModel->setGroup(sourceRows, name); + + const auto newIndex = persistentIndex.parent(); + const auto newRight = newIndex.siblingAtColumn(index.model()->columnCount() - 1); + setExpanded(newIndex, expanded); + selectionModel()->select(QItemSelection(newIndex, newRight), + QItemSelectionModel::ClearAndSelect); + selectionModel()->setCurrentIndex(newIndex, QItemSelectionModel::Current); +} + +} // namespace BSPluginList |
