mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
422 lines
14 KiB
C++
422 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 "../../interface/theme_manager.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 = themeManager->appFont(AppFont::CardText);
|
|
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);
|
|
}
|
|
}
|