From 74a454552a0c02dfb738d25d99501ea6d553f583 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Fri, 21 Aug 2026 02:54:36 +0200 Subject: [PATCH] [Cards] Artist attribution (#7092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Cards] Artist attribution Took 7 minutes Took 4 minutes * Nudge attribution pill on home screen to align Took 3 minutes Took 28 seconds Took 38 seconds * Lint. Took 3 minutes * Lint. Took 2 minutes * Fix rebase whoopsie Took 5 minutes --------- Co-authored-by: Lukas Brübach --- cockatrice/CMakeLists.txt | 1 + .../widgets/cards/art_crop_attribution.cpp | 61 +++++++++++++++++++ .../widgets/cards/art_crop_attribution.h | 41 +++++++++++++ .../interface/widgets/general/home_widget.cpp | 40 +++++++++--- .../server/user/user_card_settings_dialog.cpp | 16 +++++ .../server/user/user_card_settings_dialog.h | 2 + .../widgets/server/user/user_info_popup.cpp | 23 +++++++ .../widgets/server/user/user_info_popup.h | 1 + .../widgets/server/user/user_list_widget.cpp | 1 + .../card/printing/printing_info.h | 12 ++++ oracle/src/oracleimporter.cpp | 7 ++- 11 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp create mode 100644 cockatrice/src/interface/widgets/cards/art_crop_attribution.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 56f5b89f9..a5f9f6747 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -160,6 +160,7 @@ set(cockatrice_SOURCES src/interface/widgets/cards/additional_info/color_identity_widget.cpp src/interface/widgets/cards/additional_info/mana_cost_widget.cpp src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp + src/interface/widgets/cards/art_crop_attribution.cpp src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp diff --git a/cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp b/cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp new file mode 100644 index 000000000..8ab18ddd9 --- /dev/null +++ b/cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp @@ -0,0 +1,61 @@ +#include "art_crop_attribution.h" + +#include +#include +#include +#include + +QString buildArtAttribution(const ExactCard &card) +{ + const QString artist = card.getPrinting().getArtist(); + if (artist.isEmpty()) { + return QString(); + } + return QObject::tr("Art: %1").arg(artist); +} + +QRectF paintArtAttribution(QPainter &painter, + const QRectF &rect, + const QString &attribution, + Qt::Alignment anchor, + qreal scale) +{ + if (attribution.isEmpty()) { + return QRectF(); + } + + painter.save(); + + QFont font = painter.font(); + font.setPointSizeF(qMax(6.0, font.pointSizeF() * scale)); + painter.setFont(font); + + const QFontMetrics fm(font); + const qreal maxTextWidth = rect.width() * 0.45; + const QString elided = fm.elidedText(attribution, Qt::ElideRight, qMax(qreal(80.0) * scale, maxTextWidth)); + + const qreal pad = 6.0 * scale; + QRectF captionRect(QPointF(0, 0), QSizeF(fm.horizontalAdvance(elided) + pad * 2.0, fm.height() + pad * 2.0)); + const qreal margin = 4.0 * scale; + if (anchor.testFlag(Qt::AlignLeft)) { + captionRect.moveLeft(rect.left() + margin); + } else { + captionRect.moveRight(rect.right() - margin); + } + if (anchor.testFlag(Qt::AlignTop)) { + captionRect.moveTop(rect.top() + margin); + } else { + captionRect.moveBottom(rect.bottom() - margin); + } + + painter.setPen(Qt::NoPen); + painter.setBrush(QColor(0, 0, 0, 120)); + painter.drawRoundedRect(captionRect, 4, 4); + + painter.setPen(QColor(255, 255, 255, 220)); + painter.drawText(captionRect, Qt::AlignCenter, elided); + + painter.restore(); + + return captionRect; +} diff --git a/cockatrice/src/interface/widgets/cards/art_crop_attribution.h b/cockatrice/src/interface/widgets/cards/art_crop_attribution.h new file mode 100644 index 000000000..0b3fb3d56 --- /dev/null +++ b/cockatrice/src/interface/widgets/cards/art_crop_attribution.h @@ -0,0 +1,41 @@ +#ifndef COCKATRICE_ART_CROP_ATTRIBUTION_H +#define COCKATRICE_ART_CROP_ATTRIBUTION_H + +#include + +class ExactCard; +class QPainter; +class QRectF; +class QString; + +/** + * @brief Builds an attribution caption for a cropped card art display. + * + * When a card image's art region is shown cropped (an "art crop"), the artist + * should be credited in the same interface. Returns an empty string when the + * database has no artist data for the card. + * + * @param card The card whose art is being displayed. + * @return Caption such as "Art: John Avon", or empty. + */ +QString buildArtAttribution(const ExactCard &card); + +/** + * @brief Paints an attribution caption in a corner of a rect. + * + * Draws a subtle semi-transparent pill containing the caption, elided to fit. + * + * @param painter Painter to draw with. + * @param rect The area (e.g. the cropped art region) the caption belongs to. + * @param attribution Caption text (see buildArtAttribution()). + * @param anchor Corner of @p rect to pin the pill to (default bottom-right). + * @param scale Size multiplier for the pill (e.g. 0.8 for a smaller pill). + * @return The rect the pill was drawn in, or an empty rect if @p attribution is empty. + */ +QRectF paintArtAttribution(QPainter &painter, + const QRectF &rect, + const QString &attribution, + Qt::Alignment anchor = Qt::AlignRight | Qt::AlignBottom, + qreal scale = 1.0); + +#endif // COCKATRICE_ART_CROP_ATTRIBUTION_H diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index 8589e3517..64211721b 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -4,6 +4,7 @@ #include "../../../interface/widgets/tabs/tab_supervisor.h" #include "../../theme_manager.h" #include "../../window_main.h" +#include "../cards/art_crop_attribution.h" #include "background_sources.h" #include "home_styled_button.h" @@ -341,8 +342,9 @@ void HomeWidget::paintEvent(QPaintEvent *event) QColor semiTransparentBlack(0, 0, 0, static_cast(255 * 0.33)); painter.fillPath(roundedRectPath, semiTransparentBlack); - // Card name overlay (bottom-right) + // Card name overlay (above the attribution, bottom-right) QString cardName; + QString attribution; ExactCard card = backgroundSourceCard->getCard(); if (card) { cardName = card.getCardPtr()->getName(); @@ -350,8 +352,27 @@ void HomeWidget::paintEvent(QPaintEvent *event) cardName += " (" + card.getPrinting().getSet()->getCorrectedShortName() + ") " + card.getPrinting().getProperty("num"); } + attribution = buildArtAttribution(card); } + // Scryfall requires artist attribution wherever card art is shown cropped. + // Pin it to the bottom-right corner, using the same font as the card name pill, + // and align its right edge with the card name pill's right edge. + constexpr int margin = 15; + constexpr qreal attributionMargin = 4.0; + + QFont attributionFont = painter.font(); + attributionFont.setPointSize(14); + attributionFont.setBold(true); + painter.setFont(attributionFont); + + // paintArtAttribution insets the pill 4px from the given rect's right edge, + // so nudge the rect's right edge to land exactly on the pill's right edge. + QRectF attributionArea = rect(); + attributionArea.setRight(width() - margin + attributionMargin); + const QRectF attributionRect = paintArtAttribution(painter, attributionArea, attribution); + + // Card name bubble above the attribution (when enabled). if (!cardName.isEmpty() && SettingsCache::instance().appearance().getHomeTabDisplayCardName()) { QFont font = painter.font(); font.setPointSize(14); @@ -360,23 +381,26 @@ void HomeWidget::paintEvent(QPaintEvent *event) QFontMetrics fm(font); constexpr int padding = 10; - constexpr int margin = 15; QRect textRect = fm.boundingRect(cardName); - QRect bgRect(width() - textRect.width() - padding * 2 - margin, - height() - textRect.height() - padding * 2 - margin, textRect.width() + padding * 2, - textRect.height() + padding * 2); + int bubbleBottom = height() - margin; + if (!attributionRect.isEmpty()) { + bubbleBottom = attributionRect.top() - 6; + } + const QRect nameBubbleRect(width() - textRect.width() - padding * 2 - margin, + bubbleBottom - textRect.height() - padding * 2, textRect.width() + padding * 2, + textRect.height() + padding * 2); // Background bubble painter.setPen(Qt::NoPen); painter.setBrush(QColor(0, 0, 0, 160)); - painter.drawRoundedRect(bgRect, 8, 8); + painter.drawRoundedRect(nameBubbleRect, 8, 8); // Text painter.setPen(Qt::white); - painter.drawText(bgRect.adjusted(padding, padding, -padding, -padding), Qt::AlignRight | Qt::AlignVCenter, - cardName); + painter.drawText(nameBubbleRect.adjusted(padding, padding, -padding, -padding), + Qt::AlignRight | Qt::AlignVCenter, cardName); } QWidget::paintEvent(event); diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp index 108338332..ca32edaf1 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp @@ -1,7 +1,9 @@ #include "user_card_settings_dialog.h" #include "../../../card_picture_loader/card_picture_loader.h" +#include "../../cards/art_crop_attribution.h" #include "../../utility/completer_utils.h" +#include "card/card_search_model.h" #include "card_database_display_model.h" #include "card_database_model.h" #include "user_card_art_provider.h" @@ -18,6 +20,7 @@ #include #include #include +#include #include #include @@ -39,6 +42,12 @@ void CardArtPreviewWidget::setParams(const CardArtParams &p) update(); } +void CardArtPreviewWidget::setAttribution(const QString &attribution) +{ + attributionText = attribution; + update(); +} + void CardArtPreviewWidget::paintEvent(QPaintEvent *) { QPainter painter(this); @@ -88,6 +97,8 @@ void CardArtPreviewWidget::paintEvent(QPaintEvent *) painter.setPen(QPen(QColor(70, 80, 95), 2)); painter.setBrush(Qt::NoBrush); painter.drawEllipse(avatarRect.adjusted(-1, -1, 1, 1)); + + paintArtAttribution(painter, cardRect, attributionText); } UserCardArtSettingsDialog::UserCardArtSettingsDialog(const CardArtParams &initial, QWidget *parent) @@ -310,6 +321,11 @@ void UserCardArtSettingsDialog::reloadPreview() currentPixmap = UserCardArtProvider::cropCardArt(fullRes); preview->setPixmap(currentPixmap); preview->setParams(currentParams); + + // Only attribute the art once the new pixmap is actually displayed, so a + // cache miss (which keeps the previous pixmap on screen) doesn't pair the + // new card's attribution with the old card's art. + preview->setAttribution(buildArtAttribution(card)); } void UserCardArtSettingsDialog::onParamChanged() diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h index 018043278..397d2b26a 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h @@ -24,6 +24,7 @@ public: void setPixmap(const QPixmap &pixmap); void setParams(const CardArtParams ¶ms); + void setAttribution(const QString &attribution); protected: void paintEvent(QPaintEvent *event) override; @@ -31,6 +32,7 @@ protected: private: QPixmap sourcePixmap; CardArtParams params; + QString attributionText; }; class UserCardArtSettingsDialog : public QDialog diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp index 5d36fbcdb..014d3d4c3 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -1,5 +1,6 @@ #include "user_info_popup.h" +#include "../../cards/art_crop_attribution.h" #include "../../interface/pixel_map_generator.h" #include "../../interface/theme_manager.h" #include "../../interface/widgets/tabs/tab_supervisor.h" @@ -18,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -151,6 +153,16 @@ void UserInfoHeaderWidget::setUserData(const ServerInfo_User &_user, avatar = _avatar; cardArt = _cardArt; params = _params; + + attribution.clear(); + if (user.has_card_art_params()) { + const ExactCard card = + CardDatabaseManager::query()->getCard({QString::fromStdString(user.card_art_params().card_name()), + QString::fromStdString(user.card_art_params().card_provider_id())}); + if (card) { + attribution = buildArtAttribution(card); + } + } update(); } @@ -304,6 +316,17 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *) : UserListPainter::blend(badge.color, Qt::black, 0.35)); p.drawText(br, Qt::AlignCenter, badge.text); } + + // The painter font at this point depends on whether a badge was drawn + // (badge font vs username font), so pin an explicit font for the pill. + p.setFont(font()); + + // Only show the attribution when there is actually art on screen: on a + // cache miss cardArt is null and the pill would float over the plain + // header with no art behind it. + if (!cardArt.isNull()) { + paintArtAttribution(p, rect, attribution, Qt::AlignRight | Qt::AlignBottom, 0.8); + } } // ── UserInfoPopup ───────────────────────────────────────────────────────────── diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.h b/cockatrice/src/interface/widgets/server/user/user_info_popup.h index 851223c87..02cc2b44e 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.h +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.h @@ -95,6 +95,7 @@ private: QPixmap avatar; QPixmap cardArt; CardArtParams params; + QString attribution; }; // ── Main popup ──────────────────────────────────────────────────────────────── diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index 685325f50..3dac7944d 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -2,6 +2,7 @@ #include "../../../../client/settings/cache_settings.h" #include "../../../card_picture_loader/card_picture_loader.h" +#include "../../cards/art_crop_attribution.h" #include "../../interface/pixel_map_generator.h" #include "../../interface/theme_manager.h" #include "../../interface/widgets/tabs/tab_account.h" diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.h b/libcockatrice_card/libcockatrice/card/printing/printing_info.h index 4d174dc41..974c7bb95 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.h +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.h @@ -130,6 +130,18 @@ public: * @return The flavorName, or empty if it isn't present. */ [[nodiscard]] QString getFlavorName() const; + + /** + * @brief Returns the artist name credited for this printing's artwork. + * + * Requires a card database generated with artist data. + * + * @return The artist name, or empty if it isn't present. + */ + [[nodiscard]] QString getArtist() const + { + return getProperty("artist"); + } }; #endif // COCKATRICE_PRINTING_INFO_H diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 85859e7a2..fdb32bb8d 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -237,8 +237,11 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList }; // mtgjson name => xml name - static const QMap setInfoProperties{ - {"number", "num"}, {"rarity", "rarity"}, {"isOnlineOnly", "isOnlineOnly"}, {"isRebalanced", "isRebalanced"}}; + static const QMap setInfoProperties{{"number", "num"}, + {"rarity", "rarity"}, + {"isOnlineOnly", "isOnlineOnly"}, + {"isRebalanced", "isRebalanced"}, + {"artist", "artist"}}; // mtgjson name => xml name static const QMap identifierProperties{{"multiverseId", "muid"}, {"scryfallId", "uuid"}};