Cockatrice/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp
BruebachL fecbbab983
[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>
2026-08-24 18:22:48 +02:00

324 lines
11 KiB
C++

#include "playmat_settings_dialog.h"
#include "../../card_picture_loader/card_picture_loader.h"
#include "../cards/art_crop_attribution.h"
#include "../utility/completer_utils.h"
#include "card_database_display_model.h"
#include "card_database_model.h"
#include "playmat_preview_widget.h"
#include <QCheckBox>
#include <QComboBox>
#include <QCompleter>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPainter>
#include <QPainterPath>
#include <QPushButton>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
PlaymatSettingsDialog::PlaymatSettingsDialog(const CardRef &initialCard,
const PlaymatParams &initialParams,
QWidget *parent)
: QDialog(parent), currentCard(initialCard), currentParams(initialParams)
{
setMinimumWidth(500);
setupUi();
// Seed UI from initial values
if (!initialCard.name.isEmpty()) {
searchBar->setText(initialCard.name);
onCardNameChanged(initialCard.name);
// onCardNameChanged leaves the printing combo on the first printing in
// the database, which would silently change the deck's stored playmat
// card on accept. Restore the stored printing when it resolves locally.
const int storedPrintingIndex = providerComboBox->findData(initialCard.providerId);
if (storedPrintingIndex != -1) {
providerComboBox->setCurrentIndex(storedPrintingIndex);
} else {
// Stored printing not in the local database: keep it rather than
// silently substituting the first printing.
currentCard.providerId = initialCard.providerId;
reloadPreview();
}
}
retranslateUi();
}
CardRef PlaymatSettingsDialog::card() const
{
return currentCard;
}
PlaymatParams PlaymatSettingsDialog::params() const
{
return currentParams;
}
QDoubleSpinBox *PlaymatSettingsDialog::makeSpinBox(double min, double max, double value, double step)
{
auto *spin = new QDoubleSpinBox;
spin->setRange(min, max);
spin->setSingleStep(step);
spin->setDecimals(3);
spin->setValue(value);
return spin;
}
void PlaymatSettingsDialog::initializeSearchBar()
{
searchBar = new QLineEdit;
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this);
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15);
searchModel = cardSetup.searchModel;
proxyModel = cardSetup.proxyModel;
completer = cardSetup.completer;
searchBar->setCompleter(completer);
connectCardCompleterSearch(searchBar, cardSetup);
connect(completer, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
[this](const QString &completion) {
if (searchBar->text() != completion) {
searchBar->setText(completion);
searchBar->setCursorPosition(searchBar->text().length());
}
onCardNameChanged(completion);
});
connect(searchBar, &QLineEdit::returnPressed, this, [this]() { onCardNameChanged(searchBar->text()); });
}
void PlaymatSettingsDialog::setupUi()
{
initializeSearchBar();
providerComboBox = new QComboBox;
connect(providerComboBox, &QComboBox::currentIndexChanged, this, [this]() {
currentCard.providerId = providerComboBox->currentData().toString();
reloadPreview();
});
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);
leftMarginLabel = new QLabel;
rightMarginLabel = new QLabel;
verticalOffsetLabel = new QLabel;
zoomLabel = new QLabel;
showNumericEditorsCheck = new QCheckBox;
form->addRow(showNumericEditorsCheck);
form->addRow(leftMarginLabel, marginLSpin);
form->addRow(rightMarginLabel, marginRSpin);
form->addRow(verticalOffsetLabel, verticalOffsetSpin);
form->addRow(zoomLabel, zoomSpin);
controlsGroup = new QGroupBox;
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);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
removeButton = new QPushButton;
buttons->addButton(removeButton, QDialogButtonBox::ResetRole);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(removeButton, &QPushButton::clicked, this, [this]() {
currentCard = CardRef{}; // empty signals removal
accept();
});
// The crop surface is the primary control: dragging pans, wheel/keys zoom,
// editing exactly the same stored parameters the numeric fields do.
connect(preview, &PlaymatPreviewWidget::paramsEdited, this, [this](const PlaymatParams &edited) {
currentParams = edited;
QSignalBlocker blockMarginL(marginLSpin);
QSignalBlocker blockMarginR(marginRSpin);
QSignalBlocker blockOffset(verticalOffsetSpin);
QSignalBlocker blockZoom(zoomSpin);
marginLSpin->setValue(edited.marginPctL);
marginRSpin->setValue(edited.marginPctR);
verticalOffsetSpin->setValue(edited.verticalOffset);
zoomSpin->setValue(edited.zoom);
});
connect(showNumericEditorsCheck, &QCheckBox::toggled, this, &PlaymatSettingsDialog::setNumericEditorsVisible);
setNumericEditorsVisible(false);
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)
{
providerComboBox->clear();
auto card = CardDatabaseManager::query()->getCard({cardName});
const auto &sets = card.getInfo().getSets();
for (const auto &printings : sets) {
for (const auto &p : printings) {
QString setName = p.getSet()->getLongName();
QString collector = p.getProperty("num");
QString uuid = p.getUuid();
QString label = setName;
if (!collector.isEmpty()) {
label += " #" + collector;
}
providerComboBox->addItem(label, uuid);
}
}
}
void PlaymatSettingsDialog::onCardNameChanged(const QString &name)
{
if (name.isEmpty()) {
currentPixmap = QPixmap();
preview->setPixmap(currentPixmap);
return;
}
const ExactCard card = CardDatabaseManager::query()->getCard({name});
if (!card) {
currentPixmap = QPixmap();
preview->setPixmap(currentPixmap);
providerComboBox->clear();
return;
}
currentCard.name = name;
populateProviderCombo(name);
if (providerComboBox->count() == 0) {
currentPixmap = QPixmap();
preview->setPixmap(currentPixmap);
currentCard.providerId.clear();
return;
}
currentCard.providerId = providerComboBox->currentData().toString();
reloadPreview();
}
void PlaymatSettingsDialog::reloadPreview()
{
if (currentCard.name.isEmpty()) {
return;
}
ExactCard card = CardDatabaseManager::query()->getCard({currentCard.name, currentCard.providerId});
if (!card) {
return;
}
disconnect(pixmapUpdatedConnection);
QPixmap fullRes;
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
if (fullRes.isNull()) {
CardInfo *cardInfo = card.getCardPtr().data();
if (cardInfo) {
pixmapUpdatedConnection = connect(cardInfo, &CardInfo::pixmapUpdated, this, [this]() { reloadPreview(); });
}
return;
}
currentPixmap = fullRes;
preview->setPixmap(currentPixmap);
preview->setParams(currentParams);
preview->setAttribution(buildArtAttribution(card));
}
void PlaymatSettingsDialog::onParamChanged()
{
currentParams.marginPctL = marginLSpin->value();
currentParams.marginPctR = marginRSpin->value();
currentParams.verticalOffset = verticalOffsetSpin->value();
currentParams.zoom = zoomSpin->value();
preview->setParams(currentParams);
}
void PlaymatSettingsDialog::setNumericEditorsVisible(bool visible)
{
controlsForm->setRowVisible(leftMarginLabel, visible);
controlsForm->setRowVisible(rightMarginLabel, visible);
controlsForm->setRowVisible(verticalOffsetLabel, visible);
controlsForm->setRowVisible(zoomLabel, visible);
// A QDialog never resizes itself when its content requirements change,
// so revealing the editors would squeeze the crop group until the info
// caption ran into the preview. Re-fit the dialog to the new size hint.
adjustSize();
}
void PlaymatSettingsDialog::retranslateUi()
{
setWindowTitle(tr("Playmat Settings"));
searchBar->setPlaceholderText(tr("Type a card name..."));
cardNameLabel->setText(tr("Card name:"));
printingLabel->setText(tr("Printing:"));
showNumericEditorsCheck->setText(tr("Show numerical editors"));
leftMarginLabel->setText(tr("Left margin (%):"));
rightMarginLabel->setText(tr("Right margin (%):"));
verticalOffsetLabel->setText(tr("Vertical offset:"));
zoomLabel->setText(tr("Zoom:"));
controlsGroup->setTitle(tr("Card"));
previewGroup->setTitle(tr("Crop"));
previewCaptionLabel->setText(
tr("Drag to pan, scroll to zoom, arrow keys nudge, plus and minus zoom, Backspace or Esc restores. "
"Dimmed strips mark where a wider table crops further."));
removeButton->setText(tr("Remove Playmat"));
}