diff --git a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp index 42292b2aa..8689a19e9 100644 --- a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp +++ b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp @@ -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()); } diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp index 627b7fe34..5dc77fa2c 100644 --- a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp +++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp @@ -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; } diff --git a/cockatrice/src/interface/pixel_map_generator.cpp b/cockatrice/src/interface/pixel_map_generator.cpp index 9b8c4bcdc..5bfba1c8a 100644 --- a/cockatrice/src/interface/pixel_map_generator.cpp +++ b/cockatrice/src/interface/pixel_map_generator.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -419,57 +418,6 @@ QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded) QMap 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 ManaSymbolPixmapGenerator::masterCache; -QHash ManaSymbolPixmapGenerator::scaledCache; - QPixmap loadColorAdjustedPixmap(const QString &name) { if (qApp->palette().windowText().color().lightness() > 200) { diff --git a/cockatrice/src/interface/pixel_map_generator.h b/cockatrice/src/interface/pixel_map_generator.h index 17720166a..22f44d8db 100644 --- a/cockatrice/src/interface/pixel_map_generator.h +++ b/cockatrice/src/interface/pixel_map_generator.h @@ -7,7 +7,6 @@ #ifndef PIXMAPGENERATOR_H #define PIXMAPGENERATOR_H -#include #include #include #include @@ -126,34 +125,6 @@ public: } }; -class ManaSymbolPixmapGenerator -{ -private: - static QHash masterCache; - static QHash 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 diff --git a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp index a4cb86751..3f0f30a27 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp @@ -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 manaSymbols = findChildren(); - 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); + } } } diff --git a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.h b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.h index 315ac07d6..f776d4c77 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.h +++ b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.h @@ -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 diff --git a/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp b/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp index 18011909f..51247da7a 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp @@ -1,15 +1,15 @@ #include "mana_symbol_widget.h" #include "../../../../client/settings/cache_settings.h" -#include "../../../pixel_map_generator.h" #include #include 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); } diff --git a/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.h b/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.h index 705873dff..0f2d7acd1 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.h +++ b/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.h @@ -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; diff --git a/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.cpp b/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.cpp index 130daadf4..52f21f714 100644 --- a/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.cpp +++ b/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.cpp @@ -3,63 +3,25 @@ #include "../cards/art_crop_attribution.h" #include "playmat_utils.h" -#include #include -#include #include #include -#include -#include - -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(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(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); } diff --git a/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.h b/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.h index 88b9ec41d..55771192f 100644 --- a/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.h +++ b/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.h @@ -1,23 +1,16 @@ #ifndef COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H #define COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H -#include #include #include #include /** - * @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 ¶ms); void setAttribution(const QString &attribution); -signals: - /** @brief Emitted whenever direct manipulation (drag, wheel, keys) changes the crop parameters. */ - void paramsEdited(const PlaymatParams ¶ms); - 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 diff --git a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp index 57706cf93..72c715e13 100644 --- a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp +++ b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp @@ -7,7 +7,6 @@ #include "card_database_model.h" #include "playmat_preview_widget.h" -#include #include #include #include @@ -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")); } diff --git a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.h b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.h index 9ef306a5f..7ccd1569d 100644 --- a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.h +++ b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.h @@ -5,16 +5,13 @@ #include #include -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; diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp index 532112964..1d76b2c67 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp @@ -15,46 +15,19 @@ #include #include #include -#include #include #include -#include #include #include #include #include #include -#include -#include #include -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(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), + // Not loaded yet -- wait for the signal instead of giving up. + // card.getCardPtr() is a CardInfoPtr (QSharedPointer); // .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); -} +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h index 24e14dce5..397d2b26a 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h @@ -8,29 +8,13 @@ #include 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 ¶ms); void setAttribution(const QString &attribution); -signals: - /** @brief Emitted whenever direct manipulation (drag, wheel, keys) changes the parameters. */ - void paramsEdited(const CardArtParams ¶ms); - 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 \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index c4b5d6af6..e71eac23b 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -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 ¶ms = 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 ¶ms = 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(divider->child(i))); + auto *twi = static_cast(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(userTree->topLevelItem(i))); + auto *twi = static_cast(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(); } diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.h b/cockatrice/src/interface/widgets/server/user/user_list_widget.h index 5fed54573..e7a5116ef 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.h @@ -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; diff --git a/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp b/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp index 29bf2263e..6cf096cd4 100644 --- a/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp +++ b/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp @@ -1,6 +1,5 @@ #include "card_completer_delegate.h" -#include "../../pixel_map_generator.h" #include "../cards/additional_info/mana_cost_widget.h" #include @@ -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; } diff --git a/cockatrice/src/interface/widgets/utility/card_completer_delegate.h b/cockatrice/src/interface/widgets/utility/card_completer_delegate.h index 78667f56b..18661a21b 100644 --- a/cockatrice/src/interface/widgets/utility/card_completer_delegate.h +++ b/cockatrice/src/interface/widgets/utility/card_completer_delegate.h @@ -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 symbolCache; + // Set short codes, resolved once per card name and cached mutable QCache 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 &card) const; diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 84d5d175f..814da9808 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -390,7 +390,6 @@ int main(int argc, char *argv[]) PingPixmapGenerator::clear(); CountryPixmapGenerator::clear(); UserLevelPixmapGenerator::clear(); - ManaSymbolPixmapGenerator::clear(); return ret; } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp index 1a3876cd3..4ffc1bab7 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -413,10 +413,6 @@ bool DeckList::loadFromFile_Plain(QIODevice *device, const std::functiongetName() == DECK_ZONE_MAYBEBOARD) { - return; - } if (prefixSideboardCards && node->getName() == DECK_ZONE_SIDE) { stream << "SB: "; } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp index efe20595b..196416cde 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp @@ -41,19 +41,13 @@ QList DecklistNodeTree::getCardNodes(const QSet result; - std::function collectCards = [&collectCards, - &result](const InnerDecklistNode *node) { - for (int i = 0; i < node->size(); i++) { - if (auto *card = dynamic_cast(node->at(i))) { - result.append(card); - } else if (auto *inner = dynamic_cast(node->at(i))) { - collectCards(inner); + for (auto *zoneNode : getZoneNodes(restrictToZones)) { + for (auto *cardNode : *zoneNode) { + auto *cardCardNode = dynamic_cast(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 &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 walk = [&func, &walk](InnerDecklistNode *boardZone, - InnerDecklistNode *node) { - for (int i = 0; i < node->size(); i++) { - if (auto *card = dynamic_cast(node->at(i))) { - func(boardZone, card); - } else if (auto *inner = dynamic_cast(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(root->at(i))) { - walk(zone, zone); + InnerDecklistNode *node = dynamic_cast(root->at(i)); + for (int j = 0; j < node->size(); j++) { + DecklistCardNode *card = dynamic_cast(node->at(j)); + func(node, card); } } } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h index eae20aa23..8ef0b18a5 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h @@ -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 &func) const; diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp index 1f470695d..602ea6aec 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp @@ -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; } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h index f8fdedf30..81c37dffa 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h @@ -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