From a7641a571fa297061e9909fa84685792b004aeeb Mon Sep 17 00:00:00 2001 From: Basile Clement Date: Mon, 17 Mar 2025 00:19:39 +0100 Subject: [PATCH] Display visual feedback of where cards will go (#5737) This is part of the code from #4974, including an improved drag-and-drop API and its use to display visual feedback of card destination on the board. It does not include the improved logic for pushing cards around as I still need to figure out edge cases there - the logic for choosing where cards go is not changed, so some of the artifacts described in #4817 and #4975 (particularly around multi-card) are still present. --- cockatrice/src/game/cards/card_drag_item.cpp | 148 ++++++++++--------- cockatrice/src/game/cards/card_drag_item.h | 15 +- cockatrice/src/game/zones/card_zone.cpp | 28 +++- cockatrice/src/game/zones/card_zone.h | 21 ++- cockatrice/src/game/zones/table_zone.cpp | 99 +++++++++++-- cockatrice/src/game/zones/table_zone.h | 19 ++- 6 files changed, 230 insertions(+), 100 deletions(-) diff --git a/cockatrice/src/game/cards/card_drag_item.cpp b/cockatrice/src/game/cards/card_drag_item.cpp index d21e9168b..318aceefa 100644 --- a/cockatrice/src/game/cards/card_drag_item.cpp +++ b/cockatrice/src/game/cards/card_drag_item.cpp @@ -2,8 +2,6 @@ #include "../game_scene.h" #include "../zones/card_zone.h" -#include "../zones/table_zone.h" -#include "../zones/view_zone.h" #include "card_item.h" #include @@ -15,104 +13,112 @@ CardDragItem::CardDragItem(CardItem *_item, const QPointF &_hotSpot, bool _faceDown, AbstractCardDragItem *parentDrag) - : AbstractCardDragItem(_item, _hotSpot, parentDrag), id(_id), faceDown(_faceDown), occupied(false), currentZone(0) + : AbstractCardDragItem(_item, _hotSpot, parentDrag), id(_id), faceDown(_faceDown), currentZone(0) { } +CardDragItem::~CardDragItem() +{ + if (currentZone) { + currentZone->dragLeave(this); + currentZone = nullptr; + } +} + void CardDragItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { AbstractCardDragItem::paint(painter, option, widget); - if (occupied) + if (!isValid) { painter->fillPath(shape(), QColor(200, 0, 0, 100)); + } } void CardDragItem::updatePosition(const QPointF &cursorScenePos) { + QPointF topLeftScenePos = cursorScenePos - hotSpot; + QPointF centerScenePos = topLeftScenePos + QPointF(CARD_WIDTH_HALF, CARD_HEIGHT_HALF); + + // Use center of the card for intersection. QList colliding = - scene()->items(cursorScenePos, Qt::IntersectsItemBoundingRect, Qt::DescendingOrder, + scene()->items(centerScenePos, Qt::IntersectsItemBoundingRect, Qt::DescendingOrder, static_cast(scene())->getViewTransform()); - CardZone *cardZone = 0; - ZoneViewZone *zoneViewZone = 0; - for (int i = colliding.size() - 1; i >= 0; i--) { - CardZone *temp = qgraphicsitem_cast(colliding.at(i)); - if (!cardZone) - cardZone = temp; - if (!zoneViewZone) - zoneViewZone = qobject_cast(temp); - } - CardZone *cursorZone = 0; - if (zoneViewZone) - cursorZone = zoneViewZone; - else if (cardZone) - cursorZone = cardZone; - if (!cursorZone) - return; - currentZone = cursorZone; + CardZone *cardZone = nullptr; + for (auto *item : colliding) { + cardZone = qgraphicsitem_cast(item); - QPointF zonePos = currentZone->scenePos(); - QPointF cursorPosInZone = cursorScenePos - zonePos; - - // If we are on a Table, we center the card around the cursor, because we - // snap it into place and no longer see it being dragged. - // - // For other zones (where we do display the card under the cursor), we use - // the hotspot to feel like the card was dragged at the corresponding - // position. - TableZone *tableZone = qobject_cast(cursorZone); - QPointF closestGridPoint; - if (tableZone) - closestGridPoint = tableZone->closestGridPoint(cursorPosInZone); - else - closestGridPoint = cursorPosInZone - hotSpot; - - QPointF newPos = zonePos + closestGridPoint; - - if (newPos != pos()) { - for (int i = 0; i < childDrags.size(); i++) - childDrags[i]->setPos(newPos + childDrags[i]->getHotSpot()); - setPos(newPos); - - bool newOccupied = false; - TableZone *table = qobject_cast(cursorZone); - if (table) - if (table->getCardFromCoords(closestGridPoint)) - newOccupied = true; - if (newOccupied != occupied) { - occupied = newOccupied; - update(); + if (cardZone) { + break; } } + + if (cardZone != currentZone) { + if (currentZone) { + currentZone->dragLeave(this); + currentZone = nullptr; + } + + if (cardZone && cardZone->dragEnter(this, centerScenePos - cardZone->scenePos())) { + setValid(true); + currentZone = cardZone; + } else { + setValid(false); + } + } + + if (currentZone) { + currentZone->dragMove(this, centerScenePos - currentZone->scenePos()); + } + + if (topLeftScenePos != pos()) { + for (int i = 0; i < childDrags.size(); i++) + childDrags[i]->setPos(topLeftScenePos + childDrags[i]->getHotSpot()); + setPos(topLeftScenePos); + } } void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { setCursor(Qt::OpenHandCursor); QGraphicsScene *sc = scene(); - QPointF sp = pos(); sc->removeItem(this); - QList dragItemList; - CardZone *startZone = static_cast(item)->getZone(); - if (currentZone && !(static_cast(item)->getAttachedTo() && (startZone == currentZone))) { - if (!occupied) { - dragItemList.append(this); - } - - for (int i = 0; i < childDrags.size(); i++) { - CardDragItem *c = static_cast(childDrags[i]); - if (!occupied && !(static_cast(c->item)->getAttachedTo() && (startZone == currentZone)) && - !c->occupied) { - dragItemList.append(c); - } - sc->removeItem(c); - } + for (auto *childDrag : childDrags) { + sc->removeItem(childDrag); } if (currentZone) { - currentZone->handleDropEvent(dragItemList, startZone, (sp - currentZone->scenePos()).toPoint()); + QPointF cursorScenePos = event->scenePos(); + QPointF topLeftScenePos = cursorScenePos - hotSpot; + QPointF centerScenePos = topLeftScenePos + QPointF(CARD_WIDTH_HALF, CARD_HEIGHT_HALF); + currentZone->dragAccept(this, centerScenePos - currentZone->scenePos()); + currentZone = nullptr; } - event->accept(); + static_cast(item)->deleteDragItem(); + AbstractCardDragItem::mouseReleaseEvent(event); +} + +CardZone *CardDragItem::getStartZone() const +{ + return static_cast(item)->getZone(); +} + +QList CardDragItem::getValidItems() +{ + QList dragItemList; + CardZone *startZone = getStartZone(); + if (!(static_cast(item)->getAttachedTo() && startZone == currentZone)) { + dragItemList.append(this); + + for (int i = 0; i < childDrags.size(); i++) { + CardDragItem *c = static_cast(childDrags[i]); + if (!(static_cast(c->item)->getAttachedTo() && startZone == currentZone)) { + dragItemList.append(c); + } + } + } + + return dragItemList; } diff --git a/cockatrice/src/game/cards/card_drag_item.h b/cockatrice/src/game/cards/card_drag_item.h index 794fd6ed3..19db2757f 100644 --- a/cockatrice/src/game/cards/card_drag_item.h +++ b/cockatrice/src/game/cards/card_drag_item.h @@ -11,7 +11,7 @@ class CardDragItem : public AbstractCardDragItem private: int id; bool faceDown; - bool occupied; + bool isValid = true; CardZone *currentZone; public: @@ -20,6 +20,7 @@ public: const QPointF &_hotSpot, bool _faceDown, AbstractCardDragItem *parentDrag = 0); + virtual ~CardDragItem(); int getId() const { return id; @@ -31,6 +32,18 @@ public: void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void updatePosition(const QPointF &cursorScenePos) override; + void setValid(bool _valid) + { + if (_valid != isValid) { + isValid = _valid; + update(); + } + } + + CardZone *getStartZone() const; + + QList getValidItems(); + protected: void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; }; diff --git a/cockatrice/src/game/zones/card_zone.cpp b/cockatrice/src/game/zones/card_zone.cpp index f3ebbd214..b4806a010 100644 --- a/cockatrice/src/game/zones/card_zone.cpp +++ b/cockatrice/src/game/zones/card_zone.cpp @@ -1,11 +1,10 @@ #include "card_zone.h" #include "../cards/card_database_manager.h" +#include "../cards/card_drag_item.h" #include "../cards/card_item.h" #include "../player/player.h" #include "pb/command_move_card.pb.h" -#include "pb/serverinfo_user.pb.h" -#include "pile_zone.h" #include "view_zone.h" #include @@ -233,7 +232,26 @@ void CardZone::moveAllToZone() player->sendGameCommand(cmd); } -QPointF CardZone::closestGridPoint(const QPointF &point) +bool CardZone::dragEnter(CardDragItem *dragItem, const QPointF &pos) { - return point; -} \ No newline at end of file + Q_UNUSED(dragItem); + Q_UNUSED(pos); + + return true; +} + +void CardZone::dragMove(CardDragItem *dragItem, const QPointF &pos) +{ + Q_UNUSED(dragItem); + Q_UNUSED(pos); +} + +void CardZone::dragAccept(CardDragItem *dragItem, const QPointF &pos) +{ + handleDropEvent(dragItem->getValidItems(), dragItem->getStartZone(), pos.toPoint()); +} + +void CardZone::dragLeave(CardDragItem *dragItem) +{ + Q_UNUSED(dragItem); +} diff --git a/cockatrice/src/game/zones/card_zone.h b/cockatrice/src/game/zones/card_zone.h index 91cad179c..4bb67435f 100644 --- a/cockatrice/src/game/zones/card_zone.h +++ b/cockatrice/src/game/zones/card_zone.h @@ -58,6 +58,26 @@ public: { return Type; } + + /* Called when a card is dragged on top of the zone. + + Return `true` to accept the drag, `false` otherwise. + */ + virtual bool dragEnter(CardDragItem *dragItem, const QPointF &pos); + + /* Called when a card that has been accepted by `dragEnter` is moved over + the zone. + */ + virtual void dragMove(CardDragItem *dragItem, const QPointF &pos); + + /* Called when a card that has been accepted by `dragEnter` is dropped onto + the zone. */ + virtual void dragAccept(CardDragItem *dragItem, const QPointF &pos); + + /* Called when a card that has been accepted by `dragEnter` leaves the zone. + */ + virtual void dragLeave(CardDragItem *dragItem); + virtual void handleDropEvent(const QList &dragItem, CardZone *startZone, const QPoint &dropPoint) = 0; CardZone(Player *_player, @@ -114,7 +134,6 @@ public: return views; } virtual void reorganizeCards() = 0; - virtual QPointF closestGridPoint(const QPointF &point); bool getIsView() const { return isView; diff --git a/cockatrice/src/game/zones/table_zone.cpp b/cockatrice/src/game/zones/table_zone.cpp index 71327b9ec..1c02bb077 100644 --- a/cockatrice/src/game/zones/table_zone.cpp +++ b/cockatrice/src/game/zones/table_zone.cpp @@ -118,9 +118,66 @@ void TableZone::addCardImpl(CardItem *card, int _x, int _y) card->update(); } +bool TableZone::dragEnter(CardDragItem *dragItem, const QPointF &pos) +{ + if (!SelectZone::dragEnter(dragItem, pos)) { + return false; + } + + if (m_feedbackItem) { + scene()->removeItem(m_feedbackItem); + delete m_feedbackItem; + } + + m_feedbackItem = new QGraphicsPathItem(dragItem->getItem()->shape(), this); + m_feedbackItem->setBrush(QColor(176, 196, 222)); // lightsteelblue + m_feedbackItem->setPen(QColor(119, 136, 153)); // lightslategray + m_feedbackItem->setZValue(2000000005); + m_feedbackItem->setTransform(dragItem->getItem()->transform()); + updateFeedback(dragItem, pos); + + return true; +} + +void TableZone::dragMove(CardDragItem *dragItem, const QPointF &pos) +{ + if (m_feedbackItem) { + updateFeedback(dragItem, pos); + } + + SelectZone::dragMove(dragItem, pos); +} + +void TableZone::dragAccept(CardDragItem *dragItem, const QPointF &pos) +{ + SelectZone::dragAccept(dragItem, pos); + + if (m_feedbackItem) { + scene()->removeItem(m_feedbackItem); + delete m_feedbackItem; + m_feedbackItem = nullptr; + } +} + +void TableZone::dragLeave(CardDragItem *dragItem) +{ + SelectZone::dragLeave(dragItem); + + if (m_feedbackItem) { + scene()->removeItem(m_feedbackItem); + delete m_feedbackItem; + m_feedbackItem = nullptr; + } +} + void TableZone::handleDropEvent(const QList &dragItems, CardZone *startZone, const QPoint &dropPoint) { - handleDropEventByGrid(dragItems, startZone, mapToGrid(dropPoint)); + Q_UNUSED(dropPoint); + + // Always use the position of the feedback item for consistency + if (m_feedbackItem && m_feedbackItem->isVisible()) { + handleDropEventByGrid(dragItems, startZone, mapToGrid(m_feedbackItem->pos())); + } } void TableZone::handleDropEventByGrid(const QList &dragItems, @@ -260,12 +317,6 @@ CardItem *TableZone::getCardFromGrid(const QPoint &gridPoint) const return 0; } -CardItem *TableZone::getCardFromCoords(const QPointF &point) const -{ - QPoint gridPoint = mapToGrid(point); - return getCardFromGrid(gridPoint); -} - void TableZone::computeCardStackWidths() { // Each card stack is three grid points worth of card locations. @@ -366,18 +417,36 @@ QPoint TableZone::mapToGrid(const QPointF &mapPoint) const int xDiff = x - xStack; int gridPointX = stackCol * 3 + qMin(xDiff / STACKED_CARD_OFFSET_X, 2); - return QPoint(gridPointX, gridPointY); + return QPoint((gridPointX / 3) * 3, gridPointY); } -QPointF TableZone::closestGridPoint(const QPointF &point) +void TableZone::updateFeedback(CardDragItem *dragItem, const QPointF &point) { QPoint gridPoint = mapToGrid(point); - gridPoint.setX((gridPoint.x() / 3) * 3); - if (getCardFromGrid(gridPoint)) - gridPoint.setX(gridPoint.x() + 1); - if (getCardFromGrid(gridPoint)) - gridPoint.setX(gridPoint.x() + 1); - return mapFromGrid(gridPoint); + + for (int i = 0; i < 3; ++i) { + if (CardItem *existingCard = getCardFromGrid(gridPoint)) { + if (existingCard == dragItem->getItem()) { + // Dropping the card onto its existing position is a no-op, and + // we don't want to display the feedback item, but we don't want + // to display an invalid state either, because that'd be + // confusing. + dragItem->setValid(true); + m_feedbackItem->hide(); + return; + } + + gridPoint.setX(gridPoint.x() + 1); + } else { + dragItem->setValid(true); + m_feedbackItem->show(); + m_feedbackItem->setPos(mapFromGrid(gridPoint)); + return; + } + } + + dragItem->setValid(false); + m_feedbackItem->hide(); } int TableZone::clampValidTableRow(const int row) diff --git a/cockatrice/src/game/zones/table_zone.h b/cockatrice/src/game/zones/table_zone.h index 271276967..a10a05306 100644 --- a/cockatrice/src/game/zones/table_zone.h +++ b/cockatrice/src/game/zones/table_zone.h @@ -77,6 +77,8 @@ private: */ bool active; + QGraphicsPathItem *m_feedbackItem = nullptr; + bool isInverted() const; private slots: @@ -118,6 +120,14 @@ public: */ void toggleTapped(); + bool dragEnter(CardDragItem *dragItem, const QPointF &pos) override; + + void dragMove(CardDragItem *dragItem, const QPointF &pos) override; + + void dragAccept(CardDragItem *dragItem, const QPointF &pos) override; + + void dragLeave(CardDragItem *dragItem) override; + /** See HandleDropEventByGrid */ @@ -133,13 +143,6 @@ public: */ CardItem *getCardFromGrid(const QPoint &gridPoint) const; - /** - @return CardItem from coordinate location - */ - CardItem *getCardFromCoords(const QPointF &point) const; - - QPointF closestGridPoint(const QPointF &point) override; - static int clampValidTableRow(const int row); /** @@ -184,6 +187,8 @@ private: void paintZoneOutline(QPainter *painter); void paintLandDivider(QPainter *painter); + void updateFeedback(CardDragItem *dragItem, const QPointF &point); + /* Calculates card stack widths so mapping functions work properly */