* Improve drag & drop behavior This patch tweaks the drag & drop behavior (in particular, the grid placement) to be more intuitive. More precisely, with this patch the drag & drop will: - Only use the "hot spot" (i.e. position of the cursor on the card) for zones where the card is actually displayed around the cursor (in particular, not on the table where the card snaps to the grid). - Use better boundaries computed with respect to the center of the card (rather than its top left corner) for determining which grid cell a card should go to - Align behavior of the preview and the actual effect when overflow of the 3-card stacks occurs - Avoid visual glitches where the cursor ends up outside of the card or at incorrect offsets when moving the mouse too fast (which translates to overflows of the hot spot computation) * Address review comments - Use simpler computation for restricting hotSpot range - Prevent dropping cards onto full 3-card slots
404 lines
13 KiB
C++
404 lines
13 KiB
C++
#include "tablezone.h"
|
|
|
|
#include "arrowitem.h"
|
|
#include "carddatabase.h"
|
|
#include "carddragitem.h"
|
|
#include "carditem.h"
|
|
#include "pb/command_move_card.pb.h"
|
|
#include "pb/command_set_card_attr.pb.h"
|
|
#include "player.h"
|
|
#include "settingscache.h"
|
|
#include "thememanager.h"
|
|
|
|
#include <QGraphicsScene>
|
|
#include <QPainter>
|
|
#include <QSet>
|
|
|
|
const QColor TableZone::BACKGROUND_COLOR = QColor(100, 100, 100);
|
|
const QColor TableZone::FADE_MASK = QColor(0, 0, 0, 80);
|
|
const QColor TableZone::GRADIENT_COLOR = QColor(255, 255, 255, 150);
|
|
const QColor TableZone::GRADIENT_COLORLESS = QColor(255, 255, 255, 0);
|
|
|
|
TableZone::TableZone(Player *_p, QGraphicsItem *parent)
|
|
: SelectZone(_p, "table", true, false, true, parent), active(false)
|
|
{
|
|
connect(themeManager, SIGNAL(themeChanged()), this, SLOT(updateBg()));
|
|
connect(&SettingsCache::instance(), SIGNAL(invertVerticalCoordinateChanged()), this, SLOT(reorganizeCards()));
|
|
|
|
updateBg();
|
|
|
|
height = MARGIN_TOP + MARGIN_BOTTOM + TABLEROWS * CARD_HEIGHT + (TABLEROWS - 1) * PADDING_Y;
|
|
width = MIN_WIDTH;
|
|
currentMinimumWidth = width;
|
|
|
|
setCacheMode(DeviceCoordinateCache);
|
|
setAcceptHoverEvents(true);
|
|
}
|
|
|
|
void TableZone::updateBg()
|
|
{
|
|
update();
|
|
}
|
|
|
|
QRectF TableZone::boundingRect() const
|
|
{
|
|
return QRectF(0, 0, width, height);
|
|
}
|
|
|
|
bool TableZone::isInverted() const
|
|
{
|
|
return ((player->getMirrored() && !SettingsCache::instance().getInvertVerticalCoordinate()) ||
|
|
(!player->getMirrored() && SettingsCache::instance().getInvertVerticalCoordinate()));
|
|
}
|
|
|
|
void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
|
{
|
|
QBrush brush = themeManager->getTableBgBrush();
|
|
|
|
if (player->getZoneId() > 0) {
|
|
// If the extra image is not found, load the default one
|
|
brush = themeManager->getExtraTableBgBrush(QString::number(player->getZoneId()), brush);
|
|
}
|
|
painter->fillRect(boundingRect(), brush);
|
|
|
|
if (active) {
|
|
paintZoneOutline(painter);
|
|
} else {
|
|
// inactive player gets a darker table zone with a semi transparent black mask
|
|
// this means if the user provides a custom background it will fade
|
|
painter->fillRect(boundingRect(), FADE_MASK);
|
|
}
|
|
|
|
paintLandDivider(painter);
|
|
}
|
|
|
|
/**
|
|
Render a soft outline around the edge of the TableZone.
|
|
|
|
@param painter QPainter object
|
|
*/
|
|
void TableZone::paintZoneOutline(QPainter *painter)
|
|
{
|
|
QLinearGradient grad1(0, 0, 0, 1);
|
|
grad1.setCoordinateMode(QGradient::ObjectBoundingMode);
|
|
grad1.setColorAt(0, GRADIENT_COLOR);
|
|
grad1.setColorAt(1, GRADIENT_COLORLESS);
|
|
painter->fillRect(QRectF(0, 0, width, BOX_LINE_WIDTH), QBrush(grad1));
|
|
|
|
grad1.setFinalStop(1, 0);
|
|
painter->fillRect(QRectF(0, 0, BOX_LINE_WIDTH, height), QBrush(grad1));
|
|
|
|
grad1.setStart(0, 1);
|
|
grad1.setFinalStop(0, 0);
|
|
painter->fillRect(QRectF(0, height - BOX_LINE_WIDTH, width, BOX_LINE_WIDTH), QBrush(grad1));
|
|
|
|
grad1.setStart(1, 0);
|
|
painter->fillRect(QRectF(width - BOX_LINE_WIDTH, 0, BOX_LINE_WIDTH, height), QBrush(grad1));
|
|
}
|
|
|
|
/**
|
|
Render a division line for land placement
|
|
|
|
@painter QPainter object
|
|
*/
|
|
void TableZone::paintLandDivider(QPainter *painter)
|
|
{
|
|
// Place the line 2 grid heights down then back it off just enough to allow
|
|
// some space between a 3-card stack and the land area.
|
|
qreal separatorY = MARGIN_TOP + 2 * (CARD_HEIGHT + PADDING_Y) - STACKED_CARD_OFFSET_Y / 2;
|
|
if (isInverted())
|
|
separatorY = height - separatorY;
|
|
painter->setPen(QColor(255, 255, 255, 40));
|
|
painter->drawLine(QPointF(0, separatorY), QPointF(width, separatorY));
|
|
}
|
|
|
|
void TableZone::addCardImpl(CardItem *card, int _x, int _y)
|
|
{
|
|
cards.append(card);
|
|
card->setGridPoint(QPoint(_x, _y));
|
|
|
|
card->setParentItem(this);
|
|
card->setVisible(true);
|
|
card->update();
|
|
}
|
|
|
|
void TableZone::handleDropEvent(const QList<CardDragItem *> &dragItems, CardZone *startZone, const QPoint &dropPoint)
|
|
{
|
|
handleDropEventByGrid(dragItems, startZone, mapToGrid(dropPoint));
|
|
}
|
|
|
|
void TableZone::handleDropEventByGrid(const QList<CardDragItem *> &dragItems,
|
|
CardZone *startZone,
|
|
const QPoint &gridPoint)
|
|
{
|
|
Command_MoveCard cmd;
|
|
cmd.set_start_player_id(startZone->getPlayer()->getId());
|
|
cmd.set_start_zone(startZone->getName().toStdString());
|
|
cmd.set_target_player_id(player->getId());
|
|
cmd.set_target_zone(getName().toStdString());
|
|
cmd.set_x(gridPoint.x());
|
|
cmd.set_y(gridPoint.y());
|
|
|
|
for (const auto &item : dragItems) {
|
|
CardToMove *ctm = cmd.mutable_cards_to_move()->add_card();
|
|
ctm->set_card_id(item->getId());
|
|
ctm->set_face_down(item->getFaceDown());
|
|
if (startZone->getName() != name && !item->getFaceDown()) {
|
|
const auto &info = item->getItem()->getInfo();
|
|
if (info) {
|
|
ctm->set_pt(info->getPowTough().toStdString());
|
|
}
|
|
}
|
|
}
|
|
|
|
startZone->getPlayer()->sendGameCommand(cmd);
|
|
}
|
|
|
|
void TableZone::reorganizeCards()
|
|
{
|
|
QSet<ArrowItem *> arrowsToUpdate;
|
|
|
|
// Calculate card stack widths so mapping functions work properly
|
|
computeCardStackWidths();
|
|
|
|
for (int i = 0; i < cards.size(); ++i) {
|
|
QPoint gridPoint = cards[i]->getGridPos();
|
|
if (gridPoint.x() == -1)
|
|
continue;
|
|
|
|
QPointF mapPoint = mapFromGrid(gridPoint);
|
|
qreal x = mapPoint.x();
|
|
qreal y = mapPoint.y();
|
|
|
|
int numberAttachedCards = cards[i]->getAttachedCards().size();
|
|
qreal actualX = x + numberAttachedCards * STACKED_CARD_OFFSET_X;
|
|
qreal actualY = y;
|
|
if (numberAttachedCards)
|
|
actualY += 15;
|
|
|
|
cards[i]->setPos(actualX, actualY);
|
|
cards[i]->setRealZValue((actualY + CARD_HEIGHT) * 100000 + (actualX + 1) * 100);
|
|
|
|
QListIterator<CardItem *> attachedCardIterator(cards[i]->getAttachedCards());
|
|
int j = 0;
|
|
while (attachedCardIterator.hasNext()) {
|
|
++j;
|
|
CardItem *attachedCard = attachedCardIterator.next();
|
|
qreal childX = actualX - j * STACKED_CARD_OFFSET_X;
|
|
qreal childY = y + 5;
|
|
attachedCard->setPos(childX, childY);
|
|
attachedCard->setRealZValue((childY + CARD_HEIGHT) * 100000 + (childX + 1) * 100);
|
|
for (ArrowItem *item : attachedCard->getArrowsFrom()) {
|
|
arrowsToUpdate.insert(item);
|
|
}
|
|
for (ArrowItem *item : attachedCard->getArrowsTo()) {
|
|
arrowsToUpdate.insert(item);
|
|
}
|
|
}
|
|
|
|
for (ArrowItem *item : cards[i]->getArrowsFrom()) {
|
|
arrowsToUpdate.insert(item);
|
|
}
|
|
for (ArrowItem *item : cards[i]->getArrowsTo()) {
|
|
arrowsToUpdate.insert(item);
|
|
}
|
|
}
|
|
for (ArrowItem *item : arrowsToUpdate) {
|
|
item->updatePath();
|
|
}
|
|
|
|
resizeToContents();
|
|
update();
|
|
}
|
|
|
|
void TableZone::toggleTapped()
|
|
{
|
|
QList<QGraphicsItem *> selectedItems = scene()->selectedItems();
|
|
bool tapAll = false;
|
|
for (int i = 0; i < selectedItems.size(); i++)
|
|
if (!qgraphicsitem_cast<CardItem *>(selectedItems[i])->getTapped()) {
|
|
tapAll = true;
|
|
break;
|
|
}
|
|
QList<const ::google::protobuf::Message *> cmdList;
|
|
for (int i = 0; i < selectedItems.size(); i++) {
|
|
CardItem *temp = qgraphicsitem_cast<CardItem *>(selectedItems[i]);
|
|
if (temp->getTapped() != tapAll) {
|
|
Command_SetCardAttr *cmd = new Command_SetCardAttr;
|
|
cmd->set_zone(name.toStdString());
|
|
cmd->set_card_id(temp->getId());
|
|
cmd->set_attribute(AttrTapped);
|
|
cmd->set_attr_value(tapAll ? "1" : "0");
|
|
cmdList.append(cmd);
|
|
}
|
|
}
|
|
player->sendGameCommand(player->prepareGameCommand(cmdList));
|
|
}
|
|
|
|
CardItem *TableZone::takeCard(int position, int cardId, bool canResize)
|
|
{
|
|
CardItem *result = CardZone::takeCard(position, cardId);
|
|
if (canResize)
|
|
resizeToContents();
|
|
return result;
|
|
}
|
|
|
|
void TableZone::resizeToContents()
|
|
{
|
|
int xMax = 0;
|
|
|
|
// Find rightmost card position, which includes the left margin amount.
|
|
for (int i = 0; i < cards.size(); ++i)
|
|
if (cards[i]->pos().x() > xMax)
|
|
xMax = (int)cards[i]->pos().x();
|
|
|
|
// Minimum width is the rightmost card position plus enough room for
|
|
// another card with padding, then margin.
|
|
currentMinimumWidth = xMax + (2 * CARD_WIDTH) + PADDING_X + MARGIN_RIGHT;
|
|
|
|
if (currentMinimumWidth < MIN_WIDTH)
|
|
currentMinimumWidth = MIN_WIDTH;
|
|
|
|
if (currentMinimumWidth != width) {
|
|
prepareGeometryChange();
|
|
width = currentMinimumWidth;
|
|
emit sizeChanged();
|
|
}
|
|
}
|
|
|
|
CardItem *TableZone::getCardFromGrid(const QPoint &gridPoint) const
|
|
{
|
|
for (int i = 0; i < cards.size(); i++)
|
|
if (cards.at(i)->getGridPoint() == gridPoint)
|
|
return cards.at(i);
|
|
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.
|
|
// First pass: compute the number of cards at each card stack.
|
|
QMap<int, int> cardStackCount;
|
|
for (int i = 0; i < cards.size(); ++i) {
|
|
const QPoint &gridPoint = cards[i]->getGridPos();
|
|
if (gridPoint.x() == -1)
|
|
continue;
|
|
|
|
const int key = getCardStackMapKey(gridPoint.x() / 3, gridPoint.y());
|
|
cardStackCount.insert(key, cardStackCount.value(key, 0) + 1);
|
|
}
|
|
|
|
// Second pass: compute the width at each card stack.
|
|
cardStackWidth.clear();
|
|
for (int i = 0; i < cards.size(); ++i) {
|
|
const QPoint &gridPoint = cards[i]->getGridPos();
|
|
if (gridPoint.x() == -1)
|
|
continue;
|
|
|
|
const int key = getCardStackMapKey(gridPoint.x() / 3, gridPoint.y());
|
|
const int stackCount = cardStackCount.value(key, 0);
|
|
if (stackCount == 1)
|
|
cardStackWidth.insert(key, CARD_WIDTH + cards[i]->getAttachedCards().size() * STACKED_CARD_OFFSET_X);
|
|
else
|
|
cardStackWidth.insert(key, CARD_WIDTH + (stackCount - 1) * STACKED_CARD_OFFSET_X);
|
|
}
|
|
}
|
|
|
|
QPointF TableZone::mapFromGrid(QPoint gridPoint) const
|
|
{
|
|
qreal x, y;
|
|
|
|
// Start with margin plus stacked card offset
|
|
x = MARGIN_LEFT + (gridPoint.x() % 3) * STACKED_CARD_OFFSET_X;
|
|
|
|
// Add in width of card stack plus padding for each column
|
|
for (int i = 0; i < gridPoint.x() / 3; ++i) {
|
|
const int key = getCardStackMapKey(i, gridPoint.y());
|
|
x += cardStackWidth.value(key, CARD_WIDTH) + PADDING_X;
|
|
}
|
|
|
|
if (isInverted())
|
|
gridPoint.setY(TABLEROWS - 1 - gridPoint.y());
|
|
|
|
// Start with margin plus stacked card offset
|
|
y = MARGIN_TOP + (gridPoint.x() % 3) * STACKED_CARD_OFFSET_Y;
|
|
|
|
// Add in card size and padding for each row
|
|
for (int i = 0; i < gridPoint.y(); ++i)
|
|
y += CARD_HEIGHT + PADDING_Y;
|
|
|
|
return QPointF(x, y);
|
|
}
|
|
|
|
QPoint TableZone::mapToGrid(const QPointF &mapPoint) const
|
|
{
|
|
// Begin by calculating the y-coordinate of the grid space, which will be
|
|
// used for the x-coordinate.
|
|
|
|
// Offset point by the margin amount to reference point within grid area.
|
|
int y = mapPoint.y() - MARGIN_TOP;
|
|
|
|
// Below calculation effectively rounds to the nearest grid point.
|
|
const int gridPointHeight = CARD_HEIGHT + PADDING_Y;
|
|
int gridPointY = (y + PADDING_Y / 2) / gridPointHeight;
|
|
|
|
gridPointY = clampValidTableRow(gridPointY);
|
|
|
|
if (isInverted())
|
|
gridPointY = TABLEROWS - 1 - gridPointY;
|
|
|
|
// Calculating the x-coordinate of the grid space requires adding up the
|
|
// widths of each card stack along the row.
|
|
|
|
// Offset point by the margin amount to reference point within grid area.
|
|
int x = mapPoint.x() - MARGIN_LEFT + PADDING_X / 2;
|
|
|
|
// Maximum value is a card width from the right margin, referenced to the
|
|
// grid area.
|
|
const int xMax = width - MARGIN_LEFT - MARGIN_RIGHT - CARD_WIDTH;
|
|
|
|
int xStack = 0;
|
|
int xNextStack = 0;
|
|
int nextStackCol = 0;
|
|
while ((xNextStack <= x) && (xNextStack <= xMax)) {
|
|
xStack = xNextStack;
|
|
const int key = getCardStackMapKey(nextStackCol, gridPointY);
|
|
xNextStack += cardStackWidth.value(key, CARD_WIDTH) + PADDING_X;
|
|
nextStackCol++;
|
|
}
|
|
int stackCol = qMax(nextStackCol - 1, 0);
|
|
|
|
// Have the stack column, need to refine to the grid column. Take the
|
|
// difference between the point and the stack point and divide by stacked
|
|
// card offsets.
|
|
int xDiff = x - xStack;
|
|
int gridPointX = stackCol * 3 + qMin(xDiff / STACKED_CARD_OFFSET_X, 2);
|
|
|
|
return QPoint(gridPointX, gridPointY);
|
|
}
|
|
|
|
QPointF TableZone::closestGridPoint(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);
|
|
}
|
|
|
|
int TableZone::clampValidTableRow(const int row)
|
|
{
|
|
if (row < 0)
|
|
return 0;
|
|
if (row >= TABLEROWS)
|
|
return TABLEROWS - 1;
|
|
return row;
|
|
}
|