From 717a2deff83ea75bd18961fd8bf65ab474225809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Mon, 24 Aug 2026 19:46:19 +0200 Subject: [PATCH] [VDS] Expose filter matches as a proxy role instead of dropping rows The folder display scanned source-model rows and probed acceptance with mapFromSource(...).isValid(), reaching into both models for one answer. The proxy now keeps every row and exposes each row's search/tag/color filter result through FilterMatchRole. The folder display and the tag filter read everything off proxy indexes, and hidden previews keep their sorted position in the flow layout instead of being appended at the end. Took 11 minutes --- ...ual_deck_storage_folder_display_widget.cpp | 47 +++++++------------ ...isual_deck_storage_folder_display_widget.h | 9 ++-- .../visual_deck_storage_model.h | 12 ++++- ...l_deck_storage_sort_filter_proxy_model.cpp | 23 +++++++-- ...ual_deck_storage_sort_filter_proxy_model.h | 7 +++ .../visual_deck_storage_tag_filter_widget.cpp | 10 ++-- .../visual_deck_storage_widget.cpp | 5 +- 7 files changed, 68 insertions(+), 45 deletions(-) diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp index 3537a9b70..2ac797429 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp @@ -105,29 +105,29 @@ void VisualDeckStorageFolderDisplayWidget::continueDeckPass() passTimer.start(); auto *proxy = visualDeckStorageWidget->proxyModel(); - auto *model = visualDeckStorageWidget->model(); - const int sourceRowCount = model->rowCount(); + const int proxyRowCount = proxy->rowCount(); // Scan rows of this folder, creating missing previews, until the time budget for this // event loop turn runs out. The rest continues on the next turn. - while (deckPassRow < sourceRowCount) { + while (deckPassRow < proxyRowCount) { const int row = deckPassRow++; - const DeckPreviewData &data = model->dataForRow(row); - if (showFolders && data.folderPath != folderPath) { + const QModelIndex index = proxy->index(row, 0); + if (showFolders && index.data(VisualDeckStorageRoles::FolderPathRole).toString() != folderPath) { continue; } - deckPassPresentPaths.insert(data.filePath); + const QString filePath = index.data(VisualDeckStorageRoles::FilePathRole).toString(); + deckPassPresentPaths.insert(filePath); - DeckPreviewWidget *deckPreviewWidget = deckWidgets.value(data.filePath, nullptr); + DeckPreviewWidget *deckPreviewWidget = deckWidgets.value(filePath, nullptr); if (!deckPreviewWidget) { - deckPreviewWidget = createDeckPreviewWidget(data.filePath); + deckPreviewWidget = createDeckPreviewWidget(filePath); } - const bool accepted = proxy->mapFromSource(model->index(row, 0)).isValid(); - if (accepted == deckPreviewWidget->isHidden()) { - deckPreviewWidget->setVisible(accepted); + const bool matches = index.data(VisualDeckStorageRoles::FilterMatchRole).toBool(); + if (matches == deckPreviewWidget->isHidden()) { + deckPreviewWidget->setVisible(matches); } - if (accepted) { + if (matches) { ++visibleDeckCount; } @@ -141,7 +141,7 @@ void VisualDeckStorageFolderDisplayWidget::continueDeckPass() return; } - if (deckPassRow < sourceRowCount) { + if (deckPassRow < proxyRowCount) { QMetaObject::invokeMethod(this, &VisualDeckStorageFolderDisplayWidget::continueDeckPass, Qt::QueuedConnection); return; } @@ -152,9 +152,8 @@ void VisualDeckStorageFolderDisplayWidget::continueDeckPass() void VisualDeckStorageFolderDisplayWidget::finishDeckPass() { auto *proxy = visualDeckStorageWidget->proxyModel(); - auto *model = visualDeckStorageWidget->model(); - // Drop previews of decks that no longer exist in the source model. + // Drop previews of decks that no longer exist in the model. for (auto it = deckWidgets.begin(); it != deckWidgets.end();) { if (!deckPassPresentPaths.contains(it.key())) { flowWidget->removeWidget(it.value()); @@ -165,28 +164,16 @@ void VisualDeckStorageFolderDisplayWidget::finishDeckPass() } } - // Order the flow layout like the proxy sorts its rows. + // Order the flow layout like the proxy sorts its rows. Filtered-out decks stay part of + // the layout, hidden in their sorted place until a filter lets them through again. QStringList orderedFilePaths; orderedFilePaths.reserve(proxy->rowCount()); for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) { - const QModelIndex sourceIndex = proxy->mapToSource(proxy->index(proxyRow, 0)); - if (!sourceIndex.isValid()) { - continue; - } - const QString &filePath = model->dataForRow(sourceIndex.row()).filePath; + const QString filePath = proxy->index(proxyRow, 0).data(VisualDeckStorageRoles::FilePathRole).toString(); if (deckWidgets.contains(filePath)) { orderedFilePaths.append(filePath); } } - // Hidden previews are appended so they remain part of the layout and take their - // sorted place once a filter lets them through again. - QSet orderedSet(orderedFilePaths.cbegin(), orderedFilePaths.cend()); - for (auto it = deckWidgets.constBegin(); it != deckWidgets.constEnd(); ++it) { - if (!orderedSet.contains(it.key())) { - orderedSet.insert(it.key()); - orderedFilePaths.append(it.key()); - } - } // Re-add all widgets so the flow layout order matches the proxy order. Skipped when the // order is unchanged so that data-only updates don't invalidate the flow layout. diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h index 47370e8b8..257ce1778 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h @@ -4,9 +4,10 @@ * @brief Renders the decks of one folder of the Visual Deck Storage. * * This is a pure view: it keeps one persistent DeckPreviewWidget alive per deck - * in its folder, and shows or hides those widgets according to the accepted rows - * of the VisualDeckStorageSortFilterProxyModel. Subfolders are shown as nested - * VisualDeckStorageFolderDisplayWidgets when the "show folders" setting is enabled. + * in its folder, and shows or hides those widgets according to each row's + * FilterMatchRole in the VisualDeckStorageSortFilterProxyModel. Subfolders are + * shown as nested VisualDeckStorageFolderDisplayWidgets when the "show folders" + * setting is enabled. * * Reconciling runs as a time-budgeted chunked pass that yields to the event loop * between chunks, so scanning a large collection never stalls the ui thread. @@ -97,7 +98,7 @@ private: bool deckPassRestartRequested = false; /// Whether the first reconcile pass has run to completion at least once. bool initialPassCompleted = false; - int deckPassRow = 0; ///< Next source row to scan in the active pass. + int deckPassRow = 0; ///< Next proxy row to scan in the active pass. QSet deckPassPresentPaths; ///< File paths seen so far in the active pass. }; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h index e6fb31f50..e47edd052 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h @@ -38,7 +38,14 @@ enum LastModifiedRole, /**< QDateTime of the deck file's last modification. */ LastLoadedRole, /**< QDateTime when the deck was last loaded from the file. */ BannerCardNameRole, /**< Name of the deck's banner card. */ - BannerCardProviderIdRole /**< Provider id of the deck's banner card. */ + BannerCardProviderIdRole, /**< Provider id of the deck's banner card. */ + /** + * @brief Whether the row passes the proxy's current search / tag / color filters. + * + * Not served by this model, but by VisualDeckStorageSortFilterProxyModel on top + * of it. Declared here so every role read through a proxy index stays unique. + */ + FilterMatchRole }; } // namespace VisualDeckStorageRoles @@ -82,7 +89,8 @@ struct PendingDeckLoad * @brief The list model backing the Visual Deck Storage widget tree. * * Rows are in filesystem scan order; ordering and filtering are handled by - * VisualDeckStorageSortFilterProxyModel on top of this model. + * VisualDeckStorageSortFilterProxyModel on top of this model. The proxy keeps + * every row and exposes each row's filter result through its FilterMatchRole. */ class VisualDeckStorageModel : public QAbstractListModel { diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp index 8968f6cb3..c05da1cb3 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp @@ -104,12 +104,21 @@ void VisualDeckStorageSortFilterProxyModel::resort() sort(0); } -bool VisualDeckStorageSortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +QVariant VisualDeckStorageSortFilterProxyModel::data(const QModelIndex &index, int role) const { - if (sourceParent.isValid()) { - return true; + if (role == VisualDeckStorageRoles::FilterMatchRole) { + if (!index.isValid()) { + return true; + } + const QModelIndex sourceIndex = mapToSource(index); + return rowMatches(sourceIndex.row()); } + return QSortFilterProxyModel::data(index, role); +} + +bool VisualDeckStorageSortFilterProxyModel::rowMatches(int sourceRow) const +{ // If the match lists aren't sized to the current model yet, don't hide anything. if (sourceRow < 0 || sourceRow >= searchMatches.size() || sourceRow >= tagMatches.size() || sourceRow >= colorMatches.size()) { @@ -119,6 +128,14 @@ bool VisualDeckStorageSortFilterProxyModel::filterAcceptsRow(int sourceRow, cons return searchMatches.at(sourceRow) && tagMatches.at(sourceRow) && colorMatches.at(sourceRow); } +bool VisualDeckStorageSortFilterProxyModel::filterAcceptsRow(int /*sourceRow*/, + const QModelIndex & /*sourceParent*/) const +{ + // Rows are never dropped: the filter result is exposed per row through + // FilterMatchRole, so views can keep their widgets alive and just hide them. + return true; +} + bool VisualDeckStorageSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { const auto *source = deckSourceModel(); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h index d2842a02f..7e771f6a9 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h @@ -6,6 +6,10 @@ * Owns all search / tag / color filter state and the sort order. Filtering is * evaluated against the model's data (never against widgets), so it can run * before any view exists and re-evaluate whenever deck data finishes loading. + * + * Rows are never removed by filtering. Instead, every row carries the + * FilterMatchRole, which views read to show or hide their widgets while keeping + * them alive; all rows stay in the proxy so they keep their sorted position. */ #ifndef VISUAL_DECK_STORAGE_SORT_FILTER_PROXY_MODEL_H @@ -47,6 +51,8 @@ public: explicit VisualDeckStorageSortFilterProxyModel(QObject *parent = nullptr); + [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; + void setSourceModel(QAbstractItemModel *model) override; /// @name Filter input setters (each re-evaluates the affected matches) @@ -77,6 +83,7 @@ protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; private: + [[nodiscard]] bool rowMatches(int sourceRow) const; void resizeMatchLists(); void updateSearchMatches(); void updateTagMatches(); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp index 33740e01a..ba52cf8e9 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp @@ -2,6 +2,8 @@ #include "../general/layout_containers/flow_widget.h" #include "deck_preview/deck_preview_tag_display_widget.h" +#include "visual_deck_storage_model.h" +#include "visual_deck_storage_sort_filter_proxy_model.h" #include "visual_deck_storage_widget.h" #include @@ -36,14 +38,14 @@ QSet VisualDeckStorageTagFilterWidget::gatherAllTags() const { QSet allTags; auto *proxy = parent->proxyModel(); - auto *model = parent->model(); for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) { - const QModelIndex sourceIndex = proxy->mapToSource(proxy->index(proxyRow, 0)); - if (!sourceIndex.isValid()) { + const QModelIndex index = proxy->index(proxyRow, 0); + if (!index.data(VisualDeckStorageRoles::FilterMatchRole).toBool()) { continue; } - for (const QString &tag : model->dataForRow(sourceIndex.row()).tags) { + const QStringList deckTags = index.data(VisualDeckStorageRoles::TagsRole).toStringList(); + for (const QString &tag : deckTags) { allTags.insert(tag); } } diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp index a4b504280..acb0dcab2 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp @@ -19,8 +19,9 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(parent) { - // The model and proxy own all deck data, sorting and filtering. The view widgets below only - // display the proxy's accepted rows, so nothing touches the filesystem outside the model. + // The model and proxy own all deck data, sorting and filtering. The view widgets below + // only display the proxy's rows and their FilterMatchRole, so nothing touches the + // filesystem outside the model. storageModel = new VisualDeckStorageModel(this); storageProxyModel = new VisualDeckStorageSortFilterProxyModel(this); storageProxyModel->setSourceModel(storageModel);