mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-08 02:53:25 -08:00
* [Game] Raise rubber band above count label and collapse multi-selection on click Raise rubber band on start and collapse multi-selection to clicked card when left-clicking an already-selected card with no modifiers. * [Game] Collapse multi-selection on release instead of press Collapsing at press time dropped the selection before the drag started, which broke dragging multiple cards. Keep the selection on press and collapse it on release when the pointer stayed within the drag distance. * [Game] Defer selection collapse until the double-click window passes A release without drag collapsed the multi-selection immediately, so a double-click that followed (playSelected acts on scene()->selectedItems()) only ever saw the single clicked card. When the card handles double-clicks on the selection, arm the collapse on a single-shot timer for QApplication::doubleClickInterval() instead and cancel it from the double-click handler; a new press disarms the other cards and a selection-count guard covers rubber-band/ctrl changes meanwhile. * [Game] Keep sideboard multi-selection on double-click DeckViewCard overrides doubleClickActsOnSelection to preserve the selection, restoring master behavior where double-clicking moves all selected cards between main and side. DeckView cancels any pending collapse before reading the selection. * [Game] Remove duplicated inline selection-collapse declarations * [Game] Fix DeckView collapse call visibility and multi-selection collapse Make cancelPendingSelectionCollapse() public so DeckView can call it on selected DeckViewCards. Always arm the deferred collapse on release instead of skipping cards whose double-click acts on the selection; the double-click-interval timer plus the cancel calls in CardItem::mouseDoubleClickEvent and DeckView::mouseDoubleClickEvent already preserve the selection for double-clicks. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
439 lines
14 KiB
C++
439 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/card/database/card_database_manager.h>
|
|
#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), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0),
|
|
bgColor(Qt::transparent), isHovered(false), realZValue(0), pendingSelectionCollapse(false)
|
|
{
|
|
setCursor(Qt::OpenHandCursor);
|
|
setFlag(ItemIsSelectable);
|
|
setCacheMode(DeviceCoordinateCache);
|
|
|
|
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(cardRef.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::refreshCardInfo()
|
|
{
|
|
exactCard = CardDatabaseManager::query()->getCard(cardRef);
|
|
|
|
if (!exactCard && !cardRef.name.isEmpty()) {
|
|
CardInfo::UiAttributes attributes = {.tableRow = -1};
|
|
auto info = CardInfo::newInstance(cardRef.name, "", true, {}, {}, {}, {}, attributes);
|
|
exactCard = ExactCard(info);
|
|
}
|
|
if (exactCard) {
|
|
connect(exactCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, &AbstractCardItem::pixmapUpdated);
|
|
}
|
|
|
|
cacheBgColor();
|
|
update();
|
|
}
|
|
|
|
/**
|
|
* 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 exactCard.getInfo();
|
|
}
|
|
|
|
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 (facedown || cardRef.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, exactCard, 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() || facedown) {
|
|
painter->save();
|
|
transformPainter(painter, translatedSize, angle);
|
|
painter->setPen(Qt::white);
|
|
painter->setBackground(Qt::black);
|
|
painter->setBackgroundMode(Qt::OpaqueMode);
|
|
QString nameStr;
|
|
if (facedown) {
|
|
nameStr = "# " + QString::number(id);
|
|
} else {
|
|
QString prefix = "";
|
|
if (SettingsCache::instance().debug().getShowCardId()) {
|
|
prefix = "#" + QString::number(id) + " ";
|
|
}
|
|
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::setCardRef(const CardRef &_cardRef)
|
|
{
|
|
if (cardRef == _cardRef) {
|
|
return;
|
|
}
|
|
|
|
emit deleteCardInfoPopup(cardRef.name);
|
|
if (exactCard) {
|
|
disconnect(exactCard.getCardPtr().data(), nullptr, this, nullptr);
|
|
}
|
|
cardRef = _cardRef;
|
|
|
|
refreshCardInfo();
|
|
}
|
|
|
|
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)
|
|
{
|
|
color = _color;
|
|
cacheBgColor();
|
|
update();
|
|
}
|
|
|
|
void AbstractCardItem::cacheBgColor()
|
|
{
|
|
QChar colorChar;
|
|
if (color.isEmpty()) {
|
|
colorChar = exactCard.getInfo().getColorChar();
|
|
} else {
|
|
colorChar = color.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::setTapped(bool _tapped, bool canAnimate)
|
|
{
|
|
if (tapped == _tapped) {
|
|
return;
|
|
}
|
|
|
|
tapped = _tapped;
|
|
if (SettingsCache::instance().cardsDisplay().getTapAnimation() && canAnimate) {
|
|
static_cast<GameScene *>(scene())->registerAnimationItem(this);
|
|
} else {
|
|
tapAngle = tapped ? 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::setFaceDown(bool _facedown)
|
|
{
|
|
facedown = _facedown;
|
|
update();
|
|
}
|
|
|
|
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(cardRef.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(), cardRef);
|
|
}
|
|
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(cardRef.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);
|
|
}
|
|
}
|