mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-06 01:53:25 -08:00
Merge branch 'master' into tooomm-qt5
This commit is contained in:
commit
b3813c202b
162 changed files with 4862 additions and 653 deletions
|
|
@ -74,6 +74,8 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT
|
|||
|
||||
update();
|
||||
});
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
||||
&CardInfoPictureWidget::updatePixmap);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "card_info_text_widget.h"
|
||||
|
||||
#include "../../../game_graphics/board/card_item.h"
|
||||
#include "../../card_localization.h"
|
||||
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
|
|
@ -10,7 +11,7 @@
|
|||
#include <libcockatrice/card/game_specific_terms.h>
|
||||
#include <libcockatrice/card/relation/card_relation.h>
|
||||
|
||||
CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent), info(nullptr)
|
||||
CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent)
|
||||
{
|
||||
propsLabel = new QLabel;
|
||||
propsLabel->setOpenExternalLinks(false);
|
||||
|
|
@ -39,6 +40,12 @@ CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent), info(n
|
|||
grid->setRowStretch(1, 1);
|
||||
|
||||
retranslateUi();
|
||||
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, [this] {
|
||||
if (currentCard) {
|
||||
setCard(currentCard);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void CardInfoTextWidget::setTexts(const QString &propsText, const QString &textText)
|
||||
|
|
@ -60,7 +67,7 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard)
|
|||
|
||||
QString text = "<table width=\"100%\" border=0 cellspacing=0 cellpadding=0>";
|
||||
text += QString("<tr><td>%1</td><td width=\"5\"></td><td>%2</td></tr>")
|
||||
.arg(tr("Name:"), card->getName().toHtmlEscaped());
|
||||
.arg(tr("Name:"), CardLocalization::displayName(card).toHtmlEscaped());
|
||||
|
||||
if (!exactCard.getPrinting().isEmpty()) {
|
||||
QString setShort = exactCard.getPrinting().getSet()->getShortName().toHtmlEscaped();
|
||||
|
|
@ -94,7 +101,8 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard)
|
|||
}
|
||||
|
||||
text += "</table>";
|
||||
setTexts(text, card->getText());
|
||||
setTexts(text, CardLocalization::displayText(card));
|
||||
currentCard = exactCard;
|
||||
}
|
||||
|
||||
void CardInfoTextWidget::setInvalidCardName(const QString &cardName)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ private:
|
|||
QLabel *propsLabel;
|
||||
QScrollArea *propsScroll;
|
||||
QTextEdit *textLabel;
|
||||
CardInfoPtr info;
|
||||
ExactCard currentCard; ///< Last card set, re-rendered when the card language changes.
|
||||
void setTexts(const QString &propsText, const QString &textText);
|
||||
|
||||
public:
|
||||
|
|
|
|||
|
|
@ -345,7 +345,9 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard()
|
|||
if (!current.isValid()) {
|
||||
return {};
|
||||
}
|
||||
const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
|
||||
// The display role holds the localized card name; the edit role always carries the
|
||||
// canonical English name needed to look the card up in the database.
|
||||
const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
const QString cardProviderID = current.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
|
||||
const QModelIndex gparent = current.parent().parent();
|
||||
|
||||
|
|
@ -518,8 +520,11 @@ void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck()
|
|||
|
||||
void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus)
|
||||
{
|
||||
const QModelIndex proxyIndex = proxy->mapFromSource(newCardIndex);
|
||||
|
||||
deckView->clearSelection();
|
||||
deckView->setCurrentIndex(newCardIndex);
|
||||
deckView->setCurrentIndex(proxyIndex);
|
||||
deckView->scrollTo(proxyIndex);
|
||||
recursiveExpand(newCardIndex);
|
||||
|
||||
if (!preserveWidgetFocus) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
#include "deck_state_manager.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list_history_manager.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
DeckStateManager::DeckStateManager(QObject *parent)
|
||||
: QObject(parent), deckList(QSharedPointer<DeckList>(new DeckList)),
|
||||
deckListModel(new DeckListModel(this, deckList)), historyManager(new DeckListHistoryManager(this))
|
||||
{
|
||||
deckListModel->setDisplayLanguage(SettingsCache::instance().cardsDisplay().getCardLang());
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, deckListModel,
|
||||
[this](const QString &lang) { deckListModel->setDisplayLanguage(lang); });
|
||||
|
||||
connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, [this] {
|
||||
setModified(true);
|
||||
emit historyChanged();
|
||||
|
|
@ -260,7 +267,10 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
|
|||
return false;
|
||||
}
|
||||
|
||||
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
|
||||
// The display role holds the localized card name; the edit role always carries the
|
||||
// canonical English name needed to look the card up in the database.
|
||||
QString displayCardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
|
||||
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
|
||||
QModelIndex gparent = idx.parent().parent();
|
||||
|
||||
|
|
@ -277,7 +287,7 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
|
|||
|
||||
QString reason = tr("Moved to %1 1 × \"%2\" (%3)") //
|
||||
.arg(otherZoneName)
|
||||
.arg(cardName)
|
||||
.arg(displayCardName)
|
||||
.arg(providerId);
|
||||
|
||||
return modifyDeck(reason, [&idx, &cardName, &providerId, &otherZoneName](auto model) {
|
||||
|
|
@ -291,9 +301,8 @@ bool DeckStateManager::removeCardAtIndex(const QModelIndex &idx)
|
|||
return false;
|
||||
}
|
||||
|
||||
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
|
||||
|
||||
QString reason = tr("Removed \"%1\" (all copies)").arg(cardName);
|
||||
QString reason =
|
||||
tr("Removed \"%1\" (all copies)").arg(idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString());
|
||||
|
||||
return modifyDeck(reason, [&idx](auto model) { return model->removeRow(idx.row(), idx.parent()); });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -416,6 +416,11 @@ void DlgSettings::setTab(int index)
|
|||
}
|
||||
}
|
||||
|
||||
AbstractSettingsPage *DlgSettings::page(SettingsPage which) const
|
||||
{
|
||||
return pages.value(static_cast<int>(which));
|
||||
}
|
||||
|
||||
void DlgSettings::updateLanguage()
|
||||
{
|
||||
qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ public:
|
|||
|
||||
explicit DlgSettings(QWidget *parent = nullptr);
|
||||
void setTab(int index);
|
||||
AbstractSettingsPage *page(SettingsPage which) const;
|
||||
|
||||
private slots:
|
||||
void onTabClicked(int index);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ namespace HomeTabButtonColor
|
|||
*/
|
||||
enum Source
|
||||
{
|
||||
Automatic, ///< Extract color from background, or use theme color if no background
|
||||
FromBackground, ///< Always extract color from background
|
||||
FromThemeColors, ///< Use the theme's identity accent colors
|
||||
FromBackground, ///< Extract colour from the background image
|
||||
};
|
||||
|
||||
struct Entry
|
||||
|
|
@ -23,7 +23,7 @@ struct Entry
|
|||
|
||||
inline QList<Entry> all()
|
||||
{
|
||||
static QList<Entry> entries = {{Automatic, QT_TR_NOOP("Automatic")},
|
||||
static QList<Entry> entries = {{FromThemeColors, QT_TR_NOOP("From theme colors")},
|
||||
{FromBackground, QT_TR_NOOP("Extract from background")}};
|
||||
|
||||
return entries;
|
||||
|
|
@ -33,12 +33,12 @@ inline QList<Entry> all()
|
|||
* Safely converts an int into the corresponding Source.
|
||||
*
|
||||
* @param value The int value
|
||||
* @return The Source. Returns Source::Automatic if the value is not within range
|
||||
* @return The Source. Returns Source::FromThemeColors if the value is not within range
|
||||
*/
|
||||
inline Source intToSource(int value)
|
||||
{
|
||||
if (value > FromBackground) {
|
||||
return Automatic; // default
|
||||
return FromThemeColors; // default
|
||||
}
|
||||
|
||||
return static_cast<Source>(value);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
#include "home_tab_button_color.h"
|
||||
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QLinearGradient>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPushButton>
|
||||
|
|
@ -21,8 +23,7 @@
|
|||
#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")))
|
||||
: QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home")))
|
||||
{
|
||||
layout = new QGridLayout(this);
|
||||
|
||||
|
|
@ -54,6 +55,8 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|||
// 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,
|
||||
|
|
@ -61,12 +64,18 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|||
// 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::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);
|
||||
|
|
@ -105,32 +114,24 @@ void HomeWidget::loadBackgroundSourceDeck()
|
|||
backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList();
|
||||
}
|
||||
|
||||
static bool isDefaultBackgroundAndTheme()
|
||||
static QPair<QColor, QColor> paletteDerivedButtonColors()
|
||||
{
|
||||
QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource();
|
||||
return themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme;
|
||||
return {themeManager->appColor(AppColor::AccentStrong), themeManager->appColor(AppColor::AccentSoft)};
|
||||
}
|
||||
|
||||
QPair<QColor, QColor> HomeWidget::determineButtonColor() const
|
||||
{
|
||||
static QPair defaultColor = {QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)};
|
||||
|
||||
auto colorSource =
|
||||
HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex());
|
||||
|
||||
switch (colorSource) {
|
||||
case HomeTabButtonColor::Automatic: {
|
||||
if (isDefaultBackgroundAndTheme()) {
|
||||
return defaultColor;
|
||||
} else {
|
||||
return extractDominantColors(background);
|
||||
}
|
||||
}
|
||||
case HomeTabButtonColor::FromThemeColors:
|
||||
return paletteDerivedButtonColors();
|
||||
case HomeTabButtonColor::FromBackground:
|
||||
return extractDominantColors(background);
|
||||
}
|
||||
|
||||
return defaultColor;
|
||||
return paletteDerivedButtonColors();
|
||||
}
|
||||
|
||||
void HomeWidget::setRandomCard(ExactCard &newCard)
|
||||
|
|
@ -234,10 +235,10 @@ QGroupBox *HomeWidget::createButtons()
|
|||
QVBoxLayout *boxLayout = new QVBoxLayout;
|
||||
boxLayout->setAlignment(Qt::AlignHCenter);
|
||||
|
||||
QLabel *logoLabel = new QLabel;
|
||||
logoLabel->setPixmap(overlay.scaledToWidth(200, Qt::SmoothTransformation));
|
||||
logoLabel = new QLabel;
|
||||
logoLabel->setAlignment(Qt::AlignCenter);
|
||||
boxLayout->addWidget(logoLabel);
|
||||
updateLogoOverlay();
|
||||
boxLayout->addSpacing(25);
|
||||
|
||||
connectButton = new HomeStyledButton("Connect/Play", gradientColors);
|
||||
|
|
@ -365,13 +366,15 @@ void HomeWidget::paintEvent(QPaintEvent *event)
|
|||
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);
|
||||
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<int>(255 * 0.33));
|
||||
painter.fillPath(roundedRectPath, semiTransparentBlack);
|
||||
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;
|
||||
|
|
@ -436,3 +439,56 @@ void HomeWidget::paintEvent(QPaintEvent *event)
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
#include <QWidget>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
|
||||
class QGridLayout;
|
||||
class QLabel;
|
||||
|
||||
class HomeWidget : public QWidget
|
||||
{
|
||||
|
||||
|
|
@ -41,13 +44,14 @@ private:
|
|||
QPixmap background;
|
||||
CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr;
|
||||
DeckList backgroundSourceDeck;
|
||||
QPixmap overlay;
|
||||
QLabel *logoLabel = nullptr;
|
||||
QPair<QColor, QColor> gradientColors;
|
||||
HomeStyledButton *connectButton;
|
||||
|
||||
void setRandomCard(ExactCard &newCard);
|
||||
void loadBackgroundSourceDeck();
|
||||
QPair<QColor, QColor> determineButtonColor() const;
|
||||
void updateLogoOverlay();
|
||||
};
|
||||
|
||||
#endif // HOME_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ class BannerShaderConfig : public QObject
|
|||
Q_PROPERTY(QColor colorA READ colorA WRITE setColorA NOTIFY colorAChanged)
|
||||
Q_PROPERTY(QColor colorB READ colorB WRITE setColorB NOTIFY colorBChanged)
|
||||
Q_PROPERTY(QColor accent READ accent WRITE setAccent NOTIFY accentChanged)
|
||||
Q_PROPERTY(QColor glowColor READ glowColor WRITE setGlowColor NOTIFY glowColorChanged)
|
||||
Q_PROPERTY(QColor brandStrong READ brandStrong WRITE setBrandStrong NOTIFY brandStrongChanged)
|
||||
Q_PROPERTY(QColor brandSoft READ brandSoft WRITE setBrandSoft NOTIFY brandSoftChanged)
|
||||
Q_PROPERTY(qreal vignetteMin READ vignetteMin WRITE setVignetteMin NOTIFY vignetteMinChanged)
|
||||
|
||||
Q_PROPERTY(bool logoVisible READ logoVisible WRITE setLogoVisible NOTIFY logoVisibleChanged)
|
||||
Q_PROPERTY(qreal logoGlow READ logoGlow WRITE setLogoGlow NOTIFY logoGlowChanged)
|
||||
|
|
@ -185,6 +189,54 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
QColor glowColor() const
|
||||
{
|
||||
return m_glowColor;
|
||||
}
|
||||
void setGlowColor(const QColor &c)
|
||||
{
|
||||
if (c != m_glowColor) {
|
||||
m_glowColor = c;
|
||||
emit glowColorChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QColor brandStrong() const
|
||||
{
|
||||
return m_brandStrong;
|
||||
}
|
||||
void setBrandStrong(const QColor &c)
|
||||
{
|
||||
if (c != m_brandStrong) {
|
||||
m_brandStrong = c;
|
||||
emit brandStrongChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QColor brandSoft() const
|
||||
{
|
||||
return m_brandSoft;
|
||||
}
|
||||
void setBrandSoft(const QColor &c)
|
||||
{
|
||||
if (c != m_brandSoft) {
|
||||
m_brandSoft = c;
|
||||
emit brandSoftChanged();
|
||||
}
|
||||
}
|
||||
|
||||
qreal vignetteMin() const
|
||||
{
|
||||
return m_vignetteMin;
|
||||
}
|
||||
void setVignetteMin(qreal v)
|
||||
{
|
||||
if (v != m_vignetteMin) {
|
||||
m_vignetteMin = v;
|
||||
emit vignetteMinChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool logoVisible() const
|
||||
{
|
||||
return m_logoVisible;
|
||||
|
|
@ -222,6 +274,10 @@ signals:
|
|||
void colorAChanged();
|
||||
void colorBChanged();
|
||||
void accentChanged();
|
||||
void glowColorChanged();
|
||||
void brandStrongChanged();
|
||||
void brandSoftChanged();
|
||||
void vignetteMinChanged();
|
||||
void logoVisibleChanged();
|
||||
void logoGlowChanged();
|
||||
|
||||
|
|
@ -239,9 +295,16 @@ private:
|
|||
|
||||
bool m_frontIsA = true;
|
||||
|
||||
// Curated fallback seed values -- BannerHost overwrites these with
|
||||
// palette-derived colours (see shader_banner_widget.cpp) before the first
|
||||
// paint, so they only matter as a safe pre-first-apply default.
|
||||
QColor m_colorA{0x1A, 0x1A, 0x20};
|
||||
QColor m_colorB{0x0E, 0x0E, 0x12};
|
||||
QColor m_accent{0x8B, 0xDD, 0x6B};
|
||||
QColor m_glowColor{Qt::white};
|
||||
QColor m_brandStrong{0x13, 0x97, 0x40};
|
||||
QColor m_brandSoft{0xC9, 0xFD, 0x62};
|
||||
qreal m_vignetteMin = 0.62;
|
||||
|
||||
bool m_logoVisible = false;
|
||||
qreal m_logoGlow = 1.0;
|
||||
|
|
|
|||
15
cockatrice/src/interface/widgets/onboarding/brand_colors.h
Normal file
15
cockatrice/src/interface/widgets/onboarding/brand_colors.h
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#ifndef BRAND_COLORS_H
|
||||
#define BRAND_COLORS_H
|
||||
|
||||
#include <QColor>
|
||||
|
||||
/** @brief Cockatrice brand green.
|
||||
*
|
||||
* Single source of truth for the onboarding brand accent: it backs the
|
||||
* banner's shader-accent uniform as the curated fallback when the active
|
||||
* palette resolves no usable Highlight, and it preseads the wizard's
|
||||
* QuickSetupPanel so a freshly generated palette keeps the brand identity
|
||||
* until the user picks their own look. */
|
||||
inline const QColor kCockatriceBrandGreen(0x8B, 0xDD, 0x6B);
|
||||
|
||||
#endif // BRAND_COLORS_H
|
||||
|
|
@ -14,10 +14,31 @@
|
|||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPalette>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
/** @brief A theme's shipped identity accent, immune to any user- or auto-
|
||||
* generated palette that may currently be masking appColor(). */
|
||||
QColor themeIdentityAccent(const QString &themeDirPath, const QString &themeName)
|
||||
{
|
||||
for (const QString &scheme : {QStringLiteral("Light"), QStringLiteral("Dark")}) {
|
||||
const PaletteConfig cfg = ThemeManager::loadDefaultPaletteConfig(themeDirPath, themeName, scheme);
|
||||
if (cfg.appColors.contains(AppColor::AccentStrong)) {
|
||||
return cfg.appColors.value(AppColor::AccentStrong);
|
||||
}
|
||||
const QColor highlight = cfg.colors.value(QPalette::Active).value(QPalette::Highlight);
|
||||
if (highlight.isValid()) {
|
||||
return highlight;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
themeCombo = new QComboBox(this);
|
||||
|
|
@ -30,6 +51,15 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
|||
|
||||
quickSetupPanel = new QuickSetupPanel(this);
|
||||
|
||||
// Seed the picker from the current theme's own identity accent rather than
|
||||
// a hardcoded brand green: Plasma seeds violet, Fusion green, etc., and it
|
||||
// is immune to stale generated palettes that may mask appColor(). This was
|
||||
// initially a brand-green workaround from before Fusion became the default.
|
||||
// setAccentColor blocks signals, so this never triggers a generation.
|
||||
lastSeededTheme = SettingsCache::instance().getThemeName();
|
||||
quickSetupPanel->setAccentColor(
|
||||
themeIdentityAccent(themeManager->getAvailableThemes().value(lastSeededTheme), lastSeededTheme));
|
||||
|
||||
connect(themeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onThemeChanged);
|
||||
connect(schemeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onSchemeChanged);
|
||||
connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &ThemeSetupPage::onGenerateFromAccent);
|
||||
|
|
@ -47,7 +77,8 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
|||
// Mirrors AppearanceSettingsPage's identical listener for the combo-sync
|
||||
// half of this.
|
||||
connect(themeManager, &ThemeManager::themeChanged, this, [this] {
|
||||
const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||
const QString newTheme = SettingsCache::instance().getThemeName();
|
||||
const QString newDir = themeManager->getAvailableThemes().value(newTheme);
|
||||
const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir);
|
||||
const QString current = cfg.colorScheme;
|
||||
|
||||
|
|
@ -56,6 +87,14 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
|||
schemeCombo->setCurrentIndex(idx >= 0 ? idx : 0);
|
||||
schemeCombo->blockSignals(false);
|
||||
|
||||
// Keep the picker's accent in step with the theme's own identity; the
|
||||
// swatch seeded at construction would otherwise stay stale (e.g. green
|
||||
// from a previous theme) when the user toggles themes.
|
||||
if (newTheme != lastSeededTheme) {
|
||||
lastSeededTheme = newTheme;
|
||||
quickSetupPanel->setAccentColor(themeIdentityAccent(newDir, newTheme));
|
||||
}
|
||||
|
||||
maybeAutoGeneratePalette();
|
||||
});
|
||||
|
||||
|
|
@ -158,8 +197,14 @@ void ThemeSetupPage::maybeAutoGeneratePalette()
|
|||
const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||
const QString scheme = resolvedScheme();
|
||||
|
||||
// The theme dir may resolve to the user profile even for built-in themes
|
||||
// (getAvailableThemes gives the user copy precedence), so consult the
|
||||
// shipped palette too -- both via loadDefaultPaletteConfig's system fallback.
|
||||
// Without it, scheme flips regenerate a fresh palette from the picker accent
|
||||
// and clobber the curated colours the theme explicitly ships.
|
||||
if (PaletteConfig::fromScheme(dirPath, scheme).hasPalette() ||
|
||||
PaletteConfig::fromDefault(dirPath, scheme).hasPalette()) {
|
||||
ThemeManager::loadDefaultPaletteConfig(dirPath, SettingsCache::instance().getThemeName(), scheme)
|
||||
.hasPalette()) {
|
||||
return; // theme already has something real to show -- leave it alone
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ private:
|
|||
QComboBox *homeTabBackgroundCombo;
|
||||
|
||||
bool paletteDirty = false;
|
||||
|
||||
/// Theme whose identity accent currently seeds the picker.
|
||||
QString lastSeededTheme;
|
||||
};
|
||||
|
||||
#endif // THEME_SETUP_PAGE_H
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ Item {
|
|||
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0)
|
||||
property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0)
|
||||
property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0)
|
||||
property vector4d uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0)
|
||||
property real uVignetteMin: bannerConfig.vignetteMin
|
||||
property real uLogoGlow: bannerConfig.logoGlow
|
||||
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
|
||||
}
|
||||
|
|
@ -33,30 +35,62 @@ Item {
|
|||
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0)
|
||||
property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0)
|
||||
property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0)
|
||||
property vector4d uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0)
|
||||
property real uVignetteMin: bannerConfig.vignetteMin
|
||||
property real uLogoGlow: bannerConfig.logoGlow
|
||||
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
|
||||
}
|
||||
|
||||
// The hero logo itself — breathes cleanly over a 0.5–1.0 opacity range
|
||||
Image {
|
||||
id: logo
|
||||
anchors.centerIn: parent
|
||||
// The white logo sits at full opacity on top of the static gradient plate —
|
||||
// no glow, no breathing. The plate matches home_widget's QPainter composite.
|
||||
Item {
|
||||
id: logoHost
|
||||
visible: bannerConfig.logoVisible
|
||||
source: "qrc:/resources/cockatrice-logo-white.svg"
|
||||
anchors.centerIn: parent
|
||||
width: root.height * 0.6
|
||||
height: width * (sourceSize.height > 0 ? sourceSize.height / Math.max(sourceSize.width, 1) : 1)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
opacity: 0.5 + 0.5 * bannerConfig.logoGlow
|
||||
sourceSize: Qt.size(256, 256)
|
||||
height: width
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 300; easing.type: Easing.InOutSine } }
|
||||
// The full-color logo renders beneath the white mark and is consumed as
|
||||
// a texture (layer.enabled) by the plate shader's silhouette mask, so
|
||||
// the gradient is clipped to the bird exactly as the SVG's gradient
|
||||
// paths are. It is never drawn to the screen itself.
|
||||
Image {
|
||||
id: silhouetteMask
|
||||
anchors.fill: parent
|
||||
source: "qrc:/resources/cockatrice.svg"
|
||||
sourceSize: Qt.size(256, 256)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
}
|
||||
|
||||
transform: Scale {
|
||||
origin.x: logo.width / 2
|
||||
origin.y: logo.height / 2
|
||||
xScale: 0.94 + 0.06 * bannerConfig.logoGlow
|
||||
yScale: 0.94 + 0.06 * bannerConfig.logoGlow
|
||||
// The logo's gradient plate, drawn behind the mark: a linear
|
||||
// AccentSoft (light) -> AccentStrong (dark) sheet along the same
|
||||
// top-left -> bottom-right userSpaceOnUse axis the baked-in SVG used,
|
||||
// clipped to the bird silhouette via uSilhouette. The white highlight
|
||||
// path above is theme independent. Sized to the logo itself — no
|
||||
// rounded badge, matching home_widget's QPainter composite. Static.
|
||||
// Small ShaderEffect, Qt 6.4-safe.
|
||||
ShaderEffect {
|
||||
id: brandPlate
|
||||
anchors.fill: parent
|
||||
property vector4d uStrong: Qt.vector4d(bannerConfig.brandStrong.r, bannerConfig.brandStrong.g,
|
||||
bannerConfig.brandStrong.b, 1.0)
|
||||
property vector4d uSoft: Qt.vector4d(bannerConfig.brandSoft.r, bannerConfig.brandSoft.g,
|
||||
bannerConfig.brandSoft.b, 1.0)
|
||||
property var uSilhouette: silhouetteMask
|
||||
fragmentShader: "qrc:/onboarding/shaders/brand_plate.frag.qsb"
|
||||
}
|
||||
|
||||
Image {
|
||||
id: logoImage
|
||||
anchors.fill: parent
|
||||
source: "qrc:/resources/cockatrice-logo-white.svg"
|
||||
sourceSize: Qt.size(256, 256)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
#include "shader_banner_widget.h"
|
||||
|
||||
#include "../../theme_manager.h"
|
||||
#include "banner_shader_config.h"
|
||||
#include "brand_colors.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QPainter>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
|
|
@ -11,11 +14,98 @@
|
|||
|
||||
namespace
|
||||
{
|
||||
// Near-black base palette -- the background is dark and quiet so the green
|
||||
// accent stands out.
|
||||
constexpr QRgb kColorA = 0x1A1A20;
|
||||
constexpr QRgb kColorB = 0x0E0E12;
|
||||
constexpr QRgb kAccent = 0x8BDD6B;
|
||||
// Curated near-black stage -- used only when the active palette resolves no
|
||||
// usable window colour. Matches the banner's original design (dark and quiet
|
||||
// so the accent stands out) and satisfies design-plans §2.1's "identity
|
||||
// survives a bare palette".
|
||||
constexpr QRgb kFallbackColorA = 0x1A1A20;
|
||||
constexpr QRgb kFallbackColorB = 0x0E0E12;
|
||||
|
||||
struct SuggestedColors
|
||||
{
|
||||
QColor colorA;
|
||||
QColor colorB;
|
||||
QColor accent;
|
||||
QColor glowColor;
|
||||
QColor brandStrong;
|
||||
QColor brandSoft;
|
||||
qreal vignetteMin = 0.62;
|
||||
};
|
||||
|
||||
SuggestedColors suggestedBannerColors()
|
||||
{
|
||||
const QPalette &pal = qApp->palette();
|
||||
const QColor window = pal.color(QPalette::Active, QPalette::Window);
|
||||
// Identity accent: the theme's [AppColors] AccentStrong, which appColor()
|
||||
// resolves to QPalette::Highlight when a theme doesn't pin AccentStrong.
|
||||
// Reading bare Highlight ignored curated accent tokens (Plasma's violet
|
||||
// vs Default's green) whenever a palette didn't set the role itself.
|
||||
const QColor accentStrong = themeManager->appColor(AppColor::AccentStrong);
|
||||
if (!window.isValid() || !accentStrong.isValid()) {
|
||||
return {QColor(kFallbackColorA),
|
||||
QColor(kFallbackColorB),
|
||||
kCockatriceBrandGreen,
|
||||
QColor(Qt::white),
|
||||
kCockatriceBrandGreen,
|
||||
QColor(0xC9, 0xFD, 0x62),
|
||||
0.62};
|
||||
}
|
||||
|
||||
// The theme's brand pair: AccentStrong is the deep green, AccentSoft the
|
||||
// lime. These two appColors form the logo's "surrounding gradient" (deep
|
||||
// core grading out to the soft, brand-toned glow) on both the banner and
|
||||
// the home screen.
|
||||
const QColor brandStrong = accentStrong;
|
||||
const QColor brandSoft = themeManager->appColor(AppColor::AccentSoft);
|
||||
|
||||
// Dress the stage for the scheme so the banner never fights the
|
||||
// surrounding window in either mode. Dark palettes keep the original
|
||||
// quiet near-black stage (lightness 29 → 16) with the theme's window
|
||||
// hue; light palettes get a pastel "frosted accent" treatment built from
|
||||
// the accent hue instead of a plain near-white copy: a coloured wash that
|
||||
// clearly belongs to the theme.
|
||||
const qreal luma = 0.299 * window.red() + 0.587 * window.green() + 0.114 * window.blue();
|
||||
const bool lightStage = luma > 115.0;
|
||||
if (lightStage) {
|
||||
const int hue = accentStrong.hslHue();
|
||||
// Achromatic accents (grey) get a neutral near-white stage instead.
|
||||
const int stageSat = hue < 0 ? 0 : 64;
|
||||
const int hueSafe = hue < 0 ? 0 : hue;
|
||||
// Depth is what stops a light stage reading as a washed-out near-white
|
||||
// copy of the page behind the banner: deepen the lower pastel band and
|
||||
// raise saturation so the hue is clearly present while staying frosted.
|
||||
auto pastel = [hueSafe, stageSat](int lightness) { return QColor::fromHsl(hueSafe, stageSat, lightness); };
|
||||
auto pastelLower = [hueSafe](int lightness) { return QColor::fromHsl(hueSafe, 76, lightness); };
|
||||
// Brightness-lifted accent for additive glows: the raw accent on a
|
||||
// light stage must be mid-bright to read instead of washing out, so
|
||||
// lift lightness and saturation together.
|
||||
const int accentLightness = qBound(158, accentStrong.lightness() + 82, 198);
|
||||
const int accentSaturation = hue < 0 ? 0 : qMax(accentStrong.hslSaturation(), 180);
|
||||
const QColor liftedAccent =
|
||||
hue < 0 ? accentStrong : QColor::fromHsl(hueSafe, accentSaturation, accentLightness);
|
||||
// The centre glow (and logo tint in QML) uses the deep accent itself:
|
||||
// a coloured halo/fill behind the logo instead of a white or black one.
|
||||
return {pastel(214), pastelLower(186), liftedAccent, accentStrong, brandStrong, brandSoft, 0.80};
|
||||
}
|
||||
|
||||
// Dark stage: force the window hue down to the banner's curated darkness,
|
||||
// scaling saturation away so chromatic palettes tint it without going
|
||||
// muddy. The accent is the bright, brand-driven tone (hue from the accent
|
||||
// itself, never the -- often grey -- window), and it drives both the
|
||||
// embers/fog and the logo glow so the mark tints like the light stage.
|
||||
auto stage = [&window](int lightness) {
|
||||
const int hue = window.hslHue();
|
||||
const int saturation = hue < 0 ? 0 : qBound(0, qRound(window.hslSaturation() * (lightness / 40.0)), 255);
|
||||
return QColor::fromHsl(hue, saturation, lightness);
|
||||
};
|
||||
const int accentHue = accentStrong.hslHue();
|
||||
const int accentHueSafe = accentHue < 0 ? 0 : accentHue;
|
||||
const int accentLightness = qBound(150, accentStrong.lightness() + 70, 185);
|
||||
const int accentSaturation = accentHue < 0 ? 0 : qMax(accentStrong.hslSaturation(), 160);
|
||||
const QColor accent =
|
||||
accentHue < 0 ? accentStrong : QColor::fromHsl(accentHueSafe, accentSaturation, accentLightness);
|
||||
return {stage(29), stage(16), accent, accent, brandStrong, brandSoft, 0.62};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class GradientFallbackWidget : public QWidget
|
||||
|
|
@ -23,15 +113,27 @@ class GradientFallbackWidget : public QWidget
|
|||
public:
|
||||
using QWidget::QWidget;
|
||||
|
||||
void setColors(const QColor &a, const QColor &b)
|
||||
{
|
||||
if (a != colorA || b != colorB) {
|
||||
colorA = a;
|
||||
colorB = b;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override
|
||||
{
|
||||
QPainter painter(this);
|
||||
QLinearGradient gradient(0, 0, width(), height());
|
||||
gradient.setColorAt(0.0, QColor(kColorA));
|
||||
gradient.setColorAt(1.0, QColor(kColorB));
|
||||
gradient.setColorAt(0.0, colorA);
|
||||
gradient.setColorAt(1.0, colorB);
|
||||
painter.fillRect(rect(), gradient);
|
||||
}
|
||||
|
||||
private:
|
||||
QColor colorA{QColor(kFallbackColorA)};
|
||||
QColor colorB{QColor(kFallbackColorB)};
|
||||
};
|
||||
|
||||
BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
|
||||
|
|
@ -62,6 +164,9 @@ BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
|
|||
connect(&clock, &QTimer::timeout, this, &BannerHost::tick);
|
||||
clock.setInterval(16); // ~60fps; the shader itself is cheap, this is just a wall clock
|
||||
|
||||
connect(themeManager, &ThemeManager::themeChanged, this, &BannerHost::applyThemeColors);
|
||||
applyThemeColors();
|
||||
|
||||
applyMotifPreset(currentMotif);
|
||||
updateAspect();
|
||||
}
|
||||
|
|
@ -123,9 +228,9 @@ void BannerHost::applyMotifPreset(Motif motif)
|
|||
|
||||
const Preset p = presetFor(motif);
|
||||
|
||||
config->setColorA(QColor(kColorA));
|
||||
config->setColorB(QColor(kColorB));
|
||||
config->setAccent(QColor(kAccent));
|
||||
config->setColorA(bannerColorA);
|
||||
config->setColorB(bannerColorB);
|
||||
config->setAccent(bannerAccent);
|
||||
config->setLogoVisible(motif == Motif::Welcome);
|
||||
|
||||
if (isFirstApply) {
|
||||
|
|
@ -183,8 +288,34 @@ void BannerHost::hideEvent(QHideEvent *event)
|
|||
clock.stop();
|
||||
}
|
||||
|
||||
void BannerHost::applyThemeColors()
|
||||
{
|
||||
const SuggestedColors colors = suggestedBannerColors();
|
||||
bannerColorA = colors.colorA;
|
||||
bannerColorB = colors.colorB;
|
||||
bannerAccent = colors.accent;
|
||||
|
||||
if (usingFallback) {
|
||||
fallback->setColors(bannerColorA, bannerColorB);
|
||||
fallback->update();
|
||||
} else if (config) {
|
||||
config->setColorA(bannerColorA);
|
||||
config->setColorB(bannerColorB);
|
||||
config->setAccent(bannerAccent);
|
||||
config->setGlowColor(colors.glowColor);
|
||||
config->setBrandStrong(colors.brandStrong);
|
||||
config->setBrandSoft(colors.brandSoft);
|
||||
config->setVignetteMin(colors.vignetteMin);
|
||||
}
|
||||
}
|
||||
|
||||
void BannerHost::tick()
|
||||
{
|
||||
// Palette previews (e.g. accent drags in the wizard's QuickSetupPanel)
|
||||
// apply qApp->palette() without firing themeChanged, so re-derive here;
|
||||
// BannerShaderConfig's setters are equality-guarded, so this is a no-op
|
||||
// unless the colours actually changed.
|
||||
applyThemeColors();
|
||||
if (config) {
|
||||
qreal t = elapsed.elapsed() / 1000.0;
|
||||
config->setTime(t);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#ifndef SHADER_BANNER_WIDGET_H
|
||||
#define SHADER_BANNER_WIDGET_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QElapsedTimer>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
|
@ -53,6 +54,7 @@ protected:
|
|||
private slots:
|
||||
void tick();
|
||||
void onSceneGraphFailed();
|
||||
void applyThemeColors();
|
||||
|
||||
private:
|
||||
struct Preset
|
||||
|
|
@ -73,6 +75,12 @@ private:
|
|||
BannerShaderConfig *config = nullptr;
|
||||
GradientFallbackWidget *fallback = nullptr;
|
||||
|
||||
// Palette-derived banner colours -- the theme's window hue forced down to
|
||||
// the banner's curated darkness, plus the theme's Highlight as accent.
|
||||
QColor bannerColorA;
|
||||
QColor bannerColorB;
|
||||
QColor bannerAccent;
|
||||
|
||||
QTimer clock;
|
||||
QElapsedTimer elapsed;
|
||||
Motif currentMotif = Motif::Welcome;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ layout(std140, binding = 0) uniform buf
|
|||
vec4 uColorA;
|
||||
vec4 uColorB;
|
||||
vec4 uAccent;
|
||||
vec4 uGlowColor;
|
||||
float uVignetteMin;
|
||||
float uLogoGlow;
|
||||
};
|
||||
|
||||
|
|
@ -119,7 +121,7 @@ vec3 backgroundField(vec2 uv, float time)
|
|||
|
||||
// Accent-coloured fog layer: flowNoise peaks above 0.6 contribute accent
|
||||
float fog = flowNoise(uv * 0.8 + vec2(100.0, 50.0), time * 0.02);
|
||||
col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.10;
|
||||
col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.14;
|
||||
|
||||
return col;
|
||||
}
|
||||
|
|
@ -136,14 +138,17 @@ vec3 motifWelcome(vec2 uv, vec3 bg, float t)
|
|||
vec2 center = vec2(asp * 0.5, 0.5);
|
||||
float cDist = length(ac - center);
|
||||
|
||||
// Centre bloom at logo position; intensity scales with uLogoGlow
|
||||
// Centre bloom at logo position; intensity scales with uLogoGlow. The
|
||||
// QML brandGlow halo now supplies the primary logo surround (the two
|
||||
// brand appColors), so this shader bloom is deliberately kept as a subtle
|
||||
// ambience rather than a competing glow.
|
||||
float centreLight = bloom(cDist, 0.08 * asp, 0.40 * asp);
|
||||
col += centreLight * 0.20 * uLogoGlow;
|
||||
col += uGlowColor.rgb * centreLight * 0.20 * uLogoGlow;
|
||||
|
||||
// Flow-noise shimmer gated by Gaussian mask at centre
|
||||
float shimmer = flowNoise(ac * 0.8 + vec2(55.0, 33.0), t * 0.05) * 0.5 + 0.5;
|
||||
float shimmerMask = exp(-(cDist * cDist) / (0.18 * asp * 0.18 * asp));
|
||||
col += shimmer * shimmerMask * 0.04 * uLogoGlow;
|
||||
col += uGlowColor.rgb * shimmer * shimmerMask * 0.04 * uLogoGlow;
|
||||
|
||||
// 48 ember particles: hash-seeded position, speed, size, brightness.
|
||||
// Embers within a distance threshold of centre are deflected into an
|
||||
|
|
@ -162,8 +167,8 @@ vec3 motifWelcome(vec2 uv, vec3 bg, float t)
|
|||
float pX = baseX * asp + sin(t * driftFreq + fi * 1.7) * driftAmp * asp;
|
||||
float pY = fract(baseY + t * riseSpeed);
|
||||
|
||||
float size = 0.006 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.012;
|
||||
float bright = 0.15 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.30;
|
||||
float size = 0.010 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.014;
|
||||
float bright = 0.18 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.32;
|
||||
|
||||
// Fade out near top/bottom edges
|
||||
float edgeFade = smoothstep(0.0, 0.12, pY) * smoothstep(1.0, 0.88, pY);
|
||||
|
|
@ -232,7 +237,7 @@ vec3 motifCardDatabase(vec2 uv, vec3 bg, float t)
|
|||
|
||||
// Semi-transparent dark fill
|
||||
float fill = smoothstep(0.015, -0.005, d);
|
||||
col = mix(col, uColorB.rgb * 0.55, fill * 0.50);
|
||||
col = mix(col, uColorB.rgb * 0.60, fill * 0.62);
|
||||
|
||||
// Accent outline
|
||||
float edge = smoothstep(0.035, 0.0, abs(d));
|
||||
|
|
@ -309,7 +314,7 @@ vec3 motifAccount(vec2 uv, vec3 bg, float t)
|
|||
|
||||
// Node glow via bloom; intensity modulated by pulse
|
||||
float dist = length(ac - pos);
|
||||
col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.20, 0.45, pulse);
|
||||
col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.30, 0.55, pulse);
|
||||
}
|
||||
|
||||
// Edges: connect nodes within a radius threshold
|
||||
|
|
@ -323,19 +328,19 @@ vec3 motifAccount(vec2 uv, vec3 bg, float t)
|
|||
vec2 ba = nodePos[j] - nodePos[i];
|
||||
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
||||
float lineDist = length(pa - ba * h);
|
||||
col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.10;
|
||||
col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.14;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Central bloom at banner centre
|
||||
float cDist = length(ac - center);
|
||||
col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.12;
|
||||
col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.20;
|
||||
|
||||
// Periodic expanding ring from centre
|
||||
float ripplePhase = t * 0.4;
|
||||
float rippleDist = abs(cDist - fract(ripplePhase) * asp * 0.7);
|
||||
col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.10;
|
||||
col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.14;
|
||||
|
||||
return col;
|
||||
}
|
||||
|
|
@ -456,6 +461,8 @@ void main()
|
|||
else if (uMode < 4.5) col = motifPreferences(uv, bg, t);
|
||||
else col = motifFinish(uv, bg, t);
|
||||
|
||||
col *= mix(0.62, 1.0, vignette(uv));
|
||||
// Corner vignette; uVignetteMin is scheme-driven (0.62 on dark stages,
|
||||
// gentler on light ones so near-white corners don't go muddy grey).
|
||||
col *= mix(uVignetteMin, 1.0, vignette(uv));
|
||||
fragColor = vec4(col, 1.0) * qt_Opacity;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
#version 440
|
||||
|
||||
// The logo's gradient plate, drawn by us rather than the baked-in SVG: a
|
||||
// linear blend between the two brand appColors (light AccentSoft at the
|
||||
// top-left grading to dark AccentStrong at the bottom-right, mirroring
|
||||
// cockatrice.svg's linearGradient4265-7-8 userSpaceOnUse axis), clipped to
|
||||
// the bird's full silhouette via the full-color logo's alpha (uSilhouette).
|
||||
// The white highlight path (cockatrice-logo-white) is overlaid in QML on top,
|
||||
// exactly as the SVG stacks its white path over the gradient paths. Fully
|
||||
// static: no glow, no breathing — the plate just sits there like the home
|
||||
// widget's QPainter composite.
|
||||
|
||||
layout(location = 0) in vec2 qt_TexCoord0;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(std140, binding = 0) uniform buf
|
||||
{
|
||||
mat4 qt_Matrix;
|
||||
float qt_Opacity;
|
||||
vec4 uStrong;
|
||||
vec4 uSoft;
|
||||
};
|
||||
|
||||
// The full-color logo's alpha channel acts as the silhouette mask: the
|
||||
// gradient only appears inside the bird, exactly like the SVG's gradient paths.
|
||||
layout(binding = 1) uniform sampler2D uSilhouette;
|
||||
|
||||
void main()
|
||||
{
|
||||
// Recreate cockatrice.svg's own gradient geometry (linearGradient4265-7-8,
|
||||
// userSpaceOnUse): light AccentSoft at the start point S=(-8.097,-97.746),
|
||||
// dark AccentStrong at the end E=(162.455,295.208), on the SVG's 300x300
|
||||
// canvas. Normalized to UV space, V=E-S=(0.5685,1.3098), so
|
||||
// t = dot(uv - S_norm, V)/|V|^2 with S_norm=(-0.0270,-0.3258).
|
||||
float t = clamp(dot(qt_TexCoord0 - vec2(-0.02699, -0.32582), vec2(0.56851, 1.30985)) / 2.03891, 0.0, 1.0);
|
||||
vec3 color = mix(uSoft.rgb, uStrong.rgb, t);
|
||||
|
||||
// Anti-aliased silhouette clip from the full-color logo's alpha.
|
||||
float alpha = texture(uSilhouette, qt_TexCoord0).a;
|
||||
|
||||
fragColor = vec4(color * alpha, alpha) * qt_Opacity;
|
||||
}
|
||||
|
|
@ -1,13 +1,19 @@
|
|||
#include "printing_selector_card_overlay_widget.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../card_picture_loader/card_picture_loader.h"
|
||||
#include "../cards/card_info_picture_widget.h"
|
||||
#include "printing_selector_card_display_widget.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QFileDialog>
|
||||
#include <QImageReader>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QMouseEvent>
|
||||
#include <QPixmapCache>
|
||||
#include <QScreen>
|
||||
#include <QVBoxLayout>
|
||||
#include <QtMath>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
|
|
@ -15,6 +21,49 @@
|
|||
#include <libcockatrice/settings/card_override_settings.h>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
{
|
||||
/**
|
||||
* @brief Places the preview beside the highlighted action inside the given screen.
|
||||
*
|
||||
* Side-aware: hugs the side of the action that has room, aligned with its row, then
|
||||
* clamps every edge so the preview always lands fully on-screen on first show.
|
||||
*/
|
||||
QPoint
|
||||
previewPositionNear(const QRect &actionRect, const QSize &labelSize, const QRect &screenGeometry, int previewOffset)
|
||||
{
|
||||
const bool rightFits = actionRect.right() + previewOffset + labelSize.width() <= screenGeometry.right();
|
||||
const bool leftFits = actionRect.left() - previewOffset - labelSize.width() >= screenGeometry.left();
|
||||
|
||||
int x;
|
||||
if (rightFits) {
|
||||
x = actionRect.right() + previewOffset;
|
||||
} else if (leftFits) {
|
||||
x = actionRect.left() - previewOffset - labelSize.width();
|
||||
} else {
|
||||
x = actionRect.left();
|
||||
}
|
||||
x = qMax(screenGeometry.left(), x);
|
||||
x = qMin(screenGeometry.right() - labelSize.width() + 1, x);
|
||||
|
||||
const bool belowFits = actionRect.bottom() + previewOffset + labelSize.height() <= screenGeometry.bottom();
|
||||
const bool aboveFits = actionRect.top() - previewOffset - labelSize.height() >= screenGeometry.top();
|
||||
|
||||
int y;
|
||||
if (belowFits) {
|
||||
y = actionRect.bottom() + previewOffset;
|
||||
} else if (aboveFits) {
|
||||
y = actionRect.top() - previewOffset - labelSize.height();
|
||||
} else {
|
||||
y = actionRect.top();
|
||||
}
|
||||
y = qMax(screenGeometry.top(), y);
|
||||
y = qMin(screenGeometry.bottom() - labelSize.height() + 1, y);
|
||||
|
||||
return {x, y};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief Constructs a PrintingSelectorCardOverlayWidget for displaying a card overlay.
|
||||
*
|
||||
|
|
@ -50,6 +99,31 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa
|
|||
|
||||
initializePinBadge();
|
||||
|
||||
// Parent the preview to this overlay so it is destroyed with it (Qt::ToolTip keeps
|
||||
// it a frameless, non-activating top-level window despite the parent).
|
||||
cardOverridePreviewLabel = new QLabel(this, Qt::ToolTip);
|
||||
cardOverridePreviewLabel->setWindowFlag(Qt::FramelessWindowHint);
|
||||
cardOverridePreviewLabel->setAttribute(Qt::WA_ShowWithoutActivating);
|
||||
cardOverridePreviewLabel->setScaledContents(true);
|
||||
cardOverridePreviewLabel->hide();
|
||||
|
||||
// While the preview is visible, keep it honest: when the hovered printing's art
|
||||
// resolves (all alternate printings share the root card's CardInfo), redraw it in place.
|
||||
if (rootCard.getCardPtr()) {
|
||||
connect(rootCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this] {
|
||||
if (cardOverridePreviewLabel->isVisible()) {
|
||||
refreshPreview();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Alt-Tab / app-inactive must not strand the floating preview.
|
||||
connect(qApp, &QGuiApplication::applicationStateChanged, this, [this](Qt::ApplicationState state) {
|
||||
if (state != Qt::ApplicationActive) {
|
||||
hidePreview();
|
||||
}
|
||||
});
|
||||
|
||||
// Update when this overlay emits cardPreferenceChanged or when size/scale changes
|
||||
connect(this, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this,
|
||||
&PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility);
|
||||
|
|
@ -185,19 +259,23 @@ void PrintingSelectorCardOverlayWidget::leaveEvent(QEvent *event)
|
|||
}
|
||||
|
||||
/**
|
||||
* @brief Creates and shows a custom context menu when the right mouse button is clicked.
|
||||
* @brief Creates and shows the card-overlay context menu.
|
||||
*
|
||||
* The context menu includes an option to show related cards, which displays a submenu with actions
|
||||
* for each related card. When an action is triggered, the card information is updated, and the
|
||||
* printing selector is shown.
|
||||
* The menu includes the card art preference (Pin/Unpin Printing), the Image Overrides
|
||||
* submenu (Load Custom Image, Clear Custom Image, and one entry per alternate printing with a
|
||||
* live preview), and the Show Related cards submenu.
|
||||
*
|
||||
* @param point The position of the mouse when the right-click occurred.
|
||||
* @param point The local position the menu should pop at.
|
||||
*/
|
||||
void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
|
||||
{
|
||||
QMenu menu;
|
||||
|
||||
auto *preferenceMenu = new QMenu(tr("Preference"));
|
||||
hidePreview(); // Clear any preview state left over from a previous menu run.
|
||||
|
||||
// Submenus are owned by the stack-allocated top-level menu (addMenu() does not
|
||||
// transfer ownership).
|
||||
auto *preferenceMenu = new QMenu(tr("Preference"), &menu);
|
||||
menu.addMenu(preferenceMenu);
|
||||
|
||||
const auto &preferredProviderId =
|
||||
|
|
@ -219,8 +297,66 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
|
|||
});
|
||||
}
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
auto *overrideMenu = new QMenu(tr("Image Overrides"), &menu);
|
||||
|
||||
auto *loadCustomAction = overrideMenu->addAction(tr("Load Custom Image..."));
|
||||
auto *clearOverrideAction = overrideMenu->addAction(tr("Clear Custom Image"));
|
||||
|
||||
// Nothing to clear on a card that has no local override yet.
|
||||
clearOverrideAction->setEnabled(CardPictureLoader::hasLocalOverrides(rootCard));
|
||||
|
||||
overrideMenu->addSeparator();
|
||||
|
||||
const auto &allSets = rootCard.getInfo().getSets();
|
||||
|
||||
for (const auto &set : allSets) {
|
||||
for (const auto &printing : set) {
|
||||
if (printing == rootCard.getPrinting()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The submenu is already scoped to this card, so the rows lead with set +
|
||||
// collector; only printings with a distinct display name add their own name.
|
||||
const CardSetPtr cardSet = printing.getSet();
|
||||
if (!cardSet) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString label = tr("%1 %2").arg(cardSet->getCorrectedShortName(), printing.getProperty("num"));
|
||||
|
||||
auto *action = overrideMenu->addAction(label);
|
||||
|
||||
ExactCard overrideCard(rootCard.getCardPtr(), printing);
|
||||
action->setData(QVariant::fromValue(overrideCard));
|
||||
|
||||
connect(action, &QAction::triggered, this, [this, overrideCard]() {
|
||||
CardPictureLoader::getInstance().installPrintingOverride(rootCard, overrideCard);
|
||||
QPixmapCache::clear();
|
||||
rootCard.emitPixmapUpdated(); // refresh the overlay art in place, like the other paths
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
connect(clearOverrideAction, &QAction::triggered, this, [this]() {
|
||||
CardPictureLoader::deleteAllLocalOverrides(rootCard);
|
||||
QPixmapCache::clear();
|
||||
rootCard.emitPixmapUpdated(); // force UI refresh
|
||||
});
|
||||
|
||||
connect(loadCustomAction, &QAction::triggered, this, &PrintingSelectorCardOverlayWidget::loadCustomImage);
|
||||
|
||||
connect(overrideMenu, &QMenu::hovered, this, &PrintingSelectorCardOverlayWidget::showPreviewForAction);
|
||||
connect(overrideMenu, &QMenu::aboutToHide, this, &PrintingSelectorCardOverlayWidget::hidePreview);
|
||||
connect(overrideMenu, &QMenu::triggered, this, &PrintingSelectorCardOverlayWidget::hidePreview);
|
||||
|
||||
menu.addMenu(overrideMenu);
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
// filling out the related cards submenu
|
||||
auto *relatedMenu = new QMenu(tr("Show Related cards"));
|
||||
auto *relatedMenu = new QMenu(tr("Show Related cards"), &menu);
|
||||
menu.addMenu(relatedMenu);
|
||||
auto relatedCards = rootCard.getInfo().getAllRelatedCards();
|
||||
if (relatedCards.isEmpty()) {
|
||||
|
|
@ -235,7 +371,11 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
|
|||
});
|
||||
}
|
||||
}
|
||||
// The preview anchors itself to this popup's global geometry while it is open, so the
|
||||
// pointer must stay valid for the whole exec() and be dropped before the stack unwinds.
|
||||
previewSourceMenu = overrideMenu;
|
||||
menu.exec(this->mapToGlobal(point));
|
||||
previewSourceMenu = nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -291,3 +431,136 @@ void PrintingSelectorCardOverlayWidget::initializePinBadge()
|
|||
pinBadge->setVisible(false);
|
||||
pinBadge->raise();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Asks for an image file and installs it as the card's custom art.
|
||||
*
|
||||
* Unreadable files answer with a visible warning instead of a silent no-op.
|
||||
*/
|
||||
void PrintingSelectorCardOverlayWidget::loadCustomImage()
|
||||
{
|
||||
QString filePath = QFileDialog::getOpenFileName(this, tr("Select Card Image"), QString(),
|
||||
tr("Images (*.png *.jpg *.jpeg *.webp)"));
|
||||
|
||||
if (filePath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QPixmap pixmap(filePath);
|
||||
if (pixmap.isNull()) {
|
||||
// No silent paths: a file that cannot be read answers visibly instead of a no-op.
|
||||
QMessageBox::warning(this, tr("Load Custom Image"), tr("The selected file could not be read as an image."));
|
||||
return;
|
||||
}
|
||||
|
||||
CardPictureLoader::getInstance().saveCardImageToLocalStorage(rootCard, pixmap, true);
|
||||
|
||||
QPixmapCache::clear();
|
||||
rootCard.emitPixmapUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Shows the hover preview for a highlighted printing entry in the Image Overrides submenu.
|
||||
*
|
||||
* QMenu::hovered fires on keyboard highlight too, so the preview appears when arrows walk
|
||||
* onto a printing entry, not only under the mouse.
|
||||
*
|
||||
* Non-printing entries (e.g., Load Custom Image, Clear Custom Image) hide the preview.
|
||||
*
|
||||
* @param action The action that was highlighted.
|
||||
*/
|
||||
void PrintingSelectorCardOverlayWidget::showPreviewForAction(QAction *action)
|
||||
{
|
||||
if (!action) {
|
||||
hidePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
const QVariant data = action->data();
|
||||
|
||||
if (!data.canConvert<ExactCard>()) {
|
||||
hidePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
const ExactCard previewCard = qvariant_cast<ExactCard>(data);
|
||||
if (previewCard.isEmpty()) {
|
||||
hidePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
hoveredOverrideCard = previewCard;
|
||||
hoveredOverrideAction = action;
|
||||
refreshPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Renders the hover preview for the currently highlighted printing.
|
||||
*
|
||||
* The preview shows the loading placeholder while the art is pending and swaps in the real
|
||||
* art when it resolves. The label is positioned against its already-resized geometry so the
|
||||
* first-ever show at the screen's edges stays fully on-screen.
|
||||
*/
|
||||
void PrintingSelectorCardOverlayWidget::refreshPreview()
|
||||
{
|
||||
if (hoveredOverrideCard.isEmpty()) {
|
||||
hidePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr QSize previewSize(240, 336);
|
||||
constexpr int previewOffset = 20;
|
||||
|
||||
QPixmap pixmap;
|
||||
CardPictureLoader::getPixmap(pixmap, hoveredOverrideCard, previewSize);
|
||||
|
||||
if (pixmap.isNull()) {
|
||||
// Keep the preview honest while loading: show the loading placeholder instead of a void.
|
||||
// Fetch at the logical size and let the label scale it, so the placeholder matches the
|
||||
// real art's footprint rather than doubling on HiDPI displays.
|
||||
CardPictureLoader::getCardBackLoadingInProgressPixmap(pixmap, previewSize);
|
||||
}
|
||||
|
||||
cardOverridePreviewLabel->setPixmap(pixmap);
|
||||
// QPixmap::size() is physical pixels; the label layout must use the device-independent size
|
||||
// so the preview keeps a constant footprint across DPI settings (QScreen geometry is logical).
|
||||
const QSize labelSize = pixmap.deviceIndependentSize().toSize();
|
||||
cardOverridePreviewLabel->resize(labelSize);
|
||||
|
||||
// Anchor the preview to the walked submenu popup rather than QCursor::pos(), which is idle
|
||||
// under keyboard-only operation: a keyboard-highlighted row must preview at the same place as
|
||||
// a hovered one. The mouse path is unchanged in effect — the popup sits under the cursor, so
|
||||
// the preview stays beside the row in both modalities.
|
||||
const QMenu *popup = previewSourceMenu;
|
||||
if (!popup || !popup->isVisible() || !hoveredOverrideAction) {
|
||||
hidePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
const QRect popupGeometry = popup->geometry();
|
||||
const QRect actionRectLocal = popup->actionGeometry(hoveredOverrideAction);
|
||||
const QRect actionRect(popupGeometry.topLeft() + actionRectLocal.topLeft(), actionRectLocal.size());
|
||||
|
||||
QScreen *screen = QGuiApplication::screenAt(popupGeometry.center());
|
||||
if (!screen) {
|
||||
hidePreview();
|
||||
return;
|
||||
}
|
||||
const QRect &screenGeometry = screen->geometry();
|
||||
|
||||
cardOverridePreviewLabel->move(previewPositionNear(actionRect, labelSize, screenGeometry, previewOffset));
|
||||
cardOverridePreviewLabel->show();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Hides the hover preview and forgets the currently highlighted printing.
|
||||
*/
|
||||
void PrintingSelectorCardOverlayWidget::hidePreview()
|
||||
{
|
||||
hoveredOverrideCard = ExactCard();
|
||||
hoveredOverrideAction = nullptr;
|
||||
|
||||
if (cardOverridePreviewLabel) {
|
||||
cardOverridePreviewLabel->hide();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
|
||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||
|
||||
class QAction;
|
||||
class QMenu;
|
||||
|
||||
class PrintingSelectorCardOverlayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -43,11 +46,19 @@ private slots:
|
|||
|
||||
private:
|
||||
void initializePinBadge();
|
||||
void loadCustomImage();
|
||||
void showPreviewForAction(QAction *action);
|
||||
void refreshPreview();
|
||||
void hidePreview();
|
||||
CardInfoPictureWidget *cardInfoPicture;
|
||||
AllZonesCardAmountWidget *allZonesCardAmountWidget;
|
||||
QLabel *pinBadge = nullptr;
|
||||
AbstractTabDeckEditor *deckEditor;
|
||||
ExactCard rootCard;
|
||||
QLabel *cardOverridePreviewLabel = nullptr;
|
||||
ExactCard hoveredOverrideCard;
|
||||
QMenu *previewSourceMenu = nullptr;
|
||||
QAction *hoveredOverrideAction = nullptr;
|
||||
};
|
||||
|
||||
#endif // PRINTING_SELECTOR_CARD_OVERLAY_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ void GameSelector::joinGame(const ServerInfo_Game &game, const bool asSpectator,
|
|||
return;
|
||||
}
|
||||
|
||||
bool overrideRestrictions = !tabSupervisor->getAdminLocked();
|
||||
bool overrideRestrictions = tabSupervisor->canOverrideGameRestrictions();
|
||||
|
||||
// Joining a full game without override privileges silently becomes a
|
||||
// spectator join, so ask first instead of surprising the player.
|
||||
|
|
@ -462,7 +462,7 @@ void GameSelector::enableButtonsForIndex(const QModelIndex ¤t)
|
|||
}
|
||||
|
||||
const ServerInfo_Game &game = gameListModel->getGame(current.data(Qt::UserRole).toInt());
|
||||
bool overrideRestrictions = !tabSupervisor->getAdminLocked();
|
||||
bool overrideRestrictions = tabSupervisor->canOverrideGameRestrictions();
|
||||
|
||||
spectateButton->setEnabled(game.spectators_allowed() || overrideRestrictions);
|
||||
joinButton->setEnabled(game.player_count() < game.max_players() || overrideRestrictions);
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
connect(&schemeCombo, &QComboBox::currentIndexChanged, this,
|
||||
[this] { themeManager->setColorScheme(schemeCombo.currentData().toString()); });
|
||||
|
||||
// Qt widget style; "Default" lets the application decide
|
||||
styleCombo.addItem(tr("Default"), QStringLiteral("Default"));
|
||||
// Qt widget style; "System" lets the application decide
|
||||
styleCombo.addItem(tr("System"), QStringLiteral("System"));
|
||||
for (const QString &key : QStyleFactory::keys()) {
|
||||
styleCombo.addItem(key, key);
|
||||
}
|
||||
|
|
@ -132,6 +132,10 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
|
||||
&AppearanceSettings::setHomeTabDisplayCardName);
|
||||
|
||||
homeTabBackgroundDimCheckBox.setChecked(settings.appearance().getHomeTabBackgroundDim());
|
||||
connect(&homeTabBackgroundDimCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
|
||||
&AppearanceSettings::setHomeTabBackgroundDim);
|
||||
|
||||
for (const auto &entry : HomeTabButtonColor::all()) {
|
||||
homeTabButtonColorSourceBox.addItem(QObject::tr(entry.trKey));
|
||||
}
|
||||
|
|
@ -150,6 +154,7 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
homeTabGrid->addWidget(&homeTabDisplayCardNameCheckBox, 2, 0, 1, 2);
|
||||
homeTabGrid->addWidget(&homeTabButtonColorSourceLabel, 3, 0);
|
||||
homeTabGrid->addWidget(&homeTabButtonColorSourceBox, 3, 1);
|
||||
homeTabGrid->addWidget(&homeTabBackgroundDimCheckBox, 4, 0, 1, 2);
|
||||
|
||||
homeTabGroupBox = new QGroupBox;
|
||||
homeTabGroupBox->setLayout(homeTabGrid);
|
||||
|
|
@ -508,9 +513,12 @@ void AppearanceSettingsPage::retranslateUi()
|
|||
homeTabBackgroundShuffleFrequencyLabel.setText(tr("Home tab background shuffle frequency:"));
|
||||
homeTabBackgroundShuffleFrequencySpinBox.setSpecialValueText(tr("Disabled"));
|
||||
homeTabDisplayCardNameCheckBox.setText(tr("Display card name of background in bottom right"));
|
||||
homeTabBackgroundDimCheckBox.setText(tr("Dim the home tab background"));
|
||||
homeTabBackgroundDimCheckBox.setToolTip(
|
||||
tr("Draw a translucent overlay over the home tab background so buttons and text stand out"));
|
||||
homeTabButtonColorSourceLabel.setText(tr("Home tab button color:"));
|
||||
homeTabButtonColorSourceBox.setToolTip(
|
||||
tr("Automatic: extract from background if present, otherwise use theme default"));
|
||||
tr("Use the theme's identity accent colors, or extract colors from the background image"));
|
||||
|
||||
playmatGroupBox->setTitle(tr("Playmat settings"));
|
||||
playmatVisibilityLabel.setText(tr("Playmat visibility:"));
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ private:
|
|||
QLabel homeTabBackgroundShuffleFrequencyLabel;
|
||||
QSpinBox homeTabBackgroundShuffleFrequencySpinBox;
|
||||
QCheckBox homeTabDisplayCardNameCheckBox;
|
||||
QCheckBox homeTabBackgroundDimCheckBox;
|
||||
QLabel homeTabButtonColorSourceLabel;
|
||||
QComboBox homeTabButtonColorSourceBox;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,21 @@
|
|||
#include "general_settings_page.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../interface/card_picture_loader/card_picture_loader.h"
|
||||
#include "../main.h"
|
||||
#include "../server/user/user_info_connection.h"
|
||||
#include "update/client/release_channel.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QGridLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QTranslator>
|
||||
#include <libcockatrice/card/card_localization.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <libcockatrice/settings/tabs_settings.h>
|
||||
|
|
@ -46,10 +52,27 @@ GeneralSettingsPage::GeneralSettingsPage()
|
|||
connect(&languageBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
&GeneralSettingsPage::languageBoxChanged);
|
||||
|
||||
// card text & images language, independent of the UI language
|
||||
cardLanguageBox.addItem(tr("English"), "en");
|
||||
for (const QString &code : CardLocalization::supportedLanguages()) {
|
||||
cardLanguageBox.addItem(CardLocalization::languageDisplayName(code), code);
|
||||
}
|
||||
const int cardLangIndex = cardLanguageBox.findData(SettingsCache::instance().cardsDisplay().getCardLang());
|
||||
cardLanguageBox.setCurrentIndex(cardLangIndex < 0 ? 0 : cardLangIndex);
|
||||
|
||||
connect(&cardLanguageBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
&GeneralSettingsPage::cardLanguageBoxChanged);
|
||||
|
||||
auto *languageGrid = new QGridLayout;
|
||||
languageGrid->addWidget(&languageLabel, 0, 0);
|
||||
languageGrid->addWidget(&languageBox, 0, 1);
|
||||
languageGrid->addWidget(&advertiseTranslationPageLabel, 1, 1, Qt::AlignRight);
|
||||
languageGrid->addWidget(&cardLanguageLabel, 1, 0);
|
||||
languageGrid->addWidget(&cardLanguageBox, 1, 1);
|
||||
languageGrid->addWidget(&cardLanguageNoteLabel, 2, 1);
|
||||
languageGrid->addWidget(&advertiseTranslationPageLabel, 3, 1, Qt::AlignRight);
|
||||
|
||||
cardLanguageNoteLabel.setWordWrap(true);
|
||||
cardLanguageNoteLabel.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
languageGroupBox = new QGroupBox;
|
||||
languageGroupBox->setLayout(languageGrid);
|
||||
|
|
@ -412,6 +435,52 @@ void GeneralSettingsPage::languageBoxChanged(int index)
|
|||
SettingsCache::instance().personal().setLang(languageBox.itemData(index).toString());
|
||||
}
|
||||
|
||||
void GeneralSettingsPage::cardLanguageBoxChanged(int index)
|
||||
{
|
||||
const QString lang = cardLanguageBox.itemData(index).toString();
|
||||
SettingsCache::instance().cardsDisplay().setCardLang(lang);
|
||||
|
||||
// Switching to a non-default language only takes effect after the card
|
||||
// database is re-imported with that language selected; English data is always
|
||||
// present, so switching back to English needs no prompt.
|
||||
if (lang == "en") {
|
||||
return;
|
||||
}
|
||||
|
||||
// The binary cache does not track the language its entries were imported in,
|
||||
// and the downloaded pictures were fetched with English art names, so both are
|
||||
// stale until Oracle re-imports the database in the new language: drop them.
|
||||
QFile::remove(SettingsCache::instance().getCardDatabasePath() + ".cache");
|
||||
CardPictureLoader::clearNetworkCache();
|
||||
CardPictureLoader::clearPixmapCache();
|
||||
|
||||
// Art is resolved by the translated card name for non-English languages, so the
|
||||
// matching Scryfall URL is added to the top of the download list. It stays
|
||||
// visible in the deck editor settings, where it can be removed or reordered.
|
||||
const bool localizedUrlAdded = SettingsCache::instance().downloads().addLocalizedScryfallUrl();
|
||||
|
||||
QString message = tr("<p>The card database only contains English card data. To see cards in <b>%1</b>, "
|
||||
"<b>Oracle</b> must run once with this language selected and re-import the card "
|
||||
"database.</p>"
|
||||
"<p>The cached database and the downloaded card pictures have been cleared, so a "
|
||||
"re-import is picked up without stale entries.</p>")
|
||||
.arg(cardLanguageBox.itemText(index));
|
||||
if (localizedUrlAdded) {
|
||||
message += tr("<p>The Scryfall URL that resolves card art by translated name was added to the top of your "
|
||||
"download list. You can remove or reorder it any time.</p>");
|
||||
}
|
||||
message += tr("<p>Run Oracle now?</p>");
|
||||
|
||||
const QMessageBox::StandardButton answer = QMessageBox::question(
|
||||
this, tr("Card text & images language changed"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
|
||||
|
||||
// The answer only controls whether Oracle starts right away; the caches stay
|
||||
// cleared so the next import or launch rebuilds them in the new language.
|
||||
if (answer == QMessageBox::Yes) {
|
||||
emit cardDatabaseUpdateRequested();
|
||||
}
|
||||
}
|
||||
|
||||
void GeneralSettingsPage::updateStartupServerControlsVisibility()
|
||||
{
|
||||
const int index = startupTabSelector.currentIndex();
|
||||
|
|
@ -429,6 +498,10 @@ void GeneralSettingsPage::retranslateUi()
|
|||
|
||||
languageGroupBox->setTitle(tr("Language settings"));
|
||||
languageLabel.setText(tr("Language:"));
|
||||
cardLanguageBox.setItemText(0, tr("English"));
|
||||
cardLanguageLabel.setText(tr("Card text & images language:"));
|
||||
cardLanguageNoteLabel.setText(
|
||||
tr("Foreign card names, text and art apply after you update the card database (Oracle)."));
|
||||
advertiseTranslationPageLabel.setText(
|
||||
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ public:
|
|||
static QStringList findQmFiles();
|
||||
static QString languageName(const QString &lang);
|
||||
|
||||
signals:
|
||||
/// Request to re-import the card database with the newly selected card language
|
||||
void cardDatabaseUpdateRequested();
|
||||
|
||||
private slots:
|
||||
void deckPathButtonClicked();
|
||||
void filtersPathButtonClicked();
|
||||
|
|
@ -33,6 +37,7 @@ private slots:
|
|||
void tokenDatabasePathButtonClicked();
|
||||
void resetAllPathsClicked();
|
||||
void languageBoxChanged(int index);
|
||||
void cardLanguageBoxChanged(int index);
|
||||
void updateStartupServerControlsVisibility();
|
||||
|
||||
private:
|
||||
|
|
@ -46,6 +51,10 @@ private:
|
|||
QComboBox languageBox;
|
||||
QLabel advertiseTranslationPageLabel;
|
||||
|
||||
QLabel cardLanguageLabel;
|
||||
QComboBox cardLanguageBox;
|
||||
QLabel cardLanguageNoteLabel;
|
||||
|
||||
QLabel updateReleaseChannelLabel;
|
||||
QComboBox updateReleaseChannelBox;
|
||||
QCheckBox startupUpdateCheckCheckBox;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "archidekt_api_response_deck_display_widget.h"
|
||||
|
||||
#include "../../../../../../client/settings/cache_settings.h"
|
||||
#include "../../../../../deck_loader/card_node_function.h"
|
||||
#include "../../../../../deck_loader/deck_loader.h"
|
||||
#include "../../../../cards/card_size_widget.h"
|
||||
|
|
@ -10,6 +11,7 @@
|
|||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <libcockatrice/card/import/card_name_normalizer.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWidget *parent,
|
||||
ArchidektApiResponseDeck _response,
|
||||
|
|
@ -120,6 +122,9 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi
|
|||
}
|
||||
|
||||
model = new DeckListModel(this);
|
||||
model->setDisplayLanguage(SettingsCache::instance().cardsDisplay().getCardLang());
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, model,
|
||||
[this](const QString &lang) { model->setDisplayLanguage(lang); });
|
||||
connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset);
|
||||
|
||||
auto decklist = QSharedPointer<DeckList>(new DeckList);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include <libcockatrice/protocol/pb/event_list_rooms.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_server_message.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_join_room.pb.h>
|
||||
#include <libcockatrice/protocol/pb/room_commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/session_commands.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
|
||||
|
|
@ -185,25 +186,37 @@ void TabServer::processServerMessageEvent(const Event_ServerMessage &event)
|
|||
void TabServer::joinRoom(int id, bool setCurrent)
|
||||
{
|
||||
TabRoom *room = tabSupervisor->getRoomTabs().value(id);
|
||||
if (!room) {
|
||||
Command_JoinRoom cmd;
|
||||
cmd.set_room_id(id);
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
pend->setExtraData(setCurrent);
|
||||
connect(pend, &PendingCommand::finished, this,
|
||||
[this, id](const Response &r, const CommandContainer &c, const QVariant &v) {
|
||||
joinRoomFinished(r, c, v, id);
|
||||
});
|
||||
|
||||
client->sendCommand(pend);
|
||||
|
||||
if (room) {
|
||||
if (setCurrent) {
|
||||
tabSupervisor->setCurrentWidget((QWidget *)room);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (setCurrent) {
|
||||
tabSupervisor->setCurrentWidget((QWidget *)room);
|
||||
auto pendingIt = pendingRoomJoins.find(id);
|
||||
if (pendingIt != pendingRoomJoins.end()) {
|
||||
// A join for this room is already in flight: the room tab opens when its response
|
||||
// arrives. Fold the new request into the pending one so that, for example, clicking
|
||||
// a room the selector is auto-joining does not send a second Command_JoinRoom - the
|
||||
// server would reject that duplicate with RespContextError.
|
||||
if (setCurrent) {
|
||||
pendingIt.value() = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
pendingRoomJoins.insert(id, setCurrent);
|
||||
|
||||
Command_JoinRoom cmd;
|
||||
cmd.set_room_id(id);
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
pend->setExtraData(setCurrent);
|
||||
connect(
|
||||
pend, &PendingCommand::finished, this,
|
||||
[this, id](const Response &r, const CommandContainer &c, const QVariant &v) { joinRoomFinished(r, c, v, id); });
|
||||
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabServer::joinRoomFinished(const Response &r,
|
||||
|
|
@ -211,34 +224,72 @@ void TabServer::joinRoomFinished(const Response &r,
|
|||
const QVariant &extraData,
|
||||
int roomId)
|
||||
{
|
||||
const bool setCurrent = pendingRoomJoins.value(roomId, extraData.toBool());
|
||||
pendingRoomJoins.remove(roomId);
|
||||
const bool healedJoin = healedRoomJoins.contains(roomId);
|
||||
healedRoomJoins.remove(roomId);
|
||||
|
||||
switch (r.response_code()) {
|
||||
case Response::RespOk:
|
||||
break;
|
||||
case Response::RespNameNotFound:
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("Failed to join the server room: it doesn't exist on the server."));
|
||||
if (setCurrent) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("Failed to join the server room: it doesn't exist on the server."));
|
||||
}
|
||||
emit roomJoinFailed(roomId);
|
||||
return;
|
||||
case Response::RespContextError:
|
||||
QMessageBox::critical(
|
||||
this, tr("Error"),
|
||||
tr("The server thinks you are in the server room but your client is unable to display it. "
|
||||
"Try restarting your client."));
|
||||
emit roomJoinFailed(roomId);
|
||||
if (healedJoin) {
|
||||
// The rejoin below was already answered and the server still rejects the join, so
|
||||
// the stale-membership heal cannot help: surface the error. The guard was already
|
||||
// released above so a later user-initiated join may try a fresh heal.
|
||||
if (setCurrent) {
|
||||
QMessageBox::critical(
|
||||
this, tr("Error"),
|
||||
tr("The server thinks you are in the server room but your client is unable to display it. "
|
||||
"Try restarting your client."));
|
||||
}
|
||||
emit roomJoinFailed(roomId);
|
||||
return;
|
||||
}
|
||||
// The server already had us registered in the room even though no tab was open,
|
||||
// usually because two join attempts for the same room overlapped. Leaving and
|
||||
// rejoining makes the server reply with a fresh RespOk so the tab is displayed
|
||||
// without requiring a client restart. The guard above covers exactly the rejoin that
|
||||
// leaveAndRejoinRoom triggers, so a server that keeps replying with RespContextError
|
||||
// gets one heal attempt per join instead of an endless recursion.
|
||||
healedRoomJoins.insert(roomId);
|
||||
leaveAndRejoinRoom(roomId, setCurrent);
|
||||
return;
|
||||
case Response::RespUserLevelTooLow:
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("You do not have the required permission to join this server room."));
|
||||
if (setCurrent) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("You do not have the required permission to join this server room."));
|
||||
}
|
||||
emit roomJoinFailed(roomId);
|
||||
return;
|
||||
default:
|
||||
QMessageBox::critical(
|
||||
this, tr("Error"),
|
||||
tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code()));
|
||||
if (setCurrent) {
|
||||
QMessageBox::critical(
|
||||
this, tr("Error"),
|
||||
tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code()));
|
||||
}
|
||||
emit roomJoinFailed(roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_JoinRoom &resp = r.GetExtension(Response_JoinRoom::ext);
|
||||
emit roomJoined(resp.room_info(), extraData.toBool());
|
||||
emit roomJoined(resp.room_info(), setCurrent);
|
||||
}
|
||||
|
||||
void TabServer::leaveAndRejoinRoom(int roomId, bool setCurrent)
|
||||
{
|
||||
// Clear the stale room membership server-side. The leave is sent before the rejoin below,
|
||||
// so the server no longer considers us a member by the time the join arrives. The leave
|
||||
// response is intentionally not awaited: commands are processed in send order on the
|
||||
// connection, and a failed leave (RespNotInRoom) only means the membership was already gone.
|
||||
client->sendCommand(client->prepareRoomCommand(Command_LeaveRoom(), roomId));
|
||||
|
||||
joinRoom(roomId, setCurrent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
#include "tab.h"
|
||||
|
||||
#include <QGroupBox>
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QTextBrowser>
|
||||
#include <QTreeWidget>
|
||||
|
||||
|
|
@ -58,10 +60,17 @@ private slots:
|
|||
int roomId);
|
||||
|
||||
private:
|
||||
void leaveAndRejoinRoom(int roomId, bool setCurrent);
|
||||
|
||||
AbstractClient *client;
|
||||
RoomSelector *roomSelector;
|
||||
QTextBrowser *serverInfoBox;
|
||||
bool shouldEmitUpdate = false;
|
||||
/** Room ids with a join command in flight, mapped to whether the tab should be focused once it opens. */
|
||||
QHash<int, bool> pendingRoomJoins;
|
||||
/** Room ids for which a stale-membership heal (leave + rejoin) is currently in flight. Released as soon as the
|
||||
* rejoin has been answered, so a heal is attempted at most once per join. */
|
||||
QSet<int> healedRoomJoins;
|
||||
|
||||
public:
|
||||
TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client);
|
||||
|
|
|
|||
|
|
@ -1451,6 +1451,11 @@ bool TabSupervisor::getAdminLocked() const
|
|||
return tabAdmin->getLocked();
|
||||
}
|
||||
|
||||
bool TabSupervisor::canOverrideGameRestrictions() const
|
||||
{
|
||||
return !getAdminLocked() || (userInfo->user_level() & ServerInfo_User::IsJudge);
|
||||
}
|
||||
|
||||
void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event)
|
||||
{
|
||||
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ public:
|
|||
[[nodiscard]] QList<GameInviteOption> getGameInviteLinksForRoom(int roomId) const;
|
||||
void sendInviteToUser(const QString &userName, const QString &inviteText);
|
||||
[[nodiscard]] bool getAdminLocked() const;
|
||||
[[nodiscard]] bool canOverrideGameRestrictions() const;
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
bool switchToGameTabIfAlreadyExists(const int gameId);
|
||||
static void actShowPopup(const QString &message);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue