* move message_log_widget to game

* move files

* update headers

* fix cmakelists

* oracle fixes

* split implementation out to cpp

* fix recursive import

* fix main file

* format
This commit is contained in:
ebbit1q 2025-09-20 14:35:52 +02:00 committed by GitHub
parent f484c98152
commit 17dcaf9afa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
337 changed files with 728 additions and 721 deletions

View file

@ -0,0 +1,125 @@
#include "color_identity_widget.h"
#include "../../../../settings/cache_settings.h"
#include "mana_symbol_widget.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QPixmap>
#include <QRegularExpression>
#include <QResizeEvent>
#include <QSize>
ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, CardInfoPtr _card) : QWidget(parent), card(_card)
{
layout = new QHBoxLayout(this);
layout->setSpacing(5); // Small spacing between icons
layout->setContentsMargins(0, 0, 0, 0);
layout->setAlignment(Qt::AlignCenter); // Ensure icons are centered
setLayout(layout);
// Define the full WUBRG set (White, Blue, Black, Red, Green)
QString fullColorIdentity = "WUBRG";
if (card) {
manaCost = card->getColors(); // Get mana cost string
QStringList symbols = parseColorIdentity(manaCost); // Parse mana cost string
populateManaSymbolWidgets();
}
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageDrawUnusedColorIdentitiesChanged, this,
&ColorIdentityWidget::toggleUnusedVisibility);
}
ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, QString _manaCost)
: QWidget(parent), card(nullptr), manaCost(_manaCost)
{
layout = new QHBoxLayout(this);
layout->setSpacing(5); // Small spacing between icons
layout->setContentsMargins(0, 0, 0, 0);
layout->setAlignment(Qt::AlignCenter); // Ensure icons are centered
setLayout(layout);
populateManaSymbolWidgets();
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageDrawUnusedColorIdentitiesChanged, this,
&ColorIdentityWidget::toggleUnusedVisibility);
}
void ColorIdentityWidget::populateManaSymbolWidgets()
{
// Define the full WUBRG set (White, Blue, Black, Red, Green)
QString fullColorIdentity = "WUBRG";
QStringList symbols = parseColorIdentity(manaCost); // Parse mana cost string
if (SettingsCache::instance().getVisualDeckStorageDrawUnusedColorIdentities()) {
for (const QString symbol : fullColorIdentity) {
auto *manaSymbol = new ManaSymbolWidget(this, symbol, symbols.contains(symbol));
layout->addWidget(manaSymbol);
}
} else {
for (const QString &symbol : symbols) {
auto *manaSymbol = new ManaSymbolWidget(this, symbol, symbols.contains(symbol));
layout->addWidget(manaSymbol);
}
}
}
void ColorIdentityWidget::toggleUnusedVisibility()
{
if (layout != nullptr) {
QLayoutItem *item;
while ((item = layout->takeAt(0)) != nullptr) {
item->widget()->deleteLater(); // Delete the widget
delete item; // Delete the layout item
}
}
populateManaSymbolWidgets();
}
void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
QList<ManaSymbolWidget *> manaSymbols = findChildren<ManaSymbolWidget *>();
if (!manaSymbols.isEmpty()) {
int totalWidth = event->size().width();
int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
setFixedHeight(totalHeight);
int spacing = layout->spacing();
int count = manaSymbols.size();
int availableWidth = totalWidth - (spacing * (count - 1));
int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
for (ManaSymbolWidget *manaSymbol : manaSymbols) {
manaSymbol->setFixedSize(iconSize, iconSize);
}
}
}
QStringList ColorIdentityWidget::parseColorIdentity(const QString &cmc)
{
QStringList symbols;
// Handle split costs (e.g., "3U // 4UU")
QStringList splitCosts = cmc.split(" // ");
for (const QString &part : splitCosts) {
QRegularExpression regex(R"(\{([^}]+)\}|(\d+)|([WUBRGCSPX]))");
QRegularExpressionMatchIterator matches = regex.globalMatch(part);
while (matches.hasNext()) {
QRegularExpressionMatch match = matches.next();
if (match.captured(1).isEmpty()) { // If no `{}` group was captured, check other groups
if (!match.captured(2).isEmpty()) {
symbols.append(match.captured(2)); // Number match
} else {
symbols.append(match.captured(3)); // Single mana letter match
}
} else {
symbols.append(match.captured(1)); // `{}` enclosed match
}
}
}
return symbols;
}

View file

@ -0,0 +1,29 @@
#ifndef COLOR_IDENTITY_WIDGET_H
#define COLOR_IDENTITY_WIDGET_H
#include "../../../../card/card_info.h"
#include <QHBoxLayout>
#include <QWidget>
class ColorIdentityWidget : public QWidget
{
Q_OBJECT
public:
explicit ColorIdentityWidget(QWidget *parent, CardInfoPtr card);
explicit ColorIdentityWidget(QWidget *parent, QString manaCost);
void populateManaSymbolWidgets();
QStringList parseColorIdentity(const QString &manaString);
public slots:
void resizeEvent(QResizeEvent *event) override;
void toggleUnusedVisibility();
private:
CardInfoPtr card;
QString manaCost;
QHBoxLayout *layout;
};
#endif // COLOR_IDENTITY_WIDGET_H

View file

@ -0,0 +1,76 @@
#include "mana_cost_widget.h"
#include "mana_symbol_widget.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QPixmap>
#include <QResizeEvent>
#include <QSize>
#include <qregularexpression.h>
ManaCostWidget::ManaCostWidget(QWidget *parent, CardInfoPtr _card) : QWidget(parent), card(_card)
{
layout = new QHBoxLayout(this);
layout->setSpacing(5); // Small spacing between icons
layout->setContentsMargins(0, 0, 0, 0);
setLayout(layout);
setFixedHeight(50); // Fixed height
if (card) {
QString manaCost = card->getManaCost(); // Get mana cost string
QStringList symbols = parseManaCost(manaCost); // Parse mana cost string
for (const QString &symbol : symbols) {
ManaSymbolWidget *manaSymbol = new ManaSymbolWidget(this, symbol, true, false);
layout->addWidget(manaSymbol);
}
}
}
void ManaCostWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
QList<ManaSymbolWidget *> manaSymbols = findChildren<ManaSymbolWidget *>();
if (!manaSymbols.isEmpty()) {
int totalWidth = event->size().width();
int spacing = layout->spacing();
int count = manaSymbols.size();
// Available width minus total spacing
int availableWidth = totalWidth - (spacing * (count - 1));
int iconSize = qMin(50, availableWidth / count);
for (ManaSymbolWidget *manaSymbol : manaSymbols) {
manaSymbol->setFixedSize(iconSize, iconSize);
}
}
}
QStringList ManaCostWidget::parseManaCost(const QString &cmc)
{
QStringList symbols;
// Handle split costs (e.g., "3U // 4UU")
QStringList splitCosts = cmc.split(" // ");
for (const QString &part : splitCosts) {
QRegularExpression regex(R"(\{([^}]+)\}|(\d+)|([WUBRGCSPX]))");
QRegularExpressionMatchIterator matches = regex.globalMatch(part);
while (matches.hasNext()) {
QRegularExpressionMatch match = matches.next();
if (match.captured(1).isEmpty()) { // If no `{}` group was captured, check other groups
if (!match.captured(2).isEmpty()) {
symbols.append(match.captured(2)); // Number match
} else {
symbols.append(match.captured(3)); // Single mana letter match
}
} else {
symbols.append(match.captured(1)); // `{}` enclosed match
}
}
}
return symbols;
}

View file

@ -0,0 +1,24 @@
#ifndef MANA_COST_WIDGET_H
#define MANA_COST_WIDGET_H
#include "../../../../card/card_info.h"
#include <QHBoxLayout>
#include <QWidget>
class ManaCostWidget : public QWidget
{
Q_OBJECT
public:
explicit ManaCostWidget(QWidget *parent, CardInfoPtr card);
QStringList parseManaCost(const QString &manaString);
public slots:
void resizeEvent(QResizeEvent *event) override;
private:
CardInfoPtr card;
QHBoxLayout *layout;
};
#endif // MANA_COST_WIDGET_H

View file

@ -0,0 +1,73 @@
#include "mana_symbol_widget.h"
#include "../../../../settings/cache_settings.h"
#include <QResizeEvent>
ManaSymbolWidget::ManaSymbolWidget(QWidget *parent, QString _symbol, bool _isActive, bool _mayBeToggled)
: QLabel(parent), symbol(_symbol), isActive(_isActive), mayBeToggled(_mayBeToggled)
{
loadManaIcon();
setPixmap(manaIcon.scaled(50, 50, Qt::KeepAspectRatio, Qt::SmoothTransformation));
setMaximumWidth(50);
// Initialize opacity effect
opacityEffect = new QGraphicsOpacityEffect(this);
setGraphicsEffect(opacityEffect);
updateOpacity();
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageUnusedColorIdentitiesOpacityChanged, this,
&ManaSymbolWidget::updateOpacity);
}
void ManaSymbolWidget::toggleSymbol()
{
setColorActive(!isActive);
emit colorToggled(getSymbolChar(), isActive);
}
void ManaSymbolWidget::setColorActive(bool active)
{
if (isActive != active) {
isActive = active;
updateOpacity();
}
}
void ManaSymbolWidget::updateOpacity()
{
qreal opacity;
if (mayBeToggled) {
// UI elements that users can click on shouldn't be transparent.
opacity = isActive ? 1.0 : 0.5;
} else {
// It's just for display, they can do whatever they want.
opacity = isActive ? 1.0 : SettingsCache::instance().getVisualDeckStorageUnusedColorIdentitiesOpacity() / 100.0;
}
opacityEffect->setOpacity(opacity);
}
void ManaSymbolWidget::mousePressEvent(QMouseEvent *event)
{
Q_UNUSED(event);
if (mayBeToggled) {
toggleSymbol();
}
}
void ManaSymbolWidget::resizeEvent(QResizeEvent *event)
{
QLabel::resizeEvent(event);
setPixmap(manaIcon.scaled(event->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
void ManaSymbolWidget::loadManaIcon()
{
QString filename = "theme:icons/mana/";
if (symbol == "W" || symbol == "U" || symbol == "B" || symbol == "R" || symbol == "G") {
filename += symbol;
}
manaIcon = QPixmap(filename);
}

View file

@ -0,0 +1,47 @@
#ifndef MANA_SYMBOL_WIDGET_H
#define MANA_SYMBOL_WIDGET_H
#include <QGraphicsOpacityEffect>
#include <QLabel>
class ManaSymbolWidget : public QLabel
{
Q_OBJECT
public:
ManaSymbolWidget(QWidget *parent, QString symbol, bool isActive = true, bool mayBeToggled = false);
void toggleSymbol();
void setColorActive(bool active);
void updateOpacity();
bool isColorActive() const
{
return isActive;
};
QString getSymbol() const
{
return symbol;
};
QChar getSymbolChar() const
{
return symbol[0];
};
void loadManaIcon();
public slots:
void resizeEvent(QResizeEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
signals:
void colorToggled(QChar symbol, bool isActive);
private:
QString symbol;
QPixmap manaIcon;
bool isActive;
bool mayBeToggled;
QGraphicsOpacityEffect *opacityEffect;
};
#endif // MANA_SYMBOL_WIDGET_H

View file

@ -0,0 +1,154 @@
#include "card_group_display_widget.h"
#include "../../../../database/card_database_manager.h"
#include "../../../../deck/deck_list_model.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>
CardGroupDisplayWidget::CardGroupDisplayWidget(QWidget *parent,
DeckListModel *_deckListModel,
QPersistentModelIndex _trackedIndex,
QString _zoneName,
QString _cardGroupCategory,
QString _activeGroupCriteria,
QStringList _activeSortCriteria,
int bannerOpacity,
CardSizeWidget *_cardSizeWidget)
: QWidget(parent), deckListModel(_deckListModel), trackedIndex(_trackedIndex), zoneName(_zoneName),
cardGroupCategory(_cardGroupCategory), activeGroupCriteria(_activeGroupCriteria),
activeSortCriteria(_activeSortCriteria), cardSizeWidget(_cardSizeWidget)
{
layout = new QVBoxLayout(this);
setLayout(layout);
setMinimumSize(QSize(0, 0));
banner = new BannerWidget(this, cardGroupCategory, Qt::Orientation::Vertical, bannerOpacity);
layout->addWidget(banner);
CardGroupDisplayWidget::updateCardDisplays();
connect(deckListModel, &QAbstractItemModel::rowsInserted, this, &CardGroupDisplayWidget::onCardAddition);
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &CardGroupDisplayWidget::onCardRemoval);
}
void CardGroupDisplayWidget::clearAllDisplayWidgets()
{
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];
}
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(getLayoutParent(), true);
widget->setScaleFactor(cardSizeWidget->getSlider()->value());
widget->setCard(CardDatabaseManager::getInstance()->getCard({cardName, cardProviderId}));
connect(widget, &CardInfoPictureWithTextOverlayWidget::imageClicked, this, &CardGroupDisplayWidget::onClick);
connect(widget, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &CardGroupDisplayWidget::onHover);
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, widget, &CardInfoPictureWidget::setScaleFactor);
indexToWidgetMap.insert(index, widget);
return widget;
}
void CardGroupDisplayWidget::updateCardDisplays()
{
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));
}
}
void CardGroupDisplayWidget::onCardAddition(const QModelIndex &parent, int first, int last)
{
if (!trackedIndex.isValid()) {
emit cleanupRequested(this);
return;
}
if (parent == trackedIndex) {
for (int row = first; row <= last; ++row) {
QModelIndex child = deckListModel->index(row, 0, parent);
// Persist the index
QPersistentModelIndex persistent(child);
insertIntoLayout(constructWidgetForIndex(persistent), row);
}
}
}
void CardGroupDisplayWidget::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 CardGroupDisplayWidget::onActiveSortCriteriaChanged(QStringList _activeSortCriteria)
{
activeSortCriteria = std::move(_activeSortCriteria);
clearAllDisplayWidgets();
updateCardDisplays();
}
void CardGroupDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card)
{
emit cardClicked(event, card);
}
void CardGroupDisplayWidget::onHover(const ExactCard &card)
{
emit cardHovered(card);
}
void CardGroupDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
}

View file

@ -0,0 +1,78 @@
#ifndef CARD_GROUP_DISPLAY_WIDGET_H
#define CARD_GROUP_DISPLAY_WIDGET_H
#include "../../../../card/card_info.h"
#include "../../../../deck/deck_list_model.h"
#include "../../general/display/banner_widget.h"
#include "../card_info_picture_with_text_overlay_widget.h"
#include "../card_size_widget.h"
#include <QLabel>
#include <QVBoxLayout>
#include <QWidget>
class CardGroupDisplayWidget : public QWidget
{
Q_OBJECT
public:
CardGroupDisplayWidget(QWidget *parent,
DeckListModel *deckListModel,
QPersistentModelIndex trackedIndex,
QString zoneName,
QString cardGroupCategory,
QString activeGroupCriteria,
QStringList activeSortCriteria,
int bannerOpacity,
CardSizeWidget *cardSizeWidget);
void clearAllDisplayWidgets();
DeckListModel *deckListModel;
QPersistentModelIndex trackedIndex;
QHash<QPersistentModelIndex, QWidget *> indexToWidgetMap;
QString zoneName;
QString cardGroupCategory;
QString activeGroupCriteria;
QStringList activeSortCriteria;
CardSizeWidget *cardSizeWidget;
public slots:
void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card);
void onHover(const ExactCard &card);
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:
void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card);
void cardHovered(const ExactCard &card);
void cleanupRequested(CardGroupDisplayWidget *cardGroupDisplayWidget);
protected:
QVBoxLayout *layout;
BannerWidget *banner;
virtual QWidget *getLayoutParent()
{
return this;
}
virtual void addToLayout(QWidget *toAdd)
{
layout->addWidget(toAdd);
}
virtual void insertIntoLayout(QWidget *toInsert, int insertAt)
{
layout->insertWidget(insertAt, toInsert);
}
virtual void removeFromLayout(QWidget *toRemove)
{
layout->removeWidget(toRemove);
}
};
#endif // CARD_GROUP_DISPLAY_WIDGET_H

View file

@ -0,0 +1,52 @@
#include "flat_card_group_display_widget.h"
#include "../../../../database/card_database_manager.h"
#include "../../../../deck/deck_list_model.h"
#include "../../../../utility/card_info_comparator.h"
#include "../card_info_picture_with_text_overlay_widget.h"
#include <QResizeEvent>
#include <utility>
FlatCardGroupDisplayWidget::FlatCardGroupDisplayWidget(QWidget *parent,
DeckListModel *_deckListModel,
QPersistentModelIndex _trackedIndex,
QString _zoneName,
QString _cardGroupCategory,
QString _activeGroupCriteria,
QStringList _activeSortCriteria,
int bannerOpacity,
CardSizeWidget *_cardSizeWidget)
: CardGroupDisplayWidget(parent,
_deckListModel,
std::move(_trackedIndex),
_zoneName,
_cardGroupCategory,
_activeGroupCriteria,
_activeSortCriteria,
bannerOpacity,
_cardSizeWidget)
{
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
banner->setBuddy(flowWidget);
layout->addWidget(flowWidget);
for (const QPersistentModelIndex &idx : indexToWidgetMap.keys()) {
FlatCardGroupDisplayWidget::removeFromLayout(indexToWidgetMap.value(idx));
indexToWidgetMap.value(idx)->deleteLater();
indexToWidgetMap.remove(idx);
}
FlatCardGroupDisplayWidget::updateCardDisplays();
disconnect(deckListModel, &QAbstractItemModel::rowsInserted, this, &CardGroupDisplayWidget::onCardAddition);
disconnect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &CardGroupDisplayWidget::onCardRemoval);
connect(deckListModel, &QAbstractItemModel::rowsInserted, this, &FlatCardGroupDisplayWidget::onCardAddition);
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &FlatCardGroupDisplayWidget::onCardRemoval);
}
void FlatCardGroupDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
}

View file

@ -0,0 +1,49 @@
#ifndef FLAT_CARD_GROUP_DISPLAY_WIDGET_H
#define FLAT_CARD_GROUP_DISPLAY_WIDGET_H
#include "../../general/layout_containers/flow_widget.h"
#include "card_group_display_widget.h"
class FlatCardGroupDisplayWidget : public CardGroupDisplayWidget
{
Q_OBJECT
public:
FlatCardGroupDisplayWidget(QWidget *parent,
DeckListModel *deckListModel,
QPersistentModelIndex trackedIndex,
QString zoneName,
QString cardGroupCategory,
QString activeGroupCriteria,
QStringList activeSortCriteria,
int bannerOpacity,
CardSizeWidget *cardSizeWidget);
public slots:
void resizeEvent(QResizeEvent *event) override;
private:
FlowWidget *flowWidget;
QWidget *getLayoutParent() override
{
return flowWidget;
}
void addToLayout(QWidget *toAdd) override
{
flowWidget->addWidget(toAdd);
}
void insertIntoLayout(QWidget *toInsert, int insertAt) override
{
flowWidget->insertWidgetAtIndex(toInsert, insertAt);
}
void removeFromLayout(QWidget *toRemove) override
{
flowWidget->removeWidget(toRemove);
}
};
#endif // FLAT_CARD_GROUP_DISPLAY_WIDGET_H

View file

@ -0,0 +1,58 @@
#include "overlapped_card_group_display_widget.h"
#include "../../../../database/card_database_manager.h"
#include "../../../../deck/deck_list_model.h"
#include "../../../../utility/card_info_comparator.h"
#include "../card_info_picture_with_text_overlay_widget.h"
#include <QResizeEvent>
OverlappedCardGroupDisplayWidget::OverlappedCardGroupDisplayWidget(QWidget *parent,
DeckListModel *_deckListModel,
QPersistentModelIndex _trackedIndex,
QString _zoneName,
QString _cardGroupCategory,
QString _activeGroupCriteria,
QStringList _activeSortCriteria,
int bannerOpacity,
CardSizeWidget *_cardSizeWidget)
: CardGroupDisplayWidget(parent,
_deckListModel,
_trackedIndex,
_zoneName,
_cardGroupCategory,
_activeGroupCriteria,
_activeSortCriteria,
bannerOpacity,
_cardSizeWidget)
{
overlapWidget = new OverlapWidget(this, 80, 1, 1, Qt::Vertical, true);
banner->setBuddy(overlapWidget);
layout->addWidget(overlapWidget);
for (const QPersistentModelIndex &idx : indexToWidgetMap.keys()) {
OverlappedCardGroupDisplayWidget::removeFromLayout(indexToWidgetMap.value(idx));
indexToWidgetMap.value(idx)->deleteLater();
indexToWidgetMap.remove(idx);
}
OverlappedCardGroupDisplayWidget::updateCardDisplays();
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, this,
[this]() { overlapWidget->adjustMaxColumnsAndRows(); });
disconnect(deckListModel, &QAbstractItemModel::rowsInserted, this, &CardGroupDisplayWidget::onCardAddition);
disconnect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &CardGroupDisplayWidget::onCardRemoval);
connect(deckListModel, &QAbstractItemModel::rowsInserted, this, &OverlappedCardGroupDisplayWidget::onCardAddition);
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &OverlappedCardGroupDisplayWidget::onCardRemoval);
}
void OverlappedCardGroupDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
overlapWidget->resize(event->size());
overlapWidget->adjustMaxColumnsAndRows();
}

View file

@ -0,0 +1,49 @@
#ifndef OVERLAPPED_CARD_GROUP_DISPLAY_WIDGET_H
#define OVERLAPPED_CARD_GROUP_DISPLAY_WIDGET_H
#include "../../general/layout_containers/overlap_widget.h"
#include "card_group_display_widget.h"
class OverlappedCardGroupDisplayWidget : public CardGroupDisplayWidget
{
Q_OBJECT
public:
OverlappedCardGroupDisplayWidget(QWidget *parent,
DeckListModel *deckListModel,
QPersistentModelIndex trackedIndex,
QString zoneName,
QString cardGroupCategory,
QString activeGroupCriteria,
QStringList activeSortCriteria,
int bannerOpacity,
CardSizeWidget *cardSizeWidget);
public slots:
void resizeEvent(QResizeEvent *event) override;
private:
OverlapWidget *overlapWidget;
QWidget *getLayoutParent() override
{
return overlapWidget;
}
void addToLayout(QWidget *toAdd) override
{
overlapWidget->addWidget(toAdd);
}
void insertIntoLayout(QWidget *toInsert, int insertAt) override
{
overlapWidget->insertWidgetAtIndex(toInsert, insertAt);
}
void removeFromLayout(QWidget *toRemove) override
{
overlapWidget->removeWidget(toRemove);
}
};
#endif // OVERLAPPED_CARD_GROUP_DISPLAY_WIDGET_H

View file

@ -0,0 +1,74 @@
#include "card_info_display_widget.h"
#include "../../../database/card_database_manager.h"
#include "../../../game/board/card_item.h"
#include "../../../main.h"
#include "card_info_picture_widget.h"
#include "card_info_text_widget.h"
#include <QApplication>
#include <QScreen>
#include <QVBoxLayout>
#include <utility>
CardInfoDisplayWidget::CardInfoDisplayWidget(const CardRef &cardRef, QWidget *parent, Qt::WindowFlags flags)
: QFrame(parent, flags), aspectRatio((qreal)CARD_HEIGHT / (qreal)CARD_WIDTH)
{
setContentsMargins(3, 3, 3, 3);
pic = new CardInfoPictureWidget();
pic->setObjectName("pic");
text = new CardInfoTextWidget();
text->setObjectName("text");
connect(text, &CardInfoTextWidget::linkActivated, this, [this](const QString &card) { setCard({card}); });
auto *layout = new QVBoxLayout();
layout->setObjectName("layout");
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addWidget(pic, 0, Qt::AlignCenter);
layout->addWidget(text, 0, Qt::AlignCenter);
setLayout(layout);
setFrameStyle(QFrame::Panel | QFrame::Raised);
int pixmapHeight = QGuiApplication::primaryScreen()->geometry().height() / 3;
int pixmapWidth = static_cast<int>(pixmapHeight / aspectRatio);
pic->setFixedWidth(pixmapWidth);
pic->setFixedHeight(pixmapHeight);
setFixedWidth(pixmapWidth + 150);
setCard(cardRef);
// ensure our parent gets a valid size to position us correctly
resize(width(), sizeHint().height());
}
void CardInfoDisplayWidget::setCard(const ExactCard &card)
{
if (exactCard)
disconnect(exactCard.getCardPtr().data(), nullptr, this, nullptr);
exactCard = card;
if (exactCard)
connect(exactCard.getCardPtr().data(), &QObject::destroyed, this, &CardInfoDisplayWidget::clear);
text->setCard(exactCard.getCardPtr());
pic->setCard(exactCard);
}
void CardInfoDisplayWidget::setCard(const CardRef &cardRef)
{
setCard(CardDatabaseManager::getInstance()->guessCard(cardRef));
if (exactCard.isEmpty()) {
text->setInvalidCardName(cardRef.name);
}
}
void CardInfoDisplayWidget::setCard(AbstractCardItem *card)
{
setCard(card->getCard());
}
void CardInfoDisplayWidget::clear()
{
setCard(ExactCard());
}

View file

@ -0,0 +1,37 @@
#ifndef CARDINFOWIDGET_H
#define CARDINFOWIDGET_H
#include "../../../card/exact_card.h"
#include "card_ref.h"
#include <QComboBox>
#include <QFrame>
#include <QStringList>
class CardInfoPictureWidget;
class CardInfoTextWidget;
class AbstractCardItem;
class CardInfoDisplayWidget : public QFrame
{
Q_OBJECT
private:
qreal aspectRatio;
ExactCard exactCard;
CardInfoPictureWidget *pic;
CardInfoTextWidget *text;
public:
explicit CardInfoDisplayWidget(const CardRef &cardRef, QWidget *parent = nullptr, Qt::WindowFlags f = {});
public slots:
void setCard(const ExactCard &card);
void setCard(const CardRef &cardRef);
void setCard(AbstractCardItem *card);
private slots:
void clear();
};
#endif

View file

@ -0,0 +1,194 @@
#include "card_info_frame_widget.h"
#include "../../../database/card_database_manager.h"
#include "../../../game/board/card_item.h"
#include "../../../settings/cache_settings.h"
#include "card_info_display_widget.h"
#include "card_info_picture_widget.h"
#include "card_info_text_widget.h"
#include <QSplitter>
#include <QVBoxLayout>
#include <utility>
CardInfoFrameWidget::CardInfoFrameWidget(QWidget *parent)
: QTabWidget(parent), viewTransformationButton(nullptr), cardTextOnly(false)
{
setContentsMargins(3, 3, 3, 3);
pic = new CardInfoPictureWidget();
pic->setObjectName("pic");
connect(pic, &CardInfoPictureWidget::cardChanged, this,
qOverload<const ExactCard &>(&CardInfoFrameWidget::setCard));
text = new CardInfoTextWidget();
text->setObjectName("text");
connect(text, &CardInfoTextWidget::linkActivated, this, qOverload<const QString &>(&CardInfoFrameWidget::setCard));
tab1 = new QWidget(this);
tab2 = new QWidget(this);
tab3 = new QWidget(this);
tab1->setObjectName("tab1");
tab2->setObjectName("tab2");
tab3->setObjectName("tab3");
insertTab(ImageOnlyView, tab1, QString());
insertTab(TextOnlyView, tab2, QString());
insertTab(ImageAndTextView, tab3, QString());
connect(this, &CardInfoFrameWidget::currentChanged, this, &CardInfoFrameWidget::setViewMode);
tab1Layout = new QVBoxLayout();
tab1Layout->setObjectName("tab1Layout");
tab1Layout->setContentsMargins(0, 0, 0, 0);
tab1Layout->setSpacing(0);
tab1->setLayout(tab1Layout);
tab2Layout = new QVBoxLayout();
tab2Layout->setObjectName("tab2Layout");
tab2Layout->setContentsMargins(0, 0, 0, 0);
tab2Layout->setSpacing(0);
tab2->setLayout(tab2Layout);
splitter = new QSplitter();
splitter->setObjectName("splitter");
splitter->setOrientation(Qt::Vertical);
tab3Layout = new QVBoxLayout();
tab3Layout->setObjectName("tab3Layout");
tab3Layout->setContentsMargins(0, 0, 0, 0);
tab3Layout->setSpacing(0);
tab3Layout->addWidget(splitter);
tab3->setLayout(tab3Layout);
setViewMode(SettingsCache::instance().getCardInfoViewMode());
}
void CardInfoFrameWidget::retranslateUi()
{
setTabText(ImageOnlyView, tr("Image"));
setTabText(TextOnlyView, tr("Description"));
setTabText(ImageAndTextView, tr("Both"));
if (viewTransformationButton) {
viewTransformationButton->setText(tr("View transformation"));
}
}
void CardInfoFrameWidget::setViewTransformationButtonVisibility(bool visible)
{
if (!viewTransformationButton && visible) {
viewTransformationButton = new QPushButton();
viewTransformationButton->setObjectName("viewTransformationButton");
connect(viewTransformationButton, &QPushButton::clicked, this, &CardInfoFrameWidget::viewTransformation);
refreshLayout();
} else if (viewTransformationButton && !visible) {
// Deleting a widget automatically removes it from its parent
viewTransformationButton->deleteLater();
viewTransformationButton = nullptr;
}
}
/**
* Adds the widgets to the layouts that are relevant to the currently active tab.
*
* QWidgets can only have one parent, so we need to re-parent the shared widgets whenever we switch tabs.
*/
void CardInfoFrameWidget::refreshLayout()
{
switch (currentIndex()) {
case ImageOnlyView:
case TextOnlyView:
// We need to always parent all widgets, even the ones that aren't visible,
// since an unparented widget becomes free-floating.
tab1Layout->addWidget(pic);
if (viewTransformationButton) {
tab1Layout->addWidget(viewTransformationButton);
}
tab2Layout->addWidget(text);
break;
case ImageAndTextView:
splitter->addWidget(pic);
if (viewTransformationButton) {
splitter->addWidget(viewTransformationButton);
}
splitter->addWidget(text);
break;
default:
break;
}
retranslateUi();
}
void CardInfoFrameWidget::setViewMode(int mode)
{
if (currentIndex() != mode) {
setCurrentIndex(mode);
}
refreshLayout();
SettingsCache::instance().setCardInfoViewMode(mode);
}
static bool hasTransformation(const CardInfo &info)
{
for (const auto &cardRelation : info.getAllRelatedCards()) {
if (cardRelation->getDoesTransform()) {
return true;
}
}
return false;
}
void CardInfoFrameWidget::setCard(const ExactCard &card)
{
if (exactCard) {
disconnect(exactCard.getCardPtr().data(), nullptr, this, nullptr);
}
exactCard = card;
if (exactCard) {
connect(exactCard.getCardPtr().data(), &QObject::destroyed, this, &CardInfoFrameWidget::clearCard);
}
setViewTransformationButtonVisibility(hasTransformation(exactCard.getInfo()));
text->setCard(exactCard.getCardPtr());
pic->setCard(exactCard);
}
void CardInfoFrameWidget::setCard(const QString &cardName)
{
setCard(CardDatabaseManager::getInstance()->guessCard({cardName}));
}
void CardInfoFrameWidget::setCard(const CardRef &cardRef)
{
setCard(CardDatabaseManager::getInstance()->getCard(cardRef));
}
void CardInfoFrameWidget::setCard(AbstractCardItem *card)
{
if (card) {
setCard(card->getCard());
}
}
void CardInfoFrameWidget::viewTransformation()
{
if (exactCard) {
const auto &cardRelations = exactCard.getInfo().getAllRelatedCards();
for (const auto &cardRelation : cardRelations) {
if (cardRelation->getDoesTransform()) {
setCard(cardRelation->getName());
break;
}
}
}
}
void CardInfoFrameWidget::clearCard()
{
setCard(ExactCard());
}

View file

@ -0,0 +1,57 @@
#ifndef CARDFRAME_H
#define CARDFRAME_H
#include "../../../card/exact_card.h"
#include "card_ref.h"
#include <QPushButton>
#include <QTabWidget>
class AbstractCardItem;
class CardInfoPictureWidget;
class CardInfoTextWidget;
class QVBoxLayout;
class QSplitter;
class CardInfoFrameWidget : public QTabWidget
{
Q_OBJECT
private:
ExactCard exactCard;
CardInfoPictureWidget *pic;
CardInfoTextWidget *text;
QPushButton *viewTransformationButton;
bool cardTextOnly;
QWidget *tab1, *tab2, *tab3;
QVBoxLayout *tab1Layout, *tab2Layout, *tab3Layout;
QSplitter *splitter;
void setViewTransformationButtonVisibility(bool visible);
void refreshLayout();
public:
enum ViewMode
{
ImageOnlyView,
TextOnlyView,
ImageAndTextView
};
explicit CardInfoFrameWidget(QWidget *parent = nullptr);
ExactCard getCard()
{
return exactCard;
}
void retranslateUi();
public slots:
void setCard(const ExactCard &card);
void setCard(const QString &cardName);
void setCard(const CardRef &cardRef);
void setCard(AbstractCardItem *card);
void viewTransformation();
void clearCard();
void setViewMode(int mode);
};
#endif

View file

@ -0,0 +1,41 @@
#include "card_info_picture_art_crop_widget.h"
#include "../../../picture_loader/picture_loader.h"
CardInfoPictureArtCropWidget::CardInfoPictureArtCropWidget(QWidget *parent)
: CardInfoPictureWidget(parent, false, false)
{
hide();
}
QPixmap CardInfoPictureArtCropWidget::getProcessedBackground(const QSize &targetSize)
{
// Load the full-resolution card image, not a pre-scaled one
QPixmap fullResPixmap;
if (getCard()) {
PictureLoader::getPixmap(fullResPixmap, getCard(), QSize(745, 1040)); // or a high default size
} else {
PictureLoader::getCardBackPixmap(fullResPixmap, QSize(745, 1040));
}
// Fail-safe if loading failed
if (fullResPixmap.isNull()) {
return QPixmap(targetSize);
}
const QSize sz = fullResPixmap.size();
int marginX = sz.width() * 0.07;
int topMargin = sz.height() * 0.11;
int bottomMargin = sz.height() * 0.45;
QRect foilRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin);
foilRect = foilRect.intersected(fullResPixmap.rect()); // always clamp to source bounds
// Crop first, then scale for best quality
QPixmap cropped = fullResPixmap.copy(foilRect);
QPixmap scaled = cropped.scaled(targetSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
return scaled;
}

View file

@ -0,0 +1,17 @@
#ifndef CARD_INFO_PICTURE_ART_CROP_WIDGET_H
#define CARD_INFO_PICTURE_ART_CROP_WIDGET_H
#include "card_info_picture_widget.h"
class CardInfoPictureArtCropWidget : public CardInfoPictureWidget
{
Q_OBJECT
public:
explicit CardInfoPictureArtCropWidget(QWidget *parent = nullptr);
// Returns a processed (cropped & scaled) version of the pixmap
QPixmap getProcessedBackground(const QSize &targetSize);
};
#endif // CARD_INFO_PICTURE_ART_CROP_WIDGET_H

View file

@ -0,0 +1,102 @@
#include "card_info_picture_enlarged_widget.h"
#include "../../../picture_loader/picture_loader.h"
#include "../../../settings/cache_settings.h"
#include <QPainterPath>
#include <QStylePainter>
#include <utility>
/**
* @brief Constructs a CardPictureEnlargedWidget.
* @param parent The parent widget.
*
* Sets the widget's window flags to keep it displayed as a tooltip overlay.
*/
CardInfoPictureEnlargedWidget::CardInfoPictureEnlargedWidget(QWidget *parent) : QWidget(parent), pixmapDirty(true)
{
setWindowFlags(Qt::ToolTip); // Keeps this widget on top of everything
setAttribute(Qt::WA_TranslucentBackground);
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
Q_UNUSED(_roundCardCorners);
update();
});
}
/**
* @brief Loads the pixmap based on the given size and card information.
* @param size The desired size for the loaded pixmap.
*
* If card information is available, it loads the card's specific pixmap. Otherwise, it loads a default card back
* pixmap.
*/
void CardInfoPictureEnlargedWidget::loadPixmap(const QSize &size)
{
if (card) {
PictureLoader::getPixmap(enlargedPixmap, card, size);
} else {
PictureLoader::getCardBackPixmap(enlargedPixmap, size);
}
pixmapDirty = false;
}
/**
* @brief Sets the pixmap for the widget based on a provided card.
* @param _card The card information to load.
* @param size The desired size for the pixmap.
*
* Sets the widget's pixmap to the card image and resizes the widget to match the specified size. Triggers a repaint.
*/
void CardInfoPictureEnlargedWidget::setCardPixmap(const ExactCard &_card, const QSize size)
{
card = _card;
loadPixmap(size);
setFixedSize(size); // Set the widget size to the enlarged size
update(); // Trigger a repaint
}
/**
* @brief Custom paint event that draws the enlarged card image with rounded corners.
* @param event The paint event (unused).
*
* Checks if the pixmap is valid. Then, calculates the size and position for centering the
* scaled pixmap within the widget, applies rounded corners, and draws the pixmap.
*/
void CardInfoPictureEnlargedWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
if (width() == 0 || height() == 0 || enlargedPixmap.isNull()) {
return;
}
if (pixmapDirty) {
loadPixmap(size());
}
// Scale the size of the pixmap to fit the widget while maintaining the aspect ratio
QSize scaledSize = enlargedPixmap.size().scaled(size().width(), size().height(), Qt::KeepAspectRatio);
// Calculate the position to center the scaled pixmap
QPoint topLeft{(width() - scaledSize.width()) / 2, (height() - scaledSize.height()) / 2};
// Define the radius for rounded corners
// Adjust the radius as needed for rounded corners
qreal radius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * scaledSize.width() : 0.;
QStylePainter painter(this);
// Fill the background with transparent color to ensure rounded corners are rendered properly
painter.fillRect(rect(), Qt::transparent); // Use the transparent background
QPainterPath shape;
shape.addRoundedRect(QRect(topLeft, scaledSize), radius, radius);
painter.setClipPath(shape); // Set the clipping path
// Draw the pixmap scaled to the calculated size
painter.drawItemPixmap(QRect(topLeft, scaledSize), Qt::AlignCenter,
enlargedPixmap.scaled(scaledSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
}

View file

@ -0,0 +1,38 @@
#ifndef CARD_PICTURE_ENLARGED_WIDGET_H
#define CARD_PICTURE_ENLARGED_WIDGET_H
#include "../../../card/exact_card.h"
#include <QPixmap>
#include <QWidget>
class CardInfoPictureEnlargedWidget final : public QWidget
{
Q_OBJECT
public:
// Constructor
explicit CardInfoPictureEnlargedWidget(QWidget *parent = nullptr);
// Sets the card pixmap to display
void setCardPixmap(const ExactCard &_card, QSize size);
protected:
// Handles the painting event for the enlarged card
void paintEvent(QPaintEvent *event) override;
private:
// Cached pixmap for the enlarged card
QPixmap enlargedPixmap;
// Tracks if the pixmap needs to be refreshed/redrawn
bool pixmapDirty;
// Card information
ExactCard card;
// Loads the enlarged card pixmap
void loadPixmap(const QSize &size);
};
#endif // CARD_PICTURE_ENLARGED_WIDGET_H

View file

@ -0,0 +1,473 @@
#include "card_info_picture_widget.h"
#include "../../../database/card_database_manager.h"
#include "../../../game/board/card_item.h"
#include "../../../picture_loader/picture_loader.h"
#include "../../../settings/cache_settings.h"
#include "../../../tabs/tab_supervisor.h"
#include "../../window_main.h"
#include <QMenu>
#include <QMouseEvent>
#include <QScreen>
#include <QStylePainter>
#include <QWidget>
#include <utility>
/**
* @class CardInfoPictureWidget
* @brief Widget that displays an enlarged image of a card, loading the image based on the card's info or showing a
* default image.
*
* This widget can optionally display a larger version of the card's image when hovered over,
* depending on the `hoverToZoomEnabled` parameter.
*/
/**
* @brief Constructs a CardInfoPictureWidget.
* @param parent The parent widget, if any.
* @param hoverToZoomEnabled If this widget will spawn a larger widget when hovered over.
*
* Initializes the widget with a minimum height and sets the pixmap to a dirty state for initial loading.
*/
CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverToZoomEnabled, const bool _raiseOnEnter)
: QWidget(parent), pixmapDirty(true), hoverToZoomEnabled(_hoverToZoomEnabled), raiseOnEnter(_raiseOnEnter)
{
setMinimumHeight(baseHeight);
if (hoverToZoomEnabled) {
setMouseTracking(true);
}
enlargedPixmapWidget = new CardInfoPictureEnlargedWidget(this->window());
enlargedPixmapWidget->hide();
connect(this, &QObject::destroyed, enlargedPixmapWidget, &CardInfoPictureEnlargedWidget::deleteLater);
hoverTimer = new QTimer(this);
hoverTimer->setSingleShot(true);
connect(hoverTimer, &QTimer::timeout, this, &CardInfoPictureWidget::showEnlargedPixmap);
// Store the widget's original position
originalPos = this->pos();
// Create the animation
animation = new QPropertyAnimation(this, "pos");
animation->setDuration(200); // 200ms animation duration
animation->setEasingCurve(QEasingCurve::OutQuad);
animation->setStartValue(originalPos);
animation->setEndValue(originalPos - QPoint(0, animationOffset));
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
Q_UNUSED(_roundCardCorners);
update();
});
}
/**
* @brief Sets the card to be displayed and updates the pixmap.
* @param card A shared pointer to the card information (CardInfoPtr).
*
* Disconnects any existing signal connections from the previous card info and connects to the `pixmapUpdated`
* signal of the new card to automatically update the pixmap when the card image changes.
*/
void CardInfoPictureWidget::setCard(const ExactCard &card)
{
if (exactCard.getCardPtr()) {
disconnect(exactCard.getCardPtr().data(), nullptr, this, nullptr);
}
exactCard = card;
if (exactCard.getCardPtr()) {
connect(exactCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, &CardInfoPictureWidget::updatePixmap);
}
updatePixmap();
}
/**
* @brief Sets the hover to zoom feature.
* @param enabled If true, enables the hover-to-zoom functionality; otherwise, disables it.
*/
void CardInfoPictureWidget::setHoverToZoomEnabled(const bool enabled)
{
hoverToZoomEnabled = enabled;
setMouseTracking(enabled);
}
void CardInfoPictureWidget::setRaiseOnEnterEnabled(const bool enabled)
{
raiseOnEnter = enabled;
}
/**
* @brief Handles widget resizing by updating the pixmap size.
* @param event The resize event (unused).
*
* Calls `updatePixmap()` to ensure the image scales appropriately when the widget is resized.
*/
void CardInfoPictureWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
originalPos = pos(); // Update the baseline position
updatePixmap();
}
/**
* @brief Sets the scale factor for the widget.
* @param scale The scale factor to apply.
*
* Adjusts the widget's size according to the scale factor and updates the pixmap.
*/
void CardInfoPictureWidget::setScaleFactor(const int scale)
{
const int newWidth = baseWidth * scale / 100;
const int newHeight = static_cast<int>(newWidth * aspectRatio);
scaleFactor = scale;
setFixedSize(newWidth, newHeight);
updatePixmap();
emit cardScaleFactorChanged(scale);
}
/**
* @brief Marks the pixmap as dirty and triggers a widget repaint.
*
* Sets `pixmapDirty` to true, indicating that the pixmap needs to be reloaded before the next display.
*/
void CardInfoPictureWidget::updatePixmap()
{
pixmapDirty = true;
update();
}
/**
* @brief Loads the appropriate pixmap based on the current card info.
*
* If `info` is valid, loads the card's image. Otherwise, loads a default card back image.
*/
void CardInfoPictureWidget::loadPixmap()
{
PictureLoader::getCardBackLoadingInProgressPixmap(resizedPixmap, size());
if (exactCard) {
PictureLoader::getPixmap(resizedPixmap, exactCard, size());
} else {
PictureLoader::getCardBackLoadingFailedPixmap(resizedPixmap, size());
}
pixmapDirty = false;
}
/**
* @brief Custom paint event that draws the card image with rounded corners.
* @param event The paint event (unused).
*
* Checks if the pixmap needs to be reloaded. Then, calculates the size and position for centering the
* scaled pixmap within the widget, applies rounded corners, and draws the pixmap.
*/
void CardInfoPictureWidget::paintEvent(QPaintEvent *event)
{
QWidget::paintEvent(event);
if (width() == 0 || height() == 0) {
return;
}
if (pixmapDirty) {
loadPixmap();
}
QPixmap transformedPixmap = resizedPixmap; // Default pixmap
if (SettingsCache::instance().getAutoRotateSidewaysLayoutCards()) {
if (exactCard.getInfo().getLandscapeOrientation()) {
// Rotate pixmap 90 degrees to the left
QTransform transform;
transform.rotate(90);
transformedPixmap = resizedPixmap.transformed(transform, Qt::SmoothTransformation);
}
}
// Handle DPI scaling
qreal dpr = devicePixelRatio(); // Get the actual scaling factor
QSize availableSize = size() * dpr; // Convert to physical pixel size
// Compute final scaled size
QSize pixmapSize = transformedPixmap.size();
QSize scaledSize = pixmapSize.scaled(availableSize, Qt::KeepAspectRatio);
// Pre-scale the pixmap once before drawing
QPixmap finalPixmap = transformedPixmap.scaled(scaledSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
finalPixmap.setDevicePixelRatio(dpr); // Ensure correct display on high-DPI screens
// Compute target rectangle with explicit integer conversion
int targetX = static_cast<int>((availableSize.width() - scaledSize.width()) / (2 * dpr));
int targetY = static_cast<int>((availableSize.height() - scaledSize.height()) / (2 * dpr));
int targetW = static_cast<int>(scaledSize.width() / dpr);
int targetH = static_cast<int>(scaledSize.height() / dpr);
QRect targetRect{targetX, targetY, targetW, targetH};
// Compute rounded corner radius
// Ensure consistent rounding
qreal radius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * static_cast<qreal>(targetRect.width()) : 0.;
// Draw the pixmap with rounded corners
QStylePainter painter(this);
QPainterPath shape;
shape.addRoundedRect(targetRect, radius, radius);
painter.setClipPath(shape);
// Draw the pre-scaled pixmap directly
painter.drawPixmap(targetRect, finalPixmap);
}
/**
* @brief Provides the recommended size for the widget based on the scale factor.
* @return The recommended widget size.
*/
QSize CardInfoPictureWidget::sizeHint() const
{
return {static_cast<int>(baseWidth * scaleFactor / 100.0),
static_cast<int>(baseWidth * scaleFactor / 100.0 * aspectRatio)};
}
/**
* @brief Starts the hover timer to show the enlarged pixmap on hover.
* @param event The enter event.
*/
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void CardInfoPictureWidget::enterEvent(QEnterEvent *event)
#else
void CardInfoPictureWidget::enterEvent(QEvent *event)
#endif
{
QWidget::enterEvent(event); // Call the base class implementation
// If hover-to-zoom is enabled, start the hover timer
if (hoverToZoomEnabled) {
hoverTimer->start(hoverActivateThresholdInMs);
}
// Emit signal indicating a card is being hovered on
emit hoveredOnCard(exactCard);
if (raiseOnEnter) {
if (animation->state() == QAbstractAnimation::Running) {
animation->pause(); // Pause current animation
} else {
originalPos = this->pos(); // Update the baseline position
animation->setStartValue(originalPos);
animation->setEndValue(originalPos - QPoint(0, animationOffset));
}
animation->setDirection(QAbstractAnimation::Forward);
animation->start();
}
}
/**
* @brief Stops the hover timer and hides the enlarged pixmap when the mouse leaves.
* @param event The leave event.
*/
void CardInfoPictureWidget::leaveEvent(QEvent *event)
{
QWidget::leaveEvent(event);
if (hoverToZoomEnabled) {
hoverTimer->stop();
enlargedPixmapWidget->hide();
}
if (raiseOnEnter) {
if (animation->state() == QAbstractAnimation::Running) {
animation->pause(); // Pause current animation
}
animation->setDirection(QAbstractAnimation::Backward);
animation->start();
}
}
void CardInfoPictureWidget::moveEvent(QMoveEvent *event)
{
QWidget::moveEvent(event);
hoverTimer->stop();
enlargedPixmapWidget->hide();
if (animation->state() == QAbstractAnimation::Running) {
return;
}
originalPos = this->pos(); // Update the baseline position
}
/**
* @brief Moves the enlarged pixmap widget to follow the mouse cursor.
* @param event The mouse move event.
*/
void CardInfoPictureWidget::mouseMoveEvent(QMouseEvent *event)
{
QWidget::mouseMoveEvent(event);
if (hoverToZoomEnabled && enlargedPixmapWidget->isVisible()) {
const QPoint cursorPos = QCursor::pos();
const QRect screenGeometry = QGuiApplication::screenAt(cursorPos)->geometry();
const QSize widgetSize = enlargedPixmapWidget->size();
int newX = cursorPos.x() + enlargedPixmapOffset;
int newY = cursorPos.y() + enlargedPixmapOffset;
// Adjust if out of bounds
if (newX + widgetSize.width() > screenGeometry.right()) {
newX = cursorPos.x() - widgetSize.width() - enlargedPixmapOffset;
}
if (newY + widgetSize.height() > screenGeometry.bottom()) {
newY = cursorPos.y() - widgetSize.height() - enlargedPixmapOffset;
}
enlargedPixmapWidget->move(newX, newY);
}
}
void CardInfoPictureWidget::mousePressEvent(QMouseEvent *event)
{
QWidget::mousePressEvent(event);
if (event->button() == Qt::RightButton) {
createRightClickMenu()->popup(QCursor::pos());
} else {
emit cardClicked();
}
emit cardClicked();
}
void CardInfoPictureWidget::hideEvent(QHideEvent *event)
{
enlargedPixmapWidget->hide();
QWidget::hideEvent(event);
}
QMenu *CardInfoPictureWidget::createRightClickMenu()
{
auto *cardMenu = new QMenu(this);
if (!exactCard) {
return cardMenu;
}
cardMenu->addMenu(createViewRelatedCardsMenu());
cardMenu->addMenu(createAddToOpenDeckMenu());
return cardMenu;
}
QMenu *CardInfoPictureWidget::createViewRelatedCardsMenu()
{
auto viewRelatedCards = new QMenu(tr("View related cards"));
QList<CardRelation *> relatedCards = exactCard.getInfo().getAllRelatedCards();
auto relatedCardExists = [](const CardRelation *cardRelation) {
return CardDatabaseManager::getInstance()->getCardInfo(cardRelation->getName()) != nullptr;
};
bool atLeastOneGoodRelationFound = std::any_of(relatedCards.begin(), relatedCards.end(), relatedCardExists);
if (!atLeastOneGoodRelationFound) {
viewRelatedCards->setEnabled(false);
return viewRelatedCards;
}
for (const auto &relatedCard : relatedCards) {
const auto &relatedCardName = relatedCard->getName();
QAction *viewCard = viewRelatedCards->addAction(relatedCardName);
connect(viewCard, &QAction::triggered, this, [this, &relatedCardName] {
emit cardChanged(
CardDatabaseManager::getInstance()->getCard({relatedCardName, exactCard.getPrinting().getUuid()}));
});
viewRelatedCards->addAction(viewCard);
}
return viewRelatedCards;
}
/**
* Finds the single instance of the MainWindow in this application.
*/
static MainWindow *findMainWindow()
{
for (auto widget : QApplication::topLevelWidgets()) {
if (auto mainWindow = qobject_cast<MainWindow *>(widget)) {
return mainWindow;
}
}
// This code should be unreachable
qCritical() << "Could not find MainWindow in QApplication::topLevelWidgets";
return nullptr;
}
QMenu *CardInfoPictureWidget::createAddToOpenDeckMenu()
{
auto addToOpenDeckMenu = new QMenu(tr("Add card to deck"));
auto mainWindow = findMainWindow();
QList<AbstractTabDeckEditor *> deckEditorTabs = mainWindow->getTabSupervisor()->getDeckEditorTabs();
if (deckEditorTabs.isEmpty()) {
addToOpenDeckMenu->setEnabled(false);
return addToOpenDeckMenu;
}
for (auto &deckEditorTab : deckEditorTabs) {
auto *addCardMenu = addToOpenDeckMenu->addMenu(deckEditorTab->getTabText());
QAction *addCard = addCardMenu->addAction(tr("Mainboard"));
connect(addCard, &QAction::triggered, this, [this, deckEditorTab] {
deckEditorTab->updateCard(exactCard);
deckEditorTab->actAddCard(exactCard);
});
QAction *addCardSideboard = addCardMenu->addAction(tr("Sideboard"));
connect(addCardSideboard, &QAction::triggered, this, [this, deckEditorTab] {
deckEditorTab->updateCard(exactCard);
deckEditorTab->actAddCardToSideboard(exactCard);
});
}
return addToOpenDeckMenu;
}
/**
* @brief Displays the enlarged version of the card's pixmap near the cursor.
*
* If card information is available, the enlarged pixmap is loaded, positioned near the cursor,
* and displayed.
*/
void CardInfoPictureWidget::showEnlargedPixmap() const
{
if (!exactCard) {
return;
}
const QSize enlargedSize(static_cast<int>(size().width() * 2), static_cast<int>(size().width() * aspectRatio * 2));
enlargedPixmapWidget->setCardPixmap(exactCard, enlargedSize);
const QPoint cursorPos = QCursor::pos();
const QRect screenGeometry = QGuiApplication::screenAt(cursorPos)->geometry();
const QSize widgetSize = enlargedPixmapWidget->size();
int newX = cursorPos.x() + enlargedPixmapOffset;
int newY = cursorPos.y() + enlargedPixmapOffset;
// Adjust if out of bounds
if (newX + widgetSize.width() > screenGeometry.right()) {
newX = cursorPos.x() - widgetSize.width() - enlargedPixmapOffset;
}
if (newY + widgetSize.height() > screenGeometry.bottom()) {
newY = cursorPos.y() - widgetSize.height() - enlargedPixmapOffset;
}
enlargedPixmapWidget->move(newX, newY);
enlargedPixmapWidget->show();
}

View file

@ -0,0 +1,88 @@
#ifndef CARD_INFO_PICTURE_H
#define CARD_INFO_PICTURE_H
#include "../../../card/exact_card.h"
#include "card_info_picture_enlarged_widget.h"
#include <QPropertyAnimation>
#include <QTimer>
#include <QWidget>
inline Q_LOGGING_CATEGORY(CardInfoPictureWidgetLog, "card_info_picture_widget");
class AbstractCardItem;
class QMenu;
class CardInfoPictureWidget : public QWidget
{
Q_OBJECT
public:
explicit CardInfoPictureWidget(QWidget *parent = nullptr,
bool hoverToZoomEnabled = false,
bool raiseOnEnter = false);
ExactCard getCard()
{
return exactCard;
}
[[nodiscard]] QSize sizeHint() const override;
public slots:
void setCard(const ExactCard &card);
void setScaleFactor(int scale); // New slot for scaling
void setHoverToZoomEnabled(bool enabled);
void setRaiseOnEnterEnabled(bool enabled);
void updatePixmap();
signals:
void hoveredOnCard(const ExactCard &hoveredCard);
void cardScaleFactorChanged(int _scale);
void cardChanged(const ExactCard &card);
void cardClicked();
protected:
void resizeEvent(QResizeEvent *event) override;
void paintEvent(QPaintEvent *) override;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void enterEvent(QEnterEvent *event) override; // Qt6 signature
#else
void enterEvent(QEvent *event) override; // Qt5 signature
#endif
void leaveEvent(QEvent *event) override;
void moveEvent(QMoveEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void hideEvent(QHideEvent *event) override;
void loadPixmap();
[[nodiscard]] const QPixmap &getResizedPixmap() const
{
return resizedPixmap;
}
void showEnlargedPixmap() const;
private:
ExactCard exactCard;
qreal magicTheGatheringCardAspectRatio = 1.396;
qreal yuGiOhCardAspectRatio = 1.457;
qreal aspectRatio = magicTheGatheringCardAspectRatio;
int baseWidth = 200;
int baseHeight = 200;
double scaleFactor = 100;
QPixmap resizedPixmap;
bool pixmapDirty;
bool hoverToZoomEnabled;
bool raiseOnEnter;
int hoverActivateThresholdInMs = 500;
CardInfoPictureEnlargedWidget *enlargedPixmapWidget = nullptr;
int enlargedPixmapOffset = 10;
QTimer *hoverTimer;
QPropertyAnimation *animation;
QPoint originalPos; // Store the original position
const int animationOffset = 10; // Adjust this for how much the widget moves up
QMenu *createRightClickMenu();
QMenu *createViewRelatedCardsMenu();
QMenu *createAddToOpenDeckMenu();
};
#endif

View file

@ -0,0 +1,245 @@
#include "card_info_picture_with_text_overlay_widget.h"
#include <QFontMetrics>
#include <QPainterPath>
#include <QStylePainter>
#include <QTextOption>
/**
* @brief Constructs a CardPictureWithTextOverlay widget.
* @param parent The parent widget.
* @param hoverToZoomEnabled If this widget will spawn a larger widget when hovered over.
* @param raiseOnEnter If this widget will raise slightly when entered.
* @param textColor The color of the overlay text.
* @param outlineColor The color of the outline around the text.
* @param fontSize The font size of the overlay text.
* @param alignment The alignment of the text within the overlay.
*
* Sets the widget's size policy and default border style.
*/
CardInfoPictureWithTextOverlayWidget::CardInfoPictureWithTextOverlayWidget(QWidget *parent,
const bool hoverToZoomEnabled,
const bool raiseOnEnter,
const QColor &textColor,
const QColor &outlineColor,
const int fontSize,
const Qt::Alignment alignment)
: CardInfoPictureWidget(parent, hoverToZoomEnabled, raiseOnEnter), textColor(textColor), outlineColor(outlineColor),
fontSize(fontSize), textAlignment(alignment)
{
this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
}
/**
* @brief Sets the overlay text to be displayed on the card.
* @param text The text to overlay.
*
* Updates the widget to display the new overlay text.
*/
void CardInfoPictureWithTextOverlayWidget::setOverlayText(const QString &text)
{
overlayText = text;
update(); // Trigger a redraw to display the updated text
}
/**
* @brief Sets the color of the overlay text.
* @param color The new text color.
*/
void CardInfoPictureWithTextOverlayWidget::setTextColor(const QColor &color)
{
textColor = color;
update();
}
/**
* @brief Sets the outline color around the overlay text.
* @param color The new outline color.
*/
void CardInfoPictureWithTextOverlayWidget::setOutlineColor(const QColor &color)
{
outlineColor = color;
update();
}
/**
* @brief Sets the font size for the overlay text.
* @param size The new font size.
*/
void CardInfoPictureWithTextOverlayWidget::setFontSize(const int size)
{
fontSize = size > 0 ? size : 1;
update();
}
/**
* @brief Sets the alignment of the overlay text within the widget.
* @param alignment The new text alignment.
*/
void CardInfoPictureWithTextOverlayWidget::setTextAlignment(const Qt::Alignment alignment)
{
textAlignment = alignment;
update();
}
void CardInfoPictureWithTextOverlayWidget::mousePressEvent(QMouseEvent *event)
{
emit imageClicked(event, this);
}
/**
* @brief Paints the widget, including both the card image and the text overlay.
* @param event The paint event.
*
* Draws the card image first, then overlays text on top. The text is wrapped and centered within the image.
*/
void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
{
// Call the base class's paintEvent to draw the card image
CardInfoPictureWidget::paintEvent(event);
// If no overlay text, skip drawing the text
if (overlayText.isEmpty()) {
return;
}
QStylePainter painter(this);
// Get the pixmap from the base class using the getter
const QPixmap &pixmap = getResizedPixmap();
if (pixmap.isNull()) {
return;
}
// Calculate size and position for drawing
const QSize scaledSize = pixmap.size().scaled(size(), Qt::KeepAspectRatio);
const QPoint topLeft{(width() - scaledSize.width()) / 2, (height() - scaledSize.height()) / 2};
const QRect pixmapRect(topLeft, scaledSize);
// Calculate the optimal font size
QFont font = painter.font();
int optimalFontSize = fontSize; // Start with the user-defined font size
const QFontMetrics baseMetrics(font);
int textWidth = pixmapRect.width();
// Reduce the font size until the text fits within the pixmap's width
do {
font.setPointSize(optimalFontSize);
QFontMetrics fm(font);
int currentWidth = 0;
for (const QString &word : overlayText.split(' ')) {
currentWidth = std::max(currentWidth, fm.horizontalAdvance(word));
}
if (currentWidth <= textWidth) {
break;
}
--optimalFontSize;
} while (optimalFontSize > 1);
// Apply the calculated font size
painter.setFont(font);
// Wrap the text to fit within the pixmap width
const QFontMetrics fontMetrics(font);
QString wrappedText;
QString currentLine;
QStringList words = overlayText.split(' ');
for (const QString &word : words) {
if (fontMetrics.horizontalAdvance(currentLine + " " + word) > textWidth) {
wrappedText += currentLine + '\n';
currentLine = word;
} else {
if (!currentLine.isEmpty()) {
currentLine += " ";
}
currentLine += word;
}
}
wrappedText += currentLine;
// Calculate total text block height
int totalTextHeight = wrappedText.count('\n') * fontMetrics.height() + fontMetrics.height();
// Adjust font size if the total text height exceeds the pixmap height
while (totalTextHeight > pixmapRect.height() && optimalFontSize > 1) {
--optimalFontSize;
font.setPointSize(optimalFontSize);
painter.setFont(font);
const QFontMetrics newMetrics(font);
totalTextHeight = wrappedText.count('\n') * newMetrics.height() + newMetrics.height();
}
// Set up the text layout options
QTextOption textOption;
textOption.setAlignment(textAlignment);
// Create a text rectangle centered vertically within the pixmap rect
auto textRect = QRect(pixmapRect.left(), pixmapRect.top(), pixmapRect.width(), totalTextHeight);
textRect.moveTop((pixmapRect.height() - totalTextHeight) / 2 + pixmapRect.top());
// Draw the outlined text
drawOutlinedText(painter, textRect, wrappedText, textOption);
}
/**
* @brief Draws text with an outline for visibility.
* @param painter The painter to draw the text.
* @param textRect The rectangle area to draw the text in.
* @param text The text to display.
* @param textOption The text layout options, such as alignment.
*
* Draws an outline around the text to enhance readability before drawing the main text.
*/
void CardInfoPictureWithTextOverlayWidget::drawOutlinedText(QPainter &painter,
const QRect &textRect,
const QString &text,
const QTextOption &textOption) const
{
painter.setPen(outlineColor);
for (int dx = -1; dx <= 1; ++dx) {
for (int dy = -1; dy <= 1; ++dy) {
if (dx != 0 || dy != 0) {
QRect shiftedTextRect = textRect.translated(dx, dy);
painter.drawText(shiftedTextRect, text, textOption);
}
}
}
// Draw the main text
painter.setPen(textColor);
painter.drawText(textRect, text, textOption);
}
/**
* @brief Provides the recommended size for this widget.
* @return The suggested widget size.
*/
QSize CardInfoPictureWithTextOverlayWidget::sizeHint() const
{
return CardInfoPictureWidget::sizeHint();
}
/**
* @brief Provides the minimum recommended size for this widget.
* @return The minimum widget size.
*/
QSize CardInfoPictureWithTextOverlayWidget::minimumSizeHint() const
{
// Same as sizeHint, but ensure that there is at least some space for the pixmap
const QPixmap &pixmap = getResizedPixmap();
const QSize pixmapSize = pixmap.isNull() ? QSize(0, 0) : pixmap.size();
// Get the font metrics for the overlay text
QFont font;
font.setPointSize(fontSize);
const QFontMetrics fontMetrics(font);
// Calculate the height required for the text
const QStringList lines = overlayText.split('\n');
const int totalTextHeight = static_cast<int>(lines.size()) * fontMetrics.height();
// Return the maximum width and combined height
return {pixmapSize.width(), pixmapSize.height() + totalTextHeight};
}

View file

@ -0,0 +1,51 @@
#ifndef CARD_PICTURE_WITH_TEXT_OVERLAY_H
#define CARD_PICTURE_WITH_TEXT_OVERLAY_H
#include "card_info_picture_widget.h"
#include <QColor>
#include <QSize>
#include <QTextOption>
class CardInfoPictureWithTextOverlayWidget : public CardInfoPictureWidget
{
Q_OBJECT
public:
explicit CardInfoPictureWithTextOverlayWidget(QWidget *parent = nullptr,
bool hoverToZoomEnabled = false,
bool raiseOnEnter = false,
const QColor &textColor = Qt::white,
const QColor &outlineColor = Qt::black,
int fontSize = 12,
Qt::Alignment alignment = Qt::AlignCenter);
void setOverlayText(const QString &text);
void setTextColor(const QColor &color);
void setOutlineColor(const QColor &color);
void setFontSize(int size);
void setTextAlignment(Qt::Alignment alignment);
[[nodiscard]] QSize sizeHint() const override;
signals:
void imageClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance);
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
[[nodiscard]] QSize minimumSizeHint() const override;
private:
void drawOutlinedText(QPainter &painter,
const QRect &textRect,
const QString &text,
const QTextOption &textOption) const;
QString overlayText;
QColor textColor;
QColor outlineColor;
int fontSize;
Qt::Alignment textAlignment;
};
#endif // CARD_PICTURE_WITH_TEXT_OVERLAY_H

View file

@ -0,0 +1,80 @@
#include "card_info_text_widget.h"
#include "../../../card/game_specific_terms.h"
#include "../../../game/board/card_item.h"
#include <QGridLayout>
#include <QLabel>
#include <QTextEdit>
CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent), info(nullptr)
{
nameLabel = new QLabel;
nameLabel->setOpenExternalLinks(false);
nameLabel->setWordWrap(true);
connect(nameLabel, SIGNAL(linkActivated(const QString &)), this, SIGNAL(linkActivated(const QString &)));
textLabel = new QTextEdit();
textLabel->setReadOnly(true);
auto *grid = new QGridLayout(this);
grid->addWidget(nameLabel, 0, 0);
grid->addWidget(textLabel, 1, 0, -1, 2);
grid->setRowStretch(1, 1);
grid->setColumnStretch(1, 1);
retranslateUi();
}
void CardInfoTextWidget::setCard(CardInfoPtr card)
{
if (card == nullptr) {
nameLabel->setText("");
textLabel->setText("");
return;
}
QString text = "<table width=\"100%\" border=0 cellspacing=0 cellpadding=0>";
text += QString("<tr><td>%1</td><td width=\"5\"></td><td>%2</td></tr>")
.arg(tr("Name:"), card->getName().toHtmlEscaped());
QStringList cardProps = card->getProperties();
for (const QString &key : cardProps) {
if (key.contains("-"))
continue;
QString keyText = Mtg::getNicePropertyName(key).toHtmlEscaped() + ":";
text +=
QString("<tr><td>%1</td><td></td><td>%2</td></tr>").arg(keyText, card->getProperty(key).toHtmlEscaped());
}
auto relatedCards = card->getAllRelatedCards();
if (!relatedCards.empty()) {
text += QString("<tr><td>%1</td><td width=\"5\"></td><td>").arg(tr("Related cards:"));
for (auto *relatedCard : relatedCards) {
QString tmp = relatedCard->getName().toHtmlEscaped();
text += "<a href=\"" + tmp + "\">" + tmp + "</a><br>";
}
text += "</td></tr>";
}
text += "</table>";
nameLabel->setText(text);
textLabel->setText(card->getText());
}
void CardInfoTextWidget::setInvalidCardName(const QString &cardName)
{
nameLabel->setText(tr("Unknown card:") + " " + cardName);
textLabel->setText("");
}
void CardInfoTextWidget::retranslateUi()
{
/*
* There's no way we can really translate the text currently being rendered.
* The best we can do is invalidate the current text.
*/
setInvalidCardName("");
}

View file

@ -0,0 +1,30 @@
#ifndef CARDINFOTEXT_H
#define CARDINFOTEXT_H
#include "../../../card/card_info.h"
#include <QFrame>
class QLabel;
class QTextEdit;
class CardInfoTextWidget : public QFrame
{
Q_OBJECT
private:
QLabel *nameLabel;
QTextEdit *textLabel;
CardInfoPtr info;
public:
explicit CardInfoTextWidget(QWidget *parent = nullptr);
void retranslateUi();
void setInvalidCardName(const QString &cardName);
signals:
void linkActivated(const QString &link);
public slots:
void setCard(CardInfoPtr card);
};
#endif

View file

@ -0,0 +1,61 @@
#include "card_size_widget.h"
#include "../../../settings/cache_settings.h"
#include "../printing_selector/printing_selector.h"
#include "../visual_deck_storage/visual_deck_storage_widget.h"
/**
* @class CardSizeWidget
* @brief A widget for adjusting card sizes using a slider.
*
* This widget allows users to dynamically change the card size in a linked FlowWidget
* and updates the application's settings accordingly.
*/
CardSizeWidget::CardSizeWidget(QWidget *parent, FlowWidget *_flowWidget, int defaultValue)
: parent(parent), flowWidget(_flowWidget)
{
cardSizeLayout = new QHBoxLayout(this);
cardSizeLayout->setContentsMargins(9, 0, 9, 0);
setLayout(cardSizeLayout);
cardSizeLabel = new QLabel(tr("Card Size"), this);
cardSizeLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
cardSizeSlider = new QSlider(Qt::Horizontal, this);
cardSizeSlider->setRange(50, 250); ///< Slider range for card size adjustment.
cardSizeSlider->setValue(defaultValue); ///< Initial slider value.
cardSizeLayout->addWidget(cardSizeLabel);
cardSizeLayout->addWidget(cardSizeSlider);
if (flowWidget != nullptr) {
connect(cardSizeSlider, &QSlider::valueChanged, flowWidget, &FlowWidget::setMinimumSizeToMaxSizeHint);
}
// Debounce setup
debounceTimer.setSingleShot(true);
connect(&debounceTimer, &QTimer::timeout, this, [this] { emit cardSizeSettingUpdated(pendingValue); });
connect(cardSizeSlider, &QSlider::valueChanged, this, &CardSizeWidget::updateCardSizeSetting);
}
/**
* @brief Updates the card size setting in the application's cache.
*
* @param newValue The new card size value set by the slider.
*/
void CardSizeWidget::updateCardSizeSetting(int newValue)
{
pendingValue = newValue;
debounceTimer.start(300); // 300ms debounce time
}
/**
* @brief Gets the slider widget used for adjusting the card size.
*
* @return A pointer to the QSlider object.
*/
QSlider *CardSizeWidget::getSlider() const
{
return cardSizeSlider;
}

View file

@ -0,0 +1,41 @@
#ifndef CARD_SIZE_WIDGET_H
#define CARD_SIZE_WIDGET_H
#include "../general/layout_containers/flow_widget.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QSlider>
#include <QTimer>
#include <QWidget>
class CardSizeWidget : public QWidget
{
Q_OBJECT
public:
explicit CardSizeWidget(QWidget *parent, FlowWidget *flowWidget = nullptr, int defaultValue = 100);
[[nodiscard]] QSlider *getSlider() const;
private slots:
void updateCardSizeSetting(int newValue);
signals:
/**
* Emitted when the slider value changes, but on a debounce timer.
* Any parents that care about saving the value to settings should use this signal to indicate when to save the new
* value to settings.
*/
void cardSizeSettingUpdated(int newValue);
private:
QWidget *parent;
FlowWidget *flowWidget;
QHBoxLayout *cardSizeLayout;
QLabel *cardSizeLabel;
QSlider *cardSizeSlider;
QTimer debounceTimer; // Debounce timer
int pendingValue; // Stores the latest slider value
};
#endif // CARD_SIZE_WIDGET_H

View file

@ -0,0 +1,214 @@
#include "deck_card_zone_display_widget.h"
#include "../../../deck/deck_list_model.h"
#include "../../../utility/card_info_comparator.h"
#include "card_group_display_widgets/flat_card_group_display_widget.h"
#include "card_group_display_widgets/overlapped_card_group_display_widget.h"
#include <QResizeEvent>
DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
DeckListModel *_deckListModel,
QPersistentModelIndex _trackedIndex,
QString _zoneName,
QString _activeGroupCriteria,
QStringList _activeSortCriteria,
DisplayType _displayType,
int bannerOpacity,
int subBannerOpacity,
CardSizeWidget *_cardSizeWidget)
: QWidget(parent), deckListModel(_deckListModel), trackedIndex(_trackedIndex), zoneName(_zoneName),
activeGroupCriteria(_activeGroupCriteria), activeSortCriteria(_activeSortCriteria), displayType(_displayType),
bannerOpacity(bannerOpacity), subBannerOpacity(subBannerOpacity), cardSizeWidget(_cardSizeWidget)
{
layout = new QVBoxLayout(this);
setLayout(layout);
banner = new BannerWidget(this, zoneName, Qt::Orientation::Vertical, bannerOpacity);
layout->addWidget(banner);
cardGroupContainer = new QWidget(this);
cardGroupLayout = new QVBoxLayout(cardGroupContainer);
cardGroupContainer->setLayout(cardGroupLayout);
layout->addWidget(cardGroupContainer);
banner->setBuddy(cardGroupContainer);
displayCards();
connect(deckListModel, &QAbstractItemModel::rowsInserted, this, &DeckCardZoneDisplayWidget::onCategoryAddition);
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &DeckCardZoneDisplayWidget::onCategoryRemoval);
}
void DeckCardZoneDisplayWidget::cleanupInvalidCardGroup(CardGroupDisplayWidget *displayWidget)
{
cardGroupLayout->removeWidget(displayWidget);
displayWidget->setParent(nullptr);
for (auto idx : indexToWidgetMap.keys()) {
if (!idx.isValid()) {
indexToWidgetMap.remove(idx);
}
}
delete displayWidget;
}
void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex index)
{
auto categoryName = deckListModel->data(index.sibling(index.row(), 1), Qt::EditRole).toString();
if (indexToWidgetMap.contains(index)) {
return;
}
if (displayType == DisplayType::Overlap) {
auto *displayWidget = new OverlappedCardGroupDisplayWidget(
cardGroupContainer, deckListModel, index, zoneName, categoryName, activeGroupCriteria, activeSortCriteria,
subBannerOpacity, cardSizeWidget);
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, &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);
}
}
void DeckCardZoneDisplayWidget::displayCards()
{
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);
}
}
void DeckCardZoneDisplayWidget::onCategoryAddition(const QModelIndex &parent, int first, int last)
{
if (!trackedIndex.isValid()) {
emit requestCleanup(this);
return;
}
if (parent == trackedIndex) {
for (int i = first; i <= last; i++) {
QPersistentModelIndex index = QPersistentModelIndex(deckListModel->index(i, 0, trackedIndex));
constructAppropriateWidget(index);
}
}
}
void DeckCardZoneDisplayWidget::onCategoryRemoval(const QModelIndex &parent, int first, int last)
{
Q_UNUSED(parent);
Q_UNUSED(first);
Q_UNUSED(last);
for (const QPersistentModelIndex &idx : indexToWidgetMap.keys()) {
if (!idx.isValid()) {
cardGroupLayout->removeWidget(indexToWidgetMap.value(idx));
indexToWidgetMap.value(idx)->deleteLater();
indexToWidgetMap.remove(idx);
}
}
if (!trackedIndex.isValid()) {
emit requestCleanup(this);
}
}
void DeckCardZoneDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
for (QObject *child : layout->children()) {
QWidget *widget = qobject_cast<QWidget *>(child);
if (widget) {
widget->setMaximumWidth(width());
}
}
}
void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card)
{
emit cardClicked(event, card, zoneName);
}
void DeckCardZoneDisplayWidget::onHover(const ExactCard &card)
{
emit cardHovered(card);
}
void DeckCardZoneDisplayWidget::refreshDisplayType(const DisplayType &_displayType)
{
displayType = _displayType;
QLayoutItem *item;
while ((item = cardGroupLayout->takeAt(0)) != nullptr) {
if (item->widget()) {
item->widget()->deleteLater();
} else if (item->layout()) {
item->layout()->deleteLater();
}
delete item;
}
indexToWidgetMap.clear();
// We gotta wait for all the deleteLater's to finish so we fire after the next event cycle
auto timer = new QTimer(this);
timer->setSingleShot(true);
connect(timer, &QTimer::timeout, this, [this]() { displayCards(); });
timer->start();
}
void DeckCardZoneDisplayWidget::onActiveGroupCriteriaChanged(QString _activeGroupCriteria)
{
activeGroupCriteria = _activeGroupCriteria;
displayCards();
}
void DeckCardZoneDisplayWidget::onActiveSortCriteriaChanged(QStringList _activeSortCriteria)
{
activeSortCriteria = _activeSortCriteria;
emit activeSortCriteriaChanged(activeSortCriteria);
}
QList<QString> DeckCardZoneDisplayWidget::getGroupCriteriaValueList()
{
QList<QString> groupCriteriaValues;
QList<ExactCard> cardsInZone = deckListModel->getCardsForZone(zoneName);
for (const ExactCard &cardInZone : cardsInZone) {
groupCriteriaValues.append(cardInZone.getInfo().getProperty(activeGroupCriteria));
}
groupCriteriaValues.removeDuplicates();
groupCriteriaValues.sort();
return groupCriteriaValues;
}

View file

@ -0,0 +1,71 @@
#ifndef DECK_CARD_ZONE_DISPLAY_WIDGET_H
#define DECK_CARD_ZONE_DISPLAY_WIDGET_H
#include "../../../card/card_info.h"
#include "../../../deck/deck_list_model.h"
#include "../general/display/banner_widget.h"
#include "../general/layout_containers/overlap_widget.h"
#include "../visual_deck_editor/visual_deck_editor_widget.h"
#include "card_group_display_widgets/card_group_display_widget.h"
#include "card_info_picture_with_text_overlay_widget.h"
#include "card_size_widget.h"
#include <QVBoxLayout>
#include <QWidget>
class DeckCardZoneDisplayWidget : public QWidget
{
Q_OBJECT
public:
DeckCardZoneDisplayWidget(QWidget *parent,
DeckListModel *deckListModel,
QPersistentModelIndex trackedIndex,
QString zoneName,
QString activeGroupCriteria,
QStringList activeSortCriteria,
DisplayType displayType,
int bannerOpacity,
int subBannerOpacity,
CardSizeWidget *_cardSizeWidget);
DeckListModel *deckListModel;
QPersistentModelIndex trackedIndex;
QString zoneName;
void addCardsToOverlapWidget();
void resizeEvent(QResizeEvent *event) override;
public slots:
void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card);
void onHover(const ExactCard &card);
void cleanupInvalidCardGroup(CardGroupDisplayWidget *displayWidget);
void constructAppropriateWidget(QPersistentModelIndex index);
void displayCards();
void refreshDisplayType(const DisplayType &displayType);
void onActiveGroupCriteriaChanged(QString activeGroupCriteria);
void onActiveSortCriteriaChanged(QStringList activeSortCriteria);
QList<QString> getGroupCriteriaValueList();
void onCategoryAddition(const QModelIndex &parent, int first, int last);
void onCategoryRemoval(const QModelIndex &parent, int first, int last);
signals:
void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card, QString zoneName);
void cardHovered(const ExactCard &card);
void activeSortCriteriaChanged(QStringList activeSortCriteria);
void requestCleanup(DeckCardZoneDisplayWidget *displayWidget);
private:
QString activeGroupCriteria;
QStringList activeSortCriteria;
DisplayType displayType = DisplayType::Overlap;
int bannerOpacity = 20;
int subBannerOpacity = 10;
CardSizeWidget *cardSizeWidget;
QVBoxLayout *layout;
BannerWidget *banner;
QWidget *cardGroupContainer;
QVBoxLayout *cardGroupLayout;
OverlapWidget *overlapWidget;
QHash<QPersistentModelIndex, QWidget *> indexToWidgetMap;
};
#endif // DECK_CARD_ZONE_DISPLAY_WIDGET_H

View file

@ -0,0 +1,64 @@
#include "deck_preview_card_picture_widget.h"
#include "../../../settings/cache_settings.h"
#include <QApplication>
#include <QFileInfo>
#include <QFontMetrics>
#include <QMouseEvent>
#include <QPainterPath>
#include <QStylePainter>
#include <QTextOption>
/**
* @brief Constructs a CardPictureWithTextOverlay widget.
* @param parent The parent widget.
* @param hoverToZoomEnabled If this widget will spawn a larger widget when hovered over.
* @param textColor The color of the overlay text.
* @param outlineColor The color of the outline around the text.
* @param fontSize The font size of the overlay text.
* @param alignment The alignment of the text within the overlay.
* @param _deckLoader The Deck Loader holding the Deck associated with this preview.
*
* Sets the widget's size policy and default border style.
*/
DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent,
const bool hoverToZoomEnabled,
const bool raiseOnEnter,
const QColor &textColor,
const QColor &outlineColor,
const int fontSize,
const Qt::Alignment alignment)
: CardInfoPictureWithTextOverlayWidget(parent,
hoverToZoomEnabled,
raiseOnEnter,
textColor,
outlineColor,
fontSize,
alignment)
{
singleClickTimer = new QTimer(this);
singleClickTimer->setSingleShot(true);
connect(singleClickTimer, &QTimer::timeout, this, [this]() { emit imageClicked(lastMouseEvent, this); });
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageSelectionAnimationChanged, this,
&CardInfoPictureWidget::setRaiseOnEnterEnabled);
}
void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
lastMouseEvent = event;
singleClickTimer->start(QApplication::doubleClickInterval());
} else {
emit imageClicked(event, this);
event->accept();
}
}
void DeckPreviewCardPictureWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
singleClickTimer->stop(); // Prevent single-click logic
emit imageDoubleClicked(lastMouseEvent, this);
}
}

View file

@ -0,0 +1,36 @@
#ifndef DECK_PREVIEW_CARD_PICTURE_WIDGET_H
#define DECK_PREVIEW_CARD_PICTURE_WIDGET_H
#include "card_info_picture_with_text_overlay_widget.h"
#include <QColor>
#include <QSize>
#include <QTextOption>
class DeckPreviewCardPictureWidget final : public CardInfoPictureWithTextOverlayWidget
{
Q_OBJECT
public:
explicit DeckPreviewCardPictureWidget(QWidget *parent,
bool hoverToZoomEnabled = false,
bool raiseOnEnter = false,
const QColor &textColor = Qt::white,
const QColor &outlineColor = Qt::black,
int fontSize = 12,
Qt::Alignment alignment = Qt::AlignCenter);
signals:
void imageClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
void imageDoubleClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
private:
QTimer *singleClickTimer;
QMouseEvent *lastMouseEvent = nullptr; // Store the last mouse event
protected:
void mousePressEvent(QMouseEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
};
#endif // DECK_PREVIEW_CARD_PICTURE_WIDGET_H