mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-28 00:42:19 -07:00
Hide widgets instead of destroying, go back to signals, rename for consistency.
Took 13 minutes Took 4 seconds Took 26 minutes Took 5 seconds # Commit time for manual adjustment: # Took 9 minutes
This commit is contained in:
parent
7bbee72ec9
commit
b6a7f6d52a
12 changed files with 305 additions and 135 deletions
|
|
@ -6,7 +6,7 @@
|
|||
#include <QSet>
|
||||
|
||||
DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent)
|
||||
: QWidget(parent), layout(new QHBoxLayout(this)), visualDeckStorageWidget(parent)
|
||||
: QWidget(parent), layout(new QHBoxLayout(this))
|
||||
{
|
||||
setLayout(layout);
|
||||
layout->setSpacing(5);
|
||||
|
|
@ -55,9 +55,9 @@ void DeckPreviewColorIdentityFilterWidget::retranslateUi()
|
|||
}
|
||||
|
||||
/**
|
||||
* @brief Pushes the current filter state to the proxy model.
|
||||
* @brief The colors that are currently toggled on.
|
||||
*/
|
||||
void DeckPreviewColorIdentityFilterWidget::applyFilter()
|
||||
QSet<QChar> DeckPreviewColorIdentityFilterWidget::getActiveColors() const
|
||||
{
|
||||
QSet<QChar> activeColorSet;
|
||||
for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) {
|
||||
|
|
@ -65,15 +65,13 @@ void DeckPreviewColorIdentityFilterWidget::applyFilter()
|
|||
activeColorSet.insert(it.key());
|
||||
}
|
||||
}
|
||||
|
||||
// The VDS owns the proxy model, which exists for the widget's whole lifetime.
|
||||
visualDeckStorageWidget->proxyModel()->setColorFilter(filterMode, activeColorSet);
|
||||
return activeColorSet;
|
||||
}
|
||||
|
||||
void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar color, bool active)
|
||||
{
|
||||
activeColors[color] = active;
|
||||
applyFilter();
|
||||
emit activeColorsChanged();
|
||||
}
|
||||
|
||||
void DeckPreviewColorIdentityFilterWidget::updateFilterMode()
|
||||
|
|
@ -92,5 +90,5 @@ void DeckPreviewColorIdentityFilterWidget::updateFilterMode()
|
|||
}
|
||||
|
||||
retranslateUi(); // Update the button text
|
||||
applyFilter();
|
||||
emit filterModeChanged(filterMode);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include <QHBoxLayout>
|
||||
#include <QMap>
|
||||
#include <QPushButton>
|
||||
#include <QSet>
|
||||
#include <QWidget>
|
||||
|
||||
class VisualDeckStorageWidget;
|
||||
|
|
@ -23,18 +24,40 @@ public:
|
|||
explicit DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent);
|
||||
void retranslateUi();
|
||||
|
||||
/**
|
||||
* @brief The currently active color identity filter mode.
|
||||
*/
|
||||
[[nodiscard]] VisualDeckStorageSortFilterProxyModel::FilterMode getFilterMode() const
|
||||
{
|
||||
return filterMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The colors that are currently toggled on.
|
||||
*/
|
||||
[[nodiscard]] QSet<QChar> getActiveColors() const;
|
||||
|
||||
signals:
|
||||
/**
|
||||
* Emitted when the set of active colors changed due to user interaction.
|
||||
*/
|
||||
void activeColorsChanged();
|
||||
|
||||
/**
|
||||
* Emitted when the user cycles the color identity filter mode.
|
||||
* @param mode The new filter mode.
|
||||
*/
|
||||
void filterModeChanged(VisualDeckStorageSortFilterProxyModel::FilterMode mode);
|
||||
|
||||
private slots:
|
||||
void handleColorToggled(QChar color, bool active);
|
||||
void updateFilterMode();
|
||||
|
||||
private:
|
||||
void applyFilter();
|
||||
|
||||
QHBoxLayout *layout;
|
||||
QPushButton *toggleButton;
|
||||
QMap<QChar, bool> activeColors;
|
||||
VisualDeckStorageSortFilterProxyModel::FilterMode filterMode = VisualDeckStorageSortFilterProxyModel::Includes;
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget; ///< The VDS owning the proxy model.
|
||||
};
|
||||
|
||||
#endif // DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -52,12 +52,12 @@ void DeckPreviewDeckTagsDisplayWidget::refreshTags()
|
|||
|
||||
void DeckPreviewDeckTagsDisplayWidget::setKnownTagsProvider(const std::function<QStringList()> &provider)
|
||||
{
|
||||
knownTagsProvider_ = provider;
|
||||
knownTagsProvider = provider;
|
||||
}
|
||||
|
||||
void DeckPreviewDeckTagsDisplayWidget::setConversionPromptHandler(const std::function<bool()> &handler)
|
||||
{
|
||||
conversionPromptHandler_ = handler;
|
||||
conversionPromptHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -99,11 +99,11 @@ static QStringList findAllKnownTags()
|
|||
void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
||||
{
|
||||
// The deck editor path has no conversion prompt; the VDS path registers one.
|
||||
if (conversionPromptHandler_ && !conversionPromptHandler_()) {
|
||||
if (conversionPromptHandler && !conversionPromptHandler()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QStringList knownTags = knownTagsProvider_ ? knownTagsProvider_() : findAllKnownTags();
|
||||
const QStringList knownTags = knownTagsProvider ? knownTagsProvider() : findAllKnownTags();
|
||||
execTagDialog(knownTags);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ class DeckPreviewDeckTagsDisplayWidget : public QWidget
|
|||
|
||||
QStringList currentTags;
|
||||
FlowWidget *flowWidget;
|
||||
std::function<QStringList()> knownTagsProvider_;
|
||||
std::function<bool()> conversionPromptHandler_;
|
||||
std::function<QStringList()> knownTagsProvider;
|
||||
std::function<bool()> conversionPromptHandler;
|
||||
|
||||
public:
|
||||
explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, const QStringList &_tags = {});
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@
|
|||
|
||||
DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
||||
VisualDeckStorageWidget *_visualDeckStorageWidget,
|
||||
VisualDeckStorageModel *model,
|
||||
VisualDeckStorageModel *_model,
|
||||
const QString &_filePath)
|
||||
: QWidget(_parent), visualDeckStorageWidget(_visualDeckStorageWidget), model_(model), filePath(_filePath)
|
||||
: QWidget(_parent), visualDeckStorageWidget(_visualDeckStorageWidget), model(_model), filePath(_filePath)
|
||||
{
|
||||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
|
@ -83,7 +83,16 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
|||
layout->addWidget(bannerCardLabel);
|
||||
layout->addWidget(bannerCardComboBox);
|
||||
|
||||
connect(model_, &QAbstractItemModel::dataChanged, this, &DeckPreviewWidget::syncFromModel);
|
||||
// Only re-sync when this widget's own row changed. Without the row check, every
|
||||
// finished deck load would trigger a full resync (card db lookup + combo rebuild)
|
||||
// in every preview widget.
|
||||
connect(model, &QAbstractItemModel::dataChanged, this,
|
||||
[this](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
|
||||
const int r = row();
|
||||
if (r >= topLeft.row() && r <= bottomRight.row()) {
|
||||
syncFromModel();
|
||||
}
|
||||
});
|
||||
|
||||
retranslateUi();
|
||||
syncFromModel();
|
||||
|
|
@ -119,7 +128,7 @@ void DeckPreviewWidget::enterEvent(QEnterEvent *event)
|
|||
*/
|
||||
int DeckPreviewWidget::row() const
|
||||
{
|
||||
return model_->rowForFilePath(filePath);
|
||||
return model->rowForFilePath(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -131,7 +140,7 @@ QString DeckPreviewWidget::getDisplayName() const
|
|||
if (r == -1) {
|
||||
return {};
|
||||
}
|
||||
return model_->dataForRow(r).displayName;
|
||||
return model->dataForRow(r).displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -140,11 +149,11 @@ QString DeckPreviewWidget::getDisplayName() const
|
|||
void DeckPreviewWidget::reloadIfModified()
|
||||
{
|
||||
const int r = row();
|
||||
if (r == -1 || !model_->dataForRow(r).loadSucceeded) {
|
||||
if (r == -1 || !model->dataForRow(r).loadSucceeded) {
|
||||
return;
|
||||
}
|
||||
|
||||
model_->reloadIfModified(r);
|
||||
model->reloadIfModified(r);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -157,7 +166,7 @@ void DeckPreviewWidget::syncFromModel()
|
|||
return;
|
||||
}
|
||||
|
||||
const DeckPreviewData &data = model_->dataForRow(r);
|
||||
const DeckPreviewData &data = model->dataForRow(r);
|
||||
filePath = data.filePath;
|
||||
|
||||
const CardRef bannerCardRef = data.deck.deckList.getBannerCard();
|
||||
|
|
@ -246,7 +255,7 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString ¤tText)
|
|||
|
||||
const int r = row();
|
||||
if (r != -1) {
|
||||
const DeckList &deckList = model_->dataForRow(r).deck.deckList;
|
||||
const DeckList &deckList = model->dataForRow(r).deck.deckList;
|
||||
const QList<const DecklistCardNode *> cardsInDeck = deckList.getCardNodes();
|
||||
|
||||
for (auto currentCard : cardsInDeck) {
|
||||
|
|
@ -265,16 +274,16 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString ¤tText)
|
|||
|
||||
// This is *slightly* more performant than using addItem in a loop.
|
||||
|
||||
QStandardItemModel *model = new QStandardItemModel(pairList.size(), 1, bannerCardComboBox);
|
||||
QStandardItemModel *comboModel = new QStandardItemModel(pairList.size(), 1, bannerCardComboBox);
|
||||
|
||||
int row = 0;
|
||||
for (const auto &pair : pairList) {
|
||||
QStandardItem *item = new QStandardItem(pair.first);
|
||||
item->setData(QVariant::fromValue(pair), Qt::UserRole);
|
||||
model->setItem(row++, 0, item);
|
||||
comboModel->setItem(row++, 0, item);
|
||||
}
|
||||
|
||||
bannerCardComboBox->setModel(model);
|
||||
bannerCardComboBox->setModel(comboModel);
|
||||
|
||||
// Try to restore the previous selection by finding the currentText
|
||||
int restoredIndex = bannerCardComboBox->findText(currentText);
|
||||
|
|
@ -283,7 +292,7 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString ¤tText)
|
|||
} else {
|
||||
// Add a placeholder "-" and set it as the current selection
|
||||
const QString currentBannerCardName =
|
||||
r == -1 ? QString() : model_->dataForRow(r).deck.deckList.getBannerCard().name;
|
||||
r == -1 ? QString() : model->dataForRow(r).deck.deckList.getBannerCard().name;
|
||||
int bannerIndex = bannerCardComboBox->findText(currentBannerCardName);
|
||||
if (bannerIndex != -1) {
|
||||
bannerCardComboBox->setCurrentIndex(bannerIndex);
|
||||
|
|
@ -306,7 +315,7 @@ void DeckPreviewWidget::setBannerCard(int /* changedIndex */)
|
|||
if (r == -1) {
|
||||
return;
|
||||
}
|
||||
model_->setBannerCard(r, cardRef);
|
||||
model->setBannerCard(r, cardRef);
|
||||
bannerCardDisplayWidget->setCard(CardDatabaseManager::query()->getCard(cardRef));
|
||||
}
|
||||
|
||||
|
|
@ -330,7 +339,7 @@ void DeckPreviewWidget::setTags(const QStringList &tags)
|
|||
{
|
||||
const int r = row();
|
||||
if (r != -1) {
|
||||
model_->setTags(r, tags);
|
||||
model->setTags(r, tags);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -343,7 +352,7 @@ QMenu *DeckPreviewWidget::createRightClickMenu()
|
|||
|
||||
connect(menu->addAction(tr("Open in deck editor")), &QAction::triggered, this, [this, r] {
|
||||
if (r != -1) {
|
||||
emit openDeckEditor(model_->deckForRow(r));
|
||||
emit openDeckEditor(model->deckForRow(r));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -360,22 +369,22 @@ QMenu *DeckPreviewWidget::createRightClickMenu()
|
|||
|
||||
connect(saveToClipboardMenu->addAction(tr("Annotated")), &QAction::triggered, this, [this, r] {
|
||||
if (r != -1) {
|
||||
DeckLoader::saveToClipboard(model_->dataForRow(r).deck.deckList, true, true);
|
||||
DeckLoader::saveToClipboard(model->dataForRow(r).deck.deckList, true, true);
|
||||
}
|
||||
});
|
||||
connect(saveToClipboardMenu->addAction(tr("Annotated (No set info)")), &QAction::triggered, this, [this, r] {
|
||||
if (r != -1) {
|
||||
DeckLoader::saveToClipboard(model_->dataForRow(r).deck.deckList, true, false);
|
||||
DeckLoader::saveToClipboard(model->dataForRow(r).deck.deckList, true, false);
|
||||
}
|
||||
});
|
||||
connect(saveToClipboardMenu->addAction(tr("Not Annotated")), &QAction::triggered, this, [this, r] {
|
||||
if (r != -1) {
|
||||
DeckLoader::saveToClipboard(model_->dataForRow(r).deck.deckList, false, true);
|
||||
DeckLoader::saveToClipboard(model->dataForRow(r).deck.deckList, false, true);
|
||||
}
|
||||
});
|
||||
connect(saveToClipboardMenu->addAction(tr("Not Annotated (No set info)")), &QAction::triggered, this, [this, r] {
|
||||
if (r != -1) {
|
||||
DeckLoader::saveToClipboard(model_->dataForRow(r).deck.deckList, false, false);
|
||||
DeckLoader::saveToClipboard(model->dataForRow(r).deck.deckList, false, false);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -418,7 +427,7 @@ void DeckPreviewWidget::actRenameDeck()
|
|||
}
|
||||
|
||||
// read input
|
||||
const QString oldName = model_->dataForRow(r).deckName;
|
||||
const QString oldName = model->dataForRow(r).deckName;
|
||||
|
||||
bool ok;
|
||||
QString newName = QInputDialog::getText(this, tr("Rename deck"), tr("New name:"), QLineEdit::Normal, oldName, &ok);
|
||||
|
|
@ -427,7 +436,7 @@ void DeckPreviewWidget::actRenameDeck()
|
|||
}
|
||||
|
||||
// write change
|
||||
model_->renameDeck(r, newName);
|
||||
model->renameDeck(r, newName);
|
||||
|
||||
// The banner card text updates via the model's dataChanged signal.
|
||||
}
|
||||
|
|
@ -450,7 +459,7 @@ void DeckPreviewWidget::actRenameFile()
|
|||
}
|
||||
|
||||
// write change
|
||||
if (!model_->renameFile(r, newName)) {
|
||||
if (!model->renameFile(r, newName)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Rename failed"));
|
||||
}
|
||||
|
||||
|
|
@ -472,7 +481,7 @@ void DeckPreviewWidget::actDeleteFile()
|
|||
}
|
||||
|
||||
// write change
|
||||
if (!model_->deleteFile(r)) {
|
||||
if (!model->deleteFile(r)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Delete failed"));
|
||||
}
|
||||
|
||||
|
|
@ -515,7 +524,7 @@ bool DeckPreviewWidget::promptFileConversionIfRequired()
|
|||
return false;
|
||||
}
|
||||
|
||||
model_->convertToCockatriceFormat(row());
|
||||
model->convertToCockatriceFormat(row());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -534,7 +543,7 @@ bool DeckPreviewWidget::promptFileConversionIfRequired()
|
|||
return false;
|
||||
}
|
||||
|
||||
model_->convertToCockatriceFormat(row());
|
||||
model->convertToCockatriceFormat(row());
|
||||
|
||||
if (conversionDialog.dontAskAgain()) {
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class DeckPreviewWidget final : public QWidget
|
|||
public:
|
||||
explicit DeckPreviewWidget(QWidget *parent,
|
||||
VisualDeckStorageWidget *_visualDeckStorageWidget,
|
||||
VisualDeckStorageModel *model,
|
||||
VisualDeckStorageModel *_model,
|
||||
const QString &_filePath);
|
||||
void retranslateUi();
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ private:
|
|||
void actDeleteFile();
|
||||
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
VisualDeckStorageModel *model_;
|
||||
VisualDeckStorageModel *model;
|
||||
QString filePath;
|
||||
QVBoxLayout *layout;
|
||||
ColorIdentityWidget *colorIdentityWidget;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "visual_deck_storage_sort_filter_proxy_model.h"
|
||||
#include "visual_deck_storage_widget.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QSet>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
|
@ -59,55 +60,103 @@ VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget(
|
|||
reconcileTimer->setInterval(150);
|
||||
connect(reconcileTimer, &QTimer::timeout, this, &VisualDeckStorageFolderDisplayWidget::reconcile);
|
||||
|
||||
reconcile();
|
||||
// Building the whole folder subtree synchronously here would stall the ui thread on large
|
||||
// collections, so the first reconcile runs as a chunked pass on later event loop turns.
|
||||
scheduleReconcile();
|
||||
}
|
||||
|
||||
void VisualDeckStorageFolderDisplayWidget::scheduleReconcile()
|
||||
{
|
||||
if (deckPassActive) {
|
||||
// The active pass may be scanning stale model state, so restart it from a clean
|
||||
// slate once the current chunk yields.
|
||||
deckPassRestartRequested = true;
|
||||
return;
|
||||
}
|
||||
reconcileTimer->start();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Re-reads the proxy and rebuilds the deck previews and subfolder widgets to match.
|
||||
* @brief Starts a new chunked scan of the source model, yielding to the event loop between chunks.
|
||||
*/
|
||||
void VisualDeckStorageFolderDisplayWidget::reconcile()
|
||||
{
|
||||
reconcileDecks();
|
||||
createSubFolderWidgets();
|
||||
refreshVisibility();
|
||||
beginDeckPass();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reconciles the deck preview widgets in the flow layout with the accepted proxy rows.
|
||||
*
|
||||
* Decks are keyed by file path so that already-created preview widgets are reused
|
||||
* (their banner combo box, tag editor state, etc. is preserved) whenever possible.
|
||||
*/
|
||||
void VisualDeckStorageFolderDisplayWidget::reconcileDecks()
|
||||
void VisualDeckStorageFolderDisplayWidget::beginDeckPass()
|
||||
{
|
||||
deckPassActive = true;
|
||||
deckPassRestartRequested = false;
|
||||
deckPassRow = 0;
|
||||
visibleDeckCount = 0;
|
||||
deckPassPresentPaths.clear();
|
||||
|
||||
continueDeckPass();
|
||||
}
|
||||
|
||||
void VisualDeckStorageFolderDisplayWidget::continueDeckPass()
|
||||
{
|
||||
if (!deckPassActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
QElapsedTimer passTimer;
|
||||
passTimer.start();
|
||||
|
||||
auto *proxy = visualDeckStorageWidget->proxyModel();
|
||||
auto *model = visualDeckStorageWidget->model();
|
||||
const int sourceRowCount = model->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) {
|
||||
const int row = deckPassRow++;
|
||||
const DeckPreviewData &data = model->dataForRow(row);
|
||||
if (showFolders && data.folderPath != folderPath) {
|
||||
continue;
|
||||
}
|
||||
deckPassPresentPaths.insert(data.filePath);
|
||||
|
||||
DeckPreviewWidget *deckPreviewWidget = deckWidgets.value(data.filePath, nullptr);
|
||||
if (!deckPreviewWidget) {
|
||||
deckPreviewWidget = createDeckPreviewWidget(data.filePath);
|
||||
}
|
||||
|
||||
const bool accepted = proxy->mapFromSource(model->index(row, 0)).isValid();
|
||||
if (accepted == deckPreviewWidget->isHidden()) {
|
||||
deckPreviewWidget->setVisible(accepted);
|
||||
}
|
||||
if (accepted) {
|
||||
++visibleDeckCount;
|
||||
}
|
||||
|
||||
if (passTimer.elapsed() >= DECK_PASS_TIME_BUDGET_MS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (deckPassRestartRequested) {
|
||||
beginDeckPass();
|
||||
return;
|
||||
}
|
||||
|
||||
if (deckPassRow < sourceRowCount) {
|
||||
QMetaObject::invokeMethod(this, &VisualDeckStorageFolderDisplayWidget::continueDeckPass, Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
finishDeckPass();
|
||||
}
|
||||
|
||||
void VisualDeckStorageFolderDisplayWidget::finishDeckPass()
|
||||
{
|
||||
auto *proxy = visualDeckStorageWidget->proxyModel();
|
||||
auto *model = visualDeckStorageWidget->model();
|
||||
|
||||
// Collect the file paths this folder should display, in proxy (sorted) order.
|
||||
QStringList desiredOrder;
|
||||
for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) {
|
||||
const QModelIndex sourceIndex = proxy->mapToSource(proxy->index(proxyRow, 0));
|
||||
if (!sourceIndex.isValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const DeckPreviewData &data = model->dataForRow(sourceIndex.row());
|
||||
if (showFolders && data.folderPath != folderPath) {
|
||||
continue;
|
||||
}
|
||||
desiredOrder.append(data.filePath);
|
||||
}
|
||||
|
||||
const QSet<QString> desiredSet(desiredOrder.cbegin(), desiredOrder.cend());
|
||||
|
||||
// Drop previews of decks that no longer match.
|
||||
// Drop previews of decks that no longer exist in the source model.
|
||||
for (auto it = deckWidgets.begin(); it != deckWidgets.end();) {
|
||||
if (!desiredSet.contains(it.key())) {
|
||||
if (!deckPassPresentPaths.contains(it.key())) {
|
||||
flowWidget->removeWidget(it.value());
|
||||
it.value()->deleteLater();
|
||||
it = deckWidgets.erase(it);
|
||||
|
|
@ -116,37 +165,69 @@ void VisualDeckStorageFolderDisplayWidget::reconcileDecks()
|
|||
}
|
||||
}
|
||||
|
||||
// Create or reuse the preview widgets in the desired order.
|
||||
QList<DeckPreviewWidget *> orderedWidgets;
|
||||
orderedWidgets.reserve(desiredOrder.size());
|
||||
for (const QString &filePath : desiredOrder) {
|
||||
DeckPreviewWidget *deckPreviewWidget = deckWidgets.value(filePath, nullptr);
|
||||
if (!deckPreviewWidget) {
|
||||
deckPreviewWidget = new DeckPreviewWidget(flowWidget, visualDeckStorageWidget, model, filePath);
|
||||
connect(deckPreviewWidget, &DeckPreviewWidget::deckLoadRequested, visualDeckStorageWidget,
|
||||
&VisualDeckStorageWidget::deckLoadRequested);
|
||||
connect(deckPreviewWidget, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget,
|
||||
&VisualDeckStorageWidget::openDeckEditor);
|
||||
connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged,
|
||||
deckPreviewWidget->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor);
|
||||
deckPreviewWidget->bannerCardDisplayWidget->setScaleFactor(
|
||||
visualDeckStorageWidget->settings()->getCardSize());
|
||||
deckWidgets.insert(filePath, deckPreviewWidget);
|
||||
// Order the flow layout like the proxy sorts its rows.
|
||||
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;
|
||||
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<QString> 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());
|
||||
}
|
||||
orderedWidgets.append(deckPreviewWidget);
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (desiredOrder != lastOrderedFilePaths) {
|
||||
for (DeckPreviewWidget *deckPreviewWidget : orderedWidgets) {
|
||||
flowWidget->removeWidget(deckPreviewWidget);
|
||||
if (orderedFilePaths != lastOrderedFilePaths) {
|
||||
for (const QString &filePath : orderedFilePaths) {
|
||||
flowWidget->removeWidget(deckWidgets.value(filePath));
|
||||
}
|
||||
for (DeckPreviewWidget *deckPreviewWidget : orderedWidgets) {
|
||||
flowWidget->addWidget(deckPreviewWidget);
|
||||
for (const QString &filePath : orderedFilePaths) {
|
||||
flowWidget->addWidget(deckWidgets.value(filePath));
|
||||
}
|
||||
lastOrderedFilePaths = desiredOrder;
|
||||
lastOrderedFilePaths = orderedFilePaths;
|
||||
}
|
||||
|
||||
createSubFolderWidgets();
|
||||
|
||||
// Mark completion before evaluating visibility so this pass's own numbers decide whether
|
||||
// the folder has content. The flag only guards evaluations made *during* a build.
|
||||
deckPassActive = false;
|
||||
initialPassCompleted = true;
|
||||
|
||||
refreshVisibility();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates a deck preview widget and wires it up to the storage widget.
|
||||
*
|
||||
* @param filePath The absolute path of the deck file to preview.
|
||||
*/
|
||||
DeckPreviewWidget *VisualDeckStorageFolderDisplayWidget::createDeckPreviewWidget(const QString &filePath)
|
||||
{
|
||||
auto *deckPreviewWidget =
|
||||
new DeckPreviewWidget(flowWidget, visualDeckStorageWidget, visualDeckStorageWidget->model(), filePath);
|
||||
connect(deckPreviewWidget, &DeckPreviewWidget::deckLoadRequested, visualDeckStorageWidget,
|
||||
&VisualDeckStorageWidget::deckLoadRequested);
|
||||
connect(deckPreviewWidget, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget,
|
||||
&VisualDeckStorageWidget::openDeckEditor);
|
||||
connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged,
|
||||
deckPreviewWidget->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor);
|
||||
deckPreviewWidget->bannerCardDisplayWidget->setScaleFactor(visualDeckStorageWidget->settings()->getCardSize());
|
||||
deckWidgets.insert(filePath, deckPreviewWidget);
|
||||
return deckPreviewWidget;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -200,7 +281,7 @@ void VisualDeckStorageFolderDisplayWidget::updateShowFolders(bool enabled)
|
|||
subFolderWidgets.clear();
|
||||
}
|
||||
|
||||
reconcile();
|
||||
scheduleReconcile();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -218,10 +299,13 @@ void VisualDeckStorageFolderDisplayWidget::refreshVisibility()
|
|||
|
||||
/**
|
||||
* @brief Whether this folder shows any deck previews or has any visible subfolder.
|
||||
*
|
||||
* While the first pass is still building, the folder counts as having content so it
|
||||
* doesn't flicker or hide prematurely before its previews have been created.
|
||||
*/
|
||||
bool VisualDeckStorageFolderDisplayWidget::hasContent() const
|
||||
{
|
||||
if (!deckWidgets.isEmpty()) {
|
||||
if (!initialPassCompleted || visibleDeckCount > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,17 +3,20 @@
|
|||
* @ingroup VisualDeckStorageWidgets
|
||||
* @brief Renders the decks of one folder of the Visual Deck Storage.
|
||||
*
|
||||
* This is a pure view: it reads the accepted rows of the
|
||||
* VisualDeckStorageSortFilterProxyModel whose folder matches this widget's
|
||||
* folder path, and keeps a set of child DeckPreviewWidgets in sync with those
|
||||
* rows. Subfolders are shown as nested VisualDeckStorageFolderDisplayWidgets
|
||||
* when the "show folders" setting is enabled.
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H
|
||||
#define VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
|
|
@ -36,8 +39,8 @@ public:
|
|||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Re-reads the proxy and rebuilds the deck previews and subfolder
|
||||
* widgets to match.
|
||||
* @brief Starts a new chunked reconcile pass that re-reads the proxy and rebuilds
|
||||
* the deck previews and subfolder widgets to match.
|
||||
*/
|
||||
void reconcile();
|
||||
|
||||
|
|
@ -56,14 +59,26 @@ signals:
|
|||
void contentVisibilityChanged();
|
||||
|
||||
private:
|
||||
void reconcileDecks();
|
||||
void beginDeckPass();
|
||||
void continueDeckPass();
|
||||
void finishDeckPass();
|
||||
[[nodiscard]] DeckPreviewWidget *createDeckPreviewWidget(const QString &filePath);
|
||||
void createSubFolderWidgets();
|
||||
void refreshVisibility();
|
||||
[[nodiscard]] bool hasContent() const;
|
||||
[[nodiscard]] QStringList childFolderPaths() const;
|
||||
|
||||
/**
|
||||
* @brief The maximum time in milliseconds spent creating deck previews per event loop turn.
|
||||
*
|
||||
* Creating all previews of a large folder at once blocks the ui thread for hundreds of
|
||||
* milliseconds, so the pass is split into chunks that yield to the event loop instead.
|
||||
*/
|
||||
static constexpr int DECK_PASS_TIME_BUDGET_MS = 20;
|
||||
|
||||
bool showFolders;
|
||||
QString folderPath; ///< Path relative to the deck folder; empty for the root folder.
|
||||
QString folderPath; ///< Path relative to the deck folder, empty for the root folder.
|
||||
int visibleDeckCount = 0; ///< The number of this folder's deck previews not filtered out.
|
||||
QVBoxLayout *layout;
|
||||
QWidget *container;
|
||||
QVBoxLayout *containerLayout;
|
||||
|
|
@ -74,6 +89,16 @@ private:
|
|||
QHash<QString, VisualDeckStorageFolderDisplayWidget *> subFolderWidgets; ///< Folder path -> subfolder widget.
|
||||
QTimer *reconcileTimer = nullptr; ///< Coalesces proxy change bursts.
|
||||
QStringList lastOrderedFilePaths; ///< The deck order last applied to the flow layout.
|
||||
|
||||
/// Whether a chunked reconcile pass is currently running.
|
||||
bool deckPassActive = false;
|
||||
/// Set when the model changes mid-pass. Discards progress and restarts the scan once
|
||||
/// the current chunk finishes so the pass always converges on the latest model state.
|
||||
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.
|
||||
QSet<QString> deckPassPresentPaths; ///< File paths seen so far in the active pass.
|
||||
};
|
||||
|
||||
#endif // VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
#include "../../../filters/syntax_help.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "visual_deck_storage_widget.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QTimer>
|
||||
|
|
@ -13,9 +12,9 @@
|
|||
* Provides a search bar that allows users to search decks by filename or search
|
||||
* expression, with a debounced timer to trigger the search after the user stops typing.
|
||||
*
|
||||
* @param parent The VisualDeckStorageWidget owning the search.
|
||||
* @param parent The parent widget.
|
||||
*/
|
||||
VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent) : parent(parent)
|
||||
VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
|
@ -38,6 +37,5 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWi
|
|||
searchDebounceTimer->start(300); // 300ms debounce
|
||||
});
|
||||
|
||||
connect(searchDebounceTimer, &QTimer::timeout, this,
|
||||
[this] { this->parent->proxyModel()->setSearchText(searchBar->text()); });
|
||||
connect(searchDebounceTimer, &QTimer::timeout, this, [this] { emit searchTextChanged(searchBar->text()); });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,17 +11,23 @@
|
|||
#include <QWidget>
|
||||
|
||||
class QTimer;
|
||||
class VisualDeckStorageWidget;
|
||||
|
||||
class VisualDeckStorageSearchWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent);
|
||||
explicit VisualDeckStorageSearchWidget(QWidget *parent);
|
||||
|
||||
signals:
|
||||
/**
|
||||
* Emitted once the debounce timer fires after the user stopped typing.
|
||||
* @param text The current contents of the search bar.
|
||||
*/
|
||||
void searchTextChanged(const QString &text);
|
||||
|
||||
private:
|
||||
QHBoxLayout *layout;
|
||||
VisualDeckStorageWidget *parent;
|
||||
QLineEdit *searchBar;
|
||||
QTimer *searchDebounceTimer;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
|
|||
{
|
||||
// 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.
|
||||
model_ = new VisualDeckStorageModel(this);
|
||||
proxyModel_ = new VisualDeckStorageSortFilterProxyModel(this);
|
||||
proxyModel_->setSourceModel(model_);
|
||||
storageModel = new VisualDeckStorageModel(this);
|
||||
storageProxyModel = new VisualDeckStorageSortFilterProxyModel(this);
|
||||
storageProxyModel->setSourceModel(storageModel);
|
||||
|
||||
layout = new QVBoxLayout(this);
|
||||
layout->setSpacing(0);
|
||||
|
|
@ -86,17 +86,25 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
|
|||
refreshTimer->setSingleShot(true);
|
||||
refreshTimer->setInterval(150);
|
||||
connect(refreshTimer, &QTimer::timeout, this, [this] {
|
||||
proxyModel_->reapplyFilters();
|
||||
storageProxyModel->reapplyFilters();
|
||||
// A batch of decks finished loading: re-gather the tag chips from the visible decks once
|
||||
// the burst settles instead of on every individual load.
|
||||
tagFilterWidget->refreshTags();
|
||||
});
|
||||
connect(model_, &QAbstractItemModel::dataChanged, this, [this] { refreshTimer->start(); });
|
||||
connect(model_, &VisualDeckStorageModel::deckLoaded, this, [this] { refreshTimer->start(); });
|
||||
connect(storageModel, &QAbstractItemModel::dataChanged, this, [this] { refreshTimer->start(); });
|
||||
connect(storageModel, &VisualDeckStorageModel::deckLoaded, this, [this] { refreshTimer->start(); });
|
||||
// A deck's file path changed: re-apply the sort, since orders like "filename" depend on it.
|
||||
connect(model_, &VisualDeckStorageModel::deckFilePathChanged, this, [this] { proxyModel_->resort(); });
|
||||
connect(storageModel, &VisualDeckStorageModel::deckFilePathChanged, this, [this] { storageProxyModel->resort(); });
|
||||
connect(sortWidget, &VisualDeckStorageSortWidget::sortOrderChanged, this,
|
||||
&VisualDeckStorageWidget::updateSortOrder);
|
||||
// The filter widgets only own their ui state. Pushing it into the proxy model
|
||||
// happens here, so the children stay decoupled from the model layer.
|
||||
connect(deckPreviewColorIdentityFilterWidget, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, this,
|
||||
&VisualDeckStorageWidget::updateColorFilter);
|
||||
connect(deckPreviewColorIdentityFilterWidget, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, this,
|
||||
&VisualDeckStorageWidget::updateColorFilter);
|
||||
connect(searchWidget, &VisualDeckStorageSearchWidget::searchTextChanged, this,
|
||||
&VisualDeckStorageWidget::updateSearchFilter);
|
||||
|
||||
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
|
||||
&VisualDeckStorageWidget::createRootFolderWidget);
|
||||
|
|
@ -163,8 +171,8 @@ const VisualDeckStorageQuickSettingsWidget *VisualDeckStorageWidget::settings()
|
|||
*/
|
||||
void VisualDeckStorageWidget::reapplySortAndFilters()
|
||||
{
|
||||
proxyModel_->setSortOrder(sortWidget->currentSortOrder());
|
||||
proxyModel_->reapplyFilters();
|
||||
storageProxyModel->setSortOrder(sortWidget->currentSortOrder());
|
||||
storageProxyModel->reapplyFilters();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -172,7 +180,7 @@ void VisualDeckStorageWidget::reapplySortAndFilters()
|
|||
*/
|
||||
void VisualDeckStorageWidget::createRootFolderWidget()
|
||||
{
|
||||
model_->setDeckPath(SettingsCache::instance().paths().getDeckPath());
|
||||
storageModel->setDeckPath(SettingsCache::instance().paths().getDeckPath());
|
||||
|
||||
folderWidget =
|
||||
new VisualDeckStorageFolderDisplayWidget(this, this, QString(), false, quickSettingsWidget->getShowFolders());
|
||||
|
|
@ -194,19 +202,36 @@ void VisualDeckStorageWidget::updateShowFolders(bool enabled)
|
|||
|
||||
void VisualDeckStorageWidget::updateSortOrder()
|
||||
{
|
||||
proxyModel_->setSortOrder(sortWidget->currentSortOrder());
|
||||
storageProxyModel->setSortOrder(sortWidget->currentSortOrder());
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::updateTagFilter()
|
||||
{
|
||||
const QStringList selected = tagFilterWidget->selectedTags();
|
||||
const QStringList excluded = tagFilterWidget->excludedTags();
|
||||
proxyModel_->setTagFilter(QSet<QString>(selected.cbegin(), selected.cend()),
|
||||
QSet<QString>(excluded.cbegin(), excluded.cend()));
|
||||
storageProxyModel->setTagFilter(QSet<QString>(selected.cbegin(), selected.cend()),
|
||||
QSet<QString>(excluded.cbegin(), excluded.cend()));
|
||||
// The visible deck set changed, so the chips are re-gathered from it.
|
||||
tagFilterWidget->refreshTags();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes the color identity filter widget's state into the proxy model.
|
||||
*/
|
||||
void VisualDeckStorageWidget::updateColorFilter()
|
||||
{
|
||||
storageProxyModel->setColorFilter(deckPreviewColorIdentityFilterWidget->getFilterMode(),
|
||||
deckPreviewColorIdentityFilterWidget->getActiveColors());
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes the search bar's text into the proxy model.
|
||||
*/
|
||||
void VisualDeckStorageWidget::updateSearchFilter(const QString &text)
|
||||
{
|
||||
storageProxyModel->setSearchText(text);
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::updateTagsVisibility(const bool visible)
|
||||
{
|
||||
if (visible) {
|
||||
|
|
|
|||
|
|
@ -40,11 +40,11 @@ public:
|
|||
[[nodiscard]] const VisualDeckStorageQuickSettingsWidget *settings() const;
|
||||
[[nodiscard]] VisualDeckStorageModel *model() const
|
||||
{
|
||||
return model_;
|
||||
return storageModel;
|
||||
}
|
||||
[[nodiscard]] VisualDeckStorageSortFilterProxyModel *proxyModel() const
|
||||
{
|
||||
return proxyModel_;
|
||||
return storageProxyModel;
|
||||
}
|
||||
|
||||
public slots:
|
||||
|
|
@ -57,6 +57,8 @@ public slots:
|
|||
void updateSelectionAnimationEnabled(bool enabled);
|
||||
void updateSortOrder();
|
||||
void updateTagFilter();
|
||||
void updateColorFilter();
|
||||
void updateSearchFilter(const QString &text);
|
||||
|
||||
signals:
|
||||
void deckLoadRequested(const QString &filePath);
|
||||
|
|
@ -81,8 +83,8 @@ private:
|
|||
VisualDeckStorageQuickSettingsWidget *quickSettingsWidget;
|
||||
QScrollArea *scrollArea;
|
||||
VisualDeckStorageFolderDisplayWidget *folderWidget = nullptr;
|
||||
VisualDeckStorageModel *model_ = nullptr;
|
||||
VisualDeckStorageSortFilterProxyModel *proxyModel_ = nullptr;
|
||||
VisualDeckStorageModel *storageModel = nullptr;
|
||||
VisualDeckStorageSortFilterProxyModel *storageProxyModel = nullptr;
|
||||
QTimer *refreshTimer = nullptr; ///< Coalesces the re-apply/refresh burst following a batch of deck loads.
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue