mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-07 18:43:26 -08:00
* [Icons] Migrate UI glyphs to palette-token SVGs Runtime inversion is not a theming system: it rotates hue, breaks duotone art and mishandles alpha, and a QPixmap snapshot cannot follow a palette change. Redraw the top-level UI glyph set on a 24x24 grid with `currentColor` as the paint token, drop Inkscape/Sodipodi cruft, and delete the icons whose metaphor was duplicated (cogwheel/gear -> settings, scale_balanced -> scales, update.png -> reload). Load the glyphs through `loadThemedIcon` so the engine resolves the token from the palette on every request, and let `themePixmap` use the same resolution for one-shot pixmaps. `loadColorAdjustedPixmap` is gone; every caller now keeps a QIcon, which `SettingsButtonWidget::setButtonIcon` accepts instead of a QPixmap. Content art (mana, counters, countries) keeps its authored colors. Add a render test covering aspect ratio, palette flips, icon modes and high-DPI pixmaps. * [Tests] Keep the icon render test QApplication alive until exit Creating it as a function-local static tears it down before Qt's library statics at exit, and libQt6Network then runs QObject destructors against freed memory — a SEGV the sanitizer leg reports after the tests have already passed. Leak the application instead, the usual pattern for a test binary. * [Icons] Render lock and dropdown glyphs through the icon engine Both generators rendered the sentinel SVG to a fixed height x height pixmap at DPR 1. The pre-migration path rendered larger and let QIcon scale it, so on a 2x display the games-list lock (13px) and the banner dropdown arrow (24px) came back crisp; the new path hands the widget a DPR-1 bitmap that gets upscaled and blurred. - The games list asks the themed engine for `icons/lock` directly, so the item view paints it at the device pixel ratio and the lock also follows HighlightedText on a selected row. - The dropdown goes through `loadThemedIcon(...).pixmap(size, dpr)`, which keeps the glyph's aspect ratio and renders at the requested ratio. - LockPixmapGenerator has no consumer left, so it and its cache are gone. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
273 lines
11 KiB
C++
273 lines
11 KiB
C++
#include "tab_public_decks.h"
|
|
|
|
#include "../../../client/settings/cache_settings.h"
|
|
#include "../../deck_loader/deck_loader.h"
|
|
#include "../../theme_icon.h"
|
|
#include "../cards/card_size_widget.h"
|
|
#include "../general/layout_containers/flow_widget.h"
|
|
#include "../interface/theme_manager.h"
|
|
#include "../visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h"
|
|
#include "../visual_deck_storage/deck_preview/public_deck_preview_widget.h"
|
|
#include "../visual_deck_storage/remote_public_decks_model.h"
|
|
#include "../visual_deck_storage/visual_deck_storage_quick_settings_widget.h"
|
|
#include "../visual_deck_storage/visual_deck_storage_search_widget.h"
|
|
#include "../visual_deck_storage/visual_deck_storage_tag_filter_widget.h"
|
|
#include "tab_supervisor.h"
|
|
|
|
#include <QAbstractItemModel>
|
|
#include <QChar>
|
|
#include <QDebug>
|
|
#include <QFont>
|
|
#include <QHBoxLayout>
|
|
#include <QLabel>
|
|
#include <QList>
|
|
#include <QMessageBox>
|
|
#include <QPixmap>
|
|
#include <QSet>
|
|
#include <QStringList>
|
|
#include <QToolButton>
|
|
#include <QVBoxLayout>
|
|
#include <QWidget>
|
|
#include <functional>
|
|
#include <libcockatrice/deck_list/loaded_deck.h>
|
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_download_public.pb.h>
|
|
#include <libcockatrice/protocol/pb/response.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
|
#include <libcockatrice/protocol/pending_command.h>
|
|
#include <libcockatrice/settings/cards_display_settings.h>
|
|
#include <libcockatrice/utility/qt_utils.h>
|
|
#include <optional>
|
|
#include <qnamespace.h>
|
|
|
|
TabPublicDecks::TabPublicDecks(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &_userName)
|
|
: Tab(_tabSupervisor), client(_client), userName(_userName)
|
|
{
|
|
model = new RemotePublicDecksModel(client, this);
|
|
cardSize = SettingsCache::instance().cardsDisplay().getVisualDeckStorageCardSize();
|
|
|
|
titleLabel = new QLabel(tr("Public decks of %1").arg(userName.toHtmlEscaped()), this);
|
|
// Family comes from the InterfaceDefault theme role; bold stays caller-side.
|
|
auto applyTitleFont = [this]() {
|
|
titleLabel->setFont(QtUtils::boldFont(themeManager->appFont(AppFont::InterfaceDefault)));
|
|
};
|
|
applyTitleFont();
|
|
connect(themeManager, &ThemeManager::fontsChanged, this, [this, applyTitleFont] {
|
|
applyTitleFont();
|
|
// Preview cells paint with the live role font; repaint the grid so the
|
|
// new family shows without waiting for a scroll or hover.
|
|
flowWidget->update();
|
|
});
|
|
|
|
auto *headerLayout = new QHBoxLayout;
|
|
headerLayout->addWidget(titleLabel);
|
|
headerLayout->addStretch(1);
|
|
|
|
// Filter/toolbar row, matching the Visual Deck Storage: color identity filter
|
|
// first, the search bar stretching in the middle, and the quick-settings button
|
|
// at the end. The card size slider lives inside its popup.
|
|
emptyLabel = new QLabel(tr("This user has not published any decks."), this);
|
|
emptyLabel->setAlignment(Qt::AlignCenter);
|
|
emptyLabel->setVisible(false);
|
|
|
|
statusLabel = new QLabel(this);
|
|
statusLabel->setAlignment(Qt::AlignCenter);
|
|
statusLabel->setVisible(false);
|
|
|
|
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
|
flowWidget->setSpacing(8, 8);
|
|
|
|
colorIdentityFilter = new DeckPreviewColorIdentityFilterWidget(this);
|
|
searchWidget = new VisualDeckStorageSearchWidget(this);
|
|
refreshButton = new QToolButton(this);
|
|
refreshButton->setIcon(loadThemedIcon(QStringLiteral("icons/reload")));
|
|
refreshButton->setFixedSize(32, 32);
|
|
quickSettingsWidget = new VisualDeckStorageQuickSettingsWidget(this);
|
|
quickSettingsWidget->setPublicDecksMode(true);
|
|
|
|
auto *filterLayout = new QHBoxLayout;
|
|
filterLayout->addWidget(colorIdentityFilter);
|
|
filterLayout->addWidget(searchWidget, 1);
|
|
filterLayout->addWidget(refreshButton);
|
|
filterLayout->addWidget(quickSettingsWidget);
|
|
|
|
tagFilterWidget = new VisualDeckStorageTagFilterWidget(this);
|
|
tagFilterWidget->setAllTagsProvider([this] { return model->allTags(); });
|
|
updateTagsVisibility(quickSettingsWidget->getShowTagFilter());
|
|
|
|
auto *layout = new QVBoxLayout;
|
|
layout->addLayout(headerLayout);
|
|
layout->addLayout(filterLayout);
|
|
layout->addWidget(tagFilterWidget);
|
|
layout->addWidget(statusLabel);
|
|
layout->addWidget(emptyLabel);
|
|
layout->addWidget(flowWidget, 1);
|
|
|
|
auto *mainWidget = new QWidget(this);
|
|
mainWidget->setLayout(layout);
|
|
setCentralWidget(mainWidget);
|
|
|
|
connect(refreshButton, &QToolButton::clicked, this, [this] { model->refresh(userName); });
|
|
connect(model, &QAbstractItemModel::modelReset, this, &TabPublicDecks::rebuildGrid);
|
|
connect(model, &RemotePublicDecksModel::loadingChanged, this, &TabPublicDecks::updateLoadingState);
|
|
connect(model, &RemotePublicDecksModel::loadFailed, this, [this](const QString &message) {
|
|
lastFailureMessage = message;
|
|
statusLabel->setText(message);
|
|
statusLabel->setVisible(true);
|
|
flowWidget->setVisible(false);
|
|
emptyLabel->setVisible(false);
|
|
});
|
|
connect(searchWidget, &VisualDeckStorageSearchWidget::searchTextChanged, this,
|
|
[this](const QString &text) { model->setSearchText(text); });
|
|
connect(colorIdentityFilter, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, this,
|
|
&TabPublicDecks::updateColorFilter);
|
|
connect(colorIdentityFilter, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, this,
|
|
&TabPublicDecks::updateColorFilter);
|
|
connect(tagFilterWidget, &VisualDeckStorageTagFilterWidget::filterChanged, this, &TabPublicDecks::updateTagFilter);
|
|
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::cardSizeChanged, this,
|
|
&TabPublicDecks::updateCardSize);
|
|
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showTagFilterChanged, this,
|
|
&TabPublicDecks::updateTagsVisibility);
|
|
quickSettingsWidget->getCardSizeWidget()->enableCtrlScrollResize(flowWidget);
|
|
|
|
retranslateUi();
|
|
model->refresh(userName);
|
|
}
|
|
|
|
QString TabPublicDecks::getTabText() const
|
|
{
|
|
return tr("Public decks of %1").arg(userName);
|
|
}
|
|
|
|
void TabPublicDecks::retranslateUi()
|
|
{
|
|
// The username is another user's data, so escape it for the AutoText QLabel.
|
|
titleLabel->setText(tr("Public decks of %1").arg(userName.toHtmlEscaped()));
|
|
// The same choice rebuildGrid makes, so a language change does not swap
|
|
// the "no match" variant for the "nothing published" one.
|
|
emptyLabel->setText(model->totalCount() > 0 ? tr("No decks match your filters.")
|
|
: tr("This user has not published any decks."));
|
|
refreshButton->setToolTip(tr("Refresh"));
|
|
refreshButton->setAccessibleName(tr("Refresh"));
|
|
quickSettingsWidget->setToolTip(tr("Public Decks Settings"));
|
|
// Re-show whatever the status label is showing so a language change picks up
|
|
// the new language or, for a failure message, at least does not hide it.
|
|
if (model->isLoading()) {
|
|
updateLoadingState(true);
|
|
} else if (!lastFailureMessage.isEmpty()) {
|
|
statusLabel->setText(lastFailureMessage);
|
|
statusLabel->setVisible(true);
|
|
flowWidget->setVisible(false);
|
|
emptyLabel->setVisible(false);
|
|
} else {
|
|
updateLoadingState(false);
|
|
}
|
|
emit tabTextChanged(this, getTabText());
|
|
}
|
|
|
|
bool TabPublicDecks::closeRequest()
|
|
{
|
|
emit closing(this);
|
|
return Tab::closeRequest();
|
|
}
|
|
|
|
void TabPublicDecks::rebuildGrid()
|
|
{
|
|
flowWidget->clearLayout();
|
|
|
|
const int count = model->rowCount();
|
|
if (count == 0) {
|
|
emptyLabel->setText(model->totalCount() > 0 ? tr("No decks match your filters.")
|
|
: tr("This user has not published any decks."));
|
|
}
|
|
emptyLabel->setVisible(count == 0);
|
|
for (int i = 0; i < count; ++i) {
|
|
auto *tile = new PublicDeckPreviewWidget(flowWidget, model->entryAt(i));
|
|
tile->setScaleFactor(cardSize);
|
|
connect(tile, &PublicDeckPreviewWidget::openDeckRequested, this, &TabPublicDecks::openDeck);
|
|
flowWidget->addWidget(tile);
|
|
}
|
|
|
|
// The deck set changed, so the tag filter chips are re-gathered from it.
|
|
tagFilterWidget->refreshTags();
|
|
}
|
|
|
|
void TabPublicDecks::updateColorFilter()
|
|
{
|
|
model->setColorFilter(colorIdentityFilter->getFilterMode(), colorIdentityFilter->getActiveColors());
|
|
}
|
|
|
|
void TabPublicDecks::updateTagFilter()
|
|
{
|
|
const QStringList selectedTags = tagFilterWidget->selectedTags();
|
|
const QStringList excludedTags = tagFilterWidget->excludedTags();
|
|
model->setTagFilter(QSet<QString>(selectedTags.cbegin(), selectedTags.cend()),
|
|
QSet<QString>(excludedTags.cbegin(), excludedTags.cend()));
|
|
tagFilterWidget->refreshTags();
|
|
}
|
|
|
|
void TabPublicDecks::updateTagsVisibility(bool visible)
|
|
{
|
|
tagFilterWidget->setVisible(visible);
|
|
}
|
|
|
|
void TabPublicDecks::updateLoadingState(bool loading)
|
|
{
|
|
if (loading) {
|
|
// A new attempt is under way, so the previously shown failure, if any,
|
|
// no longer describes the current state.
|
|
lastFailureMessage.clear();
|
|
statusLabel->setText(tr("Loading public decks…"));
|
|
statusLabel->setVisible(true);
|
|
flowWidget->setVisible(false);
|
|
emptyLabel->setVisible(false);
|
|
} else {
|
|
statusLabel->setVisible(false);
|
|
flowWidget->setVisible(true);
|
|
}
|
|
}
|
|
|
|
void TabPublicDecks::updateCardSize(int scale)
|
|
{
|
|
cardSize = scale;
|
|
applyCardSize(scale);
|
|
}
|
|
|
|
void TabPublicDecks::applyCardSize(int scale)
|
|
{
|
|
const auto tiles = flowWidget->findChildren<PublicDeckPreviewWidget *>();
|
|
for (PublicDeckPreviewWidget *tile : tiles) {
|
|
tile->setScaleFactor(scale);
|
|
}
|
|
flowWidget->setMinimumSizeToMaxSizeHint();
|
|
}
|
|
|
|
void TabPublicDecks::openDeck(int deckId)
|
|
{
|
|
Command_DeckDownloadPublic cmd;
|
|
cmd.set_deck_id(deckId);
|
|
|
|
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
|
connect(pend, &PendingCommand::finished, this, &TabPublicDecks::openDeckFinished);
|
|
client->sendCommand(pend);
|
|
}
|
|
|
|
void TabPublicDecks::openDeckFinished(const Response &response, const CommandContainer & /*commandContainer*/)
|
|
{
|
|
if (response.response_code() != Response::RespOk) {
|
|
QMessageBox::warning(this, tr("Open public deck"),
|
|
tr("Failed to open the public deck (server response code %1).")
|
|
.arg(QString::number(static_cast<int>(response.response_code()))));
|
|
return;
|
|
}
|
|
|
|
const Response_DeckDownload &resp = response.GetExtension(Response_DeckDownload::ext);
|
|
std::optional<LoadedDeck> deckOpt =
|
|
DeckLoader::loadFromRemote(QString::fromStdString(resp.deck()), LoadedDeck::LoadInfo::NON_REMOTE_ID);
|
|
if (!deckOpt) {
|
|
QMessageBox::warning(this, tr("Open public deck"), tr("The public deck could not be parsed."));
|
|
return;
|
|
}
|
|
|
|
tabSupervisor->openDeckInNewTab(deckOpt.value());
|
|
}
|