mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-13 13:30:00 -08:00
Merge branch 'Cockatrice:master' into master
This commit is contained in:
commit
3d3a414473
40 changed files with 1137 additions and 355 deletions
|
|
@ -171,6 +171,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/cards/card_info_picture_widget.cpp
|
||||
src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp
|
||||
src/interface/widgets/cards/card_info_text_widget.cpp
|
||||
src/interface/widgets/cards/card_picture_delegate.cpp
|
||||
src/interface/widgets/cards/card_size_widget.cpp
|
||||
src/interface/widgets/cards/deck_card_zone_display_widget.cpp
|
||||
src/interface/widgets/cards/deck_preview_card_picture_widget.cpp
|
||||
|
|
@ -290,6 +291,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/utility/sequence_edit.cpp
|
||||
src/interface/widgets/utility/visibility_change_listener.cpp
|
||||
src/interface/widgets/utility/visibility_change_listener.h
|
||||
src/interface/widgets/visual_database_display/card_database_visual_view.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
|
||||
|
|
|
|||
|
|
@ -11,6 +11,27 @@
|
|||
|
||||
class QObject;
|
||||
|
||||
namespace
|
||||
{
|
||||
/**
|
||||
* @brief Visits every filter item below @p node, depth first.
|
||||
*/
|
||||
template <typename Callback> void forEachFilter(const FilterTreeNode *node, const Callback &callback)
|
||||
{
|
||||
if (node == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (const auto *filterItem = dynamic_cast<const FilterItem *>(node)) {
|
||||
callback(filterItem);
|
||||
}
|
||||
|
||||
for (int i = 0; i < node->childCount(); ++i) {
|
||||
forEachFilter(node->nodeAt(i), callback);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
FilterTreeModel::FilterTreeModel(QObject *parent) : QAbstractItemModel(parent)
|
||||
{
|
||||
fTree = new FilterTree;
|
||||
|
|
@ -110,46 +131,31 @@ void FilterTreeModel::clearFiltersOfType(CardFilter::Attr filterType)
|
|||
QList<const CardFilter *> FilterTreeModel::getFiltersOfType(CardFilter::Attr filterType) const
|
||||
{
|
||||
QList<const CardFilter *> filters;
|
||||
if (!fTree) {
|
||||
return filters;
|
||||
}
|
||||
|
||||
std::function<void(const FilterTreeNode *)> traverse;
|
||||
traverse = [&](const FilterTreeNode *node) {
|
||||
if (const auto *filterItem = dynamic_cast<const FilterItem *>(node)) {
|
||||
if (filterItem->attr() == filterType) {
|
||||
QString text = filterItem->text();
|
||||
filters.append(new CardFilter(text, filterItem->type(), filterItem->attr()));
|
||||
}
|
||||
forEachFilter(fTree, [&](const FilterItem *filterItem) {
|
||||
if (filterItem->attr() == filterType) {
|
||||
filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr()));
|
||||
}
|
||||
for (int i = 0; i < node->childCount(); ++i) {
|
||||
traverse(node->nodeAt(i));
|
||||
}
|
||||
};
|
||||
|
||||
traverse(fTree);
|
||||
});
|
||||
return filters;
|
||||
}
|
||||
|
||||
QStringList FilterTreeModel::getFilterTermsOfType(CardFilter::Attr filterType) const
|
||||
{
|
||||
QStringList terms;
|
||||
forEachFilter(fTree, [&](const FilterItem *filterItem) {
|
||||
if (filterItem->attr() == filterType) {
|
||||
terms.append(filterItem->text());
|
||||
}
|
||||
});
|
||||
return terms;
|
||||
}
|
||||
|
||||
QList<const CardFilter *> FilterTreeModel::allFilters() const
|
||||
{
|
||||
QList<const CardFilter *> filters;
|
||||
if (!fTree) {
|
||||
return filters;
|
||||
}
|
||||
|
||||
std::function<void(const FilterTreeNode *)> traverse;
|
||||
traverse = [&](const FilterTreeNode *node) {
|
||||
if (const auto *filterItem = dynamic_cast<const FilterItem *>(node)) {
|
||||
QString text = filterItem->text();
|
||||
filters.append(new CardFilter(text, filterItem->type(), filterItem->attr()));
|
||||
}
|
||||
for (int i = 0; i < node->childCount(); ++i) {
|
||||
traverse(node->nodeAt(i));
|
||||
}
|
||||
};
|
||||
|
||||
traverse(fTree);
|
||||
forEachFilter(fTree, [&](const FilterItem *filterItem) {
|
||||
filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr()));
|
||||
});
|
||||
return filters;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,14 @@ public slots:
|
|||
void addFilter(const CardFilter *f);
|
||||
void removeFilter(const CardFilter *f);
|
||||
void clearFiltersOfType(CardFilter::Attr filterType);
|
||||
/**
|
||||
* @brief Copies every active filter of one attribute into new CardFilter objects.
|
||||
*
|
||||
* The caller owns the returned filters. Prefer getFilterTermsOfType() when only
|
||||
* the terms are needed.
|
||||
*/
|
||||
[[nodiscard]] QList<const CardFilter *> getFiltersOfType(CardFilter::Attr filterType) const;
|
||||
/// @brief Copies every active filter into new CardFilter objects. The caller owns them.
|
||||
[[nodiscard]] QList<const CardFilter *> allFilters() const;
|
||||
|
||||
private slots:
|
||||
|
|
@ -48,6 +55,13 @@ public:
|
|||
{
|
||||
return fTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The texts of every active filter of one attribute, without allocating filters.
|
||||
*
|
||||
* Prefer this over getFiltersOfType() when only the filter terms are needed.
|
||||
*/
|
||||
[[nodiscard]] QStringList getFilterTermsOfType(CardFilter::Attr filterType) const;
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
[[nodiscard]] int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <QBrush>
|
||||
#include <QColor>
|
||||
#include <QCursor>
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
|
|
@ -51,7 +52,20 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
|
|||
update();
|
||||
});
|
||||
|
||||
connect(item, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater);
|
||||
// The source card can be destroyed mid-drag (e.g. moved by a server event). deleteLater() alone is not enough:
|
||||
// a scene repaint can run before the deferred delete and paint the dead card. Detach from the scene immediately.
|
||||
connect(_item, &QObject::destroyed, this, [this]() {
|
||||
hide();
|
||||
if (QGraphicsScene *sc = scene()) {
|
||||
sc->removeItem(this);
|
||||
}
|
||||
deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
AbstractCardItem *AbstractCardDragItem::getItem() const
|
||||
{
|
||||
return item.data();
|
||||
}
|
||||
|
||||
QPainterPath AbstractCardDragItem::shape() const
|
||||
|
|
@ -65,6 +79,10 @@ QPainterPath AbstractCardDragItem::shape() const
|
|||
|
||||
void AbstractCardDragItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
|
||||
{
|
||||
if (item.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
item->paint(painter, option, widget);
|
||||
|
||||
// adds a mask to the card so it looks like the card hasnt been placed yet
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPointF>
|
||||
#include <QPointer>
|
||||
#include <QRectF>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
|
|
@ -24,7 +25,8 @@ class AbstractCardDragItem : public QObject, public QGraphicsItem
|
|||
Q_OBJECT
|
||||
Q_INTERFACES(QGraphicsItem)
|
||||
protected:
|
||||
AbstractCardItem *item;
|
||||
/// Guarded pointer: the source card may be destroyed while this drag item still exists.
|
||||
QPointer<AbstractCardItem> item;
|
||||
QPointF hotSpot;
|
||||
QList<AbstractCardDragItem *> childDrags;
|
||||
|
||||
|
|
@ -44,10 +46,8 @@ public:
|
|||
}
|
||||
[[nodiscard]] QPainterPath shape() const override;
|
||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||
[[nodiscard]] AbstractCardItem *getItem() const
|
||||
{
|
||||
return item;
|
||||
}
|
||||
/// @return the source card, or nullptr if it has already been destroyed.
|
||||
[[nodiscard]] AbstractCardItem *getItem() const;
|
||||
[[nodiscard]] QPointF getHotSpot() const
|
||||
{
|
||||
return hotSpot;
|
||||
|
|
|
|||
|
|
@ -130,8 +130,9 @@ void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||
sc->removeItem(this);
|
||||
|
||||
QList<CardDragItem *> dragItemList;
|
||||
CardZoneLogic *startZone = static_cast<CardItem *>(item)->getZone();
|
||||
if (currentZone && !(static_cast<CardItem *>(item)->getAttachedTo() && (startZone == currentZone->getLogic()))) {
|
||||
CardZoneLogic *startZone = static_cast<CardItem *>(item.data())->getZone();
|
||||
if (currentZone &&
|
||||
!(static_cast<CardItem *>(item.data())->getAttachedTo() && (startZone == currentZone->getLogic()))) {
|
||||
if (!occupied) {
|
||||
dragItemList.append(this);
|
||||
}
|
||||
|
|
@ -139,7 +140,7 @@ void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||
for (int i = 0; i < childDrags.size(); i++) {
|
||||
CardDragItem *c = static_cast<CardDragItem *>(childDrags[i]);
|
||||
if (!occupied &&
|
||||
!(static_cast<CardItem *>(c->item)->getAttachedTo() && (startZone == currentZone->getLogic())) &&
|
||||
!(static_cast<CardItem *>(c->item.data())->getAttachedTo() && (startZone == currentZone->getLogic())) &&
|
||||
!c->occupied) {
|
||||
dragItemList.append(c);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ void DeckViewCardDragItem::updatePosition(const QPointF &cursorScenePos)
|
|||
|
||||
void DeckViewCardDragItem::handleDrop(DeckViewCardContainer *target)
|
||||
{
|
||||
auto *card = static_cast<DeckViewCard *>(item);
|
||||
auto *card = static_cast<DeckViewCard *>(item.data());
|
||||
auto *start = static_cast<DeckViewCardContainer *>(item->parentItem());
|
||||
start->removeCard(card);
|
||||
target->addCard(card);
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
|||
});
|
||||
|
||||
card.emitPixmapUpdated();
|
||||
emit getInstance().pixmapLoaded(card);
|
||||
}
|
||||
|
||||
void CardPictureLoader::deleteAllLocalOverrides(const ExactCard &card)
|
||||
|
|
|
|||
|
|
@ -146,6 +146,14 @@ signals:
|
|||
/** @brief Emitted after the worker has finished clearing the network and redirect caches. */
|
||||
void networkCacheCleared();
|
||||
|
||||
/**
|
||||
* @brief Emitted once an image has been loaded and put into the pixmap cache.
|
||||
*
|
||||
* Views that paint cards from the loader without owning a widget per card can
|
||||
* listen to this to repaint the rows whose picture just became available.
|
||||
*/
|
||||
void pixmapLoaded(const ExactCard &card);
|
||||
|
||||
private slots:
|
||||
/**
|
||||
* @brief Triggered when the user changes the picture download settings.
|
||||
|
|
|
|||
|
|
@ -43,11 +43,6 @@
|
|||
#include <qlogging.h>
|
||||
#include <qnamespace.h>
|
||||
|
||||
static constexpr qreal MTG_CARD_ASPECT_RATIO = 1.396;
|
||||
// static constexpr qreal YUGIOH_CARD_ASPECT_RATIO = 1.457;
|
||||
static constexpr qreal ASPECT_RATIO = MTG_CARD_ASPECT_RATIO;
|
||||
|
||||
static constexpr int BASE_WIDTH = 200;
|
||||
static constexpr int BASE_HEIGHT = 200;
|
||||
|
||||
static constexpr int ENLARGED_PIXMAP_OFFSET = 10;
|
||||
|
|
@ -162,12 +157,11 @@ void CardInfoPictureWidget::resizeEvent(QResizeEvent *event)
|
|||
*/
|
||||
void CardInfoPictureWidget::setScaleFactor(const int scale)
|
||||
{
|
||||
const int newWidth = BASE_WIDTH * scale / 100;
|
||||
const int newHeight = static_cast<int>(newWidth * ASPECT_RATIO);
|
||||
const QSize newSize = sizeForScaleFactor(scale);
|
||||
|
||||
scaleFactor = scale;
|
||||
|
||||
setFixedSize(newWidth, newHeight);
|
||||
setFixedSize(newSize);
|
||||
updatePixmap();
|
||||
|
||||
emit cardScaleFactorChanged(scale);
|
||||
|
|
@ -266,8 +260,13 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event)
|
|||
*/
|
||||
QSize CardInfoPictureWidget::sizeHint() const
|
||||
{
|
||||
return {static_cast<int>(BASE_WIDTH * scaleFactor / 100.0),
|
||||
static_cast<int>(BASE_WIDTH * scaleFactor / 100.0 * ASPECT_RATIO)};
|
||||
return sizeForScaleFactor(scaleFactor);
|
||||
}
|
||||
|
||||
QSize CardInfoPictureWidget::sizeForScaleFactor(int scaleFactor)
|
||||
{
|
||||
const int width = BASE_WIDTH * scaleFactor / 100;
|
||||
return {width, static_cast<int>(width * ASPECT_RATIO)};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ class CardInfoPictureWidget : public QWidget
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// Width of a card picture at 100 % scale, in pixels.
|
||||
static constexpr int BASE_WIDTH = 200;
|
||||
/// Height / width of a card picture.
|
||||
static constexpr qreal ASPECT_RATIO = 1.396;
|
||||
|
||||
explicit CardInfoPictureWidget(QWidget *parent = nullptr,
|
||||
bool hoverToZoomEnabled = false,
|
||||
bool raiseOnEnter = false);
|
||||
|
|
@ -37,6 +42,12 @@ public:
|
|||
}
|
||||
[[nodiscard]] QSize sizeHint() const override;
|
||||
|
||||
/**
|
||||
* @brief The size a card picture has at @p scaleFactor percent.
|
||||
* @param scaleFactor Percentage of BASE_WIDTH, as set by the card size slider.
|
||||
*/
|
||||
[[nodiscard]] static QSize sizeForScaleFactor(int scaleFactor);
|
||||
|
||||
public slots:
|
||||
void setCard(const ExactCard &card);
|
||||
void setScaleFactor(int scale); // New slot for scaling
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
#include "card_picture_delegate.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../card_picture_loader/card_picture_loader.h"
|
||||
#include "card_art_utils.h"
|
||||
#include "card_database_visual_display_model.h"
|
||||
#include "libcockatrice/card/printing/exact_card.h"
|
||||
#include "libcockatrice/settings/cards_display_settings.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPixmap>
|
||||
#include <QRect>
|
||||
#include <QStyleOptionViewItem>
|
||||
#include <libcockatrice/card/printing/printing_info.h>
|
||||
|
||||
CardPictureDelegate::CardPictureDelegate(CardDatabaseVisualDisplayModel *_model, QObject *parent)
|
||||
: QStyledItemDelegate(parent), model(_model)
|
||||
{
|
||||
}
|
||||
|
||||
void CardPictureDelegate::setCardSize(const QSize &size)
|
||||
{
|
||||
if (cardSize == size) {
|
||||
return;
|
||||
}
|
||||
|
||||
cardSize = size;
|
||||
}
|
||||
|
||||
void CardPictureDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
if (model == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ExactCard card = model->cardAt(index.row());
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The grid cell is the card plus SPACING; give the picture the rest.
|
||||
const int inset = SPACING / 2;
|
||||
const QRect cardArea = option.rect.adjusted(inset, inset, -inset, -inset);
|
||||
|
||||
QPixmap pixmap;
|
||||
CardPictureLoader::getCardBackLoadingInProgressPixmap(pixmap, cardArea.size());
|
||||
CardPictureLoader::getPixmap(pixmap, card, cardArea.size());
|
||||
|
||||
if (pixmap.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) {
|
||||
pixmap = CardArtUtils::rotateSidewaysLayoutArt(pixmap, card);
|
||||
}
|
||||
|
||||
// Fit the picture into the cell and center it, like CardInfoPictureWidget does.
|
||||
const QSize pictureSize = pixmap.deviceIndependentSize().toSize().scaled(cardArea.size(), Qt::KeepAspectRatio);
|
||||
const QRect pictureRect(cardArea.center().x() - pictureSize.width() / 2,
|
||||
cardArea.center().y() - pictureSize.height() / 2, pictureSize.width(),
|
||||
pictureSize.height());
|
||||
|
||||
const qreal radius = SettingsCache::instance().cardsDisplay().getRoundCardCorners()
|
||||
? 0.05 * static_cast<qreal>(pictureRect.width())
|
||||
: 0.0;
|
||||
|
||||
painter->save();
|
||||
painter->setRenderHint(QPainter::Antialiasing);
|
||||
painter->setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
QPainterPath shape;
|
||||
shape.addRoundedRect(pictureRect, radius, radius);
|
||||
painter->setClipPath(shape);
|
||||
// Source rect is in device pixels, target in logical ones: drawPixmap scales
|
||||
// between them without the device pixel ratio being applied twice.
|
||||
painter->drawPixmap(pictureRect, pixmap, QRectF(pixmap.rect()));
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
QSize CardPictureDelegate::sizeHint(const QStyleOptionViewItem & /*option*/, const QModelIndex & /*index*/) const
|
||||
{
|
||||
return cardSize + QSize(SPACING, SPACING);
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* @file card_picture_delegate.h
|
||||
* @ingroup CardWidgets
|
||||
* @brief Paints a card picture for a visual card grid item.
|
||||
*/
|
||||
|
||||
#ifndef CARD_PICTURE_DELEGATE_H
|
||||
#define CARD_PICTURE_DELEGATE_H
|
||||
|
||||
#include <QSize>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class CardDatabaseVisualDisplayModel;
|
||||
|
||||
/**
|
||||
* @brief Draws the card of a CardDatabaseVisualDisplayModel row as a picture.
|
||||
*
|
||||
* The delegate only paints rows the view asks for, so a grid of the whole card
|
||||
* database stays cheap: no widget per card and no pixmap loaded for items that
|
||||
* are off screen. Card images come from CardPictureLoader, which caches them and
|
||||
* loads missing ones asynchronously.
|
||||
*/
|
||||
class CardPictureDelegate : public QStyledItemDelegate
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// Gap left around a card picture inside its grid cell, in pixels.
|
||||
static constexpr int SPACING = 4;
|
||||
|
||||
explicit CardPictureDelegate(CardDatabaseVisualDisplayModel *_model, QObject *parent = nullptr);
|
||||
|
||||
/// The picture size inside a grid cell. The cell is this size plus SPACING.
|
||||
void setCardSize(const QSize &size);
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
[[nodiscard]] QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
|
||||
private:
|
||||
CardDatabaseVisualDisplayModel *model;
|
||||
QSize cardSize;
|
||||
};
|
||||
|
||||
#endif // CARD_PICTURE_DELEGATE_H
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
|
||||
#include <QAbstractScrollArea>
|
||||
#include <QEvent>
|
||||
#include <QFlags>
|
||||
#include <QHBoxLayout>
|
||||
|
|
@ -33,8 +34,8 @@ CardSizeWidget::CardSizeWidget(QWidget *parent, FlowWidget *_flowWidget, int def
|
|||
cardSizeLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||
|
||||
cardSizeSlider = new QSlider(Qt::Horizontal, this);
|
||||
cardSizeSlider->setRange(50, 250); ///< Slider range for card size adjustment.
|
||||
cardSizeSlider->setValue(defaultValue); ///< Initial slider value.
|
||||
cardSizeSlider->setRange(MIN_CARD_SIZE, MAX_CARD_SIZE); ///< Slider range for card size adjustment.
|
||||
cardSizeSlider->setValue(defaultValue); ///< Initial slider value.
|
||||
|
||||
cardSizeLayout->addWidget(cardSizeLabel);
|
||||
cardSizeLayout->addWidget(cardSizeSlider);
|
||||
|
|
@ -73,13 +74,26 @@ QSlider *CardSizeWidget::getSlider() const
|
|||
|
||||
void CardSizeWidget::enableCtrlScrollResize(QWidget *host)
|
||||
{
|
||||
host->installEventFilter(this);
|
||||
|
||||
const auto scrollAreas = host->findChildren<QScrollArea *>();
|
||||
for (QScrollArea *scrollArea : scrollAreas) {
|
||||
if (QWidget *content = scrollArea->widget()) {
|
||||
content->installEventFilter(this);
|
||||
const auto watchScrollArea = [this](QAbstractScrollArea *scrollArea) {
|
||||
if (scrollArea == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (QWidget *viewport = scrollArea->viewport()) {
|
||||
viewport->installEventFilter(this);
|
||||
}
|
||||
if (auto *contentScrollArea = qobject_cast<QScrollArea *>(scrollArea)) {
|
||||
if (QWidget *content = contentScrollArea->widget()) {
|
||||
content->installEventFilter(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
host->installEventFilter(this);
|
||||
watchScrollArea(qobject_cast<QAbstractScrollArea *>(host));
|
||||
|
||||
const auto scrollAreas = host->findChildren<QAbstractScrollArea *>();
|
||||
for (QAbstractScrollArea *scrollArea : scrollAreas) {
|
||||
watchScrollArea(scrollArea);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ class CardSizeWidget : public QWidget
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// Range of the card size slider, as a percentage of the base card width.
|
||||
static constexpr int MIN_CARD_SIZE = 50;
|
||||
static constexpr int MAX_CARD_SIZE = 250;
|
||||
|
||||
explicit CardSizeWidget(QWidget *parent, FlowWidget *flowWidget = nullptr, int defaultValue = 100);
|
||||
[[nodiscard]] QSlider *getSlider() const;
|
||||
|
||||
|
|
@ -31,8 +35,9 @@ public:
|
|||
* @brief Resizes the cards when the user Ctrl + scrolls over @p host or any of its descendants.
|
||||
*
|
||||
* Installs this widget as an event filter on the given host widget. If the host contains a
|
||||
* scroll area the filter is also installed on the scroll area's content widget, so the resize
|
||||
* intercepts the wheel event before the scroll area would scroll the view.
|
||||
* scroll area the filter is also installed on the scroll area's content widget, and on the
|
||||
* viewport of every scroll area base (QListView, QTableView, ...), so the resize intercepts
|
||||
* the wheel event before the scroll area would scroll the view.
|
||||
*/
|
||||
void enableCtrlScrollResize(QWidget *host);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,203 @@
|
|||
#include "card_database_visual_view.h"
|
||||
|
||||
#include "../../card_picture_loader/card_picture_loader.h"
|
||||
#include "../cards/card_info_picture_widget.h"
|
||||
#include "../cards/card_picture_delegate.h"
|
||||
#include "../cards/card_size_widget.h"
|
||||
#include "card_database_visual_display_model.h"
|
||||
#include "libcockatrice/card/printing/exact_card.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QAbstractItemView>
|
||||
#include <QApplication>
|
||||
#include <QCursor>
|
||||
#include <QEvent>
|
||||
#include <QFrame>
|
||||
#include <QModelIndex>
|
||||
#include <QMouseEvent>
|
||||
#include <QScrollBar>
|
||||
#include <QSize>
|
||||
#include <QWheelEvent>
|
||||
#include <QWidget>
|
||||
#include <QtGlobal>
|
||||
#include <qnamespace.h>
|
||||
|
||||
CardDatabaseVisualView::CardDatabaseVisualView(CardDatabaseVisualDisplayModel *_model, QWidget *parent)
|
||||
: QListView(parent), model(_model)
|
||||
{
|
||||
auto *cardDelegate = new CardPictureDelegate(model, this);
|
||||
setItemDelegate(cardDelegate);
|
||||
setModel(model);
|
||||
|
||||
setViewMode(QListView::IconMode);
|
||||
setFlow(QListView::LeftToRight);
|
||||
setWrapping(true);
|
||||
setResizeMode(QListView::Adjust);
|
||||
setMovement(QListView::Static);
|
||||
setUniformItemSizes(true);
|
||||
setLayoutMode(QListView::SinglePass);
|
||||
setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
setWordWrap(false);
|
||||
|
||||
// QListView hands the delegate's item height to the scroll bar as its single
|
||||
// step under ScrollPerPixel, so one wheel notch would jump wheelScrollLines()
|
||||
// whole card rows. Give the bar a text line instead; the wheel handler below
|
||||
// scales that line to the grid. A positive step also stops Qt from taking the
|
||||
// item height back on the next layout.
|
||||
verticalScrollBar()->setSingleStep(qMax(1, fontMetrics().height()));
|
||||
|
||||
// The grid is a read-only surface: clicks are actions on a card, not a
|
||||
// selection, and they must not steal focus from the search field.
|
||||
setSelectionMode(QAbstractItemView::NoSelection);
|
||||
setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
setFocusPolicy(Qt::NoFocus);
|
||||
|
||||
// Hover drives the card preview.
|
||||
setMouseTracking(true);
|
||||
viewport()->setMouseTracking(true);
|
||||
|
||||
connect(&CardPictureLoader::getInstance(), &CardPictureLoader::pixmapLoaded, this,
|
||||
[this] { viewport()->update(); });
|
||||
|
||||
// A new search result starts at the top, like the widget-per-card grid did.
|
||||
connect(model, &QAbstractItemModel::modelReset, this, [this] {
|
||||
verticalScrollBar()->setValue(0);
|
||||
// The rows under a stationary cursor changed with the reset: re-resolve the
|
||||
// hover so the preview follows the grid, as enterEvent did per card widget.
|
||||
hoveredRow = -1;
|
||||
updateHoveredRow(viewport()->mapFromGlobal(QCursor::pos()));
|
||||
});
|
||||
|
||||
setCardSizeFactor(cardSizeFactor);
|
||||
}
|
||||
|
||||
void CardDatabaseVisualView::setCardSizeFactor(int factor)
|
||||
{
|
||||
cardSizeFactor = qBound(CardSizeWidget::MIN_CARD_SIZE, factor, CardSizeWidget::MAX_CARD_SIZE);
|
||||
|
||||
const QSize cardSize = CardInfoPictureWidget::sizeForScaleFactor(cardSizeFactor);
|
||||
if (auto *cardDelegate = qobject_cast<CardPictureDelegate *>(itemDelegate())) {
|
||||
cardDelegate->setCardSize(cardSize);
|
||||
}
|
||||
// The delegate's sizeHint does not emit sizeHintChanged, so the grid size is what
|
||||
// makes a resize take effect. Keep the two in step.
|
||||
setGridSize(cardSize + QSize(CardPictureDelegate::SPACING, CardPictureDelegate::SPACING));
|
||||
}
|
||||
|
||||
void CardDatabaseVisualView::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
const QModelIndex index = indexAt(event->pos());
|
||||
if (index.isValid() && model != nullptr) {
|
||||
const ExactCard card = model->cardAt(index.row());
|
||||
if (card) {
|
||||
emit cardClicked(event, card);
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QListView::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void CardDatabaseVisualView::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
QListView::mouseMoveEvent(event);
|
||||
updateHoveredRow(event->pos());
|
||||
}
|
||||
|
||||
bool CardDatabaseVisualView::viewportEvent(QEvent *event)
|
||||
{
|
||||
// A per-card widget reported its hover from enterEvent, which also fires when the
|
||||
// grid changes under a stationary cursor. A QListView only reports moves, so
|
||||
// re-resolve the hover on enter and clear it on leave.
|
||||
if (event->type() == QEvent::Enter) {
|
||||
updateHoveredRow(viewport()->mapFromGlobal(QCursor::pos()));
|
||||
} else if (event->type() == QEvent::Leave) {
|
||||
hoveredRow = -1;
|
||||
}
|
||||
|
||||
return QListView::viewportEvent(event);
|
||||
}
|
||||
|
||||
void CardDatabaseVisualView::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
// Ctrl+wheel is the card-size gesture and is filtered out before it reaches
|
||||
// the view; any other modifier keeps QAbstractSlider's own page scrolling.
|
||||
if (event->modifiers() != Qt::NoModifier) {
|
||||
QListView::wheelEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
QScrollBar *bar = verticalScrollBar();
|
||||
if (!bar->isVisible()) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
// Touchpads and high-resolution wheels already report pixels: follow them.
|
||||
if (!event->pixelDelta().isNull()) {
|
||||
bar->setValue(bar->value() - event->pixelDelta().y());
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->angleDelta().y() == 0) {
|
||||
QListView::wheelEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Accumulate wheel deltas smaller than a notch, like QAbstractSlider does, so
|
||||
// a fine-grained wheel still moves.
|
||||
wheelRemainder += static_cast<qreal>(event->angleDelta().y()) / 120.0;
|
||||
const int notches = static_cast<int>(wheelRemainder);
|
||||
if (notches == 0) {
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
wheelRemainder -= notches;
|
||||
|
||||
const int pixels = -notches * wheelStep();
|
||||
bar->setValue(qBound(bar->minimum(), bar->value() + pixels, bar->maximum()));
|
||||
event->accept();
|
||||
}
|
||||
|
||||
int CardDatabaseVisualView::wheelStep() const
|
||||
{
|
||||
// A card row is far too coarse for one wheel line, and a text line is too
|
||||
// fine, so a line is an eighth of the grid cell. A notch then moves about a
|
||||
// third of a card row at any card size, and the OS line count still applies.
|
||||
const int lineHeight = qMax(1, gridSize().height() / 8);
|
||||
return qMax(1, QApplication::wheelScrollLines()) * lineHeight;
|
||||
}
|
||||
|
||||
void CardDatabaseVisualView::updateHoveredRow(const QPoint &position)
|
||||
{
|
||||
// indexAt() also answers for points outside the viewport, so a cursor parked
|
||||
// below the grid (on the size slider, say) would resolve to the row just under
|
||||
// the visible area. Only a point inside the viewport can hover a card.
|
||||
if (model == nullptr || !viewport()->rect().contains(position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QModelIndex index = indexAt(position);
|
||||
const int row = index.isValid() ? index.row() : -1;
|
||||
if (row == hoveredRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
hoveredRow = row;
|
||||
if (row >= 0) {
|
||||
const ExactCard card = model->cardAt(row);
|
||||
if (card) {
|
||||
emit cardHovered(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CardDatabaseVisualView::leaveEvent(QEvent *event)
|
||||
{
|
||||
hoveredRow = -1;
|
||||
QListView::leaveEvent(event);
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* @file card_database_visual_view.h
|
||||
* @ingroup VisualCardDatabaseWidgets
|
||||
* @brief Icon-mode view of the card database, one card picture per item.
|
||||
*/
|
||||
|
||||
#ifndef CARD_DATABASE_VISUAL_VIEW_H
|
||||
#define CARD_DATABASE_VISUAL_VIEW_H
|
||||
|
||||
#include <QListView>
|
||||
#include <QSize>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class CardDatabaseVisualDisplayModel;
|
||||
class ExactCard;
|
||||
class QEvent;
|
||||
class QMouseEvent;
|
||||
class QPoint;
|
||||
class QWheelEvent;
|
||||
|
||||
/**
|
||||
* @brief A wrapping grid of card pictures.
|
||||
*
|
||||
* The view virtualizes painting: it only asks the delegate for the items that are
|
||||
* on screen, so the whole card database can sit in the model without a widget (or
|
||||
* pixmap) per card. Clicks and hover are reported with the exact card under the
|
||||
* cursor, which is what the deck editor needs to add, decrement or preview a card.
|
||||
*/
|
||||
class CardDatabaseVisualView : public QListView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CardDatabaseVisualView(CardDatabaseVisualDisplayModel *_model, QWidget *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Sets the card size as a percentage of the base card width.
|
||||
* @param factor Percentage in the range of the card size slider.
|
||||
*/
|
||||
void setCardSizeFactor(int factor);
|
||||
|
||||
signals:
|
||||
/** @brief A card was pressed. The event is valid for the duration of the emission. */
|
||||
void cardClicked(QMouseEvent *event, const ExactCard &card);
|
||||
/** @brief The cursor moved onto a different card. */
|
||||
void cardHovered(const ExactCard &card);
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void leaveEvent(QEvent *event) override;
|
||||
bool viewportEvent(QEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
|
||||
private:
|
||||
void updateHoveredRow(const QPoint &position);
|
||||
/// Pixels one wheel notch moves, scaled to the grid and the OS line count.
|
||||
[[nodiscard]] int wheelStep() const;
|
||||
|
||||
CardDatabaseVisualDisplayModel *model;
|
||||
/// Fraction of a line left over from a wheel event smaller than one notch.
|
||||
qreal wheelRemainder = 0.0;
|
||||
int cardSizeFactor = 100;
|
||||
int hoveredRow = -1;
|
||||
};
|
||||
|
||||
#endif // CARD_DATABASE_VISUAL_VIEW_H
|
||||
|
|
@ -5,40 +5,29 @@
|
|||
#include "../../../filters/syntax_help.h"
|
||||
#include "../../key_signals.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../cards/card_info_picture_widget.h"
|
||||
#include "../cards/card_info_picture_with_text_overlay_widget.h"
|
||||
#include "../cards/card_size_widget.h"
|
||||
#include "../deck_editor/card_database_view.h"
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
#include "../utility/custom_line_edit.h"
|
||||
#include "card_database_display_model.h"
|
||||
#include "card_database_model.h"
|
||||
#include "card_database_visual_display_model.h"
|
||||
#include "card_database_visual_view.h"
|
||||
#include "deck_list_model.h"
|
||||
#include "libcockatrice/card/card_localization.h"
|
||||
#include "libcockatrice/card/database/card_database_loader.h"
|
||||
#include "libcockatrice/card/database/card_database_querier.h"
|
||||
#include "libcockatrice/card/printing/exact_card.h"
|
||||
#include "libcockatrice/card/printing/printing_info.h"
|
||||
#include "libcockatrice/filters/filter_card.h"
|
||||
#include "libcockatrice/filters/filter_tree.h"
|
||||
#include "visual_database_display_color_filter_widget.h"
|
||||
#include "visual_database_display_filter_toolbar_widget.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QAction>
|
||||
#include <QDebug>
|
||||
#include <QIcon>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QList>
|
||||
#include <QMessageLogger>
|
||||
#include <QModelIndex>
|
||||
#include <QPair>
|
||||
#include <QPersistentModelIndex>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QScrollBar>
|
||||
#include <QSharedPointer>
|
||||
#include <QSizePolicy>
|
||||
#include <QSlider>
|
||||
#include <QStringLiteral>
|
||||
|
|
@ -48,14 +37,11 @@
|
|||
#include <QVBoxLayout>
|
||||
#include <QVariant>
|
||||
#include <QtGlobal>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/card/card_info_comparator.h>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/utility/qt_utils.h>
|
||||
#include <utility>
|
||||
|
||||
class QMouseEvent;
|
||||
|
||||
|
|
@ -64,11 +50,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
|||
DeckListModel *deckListModel)
|
||||
: QWidget(parent)
|
||||
{
|
||||
debounceTimer = new QTimer(this);
|
||||
debounceTimer->setSingleShot(true); // Ensure it only fires once after the timeout
|
||||
|
||||
connect(debounceTimer, &QTimer::timeout, this, &VisualDatabaseDisplayWidget::onSearchModelChanged);
|
||||
|
||||
// Create display model
|
||||
databaseDisplayModel = new CardDatabaseDisplayModel(this);
|
||||
databaseDisplayModel->setObjectName("databaseDisplayModel");
|
||||
|
|
@ -86,23 +67,12 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
|||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
|
||||
applyCardSearchLanguage);
|
||||
|
||||
cards = new QList<ExactCard>;
|
||||
connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this,
|
||||
&VisualDatabaseDisplayWidget::modelDirty);
|
||||
|
||||
// Set up main layout and widgets
|
||||
setMinimumSize(0, 0);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
mainLayout = QtUtils::makeLayout<QVBoxLayout>(this, 0, 1);
|
||||
setLayout(mainLayout);
|
||||
|
||||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarPolicy::ScrollBarAsNeeded);
|
||||
cardSizeWidget = new CardSizeWidget(this, flowWidget,
|
||||
SettingsCache::instance().cardsDisplay().getVisualDatabaseDisplayCardSize());
|
||||
connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(),
|
||||
&CardsDisplaySettings::setVisualDatabaseDisplayCardSize);
|
||||
cardSizeWidget->enableCtrlScrollResize(flowWidget);
|
||||
|
||||
searchContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
|
||||
|
||||
searchEdit = new SearchLineEdit();
|
||||
|
|
@ -159,6 +129,25 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
|||
connect(databaseView, &CardDatabaseView::relatedCardClicked, this,
|
||||
&VisualDatabaseDisplayWidget::onRelatedCardClicked);
|
||||
|
||||
// The visual grid is a real view on a flattened model: one row per displayed
|
||||
// card picture, painted on demand.
|
||||
visualDisplayModel = new CardDatabaseVisualDisplayModel(databaseDisplayModel, CardDatabaseManager::query(), this);
|
||||
|
||||
cardView = new CardDatabaseVisualView(visualDisplayModel, this);
|
||||
cardView->setObjectName("visualCardView");
|
||||
connect(cardView, &CardDatabaseVisualView::cardClicked, this,
|
||||
&VisualDatabaseDisplayWidget::cardClickedDatabaseDisplay);
|
||||
connect(cardView, &CardDatabaseVisualView::cardHovered, this,
|
||||
&VisualDatabaseDisplayWidget::cardHoveredDatabaseDisplay);
|
||||
|
||||
const int cardSize = SettingsCache::instance().cardsDisplay().getVisualDatabaseDisplayCardSize();
|
||||
cardSizeWidget = new CardSizeWidget(this, nullptr, cardSize);
|
||||
connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(),
|
||||
&CardsDisplaySettings::setVisualDatabaseDisplayCardSize);
|
||||
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, cardView, &CardDatabaseVisualView::setCardSizeFactor);
|
||||
cardView->setCardSizeFactor(cardSize);
|
||||
cardSizeWidget->enableCtrlScrollResize(cardView);
|
||||
|
||||
colorFilterWidget = new VisualDatabaseDisplayColorFilterWidget(this, filterModel);
|
||||
|
||||
filterContainer = new VisualDatabaseDisplayFilterToolbarWidget(this, deckListModel);
|
||||
|
|
@ -190,8 +179,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
|||
databaseLoadIndicator->setVisible(false);
|
||||
}
|
||||
|
||||
QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar();
|
||||
connect(scrollBar, &QScrollBar::valueChanged, [this](int /*value*/) { loadCurrentPage(); });
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
|
|
@ -210,7 +197,7 @@ void VisualDatabaseDisplayWidget::initialize()
|
|||
|
||||
mainLayout->addWidget(databaseView);
|
||||
|
||||
mainLayout->addWidget(flowWidget);
|
||||
mainLayout->addWidget(cardView);
|
||||
|
||||
mainLayout->addWidget(cardSizeWidget);
|
||||
|
||||
|
|
@ -241,8 +228,6 @@ void VisualDatabaseDisplayWidget::initializeFilters()
|
|||
|
||||
databaseDisplayModel->setFilterTree(filterModel->filterTree());
|
||||
|
||||
QTimer::singleShot(5000, this, [this] { loadCurrentPage(); });
|
||||
|
||||
retranslateUi();
|
||||
});
|
||||
}
|
||||
|
|
@ -278,71 +263,36 @@ void VisualDatabaseDisplayWidget::updateDisplayModeButton()
|
|||
displayModeButton->setText(displayModeButton->isChecked() ? tr("Table") : tr("Visual"));
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
loadCurrentPage();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::onDisplayModeChanged(bool checked)
|
||||
{
|
||||
updateDisplayModeButton();
|
||||
if (checked) {
|
||||
// Table mode
|
||||
flowWidget->setVisible(false);
|
||||
cardView->setVisible(false);
|
||||
cardSizeWidget->setVisible(false);
|
||||
databaseView->setItemDelegate(new QStyledItemDelegate(databaseView));
|
||||
databaseView->setVisible(true);
|
||||
} else {
|
||||
// Visual mode
|
||||
flowWidget->setVisible(true);
|
||||
cardView->setVisible(true);
|
||||
cardSizeWidget->setVisible(true);
|
||||
databaseView->setVisible(false);
|
||||
populateCards();
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card)
|
||||
{
|
||||
emit cardClickedDatabaseDisplay(event, card);
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::onHover(const ExactCard &hoveredCard)
|
||||
{
|
||||
emit cardHoveredDatabaseDisplay(hoveredCard);
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::addCardToDisplay(const ExactCard &cardToAdd)
|
||||
{
|
||||
cards->append(cardToAdd);
|
||||
auto *display = new CardInfoPictureWithTextOverlayWidget(flowWidget, false);
|
||||
display->setScaleFactor(cardSizeWidget->getSlider()->value());
|
||||
display->setCard(cardToAdd);
|
||||
flowWidget->addWidget(display);
|
||||
connect(display, &CardInfoPictureWithTextOverlayWidget::cardClicked, this, &VisualDatabaseDisplayWidget::onClick);
|
||||
connect(display, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &VisualDatabaseDisplayWidget::onHover);
|
||||
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, display, &CardInfoPictureWidget::setScaleFactor);
|
||||
}
|
||||
|
||||
bool VisualDatabaseDisplayWidget::isVisualDisplayMode() const
|
||||
{
|
||||
return !displayModeButton->isChecked();
|
||||
}
|
||||
|
||||
QStringList VisualDatabaseDisplayWidget::collectSetFilterTerms() const
|
||||
{
|
||||
return filterModel->getFilterTermsOfType(CardFilter::AttrSet);
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::onSearchModelChanged()
|
||||
{
|
||||
if (flowWidget->isVisible()) {
|
||||
// Clear the current page and prepare for new data
|
||||
flowWidget->clearLayout(); // Clear existing cards
|
||||
cards->clear(); // Clear the card list
|
||||
// Reset scrollbar position to the top after loading new cards
|
||||
QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar();
|
||||
scrollBar->setValue(0); // Reset scrollbar to top
|
||||
|
||||
currentPage = 0;
|
||||
loadCurrentPage();
|
||||
qCDebug(VisualDatabaseDisplayLog) << "Search model changed";
|
||||
}
|
||||
visualDisplayModel->setSetFilterTerms(collectSetFilterTerms());
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::onSelectedCardChanged(const QString &cardName)
|
||||
|
|
@ -368,108 +318,3 @@ void VisualDatabaseDisplayWidget::onRelatedCardClicked(const QString &relatedCar
|
|||
ExactCard exactCard = CardDatabaseManager::query()->guessCard({relatedCard});
|
||||
emit cardInfoRequested(exactCard);
|
||||
}
|
||||
|
||||
bool VisualDatabaseDisplayWidget::nearEndOfPage() const
|
||||
{
|
||||
if (!flowWidget->isVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar();
|
||||
if (scrollBar->value() + scrollBar->pageStep() * 2 < scrollBar->maximum()) {
|
||||
return false; // there is at least two pages of space to scroll remaining
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::loadCurrentPage()
|
||||
{
|
||||
// Ensure only the initial page is loaded
|
||||
if (currentPage == 0) {
|
||||
if (!initialLoadScheduled) {
|
||||
initialLoadScheduled = true;
|
||||
qCDebug(VisualDatabaseDisplayLog) << "Loading the first page";
|
||||
// Defer the first page so the tab switch stays responsive. The card
|
||||
// grid builds one event loop turn later. This also applies to
|
||||
// search-driven reloads, which reset currentPage back to 0.
|
||||
QTimer::singleShot(0, this, [this] {
|
||||
initialLoadScheduled = false;
|
||||
populateCards();
|
||||
});
|
||||
}
|
||||
} else if (nearEndOfPage()) {
|
||||
// If not the first page, just load the next page and append to the flow widget
|
||||
loadNextPage();
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::populateCards()
|
||||
{
|
||||
cards->clear();
|
||||
loadNextPage();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::loadNextPage()
|
||||
{
|
||||
// Calculate the start and end indices for the next page
|
||||
int rowCount = databaseDisplayModel->rowCount();
|
||||
int start = currentPage * cardsPerPage;
|
||||
int end = qMin(start + cardsPerPage, rowCount);
|
||||
|
||||
// Load the next page of cards and add them to the flow widget
|
||||
flowWidget->setUpdatesEnabled(false);
|
||||
loadPage(start, end);
|
||||
flowWidget->setUpdatesEnabled(true);
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::loadPage(int start, int end)
|
||||
{
|
||||
QList<const CardFilter *> setFilters = filterModel->getFiltersOfType(CardFilter::AttrSet);
|
||||
for (int row = start; row < end; ++row) {
|
||||
qCDebug(VisualDatabaseDisplayLog) << "Adding " << row;
|
||||
QModelIndex index = databaseDisplayModel->index(row, CardDatabaseModel::NameColumn);
|
||||
QVariant name = databaseDisplayModel->data(index, Qt::DisplayRole);
|
||||
qCDebug(VisualDatabaseDisplayLog) << name.toString();
|
||||
|
||||
if (CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(name.toString())) {
|
||||
if (!setFilters.empty()) {
|
||||
const auto &setMap = info->getSets();
|
||||
for (const CardFilter *setFilter : setFilters) {
|
||||
const auto it = setMap.constFind(setFilter->term());
|
||||
if (it != setMap.constEnd()) {
|
||||
for (const PrintingInfo &printing : it.value()) {
|
||||
addCardToDisplay(ExactCard(info, printing));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addCardToDisplay(CardDatabaseManager::query()->getPreferredCard(info));
|
||||
}
|
||||
} else {
|
||||
qCDebug(VisualDatabaseDisplayLog) << "Card not found in database!";
|
||||
}
|
||||
}
|
||||
currentPage++;
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::modelDirty() const
|
||||
{
|
||||
debounceTimer->start(debounceTime);
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::sortCardList(const QStringList &properties,
|
||||
Qt::SortOrder order = Qt::AscendingOrder) const
|
||||
{
|
||||
CardInfoComparator comparator(properties, order);
|
||||
std::sort(cards->begin(), cards->end(), [comparator](const ExactCard &a, const ExactCard &b) {
|
||||
return comparator(a.getCardPtr(), b.getCardPtr());
|
||||
});
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
|
||||
{
|
||||
(void)topLeft;
|
||||
(void)bottomRight;
|
||||
qCDebug(VisualDatabaseDisplayLog) << "Database Data changed";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@
|
|||
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QMouseEvent>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QWidget>
|
||||
|
|
@ -21,26 +19,21 @@
|
|||
class CardDatabaseDisplayModel;
|
||||
class CardDatabaseModel;
|
||||
class CardDatabaseView;
|
||||
class CardDatabaseVisualDisplayModel;
|
||||
class CardDatabaseVisualView;
|
||||
class CardSizeWidget;
|
||||
class DeckList;
|
||||
class DeckListModel;
|
||||
class ExactCard;
|
||||
class FilterTreeModel;
|
||||
class FlowWidget;
|
||||
class OverlapControlWidget;
|
||||
class QLabel;
|
||||
class QModelIndex;
|
||||
class QMouseEvent;
|
||||
class QPushButton;
|
||||
class QScrollArea;
|
||||
class QTimer;
|
||||
class QToolButton;
|
||||
class QVBoxLayout;
|
||||
class SearchLineEdit;
|
||||
class VisualDatabaseDisplayColorFilterWidget;
|
||||
class VisualDatabaseDisplayFilterToolbarWidget;
|
||||
template <typename T> class QList;
|
||||
|
||||
inline Q_LOGGING_CATEGORY(VisualDatabaseDisplayLog, "visual_database_display");
|
||||
|
||||
class VisualDatabaseDisplayWidget : public QWidget
|
||||
{
|
||||
|
|
@ -52,14 +45,6 @@ public:
|
|||
DeckListModel *deckListModel = nullptr);
|
||||
void retranslateUi();
|
||||
|
||||
void adjustCardsPerPage();
|
||||
void populateCards();
|
||||
void loadPage(int start, int end);
|
||||
void loadNextPage();
|
||||
void loadCurrentPage();
|
||||
void sortCardList(const QStringList &properties, Qt::SortOrder order) const;
|
||||
void setDeckList(const DeckList &new_deck_list_model);
|
||||
|
||||
/**
|
||||
* @brief Sets the callback used to create a custom zone from the add-to-zone menu.
|
||||
* The callback returns the name of the created zone, or an empty string if creation was cancelled.
|
||||
|
|
@ -87,6 +72,13 @@ public:
|
|||
bool isVisualDisplayMode() const;
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Pushes the active set filters to the visual grid.
|
||||
*
|
||||
* The grid's rows follow the display model's filter and sort signals on their
|
||||
* own; this only forwards the set filters, which decide which printings the
|
||||
* grid shows.
|
||||
*/
|
||||
void onSearchModelChanged();
|
||||
|
||||
signals:
|
||||
|
|
@ -101,11 +93,6 @@ signals:
|
|||
|
||||
protected slots:
|
||||
void initialize();
|
||||
void onClick(QMouseEvent *event, const ExactCard &card);
|
||||
void onHover(const ExactCard &hoveredCard);
|
||||
void addCardToDisplay(const ExactCard &cardToAdd);
|
||||
void databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
|
||||
void modelDirty() const;
|
||||
void onDisplayModeChanged(bool checked);
|
||||
|
||||
void onSelectedCardChanged(const QString &cardName);
|
||||
|
|
@ -125,31 +112,21 @@ private:
|
|||
QToolButton *clearFilterWidget;
|
||||
VisualDatabaseDisplayFilterToolbarWidget *filterContainer;
|
||||
CardDatabaseDisplayModel *databaseDisplayModel;
|
||||
CardDatabaseVisualDisplayModel *visualDisplayModel;
|
||||
CardDatabaseView *databaseView;
|
||||
CardDatabaseVisualView *cardView;
|
||||
std::function<QString()> newZoneCreator;
|
||||
QList<ExactCard> *cards;
|
||||
QVBoxLayout *mainLayout;
|
||||
QScrollArea *scrollArea;
|
||||
FlowWidget *flowWidget;
|
||||
QWidget *overlapCategories;
|
||||
QVBoxLayout *overlapCategoriesLayout;
|
||||
OverlapControlWidget *overlapControlWidget;
|
||||
CardSizeWidget *cardSizeWidget;
|
||||
QTimer *debounceTimer;
|
||||
|
||||
int debounceTime = 300; // in Ms
|
||||
int currentPage = 0; // Current page index
|
||||
int cardsPerPage = 100; // Number of cards per page
|
||||
bool filtersInitialized = false;
|
||||
bool initialLoadScheduled = false;
|
||||
|
||||
void initializeFilters();
|
||||
void highlightAllSearchEdit();
|
||||
void updateDisplayModeButton();
|
||||
bool nearEndOfPage() const;
|
||||
[[nodiscard]] QStringList collectSetFilterTerms() const;
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void showEvent(QShowEvent *event) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ private:
|
|||
enum Attr a;
|
||||
|
||||
public:
|
||||
CardFilter(QString &term, Type type, Attr attr) : trm(term), t(type), a(attr)
|
||||
CardFilter(const QString &term, Type type, Attr attr) : trm(term), t(type), a(attr)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
set(HEADERS
|
||||
card_database_model.h
|
||||
card_database_display_model.h
|
||||
card_database_visual_display_model.h
|
||||
card/card_completer_proxy_model.h
|
||||
card/card_search_model.h
|
||||
card_set/card_sets_model.h
|
||||
|
|
@ -13,6 +14,7 @@ add_library(
|
|||
${HEADERS}
|
||||
card_database_model.cpp
|
||||
card_database_display_model.cpp
|
||||
card_database_visual_display_model.cpp
|
||||
card/card_completer_proxy_model.cpp
|
||||
card/card_search_model.cpp
|
||||
card_set/card_sets_model.cpp
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
class QObject;
|
||||
|
||||
CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr),
|
||||
: QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr), cardModel(nullptr),
|
||||
collator(NaturalSort::makeNaturalSortCollator())
|
||||
{
|
||||
filterTree = nullptr;
|
||||
|
|
@ -37,6 +37,7 @@ CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent)
|
|||
|
||||
void CardDatabaseDisplayModel::setSourceModel(QAbstractItemModel *model)
|
||||
{
|
||||
cardModel = qobject_cast<CardDatabaseModel *>(model);
|
||||
QSortFilterProxyModel::setSourceModel(model);
|
||||
|
||||
connect(model, &QAbstractItemModel::rowsInserted, this, [this]() { dirty(); });
|
||||
|
|
@ -46,6 +47,25 @@ void CardDatabaseDisplayModel::setSourceModel(QAbstractItemModel *model)
|
|||
connect(model, &QAbstractItemModel::modelReset, this, [this]() { dirty(); });
|
||||
}
|
||||
|
||||
QVariant CardDatabaseDisplayModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (role == CardInfoRole) {
|
||||
return QVariant::fromValue(cardInfoAt(index));
|
||||
}
|
||||
|
||||
return QSortFilterProxyModel::data(index, role);
|
||||
}
|
||||
|
||||
CardInfoPtr CardDatabaseDisplayModel::cardInfoAt(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid() || cardModel == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QModelIndex sourceIndex = mapToSource(index);
|
||||
return sourceIndex.isValid() ? cardModel->getCard(sourceIndex.row()) : CardInfoPtr();
|
||||
}
|
||||
|
||||
QMap<wchar_t, wchar_t> CardDatabaseDisplayModel::characterTranslation = {{L'“', L'\"'},
|
||||
{L'”', L'\"'},
|
||||
{L'‘', L'\''},
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <libcockatrice/filters/filter_string.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class CardDatabaseModel;
|
||||
class FilterTree;
|
||||
class QObject;
|
||||
template <class Key, class T> class QMap;
|
||||
|
|
@ -36,12 +37,25 @@ public:
|
|||
ShowAll
|
||||
};
|
||||
|
||||
enum Role
|
||||
{
|
||||
/**
|
||||
* @brief The CardInfoPtr behind a proxy row.
|
||||
*
|
||||
* Saves views from looking the card up again through its name, and keeps
|
||||
* them from depending on the source model's column layout.
|
||||
*/
|
||||
CardInfoRole = Qt::UserRole + 1
|
||||
};
|
||||
|
||||
private:
|
||||
FilterBool isToken;
|
||||
QString cardName, cardText;
|
||||
QSet<QString> cardNameSet, cardTypes, cardColors;
|
||||
FilterTree *filterTree;
|
||||
FilterString *filterString;
|
||||
/// The source model, kept typed so CardInfoRole does not cast per row.
|
||||
CardDatabaseModel *cardModel;
|
||||
QTimer dirtyTimer;
|
||||
CardSearchLanguage searchLanguage;
|
||||
QString searchText;
|
||||
|
|
@ -84,6 +98,16 @@ public:
|
|||
dirtyTimer.start(20);
|
||||
}
|
||||
void clearFilterAll();
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns the card behind a proxy row, without the QVariant wrapper of CardInfoRole.
|
||||
*
|
||||
* The visual grid calls this for every row on each rebuild; CardInfoRole stays the
|
||||
* generic way in for consumers that only see the model through data().
|
||||
*/
|
||||
[[nodiscard]] CardInfoPtr cardInfoAt(const QModelIndex &index) const;
|
||||
|
||||
signals:
|
||||
void modelDirty();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
#include "card_database_visual_display_model.h"
|
||||
|
||||
#include "card_database_display_model.h"
|
||||
#include "libcockatrice/card/database/card_database_querier.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QList>
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
|
||||
/// Coalesces the bursts of proxy signals a filter, sort or database load produces.
|
||||
static constexpr int REBUILD_COALESCE_MS = 30;
|
||||
|
||||
CardDatabaseVisualDisplayModel::CardDatabaseVisualDisplayModel(CardDatabaseDisplayModel *_displayModel,
|
||||
CardDatabaseQuerier *_querier,
|
||||
QObject *parent)
|
||||
: QAbstractListModel(parent), displayModel(_displayModel), querier(_querier)
|
||||
{
|
||||
rebuildTimer.setSingleShot(true);
|
||||
rebuildTimer.setInterval(REBUILD_COALESCE_MS);
|
||||
connect(&rebuildTimer, &QTimer::timeout, this, &CardDatabaseVisualDisplayModel::rebuild);
|
||||
|
||||
if (displayModel == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
connect(displayModel, &QAbstractItemModel::modelReset, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
|
||||
connect(displayModel, &QAbstractItemModel::layoutChanged, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
|
||||
connect(displayModel, &QAbstractItemModel::rowsInserted, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
|
||||
connect(displayModel, &QAbstractItemModel::rowsRemoved, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
|
||||
connect(displayModel, &QAbstractItemModel::dataChanged, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
|
||||
}
|
||||
|
||||
void CardDatabaseVisualDisplayModel::setSetFilterTerms(const QStringList &terms)
|
||||
{
|
||||
if (setFilterTerms == terms) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFilterTerms = terms;
|
||||
refresh();
|
||||
}
|
||||
|
||||
void CardDatabaseVisualDisplayModel::refresh()
|
||||
{
|
||||
rebuildTimer.stop();
|
||||
rebuild();
|
||||
}
|
||||
|
||||
void CardDatabaseVisualDisplayModel::scheduleRebuild()
|
||||
{
|
||||
rebuildTimer.start();
|
||||
}
|
||||
|
||||
void CardDatabaseVisualDisplayModel::rebuild()
|
||||
{
|
||||
beginResetModel();
|
||||
entries.clear();
|
||||
|
||||
if (displayModel != nullptr) {
|
||||
const int proxyRows = displayModel->rowCount();
|
||||
entries.reserve(proxyRows);
|
||||
for (int row = 0; row < proxyRows; ++row) {
|
||||
const CardInfoPtr info = displayModel->cardInfoAt(displayModel->index(row, 0));
|
||||
if (info) {
|
||||
appendPrintingsFor(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void CardDatabaseVisualDisplayModel::appendPrintingsFor(const CardInfoPtr &info)
|
||||
{
|
||||
if (setFilterTerms.isEmpty()) {
|
||||
entries.append({info, PrintingInfo()});
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &printingsBySet = info->getSets();
|
||||
for (const QString &term : setFilterTerms) {
|
||||
const auto printings = printingsBySet.constFind(term);
|
||||
if (printings == printingsBySet.constEnd()) {
|
||||
continue;
|
||||
}
|
||||
for (const PrintingInfo &printing : printings.value()) {
|
||||
entries.append({info, printing});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int CardDatabaseVisualDisplayModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return parent.isValid() ? 0 : static_cast<int>(entries.size());
|
||||
}
|
||||
|
||||
QVariant CardDatabaseVisualDisplayModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= entries.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (role == Qt::DisplayRole || role == Qt::AccessibleTextRole) {
|
||||
return cardAt(index.row()).getName();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
ExactCard CardDatabaseVisualDisplayModel::cardAt(int row) const
|
||||
{
|
||||
if (row < 0 || row >= entries.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const Entry &entry = entries.at(row);
|
||||
if (entry.printing.isEmpty() && setFilterTerms.isEmpty()) {
|
||||
return querier != nullptr ? querier->getPreferredCard(entry.info) : ExactCard(entry.info);
|
||||
}
|
||||
|
||||
return ExactCard(entry.info, entry.printing);
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* @file card_database_visual_display_model.h
|
||||
* @ingroup CardDatabaseModels
|
||||
* @brief A list model of the exact cards a visual card grid paints.
|
||||
*
|
||||
* The filtered and sorted CardDatabaseDisplayModel holds one row per card name.
|
||||
* A visual grid shows one picture per *printing* instead, so this model flattens
|
||||
* the proxy rows into ExactCards: the preferred printing per card normally, and
|
||||
* every printing of the selected sets while set filters are active.
|
||||
*/
|
||||
|
||||
#ifndef COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H
|
||||
#define COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H
|
||||
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
#include "libcockatrice/card/printing/exact_card.h"
|
||||
#include "libcockatrice/card/printing/printing_info.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QList>
|
||||
#include <QModelIndex>
|
||||
#include <QPointer>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class CardDatabaseDisplayModel;
|
||||
class CardDatabaseQuerier;
|
||||
|
||||
class CardDatabaseVisualDisplayModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Flattens _displayModel into the cards a visual grid displays.
|
||||
* @param _displayModel The filtered and sorted proxy model of the card database.
|
||||
* @param _querier Resolves the preferred printing of a card.
|
||||
* @param parent Parent object.
|
||||
*/
|
||||
explicit CardDatabaseVisualDisplayModel(CardDatabaseDisplayModel *_displayModel,
|
||||
CardDatabaseQuerier *_querier,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Sets the set shorthand names of the active set filters.
|
||||
*
|
||||
* With no terms every card is shown with its preferred printing. With terms
|
||||
* every printing belonging to one of the named sets is shown instead, in term
|
||||
* order. Rebuilds immediately when the terms change.
|
||||
*/
|
||||
void setSetFilterTerms(const QStringList &terms);
|
||||
|
||||
/**
|
||||
* @brief Rebuilds the row list from the proxy model right away.
|
||||
*
|
||||
* Proxy model changes normally rebuild on a short coalescing timer; call this
|
||||
* when the caller needs the new rows before returning.
|
||||
*/
|
||||
void refresh();
|
||||
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns the card painted at @p row, with its preferred printing resolved.
|
||||
* @param row Row in this model, not in the proxy model.
|
||||
*/
|
||||
[[nodiscard]] ExactCard cardAt(int row) const;
|
||||
|
||||
private:
|
||||
struct Entry
|
||||
{
|
||||
CardInfoPtr info;
|
||||
/// Empty when the entry follows the preferred printing rather than a set filter.
|
||||
PrintingInfo printing;
|
||||
};
|
||||
|
||||
void scheduleRebuild();
|
||||
void rebuild();
|
||||
void appendPrintingsFor(const CardInfoPtr &info);
|
||||
|
||||
QPointer<CardDatabaseDisplayModel> displayModel;
|
||||
CardDatabaseQuerier *querier;
|
||||
QStringList setFilterTerms;
|
||||
QList<Entry> entries;
|
||||
QTimer rebuildTimer;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H
|
||||
|
|
@ -8,6 +8,7 @@ set(HEADERS
|
|||
game/server_cardzone.h
|
||||
game/server_counter.h
|
||||
game/game_config.h
|
||||
game/role_info.h
|
||||
game/server_deck_validation_strategy.h
|
||||
game/server_game.h
|
||||
game/server_game_lifecycle_strategy.h
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
#ifndef COCKATRICE_ROLE_INFO_H
|
||||
#define COCKATRICE_ROLE_INFO_H
|
||||
|
||||
/**
|
||||
* @brief Represents information about a participant's roles.
|
||||
*/
|
||||
struct RoleInfo
|
||||
{
|
||||
bool isJudge = false; ///< Whether the participant joined as a judge
|
||||
bool isSpectator = false; ///< Whether the participant joined as a spectator
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_ROLE_INFO_H
|
||||
|
|
@ -61,10 +61,10 @@
|
|||
Server_AbstractParticipant::Server_AbstractParticipant(Server_Game *_game,
|
||||
int _playerId,
|
||||
const ServerInfo_User &_userInfo,
|
||||
bool _judge,
|
||||
const RoleInfo &_roleInfo,
|
||||
Server_AbstractUserInterface *_userInterface)
|
||||
: ServerInfo_User_Container(_userInfo), game(_game), userInterface(_userInterface), pingTime(0),
|
||||
playerId(_playerId), judge(_judge)
|
||||
playerId(_playerId), roleInfo(_roleInfo)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -98,8 +98,8 @@ void Server_AbstractParticipant::getProperties(ServerInfo_PlayerProperties &resu
|
|||
if (withUserInfo) {
|
||||
copyUserInfo(*(result.mutable_user_info()), true);
|
||||
}
|
||||
result.set_spectator(spectator);
|
||||
result.set_judge(judge);
|
||||
result.set_spectator(isSpectator());
|
||||
result.set_judge(isJudge());
|
||||
result.set_ping_seconds(pingTime);
|
||||
getPlayerProperties(result);
|
||||
}
|
||||
|
|
@ -176,7 +176,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdUnconcede(const Command_Un
|
|||
Response::ResponseCode
|
||||
Server_AbstractParticipant::cmdJudge(const Command_Judge &cmd, ResponseContainer &rc, GameEventStorage &ges)
|
||||
{
|
||||
if (!judge) {
|
||||
if (!isJudge()) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
|
|
@ -204,7 +204,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdReadyStart(const Command_R
|
|||
Response::ResponseCode
|
||||
Server_AbstractParticipant::cmdGameSay(const Command_GameSay &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
|
||||
{
|
||||
if (spectator) {
|
||||
if (isSpectator()) {
|
||||
/* Spectators can only talk if:
|
||||
* (a) the game creator allows it
|
||||
* (b) the spectator is a moderator/administrator
|
||||
|
|
@ -365,7 +365,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdNextTurn(const Command_Nex
|
|||
return Response::RespGameNotStarted;
|
||||
}
|
||||
|
||||
if (!judge) {
|
||||
if (!isJudge()) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
|
|
@ -381,7 +381,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdSetActivePhase(const Comma
|
|||
return Response::RespGameNotStarted;
|
||||
}
|
||||
|
||||
if (!judge) {
|
||||
if (!isJudge()) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
|
|
@ -415,8 +415,8 @@ Response::ResponseCode Server_AbstractParticipant::cmdReverseTurn(const Command_
|
|||
ResponseContainer & /*rc*/,
|
||||
GameEventStorage &ges)
|
||||
{
|
||||
if (!judge) {
|
||||
if (spectator) {
|
||||
if (!isJudge()) {
|
||||
if (isSpectator()) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
|
|
@ -577,7 +577,7 @@ void Server_AbstractParticipant::setUserInterface(Server_AbstractUserInterface *
|
|||
void Server_AbstractParticipant::disconnectClient()
|
||||
{
|
||||
bool isRegistered = userInfo->user_level() & ServerInfo_User::IsRegistered;
|
||||
if (!isRegistered || spectator) {
|
||||
if (!isRegistered || isSpectator()) {
|
||||
game->removeParticipant(this, Event_Leave::USER_DISCONNECTED);
|
||||
} else {
|
||||
setUserInterface(nullptr);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#define ABSTRACT_PARTICIPANT_H
|
||||
|
||||
#include "../serverinfo_user_container.h"
|
||||
#include "role_info.h"
|
||||
#include "server_arrowtarget.h"
|
||||
|
||||
#include <QMutex>
|
||||
|
|
@ -61,8 +62,7 @@ protected:
|
|||
Server_AbstractUserInterface *userInterface;
|
||||
int pingTime;
|
||||
int playerId;
|
||||
bool spectator;
|
||||
bool judge;
|
||||
RoleInfo roleInfo;
|
||||
virtual void getPlayerProperties(ServerInfo_PlayerProperties &result);
|
||||
mutable QMutex playerMutex;
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ public:
|
|||
Server_AbstractParticipant(Server_Game *_game,
|
||||
int _playerId,
|
||||
const ServerInfo_User &_userInfo,
|
||||
bool _judge,
|
||||
const RoleInfo &_roleInfo,
|
||||
Server_AbstractUserInterface *_handler);
|
||||
~Server_AbstractParticipant() override;
|
||||
virtual void prepareDestroy()
|
||||
|
|
@ -91,11 +91,11 @@ public:
|
|||
}
|
||||
bool isSpectator() const
|
||||
{
|
||||
return spectator;
|
||||
return roleInfo.isSpectator;
|
||||
}
|
||||
bool isJudge() const
|
||||
{
|
||||
return judge;
|
||||
return roleInfo.isJudge;
|
||||
}
|
||||
Server_Game *getGame() const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -81,10 +81,9 @@ Server_AbstractPlayer::Server_AbstractPlayer(Server_Game *_game,
|
|||
const ServerInfo_User &_userInfo,
|
||||
bool _judge,
|
||||
Server_AbstractUserInterface *_userInterface)
|
||||
: Server_AbstractParticipant(_game, _playerId, _userInfo, _judge, _userInterface), conceded(false), deck(nullptr),
|
||||
sideboardLocked(true), readyStart(false), nextCardId(0)
|
||||
: Server_AbstractParticipant(_game, _playerId, _userInfo, {.isJudge = _judge}, _userInterface), conceded(false),
|
||||
deck(nullptr), sideboardLocked(true), readyStart(false), nextCardId(0)
|
||||
{
|
||||
spectator = false;
|
||||
}
|
||||
|
||||
Server_AbstractPlayer::~Server_AbstractPlayer() = default;
|
||||
|
|
@ -295,7 +294,7 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges,
|
|||
{
|
||||
// Disallow controller change to other zones than the table.
|
||||
if (((targetzone->getType() != ServerInfo_Zone::PublicZone) || !targetzone->hasCoords()) &&
|
||||
(startzone->getPlayer() != targetzone->getPlayer()) && !judge) {
|
||||
(startzone->getPlayer() != targetzone->getPlayer()) && !isJudge()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
|
|
@ -808,7 +807,7 @@ Server_AbstractPlayer::cmdMoveCard(const Command_MoveCard &cmd, ResponseContaine
|
|||
return Response::RespNameNotFound;
|
||||
}
|
||||
|
||||
if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !judge) {
|
||||
if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !isJudge()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
|
|
@ -821,7 +820,7 @@ Server_AbstractPlayer::cmdMoveCard(const Command_MoveCard &cmd, ResponseContaine
|
|||
return Response::RespNameNotFound;
|
||||
}
|
||||
|
||||
if ((startPlayer != this) && (targetPlayer != this) && !judge) {
|
||||
if ((startPlayer != this) && (targetPlayer != this) && !isJudge()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
|
|
@ -1598,7 +1597,7 @@ Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseCo
|
|||
zone->addWritePermission(cmd.player_id());
|
||||
}
|
||||
|
||||
if (judge) {
|
||||
if (isJudge()) {
|
||||
ges.setOverwriteOwnership(true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
#include "libcockatrice/protocol/pb/serverinfo_replay_match.pb.h"
|
||||
#include "libcockatrice/protocol/pb/serverinfo_user.pb.h"
|
||||
#include "libcockatrice/protocol/pb/session_event.pb.h"
|
||||
#include "role_info.h"
|
||||
#include "server_abstract_player.h"
|
||||
#include "server_abstractuserinterface.h"
|
||||
#include "server_arrow.h"
|
||||
|
|
@ -479,9 +480,8 @@ void Server_Game::stopGameIfFinished()
|
|||
|
||||
Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user,
|
||||
const QString &_password,
|
||||
bool spectator,
|
||||
bool overrideRestrictions,
|
||||
bool asJudge)
|
||||
const RoleInfo &roleInfo,
|
||||
bool overrideRestrictions)
|
||||
{
|
||||
Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface();
|
||||
for (auto *participant : participants.values()) {
|
||||
|
|
@ -490,11 +490,11 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user,
|
|||
}
|
||||
}
|
||||
|
||||
if (asJudge && !(user->user_level() & ServerInfo_User::IsJudge)) {
|
||||
if (roleInfo.isJudge && !(user->user_level() & ServerInfo_User::IsJudge)) {
|
||||
return Response::RespUserLevelTooLow;
|
||||
}
|
||||
if (!(overrideRestrictions && (user->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsJudge)))) {
|
||||
if ((_password != password) && !(spectator && !spectatorsNeedPassword)) {
|
||||
if ((_password != password) && !(roleInfo.isSpectator && !spectatorsNeedPassword)) {
|
||||
return Response::RespWrongPassword;
|
||||
}
|
||||
if (!(user->user_level() & ServerInfo_User::IsRegistered) && onlyRegistered) {
|
||||
|
|
@ -510,13 +510,13 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user,
|
|||
QString::fromStdString(user->name()))) {
|
||||
return Response::RespInIgnoreList;
|
||||
}
|
||||
if (spectator) {
|
||||
if (roleInfo.isSpectator) {
|
||||
if (!spectatorsAllowed) {
|
||||
return Response::RespSpectatorsNotAllowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!spectator && (gameStarted || (getPlayerCount() >= getMaxPlayers()))) {
|
||||
if (!roleInfo.isSpectator && (gameStarted || (getPlayerCount() >= getMaxPlayers()))) {
|
||||
return Response::RespGameFull;
|
||||
}
|
||||
|
||||
|
|
@ -537,19 +537,18 @@ bool Server_Game::containsUser(const QString &userName) const
|
|||
|
||||
void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
||||
ResponseContainer &rc,
|
||||
bool spectator,
|
||||
bool judge,
|
||||
const RoleInfo &roleInfo,
|
||||
bool broadcastUpdate)
|
||||
{
|
||||
QMutexLocker locker(&gameMutex);
|
||||
|
||||
Server_AbstractParticipant *newParticipant;
|
||||
if (spectator) {
|
||||
if (roleInfo.isSpectator) {
|
||||
newParticipant = new Server_Spectator(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true),
|
||||
judge, userInterface);
|
||||
roleInfo.isJudge, userInterface);
|
||||
} else {
|
||||
newParticipant = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), judge,
|
||||
userInterface);
|
||||
newParticipant = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true),
|
||||
roleInfo.isJudge, userInterface);
|
||||
}
|
||||
|
||||
newParticipant->moveToThread(thread());
|
||||
|
|
@ -560,7 +559,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
|||
|
||||
const QString playerName = QString::fromStdString(newParticipant->getUserInfo()->name());
|
||||
participants.insert(newParticipant->getPlayerId(), newParticipant);
|
||||
if (spectator) {
|
||||
if (roleInfo.isSpectator) {
|
||||
allSpectatorsEver.insert(playerName);
|
||||
} else {
|
||||
allPlayersEver.insert(playerName);
|
||||
|
|
@ -579,7 +578,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
|||
emit gameInfoChanged(gameInfo);
|
||||
}
|
||||
|
||||
if ((newParticipant->getUserInfo()->user_level() & ServerInfo_User::IsRegistered) && !spectator) {
|
||||
if ((newParticipant->getUserInfo()->user_level() & ServerInfo_User::IsRegistered) && !roleInfo.isSpectator) {
|
||||
room->getServer()->addPersistentPlayer(playerName, room->getId(), gameId, newParticipant->getPlayerId());
|
||||
}
|
||||
|
||||
|
|
@ -871,6 +870,13 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti
|
|||
}
|
||||
}
|
||||
|
||||
bool Server_Game::canSeePrivateEventsFor(const Server_AbstractParticipant *participant, int privatePlayerId) const
|
||||
{
|
||||
return participant->getPlayerId() == privatePlayerId // same player
|
||||
|| participant->isJudge() // judges see everything
|
||||
|| (participant->isSpectator() && spectatorsSeeEverything); // spectators see everything
|
||||
}
|
||||
|
||||
void Server_Game::sendGameEventContainer(GameEventContainer *cont,
|
||||
GameEventStorageItem::EventRecipients recipients,
|
||||
int privatePlayerId)
|
||||
|
|
@ -879,8 +885,7 @@ void Server_Game::sendGameEventContainer(GameEventContainer *cont,
|
|||
|
||||
cont->set_game_id(gameId);
|
||||
for (auto *participant : participants.values()) {
|
||||
const bool playerPrivate = (participant->getPlayerId() == privatePlayerId) || participant->isJudge() ||
|
||||
(participant->isSpectator() && spectatorsSeeEverything);
|
||||
const bool playerPrivate = canSeePrivateEventsFor(participant, privatePlayerId);
|
||||
if ((recipients.testFlag(GameEventStorageItem::SendToPrivate) && playerPrivate) ||
|
||||
(recipients.testFlag(GameEventStorageItem::SendToOthers) && !playerPrivate)) {
|
||||
participant->sendGameEvent(*cont);
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class Message;
|
|||
} // namespace protobuf
|
||||
} // namespace google
|
||||
struct GameConfig;
|
||||
struct RoleInfo;
|
||||
|
||||
class Server_Game : public QObject
|
||||
{
|
||||
|
|
@ -109,6 +110,7 @@ private:
|
|||
bool omniscient,
|
||||
bool withUserInfo);
|
||||
void storeGameInformation();
|
||||
bool canSeePrivateEventsFor(const Server_AbstractParticipant *participant, int privatePlayerId) const;
|
||||
signals:
|
||||
void sigStartGameIfReady(bool override);
|
||||
void gameInfoChanged(ServerInfo_Game gameInfo);
|
||||
|
|
@ -188,12 +190,11 @@ public:
|
|||
return shareDecklistsOnLoad;
|
||||
}
|
||||
Response::ResponseCode
|
||||
checkJoin(ServerInfo_User *user, const QString &_password, bool spectator, bool overrideRestrictions, bool asJudge);
|
||||
checkJoin(ServerInfo_User *user, const QString &_password, const RoleInfo &roleInfo, bool overrideRestrictions);
|
||||
bool containsUser(const QString &userName) const;
|
||||
void addPlayer(Server_AbstractUserInterface *userInterface,
|
||||
ResponseContainer &rc,
|
||||
bool spectator,
|
||||
bool judge,
|
||||
const RoleInfo &roleInfo,
|
||||
bool broadcastUpdate = true);
|
||||
void removeParticipant(Server_AbstractParticipant *participant, Event_Leave::LeaveReason reason);
|
||||
void removeArrowsRelatedToPlayer(GameEventStorage &ges, Server_AbstractPlayer *player);
|
||||
|
|
|
|||
|
|
@ -573,7 +573,7 @@ Server_Player::cmdNextTurn(const Command_NextTurn & /*cmd*/, ResponseContainer &
|
|||
return Response::RespGameNotStarted;
|
||||
}
|
||||
|
||||
if (conceded && !judge) {
|
||||
if (conceded && !isJudge()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
|
|
@ -589,7 +589,7 @@ Response::ResponseCode Server_Player::cmdSetActivePhase(const Command_SetActiveP
|
|||
return Response::RespGameNotStarted;
|
||||
}
|
||||
|
||||
if (!judge) {
|
||||
if (!isJudge()) {
|
||||
if (conceded) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
|
@ -623,7 +623,7 @@ Response::ResponseCode
|
|||
Server_Player::cmdReverseTurn(const Command_ReverseTurn &cmd, ResponseContainer &rc, GameEventStorage &ges)
|
||||
{
|
||||
|
||||
if (!judge && conceded) {
|
||||
if (!isJudge() && conceded) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
return Server_AbstractParticipant::cmdReverseTurn(cmd, rc, ges);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ Server_Spectator::Server_Spectator(Server_Game *_game,
|
|||
const ServerInfo_User &_userInfo,
|
||||
bool _judge,
|
||||
Server_AbstractUserInterface *_userInterface)
|
||||
: Server_AbstractParticipant(_game, _playerId, _userInfo, _judge, _userInterface)
|
||||
: Server_AbstractParticipant(_game, _playerId, _userInfo, {.isJudge = _judge, .isSpectator = true}, _userInterface)
|
||||
{
|
||||
spectator = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1141,7 +1141,8 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room
|
|||
|
||||
auto *game = new Server_Game(config, room);
|
||||
|
||||
game->addPlayer(this, rc, asSpectator, asJudge, false);
|
||||
RoleInfo roleInfo{.isJudge = asJudge, .isSpectator = asSpectator};
|
||||
game->addPlayer(this, rc, roleInfo, false);
|
||||
room->addGame(game);
|
||||
|
||||
return Response::RespOk;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "server_room.h"
|
||||
|
||||
#include "game/role_info.h"
|
||||
#include "game/server_game.h"
|
||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||
#include "libcockatrice/protocol/pb/room_event.pb.h"
|
||||
|
|
@ -372,11 +373,11 @@ Response::ResponseCode Server_Room::processJoinGameCommand(const Command_JoinGam
|
|||
|
||||
QMutexLocker gameLocker(&game->gameMutex);
|
||||
|
||||
Response::ResponseCode result =
|
||||
game->checkJoin(userInterface->getUserInfo(), QString::fromStdString(cmd.password()), cmd.spectator(),
|
||||
cmd.override_restrictions(), cmd.join_as_judge());
|
||||
RoleInfo roleInfo{.isJudge = cmd.join_as_judge(), .isSpectator = cmd.spectator()};
|
||||
Response::ResponseCode result = game->checkJoin(
|
||||
userInterface->getUserInfo(), QString::fromStdString(cmd.password()), roleInfo, cmd.override_restrictions());
|
||||
if (result == Response::RespOk) {
|
||||
game->addPlayer(userInterface, rc, cmd.spectator(), cmd.join_as_judge());
|
||||
game->addPlayer(userInterface, rc, roleInfo);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,21 @@ if(WITH_ORACLE OR WITH_CLIENT)
|
|||
LIBS
|
||||
libcockatrice_filters
|
||||
)
|
||||
|
||||
# ------------------------
|
||||
# Visual Display Model Test
|
||||
# ------------------------
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
visual_display_model_test
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
visual_display_model_test.cpp
|
||||
mocks.cpp
|
||||
LIBS
|
||||
libcockatrice_card
|
||||
libcockatrice_models
|
||||
)
|
||||
endif()
|
||||
|
||||
# ------------------------
|
||||
|
|
|
|||
188
tests/carddatabase/visual_display_model_test.cpp
Normal file
188
tests/carddatabase/visual_display_model_test.cpp
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
/**
|
||||
* @file visual_display_model_test.cpp
|
||||
* @brief Tests for the flattened model behind the visual card database grid.
|
||||
*/
|
||||
|
||||
#include "card_database_display_model.h"
|
||||
#include "card_database_model.h"
|
||||
#include "card_database_visual_display_model.h"
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
#include "libcockatrice/card/database/card_database.h"
|
||||
#include "libcockatrice/card/database/card_database_querier.h"
|
||||
#include "libcockatrice/card/printing/exact_card.h"
|
||||
#include "libcockatrice/card/printing/printing_info.h"
|
||||
#include "libcockatrice/card/set/card_set.h"
|
||||
#include "libcockatrice/interfaces/noop_card_preference_provider.h"
|
||||
#include "libcockatrice/interfaces/noop_card_set_priority_controller.h"
|
||||
#include "test_card_database_path_provider.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QDate>
|
||||
#include <QEventLoop>
|
||||
#include <QHash>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <memory>
|
||||
|
||||
namespace
|
||||
{
|
||||
// GTest's main() creates no QCoreApplication, which QTimer needs for its event loop.
|
||||
const QCoreApplication &application()
|
||||
{
|
||||
static int argc = 1;
|
||||
static char executable[] = "visual_display_model_test";
|
||||
static char *argv[] = {executable, nullptr};
|
||||
static const QCoreApplication app(argc, argv);
|
||||
return app;
|
||||
}
|
||||
|
||||
/// Lets the rebuild timer fire, as it would in the running client.
|
||||
void waitForRebuild()
|
||||
{
|
||||
QEventLoop loop;
|
||||
QTimer::singleShot(100, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
class CardDatabaseVisualDisplayModelTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
(void)application();
|
||||
|
||||
db = std::make_unique<CardDatabase>(nullptr, new NoopCardPreferenceProvider(),
|
||||
new TestCardDatabasePathProvider(), new NoopCardSetPriorityController());
|
||||
db->loadCardDatabases();
|
||||
|
||||
cardModel = std::make_unique<CardDatabaseModel>(db.get(), false);
|
||||
displayModel = std::make_unique<CardDatabaseDisplayModel>();
|
||||
displayModel->setSourceModel(cardModel.get());
|
||||
visualModel = std::make_unique<CardDatabaseVisualDisplayModel>(displayModel.get(), db->query());
|
||||
visualModel->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Builds a card with @p printingCount printings in each named set.
|
||||
* @param printingCountPerSet Set short name to the number of printings in that set.
|
||||
*/
|
||||
[[nodiscard]] CardInfoPtr makeCard(const QString &name, const QMap<QString, int> &printingCountPerSet)
|
||||
{
|
||||
SetToPrintingsMap setsInfo;
|
||||
for (auto it = printingCountPerSet.constBegin(); it != printingCountPerSet.constEnd(); ++it) {
|
||||
CardSetPtr set = CardSet::newInstance(&setController, it.key(), it.key() + " test set", "expansion",
|
||||
QDate(), CardSet::PriorityPrimary);
|
||||
for (int printing = 0; printing < it.value(); ++printing) {
|
||||
QHash<QString, QString> properties;
|
||||
properties["uuid"] = QString("%1-%2-%3").arg(it.key(), name).arg(printing);
|
||||
setsInfo[it.key()].append(PrintingInfo(set, LazyPropertiesHash(properties)));
|
||||
}
|
||||
}
|
||||
|
||||
CardInfo::UiAttributes attributes = {.tableRow = 1};
|
||||
return CardInfo::newInstance(name, "", false, {}, {}, {}, setsInfo, attributes);
|
||||
}
|
||||
|
||||
NoopCardSetPriorityController setController;
|
||||
std::unique_ptr<CardDatabase> db;
|
||||
std::unique_ptr<CardDatabaseModel> cardModel;
|
||||
std::unique_ptr<CardDatabaseDisplayModel> displayModel;
|
||||
std::unique_ptr<CardDatabaseVisualDisplayModel> visualModel;
|
||||
};
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, ExposesTheCardInfoBehindAProxyRow)
|
||||
{
|
||||
ASSERT_GT(displayModel->rowCount(), 0);
|
||||
|
||||
const QVariant info = displayModel->data(displayModel->index(0, CardDatabaseModel::NameColumn),
|
||||
CardDatabaseDisplayModel::CardInfoRole);
|
||||
ASSERT_TRUE(info.isValid());
|
||||
EXPECT_FALSE(info.value<CardInfoPtr>().isNull());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, FlattensOneRowPerProxyCardWithoutSetFilters)
|
||||
{
|
||||
ASSERT_GT(displayModel->rowCount(), 0);
|
||||
EXPECT_EQ(displayModel->rowCount(), visualModel->rowCount());
|
||||
|
||||
for (int row = 0; row < visualModel->rowCount(); ++row) {
|
||||
const ExactCard card = visualModel->cardAt(row);
|
||||
ASSERT_TRUE(card) << "row " << row;
|
||||
EXPECT_EQ(
|
||||
displayModel->data(displayModel->index(row, CardDatabaseModel::NameColumn), Qt::DisplayRole).toString(),
|
||||
card.getName())
|
||||
<< "row " << row;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, ShowsOneRowPerCardWhenNoSetIsFiltered)
|
||||
{
|
||||
const int rowsBefore = visualModel->rowCount();
|
||||
|
||||
db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}}));
|
||||
visualModel->refresh();
|
||||
|
||||
EXPECT_EQ(rowsBefore + 1, visualModel->rowCount());
|
||||
EXPECT_EQ("Multi", visualModel->cardAt(visualModel->rowCount() - 1).getName());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, ShowsEveryPrintingOfAFilteredSet)
|
||||
{
|
||||
db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}}));
|
||||
visualModel->refresh();
|
||||
|
||||
visualModel->setSetFilterTerms({"TST"});
|
||||
|
||||
ASSERT_EQ(2, visualModel->rowCount());
|
||||
for (int row = 0; row < visualModel->rowCount(); ++row) {
|
||||
const ExactCard card = visualModel->cardAt(row);
|
||||
ASSERT_TRUE(card) << "row " << row;
|
||||
EXPECT_EQ("Multi", card.getName());
|
||||
ASSERT_FALSE(card.getPrinting().isEmpty()) << "row " << row;
|
||||
EXPECT_EQ("TST", card.getPrinting().getSet()->getShortName()) << "row " << row;
|
||||
}
|
||||
EXPECT_NE(visualModel->cardAt(0).getPrinting(), visualModel->cardAt(1).getPrinting());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, HidesCardsWithoutAPrintingInTheFilteredSets)
|
||||
{
|
||||
visualModel->setSetFilterTerms({"NO SUCH SET"});
|
||||
|
||||
EXPECT_EQ(0, visualModel->rowCount());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, RestoresOneRowPerCardWhenTheSetTermsClear)
|
||||
{
|
||||
db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}}));
|
||||
visualModel->refresh();
|
||||
const int rowsBefore = visualModel->rowCount();
|
||||
|
||||
visualModel->setSetFilterTerms({"TST"});
|
||||
ASSERT_EQ(2, visualModel->rowCount());
|
||||
|
||||
visualModel->setSetFilterTerms({});
|
||||
EXPECT_EQ(rowsBefore, visualModel->rowCount());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, RebuildsByItselfWhenTheProxyGainsARow)
|
||||
{
|
||||
const int rowsBefore = visualModel->rowCount();
|
||||
|
||||
db->addCard(makeCard("Late", {{"TST", 1}}));
|
||||
waitForRebuild();
|
||||
|
||||
EXPECT_EQ(rowsBefore + 1, visualModel->rowCount());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, ReportsTheCardNameForDisplayAndAccessibleText)
|
||||
{
|
||||
ASSERT_GT(visualModel->rowCount(), 0);
|
||||
const QString expected = visualModel->cardAt(0).getName();
|
||||
|
||||
EXPECT_EQ(expected, visualModel->data(visualModel->index(0), Qt::DisplayRole).toString());
|
||||
EXPECT_EQ(expected, visualModel->data(visualModel->index(0), Qt::AccessibleTextRole).toString());
|
||||
EXPECT_FALSE(visualModel->data(QModelIndex(), Qt::DisplayRole).isValid());
|
||||
}
|
||||
} // namespace
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
#include "game/game_config.h"
|
||||
#include "game/role_info.h"
|
||||
#include "game/server_game.h"
|
||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||
#include "server.h"
|
||||
|
|
@ -119,13 +120,13 @@ TEST_F(GameJoinOverrideTest, StaffBypassPasswordRestriction)
|
|||
game = makeGame(true, false, false, true);
|
||||
|
||||
// A plain user cannot override the password even with the override flag set.
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, "wrong", false, true, false), Response::RespWrongPassword);
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, "wrong", {}, true), Response::RespWrongPassword);
|
||||
// Moderators and judges may enter any game regardless of the password.
|
||||
EXPECT_EQ(game->checkJoin(&moderator, "wrong", false, true, false), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", false, true, false), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&moderator, "wrong", {}, true), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", {}, true), Response::RespOk);
|
||||
// Without the override flag judges are still subject to the password.
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", false, false, true), Response::RespWrongPassword);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "secret", false, false, true), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", {.isJudge = true}, false), Response::RespWrongPassword);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "secret", {.isJudge = true}, false), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, StaffBypassRegisteredOnlyRestriction)
|
||||
|
|
@ -133,9 +134,9 @@ TEST_F(GameJoinOverrideTest, StaffBypassRegisteredOnlyRestriction)
|
|||
game = makeGame(false, true, false, true);
|
||||
|
||||
// Without the override flag the only-registered restriction still applies.
|
||||
EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, false, false), Response::RespUserLevelTooLow);
|
||||
EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), {}, false), Response::RespUserLevelTooLow);
|
||||
// An unregistered judge may enter when overriding restrictions.
|
||||
EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, true, false), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), {}, true), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, StaffBypassBuddiesOnlyRestriction)
|
||||
|
|
@ -143,10 +144,10 @@ TEST_F(GameJoinOverrideTest, StaffBypassBuddiesOnlyRestriction)
|
|||
game = makeGame(false, false, true, true);
|
||||
|
||||
// A plain user who is not on the creator's buddy list gets rejected.
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), false, true, false), Response::RespOnlyBuddies);
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), {}, true), Response::RespOnlyBuddies);
|
||||
// Moderators and judges bypass the buddies-only restriction.
|
||||
EXPECT_EQ(game->checkJoin(&moderator, QString(), false, true, false), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, QString(), false, true, false), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&moderator, QString(), {}, true), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, QString(), {}, true), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, StaffBypassSpectatorsNotAllowedRestriction)
|
||||
|
|
@ -154,11 +155,11 @@ TEST_F(GameJoinOverrideTest, StaffBypassSpectatorsNotAllowedRestriction)
|
|||
game = makeGame(false, false, false, false);
|
||||
|
||||
// A plain user cannot spectate when the game disallows spectators.
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), true, false, false), Response::RespSpectatorsNotAllowed);
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), {.isSpectator = true}, false), Response::RespSpectatorsNotAllowed);
|
||||
// Moderators and judges may spectate any game regardless of the password
|
||||
// and the spectator restriction.
|
||||
EXPECT_EQ(game->checkJoin(&moderator, "wrong", true, true, false), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", true, true, false), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&moderator, "wrong", {.isSpectator = true}, true), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", {.isSpectator = true}, true), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, JudgeOverrideDoesNotGrantJudgeJoinToPlainUser)
|
||||
|
|
@ -166,8 +167,8 @@ TEST_F(GameJoinOverrideTest, JudgeOverrideDoesNotGrantJudgeJoinToPlainUser)
|
|||
game = makeGame(false, false, false, true);
|
||||
|
||||
// joining with join_as_judge still requires the judge flag even when overriding.
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), false, true, true), Response::RespUserLevelTooLow);
|
||||
EXPECT_EQ(game->checkJoin(&judge, QString(), false, true, true), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), {.isJudge = true}, true), Response::RespUserLevelTooLow);
|
||||
EXPECT_EQ(game->checkJoin(&judge, QString(), {.isJudge = true}, true), Response::RespOk);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
|
|||
2
vcpkg
2
vcpkg
|
|
@ -1 +1 @@
|
|||
Subproject commit 9eec002df1cd109f8f104cd966a763daf9751489
|
||||
Subproject commit 9e593bb18ea69cc5095e012465dcd675a822ed0d
|
||||
Loading…
Add table
Add a link
Reference in a new issue