#include "home_widget.h" #include "../../../client/settings/cache_settings.h" #include "../../../interface/widgets/tabs/tab_supervisor.h" #include "../../deck_loader/deck_loader.h" #include "../../pixel_map_generator.h" #include "../../theme_config.h" #include "../../theme_manager.h" #include "../../window_main.h" #include "../cards/art_crop_attribution.h" #include "../cards/card_info_picture_art_crop_widget.h" #include "abstract_client.h" #include "background_sources.h" #include "home_styled_button.h" #include "home_tab_button_color.h" #include "libcockatrice/card/card_info.h" #include "libcockatrice/card/database/card_database.h" #include "libcockatrice/card/database/card_database_loader.h" #include "libcockatrice/card/database/card_database_querier.h" #include "libcockatrice/card/printing/exact_card.h" #include "libcockatrice/card/printing/printing_info.h" #include "libcockatrice/card/set/card_set.h" #include "libcockatrice/utility/card_ref.h" #include "tutorial/tutorial_controller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) : QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home"))) { setObjectName("homeWidget"); backgroundSourceCard = new CardInfoPictureArtCropWidget(this); gradientColors = determineButtonColor(); // The launcher keeps its natural size on a large display and stays centred in the // viewport. On a small window it scrolls, instead of its combined minimum height // becoming the minimum height of the entire application. auto *scrollArea = QtUtils::makeScrollArea(this, Qt::ScrollBarAsNeeded, Qt::ScrollBarAsNeeded); scrollArea->setObjectName("homeActionsScrollArea"); scrollArea->setFrameShape(QFrame::NoFrame); scrollArea->setWidgetResizable(false); scrollArea->setAlignment(Qt::AlignCenter); // The home background is painted by this widget, so let it show through the viewport. scrollArea->viewport()->setAutoFillBackground(false); scrollArea->setWidget(createButtons()); auto *layout = new QVBoxLayout(this); layout->addWidget(scrollArea); cardChangeTimer = new QTimer(this); connect(cardChangeTimer, &QTimer::timeout, this, &HomeWidget::updateRandomCard); initializeBackgroundFromSource(); updateConnectButton(tabSupervisor->getClient()->getStatus()); connect(tabSupervisor->getClient(), &RemoteClient::statusChanged, this, &HomeWidget::updateConnectButton); connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundSourceChanged, this, &HomeWidget::initializeBackgroundFromSource); connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundShuffleFrequencyChanged, this, &HomeWidget::onBackgroundShuffleFrequencyChanged); tutorialController = new TutorialController(this); auto sequence = TutorialSequence(); sequence.addStep({connectButton, "Connect to a server to play here!"}); auto vdeStep = TutorialStep{visualDeckEditorButton, "Create a new deck from cards in the database here!"}; vdeStep.requiresInteraction = true; vdeStep.allowClickThrough = true; vdeStep.validationHint = "Open the deck editor to try it out!"; vdeStep.validationTiming = ValidationTiming::OnSignal; vdeStep.autoAdvanceOnValid = true; vdeStep.validator = []() { return true; }; vdeStep.signalHook = [this](TutorialController *controller) { return connect(visualDeckEditorButton, &QAbstractButton::clicked, controller, &TutorialController::checkValidation); }; sequence.addStep(vdeStep); sequence.addStep({visualDeckStorageButton, "Browse the decks in your local collection."}); sequence.addStep({visualDatabaseDisplayButton, "View the card database here."}); sequence.addStep( {edhrecButton, "Browse EDHRec, an external service designed to provide card recommendations for decks."}); sequence.addStep({archidektButton, "Browse Archidekt, an external service that allows users to store " "decklists and import them to your local collection."}); sequence.addStep({replayButton, "View replays of your past games here."}); sequence.addStep({exitButton, "Exit the application."}); tutorialController->addSequence(sequence); // Lambda is cleaner to read than overloading this connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this, [this] { repaint(); }); connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundDimChanged, this, [this] { repaint(); }); connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &HomeWidget::initializeBackgroundFromSource); connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &HomeWidget::updateButtonsToBackgroundColor); // Scheme flips (light/dark/system with an OS switch) fire on themeManager, // not on SettingsCache::themeChanged, so re-resolve the variant background. connect(themeManager, &ThemeManager::themeChanged, this, &HomeWidget::initializeBackgroundFromSource); connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateButtonsToBackgroundColor); connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateLogoOverlay); connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this, &HomeWidget::updateButtonsToBackgroundColor); } void HomeWidget::retranslateUi() { visualDeckEditorButton->setText(tr("Create New Deck")); visualDeckStorageButton->setText(tr("Browse Decks")); visualDatabaseDisplayButton->setText(tr("Browse Card Database")); edhrecButton->setText(tr("Browse EDHRec")); archidektButton->setText(tr("Browse Archidekt")); replayButton->setText(tr("View Replays")); if (exitButton) { exitButton->setText(tr("Quit")); } switch (connectionStatus) { case StatusDisconnected: connectButton->setText(tr("Connect")); break; case StatusLoggedIn: connectButton->setText(tr("Play")); break; default: // Connecting, registering, activating, logging in, etc. connectButton->setText(tr("Connecting...")); break; } } void HomeWidget::showEvent(QShowEvent *event) { QWidget::showEvent(event); if (!tutorialStarted && !SettingsCache::instance().userInterface().getTutorialCompleted()) { tutorialStarted = true; // Start on next event loop iteration so everything is fully painted QTimer::singleShot(3, tutorialController, [this] { tutorialController->start(); }); } } void HomeWidget::initializeBackgroundFromSource() { // The featured logo is theme/scheme-derived too; reload it alongside the // background so a theme or appearance switch doesn't leave it stale. updateLogoOverlay(); if (CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) { connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, &HomeWidget::initializeBackgroundFromSource); return; } auto backgroundSourceType = BackgroundSources::fromId(SettingsCache::instance().appearance().getHomeTabBackgroundSource()); switch (backgroundSourceType) { case BackgroundSources::Theme: cardChangeTimer->stop(); background = themePixmap(QStringLiteral("backgrounds/home")); backgroundSourceDeck = DeckList(); backgroundSourceCard->setCard(ExactCard()); updateButtonsToBackgroundColor(); update(); break; case BackgroundSources::RandomCardArt: backgroundSourceDeck = DeckList(); updateRandomCard(); onBackgroundShuffleFrequencyChanged(); break; case BackgroundSources::DeckFileArt: loadBackgroundSourceDeck(); updateRandomCard(); onBackgroundShuffleFrequencyChanged(); break; } } void HomeWidget::loadBackgroundSourceDeck() { std::optional deckOpt = DeckLoader::loadFromFile( SettingsCache::instance().paths().getDeckPath() + "background.cod", DeckFileFormat::Cockatrice, false); backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList(); } static QPair paletteDerivedButtonColors() { return {themeManager->appColor(AppColor::AccentStrong), themeManager->appColor(AppColor::AccentSoft)}; } QPair HomeWidget::determineButtonColor() const { auto colorSource = HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex()); switch (colorSource) { case HomeTabButtonColor::FromThemeColors: return paletteDerivedButtonColors(); case HomeTabButtonColor::FromBackground: return extractDominantColors(background); } return paletteDerivedButtonColors(); } void HomeWidget::setRandomCard(ExactCard &newCard) { static constexpr int ATTEMPTS = 10; for (int i = 0; i < ATTEMPTS; ++i) { ExactCard tmpCard = CardDatabaseManager::query()->getRandomCard(); if (tmpCard != backgroundSourceCard->getCard() && tmpCard.getCardPtr()->getProperty("layout") == "normal" && tmpCard.getPrinting().getSet() != nullptr) { newCard = tmpCard; return; } } qWarning() << "failed to set random card image after" << ATTEMPTS << "attempts"; } void HomeWidget::updateRandomCard() { auto backgroundSourceType = BackgroundSources::fromId(SettingsCache::instance().appearance().getHomeTabBackgroundSource()); ExactCard newCard; switch (backgroundSourceType) { case BackgroundSources::Theme: break; case BackgroundSources::RandomCardArt: setRandomCard(newCard); break; case BackgroundSources::DeckFileArt: QList cardRefs = backgroundSourceDeck.getCardRefList(); ExactCard oldCard = backgroundSourceCard->getCard(); if (!cardRefs.empty()) { if (cardRefs.size() == 1) { newCard = CardDatabaseManager::query()->getCard(cardRefs.first()); } else { // Keep picking until different do { int idx = QRandomGenerator::global()->bounded(cardRefs.size()); newCard = CardDatabaseManager::query()->getCard(cardRefs.at(idx)); } while (newCard == oldCard); } } else { do { newCard = CardDatabaseManager::query()->getRandomCard(); } while (newCard == oldCard); } break; } if (!newCard) { return; } connect(newCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, &HomeWidget::updateBackgroundProperties); backgroundSourceCard->setCard(newCard); background = backgroundSourceCard->getBackground(); } void HomeWidget::onBackgroundShuffleFrequencyChanged() { cardChangeTimer->stop(); if (SettingsCache::instance().appearance().getHomeTabBackgroundShuffleFrequency() > 0) { cardChangeTimer->start(SettingsCache::instance().appearance().getHomeTabBackgroundShuffleFrequency() * 1000); } } void HomeWidget::updateBackgroundProperties() { background = backgroundSourceCard->getBackground(); updateButtonsToBackgroundColor(); update(); // Triggers repaint } void HomeWidget::updateButtonsToBackgroundColor() { gradientColors = determineButtonColor(); for (HomeStyledButton *button : findChildren()) { button->updateStylesheet(gradientColors); button->update(); } } QGroupBox *HomeWidget::createButtons() { QGroupBox *box = new QGroupBox(this); box->setObjectName("homeButtonsBox"); // The box only stacks the logo and the actions: no frame, and no background of its // own, so the home background art stays visible. It never shows a title. The rule is // load-bearing: QScrollArea::setWidget() turns autoFillBackground on for its widget. box->setStyleSheet("QGroupBox { background: transparent; border: none; }"); // Family comes from the InterfaceDefault theme role; the 20px size stays caller-side. auto applyBoxFont = [box]() { QFont boxFont = themeManager->appFont(AppFont::InterfaceDefault); boxFont.setPixelSize(20); box->setFont(boxFont); }; applyBoxFont(); connect(themeManager, &ThemeManager::fontsChanged, this, applyBoxFont); QVBoxLayout *boxLayout = new QVBoxLayout; boxLayout->setAlignment(Qt::AlignHCenter); // The scroll area is not resizable, so the box has to size itself: SetFixedSize keeps // it at its content size and re-fixes it whenever the labels or fonts change, which is // what makes the scroll area's scroll bars follow the content. boxLayout->setSizeConstraint(QLayout::SetFixedSize); logoLabel = new QLabel; logoLabel->setAlignment(Qt::AlignCenter); boxLayout->addWidget(logoLabel); updateLogoOverlay(); boxLayout->addSpacing(25); connectButton = new HomeStyledButton(QString(), gradientColors); connectButton->setObjectName("homeConnectButton"); boxLayout->addWidget(connectButton); visualDeckEditorButton = new HomeStyledButton(QString(), gradientColors); visualDeckEditorButton->setObjectName("homeCreateNewDeckButton"); connect(visualDeckEditorButton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->openDeckInNewTab(LoadedDeck()); }); boxLayout->addWidget(visualDeckEditorButton); visualDeckStorageButton = new HomeStyledButton(QString(), gradientColors); visualDeckStorageButton->setObjectName("homeBrowseDecksButton"); connect(visualDeckStorageButton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabVisualDeckStorage(true); }); boxLayout->addWidget(visualDeckStorageButton); visualDatabaseDisplayButton = new HomeStyledButton(QString(), gradientColors); visualDatabaseDisplayButton->setObjectName("homeBrowseCardDatabaseButton"); connect(visualDatabaseDisplayButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addVisualDatabaseDisplayTab); boxLayout->addWidget(visualDatabaseDisplayButton); edhrecButton = new HomeStyledButton(QString(), gradientColors); edhrecButton->setObjectName("homeBrowseEdhrecButton"); connect(edhrecButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addEdhrecMainTab); boxLayout->addWidget(edhrecButton); archidektButton = new HomeStyledButton(QString(), gradientColors); archidektButton->setObjectName("homeBrowseArchidektButton"); connect(archidektButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addArchidektTab); boxLayout->addWidget(archidektButton); replayButton = new HomeStyledButton(QString(), gradientColors); replayButton->setObjectName("homeViewReplaysButton"); connect(replayButton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); }); boxLayout->addWidget(replayButton); if (qobject_cast(tabSupervisor->parentWidget())) { exitButton = new HomeStyledButton(QString(), gradientColors); connect(exitButton, &QPushButton::clicked, qobject_cast(tabSupervisor->parentWidget()), &MainWindow::actExit); boxLayout->addWidget(exitButton); } box->setLayout(boxLayout); return box; } void HomeWidget::updateConnectButton(const ClientStatus status) { connectionStatus = status; disconnect(connectButton, &QPushButton::clicked, nullptr, nullptr); switch (status) { case StatusConnecting: connectButton->setEnabled(false); break; case StatusDisconnected: connectButton->setEnabled(true); connect(connectButton, &QPushButton::clicked, qobject_cast(tabSupervisor->parentWidget()), &MainWindow::actConnect); break; case StatusLoggedIn: connectButton->setEnabled(true); connect(connectButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::switchToFirstAvailableNetworkTab); break; default: break; } retranslateUi(); } QPair HomeWidget::extractDominantColors(const QPixmap &pixmap) { // Step 1: Downscale image for performance QImage image = pixmap.toImage() .scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation) .convertToFormat(QImage::Format_RGB32); QMap colorCount; // Step 2: Count quantized colors for (int y = 0; y < image.height(); ++y) { const QRgb *scanLine = reinterpret_cast(image.scanLine(y)); for (int x = 0; x < image.width(); ++x) { QColor color = QColor::fromRgb(scanLine[x]); int r = color.red() & 0xF0; int g = color.green() & 0xF0; int b = color.blue() & 0xF0; QRgb quantized = qRgb(r, g, b); colorCount[quantized]++; } } // Step 3: Sort by frequency QVector> sortedColors; for (auto it = colorCount.constBegin(); it != colorCount.constEnd(); ++it) { sortedColors.append(qMakePair(it.key(), it.value())); } std::sort(sortedColors.begin(), sortedColors.end(), [](const QPair &a, const QPair &b) { return a.second > b.second; }); // Step 4: Pick top two distinct colors QColor first = QColor(sortedColors.value(0).first); QColor second = first; for (int i = 1; i < sortedColors.size(); ++i) { QColor candidate = QColor(sortedColors[i].first); if (candidate != first) { second = candidate; break; } } return QPair(first, second); } void HomeWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); if (!background.isNull()) { QSize widgetSize = size() * devicePixelRatio(); QPixmap toDraw = background.scaled(widgetSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); // Draw scaled background centered QSize bgSize = toDraw.size(); QPoint topLeft((widgetSize.width() - bgSize.width()) / (devicePixelRatio() * 2), // undo scaling for painter (widgetSize.height() - bgSize.height()) / (devicePixelRatio() * 2)); painter.drawPixmap(topLeft, toDraw); } if (SettingsCache::instance().appearance().getHomeTabBackgroundDim()) { // Draw translucent black overlay with rounded corners QRectF overlayRect(5, 5, width() - 10, height() - 10); QPainterPath roundedRectPath; roundedRectPath.addRoundedRect(overlayRect, 20, 20); QColor semiTransparentBlack(0, 0, 0, static_cast(255 * 0.33)); painter.fillPath(roundedRectPath, semiTransparentBlack); } // Card name overlay (above the attribution, bottom-right) QString cardName; QString attribution; ExactCard card = backgroundSourceCard->getCard(); if (card) { cardName = card.getCardPtr()->getName(); if (card.getPrinting().getSet() != nullptr) { 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; // Family comes from the InterfaceDefault theme role; the 14px sizes and // bold weights stay caller-side as before. QFont attributionFont = themeManager->appFont(AppFont::InterfaceDefault); 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 = themeManager->appFont(AppFont::InterfaceDefault); font.setPointSize(14); font.setBold(true); painter.setFont(font); QFontMetrics fm(font); constexpr int padding = 10; QRect textRect = fm.boundingRect(cardName); 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(nameBubbleRect, 8, 8); // Text painter.setPen(Qt::white); painter.drawText(nameBubbleRect.adjusted(padding, padding, -padding, -padding), Qt::AlignRight | Qt::AlignVCenter, cardName); } QWidget::paintEvent(event); } void HomeWidget::updateLogoOverlay() { // Emulate cockatrice.svg in Qt rather than rendering the baked-in SVG. // The SVG has no separate plate: the gradient fills the bird's silhouette // paths (light #c9fd62/AccentSoft at the top-left, dark #139740/AccentStrong // toward the bottom-right — the SVG's linearGradient4265-7-8 stops along // its userSpaceOnUse axis), and the white highlight path // (cockatrice-logo-white) sits on top. So we paint that gradient clipped to // the full logo silhouette (the full-color logo's alpha), then overlay the // white mark. Colours stay fully theme-driven and independent of the static // greens baked into the SVG. const QColor strong = themeManager->appColor(AppColor::AccentStrong); const QColor soft = themeManager->appColor(AppColor::AccentSoft); const QPixmap silhouette = themePixmap(QStringLiteral("cockatrice")).scaledToWidth(200, Qt::SmoothTransformation); const QPixmap whiteMark = themePixmap(QStringLiteral("cockatrice-logo-white")).scaledToWidth(200, Qt::SmoothTransformation); if (silhouette.isNull() || whiteMark.isNull()) { return; } QPixmap composite(silhouette.size()); composite.fill(Qt::transparent); { QPainter painter(&composite); painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::SmoothPixmapTransform); // Recreate cockatrice.svg's own gradient geometry (linearGradient // 4265-7-8, userSpaceOnUse): light AccentSoft at S=(-8.097,-97.746), // dark AccentStrong at E=(162.455,295.208), on the SVG's 300x300 // canvas. Scale those coordinates to this composite's size. const qreal scale = composite.width() / 300.0; QLinearGradient gradient(QPointF(-8.097, -97.746) * scale, QPointF(162.455, 295.208) * scale); gradient.setColorAt(0.0, soft); gradient.setColorAt(1.0, strong); painter.fillRect(composite.rect(), gradient); // Clip the gradient to the full logo silhouette exactly as the SVG's // gradient paths are confined to the bird. painter.setCompositionMode(QPainter::CompositionMode_DestinationIn); painter.drawPixmap(0, 0, silhouette); painter.setCompositionMode(QPainter::CompositionMode_SourceOver); painter.drawPixmap(0, 0, whiteMark); } if (logoLabel) { logoLabel->setPixmap(composite); } }