Sort VDE groups internally (#6102)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (Debian, DEB, 13) (push) Blocked by required conditions
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (Debian, DEB, skip, 11) (push) Blocked by required conditions
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (Debian, DEB, skip, 12) (push) Blocked by required conditions
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (Fedora, RPM, 42) (push) Blocked by required conditions
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (Fedora, RPM, skip, 41) (push) Blocked by required conditions
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (Ubuntu, DEB, 24.04) (push) Blocked by required conditions
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (Ubuntu, DEB, skip, 22.04) (push) Blocked by required conditions
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (yes, Arch, skip) (push) Blocked by required conditions
Build Desktop / macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} (1, macos-13, Intel, 13, Release, 14.3.1) (push) Blocked by required conditions
Build Desktop / macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} (1, macos-14, Apple, 14, Release, 15.4) (push) Blocked by required conditions
Build Desktop / macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} (1, macos-15, Apple, 15, Release, 16.2) (push) Blocked by required conditions
Build Desktop / macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} (macos-15, Apple, 15, Debug, 16.2) (push) Blocked by required conditions
Build Desktop / Windows ${{matrix.target}} (msvc2019_64, 5.15.*, 7) (push) Blocked by required conditions
Build Desktop / Windows ${{matrix.target}} (msvc2019_64, qtimageformats qtmultimedia qtwebsockets, 6.6.*, 10) (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run

This commit is contained in:
BruebachL 2025-09-06 15:06:59 +02:00 committed by GitHub
parent bd65aae81e
commit 7f842bb1e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 181 additions and 111 deletions

View File

@ -256,6 +256,7 @@ set(cockatrice_SOURCES
src/settings/shortcut_treeview.cpp
src/settings/shortcuts_settings.cpp
src/utility/card_info_comparator.cpp
src/utility/deck_list_sort_filter_proxy_model.h
src/utility/key_signals.cpp
src/utility/levenshtein.cpp
src/utility/logger.cpp

View File

@ -3,6 +3,7 @@
#include "../../../../../deck/deck_list_model.h"
#include "../../../../../game/cards/card_database_manager.h"
#include "../../../../../utility/card_info_comparator.h"
#include "../../../../../utility/deck_list_sort_filter_proxy_model.h"
#include "../card_info_picture_with_text_overlay_widget.h"
#include <QResizeEvent>
@ -34,10 +35,18 @@ CardGroupDisplayWidget::CardGroupDisplayWidget(QWidget *parent,
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &CardGroupDisplayWidget::onCardRemoval);
}
QWidget *CardGroupDisplayWidget::constructWidgetForIndex(int rowIndex)
void CardGroupDisplayWidget::clearAllDisplayWidgets()
{
QPersistentModelIndex index = QPersistentModelIndex(deckListModel->index(rowIndex, 0, trackedIndex));
for (auto idx : indexToWidgetMap.keys()) {
auto displayWidget = indexToWidgetMap.value(idx);
removeFromLayout(displayWidget);
indexToWidgetMap.remove(idx);
delete displayWidget;
}
}
QWidget *CardGroupDisplayWidget::constructWidgetForIndex(QPersistentModelIndex index)
{
if (indexToWidgetMap.contains(index)) {
return indexToWidgetMap[index];
}
@ -58,8 +67,28 @@ QWidget *CardGroupDisplayWidget::constructWidgetForIndex(int rowIndex)
void CardGroupDisplayWidget::updateCardDisplays()
{
for (int i = 0; i < deckListModel->rowCount(trackedIndex); ++i) {
addToLayout(constructWidgetForIndex(i));
DeckListSortFilterProxyModel proxy;
proxy.setSourceModel(deckListModel);
proxy.setSortCriteria(activeSortCriteria);
// This doesn't really matter since overwrite the whole lessThan function to just compare dynamically anyway.
proxy.setSortRole(Qt::EditRole);
proxy.sort(1, Qt::AscendingOrder);
// 1. trackedIndex is a source index → map it to proxy space
QModelIndex proxyParent = proxy.mapFromSource(trackedIndex);
// 2. iterate children under the proxy parent
for (int i = 0; i < proxy.rowCount(proxyParent); ++i) {
QModelIndex proxyIndex = proxy.index(i, 0, proxyParent);
// 3. map back to source
QModelIndex sourceIndex = proxy.mapToSource(proxyIndex);
// 4. persist the source index
QPersistentModelIndex persistent(sourceIndex);
addToLayout(constructWidgetForIndex(persistent));
}
}
@ -69,9 +98,15 @@ void CardGroupDisplayWidget::onCardAddition(const QModelIndex &parent, int first
emit cleanupRequested(this);
return;
}
if (parent == trackedIndex) {
for (int i = first; i <= last; i++) {
insertIntoLayout(constructWidgetForIndex(i), i);
for (int row = first; row <= last; ++row) {
QModelIndex child = deckListModel->index(row, 0, parent);
// Persist the index
QPersistentModelIndex persistent(child);
insertIntoLayout(constructWidgetForIndex(persistent), row);
}
}
}
@ -88,12 +123,21 @@ void CardGroupDisplayWidget::onCardRemoval(const QModelIndex &parent, int first,
indexToWidgetMap.remove(idx);
}
}
if (!trackedIndex.isValid()) {
emit cleanupRequested(this);
}
}
}
void CardGroupDisplayWidget::onActiveSortCriteriaChanged(QStringList _activeSortCriteria)
{
activeSortCriteria = std::move(_activeSortCriteria);
clearAllDisplayWidgets();
updateCardDisplays();
}
void CardGroupDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card)
{
emit cardClicked(event, card);

View File

@ -25,6 +25,7 @@ public:
QStringList activeSortCriteria,
int bannerOpacity,
CardSizeWidget *cardSizeWidget);
void clearAllDisplayWidgets();
DeckListModel *deckListModel;
QPersistentModelIndex trackedIndex;
@ -38,10 +39,11 @@ public:
public slots:
void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card);
void onHover(const ExactCard &card);
virtual QWidget *constructWidgetForIndex(int rowIndex);
virtual QWidget *constructWidgetForIndex(QPersistentModelIndex index);
virtual void updateCardDisplays();
virtual void onCardAddition(const QModelIndex &parent, int first, int last);
virtual void onCardRemoval(const QModelIndex &parent, int first, int last);
void onActiveSortCriteriaChanged(QStringList activeSortCriteria);
void resizeEvent(QResizeEvent *event) override;
signals:

View File

@ -46,59 +46,6 @@ FlatCardGroupDisplayWidget::FlatCardGroupDisplayWidget(QWidget *parent,
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &FlatCardGroupDisplayWidget::onCardRemoval);
}
void FlatCardGroupDisplayWidget::onCardAddition(const QModelIndex &parent, int first, int last)
{
if (!trackedIndex.isValid()) {
emit cleanupRequested(this);
return;
}
if (parent == trackedIndex) {
for (int i = first; i <= last; i++) {
insertIntoLayout(constructWidgetForIndex(i), i);
}
}
}
void FlatCardGroupDisplayWidget::onCardRemoval(const QModelIndex &parent, int first, int last)
{
Q_UNUSED(first);
Q_UNUSED(last);
if (parent == trackedIndex) {
for (const QPersistentModelIndex &idx : indexToWidgetMap.keys()) {
if (!idx.isValid()) {
removeFromLayout(indexToWidgetMap.value(idx));
indexToWidgetMap.value(idx)->deleteLater();
indexToWidgetMap.remove(idx);
}
}
if (!trackedIndex.isValid()) {
emit cleanupRequested(this);
}
}
}
QWidget *FlatCardGroupDisplayWidget::constructWidgetForIndex(int row)
{
QPersistentModelIndex index = QPersistentModelIndex(deckListModel->index(row, 0, trackedIndex));
if (indexToWidgetMap.contains(index)) {
return indexToWidgetMap[index];
}
auto cardName = deckListModel->data(index.sibling(index.row(), 1), Qt::EditRole).toString();
auto cardProviderId = deckListModel->data(index.sibling(index.row(), 4), Qt::EditRole).toString();
auto widget = new CardInfoPictureWithTextOverlayWidget(flowWidget, true);
widget->setScaleFactor(cardSizeWidget->getSlider()->value());
widget->setCard(CardDatabaseManager::getInstance()->getCard({cardName, cardProviderId}));
connect(widget, &CardInfoPictureWithTextOverlayWidget::imageClicked, this, &FlatCardGroupDisplayWidget::onClick);
connect(widget, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &FlatCardGroupDisplayWidget::onHover);
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, widget, &CardInfoPictureWidget::setScaleFactor);
indexToWidgetMap.insert(index, widget);
return widget;
}
void FlatCardGroupDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);

View File

@ -20,10 +20,7 @@ public:
CardSizeWidget *cardSizeWidget);
public slots:
QWidget *constructWidgetForIndex(int row) override;
void resizeEvent(QResizeEvent *event) override;
void onCardAddition(const QModelIndex &parent, int first, int last) override;
void onCardRemoval(const QModelIndex &parent, int first, int last) override;
private:
FlowWidget *flowWidget;

View File

@ -49,37 +49,6 @@ OverlappedCardGroupDisplayWidget::OverlappedCardGroupDisplayWidget(QWidget *pare
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &OverlappedCardGroupDisplayWidget::onCardRemoval);
}
void OverlappedCardGroupDisplayWidget::onCardAddition(const QModelIndex &parent, int first, int last)
{
if (!trackedIndex.isValid()) {
emit cleanupRequested(this);
return;
}
if (parent == trackedIndex) {
for (int i = first; i <= last; i++) {
insertIntoLayout(constructWidgetForIndex(i), i);
}
}
}
void OverlappedCardGroupDisplayWidget::onCardRemoval(const QModelIndex &parent, int first, int last)
{
Q_UNUSED(first);
Q_UNUSED(last);
if (parent == trackedIndex) {
for (const QPersistentModelIndex &idx : indexToWidgetMap.keys()) {
if (!idx.isValid()) {
removeFromLayout(indexToWidgetMap.value(idx));
indexToWidgetMap.value(idx)->deleteLater();
indexToWidgetMap.remove(idx);
}
}
if (!trackedIndex.isValid()) {
emit cleanupRequested(this);
}
}
}
void OverlappedCardGroupDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);

View File

@ -20,8 +20,6 @@ public:
CardSizeWidget *cardSizeWidget);
public slots:
void onCardAddition(const QModelIndex &parent, int first, int last) override;
void onCardRemoval(const QModelIndex &parent, int first, int last) override;
void resizeEvent(QResizeEvent *event) override;
private:

View File

@ -62,22 +62,26 @@ void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex
auto *displayWidget = new OverlappedCardGroupDisplayWidget(
cardGroupContainer, deckListModel, index, zoneName, categoryName, activeGroupCriteria, activeSortCriteria,
subBannerOpacity, cardSizeWidget);
connect(displayWidget, SIGNAL(cardClicked(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), this,
SLOT(onClick(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)));
connect(displayWidget, SIGNAL(cardHovered(ExactCard)), this, SLOT(onHover(ExactCard)));
connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardClicked, this,
&DeckCardZoneDisplayWidget::onClick);
connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardHovered, this,
&DeckCardZoneDisplayWidget::onHover);
connect(displayWidget, &CardGroupDisplayWidget::cleanupRequested, this,
&DeckCardZoneDisplayWidget::cleanupInvalidCardGroup);
connect(this, &DeckCardZoneDisplayWidget::activeSortCriteriaChanged, displayWidget,
&CardGroupDisplayWidget::onActiveSortCriteriaChanged);
cardGroupLayout->addWidget(displayWidget);
indexToWidgetMap.insert(index, displayWidget);
} else if (displayType == DisplayType::Flat) {
auto *displayWidget =
new FlatCardGroupDisplayWidget(cardGroupContainer, deckListModel, index, zoneName, categoryName,
activeGroupCriteria, activeSortCriteria, subBannerOpacity, cardSizeWidget);
connect(displayWidget, SIGNAL(cardClicked(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)), this,
SLOT(onClick(QMouseEvent *, CardInfoPictureWithTextOverlayWidget *)));
connect(displayWidget, SIGNAL(cardHovered(ExactCard)), this, SLOT(onHover(ExactCard)));
connect(displayWidget, &FlatCardGroupDisplayWidget::cardClicked, this, &DeckCardZoneDisplayWidget::onClick);
connect(displayWidget, &FlatCardGroupDisplayWidget::cardHovered, this, &DeckCardZoneDisplayWidget::onHover);
connect(displayWidget, &CardGroupDisplayWidget::cleanupRequested, this,
&DeckCardZoneDisplayWidget::cleanupInvalidCardGroup);
connect(this, &DeckCardZoneDisplayWidget::activeSortCriteriaChanged, displayWidget,
&CardGroupDisplayWidget::onActiveSortCriteriaChanged);
cardGroupLayout->addWidget(displayWidget);
indexToWidgetMap.insert(index, displayWidget);
}
@ -85,9 +89,25 @@ void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex
void DeckCardZoneDisplayWidget::displayCards()
{
for (int i = 0; i < deckListModel->rowCount(trackedIndex); ++i) {
QPersistentModelIndex index = QPersistentModelIndex(deckListModel->index(i, 0, trackedIndex));
constructAppropriateWidget(index);
QSortFilterProxyModel proxy;
proxy.setSourceModel(deckListModel);
proxy.setSortRole(Qt::EditRole);
proxy.sort(1, Qt::AscendingOrder);
// 1. trackedIndex is a source index → map it to proxy space
QModelIndex proxyParent = proxy.mapFromSource(trackedIndex);
// 2. iterate children under the proxy parent
for (int i = 0; i < proxy.rowCount(proxyParent); ++i) {
QModelIndex proxyIndex = proxy.index(i, 0, proxyParent);
// 3. map back to source
QModelIndex sourceIndex = proxy.mapToSource(proxyIndex);
// 4. persist the source index
QPersistentModelIndex persistent(sourceIndex);
constructAppropriateWidget(persistent);
}
}

View File

@ -270,16 +270,25 @@ void VisualDeckEditorWidget::onCardRemoval(const QModelIndex &parent, int first,
void VisualDeckEditorWidget::constructZoneWidgetsFromDeckListModel()
{
for (int i = 0; i < deckListModel->rowCount(deckListModel->parent(QModelIndex())); i++) {
QPersistentModelIndex index = QPersistentModelIndex(deckListModel->index(i, 0, deckListModel->getRoot()));
QSortFilterProxyModel proxy;
proxy.setSourceModel(deckListModel);
proxy.setSortRole(Qt::EditRole);
proxy.sort(1, Qt::AscendingOrder);
if (indexToWidgetMap.contains(index)) {
for (int i = 0; i < proxy.rowCount(); ++i) {
QModelIndex proxyIndex = proxy.index(i, 0);
QModelIndex sourceIndex = proxy.mapToSource(proxyIndex);
// Make a persistent index from the *source* model
QPersistentModelIndex persistent(sourceIndex);
if (indexToWidgetMap.contains(persistent)) {
continue;
}
DeckCardZoneDisplayWidget *zoneDisplayWidget = new DeckCardZoneDisplayWidget(
zoneContainer, deckListModel, index,
deckListModel->data(index.sibling(index.row(), 1), Qt::EditRole).toString(), activeGroupCriteria,
zoneContainer, deckListModel, persistent,
deckListModel->data(persistent.sibling(persistent.row(), 1), Qt::EditRole).toString(), activeGroupCriteria,
activeSortCriteria, currentDisplayType, 20, 10, cardSizeWidget);
connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardHovered, this, &VisualDeckEditorWidget::onHover);
connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardClicked, this, &VisualDeckEditorWidget::onCardClick);
@ -293,7 +302,7 @@ void VisualDeckEditorWidget::constructZoneWidgetsFromDeckListModel()
&DeckCardZoneDisplayWidget::refreshDisplayType);
zoneContainerLayout->addWidget(zoneDisplayWidget);
indexToWidgetMap.insert(index, zoneDisplayWidget);
indexToWidgetMap.insert(persistent, zoneDisplayWidget);
}
}

View File

@ -142,6 +142,8 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const
return {};
}
}
case Qt::UserRole + 1:
return false;
case Qt::BackgroundRole: {
int color = 90 + 60 * node->depth();
return QBrush(QColor(color, 255, color));
@ -171,6 +173,8 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const
return {};
}
}
case Qt::UserRole + 1:
return true;
case Qt::BackgroundRole: {
int color = 255 - (index.row() % 2) * 30;
return QBrush(QColor(color, color, color));

View File

@ -0,0 +1,79 @@
#ifndef COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H
#define COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H
#include "../game/cards/card_database_manager.h"
#include <QSortFilterProxyModel>
class DeckListSortFilterProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
explicit DeckListSortFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent)
{
}
void setSortCriteria(const QStringList &criteria)
{
sortCriteria = criteria;
invalidate(); // re-sort
}
protected:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override
{
auto *src = sourceModel();
// Inner nodes? -> sort alphabetically by column 1
bool leftIsCard = src->data(left, Qt::UserRole + 1).toBool();
bool rightIsCard = src->data(right, Qt::UserRole + 1).toBool();
if (!leftIsCard || !rightIsCard) {
QString lName = src->data(left.siblingAtColumn(1), Qt::EditRole).toString();
QString rName = src->data(right.siblingAtColumn(1), Qt::EditRole).toString();
return lName.localeAwareCompare(rName) < 0;
}
// Both are cards -> apply sort criteria
auto *lNode = static_cast<DecklistModelCardNode *>(left.internalPointer());
auto *rNode = static_cast<DecklistModelCardNode *>(right.internalPointer());
CardInfoPtr lInfo = CardDatabaseManager::getInstance()->guessCard({lNode->getName()}).getCardPtr();
CardInfoPtr rInfo = CardDatabaseManager::getInstance()->guessCard({rNode->getName()}).getCardPtr();
// Example: multiple tie-break criteria (colors > cmc > name)
for (const QString &crit : sortCriteria) {
if (crit == "name") {
QString ln = lNode->getName();
QString rn = rNode->getName();
int cmp = ln.localeAwareCompare(rn);
if (cmp != 0)
return cmp < 0;
} else if (crit == "cmc") {
int lc = lInfo ? lInfo->getCmc().toInt() : 0;
int rc = rInfo ? rInfo->getCmc().toInt() : 0;
if (lc != rc)
return lc < rc;
} else if (crit == "colors") {
QString lr = lInfo ? lInfo->getColors() : QString();
QString rr = rInfo ? rInfo->getColors() : QString();
int cmp = lr.localeAwareCompare(rr);
if (cmp != 0)
return cmp < 0;
} else if (crit == "maintype") {
QString lr = lInfo ? lInfo->getMainCardType() : QString();
QString rr = rInfo ? rInfo->getMainCardType() : QString();
int cmp = lr.localeAwareCompare(rr);
if (cmp != 0)
return cmp < 0;
}
}
return false;
}
private:
QStringList sortCriteria;
};
#endif // COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H