mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-24 02:13:02 -07:00
Compare commits
6 commits
c3599be89b
...
24d8d8be3b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24d8d8be3b | ||
|
|
b13c682a7a | ||
|
|
22b0f69706 | ||
|
|
fecbbab983 | ||
|
|
0b0ec64fe7 | ||
|
|
0d0488cb5b |
24 changed files with 905 additions and 160 deletions
|
|
@ -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()) {
|
||||
if (dbCard && !dbCard->getIsToken() && node->getName() != DECK_ZONE_MAYBEBOARD) {
|
||||
DecklistCardNode *addedCard = destination.addCard(card->getName(), node->getName(), -1);
|
||||
addedCard->setNumber(card->getNumber());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
if (!dbCard || dbCard->getIsToken() || node->getName() == DECK_ZONE_MAYBEBOARD) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <QApplication>
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
#include <QImageReader>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QSvgRenderer>
|
||||
|
|
@ -418,6 +419,57 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#ifndef PIXMAPGENERATOR_H
|
||||
#define PIXMAPGENERATOR_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QIcon>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
|
|
@ -125,6 +126,34 @@ 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
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ 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) {
|
||||
|
|
@ -73,21 +78,35 @@ 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;
|
||||
}
|
||||
|
||||
if (!manaSymbols.isEmpty()) {
|
||||
int totalWidth = event->size().width();
|
||||
int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
|
||||
setFixedHeight(totalHeight);
|
||||
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
|
||||
|
||||
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
|
||||
if (iconSize == lastIconSize) {
|
||||
return;
|
||||
}
|
||||
lastIconSize = iconSize;
|
||||
|
||||
for (ManaSymbolWidget *manaSymbol : manaSymbols) {
|
||||
manaSymbol->setFixedSize(iconSize, iconSize);
|
||||
}
|
||||
for (ManaSymbolWidget *manaSymbol : manaSymbols) {
|
||||
manaSymbol->setFixedSize(iconSize, iconSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ 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
|
||||
|
|
|
|||
|
|
@ -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(_symbol), isActive(_isActive), mayBeToggled(_mayBeToggled)
|
||||
: QLabel(parent), symbol(std::move(_symbol)), isActive(_isActive), mayBeToggled(_mayBeToggled)
|
||||
{
|
||||
loadManaIcon();
|
||||
setPixmap(manaIcon.scaled(50, 50, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
setPixmap(ManaSymbolPixmapGenerator::generatePixmap(symbol, QSize(50, 50)));
|
||||
setMaximumWidth(50);
|
||||
|
||||
// Initialize opacity effect
|
||||
|
|
@ -64,16 +64,13 @@ void ManaSymbolWidget::mousePressEvent(QMouseEvent *event)
|
|||
void ManaSymbolWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QLabel::resizeEvent(event);
|
||||
setPixmap(manaIcon.scaled(event->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
const QSize newSize = event->size();
|
||||
|
||||
void ManaSymbolWidget::loadManaIcon()
|
||||
{
|
||||
QString filename = "theme:icons/mana/";
|
||||
|
||||
if (symbol == "W" || symbol == "U" || symbol == "B" || symbol == "R" || symbol == "G") {
|
||||
filename += symbol;
|
||||
// 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;
|
||||
}
|
||||
|
||||
manaIcon = QPixmap(filename);
|
||||
setPixmap(ManaSymbolPixmapGenerator::generatePixmap(symbol, newSize));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,6 @@ public:
|
|||
return symbol[0];
|
||||
}
|
||||
|
||||
void loadManaIcon();
|
||||
|
||||
public slots:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
|
|
@ -44,7 +42,6 @@ signals:
|
|||
|
||||
private:
|
||||
QString symbol;
|
||||
QPixmap manaIcon;
|
||||
bool isActive;
|
||||
bool mayBeToggled;
|
||||
QGraphicsOpacityEffect *opacityEffect;
|
||||
|
|
|
|||
|
|
@ -3,25 +3,63 @@
|
|||
#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)
|
||||
{
|
||||
setMinimumSize(400, 120);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
// 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)));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -31,6 +69,94 @@ 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);
|
||||
|
|
@ -56,23 +182,40 @@ void PlaymatPreviewWidget::paintEvent(QPaintEvent *)
|
|||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
const QRectF playArea = activePlayArea();
|
||||
|
||||
// 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);
|
||||
painter.setClipping(false);
|
||||
|
||||
// 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));
|
||||
|
||||
// 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));
|
||||
|
|
@ -89,11 +232,150 @@ 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 entire play area
|
||||
// Border around the viewport = boundary of every plausible framing.
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,23 @@
|
|||
#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 Preview widget that shows how a playmat card art will appear
|
||||
* @brief Interactive crop surface showing how a playmat card art will appear
|
||||
* across the combined table + stack play area.
|
||||
*
|
||||
* Renders a miniature mockup with the card art applied using the
|
||||
* given PlaymatParams, including faint zone divider lines.
|
||||
* 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.
|
||||
*/
|
||||
class PlaymatPreviewWidget : public QWidget
|
||||
{
|
||||
|
|
@ -23,13 +30,32 @@ 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
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "card_database_model.h"
|
||||
#include "playmat_preview_widget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QCompleter>
|
||||
#include <QDialogButtonBox>
|
||||
|
|
@ -48,10 +49,6 @@ PlaymatSettingsDialog::PlaymatSettingsDialog(const CardRef &initialCard,
|
|||
reloadPreview();
|
||||
}
|
||||
}
|
||||
marginLSpin->setValue(initialParams.marginPctL);
|
||||
marginRSpin->setValue(initialParams.marginPctR);
|
||||
verticalOffsetSpin->setValue(initialParams.verticalOffset);
|
||||
zoomSpin->setValue(initialParams.zoom);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
|
@ -112,23 +109,32 @@ 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;
|
||||
form->addRow(cardNameLabel, searchBar);
|
||||
form->addRow(printingLabel, providerComboBox);
|
||||
|
||||
showNumericEditorsCheck = new QCheckBox;
|
||||
|
||||
form->addRow(showNumericEditorsCheck);
|
||||
form->addRow(leftMarginLabel, marginLSpin);
|
||||
form->addRow(rightMarginLabel, marginRSpin);
|
||||
form->addRow(verticalOffsetLabel, verticalOffsetSpin);
|
||||
|
|
@ -138,9 +144,14 @@ 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);
|
||||
|
||||
|
|
@ -155,16 +166,35 @@ void PlaymatSettingsDialog::setupUi()
|
|||
accept();
|
||||
});
|
||||
|
||||
auto *root = new QVBoxLayout;
|
||||
root->addWidget(controlsGroup);
|
||||
root->addWidget(previewGroup);
|
||||
root->addWidget(buttons);
|
||||
setLayout(root);
|
||||
// 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);
|
||||
|
||||
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)
|
||||
|
|
@ -261,17 +291,34 @@ 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("Parameters"));
|
||||
previewGroup->setTitle(tr("Preview"));
|
||||
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."));
|
||||
removeButton->setText(tr("Remove Playmat"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,16 @@
|
|||
#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;
|
||||
|
|
@ -21,10 +24,11 @@ class PlaymatPreviewWidget;
|
|||
/**
|
||||
* @brief Dialog for configuring the playmat card art for a deck.
|
||||
*
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
class PlaymatSettingsDialog : public QDialog
|
||||
{
|
||||
|
|
@ -40,14 +44,15 @@ public:
|
|||
|
||||
private slots:
|
||||
void onCardNameChanged(const QString &name);
|
||||
void reloadPreview();
|
||||
void onParamChanged();
|
||||
void reloadPreview();
|
||||
|
||||
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;
|
||||
|
|
@ -63,6 +68,9 @@ private:
|
|||
|
||||
QLabel *cardNameLabel;
|
||||
QLabel *printingLabel;
|
||||
QLabel *previewCaptionLabel;
|
||||
QCheckBox *showNumericEditorsCheck;
|
||||
QFormLayout *controlsForm;
|
||||
QLabel *leftMarginLabel;
|
||||
QLabel *rightMarginLabel;
|
||||
QLabel *verticalOffsetLabel;
|
||||
|
|
@ -75,6 +83,7 @@ private:
|
|||
QDoubleSpinBox *marginRSpin;
|
||||
QDoubleSpinBox *verticalOffsetSpin;
|
||||
QDoubleSpinBox *zoomSpin;
|
||||
|
||||
PlaymatPreviewWidget *preview;
|
||||
|
||||
QPixmap currentPixmap;
|
||||
|
|
|
|||
|
|
@ -15,19 +15,46 @@
|
|||
#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)
|
||||
|
|
@ -39,6 +66,7 @@ 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();
|
||||
}
|
||||
|
||||
|
|
@ -67,9 +95,22 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -80,7 +121,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);
|
||||
|
|
@ -99,37 +140,200 @@ 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)
|
||||
{
|
||||
setWindowTitle(tr("Card Art Settings"));
|
||||
// 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);
|
||||
|
||||
setMinimumWidth(500);
|
||||
setupUi();
|
||||
|
||||
// Seed UI from initial params
|
||||
if (!initial.cardName.isEmpty()) {
|
||||
searchBar->setText(initial.cardName);
|
||||
onCardNameChanged(initial.cardName);
|
||||
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);
|
||||
|
||||
// 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(initial.cardProviderId);
|
||||
const int storedPrintingIndex = providerComboBox->findData(storedProviderId);
|
||||
if (storedPrintingIndex != -1) {
|
||||
providerComboBox->setCurrentIndex(storedPrintingIndex);
|
||||
} else {
|
||||
} else if (!storedProviderId.isEmpty()) {
|
||||
// Stored printing not in the local database: keep it rather than
|
||||
// silently substituting the first printing.
|
||||
currentParams.cardProviderId = initial.cardProviderId;
|
||||
currentParams.cardProviderId = storedProviderId;
|
||||
reloadPreview();
|
||||
}
|
||||
}
|
||||
marginLSpin->setValue(initial.marginPctL);
|
||||
marginRSpin->setValue(initial.marginPctR);
|
||||
verticalOffsetSpin->setValue(initial.verticalOffset);
|
||||
zoomSpin->setValue(initial.zoom);
|
||||
marginLSpin->setValue(currentParams.marginPctL);
|
||||
marginRSpin->setValue(currentParams.marginPctR);
|
||||
}
|
||||
|
||||
CardArtParams UserCardArtSettingsDialog::params() const
|
||||
|
|
@ -150,7 +354,6 @@ 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);
|
||||
|
|
@ -190,29 +393,33 @@ 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;
|
||||
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);
|
||||
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);
|
||||
|
||||
auto *controlsGroup = new QGroupBox(tr("Parameters"));
|
||||
controlsGroup = new QGroupBox;
|
||||
controlsGroup->setLayout(form);
|
||||
|
||||
preview = new CardArtPreviewWidget;
|
||||
|
||||
auto *previewLayout = new QVBoxLayout;
|
||||
previewLayout->addWidget(preview);
|
||||
auto *previewGroup = new QGroupBox(tr("Preview"));
|
||||
previewCaptionLabel = new QLabel;
|
||||
previewCaptionLabel->setAlignment(Qt::AlignCenter);
|
||||
previewCaptionLabel->setWordWrap(true);
|
||||
previewLayout->addWidget(previewCaptionLabel);
|
||||
previewGroup = new QGroupBox;
|
||||
previewGroup->setLayout(previewLayout);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
auto *removeBtn = new QPushButton(tr("Remove Banner Card"));
|
||||
removeBtn = new QPushButton;
|
||||
buttons->addButton(removeBtn, QDialogButtonBox::ResetRole);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
|
|
@ -222,16 +429,38 @@ void UserCardArtSettingsDialog::setupUi()
|
|||
accept();
|
||||
});
|
||||
|
||||
// The banner leads visually, card selection and margins support it below.
|
||||
auto *root = new QVBoxLayout;
|
||||
root->addWidget(controlsGroup);
|
||||
root->addWidget(previewGroup);
|
||||
root->addWidget(controlsGroup);
|
||||
root->addWidget(buttons);
|
||||
setLayout(root);
|
||||
|
||||
connect(marginLSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
|
||||
connect(marginRSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
|
||||
connect(verticalOffsetSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
|
||||
connect(zoomSpin, &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"));
|
||||
}
|
||||
|
||||
void UserCardArtSettingsDialog::populateProviderCombo(const QString &cardName)
|
||||
|
|
@ -281,7 +510,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();
|
||||
|
|
@ -311,7 +540,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.
|
||||
|
|
@ -321,8 +550,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) {
|
||||
|
|
@ -345,7 +574,5 @@ void UserCardArtSettingsDialog::onParamChanged()
|
|||
{
|
||||
currentParams.marginPctL = marginLSpin->value();
|
||||
currentParams.marginPctR = marginRSpin->value();
|
||||
currentParams.verticalOffset = verticalOffsetSpin->value();
|
||||
currentParams.zoom = zoomSpin->value();
|
||||
preview->setParams(currentParams);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,29 @@
|
|||
#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
|
||||
|
|
@ -26,13 +42,31 @@ 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
|
||||
|
|
@ -51,6 +85,7 @@ private slots:
|
|||
|
||||
private:
|
||||
void setupUi();
|
||||
void retranslateUi();
|
||||
void populateProviderCombo(const QString &cardName);
|
||||
void initializeSearchBar();
|
||||
QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step);
|
||||
|
|
@ -66,14 +101,20 @@ 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
|
||||
|
|
|
|||
|
|
@ -718,11 +718,19 @@ 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);
|
||||
requestAvatarsForVisibleItems();
|
||||
requestVisibleItemResources();
|
||||
});
|
||||
|
||||
// Forward join requests from popup upward
|
||||
|
|
@ -738,7 +746,7 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
|||
|
||||
// Keep the popup-less scroll path alive for avatar prefetch.
|
||||
connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this,
|
||||
[this] { requestAvatarsForVisibleItems(); });
|
||||
[this] { requestVisibleItemResources(); });
|
||||
}
|
||||
|
||||
// Section dividers can be collapsed/expanded by the user. Surface those
|
||||
|
|
@ -915,7 +923,7 @@ void UserListWidget::showEvent(QShowEvent *e)
|
|||
if (!userInfoPopup) {
|
||||
return;
|
||||
}
|
||||
requestAvatarsForVisibleItems();
|
||||
requestVisibleItemResources();
|
||||
}
|
||||
|
||||
void UserListWidget::applyDisplayMode()
|
||||
|
|
@ -1075,6 +1083,10 @@ 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();
|
||||
|
|
@ -1256,7 +1268,7 @@ void UserListWidget::endBulkLoad()
|
|||
bulkLoading = false;
|
||||
sortItems();
|
||||
updateCount(); // divider counts were deferred during the bulk build
|
||||
requestAvatarsForVisibleItems();
|
||||
requestVisibleItemResources();
|
||||
userTree->viewport()->update();
|
||||
}
|
||||
|
||||
|
|
@ -1269,8 +1281,20 @@ bool UserListWidget::isItemNearViewport(const UserListTWI *item) const
|
|||
return userTree->visualItemRect(item).intersects(nearView);
|
||||
}
|
||||
|
||||
void UserListWidget::requestAvatarsForVisibleItems()
|
||||
void UserListWidget::requestVisibleItemResources()
|
||||
{
|
||||
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) {
|
||||
|
|
@ -1279,20 +1303,14 @@ void UserListWidget::requestAvatarsForVisibleItems()
|
|||
continue;
|
||||
}
|
||||
for (int i = 0; i < divider->childCount(); ++i) {
|
||||
auto *twi = static_cast<UserListTWI *>(divider->child(i));
|
||||
if (isItemNearViewport(twi)) {
|
||||
avatarProvider->requestAvatar(QString::fromStdString(twi->getUserInfo().name()));
|
||||
}
|
||||
requestResources(static_cast<UserListTWI *>(divider->child(i)));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < userTree->topLevelItemCount(); ++i) {
|
||||
auto *twi = static_cast<UserListTWI *>(userTree->topLevelItem(i));
|
||||
if (isItemNearViewport(twi)) {
|
||||
avatarProvider->requestAvatar(QString::fromStdString(twi->getUserInfo().name()));
|
||||
}
|
||||
requestResources(static_cast<UserListTWI *>(userTree->topLevelItem(i)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1523,9 +1541,9 @@ void UserListWidget::updateCount()
|
|||
}
|
||||
}
|
||||
|
||||
void UserListWidget::setShowTitle(bool showTitle)
|
||||
void UserListWidget::setShowTitle(bool _showTitle)
|
||||
{
|
||||
this->showTitle = showTitle;
|
||||
this->showTitle = _showTitle;
|
||||
updateCount();
|
||||
}
|
||||
|
||||
|
|
@ -1572,7 +1590,7 @@ void UserListWidget::applyFilter()
|
|||
}
|
||||
updateSectionDivider(section);
|
||||
}
|
||||
requestAvatarsForVisibleItems();
|
||||
requestVisibleItemResources();
|
||||
userTree->viewport()->update();
|
||||
emit userListChanged();
|
||||
return;
|
||||
|
|
@ -1590,7 +1608,7 @@ void UserListWidget::applyFilter()
|
|||
}
|
||||
}
|
||||
|
||||
requestAvatarsForVisibleItems();
|
||||
requestVisibleItemResources();
|
||||
userTree->viewport()->update();
|
||||
emit userListChanged();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ private:
|
|||
bool isPressInsideListUi(const QWidget *widget) const;
|
||||
void clearSelectionAndClosePopup();
|
||||
bool isItemNearViewport(const UserListTWI *item) const;
|
||||
void requestAvatarsForVisibleItems();
|
||||
void requestVisibleItemResources();
|
||||
|
||||
// Sectioned mode (single tree with inline dividers)
|
||||
bool sectioned = false;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "card_completer_delegate.h"
|
||||
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../cards/additional_info/mana_cost_widget.h"
|
||||
|
||||
#include <QFontMetrics>
|
||||
|
|
@ -84,7 +85,6 @@ QColor CardCompleterDelegate::accentForColors(const QString &colors)
|
|||
|
||||
CardCompleterDelegate::CardCompleterDelegate(QObject *parent) : QStyledItemDelegate(parent)
|
||||
{
|
||||
symbolCache.setMaxCost(64);
|
||||
setCodeCache.setMaxCost(64);
|
||||
}
|
||||
|
||||
|
|
@ -108,36 +108,16 @@ 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 = cachedSymbolPixmap(symbol, radius * 2);
|
||||
const QPixmap px = ManaSymbolPixmapGenerator::generatePixmap(symbol, QSize(radius * 2, radius * 2));
|
||||
|
||||
if (px && !px->isNull()) {
|
||||
p->drawPixmap(pip, *px);
|
||||
if (!px.isNull()) {
|
||||
p->drawPixmap(pip, px);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,9 +31,6 @@ 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;
|
||||
|
||||
|
|
@ -47,9 +44,6 @@ 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -390,6 +390,7 @@ int main(int argc, char *argv[])
|
|||
PingPixmapGenerator::clear();
|
||||
CountryPixmapGenerator::clear();
|
||||
UserLevelPixmapGenerator::clear();
|
||||
ManaSymbolPixmapGenerator::clear();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -413,6 +413,10 @@ 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: ";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,13 +41,19 @@ QList<const DecklistCardNode *> DecklistNodeTree::getCardNodes(const QSet<QStrin
|
|||
{
|
||||
QList<const DecklistCardNode *> result;
|
||||
|
||||
for (auto *zoneNode : getZoneNodes(restrictToZones)) {
|
||||
for (auto *cardNode : *zoneNode) {
|
||||
auto *cardCardNode = dynamic_cast<DecklistCardNode *>(cardNode);
|
||||
if (cardCardNode) {
|
||||
result.append(cardCardNode);
|
||||
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)) {
|
||||
collectCards(zoneNode);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -160,13 +166,22 @@ bool DecklistNodeTree::deleteNode(AbstractDecklistNode *node, InnerDecklistNode
|
|||
|
||||
void DecklistNodeTree::forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const
|
||||
{
|
||||
// Support for this is only possible if the internal structure
|
||||
// doesn't get more complicated.
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (int i = 0; i < root->size(); i++) {
|
||||
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);
|
||||
if (auto *zone = dynamic_cast<InnerDecklistNode *>(root->at(i))) {
|
||||
walk(zone, zone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,9 +78,10 @@ public:
|
|||
bool deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Apply a function to every card in the deck tree. This can modify the cards.
|
||||
* @brief Applies a function to every card in the deck tree. This can modify the cards.
|
||||
*
|
||||
* @param func Function taking (zone node, card node).
|
||||
* @param func Function taking (top-level board zone node, card node). Cards nested
|
||||
* in custom zones are reported with their board zone.
|
||||
*/
|
||||
void forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const;
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@
|
|||
#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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue