Compare commits

..

No commits in common. "24d8d8be3b7fdf9f3a89ff3124c42ddfd3b67d0b" and "c3599be89b3ed7229f783e80be3ea7b1449f127c" have entirely different histories.

24 changed files with 160 additions and 905 deletions

View file

@ -72,7 +72,7 @@ void DeckStatsInterface::copyDeckWithoutTokens(const DeckList &source, DeckList
{
auto copyIfNotAToken = [&destination](const auto node, const auto card) {
CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName());
if (dbCard && !dbCard->getIsToken() && node->getName() != DECK_ZONE_MAYBEBOARD) {
if (dbCard && !dbCard->getIsToken()) {
DecklistCardNode *addedCard = destination.addCard(card->getName(), node->getName(), -1);
addedCard->setNumber(card->getNumber());
}

View file

@ -99,7 +99,7 @@ void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckLi
{
auto copyMainOrSide = [&mainboard, &sideboard](const auto node, const auto card) {
CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName());
if (!dbCard || dbCard->getIsToken() || node->getName() == DECK_ZONE_MAYBEBOARD) {
if (!dbCard || dbCard->getIsToken()) {
return;
}

View file

@ -3,7 +3,6 @@
#include <QApplication>
#include <QDomDocument>
#include <QFile>
#include <QImageReader>
#include <QPainter>
#include <QPalette>
#include <QSvgRenderer>
@ -419,57 +418,6 @@ QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded)
QMap<QString, QPixmap> DropdownIconPixmapGenerator::pmCache;
namespace
{
/// Longest side mana symbols are rendered at before being scaled to their final size.
constexpr int MASTER_ICON_SIZE = 128;
QString manaSymbolCacheKey(const QString &symbol, const QSize &size)
{
return symbol + QLatin1Char('|') + QString::number(size.width()) + QLatin1Char('x') +
QString::number(size.height());
}
} // namespace
const QPixmap &ManaSymbolPixmapGenerator::masterIcon(const QString &symbol)
{
auto it = masterCache.constFind(symbol);
if (it != masterCache.constEnd()) {
return it.value();
}
QImageReader reader("theme:icons/mana/" + symbol);
QSize sourceSize = reader.size();
if (!sourceSize.isEmpty()) {
sourceSize.scale(QSize(MASTER_ICON_SIZE, MASTER_ICON_SIZE), Qt::KeepAspectRatio);
reader.setScaledSize(sourceSize);
}
const QPixmap rendered = QPixmap::fromImageReader(&reader);
return masterCache.insert(symbol, rendered).value();
}
QPixmap ManaSymbolPixmapGenerator::generatePixmap(const QString &symbol, const QSize &size)
{
const QString key = manaSymbolCacheKey(symbol, size);
auto it = scaledCache.constFind(key);
if (it != scaledCache.constEnd()) {
return it.value();
}
const QPixmap &icon = masterIcon(symbol);
if (icon.isNull()) {
return {};
}
QPixmap scaled = icon.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
scaledCache.insert(key, scaled);
return scaled;
}
QHash<QString, QPixmap> ManaSymbolPixmapGenerator::masterCache;
QHash<QString, QPixmap> ManaSymbolPixmapGenerator::scaledCache;
QPixmap loadColorAdjustedPixmap(const QString &name)
{
if (qApp->palette().windowText().color().lightness() > 200) {

View file

@ -7,7 +7,6 @@
#ifndef PIXMAPGENERATOR_H
#define PIXMAPGENERATOR_H
#include <QHash>
#include <QIcon>
#include <QLoggingCategory>
#include <QMap>
@ -126,34 +125,6 @@ public:
}
};
class ManaSymbolPixmapGenerator
{
private:
static QHash<QString, QPixmap> masterCache;
static QHash<QString, QPixmap> scaledCache;
/**
* @brief Renders \a symbol once at a fixed moderate size, so repeated scalings never
* re-rasterize the source file (SVG sources can be very expensive to rasterize).
*/
static const QPixmap &masterIcon(const QString &symbol);
public:
/**
* @brief Returns a smooth-scaled rendering of the given mana symbol icon.
*
* Results are shared between all callers via a process-wide cache keyed by symbol
* and size, so scaling work is done once per distinct combination instead of once
* per widget creation or resize.
*/
static QPixmap generatePixmap(const QString &symbol, const QSize &size);
static void clear()
{
masterCache.clear();
scaledCache.clear();
}
};
QPixmap loadColorAdjustedPixmap(const QString &name);
#endif

View file

@ -41,11 +41,6 @@ void ColorIdentityWidget::populateManaSymbolWidgets()
// clear old layout
QtUtils::clearLayoutRec(layout);
// The freshly created symbols haven't been sized yet, so force the next resize pass
// to apply the symbol size again.
lastIconSize = -1;
lastWidth = -1;
// populate mana symbols
if (SettingsCache::instance().visualDeckStorage().getVisualDeckStorageDrawUnusedColorIdentities()) {
for (const QString symbol : fullColorIdentity) {
@ -78,35 +73,21 @@ void ColorIdentityWidget::toggleUnusedVisibility()
void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
// Layout passes resize this widget repeatedly with identical sizes, so bail out before
// touching the children when neither the width nor the resulting symbol size changed.
const int totalWidth = event->size().width();
if (totalWidth == lastWidth && lastIconSize != -1) {
return;
}
lastWidth = totalWidth;
const int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
setFixedHeight(totalHeight);
QList<ManaSymbolWidget *> manaSymbols = findChildren<ManaSymbolWidget *>();
if (manaSymbols.isEmpty()) {
return;
}
const int spacing = layout->spacing();
const int count = manaSymbols.size();
const int availableWidth = totalWidth - (spacing * (count - 1));
const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
if (!manaSymbols.isEmpty()) {
int totalWidth = event->size().width();
int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
setFixedHeight(totalHeight);
if (iconSize == lastIconSize) {
return;
}
lastIconSize = iconSize;
int spacing = layout->spacing();
int count = manaSymbols.size();
int availableWidth = totalWidth - (spacing * (count - 1));
int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
for (ManaSymbolWidget *manaSymbol : manaSymbols) {
manaSymbol->setFixedSize(iconSize, iconSize);
for (ManaSymbolWidget *manaSymbol : manaSymbols) {
manaSymbol->setFixedSize(iconSize, iconSize);
}
}
}

View file

@ -30,8 +30,6 @@ public slots:
private:
QString colorIdentity;
QHBoxLayout *layout;
int lastIconSize = -1; ///< The symbol size last applied, to skip redundant resize passes.
int lastWidth = -1; ///< The width last processed, to skip redundant resize passes.
};
#endif // COLOR_IDENTITY_WIDGET_H

View file

@ -1,15 +1,15 @@
#include "mana_symbol_widget.h"
#include "../../../../client/settings/cache_settings.h"
#include "../../../pixel_map_generator.h"
#include <QResizeEvent>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
ManaSymbolWidget::ManaSymbolWidget(QWidget *parent, QString _symbol, bool _isActive, bool _mayBeToggled)
: QLabel(parent), symbol(std::move(_symbol)), isActive(_isActive), mayBeToggled(_mayBeToggled)
: QLabel(parent), symbol(_symbol), isActive(_isActive), mayBeToggled(_mayBeToggled)
{
setPixmap(ManaSymbolPixmapGenerator::generatePixmap(symbol, QSize(50, 50)));
loadManaIcon();
setPixmap(manaIcon.scaled(50, 50, Qt::KeepAspectRatio, Qt::SmoothTransformation));
setMaximumWidth(50);
// Initialize opacity effect
@ -64,13 +64,16 @@ void ManaSymbolWidget::mousePressEvent(QMouseEvent *event)
void ManaSymbolWidget::resizeEvent(QResizeEvent *event)
{
QLabel::resizeEvent(event);
const QSize newSize = event->size();
setPixmap(manaIcon.scaled(event->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
// Skip the rescale when the size didn't actually change: layout passes resize these
// widgets repeatedly with identical sizes.
if (newSize.isEmpty() || pixmap().size() == newSize) {
return;
void ManaSymbolWidget::loadManaIcon()
{
QString filename = "theme:icons/mana/";
if (symbol == "W" || symbol == "U" || symbol == "B" || symbol == "R" || symbol == "G") {
filename += symbol;
}
setPixmap(ManaSymbolPixmapGenerator::generatePixmap(symbol, newSize));
manaIcon = QPixmap(filename);
}

View file

@ -33,6 +33,8 @@ public:
return symbol[0];
}
void loadManaIcon();
public slots:
void resizeEvent(QResizeEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
@ -42,6 +44,7 @@ signals:
private:
QString symbol;
QPixmap manaIcon;
bool isActive;
bool mayBeToggled;
QGraphicsOpacityEffect *opacityEffect;

View file

@ -3,63 +3,25 @@
#include "../cards/art_crop_attribution.h"
#include "playmat_utils.h"
#include <QKeyEvent>
#include <QLinearGradient>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QWheelEvent>
#include <cmath>
namespace
{
// Mirrors the dialog/proto clamps so gestures can never produce an
// out of range parameter. The zoom FLOOR is dynamic: see
// playmatClampedZoom(), zooming out stops where the sampling window would
// exceed the card itself, so there is no dead range at the bottom end.
constexpr qreal MAX_MARGIN = 0.95;
// Range of in game stack+table aspect ratios worth designing for, derived
// from PlayerGraphicsItem::paint()'s combinedArea = stack ∪ table:
// height = 10 + 30 + 3*102 + 2*30 = 406 (TableZone rows)
// width = 1.5*72 + (20 + 5*72 + 15) = 503 (StackZone + TableZone
// at MIN_WIDTH)
// The area's shape depends on GAME CONTENT (played card columns widen the
// table by ~107 px each), not on the window size. Fresh board ≈ 503/406,
// a table grown to roughly double its minimum width ≈ 2.2.
constexpr qreal MIN_TABLE_ASPECT = 503.0 / 406.0; // fresh board: most generous framing
constexpr qreal MAX_TABLE_ASPECT = 2.2; // well developed, wide table
// Keyboard nudge steps (viewport convention: Down looks further down).
constexpr qreal KEY_PAN_MARGIN_STEP = 0.005;
constexpr qreal KEY_PAN_OFFSET_STEP = 0.01;
constexpr qreal KEY_ZOOM_STEP = 1.05;
constexpr qreal WHEEL_ZOOM_BASE = 1.15; // zoom factor per wheel notch
} // namespace
PlaymatPreviewWidget::PlaymatPreviewWidget(QWidget *parent) : QWidget(parent)
{
// The crop is square and drawn contain fit, so the height decides its
// on screen size, keep it generous but let the dialog compress on small
// or high DPI screens
setMinimumSize(400, 180);
QSizePolicy sp(QSizePolicy::Expanding, QSizePolicy::Expanding);
setSizePolicy(sp);
setFocusPolicy(Qt::StrongFocus);
setCursor(Qt::OpenHandCursor);
setAccessibleName(tr("Playmat crop"));
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
setMinimumSize(400, 120);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
}
void PlaymatPreviewWidget::setPixmap(const QPixmap &pixmap)
{
sourcePixmap = pixmap;
setCursor(sourcePixmap.isNull() ? Qt::ArrowCursor : Qt::OpenHandCursor);
update();
}
void PlaymatPreviewWidget::setParams(const PlaymatParams &p)
{
params = p;
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
update();
}
@ -69,94 +31,6 @@ void PlaymatPreviewWidget::setAttribution(const QString &attribution)
update();
}
QRectF PlaymatPreviewWidget::activePlayArea() const
{
// The viewport is a frame shaped like a fresh board's stack+table area
// (kMinTableAspect): the most generous framing the game will produce.
// Dimmed strips mark where a wider, developed table crops further.
const QRectF cardRect = QRectF(rect()).adjusted(3, 2, -3, -2);
return PlaymatUtils::aspectFitRect(cardRect.adjusted(6, 4, -4, -4), MIN_TABLE_ASPECT);
}
qreal PlaymatPreviewWidget::samplingWindowSide() const
{
if (sourcePixmap.isNull()) {
return 0.0;
}
// Same clamped window the render path uses, gestures and painting must
// never disagree about geometry.
return PlaymatUtils::playmatWindowSide(sourcePixmap.size(), params);
}
qreal PlaymatPreviewWidget::widgetToSourceScale() const
{
const qreal cropSide = samplingWindowSide();
const QRectF area = activePlayArea();
if (cropSide <= 0.0 || area.isEmpty()) {
return 0.0;
}
// Mirror coverFitRect(): the square crop into the (wider) viewport fills
// its width.
return area.width() / cropSide;
}
void PlaymatPreviewWidget::applyCropDelta(qreal dMarginL, qreal dMarginR, qreal dOffset, qreal zoomFactor)
{
PlaymatParams next = params;
if (dMarginL + dMarginR == 0.0) {
// Pure horizontal pan rebalances the margins along their
// sum constant segment. Individual bounds must not break that
// invariant, otherwise repeated corner drags let one margin grow
// without end, collapsing the viewing window and desynchronizing the
// visual zoom from the readout.
const qreal sum = params.marginPctL + params.marginPctR;
const qreal lo = qMax(0.0, sum - MAX_MARGIN);
const qreal hi = qMin(sum, MAX_MARGIN);
next.marginPctL = qBound(lo, params.marginPctL + dMarginL, hi);
next.marginPctR = sum - next.marginPctL;
} else {
next.marginPctL = qBound(0.0, params.marginPctL + dMarginL, MAX_MARGIN);
next.marginPctR = qBound(0.0, params.marginPctR + dMarginR, MAX_MARGIN);
}
next.verticalOffset = qBound(0.0, params.verticalOffset + dOffset, 1.0);
// Clamp through the shared helper so the floor tracks the new margins:
// zooming out stops exactly where the window reaches the card bounds.
next.zoom = params.zoom * zoomFactor;
if (!sourcePixmap.isNull()) {
next.zoom = PlaymatUtils::playmatClampedZoom(sourcePixmap.size(), next);
}
if (sameCrop(next, params)) {
return;
}
params = next;
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
update();
emit paramsEdited(params);
}
bool PlaymatPreviewWidget::sameCrop(const PlaymatParams &a, const PlaymatParams &b) const
{
// Exact comparison on purpose: clamped assignments yield identical bits,
// while qFuzzyCompare based equality misbehaves around zero
return a.marginPctL == b.marginPctL && a.marginPctR == b.marginPctR && a.verticalOffset == b.verticalOffset &&
a.zoom == b.zoom;
}
void PlaymatPreviewWidget::restoreSnapshot()
{
// The snapshot only ever holds values that passed the gesture clamps,
// so it is safe to restore verbatim
if (sameCrop(paramsAtFocusIn, params)) {
return;
}
params = paramsAtFocusIn;
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
update();
emit paramsEdited(params);
}
void PlaymatPreviewWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
@ -182,40 +56,23 @@ void PlaymatPreviewWidget::paintEvent(QPaintEvent *)
return;
}
const QRectF playArea = activePlayArea();
// Draw the playmat art using the same logic as PlayerGraphicsItem
// The preview area represents the combined stack+table play area
// Stack is ~20% width on the left, table is ~80% on the right
const QRectF playArea = cardRect.adjusted(6, 4, -4, -4);
// Exactly the game's pipeline (player_graphics_item): cover fit the crop
// into the table shaped viewport, centered, so the frame shows precisely
// what a minimum aspect window shows, and dragging moves the art behind
// the fixed frame.
const QRectF srcRect = PlaymatUtils::computeArtSourceRect(sourcePixmap.size(), params);
const QRectF dstRect = PlaymatUtils::coverFitRect(playArea, srcRect.size());
painter.setClipRect(playArea.toRect());
painter.drawPixmap(dstRect, sourcePixmap, srcRect);
// Wider (developed) tables crop further: mark where a kMaxTableAspect
// board stops. Palette driven so theme authors can recolor the markers.
const qreal wideBandHeight = playArea.height() * (MIN_TABLE_ASPECT / MAX_TABLE_ASPECT);
const qreal stripHeight = (playArea.height() - wideBandHeight) / 2.0;
QColor stripColor = palette().color(QPalette::Window);
stripColor.setAlpha(150);
painter.fillRect(QRectF(playArea.left(), playArea.top(), playArea.width(), stripHeight), stripColor);
painter.fillRect(QRectF(playArea.left(), playArea.bottom() - stripHeight, playArea.width(), stripHeight),
stripColor);
QColor hairlineColor = palette().color(QPalette::Highlight);
hairlineColor.setAlpha(110);
painter.setPen(QPen(hairlineColor, 1));
painter.drawLine(QPointF(playArea.left(), playArea.top() + stripHeight),
QPointF(playArea.right(), playArea.top() + stripHeight));
painter.drawLine(QPointF(playArea.left(), playArea.bottom() - stripHeight),
QPointF(playArea.right(), playArea.bottom() - stripHeight));
painter.setClipping(false);
// Draw zone divider: stack is roughly the left portion
const double stackWidthRatio = 0.18; // Stack is about 18% of total play area
const double stackDividerX = playArea.left() + playArea.width() * stackWidthRatio;
// Subtle semi transparent overlays to distinguish zones
// Subtle semi-transparent overlays to distinguish zones
// Stack zone overlay (slightly darker)
QRectF stackOverlay(playArea.left(), playArea.top(), playArea.width() * stackWidthRatio, playArea.height());
painter.fillRect(stackOverlay, QColor(0, 0, 0, 40));
@ -232,150 +89,11 @@ void PlaymatPreviewWidget::paintEvent(QPaintEvent *)
const double landDividerY = playArea.top() + playArea.height() * 0.65;
painter.setPen(QPen(QColor(255, 255, 255, 30), 1));
painter.drawLine(QPointF(stackDividerX, landDividerY), QPointF(playArea.right(), landDividerY));
painter.setClipping(false);
// Border around the viewport = boundary of every plausible framing.
// Border around entire play area
painter.setPen(QPen(QColor(70, 80, 95, 120), 1));
painter.setBrush(Qt::NoBrush);
painter.drawRoundedRect(playArea.adjusted(0, 0, -1, -1), 3, 3);
// Visible keyboard focus per the focus cursor contract, Tab must show
// where the keys land
if (hasFocus()) {
QPen focusPen(palette().color(QPalette::Highlight), 2);
painter.setPen(focusPen);
painter.drawRoundedRect(playArea.adjusted(-1, -1, 1, 1), 3, 3);
}
paintArtAttribution(painter, playArea, attributionText, Qt::AlignRight | Qt::AlignBottom, 0.8);
// Zoom readout so the gesture has a visible, stable counterpart.
QColor ink = palette().color(QPalette::WindowText);
ink.setAlpha(160);
painter.setPen(ink);
painter.drawText(QPointF(playArea.left() + 8, playArea.bottom() - 8),
tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
}
void PlaymatPreviewWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton || sourcePixmap.isNull() || samplingWindowSide() <= 0.0) {
QWidget::mousePressEvent(event);
return;
}
lastDragPos = event->pos();
setCursor(Qt::ClosedHandCursor);
event->accept();
}
void PlaymatPreviewWidget::mouseMoveEvent(QMouseEvent *event)
{
if (!(event->buttons() & Qt::LeftButton) || sourcePixmap.isNull()) {
QWidget::mouseMoveEvent(event);
return;
}
const QPointF delta = QPointF(event->pos() - lastDragPos);
lastDragPos = event->pos();
const qreal scale = widgetToSourceScale();
// Vertical travel of the SAMPLING window: verticalOffset moves its top
// edge by exactly this much per unit, identical to the render path.
// Windows taller than the art (square/landscape sources zoomed out)
// leave no travel, vertical drags are then boundary no ops.
const qreal travel = static_cast<qreal>(sourcePixmap.height()) - samplingWindowSide();
if (scale <= 0.0) {
event->accept();
return;
}
// Dragging moves the ART with the cursor, so the viewing window slides the
// other way. Horizontal panning rebalances the margins (their sum, hence
// the window width, stays constant), vertical panning moves the window's
// top edge within its available travel.
const qreal sourceW = sourcePixmap.width();
const qreal dMargin = -(delta.x() / scale) / sourceW;
const qreal dOffset = travel > 0.5 ? -(delta.y() / scale) / travel : 0.0;
applyCropDelta(dMargin, -dMargin, dOffset, 1.0);
event->accept();
}
void PlaymatPreviewWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
setCursor(sourcePixmap.isNull() ? Qt::ArrowCursor : Qt::OpenHandCursor);
event->accept();
return;
}
QWidget::mouseReleaseEvent(event);
}
void PlaymatPreviewWidget::wheelEvent(QWheelEvent *event)
{
if (sourcePixmap.isNull() || samplingWindowSide() <= 0.0) {
QWidget::wheelEvent(event);
return;
}
const qreal notches = static_cast<qreal>(event->angleDelta().y()) / 120.0;
if (notches == 0.0) {
event->accept();
return;
}
applyCropDelta(0.0, 0.0, 0.0, std::pow(WHEEL_ZOOM_BASE, notches));
event->accept();
}
void PlaymatPreviewWidget::keyPressEvent(QKeyEvent *event)
{
if (sourcePixmap.isNull()) {
QWidget::keyPressEvent(event);
return;
}
switch (event->key()) {
case Qt::Key_Escape:
if (sameCrop(params, paramsAtFocusIn)) {
// Nothing to undo on this surface, let the event reach the
// dialog so Esc keeps its close meaning there
QWidget::keyPressEvent(event);
return;
}
restoreSnapshot();
break;
case Qt::Key_Backspace:
restoreSnapshot();
break;
case Qt::Key_Left:
applyCropDelta(-KEY_PAN_MARGIN_STEP, KEY_PAN_MARGIN_STEP, 0.0, 1.0);
break;
case Qt::Key_Right:
applyCropDelta(KEY_PAN_MARGIN_STEP, -KEY_PAN_MARGIN_STEP, 0.0, 1.0);
break;
case Qt::Key_Up:
applyCropDelta(0.0, 0.0, -KEY_PAN_OFFSET_STEP, 1.0);
break;
case Qt::Key_Down:
applyCropDelta(0.0, 0.0, KEY_PAN_OFFSET_STEP, 1.0);
break;
case Qt::Key_Plus:
case Qt::Key_Equal:
applyCropDelta(0.0, 0.0, 0.0, KEY_ZOOM_STEP);
break;
case Qt::Key_Minus:
applyCropDelta(0.0, 0.0, 0.0, 1.0 / KEY_ZOOM_STEP);
break;
default:
QWidget::keyPressEvent(event);
return;
}
event->accept();
}
void PlaymatPreviewWidget::focusInEvent(QFocusEvent *event)
{
// Snapshot for the Esc or Backspace reset, restoring whatever the user
// had when the surface took focus
paramsAtFocusIn = params;
QWidget::focusInEvent(event);
}

View file

@ -1,23 +1,16 @@
#ifndef COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H
#define COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H
#include <QFocusEvent>
#include <QPixmap>
#include <QWidget>
#include <libcockatrice/deck_list/deck_list.h>
/**
* @brief Interactive crop surface showing how a playmat card art will appear
* @brief Preview widget that shows how a playmat card art will appear
* across the combined table + stack play area.
*
* Renders a fixed frame shaped like a fresh board's stack+table area (the
* most generous framing the game produces): it shows the tallest slice of
* the square crop in normal play, with dimmed strips marking where a wider,
* developed table crops further, exactly the game's own render pipeline.
* The widget doubles as the editor's primary crop control: dragging pans the
* art behind the frame, the wheel zooms, and arrow keys nudge, mirroring
* the stored parameters (margins pan horizontally, verticalOffset
* vertically, zoom scales) so no separate numeric controls are needed.
* Renders a miniature mockup with the card art applied using the
* given PlaymatParams, including faint zone divider lines.
*/
class PlaymatPreviewWidget : public QWidget
{
@ -30,32 +23,13 @@ public:
void setParams(const PlaymatParams &params);
void setAttribution(const QString &attribution);
signals:
/** @brief Emitted whenever direct manipulation (drag, wheel, keys) changes the crop parameters. */
void paramsEdited(const PlaymatParams &params);
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
void focusInEvent(QFocusEvent *event) override;
private:
QRectF activePlayArea() const; ///< destination rect used for rendering AND gesture math
qreal samplingWindowSide() const; ///< clamped square window side, shared with the render path
qreal widgetToSourceScale() const;
void applyCropDelta(qreal dMarginL, qreal dMarginR, qreal dOffset, qreal zoomFactor);
bool sameCrop(const PlaymatParams &a, const PlaymatParams &b) const;
void restoreSnapshot();
QPixmap sourcePixmap;
PlaymatParams params;
PlaymatParams paramsAtFocusIn; ///< crop as of the latest focus gain, restored by Esc or Backspace
QString attributionText;
QPoint lastDragPos; ///< widget space position of the previous mouse move while panning
};
#endif // COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H

View file

@ -7,7 +7,6 @@
#include "card_database_model.h"
#include "playmat_preview_widget.h"
#include <QCheckBox>
#include <QComboBox>
#include <QCompleter>
#include <QDialogButtonBox>
@ -49,6 +48,10 @@ PlaymatSettingsDialog::PlaymatSettingsDialog(const CardRef &initialCard,
reloadPreview();
}
}
marginLSpin->setValue(initialParams.marginPctL);
marginRSpin->setValue(initialParams.marginPctR);
verticalOffsetSpin->setValue(initialParams.verticalOffset);
zoomSpin->setValue(initialParams.zoom);
retranslateUi();
}
@ -109,32 +112,23 @@ void PlaymatSettingsDialog::setupUi()
connect(providerComboBox, &QComboBox::currentIndexChanged, this, [this]() {
currentCard.providerId = providerComboBox->currentData().toString();
reloadPreview();
onParamChanged();
});
auto *form = new QFormLayout;
controlsForm = form;
cardNameLabel = new QLabel;
printingLabel = new QLabel;
form->addRow(cardNameLabel, searchBar);
form->addRow(printingLabel, providerComboBox);
// Numerical editors expose the raw PlaymatParams for precise input. They
// share the same form as the rows above so every field lines up on one
// label column. They stay hidden until requested since the crop surface
// is the primary control.
marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01);
marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01);
verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01);
zoomSpin = makeSpinBox(0.1, 4.0, currentParams.zoom, 0.05);
auto *form = new QFormLayout;
cardNameLabel = new QLabel;
printingLabel = new QLabel;
leftMarginLabel = new QLabel;
rightMarginLabel = new QLabel;
verticalOffsetLabel = new QLabel;
zoomLabel = new QLabel;
showNumericEditorsCheck = new QCheckBox;
form->addRow(showNumericEditorsCheck);
form->addRow(cardNameLabel, searchBar);
form->addRow(printingLabel, providerComboBox);
form->addRow(leftMarginLabel, marginLSpin);
form->addRow(rightMarginLabel, marginRSpin);
form->addRow(verticalOffsetLabel, verticalOffsetSpin);
@ -144,14 +138,9 @@ void PlaymatSettingsDialog::setupUi()
controlsGroup->setLayout(form);
preview = new PlaymatPreviewWidget;
preview->setParams(currentParams);
auto *previewLayout = new QVBoxLayout;
previewLayout->addWidget(preview);
previewCaptionLabel = new QLabel;
previewCaptionLabel->setAlignment(Qt::AlignCenter);
previewCaptionLabel->setWordWrap(true);
previewLayout->addWidget(previewCaptionLabel);
previewGroup = new QGroupBox;
previewGroup->setLayout(previewLayout);
@ -166,35 +155,16 @@ void PlaymatSettingsDialog::setupUi()
accept();
});
// The crop surface is the primary control: dragging pans, wheel/keys zoom,
// editing exactly the same stored parameters the numeric fields do.
connect(preview, &PlaymatPreviewWidget::paramsEdited, this, [this](const PlaymatParams &edited) {
currentParams = edited;
QSignalBlocker blockMarginL(marginLSpin);
QSignalBlocker blockMarginR(marginRSpin);
QSignalBlocker blockOffset(verticalOffsetSpin);
QSignalBlocker blockZoom(zoomSpin);
marginLSpin->setValue(edited.marginPctL);
marginRSpin->setValue(edited.marginPctR);
verticalOffsetSpin->setValue(edited.verticalOffset);
zoomSpin->setValue(edited.zoom);
});
connect(showNumericEditorsCheck, &QCheckBox::toggled, this, &PlaymatSettingsDialog::setNumericEditorsVisible);
setNumericEditorsVisible(false);
auto *root = new QVBoxLayout;
root->addWidget(controlsGroup);
root->addWidget(previewGroup);
root->addWidget(buttons);
setLayout(root);
connect(marginLSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
connect(marginRSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
connect(verticalOffsetSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
// The crop surface leads visually, card selection supports it below.
auto *root = new QVBoxLayout;
root->addWidget(previewGroup);
root->addWidget(controlsGroup);
root->addWidget(buttons);
setLayout(root);
}
void PlaymatSettingsDialog::populateProviderCombo(const QString &cardName)
@ -291,34 +261,17 @@ void PlaymatSettingsDialog::onParamChanged()
preview->setParams(currentParams);
}
void PlaymatSettingsDialog::setNumericEditorsVisible(bool visible)
{
controlsForm->setRowVisible(leftMarginLabel, visible);
controlsForm->setRowVisible(rightMarginLabel, visible);
controlsForm->setRowVisible(verticalOffsetLabel, visible);
controlsForm->setRowVisible(zoomLabel, visible);
// A QDialog never resizes itself when its content requirements change,
// so revealing the editors would squeeze the crop group until the info
// caption ran into the preview. Re-fit the dialog to the new size hint.
adjustSize();
}
void PlaymatSettingsDialog::retranslateUi()
{
setWindowTitle(tr("Playmat Settings"));
searchBar->setPlaceholderText(tr("Type a card name..."));
cardNameLabel->setText(tr("Card name:"));
printingLabel->setText(tr("Printing:"));
showNumericEditorsCheck->setText(tr("Show numerical editors"));
leftMarginLabel->setText(tr("Left margin (%):"));
rightMarginLabel->setText(tr("Right margin (%):"));
verticalOffsetLabel->setText(tr("Vertical offset:"));
zoomLabel->setText(tr("Zoom:"));
controlsGroup->setTitle(tr("Card"));
previewGroup->setTitle(tr("Crop"));
previewCaptionLabel->setText(
tr("Drag to pan, scroll to zoom, arrow keys nudge, plus and minus zoom, Backspace or Esc restores. "
"Dimmed strips mark where a wider table crops further."));
controlsGroup->setTitle(tr("Parameters"));
previewGroup->setTitle(tr("Preview"));
removeButton->setText(tr("Remove Playmat"));
}

View file

@ -5,16 +5,13 @@
#include <QPixmap>
#include <libcockatrice/deck_list/deck_list.h>
class QCheckBox;
class QComboBox;
class QCompleter;
class QDoubleSpinBox;
class QFormLayout;
class QGroupBox;
class QLabel;
class QLineEdit;
class QPushButton;
class QWidget;
class CardDatabaseModel;
class CardDatabaseDisplayModel;
class CardSearchModel;
@ -24,11 +21,10 @@ class PlaymatPreviewWidget;
/**
* @brief Dialog for configuring the playmat card art for a deck.
*
* The crop surface is the primary control: drag to pan the visible art,
* scroll (or +/- keys) to zoom, arrow keys to nudge. Card name and printing
* are selected below. A checkbox reveals optional numerical editors for the
* raw PlaymatParams. These controls edit the same stored PlaymatParams that
* ship in deck files and player properties.
* Allows the user to select a card from the database and adjust
* positioning parameters (margins, zoom, vertical offset) for how
* the card art appears as a playmat background across the
* combined table + stack play area.
*/
class PlaymatSettingsDialog : public QDialog
{
@ -44,15 +40,14 @@ public:
private slots:
void onCardNameChanged(const QString &name);
void onParamChanged();
void reloadPreview();
void onParamChanged();
private:
void setupUi();
void populateProviderCombo(const QString &cardName);
void initializeSearchBar();
void retranslateUi();
void setNumericEditorsVisible(bool visible);
QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step);
QLineEdit *searchBar;
@ -68,9 +63,6 @@ private:
QLabel *cardNameLabel;
QLabel *printingLabel;
QLabel *previewCaptionLabel;
QCheckBox *showNumericEditorsCheck;
QFormLayout *controlsForm;
QLabel *leftMarginLabel;
QLabel *rightMarginLabel;
QLabel *verticalOffsetLabel;
@ -83,7 +75,6 @@ private:
QDoubleSpinBox *marginRSpin;
QDoubleSpinBox *verticalOffsetSpin;
QDoubleSpinBox *zoomSpin;
PlaymatPreviewWidget *preview;
QPixmap currentPixmap;

View file

@ -15,46 +15,19 @@
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPushButton>
#include <QRegularExpression>
#include <QVBoxLayout>
#include <QWheelEvent>
#include <cmath>
#include <libcockatrice/card/database/card_database_manager.h>
namespace
{
// Gesture clamps and step sizes for this direct manipulation surface. The
// gesture zoom floor is 1.0: basescale already cover fits the art, so any
// smaller scale would underfill the strip.
constexpr qreal kMinGestureZoom = 1.0;
constexpr qreal kMaxZoom = 4.0;
constexpr qreal kKeyPanOffsetStep = 0.01;
constexpr qreal kKeyZoomStep = 1.05;
constexpr qreal kWheelZoomBase = 1.15; // zoom factor per wheel notch
} // namespace
CardArtPreviewWidget::CardArtPreviewWidget(QWidget *parent) : QWidget(parent)
{
setMinimumSize(400, 72);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
setFocusPolicy(Qt::StrongFocus);
setAccessibleName(tr("Banner preview"));
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
}
void CardArtPreviewWidget::focusInEvent(QFocusEvent *event)
{
// Snapshot for the Esc or Backspace reset, restoring whatever the user
// had when the surface took focus
paramsAtFocusIn = params;
QWidget::focusInEvent(event);
}
void CardArtPreviewWidget::setPixmap(const QPixmap &pixmap)
@ -66,7 +39,6 @@ void CardArtPreviewWidget::setPixmap(const QPixmap &pixmap)
void CardArtPreviewWidget::setParams(const CardArtParams &p)
{
params = p;
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
update();
}
@ -95,22 +67,9 @@ void CardArtPreviewWidget::paintEvent(QPaintEvent *)
painter.setBrush(accentColor);
painter.drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
// Visible keyboard focus per the focus cursor contract, Tab must show
// where the keys land, including in the empty state
const auto paintFocusRing = [&painter, &cardRect, this]() {
if (!hasFocus()) {
return;
}
QPen focusPen(palette().color(QPalette::Highlight), 2);
painter.setPen(focusPen);
painter.setBrush(Qt::NoBrush);
painter.drawRoundedRect(cardRect.adjusted(-1, -1, 1, 1), 6, 6);
};
if (sourcePixmap.isNull()) {
painter.setPen(QColor(150, 150, 150));
painter.drawText(rect, Qt::AlignCenter, tr("No card selected"));
paintFocusRing();
return;
}
@ -121,7 +80,7 @@ void CardArtPreviewWidget::paintEvent(QPaintEvent *)
&sourcePixmap // direct pixmap
);
// Avatar placeholder so the left margin interaction is visible
// Avatar placeholder so the left-margin interaction is visible
const int avatarX = rect.left() + 14;
const int avatarY = rect.top() + (rect.height() - 36) / 2;
const QRect avatarRect(avatarX, avatarY, 36, 36);
@ -140,200 +99,37 @@ void CardArtPreviewWidget::paintEvent(QPaintEvent *)
painter.drawEllipse(avatarRect.adjusted(-1, -1, 1, 1));
paintArtAttribution(painter, cardRect, attributionText);
paintFocusRing();
}
qreal CardArtPreviewWidget::bannerTravel() const
{
if (sourcePixmap.isNull()) {
return 0.0;
}
// Mirror UserListPainter::drawCardArt() exactly: same strip metrics, the
// copy is drawn 1:1, so output pixels equal widget pixels here.
const int cardH = rect().height() - 4;
const int totalW = (rect().right() - 4) - rect().left();
const int marginL = qRound(totalW * params.marginPctL);
const int marginR = qRound(totalW * params.marginPctR);
const int drawW = totalW - marginL - marginR;
const double basescale = qMax(double(drawW) / sourcePixmap.width(), double(cardH) / sourcePixmap.height());
// qRound for literal parity with drawCardArt, which rounds the scaled
// height before computing travel
const double scaledH = qRound(sourcePixmap.height() * basescale * params.zoom);
return scaledH - cardH;
}
void CardArtPreviewWidget::applyCropDelta(qreal dOffset, qreal zoomFactor)
{
CardArtParams next = params;
next.verticalOffset = qBound(0.0, params.verticalOffset + dOffset, 1.0);
next.zoom = qBound(kMinGestureZoom, params.zoom * zoomFactor, kMaxZoom);
if (sameCrop(next, params)) {
return;
}
params = next;
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
update();
emit paramsEdited(params);
}
bool CardArtPreviewWidget::sameCrop(const CardArtParams &a, const CardArtParams &b) const
{
// Exact comparison on purpose: clamped assignments yield identical bits,
// while qFuzzyCompare based equality misbehaves around zero
return a.verticalOffset == b.verticalOffset && a.zoom == b.zoom;
}
void CardArtPreviewWidget::restoreSnapshot()
{
// The snapshot only ever holds values that passed the gesture clamps,
// so it is safe to restore verbatim
if (sameCrop(paramsAtFocusIn, params)) {
return;
}
params = paramsAtFocusIn;
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
update();
emit paramsEdited(params);
}
void CardArtPreviewWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton || sourcePixmap.isNull() || bannerTravel() <= 0.5) {
QWidget::mousePressEvent(event);
return;
}
dragging = true;
lastDragPos = event->pos();
setCursor(Qt::ClosedHandCursor);
event->accept();
}
void CardArtPreviewWidget::mouseMoveEvent(QMouseEvent *event)
{
if (!dragging || sourcePixmap.isNull()) {
QWidget::mouseMoveEvent(event);
return;
}
const qreal dy = event->pos().y() - lastDragPos.y();
lastDragPos = event->pos();
const qreal travel = bannerTravel();
if (travel <= 0.5) {
event->accept();
return;
}
// Dragging moves the ART with the cursor, so the crop window slides the
// other way through the available travel.
applyCropDelta(-dy / travel, 1.0);
event->accept();
}
void CardArtPreviewWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
dragging = false;
unsetCursor();
event->accept();
return;
}
QWidget::mouseReleaseEvent(event);
}
void CardArtPreviewWidget::wheelEvent(QWheelEvent *event)
{
if (sourcePixmap.isNull()) {
QWidget::wheelEvent(event);
return;
}
const qreal notches = static_cast<qreal>(event->angleDelta().y()) / 120.0;
if (notches == 0.0) {
event->accept();
return;
}
applyCropDelta(0.0, std::pow(kWheelZoomBase, notches));
event->accept();
}
void CardArtPreviewWidget::keyPressEvent(QKeyEvent *event)
{
if (sourcePixmap.isNull()) {
QWidget::keyPressEvent(event);
return;
}
switch (event->key()) {
case Qt::Key_Escape:
if (sameCrop(params, paramsAtFocusIn)) {
// Nothing to undo on this surface, let the event reach the
// dialog so Esc keeps its close meaning there
QWidget::keyPressEvent(event);
return;
}
restoreSnapshot();
break;
case Qt::Key_Backspace:
restoreSnapshot();
break;
case Qt::Key_Up:
applyCropDelta(-kKeyPanOffsetStep, 1.0);
break;
case Qt::Key_Down:
applyCropDelta(kKeyPanOffsetStep, 1.0);
break;
case Qt::Key_Plus:
case Qt::Key_Equal:
applyCropDelta(0.0, kKeyZoomStep);
break;
case Qt::Key_Minus:
applyCropDelta(0.0, 1.0 / kKeyZoomStep);
break;
default:
QWidget::keyPressEvent(event);
return;
}
event->accept();
}
UserCardArtSettingsDialog::UserCardArtSettingsDialog(const CardArtParams &initial, QWidget *parent)
: QDialog(parent), currentParams(initial)
{
// Legacy stored banners may carry zoom below the gesture floor or an out
// of range offset. Normalize once on open so the preview renders filled
// and Ok saves a state the gestures can reach again
currentParams.zoom = qBound(kMinGestureZoom, currentParams.zoom, kMaxZoom);
currentParams.verticalOffset = qBound(0.0, currentParams.verticalOffset, 1.0);
setWindowTitle(tr("Card Art Settings"));
setMinimumWidth(500);
setupUi();
// Seed UI from initial params
if (!currentParams.cardName.isEmpty()) {
// onCardNameChanged overwrites cardProviderId with the first printing,
// so remember the stored one before it runs
const QString storedProviderId = currentParams.cardProviderId;
searchBar->setText(currentParams.cardName);
onCardNameChanged(currentParams.cardName);
if (!initial.cardName.isEmpty()) {
searchBar->setText(initial.cardName);
onCardNameChanged(initial.cardName);
// onCardNameChanged leaves the printing combo on the first printing in
// the database, which would silently change the stored banner card on
// accept. Restore the stored printing when it resolves locally.
const int storedPrintingIndex = providerComboBox->findData(storedProviderId);
const int storedPrintingIndex = providerComboBox->findData(initial.cardProviderId);
if (storedPrintingIndex != -1) {
providerComboBox->setCurrentIndex(storedPrintingIndex);
} else if (!storedProviderId.isEmpty()) {
} else {
// Stored printing not in the local database: keep it rather than
// silently substituting the first printing.
currentParams.cardProviderId = storedProviderId;
currentParams.cardProviderId = initial.cardProviderId;
reloadPreview();
}
}
marginLSpin->setValue(currentParams.marginPctL);
marginRSpin->setValue(currentParams.marginPctR);
marginLSpin->setValue(initial.marginPctL);
marginRSpin->setValue(initial.marginPctR);
verticalOffsetSpin->setValue(initial.verticalOffset);
zoomSpin->setValue(initial.zoom);
}
CardArtParams UserCardArtSettingsDialog::params() const
@ -354,6 +150,7 @@ QDoubleSpinBox *UserCardArtSettingsDialog::makeSpinBox(double min, double max, d
void UserCardArtSettingsDialog::initializeSearchBar()
{
searchBar = new QLineEdit;
searchBar->setPlaceholderText(tr("Type a card name..."));
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this);
@ -393,33 +190,29 @@ void UserCardArtSettingsDialog::setupUi()
marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01);
marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01);
verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01);
zoomSpin = makeSpinBox(0.1, 4.0, currentParams.zoom, 0.05);
auto *form = new QFormLayout;
cardNameLabel = new QLabel;
printingLabel = new QLabel;
marginLLabel = new QLabel;
marginRLabel = new QLabel;
form->addRow(cardNameLabel, searchBar);
form->addRow(printingLabel, providerComboBox);
form->addRow(marginLLabel, marginLSpin);
form->addRow(marginRLabel, marginRSpin);
form->addRow(tr("Card name:"), searchBar);
form->addRow(tr("Card ProviderId:"), providerComboBox);
form->addRow(tr("Left margin (%):"), marginLSpin);
form->addRow(tr("Right margin (%):"), marginRSpin);
form->addRow(tr("Vertical offset:"), verticalOffsetSpin);
form->addRow(tr("Zoom:"), zoomSpin);
controlsGroup = new QGroupBox;
auto *controlsGroup = new QGroupBox(tr("Parameters"));
controlsGroup->setLayout(form);
preview = new CardArtPreviewWidget;
auto *previewLayout = new QVBoxLayout;
previewLayout->addWidget(preview);
previewCaptionLabel = new QLabel;
previewCaptionLabel->setAlignment(Qt::AlignCenter);
previewCaptionLabel->setWordWrap(true);
previewLayout->addWidget(previewCaptionLabel);
previewGroup = new QGroupBox;
auto *previewGroup = new QGroupBox(tr("Preview"));
previewGroup->setLayout(previewLayout);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
removeBtn = new QPushButton;
auto *removeBtn = new QPushButton(tr("Remove Banner Card"));
buttons->addButton(removeBtn, QDialogButtonBox::ResetRole);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
@ -429,38 +222,16 @@ void UserCardArtSettingsDialog::setupUi()
accept();
});
// The banner leads visually, card selection and margins support it below.
auto *root = new QVBoxLayout;
root->addWidget(previewGroup);
root->addWidget(controlsGroup);
root->addWidget(previewGroup);
root->addWidget(buttons);
setLayout(root);
connect(marginLSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
connect(marginRSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
// Gestures are the only editors of offset and zoom on this surface.
// Margins stay explicit numeric controls: they trim the strip's
// sides and have no natural drag mapping.
connect(preview, &CardArtPreviewWidget::paramsEdited, this,
[this](const CardArtParams &edited) { currentParams = edited; });
retranslateUi();
}
void UserCardArtSettingsDialog::retranslateUi()
{
setWindowTitle(tr("Card Art Settings"));
searchBar->setPlaceholderText(tr("Type a card name..."));
cardNameLabel->setText(tr("Card name:"));
printingLabel->setText(tr("Printing:"));
marginLLabel->setText(tr("Left margin (%):"));
marginRLabel->setText(tr("Right margin (%):"));
controlsGroup->setTitle(tr("Card"));
previewCaptionLabel->setText(
tr("Drag to pan, scroll to zoom, arrow keys nudge, plus and minus zoom, Backspace or Esc restores."));
previewGroup->setTitle(tr("Banner"));
removeBtn->setText(tr("Remove Banner Card"));
connect(verticalOffsetSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
}
void UserCardArtSettingsDialog::populateProviderCombo(const QString &cardName)
@ -510,7 +281,7 @@ void UserCardArtSettingsDialog::onCardNameChanged(const QString &name)
populateProviderCombo(name);
if (providerComboBox->count() == 0) {
// No printings found for this card, nothing to preview.
// No printings found for this card; nothing to preview.
currentPixmap = QPixmap();
preview->setPixmap(currentPixmap);
currentParams.cardProviderId.clear();
@ -540,7 +311,7 @@ void UserCardArtSettingsDialog::reloadPreview()
// whichever CardInfo we just asked for, so the preview catches up once
// the image actually arrives instead of staying on the placeholder.
//
// Disconnect any previous listener first, otherwise switching cards
// Disconnect any previous listener first -- otherwise switching cards
// repeatedly stacks up connections to old CardInfo objects, each of
// which would still fire reloadPreview() (harmlessly, but wastefully)
// whenever ITS art finishes loading later.
@ -550,8 +321,8 @@ void UserCardArtSettingsDialog::reloadPreview()
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
if (fullRes.isNull()) {
// Not loaded yet, wait for the signal instead of giving up.
// card.getCardPtr() is a CardInfoPtr (QSharedPointer<CardInfo>),
// Not loaded yet -- wait for the signal instead of giving up.
// card.getCardPtr() is a CardInfoPtr (QSharedPointer<CardInfo>);
// .data() gives the raw QObject* needed for connect().
CardInfo *cardInfo = card.getCardPtr().data();
if (cardInfo) {
@ -574,5 +345,7 @@ void UserCardArtSettingsDialog::onParamChanged()
{
currentParams.marginPctL = marginLSpin->value();
currentParams.marginPctR = marginRSpin->value();
currentParams.verticalOffset = verticalOffsetSpin->value();
currentParams.zoom = zoomSpin->value();
preview->setParams(currentParams);
}
}

View file

@ -8,29 +8,13 @@
#include <QPixmap>
class QCompleter;
class QFocusEvent;
class QGroupBox;
class QKeyEvent;
class QMouseEvent;
class QLineEdit;
class QDoubleSpinBox;
class QLabel;
class QPushButton;
class QWheelEvent;
class CardDatabaseModel;
class CardDatabaseDisplayModel;
class CardSearchModel;
class CardCompleterProxyModel;
/**
* @brief Interactive preview of the user list banner art.
*
* Renders the banner strip with the given CardArtParams through the same
* UserListPainter::drawCardArt() the live delegate uses, including the
* avatar placeholder and fade masks. Dragging pans the art vertically at
* output scale, the wheel zooms, arrow keys nudge, Backspace or Esc
* restores the parameters as of focus gain.
*/
class CardArtPreviewWidget : public QWidget
{
Q_OBJECT
@ -42,31 +26,13 @@ public:
void setParams(const CardArtParams &params);
void setAttribution(const QString &attribution);
signals:
/** @brief Emitted whenever direct manipulation (drag, wheel, keys) changes the parameters. */
void paramsEdited(const CardArtParams &params);
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
void focusInEvent(QFocusEvent *event) override;
private:
qreal bannerTravel() const; ///< vertical travel of the art behind the strip, in output pixels
void applyCropDelta(qreal dOffset, qreal zoomFactor);
bool sameCrop(const CardArtParams &a, const CardArtParams &b) const;
void restoreSnapshot();
QPixmap sourcePixmap;
CardArtParams params;
CardArtParams paramsAtFocusIn; ///< crop as of the latest focus gain, restored by Esc or Backspace
QString attributionText;
QPoint lastDragPos; ///< widget space position of the previous mouse move while panning
bool dragging{false}; ///< true between an accepted press and its release, guards stale drag positions
};
class UserCardArtSettingsDialog : public QDialog
@ -85,7 +51,6 @@ private slots:
private:
void setupUi();
void retranslateUi();
void populateProviderCombo(const QString &cardName);
void initializeSearchBar();
QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step);
@ -101,20 +66,14 @@ private:
QMetaObject::Connection pixmapUpdatedConnection;
QLabel *cardNameLabel;
QLabel *printingLabel;
QLabel *marginLLabel;
QLabel *marginRLabel;
QGroupBox *controlsGroup;
QLabel *previewCaptionLabel;
QGroupBox *previewGroup;
QPushButton *removeBtn;
QDoubleSpinBox *marginLSpin;
QDoubleSpinBox *marginRSpin;
QDoubleSpinBox *verticalOffsetSpin;
QDoubleSpinBox *zoomSpin;
CardArtPreviewWidget *preview;
QPixmap currentPixmap;
CardArtParams currentParams;
};
#endif // COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H
#endif // COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H

View file

@ -718,19 +718,11 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
}
});
// Section dividers can be collapsed/expanded by the user. Surface those
// changes only from real user interaction. Programmatic expansion is
// applied through setSectionExpanded() / setExpandedProgrammatically().
connect(userTree, &QTreeWidget::itemExpanded, this,
[this](QTreeWidgetItem *item) { handleSectionExpansion(item, true); });
connect(userTree, &QTreeWidget::itemCollapsed, this,
[this](QTreeWidgetItem *item) { handleSectionExpansion(item, false); });
// Hide popup when list scrolls (reference row has moved)
connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] {
showPopupTimer->stop();
hidePopup(true);
requestVisibleItemResources();
requestAvatarsForVisibleItems();
});
// Forward join requests from popup upward
@ -746,7 +738,7 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
// Keep the popup-less scroll path alive for avatar prefetch.
connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this,
[this] { requestVisibleItemResources(); });
[this] { requestAvatarsForVisibleItems(); });
}
// Section dividers can be collapsed/expanded by the user. Surface those
@ -923,7 +915,7 @@ void UserListWidget::showEvent(QShowEvent *e)
if (!userInfoPopup) {
return;
}
requestVisibleItemResources();
requestAvatarsForVisibleItems();
}
void UserListWidget::applyDisplayMode()
@ -1083,10 +1075,6 @@ void UserListWidget::showPopupForUser(UserListTWI *item)
const QString userName = QString::fromStdString(item->getUserInfo().name());
avatarProvider->requestAvatar(userName); // ensure the hovered user's avatar is fetched promptly
if (cardArtParamsMap.contains(userName)) {
const CardArtParams &params = cardArtParamsMap.value(userName);
cardArtProvider->requestCardArt(userName, params.cardName, params.cardProviderId);
}
const ServerInfo_User &info = item->getUserInfo();
const bool online = item->data(0, UserListRoles::Online).toBool();
@ -1268,7 +1256,7 @@ void UserListWidget::endBulkLoad()
bulkLoading = false;
sortItems();
updateCount(); // divider counts were deferred during the bulk build
requestVisibleItemResources();
requestAvatarsForVisibleItems();
userTree->viewport()->update();
}
@ -1281,20 +1269,8 @@ bool UserListWidget::isItemNearViewport(const UserListTWI *item) const
return userTree->visualItemRect(item).intersects(nearView);
}
void UserListWidget::requestVisibleItemResources()
void UserListWidget::requestAvatarsForVisibleItems()
{
const auto requestResources = [this](UserListTWI *twi) {
if (!isItemNearViewport(twi)) {
return;
}
const QString userName = QString::fromStdString(twi->getUserInfo().name());
avatarProvider->requestAvatar(userName);
if (cardArtParamsMap.contains(userName)) {
const CardArtParams &params = cardArtParamsMap.value(userName);
cardArtProvider->requestCardArt(userName, params.cardName, params.cardProviderId);
}
};
if (sectioned) {
// Top level items are dividers, user rows hang below them.
for (const Section section : sectionIds) {
@ -1303,14 +1279,20 @@ void UserListWidget::requestVisibleItemResources()
continue;
}
for (int i = 0; i < divider->childCount(); ++i) {
requestResources(static_cast<UserListTWI *>(divider->child(i)));
auto *twi = static_cast<UserListTWI *>(divider->child(i));
if (isItemNearViewport(twi)) {
avatarProvider->requestAvatar(QString::fromStdString(twi->getUserInfo().name()));
}
}
}
return;
}
for (int i = 0; i < userTree->topLevelItemCount(); ++i) {
requestResources(static_cast<UserListTWI *>(userTree->topLevelItem(i)));
auto *twi = static_cast<UserListTWI *>(userTree->topLevelItem(i));
if (isItemNearViewport(twi)) {
avatarProvider->requestAvatar(QString::fromStdString(twi->getUserInfo().name()));
}
}
}
@ -1541,9 +1523,9 @@ void UserListWidget::updateCount()
}
}
void UserListWidget::setShowTitle(bool _showTitle)
void UserListWidget::setShowTitle(bool showTitle)
{
this->showTitle = _showTitle;
this->showTitle = showTitle;
updateCount();
}
@ -1590,7 +1572,7 @@ void UserListWidget::applyFilter()
}
updateSectionDivider(section);
}
requestVisibleItemResources();
requestAvatarsForVisibleItems();
userTree->viewport()->update();
emit userListChanged();
return;
@ -1608,7 +1590,7 @@ void UserListWidget::applyFilter()
}
}
requestVisibleItemResources();
requestAvatarsForVisibleItems();
userTree->viewport()->update();
emit userListChanged();
}

View file

@ -189,7 +189,7 @@ private:
bool isPressInsideListUi(const QWidget *widget) const;
void clearSelectionAndClosePopup();
bool isItemNearViewport(const UserListTWI *item) const;
void requestVisibleItemResources();
void requestAvatarsForVisibleItems();
// Sectioned mode (single tree with inline dividers)
bool sectioned = false;

View file

@ -1,6 +1,5 @@
#include "card_completer_delegate.h"
#include "../../pixel_map_generator.h"
#include "../cards/additional_info/mana_cost_widget.h"
#include <QFontMetrics>
@ -85,6 +84,7 @@ QColor CardCompleterDelegate::accentForColors(const QString &colors)
CardCompleterDelegate::CardCompleterDelegate(QObject *parent) : QStyledItemDelegate(parent)
{
symbolCache.setMaxCost(64);
setCodeCache.setMaxCost(64);
}
@ -108,16 +108,36 @@ QSize CardCompleterDelegate::sizeHint(const QStyleOptionViewItem &option, const
// Mana symbol painting
// ---------------------------------------------------------------------------
const QPixmap *CardCompleterDelegate::cachedSymbolPixmap(const QString &symbol, int size) const
{
const QString key = symbol + QString::number(size);
if (symbolCache.contains(key)) {
return symbolCache[key];
}
QPixmap src(QString("theme:icons/mana/%1").arg(symbol));
if (!src.isNull()) {
auto *pm = new QPixmap(src.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation));
symbolCache.insert(key, pm);
return pm;
}
return nullptr;
}
// ---------------------------------------------------------------------------
void CardCompleterDelegate::drawManaSymbol(QPainter *p, QPoint centre, const QString &symbol, int radius) const
{
const QRect pip(centre.x() - radius, centre.y() - radius, radius * 2, radius * 2);
const QPixmap px = ManaSymbolPixmapGenerator::generatePixmap(symbol, QSize(radius * 2, radius * 2));
const QPixmap *px = cachedSymbolPixmap(symbol, radius * 2);
if (!px.isNull()) {
p->drawPixmap(pip, px);
if (px && !px->isNull()) {
p->drawPixmap(pip, *px);
return;
}

View file

@ -31,6 +31,9 @@ public:
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
private:
// Mana symbol pixmaps, loaded once and cached
mutable QCache<QString, QPixmap> symbolCache;
// Set short codes, resolved once per card name and cached
mutable QCache<QString, QString> setCodeCache;
@ -44,6 +47,9 @@ private:
// adventure costs ("1W // W") are drawn as separate groups. Returns the left-most x used
int drawManaCost(QPainter *p, const QRect &row, const QString &manaCost, int radius) const;
// Load (or return cached) a mana icon pixmap; falls back to painted circle
const QPixmap *cachedSymbolPixmap(const QString &symbol, int size) const;
// Resolve the preferred printing's set short code for a card
QString setCodeForCard(const QSharedPointer<CardInfo> &card) const;

View file

@ -390,7 +390,6 @@ int main(int argc, char *argv[])
PingPixmapGenerator::clear();
CountryPixmapGenerator::clear();
UserLevelPixmapGenerator::clear();
ManaSymbolPixmapGenerator::clear();
return ret;
}

View file

@ -413,10 +413,6 @@ bool DeckList::loadFromFile_Plain(QIODevice *device, const std::function<QString
bool DeckList::saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards) const
{
auto writeToStream = [&stream, prefixSideboardCards, slashTappedOutSplitCards](const auto node, const auto card) {
// The maybeboard is scratch space and never exported.
if (node->getName() == DECK_ZONE_MAYBEBOARD) {
return;
}
if (prefixSideboardCards && node->getName() == DECK_ZONE_SIDE) {
stream << "SB: ";
}

View file

@ -41,19 +41,13 @@ QList<const DecklistCardNode *> DecklistNodeTree::getCardNodes(const QSet<QStrin
{
QList<const DecklistCardNode *> result;
std::function<void(const InnerDecklistNode *)> collectCards = [&collectCards,
&result](const InnerDecklistNode *node) {
for (int i = 0; i < node->size(); i++) {
if (auto *card = dynamic_cast<const DecklistCardNode *>(node->at(i))) {
result.append(card);
} else if (auto *inner = dynamic_cast<const InnerDecklistNode *>(node->at(i))) {
collectCards(inner);
for (auto *zoneNode : getZoneNodes(restrictToZones)) {
for (auto *cardNode : *zoneNode) {
auto *cardCardNode = dynamic_cast<DecklistCardNode *>(cardNode);
if (cardCardNode) {
result.append(cardCardNode);
}
}
};
for (auto *zoneNode : getZoneNodes(restrictToZones)) {
collectCards(zoneNode);
}
return result;
@ -166,22 +160,13 @@ bool DecklistNodeTree::deleteNode(AbstractDecklistNode *node, InnerDecklistNode
void DecklistNodeTree::forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const
{
// Cards nested in custom zones are reported with their top-level board zone
// so that callers can classify cards by board (main/side/maybeboard/tokens).
std::function<void(InnerDecklistNode *, InnerDecklistNode *)> walk = [&func, &walk](InnerDecklistNode *boardZone,
InnerDecklistNode *node) {
for (int i = 0; i < node->size(); i++) {
if (auto *card = dynamic_cast<DecklistCardNode *>(node->at(i))) {
func(boardZone, card);
} else if (auto *inner = dynamic_cast<InnerDecklistNode *>(node->at(i))) {
walk(boardZone, inner);
}
}
};
// Support for this is only possible if the internal structure
// doesn't get more complicated.
for (int i = 0; i < root->size(); i++) {
if (auto *zone = dynamic_cast<InnerDecklistNode *>(root->at(i))) {
walk(zone, zone);
InnerDecklistNode *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
for (int j = 0; j < node->size(); j++) {
DecklistCardNode *card = dynamic_cast<DecklistCardNode *>(node->at(j));
func(node, card);
}
}
}

View file

@ -78,10 +78,9 @@ public:
bool deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode = nullptr);
/**
* @brief Applies a function to every card in the deck tree. This can modify the cards.
* @brief Apply a function to every card in the deck tree. This can modify the cards.
*
* @param func Function taking (top-level board zone node, card node). Cards nested
* in custom zones are reported with their board zone.
* @param func Function taking (zone node, card node).
*/
void forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const;

View file

@ -28,8 +28,6 @@ QString InnerDecklistNode::visibleNameFromName(const QString &_name)
return QObject::tr("Sideboard");
} else if (_name == DECK_ZONE_TOKENS) {
return QObject::tr("Tokens");
} else if (_name == DECK_ZONE_MAYBEBOARD) {
return QObject::tr("Maybeboard");
} else {
return _name;
}

View file

@ -24,8 +24,6 @@
#define DECK_ZONE_SIDE "side"
/** @brief Constant for the "tokens" zone name. */
#define DECK_ZONE_TOKENS "tokens"
/** @brief Constant for the "maybeboard" zone name. */
#define DECK_ZONE_MAYBEBOARD "maybeboard"
/**
* @class InnerDecklistNode