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);