mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-08-26 02:35:47 -05:00
[Client] Replace playmat crop spinboxes with a direct manipulation preview (#7161)
* [Client] Replace playmat crop spinboxes with a direct manipulation preview The numeric fields exposed raw parameters with no relation to what the game draws, accepted values the renderer clamps away, and no way to see the result before committing The preview now renders through the exact game pipeline into a viewport shaped like a fresh board stack plus table area, with dimmed strips marking where a developed table crops further. Drag pans, wheel zooms, arrow keys nudge, plus and minus zoom, Backspace or Esc restores the crop as of focus gain and lets Esc close the dialog when unchanged. Focus ring and accessible name and description cover keyboard and screen reader users, new paints use palette roles so themes recolor them Took 25 minutes Took 3 minutes Took 54 seconds * Remove stale constant Took 3 minutes Took 26 seconds * Rebase Took 2 minutes Took 3 seconds * Add editor spinboxes again Took 4 minutes --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user