mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-28 08:52:19 -07:00
[UserList] Add a light mode theme
Took 12 minutes Took 11 seconds Took 5 minutes Took 2 minutes
This commit is contained in:
parent
46d3dba6b4
commit
941b1e5c25
8 changed files with 659 additions and 341 deletions
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ¶ms)
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 ¶ms);
|
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
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 §ionId, 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 §ionId)
|
||||||
// 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 §ionId, 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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue