mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 00:55:09 -07:00
* [Client] Add [AppColors] application palette roles The home-tab buttons' gradient over the static theme background was hardcoded, and accent-derived fallbacks could not be themed or edited: QPalette's role set is closed, so any application-specific color has to live in Cockatrice's own palette layer. - Add an AppColor::Role enum (AccentStrong / AccentSoft) stored on PaletteConfig and round-tripped from palette-<scheme>.toml under a new [AppColors] section. - Cache the applied app colors in ThemeManager and expose appColor(Role) with a palette-accent-derived fallback; emit paletteChanged() from applyStyleAndPalette so previews, scheme switches and OS dark mode repaint palette-driven widgets. - Fill both app roles in PaletteGenerator::fromAccent and surface them as a dedicated section in the palette editor. - Drive the home-tab buttons from appColor() whenever the background source is the theme (any theme, not just built-ins). - Ship AccentStrong / AccentSoft values in the Fusion and Default default palettes so the static home-tab buttons keep their classic greens. # Conflicts: # cockatrice/src/interface/widgets/general/home_widget.cpp * [AppColors] Address review comments - PaletteEditorDialog::onSave(): compare whole PaletteConfig (colors and appColors) so a change to only AccentStrong/AccentSoft writes the file; add PaletteConfig::operator==. - appColor(): derive both roles from QPalette::Highlight unconditionally. The Fusion palettes pin Accent to near-Window values, and QPalette::Accent only exists on Qt 6.6+, so keying on it made identical themes render very differently across Qt versions. - themeChangedSlot(): merge the theme default's [AppColors] into a custom palette that predates the section instead of all-or-nothing per file; hasPalette() now counts an appColors-only file as a palette. - Add Default/palette-default-light.toml so the Default theme's Light scheme keeps the classic greens instead of falling back to the OS accent. - home_widget: restore the isBuiltInTheme() half of the Automatic condition; non-built-in themes extract button colors from their own background art. - palette_grid_widget: use appEnum.value(i) for the role cast (3 sites), append appHeader to headerLabels, fix the 'Lighted' typo. * [Themes] Route theme writes to the user themes directory setColorScheme()/setStyleName() and the palette editor wrote directly to the resolved theme directory, which for built-in themes is the read-only system (install) location. Changes therefore landed in the install dir and were lost on upgrade. Add ThemeManager::writableThemeDir(), which always resolves to the user themes directory, and route all theme writes through it. The palette editor reuses the same helper, dropping its private writability probe. * [Home] Replace 'Automatic' button color with explicit theme colors default The Automatic option gated on isBuiltInTheme(): built-in themes used the theme's accent colors, while non-built-in themes extracted colors from their own background art. That made the result depend on the theme's origin rather than what the user actually sees. Remove Automatic and expose two explicit choices: 'From theme colors' (always the theme's identity accents, now the default) and 'Extract from background' (always sample the painted background). Drop the now-unused isBuiltInTheme() helper. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
431 lines
17 KiB
C++
431 lines
17 KiB
C++
#include "home_widget.h"
|
|
|
|
#include "../../../client/settings/cache_settings.h"
|
|
#include "../../../interface/widgets/tabs/tab_supervisor.h"
|
|
#include "../../pixel_map_generator.h"
|
|
#include "../../theme_manager.h"
|
|
#include "../../window_main.h"
|
|
#include "../cards/art_crop_attribution.h"
|
|
#include "background_sources.h"
|
|
#include "home_styled_button.h"
|
|
#include "home_tab_button_color.h"
|
|
|
|
#include <QGroupBox>
|
|
#include <QPainter>
|
|
#include <QPainterPath>
|
|
#include <QPushButton>
|
|
#include <QVBoxLayout>
|
|
#include <libcockatrice/card/database/card_database_manager.h>
|
|
#include <libcockatrice/network/client/remote/remote_client.h>
|
|
#include <libcockatrice/settings/appearance_settings.h>
|
|
#include <libcockatrice/settings/paths_settings.h>
|
|
|
|
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|
: QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home"))),
|
|
overlay(themePixmap(QStringLiteral("cockatrice")))
|
|
{
|
|
layout = new QGridLayout(this);
|
|
|
|
backgroundSourceCard = new CardInfoPictureArtCropWidget(this);
|
|
|
|
gradientColors = determineButtonColor();
|
|
|
|
layout->addWidget(createButtons(), 1, 1, Qt::AlignVCenter | Qt::AlignHCenter);
|
|
|
|
layout->setRowStretch(0, 1);
|
|
layout->setRowStretch(2, 1);
|
|
layout->setColumnStretch(0, 1);
|
|
layout->setColumnStretch(2, 1);
|
|
|
|
setLayout(layout);
|
|
|
|
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);
|
|
// Lambda is cleaner to read than overloading this
|
|
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, 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(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this,
|
|
&HomeWidget::updateButtonsToBackgroundColor);
|
|
}
|
|
|
|
void HomeWidget::initializeBackgroundFromSource()
|
|
{
|
|
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<LoadedDeck> deckOpt = DeckLoader::loadFromFile(
|
|
SettingsCache::instance().paths().getDeckPath() + "background.cod", DeckFileFormat::Cockatrice, false);
|
|
backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList();
|
|
}
|
|
|
|
static QPair<QColor, QColor> paletteDerivedButtonColors()
|
|
{
|
|
return {themeManager->appColor(AppColor::AccentStrong), themeManager->appColor(AppColor::AccentSoft)};
|
|
}
|
|
|
|
QPair<QColor, QColor> 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<CardRef> 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<HomeStyledButton *>()) {
|
|
button->updateStylesheet(gradientColors);
|
|
button->update();
|
|
}
|
|
}
|
|
|
|
QGroupBox *HomeWidget::createButtons()
|
|
{
|
|
QGroupBox *box = new QGroupBox(this);
|
|
box->setStyleSheet(R"(
|
|
QGroupBox {
|
|
font-size: 20px;
|
|
color: white; /* Title text color */
|
|
background: transparent;
|
|
}
|
|
|
|
QGroupBox::title {
|
|
color: white;
|
|
subcontrol-origin: margin;
|
|
subcontrol-position: top center; /* or top left / right */
|
|
}
|
|
)");
|
|
box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
QVBoxLayout *boxLayout = new QVBoxLayout;
|
|
boxLayout->setAlignment(Qt::AlignHCenter);
|
|
|
|
QLabel *logoLabel = new QLabel;
|
|
logoLabel->setPixmap(overlay.scaledToWidth(200, Qt::SmoothTransformation));
|
|
logoLabel->setAlignment(Qt::AlignCenter);
|
|
boxLayout->addWidget(logoLabel);
|
|
boxLayout->addSpacing(25);
|
|
|
|
connectButton = new HomeStyledButton("Connect/Play", gradientColors);
|
|
boxLayout->addWidget(connectButton);
|
|
|
|
auto visualDeckEditorButton = new HomeStyledButton(tr("Create New Deck"), gradientColors);
|
|
connect(visualDeckEditorButton, &QPushButton::clicked, tabSupervisor,
|
|
[this] { tabSupervisor->openDeckInNewTab(LoadedDeck()); });
|
|
boxLayout->addWidget(visualDeckEditorButton);
|
|
auto visualDeckStorageButton = new HomeStyledButton(tr("Browse Decks"), gradientColors);
|
|
connect(visualDeckStorageButton, &QPushButton::clicked, tabSupervisor,
|
|
[this] { tabSupervisor->actTabVisualDeckStorage(true); });
|
|
boxLayout->addWidget(visualDeckStorageButton);
|
|
auto visualDatabaseDisplayButton = new HomeStyledButton(tr("Browse Card Database"), gradientColors);
|
|
connect(visualDatabaseDisplayButton, &QPushButton::clicked, tabSupervisor,
|
|
&TabSupervisor::addVisualDatabaseDisplayTab);
|
|
boxLayout->addWidget(visualDatabaseDisplayButton);
|
|
auto edhrecButton = new HomeStyledButton(tr("Browse EDHRec"), gradientColors);
|
|
connect(edhrecButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addEdhrecMainTab);
|
|
boxLayout->addWidget(edhrecButton);
|
|
auto archidektButton = new HomeStyledButton(tr("Browse Archidekt"), gradientColors);
|
|
connect(archidektButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addArchidektTab);
|
|
boxLayout->addWidget(archidektButton);
|
|
auto replaybutton = new HomeStyledButton(tr("View Replays"), gradientColors);
|
|
connect(replaybutton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); });
|
|
boxLayout->addWidget(replaybutton);
|
|
if (qobject_cast<MainWindow *>(tabSupervisor->parentWidget())) {
|
|
auto exitButton = new HomeStyledButton(tr("Quit"), gradientColors);
|
|
connect(exitButton, &QPushButton::clicked, qobject_cast<MainWindow *>(tabSupervisor->parentWidget()),
|
|
&MainWindow::actExit);
|
|
boxLayout->addWidget(exitButton);
|
|
}
|
|
|
|
box->setLayout(boxLayout);
|
|
return box;
|
|
}
|
|
|
|
void HomeWidget::updateConnectButton(const ClientStatus status)
|
|
{
|
|
disconnect(connectButton, &QPushButton::clicked, nullptr, nullptr);
|
|
switch (status) {
|
|
case StatusConnecting:
|
|
connectButton->setText(tr("Connecting..."));
|
|
connectButton->setEnabled(false);
|
|
break;
|
|
case StatusDisconnected:
|
|
connectButton->setText(tr("Connect"));
|
|
connectButton->setEnabled(true);
|
|
connect(connectButton, &QPushButton::clicked, qobject_cast<MainWindow *>(tabSupervisor->parentWidget()),
|
|
&MainWindow::actConnect);
|
|
break;
|
|
case StatusLoggedIn:
|
|
connectButton->setText(tr("Play"));
|
|
connectButton->setEnabled(true);
|
|
connect(connectButton, &QPushButton::clicked, tabSupervisor,
|
|
&TabSupervisor::switchToFirstAvailableNetworkTab);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
QPair<QColor, QColor> 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<QRgb, int> colorCount;
|
|
|
|
// Step 2: Count quantized colors
|
|
for (int y = 0; y < image.height(); ++y) {
|
|
const QRgb *scanLine = reinterpret_cast<const QRgb *>(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<QPair<QRgb, int>> 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<QRgb, int> &a, const QPair<QRgb, int> &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<QColor, QColor>(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);
|
|
}
|
|
|
|
// 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<int>(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;
|
|
|
|
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);
|
|
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);
|
|
}
|