Cockatrice/cockatrice/src/game_graphics/board/abstract_card_item.cpp
BruebachL 5d0d466930
[Refactor] Add card name, printing id, and color to CardState (#7433)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:13 +01:00

421 lines
14 KiB
C++

#include "abstract_card_item.h"
#include "../../client/settings/cache_settings.h"
#include "../../interface/card_localization.h"
#include "../../interface/card_picture_loader/card_picture_loader.h"
#include "../card_dimensions.h"
#include "../game_scene.h"
#include "../z_values.h"
#include "abstract_graphics_item.h"
#include "libcockatrice/card/card_info.h"
#include "libcockatrice/card/database/card_database_querier.h"
#include "libcockatrice/card/printing/exact_card.h"
#include "libcockatrice/utility/card_ref.h"
#include <QApplication>
#include <QBrush>
#include <QChar>
#include <QCursor>
#include <QFlags>
#include <QFont>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QHash>
#include <QLineF>
#include <QList>
#include <QPainter>
#include <QPainterPath>
#include <QPen>
#include <QPixmap>
#include <QPointF>
#include <QRectF>
#include <QTimer>
#include <QTransform>
#include <QVariant>
#include <QtGlobal>
#include <algorithm>
#include <libcockatrice/settings/appearance_settings.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/debug_settings.h>
#include <qnamespace.h>
class QSizeF;
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
: ArrowTarget(_owner, parent), state(new CardState(this, nullptr, cardRef, _id)), tapAngle(0),
bgColor(Qt::transparent), isHovered(false), realZValue(0)
{
setCursor(Qt::OpenHandCursor);
setFlag(ItemIsSelectable);
setCacheMode(DeviceCoordinateCache);
connect(state, &CardState::cardPixmapUpdated, this, &AbstractCardItem::pixmapUpdated);
connect(state, &CardState::cardInfoChanged, this, &AbstractCardItem::onCardInfoChanged);
connect(state, &CardState::cardRefChanged, this, &AbstractCardItem::onCardRefChanged);
connect(state, &CardState::tappedChanged, this, &AbstractCardItem::onTappedChanged);
connect(state, &CardState::facedownChanged, this, [this] { update(); });
connect(state, &CardState::colorChanged, this, [this] {
cacheBgColor();
update();
});
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
[this] { update(); });
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
[this] { update(); });
refreshCardInfo();
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
[this](bool _roundCardCorners) {
Q_UNUSED(_roundCardCorners);
prepareGeometryChange();
update();
});
}
AbstractCardItem::~AbstractCardItem()
{
emit deleteCardInfoPopup(state->getCardRef().name);
}
QRectF AbstractCardItem::boundingRect() const
{
return QRectF(0, 0, CardDimensions::WIDTH_F, CardDimensions::HEIGHT_F);
}
QPainterPath AbstractCardItem::shape() const
{
QPainterPath shape;
qreal cardCornerRadius =
SettingsCache::instance().cardsDisplay().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
return shape;
}
void AbstractCardItem::pixmapUpdated()
{
update();
emit sigPixmapUpdated();
}
void AbstractCardItem::onCardInfoChanged()
{
cacheBgColor();
update();
}
void AbstractCardItem::onCardRefChanged(const CardRef &oldCardRef, const CardRef & /*newCardRef*/)
{
emit deleteCardInfoPopup(oldCardRef.name);
}
void AbstractCardItem::refreshCardInfo()
{
state->refreshCardInfo();
}
/**
* Convenience method to get the CardInfo of the exactCard
* @return A const reference to the CardInfo, or an empty CardInfo if card was null
*/
const CardInfo &AbstractCardItem::getCardInfo() const
{
return state->getCardInfo();
}
void AbstractCardItem::setRealZValue(qreal _zValue)
{
realZValue = _zValue;
// During hover, zValue is overridden to HOVERED_CARD. Layout operations
// like reorganizeCards() call setRealZValue() on all cards including the
// hovered one — skip setZValue() here to avoid clobbering the override.
if (!isHovered) {
setZValue(_zValue);
}
}
QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const
{
return QSizeF(painter->combinedTransform().map(QLineF(0, 0, boundingRect().width(), 0)).length(),
painter->combinedTransform().map(QLineF(0, 0, 0, boundingRect().height())).length());
}
void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle)
{
const int MAX_FONT_SIZE = SettingsCache::instance().appearance().getMaxFontSize();
const int fontSize = std::max(9, MAX_FONT_SIZE);
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());
int scale = resetPainterTransform(painter);
painter->translate(totalBoundingRect.width() / 2, totalBoundingRect.height() / 2);
painter->rotate(angle);
painter->translate(-translatedSize.width() / 2, -translatedSize.height() / 2);
QFont f;
f.setPixelSize(fontSize * scale);
painter->setFont(f);
}
void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedSize, int angle)
{
qreal scaleFactor = translatedSize.width() / boundingRect().width();
QPixmap translatedPixmap;
bool paintImage = true;
if (state->getFaceDown() || state->getCardRef().name.isEmpty()) {
// never reveal card color, always paint the card back
CardPictureLoader::getCardBackPixmap(translatedPixmap, translatedSize.toSize());
} else {
// don't even spend time trying to load the picture if our size is too small
if (translatedSize.width() > 10) {
CardPictureLoader::getPixmap(translatedPixmap, state->getCard(), translatedSize.toSize());
if (translatedPixmap.isNull()) {
paintImage = false;
}
} else {
paintImage = false;
}
}
painter->save();
if (paintImage) {
painter->save();
painter->setClipPath(shape());
painter->drawPixmap(boundingRect(), translatedPixmap, QRectF({0, 0}, translatedPixmap.size()));
painter->restore();
} else {
painter->setBrush(bgColor);
painter->drawPath(shape());
}
if (translatedPixmap.isNull() || SettingsCache::instance().cardsDisplay().getDisplayCardNames() ||
state->getFaceDown()) {
painter->save();
transformPainter(painter, translatedSize, angle);
painter->setPen(Qt::white);
painter->setBackground(Qt::black);
painter->setBackgroundMode(Qt::OpaqueMode);
QString nameStr;
if (state->getFaceDown()) {
nameStr = "# " + QString::number(state->getId());
} else {
QString prefix = "";
if (SettingsCache::instance().debug().getShowCardId()) {
prefix = "#" + QString::number(state->getId()) + " ";
}
nameStr = prefix + CardLocalization::displayName(getCardInfo());
}
painter->drawText(QRectF(3 * scaleFactor, 3 * scaleFactor, translatedSize.width() - 6 * scaleFactor,
translatedSize.height() - 6 * scaleFactor),
Qt::AlignTop | Qt::AlignLeft | Qt::TextWrapAnywhere, nameStr);
painter->restore();
}
painter->restore();
}
void AbstractCardItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
{
painter->save();
QSizeF translatedSize = getTranslatedSize(painter);
paintPicture(painter, translatedSize, tapAngle);
painter->setRenderHint(QPainter::Antialiasing, false);
if (isSelected() || isHovered) {
QPen pen;
if (isHovered) {
pen.setColor(Qt::yellow);
}
if (isSelected()) {
pen.setColor(Qt::red);
}
pen.setWidth(0); // Cosmetic pen
painter->setPen(pen);
painter->drawPath(shape());
}
painter->restore();
}
void AbstractCardItem::setHovered(bool _hovered)
{
if (isHovered == _hovered) {
return;
}
if (_hovered) {
processHoverEvent();
} else {
// Mark the hovered card's current scene footprint dirty so overlapped
// sibling zones (e.g. StackZone) repaint after the card moves away.
if (scene()) {
scene()->update(sceneBoundingRect());
}
}
isHovered = _hovered;
setZValue(_hovered ? ZValues::HOVERED_CARD : realZValue);
setScale(_hovered && SettingsCache::instance().cardsDisplay().getScaleCards() ? 1.1 : 1);
setTransformOriginPoint(_hovered ? CardDimensions::WIDTH_HALF_F : 0, _hovered ? CardDimensions::HEIGHT_HALF_F : 0);
update();
}
void AbstractCardItem::setColor(const QString &_color)
{
state->setColor(_color);
}
void AbstractCardItem::cacheBgColor()
{
QChar colorChar;
const QString cardColor = state->getColor();
if (cardColor.isEmpty()) {
colorChar = state->getCardInfo().getColorChar();
} else {
colorChar = cardColor.at(0);
}
switch (colorChar.toLower().toLatin1()) {
case 'b':
bgColor = QColor(0, 0, 0);
break;
case 'u':
bgColor = QColor(0, 140, 180);
break;
case 'w':
bgColor = QColor(255, 250, 140);
break;
case 'r':
bgColor = QColor(230, 0, 0);
break;
case 'g':
bgColor = QColor(0, 160, 0);
break;
case 'm':
bgColor = QColor(250, 190, 30);
break;
default:
bgColor = QColor(230, 230, 230);
break;
}
}
void AbstractCardItem::onTappedChanged(bool newTapped, bool canAnimate)
{
if (SettingsCache::instance().cardsDisplay().getTapAnimation() && canAnimate) {
static_cast<GameScene *>(scene())->registerAnimationItem(this);
} else {
tapAngle = newTapped ? 90 : 0;
setTransform(QTransform()
.translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F)
.rotate(tapAngle)
.translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F));
update();
}
}
bool AbstractCardItem::animationEvent()
{
return false;
}
void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
cancelPendingSelectionCollapse();
if (scene() != nullptr) {
// A new press starts a new gesture: no card may collapse the selection out from under it afterwards.
for (QGraphicsItem *selectedItem : scene()->selectedItems()) {
auto *selectedCard = qgraphicsitem_cast<AbstractCardItem *>(selectedItem);
if (selectedCard != nullptr && selectedCard != this) {
selectedCard->cancelPendingSelectionCollapse();
}
}
}
if ((event->modifiers() & Qt::AltModifier) && event->button() == Qt::LeftButton) {
emit cardShiftClicked(state->getCardRef().name);
} else if ((event->modifiers() & Qt::ControlModifier)) {
setSelected(!isSelected());
} else if (!isSelected()) {
scene()->clearSelection();
setSelected(true);
} else if (event->button() == Qt::LeftButton && scene()->selectedItems().count() > 1) {
// Part of a multi-selection; collapse only if this turns out to be a click rather than a drag,
// so that dragging the whole selection keeps it intact.
pendingSelectionCollapse = true;
}
if (event->button() == Qt::LeftButton) {
setCursor(Qt::ClosedHandCursor);
} else if (event->button() == Qt::MiddleButton) {
emit showCardInfoPopup(event->screenPos(), state->getCardRef());
}
event->accept();
}
void AbstractCardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if (pendingSelectionCollapse) {
pendingSelectionCollapse = false;
bool dragged = (event->screenPos() - event->buttonDownScreenPos(event->button())).manhattanLength() >=
2 * QApplication::startDragDistance();
if (!dragged) {
if (doubleClickActsOnSelection()) {
// A double-click may still arrive and must act on the whole multi-selection, so collapse only
// once the double-click window has passed without one. The generation guard (and the receiver
// context) drops the collapse if the item is gone, deselected, or a double-click cancelled it;
// the count guard drops it if the selection changed meanwhile (rubber band, ctrl-toggle).
const int generation = ++selectionCollapseGeneration;
const int armedSelectionCount = scene()->selectedItems().count();
QTimer::singleShot(QApplication::doubleClickInterval(), this, [this, generation, armedSelectionCount] {
if (generation == selectionCollapseGeneration && scene() != nullptr && isSelected() &&
scene()->selectedItems().count() == armedSelectionCount && armedSelectionCount > 1) {
scene()->clearSelection();
setSelected(true);
}
});
} else {
scene()->clearSelection();
setSelected(true);
}
}
}
if (event->button() == Qt::MiddleButton) {
emit deleteCardInfoPopup(state->getCardRef().name);
}
// This function ensures the parent function doesn't mess around with our selection.
event->accept();
}
void AbstractCardItem::cancelPendingSelectionCollapse()
{
pendingSelectionCollapse = false;
++selectionCollapseGeneration;
}
bool AbstractCardItem::doubleClickActsOnSelection() const
{
return false;
}
void AbstractCardItem::processHoverEvent()
{
emit hovered(this);
}
QVariant AbstractCardItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
if (change == ItemSelectedHasChanged) {
update();
return value;
} else {
return ArrowTarget::itemChange(change, value);
}
}