This commit is contained in:
sentry[bot] 2026-11-04 15:22:30 +01:00 • committed by GitHub
commit f05bdb65f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -10,7 +10,7 @@
#include <QElapsedTimer> #include <QElapsedTimer>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QFutureWatcher> #include <QFuture>
#include <QMetaObject> #include <QMetaObject>
#include <QMetaType> #include <QMetaType>
#include <QSet> #include <QSet>
@ -244,15 +244,15 @@ void VisualDeckStorageModel::startScan()
// The scan (directory walk + one stat per file) runs on a worker thread so that // 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. // constructing the widget never stalls the UI thread on a large deck folder.
auto *watcher = new QFutureWatcher<DeckScanResult>(this); // The continuation is bound to this model as its context object: it runs on the UI thread,
connect(watcher, &QFutureWatcher<DeckScanResult>::finished, this, [this, watcher, generation] { // and Qt drops it if the model is destroyed first, so no result is delivered to a dead model.
watcher->deleteLater(); QFuture<DeckScanResult> scan =
QtConcurrent::run([currentDeckPath]() -> DeckScanResult { return scanDeckDirectory(currentDeckPath); });
scan.then(this, [this, generation](const DeckScanResult &result) {
if (generation != scanGeneration) { if (generation != scanGeneration) {
return; // A newer scan started while this one was running; drop the stale result. return; // A newer scan started while this one was running; drop the stale result.
} }
const DeckScanResult result = watcher->result();
folderPaths = result.folderPaths; folderPaths = result.folderPaths;
if (result.decks.isEmpty()) { if (result.decks.isEmpty()) {
@ -268,9 +268,6 @@ void VisualDeckStorageModel::startScan()
beginLoad(row); beginLoad(row);
} }
}); });
watcher->setFuture(
QtConcurrent::run([currentDeckPath]() -> DeckScanResult { return scanDeckDirectory(currentDeckPath); }));
} }
void VisualDeckStorageModel::beginLoad(int row) void VisualDeckStorageModel::beginLoad(int row)
@ -286,32 +283,7 @@ void VisualDeckStorageModel::beginLoad(int row)
const DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(filePath); const DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(filePath);
const int generation = scanGeneration; const int generation = scanGeneration;
auto *watcher = new QFutureWatcher<std::optional<DeckLoadResult>>(this); auto load = QtConcurrent::run([filePath, fmt]() -> std::optional<DeckLoadResult> {
connect(watcher, &QFutureWatcher<std::optional<DeckLoadResult>>::finished, this,
[this, watcher, filePath, generation] {
watcher->deleteLater();
if (generation != scanGeneration) {
return; // The deck list was re-scanned while this load was running; drop the stale result.
}
// Queue the result and apply a bounded number per event loop turn so that
// finishing hundreds of loads at once cannot stall the UI thread.
const std::optional<DeckLoadResult> result = watcher->result();
PendingDeckLoad pending;
pending.filePath = filePath;
pending.generation = generation;
if (result) {
pending.ok = true;
pending.deck = std::move(result->deck);
pending.lastModified = result->lastModified;
pending.colorIdentity = std::move(result->colorIdentity);
}
pendingLoads.append(std::move(pending));
schedulePendingLoadDrain();
});
watcher->setFuture(QtConcurrent::run([filePath, fmt]() -> std::optional<DeckLoadResult> {
std::optional<LoadedDeck> deck = DeckLoader::loadFromFile(filePath, fmt, false); std::optional<LoadedDeck> deck = DeckLoader::loadFromFile(filePath, fmt, false);
if (!deck) { if (!deck) {
return std::nullopt; return std::nullopt;
@ -320,7 +292,28 @@ void VisualDeckStorageModel::beginLoad(int row)
// keep the completion handler on the UI thread cheap. // keep the completion handler on the UI thread cheap.
const QString colorIdentity = getDeckColorIdentity(deck->deckList, CardDatabaseManager::query()); const QString colorIdentity = getDeckColorIdentity(deck->deckList, CardDatabaseManager::query());
return DeckLoadResult{std::move(*deck), QFileInfo(filePath).lastModified(), colorIdentity}; return DeckLoadResult{std::move(*deck), QFileInfo(filePath).lastModified(), colorIdentity};
})); });
// Bound to this model as context: runs on the UI thread and is dropped if the model is destroyed first.
load.then(this, [this, filePath, generation](std::optional<DeckLoadResult> result) {
if (generation != scanGeneration) {
return; // The deck list was re-scanned while this load was running; drop the stale result.
}
// Queue the result and apply a bounded number per event loop turn so that
// finishing hundreds of loads at once cannot stall the UI thread.
PendingDeckLoad pending;
pending.filePath = filePath;
pending.generation = generation;
if (result) {
pending.ok = true;
pending.deck = std::move(result->deck);
pending.lastModified = result->lastModified;
pending.colorIdentity = std::move(result->colorIdentity);
}
pendingLoads.append(std::move(pending));
schedulePendingLoadDrain();
});
} }
void VisualDeckStorageModel::schedulePendingLoadDrain() void VisualDeckStorageModel::schedulePendingLoadDrain()