Add subtype breakdown counter for card selection

Display a categorized count of creature subtypes (and other card type
  subtypes) when multiple cards are selected. The breakdown appears above
  the total selection counter in the bottom-right corner.

  Subtypes are grouped by main card type and sorted by frequency, with
  the most common subtypes positioned adjacent to the total count for
  quick reference. The feature can be toggled via a new checkbox in
  Settings > User Interface.
This commit is contained in:
DawnFire42 2026-05-15 20:49:59 -04:00
parent da4ba222c0
commit b18bae5e05
No known key found for this signature in database
GPG key ID: 24BB855EE2911B33
7 changed files with 216 additions and 22 deletions

View file

@ -1,12 +1,15 @@
#include "game_view.h"
#include "../client/settings/cache_settings.h"
#include "board/card_item.h"
#include "game_scene.h"
#include <QAction>
#include <QLabel>
#include <QMap>
#include <QResizeEvent>
#include <QRubberBand>
#include <algorithm>
// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings.
// This subclass disables that behavior so dragCountLabel can appear above it.
@ -42,7 +45,7 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
connect(scene, &GameScene::sigStartRubberBand, this, &GameView::startRubberBand);
connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand);
connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand);
connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); });
connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateSelectionCount(); });
aCloseMostRecentZoneView = new QAction(this);
@ -53,21 +56,30 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
refreshShortcuts();
rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this);
const QString countLabelStyle = "color: white; "
"font-size: 14px; "
"font-weight: bold; "
"background-color: rgba(0, 0, 0, 160); "
"border-radius: 3px; "
"padding: 1px 2px;";
const QString baseProperties = "color: white; "
"font-family: monospace; "
"background-color: rgba(0, 0, 0, 160); "
"border-radius: 3px; "
"padding: 1px 2px; "
"white-space: pre;";
const QString dragCountLabelStyle = baseProperties + "font-size: 14px; font-weight: bold;";
const QString totalCountLabelStyle = baseProperties + "font-size: 16px; font-weight: bold;";
const QString subtypeCountLabelStyle = baseProperties + "font-size: 12px;";
dragCountLabel = new QLabel(this);
dragCountLabel->setStyleSheet(countLabelStyle);
dragCountLabel->setStyleSheet(dragCountLabelStyle);
dragCountLabel->hide();
dragCountLabel->raise();
totalCountLabel = new QLabel(this);
totalCountLabel->setStyleSheet(countLabelStyle);
totalCountLabel->setStyleSheet(totalCountLabelStyle);
totalCountLabel->hide();
subtypeCountLabel = new QLabel(this);
subtypeCountLabel->setStyleSheet(subtypeCountLabelStyle);
subtypeCountLabel->setTextFormat(Qt::RichText);
subtypeCountLabel->hide();
}
void GameView::resizeEvent(QResizeEvent *event)
@ -80,7 +92,7 @@ void GameView::resizeEvent(QResizeEvent *event)
}
updateSceneRect(scene()->sceneRect());
updateTotalSelectionCount(event->size());
updateSelectionCount(event->size());
}
void GameView::updateSceneRect(const QRectF &rect)
@ -162,27 +174,182 @@ void GameView::refreshShortcuts()
SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView"));
}
void GameView::updateTotalSelectionCount(const QSize &viewSize)
/** @brief Extracts subtypes from a card face type string (e.g., "Creature — Human Wizard" -> ["Human", "Wizard"]) */
static QStringList extractSubtypesFromFace(const QString &faceType)
{
if (!SettingsCache::instance().getShowTotalSelectionCount()) {
totalCountLabel->hide();
return;
QStringList parts = faceType.split(QStringLiteral(""));
if (parts.size() > 1) {
return parts[1].split(QStringLiteral(" "), Qt::SkipEmptyParts);
}
return {};
}
QString GameView::buildSubtypeCountText() const
{
GameScene *gameScene = dynamic_cast<GameScene *>(scene());
if (!gameScene) {
return QString();
}
// Map: main card type -> (subtype -> count)
QMap<QString, QMap<QString, int>> subtypesByMainType;
// Track cards contributing subtypes per main type (for group ordering)
QMap<QString, int> cardCountPerMainType;
for (CardItem *card : gameScene->selectedCards()) {
if (card->getFaceDown() || card->getCard().isEmpty()) {
continue;
}
QString mainType = card->getCardInfo().getMainCardType();
if (mainType.isEmpty()) {
mainType = QStringLiteral("Other");
}
QString cardType = card->getCardInfo().getCardType();
QStringList cardFaces = cardType.split(QStringLiteral(" // "));
bool contributedSubtypes = false;
for (const QString &face : cardFaces) {
QStringList subtypes = extractSubtypesFromFace(face);
for (const QString &subtype : subtypes) {
subtypesByMainType[mainType][subtype]++;
contributedSubtypes = true;
}
}
if (contributedSubtypes) {
cardCountPerMainType[mainType]++;
}
}
if (subtypesByMainType.isEmpty()) {
return QString();
}
// Build groups with sorted subtypes
struct MainTypeGroup
{
QString mainType;
int cardCount;
QList<QPair<QString, int>> subtypes;
};
QList<MainTypeGroup> groups;
for (auto it = subtypesByMainType.constBegin(); it != subtypesByMainType.constEnd(); ++it) {
MainTypeGroup group;
group.mainType = it.key();
group.cardCount = cardCountPerMainType.value(it.key(), 0);
for (auto subIt = it.value().constBegin(); subIt != it.value().constEnd(); ++subIt) {
group.subtypes.append({subIt.key(), subIt.value()});
}
/**
* Sort subtypes: by count ascending (lower counts at top of the list), then alphabetically.
* Since the subtype list displays above the total count label (bottom-right corner),
* ascending order places the most common subtypes visually adjacent to the total.
*/
std::sort(group.subtypes.begin(), group.subtypes.end(),
[](const QPair<QString, int> &a, const QPair<QString, int> &b) {
if (a.second != b.second) {
return a.second < b.second;
}
return a.first < b.first;
});
groups.append(group);
}
// Sort groups: by card count ascending, then alphabetically by main type
std::sort(groups.begin(), groups.end(), [](const MainTypeGroup &a, const MainTypeGroup &b) {
if (a.cardCount != b.cardCount) {
return a.cardCount < b.cardCount;
}
return a.mainType < b.mainType;
});
// Flatten to final ordered list
QList<QPair<QString, int>> sortedEntries;
for (const MainTypeGroup &group : groups) {
for (const auto &entry : group.subtypes) {
sortedEntries.append(entry);
}
}
// Calculate padding widths
int maxNameLen = 0;
int maxCountLen = 0;
for (const auto &entry : sortedEntries) {
maxNameLen = qMax(maxNameLen, entry.first.length());
maxCountLen = qMax(maxCountLen, QString::number(entry.second).length());
}
// Format output
QStringList lines;
for (const auto &entry : sortedEntries) {
QString name = entry.first.toHtmlEscaped();
QString count = QString::number(entry.second);
QString namePadding = QString(QStringLiteral("&nbsp;")).repeated(maxNameLen - entry.first.length());
QString countPadding = QString(QStringLiteral("&nbsp;")).repeated(maxCountLen - count.length());
lines << QStringLiteral(
"%1%2 <span style='font-size:14px;font-weight:bold;vertical-align:middle;'>%3%4</span>")
.arg(namePadding, name, countPadding, count);
}
return lines.join(QStringLiteral("<br>"));
}
void GameView::updateSelectionCount(const QSize &viewSize)
{
constexpr int kMarginInPixels = 10;
constexpr int kSpacingBetweenLabels = 4;
int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width();
int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height();
int count = scene()->selectedItems().count();
if (count > 1) {
if (!SettingsCache::instance().getShowTotalSelectionCount() || count <= 1) {
totalCountLabel->hide();
} else {
totalCountLabel->setText(QString::number(count));
totalCountLabel->adjustSize();
constexpr int kMarginInPixels = 10;
int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width();
int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height();
int x = availableWidth - totalCountLabel->width() - kMarginInPixels;
int y = availableHeight - totalCountLabel->height() - kMarginInPixels;
totalCountLabel->move(x, y);
totalCountLabel->show();
} else {
totalCountLabel->hide();
}
if (!SettingsCache::instance().getShowSubtypeSelectionCount() || count <= 1) {
subtypeCountLabel->hide();
return;
}
QString subtypeText = buildSubtypeCountText();
if (subtypeText.isEmpty()) {
subtypeCountLabel->hide();
return;
}
subtypeCountLabel->setText(subtypeText);
subtypeCountLabel->adjustSize();
int x = availableWidth - subtypeCountLabel->width() - kMarginInPixels;
int y;
if (totalCountLabel->isVisible()) {
y = totalCountLabel->y() - subtypeCountLabel->height() - kSpacingBetweenLabels;
} else {
y = availableHeight - subtypeCountLabel->height() - kMarginInPixels;
}
y = qMax(kMarginInPixels, y);
subtypeCountLabel->move(x, y);
subtypeCountLabel->show();
}