diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp index 3611a8c1f..dc22d4cb9 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp @@ -137,6 +137,39 @@ VisualDeckStorageModel::VisualDeckStorageModel(QObject *parent) : QAbstractListM { } +VisualDeckStorageModel::~VisualDeckStorageModel() +{ + // Invalidate every outstanding result and drop queued work. + ++scanGeneration; + pendingLoads.clear(); + drainScheduled = false; + + // Detach and destroy the watchers of still-running futures explicitly, before + // QObject teardown, so none of their finished() lambdas (which capture this) + // can fire into a half-destroyed model. Futures that have not started yet are + // cancelled; ones already running finish on the pool and their result is dropped. + const QList> watchers = std::exchange(activeWatchers, {}); + for (const QPointer &watcher : watchers) { + if (!watcher) { + continue; + } + disconnect(watcher, nullptr, this, nullptr); + watcher->cancel(); + delete watcher.data(); + } +} + +void VisualDeckStorageModel::trackWatcher(QFutureWatcherBase *watcher) +{ + activeWatchers.append(QPointer(watcher)); +} + +void VisualDeckStorageModel::forgetWatcher(QFutureWatcherBase *watcher) +{ + activeWatchers.removeIf( + [watcher](const QPointer &tracked) { return !tracked || tracked.data() == watcher; }); +} + int VisualDeckStorageModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : decks.size(); @@ -245,7 +278,9 @@ void VisualDeckStorageModel::startScan() // The scan (directory walk + one stat per file) runs on a worker thread so that // constructing the widget never stalls the UI thread on a large deck folder. auto *watcher = new QFutureWatcher(this); + trackWatcher(watcher); connect(watcher, &QFutureWatcher::finished, this, [this, watcher, generation] { + forgetWatcher(watcher); watcher->deleteLater(); if (generation != scanGeneration) { @@ -287,8 +322,10 @@ void VisualDeckStorageModel::beginLoad(int row) const int generation = scanGeneration; auto *watcher = new QFutureWatcher>(this); + trackWatcher(watcher); connect(watcher, &QFutureWatcher>::finished, this, [this, watcher, filePath, generation] { + forgetWatcher(watcher); watcher->deleteLater(); if (generation != scanGeneration) { 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 f9af6f318..9adf04083 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 @@ -17,9 +17,11 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -107,6 +109,12 @@ class VisualDeckStorageModel : public QAbstractListModel public: explicit VisualDeckStorageModel(QObject *parent = nullptr); + /** + * @brief Cancels and detaches every background scan/load still in flight, so no + * QFutureWatcher can deliver a result into (or outlive) a destroyed model. + */ + ~VisualDeckStorageModel() override; + /// @name Qt model overrides ///@{ [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; @@ -177,6 +185,8 @@ private: static void recomputeDeckMetadata(DeckPreviewData &data, bool recomputeColorIdentity = true); void reindexFilePaths(); void schedulePendingLoadDrain(); + void trackWatcher(QFutureWatcherBase *watcher); + void forgetWatcher(QFutureWatcherBase *watcher); private slots: void drainPendingLoads(); @@ -191,6 +201,7 @@ private: int scanGeneration = 0; ///< Bumped on every scan so stale results are ignored. QVector pendingLoads; ///< Finished background loads waiting to be applied. bool drainScheduled = false; ///< Whether a queued drain pass is already pending. + QList> activeWatchers; ///< Scan/load watchers whose future has not finished yet. }; #endif // VISUAL_DECK_STORAGE_MODEL_H 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 4e1d6293f..724be6cdf 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 @@ -157,6 +157,36 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare } } +VisualDeckStorageWidget::~VisualDeckStorageWidget() +{ + // Tear down in dependency order instead of relying on QObject child creation order, + // which would destroy the model first while the proxy and the deck preview widgets + // (which hold raw VisualDeckStorageModel pointers) are still alive. + if (refreshTimer) { + refreshTimer->stop(); + } + + // 1. Views: folder and deck preview widgets live inside the scroll area. + delete scrollArea; + scrollArea = nullptr; + folderWidget = nullptr; + + // 2. Proxy: detach it from the model before either is destroyed. + if (storageProxyModel) { + disconnect(storageProxyModel, nullptr, this, nullptr); + storageProxyModel->setSourceModel(nullptr); + delete storageProxyModel; + storageProxyModel = nullptr; + } + + // 3. Model last; its destructor cancels and detaches in-flight background loads. + if (storageModel) { + disconnect(storageModel, nullptr, this, nullptr); + delete storageModel; + storageModel = nullptr; + } +} + void VisualDeckStorageWidget::refreshIfPossible() { if (scrollArea->widget() != databaseLoadIndicator) { diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h index 19d16f107..ad57d4ef8 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h @@ -34,6 +34,7 @@ class VisualDeckStorageWidget final : public QWidget Q_OBJECT public: explicit VisualDeckStorageWidget(QWidget *parent); + ~VisualDeckStorageWidget() override; void refreshIfPossible(); void retranslateUi(); void setShareSelectable(bool selectable);