[UserList] Add a light mode theme

Took 12 minutes

Took 11 seconds


Took 5 minutes

Took 2 minutes
This commit is contained in:
Lukas Brübach 2026-08-15 12:53:25 +02:00
parent 46d3dba6b4
commit 941b1e5c25
8 changed files with 659 additions and 341 deletions

View file

@ -123,7 +123,7 @@ void ThemeManager::ensureThemeDirectoryExists()
} }
} }
bool ThemeManager::isDarkMode(const QString &themeDirPath) bool ThemeManager::isDarkMode(const QString &themeDirPath) const
{ {
ThemeConfig themeConfig = ThemeConfig::fromThemeDir(themeDirPath); ThemeConfig themeConfig = ThemeConfig::fromThemeDir(themeDirPath);
if (themeConfig.colorScheme.compare("Dark", Qt::CaseInsensitive) == 0) { if (themeConfig.colorScheme.compare("Dark", Qt::CaseInsensitive) == 0) {

View file

@ -66,7 +66,14 @@ protected:
public: public:
bool isBuiltInTheme(); bool isBuiltInTheme();
bool isDarkMode(const QString &themeDirPath); // Explicit color scheme of the theme: theme.cfg's ColorScheme setting
// (Dark/Light), falling back to the OS color scheme when it is "System".
bool isDarkMode(const QString &themeDirPath) const;
// The resolved scheme of the currently active theme.
bool isDarkModeActive() const
{
return isDarkMode(currentThemePath);
}
QStringMap &getAvailableThemes(); QStringMap &getAvailableThemes();
// Returns the path to the currently active theme directory (empty = default) // Returns the path to the currently active theme directory (empty = default)
QString getCurrentThemePath() const QString getCurrentThemePath() const

View file

@ -1,6 +1,7 @@
#include "user_info_popup.h" #include "user_info_popup.h"
#include "../../interface/pixel_map_generator.h" #include "../../interface/pixel_map_generator.h"
#include "../../interface/theme_manager.h"
#include "../../interface/widgets/tabs/tab_supervisor.h" #include "../../interface/widgets/tabs/tab_supervisor.h"
#include "user_list_painter.h" #include "user_list_painter.h"
@ -22,6 +23,42 @@
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h> #include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
/// Qt stylesheets accept #aarrggbb, which is QColor::name(QColor::HexArgb).
static QString colorStr(const QColor &color)
{
return color.name(QColor::HexArgb);
}
PopupTheme PopupTheme::fromPalette(const QPalette &palette, bool dark)
{
PopupTheme t;
t.dark = dark;
const QColor window = palette.color(QPalette::Window);
const QColor base = palette.color(QPalette::Base);
const QColor mid = palette.color(QPalette::Mid);
const QColor text = palette.color(QPalette::Text);
const QColor disabledText = palette.color(QPalette::Disabled, QPalette::Text);
const QColor highlight = palette.color(QPalette::Highlight);
t.bg = window;
t.border = mid;
t.text = text;
t.subText = disabledText;
t.statusText = disabledText;
t.buttonBg = base;
t.buttonBorder = mid;
t.buttonHover = UserListPainter::blend(base, highlight, dark ? 0.30 : 0.12);
t.buttonPressed = UserListPainter::blend(base, highlight, dark ? 0.50 : 0.25);
t.buttonDisabled = disabledText;
t.closeBg = UserListPainter::blend(base, window, 0.5);
t.closeHover = dark ? QColor(200, 50, 50) : UserListPainter::blend(QColor(200, 50, 50), base, 0.45);
t.gamesRow = base;
t.gamesSelected = UserListPainter::blend(base, highlight, dark ? 0.45 : 0.30);
t.gamesSeparator = mid;
t.gamesSeparator.setAlpha(90);
return t;
}
// ── Compact game row delegate ───────────────────────────────────────────────── // ── Compact game row delegate ─────────────────────────────────────────────────
class PopupGameDelegate : public QStyledItemDelegate class PopupGameDelegate : public QStyledItemDelegate
@ -48,8 +85,14 @@ public:
const QRect rect = option.rect; const QRect rect = option.rect;
const ServerInfo_Game game = var.value<ServerInfo_Game>(); const ServerInfo_Game game = var.value<ServerInfo_Game>();
const bool selected = option.state & QStyle::State_Selected; const bool selected = option.state & QStyle::State_Selected;
const bool dark = themeManager && themeManager->isDarkModeActive();
// The widget palette can be stale after a runtime theme change, so the
// rows are styled from the application palette (always current).
const QPalette pal = qApp->palette();
const QColor base = pal.color(QPalette::Base);
const QColor highlight = pal.color(QPalette::Highlight);
p->fillRect(rect, selected ? QColor(35, 45, 62) : QColor(14, 18, 26)); p->fillRect(rect, selected ? UserListPainter::blend(base, highlight, dark ? 0.45 : 0.30) : base);
// State colour dot // State colour dot
const QColor dot = game.started() ? QColor(239, 68, 68) const QColor dot = game.started() ? QColor(239, 68, 68)
@ -64,7 +107,7 @@ public:
QFont tf = option.font; QFont tf = option.font;
tf.setBold(true); tf.setBold(true);
p->setFont(tf); p->setFont(tf);
p->setPen(QColor(205, 215, 230)); p->setPen(pal.color(QPalette::Text));
const int textX = rect.left() + 26; const int textX = rect.left() + 26;
const int countW = 52; const int countW = 52;
const int titleW = rect.width() - textX - countW - 6; const int titleW = rect.width() - textX - countW - 6;
@ -74,13 +117,15 @@ public:
// Player count // Player count
const bool full = game.player_count() >= game.max_players(); const bool full = game.player_count() >= game.max_players();
p->setFont(option.font); p->setFont(option.font);
p->setPen(full ? QColor(249, 115, 22) : QColor(110, 128, 150)); p->setPen(full ? QColor(249, 115, 22) : pal.color(QPalette::Disabled, QPalette::Text));
p->drawText(QRect(rect.right() - countW - 4, rect.top(), countW, rect.height()), p->drawText(QRect(rect.right() - countW - 4, rect.top(), countW, rect.height()),
Qt::AlignVCenter | Qt::AlignRight, Qt::AlignVCenter | Qt::AlignRight,
QStringLiteral("%1/%2").arg(game.player_count()).arg(game.max_players())); QStringLiteral("%1/%2").arg(game.player_count()).arg(game.max_players()));
// Row separator // Row separator
p->setPen(QColor(24, 32, 44)); QColor separator = pal.color(QPalette::Mid);
separator.setAlpha(90);
p->setPen(separator);
p->drawLine(rect.bottomLeft(), rect.bottomRight()); p->drawLine(rect.bottomLeft(), rect.bottomRight());
p->restore(); p->restore();
@ -95,17 +140,17 @@ UserInfoHeaderWidget::UserInfoHeaderWidget(QWidget *parent) : QWidget(parent)
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
} }
void UserInfoHeaderWidget::setUserData(const ServerInfo_User &user, void UserInfoHeaderWidget::setUserData(const ServerInfo_User &_user,
bool online, bool _online,
const QPixmap &avatar, const QPixmap &_avatar,
const QPixmap &cardArt, const QPixmap &_cardArt,
const CardArtParams &params) const CardArtParams &_params)
{ {
m_user = user; user = _user;
m_online = online; online = _online;
m_avatar = avatar; avatar = _avatar;
m_cardArt = cardArt; cardArt = _cardArt;
m_params = params; params = _params;
update(); update();
} }
@ -115,29 +160,37 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
p.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing); p.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
const QRect rect = this->rect(); const QRect rect = this->rect();
const UserLevelFlags level(m_user.user_level()); const UserLevelFlags level(user.user_level());
const QString userName = QString::fromStdString(m_user.name()); const QString userName = QString::fromStdString(user.name());
const QString privLevel = QString::fromStdString(m_user.privlevel()); const QString privLevel = QString::fromStdString(user.privlevel());
// Dark base const bool dark = themeManager && themeManager->isDarkModeActive();
p.fillRect(rect, QColor(14, 18, 26)); const UserListPainter::Style style = UserListPainter::resolveStyle(qApp->palette(), dark);
// Palette surface
{
QLinearGradient bg(0, 0, rect.width(), 0);
bg.setColorAt(0, style.cardStart);
bg.setColorAt(1, style.cardEnd);
p.fillRect(rect, bg);
}
// ── Card art background ─────────────────────────────────────────────────── // ── Card art background ───────────────────────────────────────────────────
if (!m_cardArt.isNull()) { if (!cardArt.isNull()) {
const int w = rect.width(); const int w = rect.width();
const int h = rect.height(); const int h = rect.height();
const int mL = qRound(w * m_params.marginPctL); const int mL = qRound(w * params.marginPctL);
const int mR = qRound(w * m_params.marginPctR); const int mR = qRound(w * params.marginPctR);
const int dW = w - mL - mR; const int dW = w - mL - mR;
const double base = qMax(double(dW) / m_cardArt.width(), double(h) / m_cardArt.height()); const double base = qMax(double(dW) / cardArt.width(), double(h) / cardArt.height());
const double scale = base * m_params.zoom; const double scale = base * params.zoom;
const int sW = qRound(m_cardArt.width() * scale); const int sW = qRound(cardArt.width() * scale);
const int sH = qRound(m_cardArt.height() * scale); const int sH = qRound(cardArt.height() * scale);
const QPixmap scaled = m_cardArt.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); const QPixmap scaled = cardArt.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
const int srcX = (sW - dW) / 2; const int srcX = (sW - dW) / 2;
const int srcY = qBound(0, qRound((sH - h) * m_params.verticalOffset), qMax(0, sH - h)); const int srcY = qBound(0, qRound((sH - h) * params.verticalOffset), qMax(0, sH - h));
QImage img = scaled.copy(srcX, srcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); QImage img = scaled.copy(srcX, srcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
{ {
@ -155,12 +208,14 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
p.setOpacity(1.0); p.setOpacity(1.0);
} }
// Bottom gradient overlay so avatar and text are always legible // Bottom gradient overlay so avatar and text are always legible. The scrim
// is the palette's Window color so it reads naturally in either scheme.
{ {
const QColor scrim = qApp->palette().color(QPalette::Window);
QLinearGradient ov(0, 0, 0, rect.height()); QLinearGradient ov(0, 0, 0, rect.height());
ov.setColorAt(0.0, QColor(14, 18, 26, 0)); ov.setColorAt(0.0, QColor(scrim.red(), scrim.green(), scrim.blue(), 0));
ov.setColorAt(0.55, QColor(14, 18, 26, 110)); ov.setColorAt(0.55, QColor(scrim.red(), scrim.green(), scrim.blue(), 110));
ov.setColorAt(1.0, QColor(14, 18, 26, 230)); ov.setColorAt(1.0, QColor(scrim.red(), scrim.green(), scrim.blue(), 230));
p.fillRect(rect, ov); p.fillRect(rect, ov);
} }
@ -187,20 +242,20 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
p.save(); p.save();
p.setClipPath(clip); p.setClipPath(clip);
if (!m_avatar.isNull()) { if (!avatar.isNull()) {
p.drawPixmap(ar, m_avatar.scaled(ar.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); p.drawPixmap(ar, avatar.scaled(ar.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
} else { } else {
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
p.setBrush(accent.darker(200)); p.setBrush(UserListPainter::blend(accent, style.base, dark ? 0.45 : 0.72));
p.drawEllipse(ar); p.drawEllipse(ar);
const QPixmap pawn = const QPixmap pawn =
UserLevelPixmapGenerator::generatePixmap(AvatarPawnSize, level, m_user.pawn_colors(), false, privLevel); UserLevelPixmapGenerator::generatePixmap(AvatarPawnSize, level, user.pawn_colors(), false, privLevel);
p.drawPixmap(ar.center().x() - AvatarPawnSize / 2, ar.center().y() - AvatarPawnSize / 2, pawn); p.drawPixmap(ar.center().x() - AvatarPawnSize / 2, ar.center().y() - AvatarPawnSize / 2, pawn);
} }
p.restore(); p.restore();
// Status ring // Status ring
p.setPen(QPen(m_online ? QColor(34, 197, 94) : QColor(70, 80, 95), 2.5)); p.setPen(QPen(online ? QColor(34, 197, 94) : style.ringOffline, 2.5));
p.setBrush(Qt::NoBrush); p.setBrush(Qt::NoBrush);
p.drawEllipse(QRectF(ar).adjusted(-1.25, -1.25, 1.25, 1.25)); p.drawEllipse(QRectF(ar).adjusted(-1.25, -1.25, 1.25, 1.25));
@ -212,7 +267,7 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
nf.setBold(true); nf.setBold(true);
nf.setPointSizeF(nf.pointSizeF() * 1.12); nf.setPointSizeF(nf.pointSizeF() * 1.12);
p.setFont(nf); p.setFont(nf);
p.setPen(m_online ? QColor(220, 228, 240) : QColor(90, 100, 115)); p.setPen(online ? style.textOnline : style.textOffline);
p.drawText(QRect(tx, ay, tw, AvatarSize / 2 + 4), Qt::AlignBottom | Qt::AlignLeft, p.drawText(QRect(tx, ay, tw, AvatarSize / 2 + 4), Qt::AlignBottom | Qt::AlignLeft,
QFontMetrics(nf).elidedText(userName, Qt::ElideRight, tw)); QFontMetrics(nf).elidedText(userName, Qt::ElideRight, tw));
@ -243,143 +298,173 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
const int bw = bfm.horizontalAdvance(badge.text) + 10; const int bw = bfm.horizontalAdvance(badge.text) + 10;
const QRect br(tx, ay + AvatarSize / 2 + 6, bw, 15); const QRect br(tx, ay + AvatarSize / 2 + 6, bw, 15);
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
p.setBrush(badge.color.darker(160)); p.setBrush(UserListPainter::blend(badge.color, style.base, dark ? 0.55 : 0.78));
p.drawRoundedRect(br, 3, 3); p.drawRoundedRect(br, 3, 3);
p.setPen(badge.color.lighter(150)); p.setPen(dark ? UserListPainter::blend(badge.color, Qt::white, 0.5)
: UserListPainter::blend(badge.color, Qt::black, 0.35));
p.drawText(br, Qt::AlignCenter, badge.text); p.drawText(br, Qt::AlignCenter, badge.text);
} }
} }
// ── UserInfoPopup ───────────────────────────────────────────────────────────── // ── UserInfoPopup ─────────────────────────────────────────────────────────────
UserInfoPopup::UserInfoPopup(TabSupervisor *ts, UserInfoPopup::UserInfoPopup(TabSupervisor *_ts,
AbstractClient *client, AbstractClient *_client,
const QMap<QString, QPixmap> *avatarCache, const QMap<QString, QPixmap> *_avatarCache,
const QMap<QString, QPixmap> *cardArtCache, const QMap<QString, QPixmap> *_cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap, const QMap<QString, CardArtParams> *_cardArtParamsMap,
QWidget *parent) QWidget *parent)
: QFrame(parent, Qt::Tool | Qt::FramelessWindowHint), m_ts(ts), m_client(client), m_avatarCache(avatarCache), : QFrame(parent, Qt::Tool | Qt::FramelessWindowHint), ts(_ts), client(_client), avatarCache(_avatarCache),
m_cardArtCache(cardArtCache), m_cardArtParamsMap(cardArtParamsMap) cardArtCache(_cardArtCache), cardArtParamsMap(_cardArtParamsMap)
{ {
setAttribute(Qt::WA_ShowWithoutActivating); setAttribute(Qt::WA_ShowWithoutActivating);
setFixedWidth(PopupWidth); setFixedWidth(PopupWidth);
setFrameShape(QFrame::NoFrame); setFrameShape(QFrame::NoFrame);
buildUi(); buildUi();
// Re-theme the popup chrome when the theme or its color scheme changes.
if (themeManager) {
connect(themeManager, &ThemeManager::themeChanged, this, &UserInfoPopup::applyTheme);
}
} }
void UserInfoPopup::buildUi() void UserInfoPopup::buildUi()
{ {
setStyleSheet(QStringLiteral("UserInfoPopup {"
" background:#0e1218;"
" border:1px solid #1e2838;"
" border-radius:8px;"
"}"));
auto *root = new QVBoxLayout(this); auto *root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0); root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0); root->setSpacing(0);
// Header // Header
m_header = new UserInfoHeaderWidget(this); header = new UserInfoHeaderWidget(this);
root->addWidget(m_header); root->addWidget(header);
// Action area — rebuilt per user // Action area — rebuilt per user
m_actionArea = new QWidget(this); actionArea = new QWidget(this);
m_actionArea->setStyleSheet(QStringLiteral("background:#0e1218;")); root->addWidget(actionArea);
root->addWidget(m_actionArea);
// Thin separator // Thin separator
auto *sep = new QFrame(this); separator = new QFrame(this);
sep->setFrameShape(QFrame::HLine); separator->setFrameShape(QFrame::HLine);
sep->setStyleSheet(QStringLiteral("color:#1a2434; margin: 0 8px;")); root->addWidget(separator);
root->addWidget(sep);
// Games header row // Games header row
auto *gh = new QHBoxLayout; auto *gh = new QHBoxLayout;
gh->setContentsMargins(10, 4, 8, 2); gh->setContentsMargins(10, 4, 8, 2);
auto *gl = new QLabel(tr("Games"), this); gamesLabel = new QLabel(tr("Games"), this);
gl->setStyleSheet(QStringLiteral("color:#6882a0; font-size:11px; font-weight:bold; background:transparent;")); gh->addWidget(gamesLabel);
gh->addWidget(gl);
gh->addStretch(); gh->addStretch();
m_refreshBtn = new QPushButton(QStringLiteral("↻"), this); refreshBtn = new QPushButton(QStringLiteral("↻"), this);
m_refreshBtn->setFixedSize(20, 20); refreshBtn->setFixedSize(20, 20);
m_refreshBtn->setFlat(true); refreshBtn->setFlat(true);
m_refreshBtn->setStyleSheet( connect(refreshBtn, &QPushButton::clicked, this, &UserInfoPopup::refreshGames);
QStringLiteral("QPushButton{color:#6882a0;border:none;font-size:14px;background:transparent;}" gh->addWidget(refreshBtn);
"QPushButton:hover{color:white;}"));
connect(m_refreshBtn, &QPushButton::clicked, this, &UserInfoPopup::refreshGames);
gh->addWidget(m_refreshBtn);
root->addLayout(gh); root->addLayout(gh);
// Status label // Status label
m_gamesStatus = new QLabel(this); gamesStatus = new QLabel(this);
m_gamesStatus->setAlignment(Qt::AlignCenter); gamesStatus->setAlignment(Qt::AlignCenter);
m_gamesStatus->setStyleSheet( root->addWidget(gamesStatus);
QStringLiteral("color:#3a4a5e; font-size:11px; padding:10px; background:transparent;"));
root->addWidget(m_gamesStatus);
// Games list // Games list
m_gamesModel = new QStandardItemModel(this); gamesModel = new QStandardItemModel(this);
m_gamesView = new QListView(this); gamesView = new QListView(this);
m_gamesView->setModel(m_gamesModel); gamesView->setModel(gamesModel);
m_gamesView->setItemDelegate(new PopupGameDelegate(m_gamesView)); gamesView->setItemDelegate(new PopupGameDelegate(gamesView));
m_gamesView->setFrameShape(QFrame::NoFrame); gamesView->setFrameShape(QFrame::NoFrame);
m_gamesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); gamesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_gamesView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); gamesView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_gamesView->setMaximumHeight(220); gamesView->setMaximumHeight(220);
m_gamesView->setStyleSheet(QStringLiteral("QListView{background:#0e1218;border:none;}" gamesView->setContextMenuPolicy(Qt::CustomContextMenu);
"QListView::item:selected{background:#232e42;}")); connect(gamesView, &QListView::customContextMenuRequested, this, &UserInfoPopup::onGamesContextMenu);
m_gamesView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_gamesView, &QListView::customContextMenuRequested, this, &UserInfoPopup::onGamesContextMenu);
root->addWidget(m_gamesView); root->addWidget(gamesView);
// Close button — positioned absolutely in the top-right corner // Close button — positioned absolutely in the top-right corner
m_closeBtn = new QPushButton(QStringLiteral("✕"), this); closeBtn = new QPushButton(QStringLiteral("✕"), this);
m_closeBtn->setFixedSize(22, 22); closeBtn->setFixedSize(22, 22);
m_closeBtn->setFlat(true); closeBtn->setFlat(true);
m_closeBtn->setStyleSheet(QStringLiteral("QPushButton{background:rgba(14,18,26,180);color:#607080;" connect(closeBtn, &QPushButton::clicked, this, &UserInfoPopup::closeRequested);
"border:none;border-radius:11px;font-size:10px;}"
"QPushButton:hover{color:white;background:rgba(200,50,50,200);}")); applyTheme();
connect(m_closeBtn, &QPushButton::clicked, this, &UserInfoPopup::closeRequested); }
void UserInfoPopup::applyTheme()
{
const bool dark = themeManager && themeManager->isDarkModeActive();
theme = PopupTheme::fromPalette(qApp->palette(), dark);
setStyleSheet(QStringLiteral("UserInfoPopup {"
" background:%1;"
" border:1px solid %2;"
" border-radius:8px;"
"}")
.arg(colorStr(theme.bg), colorStr(theme.border)));
actionArea->setStyleSheet(QStringLiteral("background:%1;").arg(colorStr(theme.bg)));
separator->setStyleSheet(QStringLiteral("color:%1; margin: 0 8px;").arg(colorStr(theme.border)));
gamesLabel->setStyleSheet(QStringLiteral("color:%1; font-size:11px; font-weight:bold; background:transparent;")
.arg(colorStr(theme.subText)));
refreshBtn->setStyleSheet(QStringLiteral("QPushButton{color:%1;border:none;font-size:14px;background:transparent;}"
"QPushButton:hover{color:%2;}")
.arg(colorStr(theme.subText), colorStr(theme.text)));
gamesStatus->setStyleSheet(QStringLiteral("color:%1; font-size:11px; padding:10px; background:transparent;")
.arg(colorStr(theme.statusText)));
gamesView->setStyleSheet(QStringLiteral("QListView{background:%1;border:none;}"
"QListView::item:selected{background:%2;}")
.arg(colorStr(theme.gamesRow), colorStr(theme.gamesSelected)));
closeBtn->setStyleSheet(
QStringLiteral("QPushButton{background:%1;color:%2;"
"border:none;border-radius:11px;font-size:10px;}"
"QPushButton:hover{color:%3;background:%4;}")
.arg(colorStr(theme.closeBg), colorStr(theme.subText), colorStr(theme.text), colorStr(theme.closeHover)));
header->update();
} }
// ── Action button factory ───────────────────────────────────────────────────── // ── Action button factory ─────────────────────────────────────────────────────
static QPushButton *makeBtn(const QString &label, const QString &tip, QWidget *p) static QPushButton *makeBtn(const QString &label, const QString &tip, QWidget *p, const PopupTheme &t)
{ {
auto *b = new QPushButton(label, p); auto *b = new QPushButton(label, p);
b->setToolTip(tip); b->setToolTip(tip);
b->setFixedHeight(26); b->setFixedHeight(26);
b->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); b->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
b->setStyleSheet(QStringLiteral("QPushButton{" b->setStyleSheet(QStringLiteral("QPushButton{"
" background:#192030;color:#b8c8de;border:1px solid #263040;" " background:%1;color:%2;border:1px solid %3;"
" border-radius:4px;font-size:11px;padding:0 4px;" " border-radius:4px;font-size:11px;padding:0 4px;"
"}" "}"
"QPushButton:hover{background:#223050;color:white;}" "QPushButton:hover{background:%4;color:%5;}"
"QPushButton:pressed{background:#162030;}" "QPushButton:pressed{background:%6;}"
"QPushButton:disabled{color:#384858;border-color:#192030;}")); "QPushButton:disabled{color:%7;border-color:%3;}")
.arg(colorStr(t.buttonBg), colorStr(t.text), colorStr(t.buttonBorder), colorStr(t.buttonHover),
colorStr(t.text), colorStr(t.buttonPressed), colorStr(t.buttonDisabled)));
return b; return b;
} }
void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored) void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored)
{ {
// Clear previous contents // Clear previous contents
delete m_actionArea->layout(); delete actionArea->layout();
const auto old = m_actionArea->findChildren<QPushButton *>(QString{}, Qt::FindDirectChildrenOnly); const auto old = actionArea->findChildren<QPushButton *>(QString{}, Qt::FindDirectChildrenOnly);
for (auto *w : old) { for (auto *w : old) {
w->deleteLater(); w->deleteLater();
} }
const QString name = QString::fromStdString(userInfo.name()); const QString name = QString::fromStdString(userInfo.name());
const auto ownLevel = UserLevelFlags(m_ts->getUserInfo()->user_level()); const auto ownLevel = UserLevelFlags(ts->getUserInfo()->user_level());
const bool isSelf = (name == QString::fromStdString(m_ts->getUserInfo()->name())); const bool isSelf = (name == QString::fromStdString(ts->getUserInfo()->name()));
const bool isMod = ownLevel.testFlag(ServerInfo_User::IsModerator); const bool isMod = ownLevel.testFlag(ServerInfo_User::IsModerator);
const bool isAdmin = ownLevel.testFlag(ServerInfo_User::IsAdmin); const bool isAdmin = ownLevel.testFlag(ServerInfo_User::IsAdmin);
const auto their = UserLevelFlags(userInfo.user_level()); const auto their = UserLevelFlags(userInfo.user_level());
const bool isReg = their.testFlag(ServerInfo_User::IsRegistered); const bool isReg = their.testFlag(ServerInfo_User::IsRegistered);
auto *grid = new QGridLayout(m_actionArea); auto *grid = new QGridLayout(actionArea);
grid->setContentsMargins(8, 6, 8, 6); grid->setContentsMargins(8, 6, 8, 6);
grid->setSpacing(4); grid->setSpacing(4);
@ -394,16 +479,16 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
}; };
// ── Always visible ──────────────────────────────────────────────────────── // ── Always visible ────────────────────────────────────────────────────────
auto *chat = makeBtn(tr("Chat"), tr("Open private chat"), m_actionArea); auto *chat = makeBtn(tr("Chat"), tr("Open private chat"), actionArea, theme);
chat->setEnabled(!isSelf && online); chat->setEnabled(!isSelf && online);
connect(chat, &QPushButton::clicked, this, [this, name] { emit chatRequested(name); }); connect(chat, &QPushButton::clicked, this, [this, name] { emit chatRequested(name); });
add(chat); add(chat);
auto *prof = makeBtn(tr("Profile"), tr("View user profile"), m_actionArea); auto *prof = makeBtn(tr("Profile"), tr("View user profile"), actionArea, theme);
connect(prof, &QPushButton::clicked, this, [this, name] { emit detailsRequested(name); }); connect(prof, &QPushButton::clicked, this, [this, name] { emit detailsRequested(name); });
add(prof); add(prof);
auto *games = makeBtn(tr("Games"), tr("Show this user's games"), m_actionArea); auto *games = makeBtn(tr("Games"), tr("Show this user's games"), actionArea, theme);
games->setEnabled(!isSelf && online); games->setEnabled(!isSelf && online);
connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); }); connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); });
add(games); add(games);
@ -411,20 +496,20 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
// ── Buddy / ignore (registered users only) ──────────────────────────────── // ── Buddy / ignore (registered users only) ────────────────────────────────
if (!isSelf && isReg) { if (!isSelf && isReg) {
if (isBuddy) { if (isBuddy) {
auto *b = makeBtn(tr("− Buddy"), tr("Remove from buddy list"), m_actionArea); auto *b = makeBtn(tr("− Buddy"), tr("Remove from buddy list"), actionArea, theme);
connect(b, &QPushButton::clicked, this, [this, name] { emit removeBuddyRequested(name); }); connect(b, &QPushButton::clicked, this, [this, name] { emit removeBuddyRequested(name); });
add(b); add(b);
} else { } else {
auto *b = makeBtn(tr("+ Buddy"), tr("Add to buddy list"), m_actionArea); auto *b = makeBtn(tr("+ Buddy"), tr("Add to buddy list"), actionArea, theme);
connect(b, &QPushButton::clicked, this, [this, name] { emit addBuddyRequested(name); }); connect(b, &QPushButton::clicked, this, [this, name] { emit addBuddyRequested(name); });
add(b); add(b);
} }
if (isIgnored) { if (isIgnored) {
auto *b = makeBtn(tr("− Ignore"), tr("Remove from ignore list"), m_actionArea); auto *b = makeBtn(tr("− Ignore"), tr("Remove from ignore list"), actionArea, theme);
connect(b, &QPushButton::clicked, this, [this, name] { emit removeIgnoreRequested(name); }); connect(b, &QPushButton::clicked, this, [this, name] { emit removeIgnoreRequested(name); });
add(b); add(b);
} else { } else {
auto *b = makeBtn(tr("+ Ignore"), tr("Add to ignore list"), m_actionArea); auto *b = makeBtn(tr("+ Ignore"), tr("Add to ignore list"), actionArea, theme);
connect(b, &QPushButton::clicked, this, [this, name] { emit addIgnoreRequested(name); }); connect(b, &QPushButton::clicked, this, [this, name] { emit addIgnoreRequested(name); });
add(b); add(b);
} }
@ -437,10 +522,10 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
col = 0; col = 0;
} // start mod section on a fresh row } // start mod section on a fresh row
auto *ban = makeBtn(tr("Ban"), tr("Ban from server"), m_actionArea); auto *ban = makeBtn(tr("Ban"), tr("Ban from server"), actionArea, theme);
auto *warn = makeBtn(tr("Warn"), tr("Warn user"), m_actionArea); auto *warn = makeBtn(tr("Warn"), tr("Warn user"), actionArea, theme);
auto *bLog = makeBtn(tr("Ban log"), tr("View ban history"), m_actionArea); auto *bLog = makeBtn(tr("Ban log"), tr("View ban history"), actionArea, theme);
auto *wLog = makeBtn(tr("Warn log"), tr("View warning history"), m_actionArea); auto *wLog = makeBtn(tr("Warn log"), tr("View warning history"), actionArea, theme);
connect(ban, &QPushButton::clicked, this, [this, name] { emit banRequested(name); }); connect(ban, &QPushButton::clicked, this, [this, name] { emit banRequested(name); });
connect(warn, &QPushButton::clicked, this, [this, name] { emit warnRequested(name); }); connect(warn, &QPushButton::clicked, this, [this, name] { emit warnRequested(name); });
connect(bLog, &QPushButton::clicked, this, [this, name] { emit banHistoryRequested(name); }); connect(bLog, &QPushButton::clicked, this, [this, name] { emit banHistoryRequested(name); });
@ -453,31 +538,31 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
// ── Admin actions ───────────────────────────────────────────────────────── // ── Admin actions ─────────────────────────────────────────────────────────
if (!isSelf && isAdmin) { if (!isSelf && isAdmin) {
auto *notes = makeBtn(tr("Notes"), tr("View admin notes"), m_actionArea); auto *notes = makeBtn(tr("Notes"), tr("View admin notes"), actionArea, theme);
connect(notes, &QPushButton::clicked, this, [this, name] { emit adminNotesRequested(name); }); connect(notes, &QPushButton::clicked, this, [this, name] { emit adminNotesRequested(name); });
add(notes); add(notes);
if (their.testFlag(ServerInfo_User::IsModerator)) { if (their.testFlag(ServerInfo_User::IsModerator)) {
auto *b = makeBtn(tr("− Mod"), tr("Demote from moderator"), m_actionArea); auto *b = makeBtn(tr("− Mod"), tr("Demote from moderator"), actionArea, theme);
connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromModRequested(name); }); connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromModRequested(name); });
add(b); add(b);
} else if (isReg) { } else if (isReg) {
auto *b = makeBtn(tr("+ Mod"), tr("Promote to moderator"), m_actionArea); auto *b = makeBtn(tr("+ Mod"), tr("Promote to moderator"), actionArea, theme);
connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToModRequested(name); }); connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToModRequested(name); });
add(b); add(b);
} }
if (their.testFlag(ServerInfo_User::IsJudge)) { if (their.testFlag(ServerInfo_User::IsJudge)) {
auto *b = makeBtn(tr("− Judge"), tr("Demote from judge"), m_actionArea); auto *b = makeBtn(tr("− Judge"), tr("Demote from judge"), actionArea, theme);
connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromJudgeRequested(name); }); connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromJudgeRequested(name); });
add(b); add(b);
} else if (isReg) { } else if (isReg) {
auto *b = makeBtn(tr("+ Judge"), tr("Promote to judge"), m_actionArea); auto *b = makeBtn(tr("+ Judge"), tr("Promote to judge"), actionArea, theme);
connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToJudgeRequested(name); }); connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToJudgeRequested(name); });
add(b); add(b);
} }
} }
m_actionArea->adjustSize(); actionArea->adjustSize();
} }
void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored) void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored)
@ -488,7 +573,7 @@ void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool on
void UserInfoPopup::onGamesContextMenu(const QPoint &pos) void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
{ {
const QModelIndex idx = m_gamesView->indexAt(pos); const QModelIndex idx = gamesView->indexAt(pos);
if (!idx.isValid()) { if (!idx.isValid()) {
return; return;
} }
@ -501,8 +586,9 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
QMenu menu(this); QMenu menu(this);
menu.setStyleSheet( menu.setStyleSheet(
QStringLiteral("QMenu{background:#12182a;color:#c8d8ec;border:1px solid #1e2838;border-radius:4px;}" QStringLiteral("QMenu{background:%1;color:%2;border:1px solid %3;border-radius:4px;}"
"QMenu::item:selected{background:#223050;}")); "QMenu::item:selected{background:%4;}")
.arg(colorStr(theme.bg), colorStr(theme.text), colorStr(theme.border), colorStr(theme.buttonHover)));
const bool canJoin = !game.started() && game.player_count() < game.max_players(); const bool canJoin = !game.started() && game.player_count() < game.max_players();
QAction *join = menu.addAction(tr("Join game")); QAction *join = menu.addAction(tr("Join game"));
@ -513,7 +599,7 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
spec = menu.addAction(tr("Spectate")); spec = menu.addAction(tr("Spectate"));
} }
const QAction *chosen = menu.exec(m_gamesView->viewport()->mapToGlobal(pos)); const QAction *chosen = menu.exec(gamesView->viewport()->mapToGlobal(pos));
if (!chosen) { if (!chosen) {
return; return;
} }
@ -529,17 +615,17 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
void UserInfoPopup::refreshHeader() void UserInfoPopup::refreshHeader()
{ {
if (m_currentUser.isEmpty()) { if (currentUser.isEmpty()) {
return; return;
} }
const QPixmap avatar = m_avatarCache ? m_avatarCache->value(m_currentUser) : QPixmap{}; const QPixmap avatar = avatarCache ? avatarCache->value(currentUser) : QPixmap{};
const CardArtParams params = (m_cardArtParamsMap && m_cardArtParamsMap->contains(m_currentUser)) const CardArtParams params = (cardArtParamsMap && cardArtParamsMap->contains(currentUser))
? m_cardArtParamsMap->value(m_currentUser) ? cardArtParamsMap->value(currentUser)
: CardArtParams{}; : CardArtParams{};
const QString artKey = m_currentUser + u'|' + params.cardName + u'|' + params.cardProviderId; const QString artKey = currentUser + u'|' + params.cardName + u'|' + params.cardProviderId;
const QPixmap cardArt = (m_cardArtCache && !params.cardName.isEmpty()) ? m_cardArtCache->value(artKey) : QPixmap{}; const QPixmap cardArt = (cardArtCache && !params.cardName.isEmpty()) ? cardArtCache->value(artKey) : QPixmap{};
m_header->setUserData(m_currentUserInfo, m_currentOnline, avatar, cardArt, params); header->setUserData(currentUserInfo, currentOnline, avatar, cardArt, params);
} }
void UserInfoPopup::showForUser(const QString &userName, void UserInfoPopup::showForUser(const QString &userName,
@ -548,9 +634,9 @@ void UserInfoPopup::showForUser(const QString &userName,
bool isBuddy, bool isBuddy,
bool isIgnored) bool isIgnored)
{ {
m_currentUser = userName; currentUser = userName;
m_currentUserInfo = userInfo; currentUserInfo = userInfo;
m_currentOnline = online; currentOnline = online;
// Header // Header
refreshHeader(); refreshHeader();
@ -559,14 +645,14 @@ void UserInfoPopup::showForUser(const QString &userName,
rebuildActionButtons(userInfo, online, isBuddy, isIgnored); rebuildActionButtons(userInfo, online, isBuddy, isIgnored);
// Games list reset // Games list reset
m_gamesModel->clear(); gamesModel->clear();
m_gamesView->hide(); gamesView->hide();
m_gamesStatus->setText(tr("Loading games…")); gamesStatus->setText(tr("Loading games…"));
m_gamesStatus->show(); gamesStatus->show();
// Close button — top-right corner, above everything // Close button — top-right corner, above everything
m_closeBtn->move(PopupWidth - m_closeBtn->width() - 6, 6); closeBtn->move(PopupWidth - closeBtn->width() - 6, 6);
m_closeBtn->raise(); closeBtn->raise();
adjustSize(); adjustSize();
fetchGames(); fetchGames();
@ -576,40 +662,40 @@ void UserInfoPopup::showForUser(const QString &userName,
void UserInfoPopup::fetchGames() void UserInfoPopup::fetchGames()
{ {
if (!m_client || m_currentUser.isEmpty()) { if (!client || currentUser.isEmpty()) {
return; return;
} }
Command_GetGamesOfUser cmd; Command_GetGamesOfUser cmd;
cmd.set_user_name(m_currentUser.toStdString()); cmd.set_user_name(currentUser.toStdString());
const QString snapshot = m_currentUser; const QString snapshot = currentUser;
PendingCommand *pend = m_client->prepareSessionCommand(cmd); PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, connect(pend, &PendingCommand::finished, this,
[this, snapshot](const Response &r) { onGamesReceived(r, snapshot); }); [this, snapshot](const Response &r) { onGamesReceived(r, snapshot); });
m_client->sendCommand(pend); client->sendCommand(pend);
} }
void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser) void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser)
{ {
if (forUser != m_currentUser) { if (forUser != currentUser) {
return; // stale response — different user showing now return; // stale response — different user showing now
} }
m_gamesModel->clear(); gamesModel->clear();
if (r.response_code() != Response::RespOk) { if (r.response_code() != Response::RespOk) {
m_gamesStatus->setText(tr("Could not load games.")); gamesStatus->setText(tr("Could not load games."));
m_gamesStatus->show(); gamesStatus->show();
m_gamesView->hide(); gamesView->hide();
return; return;
} }
const auto &resp = r.GetExtension(Response_GetGamesOfUser::ext); const auto &resp = r.GetExtension(Response_GetGamesOfUser::ext);
if (resp.game_list_size() == 0) { if (resp.game_list_size() == 0) {
m_gamesStatus->setText(tr("No active games.")); gamesStatus->setText(tr("No active games."));
m_gamesStatus->show(); gamesStatus->show();
m_gamesView->hide(); gamesView->hide();
return; return;
} }
@ -617,29 +703,29 @@ void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser)
auto *item = new QStandardItem; auto *item = new QStandardItem;
item->setData(QVariant::fromValue(resp.game_list(i)), PopupRoles::GameData); item->setData(QVariant::fromValue(resp.game_list(i)), PopupRoles::GameData);
item->setEditable(false); item->setEditable(false);
m_gamesModel->appendRow(item); gamesModel->appendRow(item);
} }
m_gamesStatus->hide(); gamesStatus->hide();
m_gamesView->show(); gamesView->show();
// Fit exactly to the number of visible rows, scroll when more than 5 // Fit exactly to the number of visible rows, scroll when more than 5
constexpr int rowH = 38; // must match PopupGameDelegate::sizeHint constexpr int rowH = 38; // must match PopupGameDelegate::sizeHint
constexpr int maxRows = 5; constexpr int maxRows = 5;
const int count = m_gamesModel->rowCount(); const int count = gamesModel->rowCount();
const int visible = qMin(count, maxRows); const int visible = qMin(count, maxRows);
m_gamesView->setFixedHeight(visible * rowH + 2); gamesView->setFixedHeight(visible * rowH + 2);
m_gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff); gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
adjustSize(); adjustSize();
} }
void UserInfoPopup::refreshGames() void UserInfoPopup::refreshGames()
{ {
m_gamesModel->clear(); gamesModel->clear();
m_gamesView->hide(); gamesView->hide();
m_gamesStatus->setText(tr("Loading games…")); gamesStatus->setText(tr("Loading games…"));
m_gamesStatus->show(); gamesStatus->show();
fetchGames(); fetchGames();
} }

View file

@ -26,6 +26,35 @@ namespace PopupRoles
constexpr int GameData = Qt::UserRole + 10; constexpr int GameData = Qt::UserRole + 10;
} }
// ── Popup theme ───────────────────────────────────────────────────────────────
/**
* Palette-derived colors for the popup chrome. Both color schemes read from
* the active QPalette so custom palettes are respected; @c dark only tunes the
* blend strengths.
*/
struct PopupTheme
{
bool dark = false;
QColor bg;
QColor border;
QColor text;
QColor subText;
QColor buttonBg;
QColor buttonBorder;
QColor buttonHover;
QColor buttonPressed;
QColor buttonDisabled;
QColor closeBg;
QColor closeHover;
QColor gamesRow;
QColor gamesSelected;
QColor gamesSeparator;
QColor statusText;
static PopupTheme fromPalette(const QPalette &palette, bool dark);
};
// ── Header widget ───────────────────────────────────────────────────────────── // ── Header widget ─────────────────────────────────────────────────────────────
/** /**
@ -51,21 +80,21 @@ class UserInfoHeaderWidget : public QWidget
public: public:
explicit UserInfoHeaderWidget(QWidget *parent = nullptr); explicit UserInfoHeaderWidget(QWidget *parent = nullptr);
void setUserData(const ServerInfo_User &user, void setUserData(const ServerInfo_User &_user,
bool online, bool _online,
const QPixmap &avatar, const QPixmap &_avatar,
const QPixmap &cardArt, const QPixmap &_cardArt,
const CardArtParams &params); const CardArtParams &_params);
protected: protected:
void paintEvent(QPaintEvent *e) override; void paintEvent(QPaintEvent *e) override;
private: private:
ServerInfo_User m_user; ServerInfo_User user;
bool m_online = false; bool online = false;
QPixmap m_avatar; QPixmap avatar;
QPixmap m_cardArt; QPixmap cardArt;
CardArtParams m_params; CardArtParams params;
}; };
// ── Main popup ──────────────────────────────────────────────────────────────── // ── Main popup ────────────────────────────────────────────────────────────────
@ -93,11 +122,11 @@ class UserInfoPopup : public QFrame
static constexpr int PopupWidth = 316; static constexpr int PopupWidth = 316;
public: public:
explicit UserInfoPopup(TabSupervisor *tabSupervisor, explicit UserInfoPopup(TabSupervisor *_ts,
AbstractClient *client, AbstractClient *_client,
const QMap<QString, QPixmap> *avatarCache, const QMap<QString, QPixmap> *_avatarCache,
const QMap<QString, QPixmap> *cardArtCache, const QMap<QString, QPixmap> *_cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap, const QMap<QString, CardArtParams> *_cardArtParamsMap,
QWidget *parent); QWidget *parent);
/** /**
@ -108,9 +137,9 @@ public:
showForUser(const QString &userName, const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored); showForUser(const QString &userName, const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored);
void fetchGames(); void fetchGames();
[[nodiscard]] QString currentUser() const [[nodiscard]] QString getCurrentUser() const
{ {
return m_currentUser; return currentUser;
} }
/** Called when buddy/ignore status changes externally while popup is open. */ /** Called when buddy/ignore status changes externally while popup is open. */
@ -156,25 +185,30 @@ private slots:
private: private:
void buildUi(); void buildUi();
void applyTheme();
void rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored); void rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored);
TabSupervisor *m_ts; TabSupervisor *ts;
AbstractClient *m_client; AbstractClient *client;
const QMap<QString, QPixmap> *m_avatarCache; const QMap<QString, QPixmap> *avatarCache;
const QMap<QString, QPixmap> *m_cardArtCache; const QMap<QString, QPixmap> *cardArtCache;
const QMap<QString, CardArtParams> *m_cardArtParamsMap; const QMap<QString, CardArtParams> *cardArtParamsMap;
QString m_currentUser; PopupTheme theme;
ServerInfo_User m_currentUserInfo;
bool m_currentOnline = false;
UserInfoHeaderWidget *m_header; QString currentUser;
QWidget *m_actionArea; ///< rebuilt per user ServerInfo_User currentUserInfo;
QListView *m_gamesView; bool currentOnline = false;
QStandardItemModel *m_gamesModel;
QLabel *m_gamesStatus; UserInfoHeaderWidget *header;
QPushButton *m_closeBtn; QWidget *actionArea; ///< rebuilt per user
QPushButton *m_refreshBtn; QLabel *gamesLabel;
QFrame *separator;
QListView *gamesView;
QStandardItemModel *gamesModel;
QLabel *gamesStatus;
QPushButton *closeBtn;
QPushButton *refreshBtn;
}; };
#endif // COCKATRICE_USER_INFO_POPUP_H #endif // COCKATRICE_USER_INFO_POPUP_H

View file

@ -3,6 +3,7 @@
#include "../../interface/pixel_map_generator.h" #include "../../interface/pixel_map_generator.h"
#include <QAbstractScrollArea> #include <QAbstractScrollArea>
#include <QApplication>
#include <QPainter> #include <QPainter>
#include <QPainterPath> #include <QPainterPath>
#include <QScrollBar> #include <QScrollBar>
@ -19,6 +20,29 @@ QSize UserListPainter::sizeHint()
return QSize(0, RowHeight); return QSize(0, RowHeight);
} }
UserListPainter::Style UserListPainter::resolveStyle(const QPalette &palette, bool dark)
{
Style style;
style.dark = dark;
const QColor base = palette.color(QPalette::Base);
const QColor alt = palette.color(QPalette::AlternateBase);
style.cardStart = base;
style.cardEnd = (alt != base) ? alt : palette.color(QPalette::Midlight);
style.base = base;
style.textOnline = palette.color(QPalette::Text);
style.textOffline = palette.color(QPalette::Disabled, QPalette::Text);
style.ringOffline = palette.color(QPalette::Disabled, QPalette::Text);
style.dropShadow = dark;
return style;
}
QColor UserListPainter::blend(const QColor &a, const QColor &b, qreal t)
{
const qreal u = 1.0 - t;
return QColor(qRound(a.red() * u + b.red() * t), qRound(a.green() * u + b.green() * t),
qRound(a.blue() * u + b.blue() * t), qRound(a.alpha() * u + b.alpha() * t));
}
QColor UserListPainter::getAccentColor(const UserLevelFlags &userLevel, bool online) QColor UserListPainter::getAccentColor(const UserLevelFlags &userLevel, bool online)
{ {
QColor accentColor; QColor accentColor;
@ -59,11 +83,22 @@ int UserListPainter::getCardRight(const QStyleOptionViewItem &option, const QRec
void UserListPainter::drawBackground(QPainter *painter, void UserListPainter::drawBackground(QPainter *painter,
const QRectF &cardRect, const QRectF &cardRect,
const QColor &accentColor, const QColor &accentColor,
bool selected) bool selected,
const Style &style)
{ {
QLinearGradient bg(cardRect.topLeft(), cardRect.topRight()); QLinearGradient bg(cardRect.topLeft(), cardRect.topRight());
bg.setColorAt(0, selected ? accentColor.darker(130) : accentColor.darker(320)); if (selected) {
bg.setColorAt(1, selected ? QColor(40, 48, 60) : QColor(18, 22, 30)); bg.setColorAt(0, blend(style.cardStart, accentColor, 0.20));
bg.setColorAt(1, blend(style.cardEnd, accentColor, 0.13));
} else {
// Gentle accent tint so a role's color stays visible on unselected
// cards without shouting over the list. Light cards need a slightly
// stronger blend because the base is near-white.
const qreal startT = style.dark ? 0.12 : 0.16;
const qreal endT = style.dark ? 0.08 : 0.10;
bg.setColorAt(0, blend(style.cardStart, accentColor, startT));
bg.setColorAt(1, blend(style.cardEnd, accentColor, endT));
}
painter->setPen(Qt::NoPen); painter->setPen(Qt::NoPen);
painter->setBrush(bg); painter->setBrush(bg);
@ -163,7 +198,8 @@ void UserListPainter::drawAvatar(QPainter *painter,
const UserLevelFlags &userLevel, const UserLevelFlags &userLevel,
const ServerInfo_User &userInfo, const ServerInfo_User &userInfo,
const QString &privLevel, const QString &privLevel,
const QMap<QString, QPixmap> *avatarCache) const QMap<QString, QPixmap> *avatarCache,
const Style &style)
{ {
QPainterPath clipPath; QPainterPath clipPath;
clipPath.addEllipse(avatarRect); clipPath.addEllipse(avatarRect);
@ -183,7 +219,7 @@ void UserListPainter::drawAvatar(QPainter *painter,
} }
if (!drewAvatar) { if (!drewAvatar) {
painter->setBrush(accentColor.darker(200)); painter->setBrush(blend(accentColor, style.base, style.dark ? 0.45 : 0.72));
painter->setPen(Qt::NoPen); painter->setPen(Qt::NoPen);
painter->drawEllipse(avatarRect); painter->drawEllipse(avatarRect);
@ -196,9 +232,9 @@ void UserListPainter::drawAvatar(QPainter *painter,
painter->restore(); painter->restore();
} }
void UserListPainter::drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online) void UserListPainter::drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online, const Style &style)
{ {
const QColor statusColor = online ? QColor(34, 197, 94) : QColor(70, 80, 95); const QColor statusColor = online ? QColor(34, 197, 94) : style.ringOffline;
painter->setPen(QPen(statusColor, 2)); painter->setPen(QPen(statusColor, 2));
painter->setBrush(Qt::NoBrush); painter->setBrush(Qt::NoBrush);
@ -212,7 +248,7 @@ void UserListPainter::drawUserName(QPainter *painter,
int textX, int textX,
const QString &userName, const QString &userName,
bool online, bool online,
bool selected) const Style &style)
{ {
QFont nameFont = option.font; QFont nameFont = option.font;
nameFont.setBold(true); nameFont.setBold(true);
@ -221,10 +257,12 @@ void UserListPainter::drawUserName(QPainter *painter,
const QRect nameRect(textX, rect.top() + 8, cardRight - textX - 10, 20); const QRect nameRect(textX, rect.top() + 8, cardRight - textX - 10, 20);
const QString elidedName = QFontMetrics(nameFont).elidedText(userName, Qt::ElideRight, cardRight - textX - 10); const QString elidedName = QFontMetrics(nameFont).elidedText(userName, Qt::ElideRight, cardRight - textX - 10);
painter->setPen(QColor(0, 0, 0, 200)); if (style.dropShadow) {
painter->drawText(nameRect.translated(1, 1), Qt::AlignVCenter | Qt::AlignLeft, elidedName); painter->setPen(QColor(0, 0, 0, 200));
painter->drawText(nameRect.translated(1, 1), Qt::AlignVCenter | Qt::AlignLeft, elidedName);
}
painter->setPen(online ? (selected ? Qt::white : QColor(226, 232, 240)) : QColor(90, 100, 115)); painter->setPen(online ? style.textOnline : style.textOffline);
painter->drawText(nameRect, Qt::AlignVCenter | Qt::AlignLeft, elidedName); painter->drawText(nameRect, Qt::AlignVCenter | Qt::AlignLeft, elidedName);
} }
@ -262,7 +300,8 @@ void UserListPainter::drawBadges(QPainter *painter,
const QRect &rect, const QRect &rect,
int cardRight, int cardRight,
const QList<Badge> &badges, const QList<Badge> &badges,
bool online) bool online,
const Style &style)
{ {
if (badges.isEmpty()) { if (badges.isEmpty()) {
return; return;
@ -284,15 +323,17 @@ void UserListPainter::drawBadges(QPainter *painter,
int bx = cardRight - 6 - totalBadgeW; int bx = cardRight - 6 - totalBadgeW;
for (const Badge &b : badges) { for (const Badge &b : badges) {
const QColor col = online ? b.color : b.color.darker(180); const QColor col = online ? b.color : blend(b.color, style.base, 0.55);
const QColor surface = blend(col, style.base, style.dark ? 0.55 : 0.78);
const QColor text = style.dark ? blend(col, Qt::white, 0.5) : blend(col, Qt::black, 0.35);
const int bw = fm.horizontalAdvance(b.text) + 8; const int bw = fm.horizontalAdvance(b.text) + 8;
const QRect br(bx, rect.top() + 44, bw, 13); const QRect br(bx, rect.top() + 44, bw, 13);
painter->setPen(Qt::NoPen); painter->setPen(Qt::NoPen);
painter->setBrush(col.darker(online ? 160 : 220)); painter->setBrush(surface);
painter->drawRoundedRect(br, 3, 3); painter->drawRoundedRect(br, 3, 3);
painter->setPen(col.lighter(online ? 160 : 100)); painter->setPen(text);
painter->drawText(br, Qt::AlignCenter, b.text); painter->drawText(br, Qt::AlignCenter, b.text);
bx += bw + 4; bx += bw + 4;
@ -305,11 +346,19 @@ void UserListPainter::paint(QPainter *painter,
const ServerInfo_User &userInfo, const ServerInfo_User &userInfo,
const QMap<QString, QPixmap> *avatarCache, const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache, const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap) const QMap<QString, CardArtParams> *cardArtParamsMap,
bool dark)
{ {
painter->save(); painter->save();
painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing); painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
// The delegate supplies the application palette in option.palette, which
// always reflects the active theme — the widget palette can be stale after
// a runtime theme change, so it is only used as a defensive fallback.
const QPalette pal =
option.palette == QPalette() ? (option.widget ? option.widget->palette() : qApp->palette()) : option.palette;
const Style style = resolveStyle(pal, dark);
const QRect rect = option.rect; const QRect rect = option.rect;
const bool online = index.data(Qt::UserRole + 1).toBool(); const bool online = index.data(Qt::UserRole + 1).toBool();
const bool selected = option.state & QStyle::State_Selected; const bool selected = option.state & QStyle::State_Selected;
@ -324,19 +373,19 @@ void UserListPainter::paint(QPainter *painter,
? cardArtParamsMap->value(userName) ? cardArtParamsMap->value(userName)
: CardArtParams{}; : CardArtParams{};
drawBackground(painter, cardRect, accentColor, selected); drawBackground(painter, cardRect, accentColor, selected, style);
drawCardArt(painter, rect, cardRight, userName, cardArtCache, params); drawCardArt(painter, rect, cardRight, userName, cardArtCache, params);
const QRect avatarRect = getAvatarRect(rect); const QRect avatarRect = getAvatarRect(rect);
drawAvatar(painter, avatarRect, userName, accentColor, userLevel, userInfo, privLevel, avatarCache); drawAvatar(painter, avatarRect, userName, accentColor, userLevel, userInfo, privLevel, avatarCache, style);
drawStatusRing(painter, avatarRect, online); drawStatusRing(painter, avatarRect, online, style);
const int textX = avatarRect.right() + TextSpacing; const int textX = avatarRect.right() + TextSpacing;
drawUserName(painter, option, rect, cardRight, textX, userName, online, selected); drawUserName(painter, option, rect, cardRight, textX, userName, online, style);
drawCountryFlag(painter, rect, textX, userInfo); drawCountryFlag(painter, rect, textX, userInfo);
const QList<Badge> badges = buildBadges(userLevel, privLevel); const QList<Badge> badges = buildBadges(userLevel, privLevel);
drawBadges(painter, option, rect, cardRight, badges, online); drawBadges(painter, option, rect, cardRight, badges, online, style);
painter->restore(); painter->restore();
} }

View file

@ -6,6 +6,7 @@
#include <QColor> #include <QColor>
#include <QList> #include <QList>
#include <QMap> #include <QMap>
#include <QPalette>
#include <QPixmap> #include <QPixmap>
#include <QRect> #include <QRect>
#include <QSize> #include <QSize>
@ -28,13 +29,36 @@ struct CardArtParams
class UserListPainter class UserListPainter
{ {
public: public:
/**
* Palette-derived surface colors for the current color scheme. Both the
* light and the dark scheme read from the active QPalette so custom
* palettes are respected; @c dark only tunes the blend strengths (and
* whether the name text keeps its drop shadow).
*/
struct Style
{
bool dark = true;
QColor cardStart; ///< row fill, left edge (normal)
QColor cardEnd; ///< row fill, right edge (normal)
QColor base; ///< lightest surface, used for blending accent hues
QColor textOnline;
QColor textOffline;
QColor ringOffline;
bool dropShadow = false;
};
static Style resolveStyle(const QPalette &palette, bool dark);
/// Linear interpolation: @p t = 0 returns @p a, @p t = 1 returns @p b.
static QColor blend(const QColor &a, const QColor &b, qreal t);
static void paint(QPainter *painter, static void paint(QPainter *painter,
const QStyleOptionViewItem &option, const QStyleOptionViewItem &option,
const QModelIndex &index, const QModelIndex &index,
const ServerInfo_User &userInfo, const ServerInfo_User &userInfo,
const QMap<QString, QPixmap> *avatarCache, const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache, const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap); const QMap<QString, CardArtParams> *cardArtParamsMap,
bool dark);
static QSize sizeHint(); static QSize sizeHint();
@ -55,7 +79,11 @@ private:
static QColor getAccentColor(const UserLevelFlags &userLevel, bool online); static QColor getAccentColor(const UserLevelFlags &userLevel, bool online);
static int getCardRight(const QStyleOptionViewItem &option, const QRect &rect); static int getCardRight(const QStyleOptionViewItem &option, const QRect &rect);
static void drawBackground(QPainter *painter, const QRectF &cardRect, const QColor &accentColor, bool selected); static void drawBackground(QPainter *painter,
const QRectF &cardRect,
const QColor &accentColor,
bool selected,
const Style &style);
static QRect getAvatarRect(const QRect &rect); static QRect getAvatarRect(const QRect &rect);
static void drawAvatar(QPainter *painter, static void drawAvatar(QPainter *painter,
const QRect &avatarRect, const QRect &avatarRect,
@ -64,8 +92,9 @@ private:
const UserLevelFlags &userLevel, const UserLevelFlags &userLevel,
const ServerInfo_User &userInfo, const ServerInfo_User &userInfo,
const QString &privLevel, const QString &privLevel,
const QMap<QString, QPixmap> *avatarCache); const QMap<QString, QPixmap> *avatarCache,
static void drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online); const Style &style);
static void drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online, const Style &style);
static void drawUserName(QPainter *painter, static void drawUserName(QPainter *painter,
const QStyleOptionViewItem &option, const QStyleOptionViewItem &option,
const QRect &rect, const QRect &rect,
@ -73,7 +102,7 @@ private:
int textX, int textX,
const QString &userName, const QString &userName,
bool online, bool online,
bool selected); const Style &style);
static void drawCountryFlag(QPainter *painter, const QRect &rect, int textX, const ServerInfo_User &userInfo); static void drawCountryFlag(QPainter *painter, const QRect &rect, int textX, const ServerInfo_User &userInfo);
static QList<Badge> buildBadges(const UserLevelFlags &userLevel, const QString &privLevel); static QList<Badge> buildBadges(const UserLevelFlags &userLevel, const QString &privLevel);
static void drawBadges(QPainter *painter, static void drawBadges(QPainter *painter,
@ -81,7 +110,8 @@ private:
const QRect &rect, const QRect &rect,
int cardRight, int cardRight,
const QList<Badge> &badges, const QList<Badge> &badges,
bool online); bool online,
const Style &style);
}; };
#endif // COCKATRICE_USER_LIST_PAINTER_H #endif // COCKATRICE_USER_LIST_PAINTER_H

View file

@ -3,6 +3,7 @@
#include "../../../../client/settings/cache_settings.h" #include "../../../../client/settings/cache_settings.h"
#include "../../../card_picture_loader/card_picture_loader.h" #include "../../../card_picture_loader/card_picture_loader.h"
#include "../../interface/pixel_map_generator.h" #include "../../interface/pixel_map_generator.h"
#include "../../interface/theme_manager.h"
#include "../../interface/widgets/tabs/tab_account.h" #include "../../interface/widgets/tabs/tab_account.h"
#include "../../interface/widgets/tabs/tab_supervisor.h" #include "../../interface/widgets/tabs/tab_supervisor.h"
#include "../game_selector.h" #include "../game_selector.h"
@ -332,11 +333,11 @@ constexpr int UserInfo = Qt::UserRole + 2;
// rows (UserListTWI, which uses QTreeWidgetItem::Type) by this item type. // rows (UserListTWI, which uses QTreeWidgetItem::Type) by this item type.
constexpr int SectionItemType = QTreeWidgetItem::UserType + 1; constexpr int SectionItemType = QTreeWidgetItem::UserType + 1;
UserListItemDelegate::UserListItemDelegate(QObject *const parent, UserListItemDelegate::UserListItemDelegate(QTreeWidget *tree,
const QMap<QString, QPixmap> *avatarCache, const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache, const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap) const QMap<QString, CardArtParams> *cardArtParamsMap)
: QStyledItemDelegate(parent), avatarCache(avatarCache), cardArtCache(cardArtCache), : QStyledItemDelegate(tree), tree(tree), avatarCache(avatarCache), cardArtCache(cardArtCache),
cardArtParamsMap(cardArtParamsMap) cardArtParamsMap(cardArtParamsMap)
{ {
} }
@ -369,20 +370,121 @@ QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const Q
void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{ {
if (!SettingsCache::instance().appearance().getStyleUserList()) { const bool styled = SettingsCache::instance().appearance().getStyleUserList();
QStyledItemDelegate::paint(painter, option, index); // UserInfo/Online are stored on column 0 only; the name lives on column 2,
// so resolve the user data against column 0 no matter which cell is painted.
const QModelIndex userIndex = index.siblingAtColumn(0).isValid() ? index.siblingAtColumn(0) : index;
const QVariant var = userIndex.data(UserListRoles::UserInfo);
if (styled && var.isValid()) {
// Styled card rows: the tree's cached palette can be stale after a
// runtime theme change, so paint from the application palette (always
// current) instead of option.palette (frozen at the last style switch).
QStyleOptionViewItem opt = option;
opt.palette = qApp->palette();
UserListPainter::paint(painter, opt, index, var.value<ServerInfo_User>(), avatarCache, cardArtCache,
cardArtParamsMap, themeManager && themeManager->isDarkModeActive());
return; return;
} }
const QVariant var = index.data(UserListRoles::UserInfo); // Unstyled rows and section dividers are painted manually so every color
// is derived from the current application palette at paint time: the widget
// palette, the view's alternation, and the stored per-item brushes all go
// stale after a runtime theme change.
const QPalette appPal = qApp->palette();
const bool selected = option.state & QStyle::State_Selected;
const bool hovered = option.state & QStyle::State_MouseOver;
if (!var.isValid()) { // Row background: selection, hover, zebra striping, plain base.
QStyledItemDelegate::paint(painter, option, index); QColor bg = appPal.color(QPalette::Base);
return; if (selected) {
bg = appPal.color(QPalette::Highlight);
} else if (hovered) {
bg = UserListPainter::blend(appPal.color(QPalette::Base), appPal.color(QPalette::Highlight), 0.12);
} else if (var.isValid()) {
// Zebra with per-section parity: the dividers are top-level rows too,
// so the view's own alternation would drift across sections. Count the
// visible user rows back to the previous divider within the same parent
// (sectioned mode stores users as children of the dividers, flat mode
// as top-level rows), so the stripe restarts at every divider and at
// the tree top. Hidden filter matches are skipped the same way the view
// skips them, so adjacent visible rows always alternate.
int usersSinceDivider = 0;
const QModelIndex parent = userIndex.parent();
for (int r = userIndex.row() - 1; r >= 0; --r) {
if (tree->isRowHidden(r, parent)) {
continue;
}
const QModelIndex above = userIndex.model()->index(r, 0, parent);
if (above.isValid() && above.data(UserListRoles::UserInfo).isValid()) {
++usersSinceDivider;
} else {
break;
}
}
if (usersSinceDivider % 2 == 1) {
bg = appPal.color(QPalette::AlternateBase);
}
}
// Paint the row background. In the column-0 pass the fill spans the full
// viewport width so stripes, hover and selection cover the whole row (the
// name column is content-sized in unstyled mode); later column passes fill
// only their own cell, which is the same color and cannot cover the icons.
QRect bgRect = option.rect;
if (index.column() == 0) {
bgRect = QRect(0, option.rect.top(), tree->viewport()->width(), option.rect.height());
}
painter->fillRect(bgRect, bg);
// Text color.
QColor fg = appPal.color(QPalette::Text);
if (selected) {
fg = appPal.color(QPalette::HighlightedText);
} else if (!var.isValid()) {
// Section divider: muted application text color.
fg = appPal.color(QPalette::WindowText);
fg.setAlpha(170);
} else if (index.column() == 2) {
// Name column: online/offline color recomputed at paint time instead
// of trusting the brush stored at login time.
QTreeWidgetItem *item = tree->itemFromIndex(index);
const bool online = item && item->data(0, UserListRoles::Online).toBool();
if (online) {
fg = appPal.color(QPalette::WindowText);
} else {
fg = (themeManager && themeManager->isDarkModeActive())
? QColor(Qt::gray)
: UserListPainter::blend(appPal.color(QPalette::Text), appPal.color(QPalette::Mid), 0.5);
}
} }
UserListPainter::paint(painter, option, index, var.value<ServerInfo_User>(), avatarCache, cardArtCache, // Icon (level badge in column 0, country flag in column 1).
cardArtParamsMap); QRect textRect = option.rect;
const QIcon icon = index.data(Qt::DecorationRole).value<QIcon>();
if (!icon.isNull()) {
const QSize iconSize = icon.actualSize(QSize(18, 18));
const QRect iconRect(option.rect.left() + 2, option.rect.center().y() - iconSize.height() / 2, iconSize.width(),
iconSize.height());
icon.paint(painter, iconRect);
textRect.setLeft(iconRect.right() + 4);
}
// Text (name column / divider title), elided to the row width.
painter->save();
painter->setPen(fg);
const QFont itemFont = index.data(Qt::FontRole).value<QFont>();
painter->setFont(itemFont.isCopyOf(QFont()) ? option.font : itemFont);
const QString text = index.data(Qt::DisplayRole).toString();
const QString elided = painter->fontMetrics().elidedText(text, Qt::ElideRight, textRect.width() - 4);
painter->drawText(textRect.adjusted(2, 0, -2, 0), Qt::AlignLeft | Qt::AlignVCenter, elided);
painter->restore();
// Focus indicator for the current item.
if (option.state & QStyle::State_HasFocus) {
painter->setPen(appPal.color(QPalette::Highlight));
painter->drawRect(option.rect.adjusted(0, 0, -1, -1));
}
} }
UserListTWI::UserListTWI(const ServerInfo_User &_userInfo) : QTreeWidgetItem(Type) UserListTWI::UserListTWI(const ServerInfo_User &_userInfo) : QTreeWidgetItem(Type)
@ -406,8 +508,10 @@ void UserListTWI::setUserInfo(const ServerInfo_User &_userInfo)
void UserListTWI::setOnline(bool online) void UserListTWI::setOnline(bool online)
{ {
// Only the online state is stored here: the delegate derives the
// online/offline text color at paint time from the current application
// palette, so no brush is cached (it would go stale on theme change).
setData(0, UserListRoles::Online, online); setData(0, UserListRoles::Online, online);
setData(2, Qt::ForegroundRole, online ? qApp->palette().brush(QPalette::WindowText) : QBrush(Qt::gray));
} }
/** /**
@ -476,9 +580,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
avatarProvider = new UserAvatarProvider(client, this); avatarProvider = new UserAvatarProvider(client, this);
cardArtProvider = new UserCardArtProvider(this); cardArtProvider = new UserCardArtProvider(this);
itemDelegate =
new UserListItemDelegate(this, &avatarProvider->cache(), &cardArtProvider->cache(), &cardArtParamsMap);
userContextMenu = new UserContextMenu(tabSupervisor, this); userContextMenu = new UserContextMenu(tabSupervisor, this);
connect(userContextMenu, &UserContextMenu::openMessageDialog, this, &UserListWidget::openMessageDialog); connect(userContextMenu, &UserContextMenu::openMessageDialog, this, &UserListWidget::openMessageDialog);
@ -489,6 +590,8 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
userTree->setHeaderHidden(true); userTree->setHeaderHidden(true);
userTree->setRootIsDecorated(false); userTree->setRootIsDecorated(false);
userTree->setIconSize(QSize(20, 18)); userTree->setIconSize(QSize(20, 18));
itemDelegate =
new UserListItemDelegate(userTree, &avatarProvider->cache(), &cardArtProvider->cache(), &cardArtParamsMap);
userTree->setItemDelegate(itemDelegate); userTree->setItemDelegate(itemDelegate);
userTree->setAlternatingRowColors(true); userTree->setAlternatingRowColors(true);
userTree->hideColumn(1); userTree->hideColumn(1);
@ -499,28 +602,28 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
userTree->header()->setStretchLastSection(true); userTree->header()->setStretchLastSection(true);
// ── Hover popup ─────────────────────────────────────────────────────────── // ── Hover popup ───────────────────────────────────────────────────────────
m_userInfoPopup = new UserInfoPopup(tabSupervisor, tabSupervisor->getClient(), &avatarProvider->cache(), userInfoPopup = new UserInfoPopup(tabSupervisor, tabSupervisor->getClient(), &avatarProvider->cache(),
&cardArtProvider->cache(), &cardArtParamsMap, &cardArtProvider->cache(), &cardArtParamsMap,
window()); // parented to main window so it floats above siblings window()); // parented to main window so it floats above siblings
m_userInfoPopup->hide(); userInfoPopup->hide();
m_userInfoPopup->setWindowOpacity(0.0); userInfoPopup->setWindowOpacity(0.0);
m_userInfoPopup->installEventFilter(this); userInfoPopup->installEventFilter(this);
m_showPopupTimer = new QTimer(this); showPopupTimer = new QTimer(this);
m_showPopupTimer->setSingleShot(true); showPopupTimer->setSingleShot(true);
m_showPopupTimer->setInterval(280); showPopupTimer->setInterval(280);
connect(m_showPopupTimer, &QTimer::timeout, this, [this] { connect(showPopupTimer, &QTimer::timeout, this, [this] {
if (!m_hoveredUser.isEmpty()) { if (!hoveredUser.isEmpty()) {
showPopupForUser(m_hoveredUser); showPopupForUser(hoveredUser);
} }
}); });
m_hidePopupTimer = new QTimer(this); hidePopupTimer = new QTimer(this);
m_hidePopupTimer->setSingleShot(true); hidePopupTimer->setSingleShot(true);
m_hidePopupTimer->setInterval(160); hidePopupTimer->setInterval(160);
connect(m_hidePopupTimer, &QTimer::timeout, this, [this] { connect(hidePopupTimer, &QTimer::timeout, this, [this] {
if (!m_popupPinned && !m_userInfoPopup->underMouse() && !userTree->underMouse()) { if (!popupPinned && !userInfoPopup->underMouse() && !userTree->underMouse()) {
hidePopup(); hidePopup();
} }
}); });
@ -546,16 +649,16 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
return; // divider rows have no user popup return; // divider rows have no user popup
} }
const QString name = static_cast<UserListTWI *>(item)->getUserInfo().name().c_str(); const QString name = static_cast<UserListTWI *>(item)->getUserInfo().name().c_str();
m_popupPinned = false; // reset so showPopupForUser can update popupPinned = false; // reset so showPopupForUser can update
showPopupForUser(name); showPopupForUser(name);
m_popupPinned = true; // pin after showing popupPinned = true; // pin after showing
}); });
connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this, connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this,
[this](const QItemSelection &sel, const QItemSelection &) { [this](const QItemSelection &sel, const QItemSelection &) {
// if (m_rebuildingTree) return; // if (m_rebuildingTree) return;
if (sel.isEmpty() && m_popupPinned) { if (sel.isEmpty() && popupPinned) {
m_popupPinned = false; popupPinned = false;
hidePopup(); hidePopup();
} }
}); });
@ -570,13 +673,13 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
// Hide popup when list scrolls (reference row has moved) // Hide popup when list scrolls (reference row has moved)
connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] { connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] {
m_showPopupTimer->stop(); showPopupTimer->stop();
hidePopup(true); hidePopup(true);
requestAvatarsForVisibleItems(); requestAvatarsForVisibleItems();
}); });
// Forward join requests from popup upward // Forward join requests from popup upward
connect(m_userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested); connect(userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested);
connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this, &UserListWidget::refreshVisibleUserHeader); connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this, &UserListWidget::refreshVisibleUserHeader);
connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this, &UserListWidget::refreshVisibleUserHeader); connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this, &UserListWidget::refreshVisibleUserHeader);
@ -585,6 +688,17 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
&UserListWidget::applyDisplayMode); &UserListWidget::applyDisplayMode);
applyDisplayMode(); applyDisplayMode();
// The tree's cached palette can go stale after a runtime theme change (Qt
// freezes widget palettes when the style is switched), so rows and dividers
// derive all colors from the application palette at paint time. The theme
// change only needs a repaint to pick them up.
if (themeManager) {
connect(themeManager, &ThemeManager::themeChanged, this, [this] {
userTree->viewport()->update();
userTree->update();
});
}
QVBoxLayout *vbox = new QVBoxLayout; QVBoxLayout *vbox = new QVBoxLayout;
vbox->addWidget(userTree); vbox->addWidget(userTree);
@ -662,7 +776,7 @@ void UserListWidget::bind(UserListManager *mgr)
// ── Popup button refresh ────────────────────────────────────────────────── // ── Popup button refresh ──────────────────────────────────────────────────
// Any buddy/ignore mutation while the popup is open refreshes its buttons // Any buddy/ignore mutation while the popup is open refreshes its buttons
auto refreshIfPopupOpen = [this](const QString &name) { auto refreshIfPopupOpen = [this](const QString &name) {
if (m_userInfoPopup && m_userInfoPopup->isVisible() && m_userInfoPopup->currentUser() == name) { if (userInfoPopup && userInfoPopup->isVisible() && userInfoPopup->getCurrentUser() == name) {
refreshPopupButtons(name); refreshPopupButtons(name);
} }
}; };
@ -683,8 +797,8 @@ void UserListWidget::bind(UserListManager *mgr)
void UserListWidget::refreshVisibleUserHeader(const QString &name) void UserListWidget::refreshVisibleUserHeader(const QString &name)
{ {
userTree->viewport()->update(); userTree->viewport()->update();
if (m_userInfoPopup->isVisible() && m_userInfoPopup->currentUser() == name) { if (userInfoPopup->isVisible() && userInfoPopup->getCurrentUser() == name) {
m_userInfoPopup->refreshHeader(); userInfoPopup->refreshHeader();
} }
} }
@ -700,15 +814,15 @@ void UserListWidget::refreshPopupButtons(const QString &userName)
const bool isBuddy = proxy->isUserBuddy(userName); const bool isBuddy = proxy->isUserBuddy(userName);
const bool isIgn = proxy->isUserIgnored(userName); const bool isIgn = proxy->isUserIgnored(userName);
m_userInfoPopup->updateActionButtons(item->getUserInfo(), online, isBuddy, isIgn); userInfoPopup->updateActionButtons(item->getUserInfo(), online, isBuddy, isIgn);
positionPopup(userName); // height may have changed — reposition positionPopup(userName); // height may have changed — reposition
} }
void UserListWidget::hideEvent(QHideEvent *e) void UserListWidget::hideEvent(QHideEvent *e)
{ {
QGroupBox::hideEvent(e); QGroupBox::hideEvent(e);
m_showPopupTimer->stop(); showPopupTimer->stop();
m_hidePopupTimer->stop(); hidePopupTimer->stop();
hidePopup(true); hidePopup(true);
} }
@ -739,39 +853,39 @@ void UserListWidget::applyDisplayMode()
void UserListWidget::connectPopupSignals() void UserListWidget::connectPopupSignals()
{ {
connect(m_userInfoPopup, &UserInfoPopup::closeRequested, this, [this] { connect(userInfoPopup, &UserInfoPopup::closeRequested, this, [this] {
m_popupPinned = false; popupPinned = false;
hidePopup(true); hidePopup(true);
}); });
connect(m_userInfoPopup, &UserInfoPopup::mouseEnteredPopup, m_hidePopupTimer, &QTimer::stop); connect(userInfoPopup, &UserInfoPopup::mouseEnteredPopup, hidePopupTimer, &QTimer::stop);
connect(m_userInfoPopup, &UserInfoPopup::mouseLeftPopup, this, [this] { connect(userInfoPopup, &UserInfoPopup::mouseLeftPopup, this, [this] {
if (!m_popupPinned) { if (!popupPinned) {
m_hidePopupTimer->start(); hidePopupTimer->start();
} }
}); });
// Wire all action signals to UserContextMenu::exec*() // Wire all action signals to UserContextMenu::exec*()
connect(m_userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat); connect(userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat);
connect(m_userInfoPopup, &UserInfoPopup::detailsRequested, userContextMenu, &UserContextMenu::execDetails); connect(userInfoPopup, &UserInfoPopup::detailsRequested, userContextMenu, &UserContextMenu::execDetails);
connect(m_userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames); connect(userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames);
connect(m_userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy); connect(userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy);
connect(m_userInfoPopup, &UserInfoPopup::removeBuddyRequested, userContextMenu, connect(userInfoPopup, &UserInfoPopup::removeBuddyRequested, userContextMenu,
&UserContextMenu::execRemoveFromBuddy); &UserContextMenu::execRemoveFromBuddy);
connect(m_userInfoPopup, &UserInfoPopup::addIgnoreRequested, userContextMenu, &UserContextMenu::execAddToIgnore); connect(userInfoPopup, &UserInfoPopup::addIgnoreRequested, userContextMenu, &UserContextMenu::execAddToIgnore);
connect(m_userInfoPopup, &UserInfoPopup::removeIgnoreRequested, userContextMenu, connect(userInfoPopup, &UserInfoPopup::removeIgnoreRequested, userContextMenu,
&UserContextMenu::execRemoveFromIgnore); &UserContextMenu::execRemoveFromIgnore);
connect(m_userInfoPopup, &UserInfoPopup::banRequested, userContextMenu, &UserContextMenu::execBan); connect(userInfoPopup, &UserInfoPopup::banRequested, userContextMenu, &UserContextMenu::execBan);
connect(m_userInfoPopup, &UserInfoPopup::warnRequested, userContextMenu, &UserContextMenu::execWarn); connect(userInfoPopup, &UserInfoPopup::warnRequested, userContextMenu, &UserContextMenu::execWarn);
connect(m_userInfoPopup, &UserInfoPopup::banHistoryRequested, userContextMenu, &UserContextMenu::execBanHistory); connect(userInfoPopup, &UserInfoPopup::banHistoryRequested, userContextMenu, &UserContextMenu::execBanHistory);
connect(m_userInfoPopup, &UserInfoPopup::warnHistoryRequested, userContextMenu, &UserContextMenu::execWarnHistory); connect(userInfoPopup, &UserInfoPopup::warnHistoryRequested, userContextMenu, &UserContextMenu::execWarnHistory);
connect(m_userInfoPopup, &UserInfoPopup::adminNotesRequested, userContextMenu, &UserContextMenu::execAdminNotes); connect(userInfoPopup, &UserInfoPopup::adminNotesRequested, userContextMenu, &UserContextMenu::execAdminNotes);
connect(m_userInfoPopup, &UserInfoPopup::promoteToModRequested, this, connect(userInfoPopup, &UserInfoPopup::promoteToModRequested, this,
[this](const QString &n) { userContextMenu->execAdjustMod(n, true); }); [this](const QString &n) { userContextMenu->execAdjustMod(n, true); });
connect(m_userInfoPopup, &UserInfoPopup::demoteFromModRequested, this, connect(userInfoPopup, &UserInfoPopup::demoteFromModRequested, this,
[this](const QString &n) { userContextMenu->execAdjustMod(n, false); }); [this](const QString &n) { userContextMenu->execAdjustMod(n, false); });
connect(m_userInfoPopup, &UserInfoPopup::promoteToJudgeRequested, this, connect(userInfoPopup, &UserInfoPopup::promoteToJudgeRequested, this,
[this](const QString &n) { userContextMenu->execAdjustJudge(n, true); }); [this](const QString &n) { userContextMenu->execAdjustJudge(n, true); });
connect(m_userInfoPopup, &UserInfoPopup::demoteFromJudgeRequested, this, connect(userInfoPopup, &UserInfoPopup::demoteFromJudgeRequested, this,
[this](const QString &n) { userContextMenu->execAdjustJudge(n, false); }); [this](const QString &n) { userContextMenu->execAdjustJudge(n, false); });
} }
@ -789,25 +903,25 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event)
hovName = QString::fromStdString(static_cast<UserListTWI *>(hoveredItem)->getUserInfo().name()); hovName = QString::fromStdString(static_cast<UserListTWI *>(hoveredItem)->getUserInfo().name());
} }
if (hovName != m_hoveredUser) { if (hovName != hoveredUser) {
m_hoveredUser = hovName; hoveredUser = hovName;
if (!hovName.isEmpty()) { if (!hovName.isEmpty()) {
m_hidePopupTimer->stop(); hidePopupTimer->stop();
if (!m_popupPinned) { if (!popupPinned) {
m_showPopupTimer->start(); showPopupTimer->start();
} }
} else { } else {
m_showPopupTimer->stop(); showPopupTimer->stop();
if (!m_popupPinned) { if (!popupPinned) {
m_hidePopupTimer->start(); hidePopupTimer->start();
} }
} }
} }
} else if (event->type() == QEvent::Leave) { } else if (event->type() == QEvent::Leave) {
m_hoveredUser.clear(); hoveredUser.clear();
m_showPopupTimer->stop(); showPopupTimer->stop();
if (!m_popupPinned) { if (!popupPinned) {
m_hidePopupTimer->start(); hidePopupTimer->start();
} }
} }
} }
@ -829,20 +943,20 @@ void UserListWidget::showPopupForUser(const QString &userName)
const bool isBuddy = userContextMenu->getUserListProxy()->isUserBuddy(userName); const bool isBuddy = userContextMenu->getUserListProxy()->isUserBuddy(userName);
const bool isIgn = userContextMenu->getUserListProxy()->isUserIgnored(userName); const bool isIgn = userContextMenu->getUserListProxy()->isUserIgnored(userName);
m_userInfoPopup->showForUser(userName, info, online, isBuddy, isIgn); userInfoPopup->showForUser(userName, info, online, isBuddy, isIgn);
// Realize the native window at opacity 0 before positioning so that: // Realize the native window at opacity 0 before positioning so that:
// 1) move() applies to an existing native handle (not overridden by Qt's // 1) move() applies to an existing native handle (not overridden by Qt's
// default centering logic on first show) // default centering logic on first show)
// 2) adjustSize() inside positionPopup() can measure the final laid-out // 2) adjustSize() inside positionPopup() can measure the final laid-out
// geometry correctly // geometry correctly
m_userInfoPopup->setWindowOpacity(0.0); userInfoPopup->setWindowOpacity(0.0);
m_userInfoPopup->show(); userInfoPopup->show();
m_userInfoPopup->raise(); userInfoPopup->raise();
positionPopup(userName); // geometry is now accurate; move() sticks positionPopup(userName); // geometry is now accurate; move() sticks
auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup); auto *fade = new QPropertyAnimation(userInfoPopup, "windowOpacity", userInfoPopup);
fade->setDuration(120); fade->setDuration(120);
fade->setStartValue(0.0); fade->setStartValue(0.0);
fade->setEndValue(1.0); fade->setEndValue(1.0);
@ -862,9 +976,9 @@ void UserListWidget::positionPopup(const QString &userName)
const QPoint vpTL = vp->mapToGlobal(vp->rect().topLeft()); const QPoint vpTL = vp->mapToGlobal(vp->rect().topLeft());
const QPoint vpTR = vp->mapToGlobal(vp->rect().topRight()); const QPoint vpTR = vp->mapToGlobal(vp->rect().topRight());
m_userInfoPopup->adjustSize(); userInfoPopup->adjustSize();
const int popW = m_userInfoPopup->width(); const int popW = userInfoPopup->width();
const int popH = m_userInfoPopup->height(); const int popH = userInfoPopup->height();
const int margin = 12; const int margin = 12;
QScreen *activeScreen = QGuiApplication::screenAt(itemTL); QScreen *activeScreen = QGuiApplication::screenAt(itemTL);
@ -901,28 +1015,28 @@ void UserListWidget::positionPopup(const QString &userName)
} }
y = qBound(screen.top() + margin, y, screen.bottom() - popH - margin); y = qBound(screen.top() + margin, y, screen.bottom() - popH - margin);
m_userInfoPopup->move(x, y); userInfoPopup->move(x, y);
} }
void UserListWidget::hidePopup(bool immediate) void UserListWidget::hidePopup(bool immediate)
{ {
m_showPopupTimer->stop(); showPopupTimer->stop();
m_hidePopupTimer->stop(); hidePopupTimer->stop();
if (!m_userInfoPopup->isVisible()) { if (!userInfoPopup->isVisible()) {
return; return;
} }
if (immediate) { if (immediate) {
m_userInfoPopup->hide(); userInfoPopup->hide();
return; return;
} }
// Fade out // Fade out
auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup); auto *fade = new QPropertyAnimation(userInfoPopup, "windowOpacity", userInfoPopup);
fade->setDuration(100); fade->setDuration(100);
fade->setStartValue(m_userInfoPopup->windowOpacity()); fade->setStartValue(userInfoPopup->windowOpacity());
fade->setEndValue(0.0); fade->setEndValue(0.0);
connect(fade, &QPropertyAnimation::finished, m_userInfoPopup, &QWidget::hide); connect(fade, &QPropertyAnimation::finished, userInfoPopup, &QWidget::hide);
fade->start(QAbstractAnimation::DeleteWhenStopped); fade->start(QAbstractAnimation::DeleteWhenStopped);
} }
@ -948,12 +1062,12 @@ void UserListWidget::retranslateUi()
void UserListWidget::beginBulkLoad() void UserListWidget::beginBulkLoad()
{ {
m_bulkLoading = true; bulkLoading = true;
} }
void UserListWidget::endBulkLoad() void UserListWidget::endBulkLoad()
{ {
m_bulkLoading = false; bulkLoading = false;
sortItems(); sortItems();
requestAvatarsForVisibleItems(); requestAvatarsForVisibleItems();
userTree->viewport()->update(); userTree->viewport()->update();
@ -1095,12 +1209,12 @@ void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)
++onlineCount; ++onlineCount;
} }
updateCount(); updateCount();
if (!m_bulkLoading && isItemNearViewport(item)) { if (!bulkLoading && isItemNearViewport(item)) {
avatarProvider->requestAvatar(userName); avatarProvider->requestAvatar(userName);
} }
} }
item->setOnline(online); item->setOnline(online);
if (!m_bulkLoading) { if (!bulkLoading) {
sortItems(); sortItems();
applyFilter(); applyFilter();
userTree->viewport()->update(); userTree->viewport()->update();
@ -1132,12 +1246,12 @@ void UserListWidget::processUserInfo(const QString &sectionId, const ServerInfo_
++onlineCount; ++onlineCount;
} }
updateCount(); updateCount();
if (!m_bulkLoading && isItemNearViewport(item)) { if (!bulkLoading && isItemNearViewport(item)) {
avatarProvider->requestAvatar(userName); avatarProvider->requestAvatar(userName);
} }
} }
item->setOnline(online); item->setOnline(online);
if (!m_bulkLoading) { if (!bulkLoading) {
sortItems(); sortItems();
applyFilter(); applyFilter();
userTree->viewport()->update(); userTree->viewport()->update();
@ -1363,9 +1477,6 @@ QTreeWidgetItem *UserListWidget::createSectionItem(const QString &sectionId)
// A little taller than a plain text row so the header reads as a section // A little taller than a plain text row so the header reads as a section
// separator without matching the full user-row height. // separator without matching the full user-row height.
divider->setSizeHint(0, QSize(0, QFontMetrics(font).height() + 16)); divider->setSizeHint(0, QSize(0, QFontMetrics(font).height() + 16));
QColor muted = palette().color(QPalette::WindowText);
muted.setAlpha(170);
divider->setForeground(0, QBrush(muted));
userTree->addTopLevelItem(divider); userTree->addTopLevelItem(divider);
// QTreeWidgetItem::setFirstColumnSpanned() is a no-op while the item is // QTreeWidgetItem::setFirstColumnSpanned() is a no-op while the item is
@ -1534,7 +1645,7 @@ void UserListWidget::moveToSection(const QString &sectionId, const QString &user
return; return;
} }
target->addChild(item); // reparents: the item leaves its old section target->addChild(item); // reparents: the item leaves its old section
if (!m_bulkLoading) { if (!bulkLoading) {
sortItems(); sortItems();
applyFilter(); applyFilter();
userTree->viewport()->update(); userTree->viewport()->update();

View file

@ -105,12 +105,13 @@ public:
class UserListItemDelegate : public QStyledItemDelegate class UserListItemDelegate : public QStyledItemDelegate
{ {
QTreeWidget *tree;
const QMap<QString, QPixmap> *avatarCache; const QMap<QString, QPixmap> *avatarCache;
const QMap<QString, QPixmap> *cardArtCache; const QMap<QString, QPixmap> *cardArtCache;
const QMap<QString, CardArtParams> *cardArtParamsMap; const QMap<QString, CardArtParams> *cardArtParamsMap;
public: public:
explicit UserListItemDelegate(QObject *const parent, explicit UserListItemDelegate(QTreeWidget *tree,
const QMap<QString, QPixmap> *avatarCache, const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache, const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap); const QMap<QString, CardArtParams> *cardArtParamsMap);
@ -156,12 +157,12 @@ private:
UserCardArtProvider *cardArtProvider = nullptr; UserCardArtProvider *cardArtProvider = nullptr;
QMap<QString, CardArtParams> cardArtParamsMap; QMap<QString, CardArtParams> cardArtParamsMap;
// ── Hover popup ─────────────────────────────────────────────────────────── // ── Hover popup ───────────────────────────────────────────────────────────
UserInfoPopup *m_userInfoPopup = nullptr; UserInfoPopup *userInfoPopup = nullptr;
QTimer *m_showPopupTimer = nullptr; QTimer *showPopupTimer = nullptr;
QTimer *m_hidePopupTimer = nullptr; QTimer *hidePopupTimer = nullptr;
QString m_hoveredUser; QString hoveredUser;
bool m_popupPinned = false; bool popupPinned = false;
bool m_bulkLoading = false; bool bulkLoading = false;
void showPopupForUser(const QString &userName); void showPopupForUser(const QString &userName);
void hidePopup(bool immediate = false); void hidePopup(bool immediate = false);
@ -193,7 +194,7 @@ private:
TabSupervisor *tabSupervisor; TabSupervisor *tabSupervisor;
AbstractClient *client; AbstractClient *client;
UserListType type; UserListType type;
QTreeWidget *userTree; QTreeWidget *userTree = nullptr;
UserListItemDelegate *itemDelegate; UserListItemDelegate *itemDelegate;
UserContextMenu *userContextMenu; UserContextMenu *userContextMenu;
int onlineCount; int onlineCount;