[Room][UserList] Introduce style delegate for user list

- Allow users to set a card name and parameters as their background banner
- Allow mods to white/blacklist cards
- Allow toggling back to the old display style

Took 7 minutes

Took 28 seconds

Took 2 minutes

Took 2 minutes
This commit is contained in:
Lukas Brübach 2026-06-07 10:13:07 +02:00
parent bdb0f12f66
commit aff93a4435
35 changed files with 1977 additions and 26 deletions

View file

@ -0,0 +1,48 @@
#include "user_avatar_provider.h"
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/response_get_user_info.pb.h>
#include <libcockatrice/protocol/pending_command.h>
UserAvatarProvider::UserAvatarProvider(AbstractClient *client, QObject *parent) : QObject(parent), client(client)
{
}
const QMap<QString, QPixmap> &UserAvatarProvider::cache() const
{
return avatarCache;
}
void UserAvatarProvider::requestAvatar(const QString &userName)
{
if (avatarCache.contains(userName) || pending.contains(userName)) {
return;
}
pending.insert(userName);
Command_GetUserInfo cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, [this, userName](const Response &r) {
pending.remove(userName);
const auto &response = r.GetExtension(Response_GetUserInfo::ext);
const auto &user = response.user_info();
const std::string &bmp = user.avatar_bmp();
QPixmap avatar;
if (!bmp.empty() &&
avatar.loadFromData(reinterpret_cast<const uchar *>(bmp.data()), static_cast<uint>(bmp.size()))) {
avatarCache.insert(userName, avatar);
} else {
avatarCache.insert(userName, QPixmap());
}
emit avatarUpdated(userName);
});
client->sendCommand(pend);
}

View file

@ -0,0 +1,30 @@
#ifndef COCKATRICE_USER_AVATAR_PROVIDER_H
#define COCKATRICE_USER_AVATAR_PROVIDER_H
#include <QMap>
#include <QObject>
#include <QPixmap>
#include <QSet>
class AbstractClient;
class UserAvatarProvider : public QObject
{
Q_OBJECT
public:
explicit UserAvatarProvider(AbstractClient *client, QObject *parent = nullptr);
void requestAvatar(const QString &userName);
const QMap<QString, QPixmap> &cache() const;
signals:
void avatarUpdated(const QString &userName);
private:
AbstractClient *client;
QMap<QString, QPixmap> avatarCache;
QSet<QString> pending;
};
#endif // COCKATRICE_USER_AVATAR_PROVIDER_H

View file

@ -0,0 +1,143 @@
#include "user_card_art_provider.h"
#include "../../../card_picture_loader/card_picture_loader.h"
#include <QPointer>
#include <libcockatrice/card/database/card_database_manager.h>
static QString makeKey(const QString &user, const QString &card)
{
return user + u'|' + card;
}
UserCardArtProvider::UserCardArtProvider(QObject *parent) : QObject(parent)
{
dbReady = (CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok);
if (!dbReady) {
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
&UserCardArtProvider::onDatabaseReady);
}
}
void UserCardArtProvider::onDatabaseReady()
{
dbReady = true;
processQueue();
}
const QMap<QString, QPixmap> &UserCardArtProvider::cache() const
{
return cardArtCache;
}
void UserCardArtProvider::requestCardArt(const QString &userName, const QString &cardName)
{
if (cardName.isEmpty()) {
return;
}
const QString key = makeKey(userName, cardName);
if (cardArtCache.contains(key) || pending.contains(key)) {
return;
}
pending.insert(key);
queue.enqueue(key);
processQueue();
}
QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes)
{
const QSize sz = fullRes.size();
const int marginX = sz.width() * 0.07;
const int topMargin = sz.height() * 0.11;
const int bottomMargin = sz.height() * 0.45;
const QRect foilRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin);
return fullRes.copy(foilRect.intersected(fullRes.rect()));
}
void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap)
{
if (!cardArtCache.contains(key)) {
cacheInsertionOrder.append(key);
while (cacheInsertionOrder.size() > MaxCacheEntries) {
const QString evicted = cacheInsertionOrder.takeFirst();
cardArtCache.remove(evicted);
}
}
cardArtCache.insert(key, pixmap);
}
void UserCardArtProvider::processQueue()
{
if (!dbReady) {
return;
}
if (queue.isEmpty()) {
return;
}
const QString key = queue.dequeue();
const QStringList parts = key.split(u'|');
if (parts.size() != 2) {
pending.remove(key);
processQueue();
return;
}
const QString userName = parts.at(0);
const QString cardName = parts.at(1);
ExactCard card = CardDatabaseManager::query()->getCard({cardName});
if (!card) {
pending.remove(key);
processQueue();
return;
}
QPixmap fullRes;
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
// If already available, store immediately
if (!fullRes.isNull()) {
insertIntoCache(key, cropCardArt(fullRes));
pending.remove(key);
emit cardArtUpdated(userName);
processQueue();
return;
}
// Otherwise wait for async load
QPointer<UserCardArtProvider> self(this);
connect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [self, key, userName, card]() mutable {
if (!self) {
return;
}
disconnect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, self, nullptr);
QPixmap fullRes;
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
if (!fullRes.isNull()) {
self->insertIntoCache(key, self->cropCardArt(fullRes));
} else {
self->insertIntoCache(key, QPixmap());
}
self->pending.remove(key);
emit self->cardArtUpdated(userName);
self->processQueue();
});
}

View file

@ -0,0 +1,39 @@
#ifndef COCKATRICE_USER_CARD_ART_PROVIDER_H
#define COCKATRICE_USER_CARD_ART_PROVIDER_H
#include <QMap>
#include <QObject>
#include <QPixmap>
#include <QQueue>
#include <QSet>
class UserCardArtProvider : public QObject
{
Q_OBJECT
public:
explicit UserCardArtProvider(QObject *parent = nullptr);
void requestCardArt(const QString &userName, const QString &cardName);
const QMap<QString, QPixmap> &cache() const;
static QPixmap cropCardArt(const QPixmap &fullRes);
signals:
void cardArtUpdated(const QString &userName);
public slots:
void onDatabaseReady();
private:
bool dbReady = false;
static constexpr int MaxCacheEntries = 300;
QList<QString> cacheInsertionOrder; // FIFO eviction
QMap<QString, QPixmap> cardArtCache;
QSet<QString> pending;
QQueue<QString> queue;
void processQueue();
void insertIntoCache(const QString &key, const QPixmap &pixmap);
};
#endif // COCKATRICE_USER_CARD_ART_PROVIDER_H

View file

@ -0,0 +1,264 @@
#include "user_card_settings_dialog.h"
#include "../../../card_picture_loader/card_picture_loader.h"
#include "card/card_completer_proxy_model.h"
#include "card/card_search_model.h"
#include "card_database_display_model.h"
#include "card_database_model.h"
#include "user_card_art_provider.h"
#include "user_list_painter.h"
#include <QCompleter>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPainter>
#include <QPainterPath>
#include <QPushButton>
#include <QRegularExpression>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
CardArtPreviewWidget::CardArtPreviewWidget(QWidget *parent) : QWidget(parent)
{
setMinimumSize(400, 72);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
}
void CardArtPreviewWidget::setPixmap(const QPixmap &pixmap)
{
sourcePixmap = pixmap;
update();
}
void CardArtPreviewWidget::setParams(const CardArtParams &p)
{
params = p;
update();
}
void CardArtPreviewWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
const QRect rect = this->rect();
const QColor accentColor(100, 116, 139);
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
QLinearGradient bg(cardRect.topLeft(), cardRect.topRight());
bg.setColorAt(0, accentColor.darker(320));
bg.setColorAt(1, QColor(18, 22, 30));
painter.setPen(Qt::NoPen);
painter.setBrush(bg);
painter.drawRoundedRect(cardRect, 6, 6);
painter.setBrush(accentColor);
painter.drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
if (sourcePixmap.isNull()) {
painter.setPen(QColor(150, 150, 150));
painter.drawText(rect, Qt::AlignCenter, tr("No card selected"));
return;
}
UserListPainter::drawCardArt(&painter, rect, rect.right() - 4,
QString(), // userName not needed for override path
nullptr, // no cache
params,
&sourcePixmap // 👈 direct pixmap
);
// Avatar placeholder so the left-margin interaction is visible
const int avatarX = rect.left() + 14;
const int avatarY = rect.top() + (rect.height() - 36) / 2;
const QRect avatarRect(avatarX, avatarY, 36, 36);
QPainterPath clip;
clip.addEllipse(avatarRect);
painter.save();
painter.setClipPath(clip);
painter.setBrush(accentColor.darker(200));
painter.setPen(Qt::NoPen);
painter.drawEllipse(avatarRect);
painter.restore();
painter.setPen(QPen(QColor(70, 80, 95), 2));
painter.setBrush(Qt::NoBrush);
painter.drawEllipse(avatarRect.adjusted(-1, -1, 1, 1));
}
UserCardArtSettingsDialog::UserCardArtSettingsDialog(const CardArtParams &initial, QWidget *parent)
: QDialog(parent), currentParams(initial)
{
setWindowTitle(tr("Card Art Settings"));
setMinimumWidth(500);
setupUi();
// Seed UI from initial params
if (!initial.cardName.isEmpty()) {
searchBar->setText(initial.cardName);
onCardNameChanged(initial.cardName);
}
marginLSpin->setValue(initial.marginPctL);
marginRSpin->setValue(initial.marginPctR);
verticalOffsetSpin->setValue(initial.verticalOffset);
zoomSpin->setValue(initial.zoom);
}
CardArtParams UserCardArtSettingsDialog::params() const
{
return currentParams;
}
QDoubleSpinBox *UserCardArtSettingsDialog::makeSpinBox(double min, double max, double value, double step)
{
auto *spin = new QDoubleSpinBox;
spin->setRange(min, max);
spin->setSingleStep(step);
spin->setDecimals(3);
spin->setValue(value);
return spin;
}
void UserCardArtSettingsDialog::initializeSearchBar()
{
searchBar = new QLineEdit;
searchBar->setPlaceholderText(tr("Type a card name..."));
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this);
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
searchModel = new CardSearchModel(cardDatabaseDisplayModel, this);
proxyModel = new CardCompleterProxyModel(this);
proxyModel->setSourceModel(searchModel);
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
proxyModel->setFilterRole(Qt::DisplayRole);
completer = new QCompleter(proxyModel, this);
completer->setCompletionRole(Qt::DisplayRole);
completer->setCompletionMode(QCompleter::PopupCompletion);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setFilterMode(Qt::MatchContains);
completer->setMaxVisibleItems(15);
searchBar->setCompleter(completer);
connect(searchBar, &QLineEdit::textEdited, searchModel, &CardSearchModel::updateSearchResults);
connect(searchBar, &QLineEdit::textEdited, this, [this](const QString &text) {
const QString pattern = ".*" + QRegularExpression::escape(text) + ".*";
proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
if (!text.isEmpty()) {
completer->complete();
}
});
connect(completer, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
[this](const QString &completion) {
if (searchBar->text() != completion) {
searchBar->setText(completion);
searchBar->setCursorPosition(searchBar->text().length());
}
onCardNameChanged(completion);
});
// Also trigger a load when the user hits Return on a typed name
connect(searchBar, &QLineEdit::returnPressed, this, [this]() { onCardNameChanged(searchBar->text()); });
}
void UserCardArtSettingsDialog::setupUi()
{
initializeSearchBar();
marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01);
marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01);
verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01);
zoomSpin = makeSpinBox(0.1, 4.0, currentParams.zoom, 0.05);
auto *form = new QFormLayout;
form->addRow(tr("Card name:"), searchBar);
form->addRow(tr("Left margin (%):"), marginLSpin);
form->addRow(tr("Right margin (%):"), marginRSpin);
form->addRow(tr("Vertical offset:"), verticalOffsetSpin);
form->addRow(tr("Zoom:"), zoomSpin);
auto *controlsGroup = new QGroupBox(tr("Parameters"));
controlsGroup->setLayout(form);
preview = new CardArtPreviewWidget;
auto *previewLayout = new QVBoxLayout;
previewLayout->addWidget(preview);
auto *previewGroup = new QGroupBox(tr("Preview"));
previewGroup->setLayout(previewLayout);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
auto *removeBtn = new QPushButton(tr("Remove Banner Card"));
buttons->addButton(removeBtn, QDialogButtonBox::ResetRole);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(removeBtn, &QPushButton::clicked, this, [this]() {
currentParams = CardArtParams{}; // empty cardName signals removal
accept();
});
auto *root = new QVBoxLayout;
root->addWidget(controlsGroup);
root->addWidget(previewGroup);
root->addWidget(buttons);
setLayout(root);
connect(marginLSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
connect(marginRSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
connect(verticalOffsetSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
}
void UserCardArtSettingsDialog::onCardNameChanged(const QString &name)
{
if (name.isEmpty()) {
currentPixmap = QPixmap();
preview->setPixmap(currentPixmap);
return;
}
const ExactCard card = CardDatabaseManager::query()->getCard({name});
if (!card) {
currentPixmap = QPixmap();
preview->setPixmap(currentPixmap);
return;
}
currentParams.cardName = name;
QPixmap fullRes;
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
if (fullRes.isNull()) {
connect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this, card](const PrintingInfo &) {
disconnect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, nullptr);
QPixmap loaded;
CardPictureLoader::getPixmap(loaded, card, QSize(745, 1040));
currentPixmap = UserCardArtProvider::cropCardArt(loaded);
preview->setPixmap(currentPixmap);
});
return;
}
currentPixmap = UserCardArtProvider::cropCardArt(fullRes);
preview->setPixmap(currentPixmap);
}
void UserCardArtSettingsDialog::onParamChanged()
{
currentParams.marginPctL = marginLSpin->value();
currentParams.marginPctR = marginRSpin->value();
currentParams.verticalOffset = verticalOffsetSpin->value();
currentParams.zoom = zoomSpin->value();
preview->setParams(currentParams);
}

View file

@ -0,0 +1,70 @@
#ifndef COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H
#define COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H
#include "user_list_painter.h"
#include <QDialog>
#include <QPixmap>
class QCompleter;
class QLineEdit;
class QDoubleSpinBox;
class CardDatabaseModel;
class CardDatabaseDisplayModel;
class CardSearchModel;
class CardCompleterProxyModel;
class CardArtPreviewWidget : public QWidget
{
Q_OBJECT
public:
explicit CardArtPreviewWidget(QWidget *parent = nullptr);
void setPixmap(const QPixmap &pixmap);
void setParams(const CardArtParams &params);
protected:
void paintEvent(QPaintEvent *event) override;
private:
QPixmap sourcePixmap;
CardArtParams params;
};
class UserCardArtSettingsDialog : public QDialog
{
Q_OBJECT
public:
explicit UserCardArtSettingsDialog(const CardArtParams &initial = {}, QWidget *parent = nullptr);
CardArtParams params() const;
private slots:
void onCardNameChanged(const QString &name);
void onParamChanged();
private:
void setupUi();
void initializeSearchBar();
QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step);
QLineEdit *searchBar;
QCompleter *completer;
CardDatabaseModel *cardDatabaseModel;
CardDatabaseDisplayModel *cardDatabaseDisplayModel;
CardSearchModel *searchModel;
CardCompleterProxyModel *proxyModel;
QDoubleSpinBox *marginLSpin;
QDoubleSpinBox *marginRSpin;
QDoubleSpinBox *verticalOffsetSpin;
QDoubleSpinBox *zoomSpin;
CardArtPreviewWidget *preview;
QPixmap currentPixmap;
CardArtParams currentParams;
};
#endif // COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H

View file

@ -5,6 +5,7 @@
#include "../../interface/widgets/dialogs/dlg_edit_password.h"
#include "../../interface/widgets/dialogs/dlg_edit_user.h"
#include "../../interface/widgets/utility/get_text_with_max.h"
#include "user_card_settings_dialog.h"
#include <QDateTime>
#include <QGridLayout>
@ -61,11 +62,13 @@ UserInfoBox::UserInfoBox(AbstractClient *_client, bool _editable, QWidget *paren
buttonsLayout->addWidget(&editButton);
buttonsLayout->addWidget(&passwordButton);
buttonsLayout->addWidget(&avatarButton);
buttonsLayout->addWidget(&bannerCardButton);
mainLayout->addLayout(buttonsLayout, 7, 0, 1, 3);
connect(&editButton, &QPushButton::clicked, this, &UserInfoBox::actEdit);
connect(&passwordButton, &QPushButton::clicked, this, &UserInfoBox::actPassword);
connect(&avatarButton, &QPushButton::clicked, this, &UserInfoBox::actAvatar);
connect(&bannerCardButton, &QPushButton::clicked, this, &UserInfoBox::actBannerCard);
}
setWindowTitle(tr("User Information"));
@ -83,11 +86,15 @@ void UserInfoBox::retranslateUi()
editButton.setText(tr("Edit"));
passwordButton.setText(tr("Change password"));
avatarButton.setText(tr("Change avatar"));
bannerCardButton.setText(tr("Edit Banner Card"));
}
void UserInfoBox::updateInfo(const ServerInfo_User &user)
{
userLevel = UserLevelFlags(user.user_level());
currentUserInfo = user;
hasUserInfo = true;
const UserLevelFlags userLevel(user.user_level());
pawnColors = user.pawn_colors();
privLevel = QString::fromStdString(user.privlevel());
@ -306,6 +313,48 @@ void UserInfoBox::actAvatar()
client->sendCommand(pend);
}
void UserInfoBox::actBannerCard()
{
CardArtParams initial;
if (hasUserInfo && currentUserInfo.has_card_art_params()) {
const auto &cap = currentUserInfo.card_art_params();
initial.cardName = QString::fromStdString(cap.card_name());
initial.marginPctL = cap.margin_pct_l();
initial.marginPctR = cap.margin_pct_r();
initial.verticalOffset = cap.vertical_offset();
initial.zoom = cap.zoom();
}
UserCardArtSettingsDialog dlg(initial, this);
if (dlg.exec() != QDialog::Accepted) {
return;
}
const CardArtParams p = dlg.params();
Command_SetCardArtParams cmd;
cmd.set_card_name(p.cardName.toStdString());
if (!p.cardName.isEmpty()) {
cmd.set_margin_pct_l(p.marginPctL);
cmd.set_margin_pct_r(p.marginPctR);
cmd.set_vertical_offset(p.verticalOffset);
cmd.set_zoom(p.zoom);
}
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, [p, this](const Response &r) {
if (r.response_code() != Response::RespOk) {
QMessageBox::critical(this, tr("Error"),
tr("The selected card is blacklisted on this server or another error occurred."));
} else {
updateInfo(nameLabel.text()); // re-fetch so currentUserInfo reflects the change
QMessageBox::information(this, tr("Information"),
p.cardName.isEmpty() ? tr("Banner card removed.") : tr("Banner card updated."));
}
});
client->sendCommand(pend);
}
void UserInfoBox::processEditResponse(const Response &r)
{
switch (r.response_code()) {

View file

@ -11,6 +11,7 @@
#include <QLabel>
#include <QPushButton>
#include <QWidget>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/network/server/remote/user_level.h>
#include <libcockatrice/utility/days_years_between.h>
@ -25,9 +26,11 @@ private:
bool editable;
QLabel avatarPic, userLevelIcon, nameLabel, realNameLabel1, realNameLabel2, countryLabel1, countryLabel2,
countryLabel3, userLevelLabel1, userLevelLabel2, accountAgeLabel1, accountAgeLabel2;
QPushButton editButton, passwordButton, avatarButton;
QPushButton editButton, passwordButton, avatarButton, bannerCardButton;
QPixmap avatarPixmap;
bool hasAvatar;
ServerInfo_User currentUserInfo;
bool hasUserInfo = false;
UserLevelFlags userLevel;
ServerInfo_User::PawnColorsOverride pawnColors;
QString privLevel;
@ -47,6 +50,7 @@ private slots:
void actEditInternal(const Response &r);
void actPassword();
void actAvatar();
void actBannerCard();
public slots:
void updateInfo(const ServerInfo_User &user);
void updateInfo(const QString &userName);

View file

@ -0,0 +1,342 @@
#include "user_list_painter.h"
#include "../../interface/pixel_map_generator.h"
#include <QAbstractScrollArea>
#include <QPainter>
#include <QPainterPath>
#include <QScrollBar>
#include <QStyle>
#include <QStyleOptionViewItem>
static constexpr int RowHeight = 72;
static constexpr int AvatarSize = 36;
static constexpr int LeftPadding = 14;
static constexpr int TextSpacing = 10;
QSize UserListPainter::sizeHint()
{
return QSize(0, RowHeight);
}
QColor UserListPainter::getAccentColor(const UserLevelFlags &userLevel, bool online)
{
QColor accentColor;
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
accentColor = QColor(245, 158, 11);
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
accentColor = QColor(59, 130, 246);
} else if (userLevel.testFlag(ServerInfo_User::IsJudge)) {
accentColor = QColor(168, 85, 247);
} else {
accentColor = QColor(100, 116, 139);
}
if (!online) {
accentColor = accentColor.darker(160);
}
return accentColor;
}
int UserListPainter::getCardRight(const QStyleOptionViewItem &option, const QRect &rect)
{
int scrollBarWidth = 0;
if (const auto *scrollArea = qobject_cast<const QAbstractScrollArea *>(option.widget)) {
const QScrollBar *sb = scrollArea->verticalScrollBar();
if (sb && sb->isVisible()) {
scrollBarWidth = sb->width();
}
}
const int viewportRight = option.widget ? option.widget->width() - scrollBarWidth : rect.right();
return qMin(rect.right(), viewportRight - 4);
}
void UserListPainter::drawBackground(QPainter *painter,
const QRectF &cardRect,
const QColor &accentColor,
bool selected)
{
QLinearGradient bg(cardRect.topLeft(), cardRect.topRight());
bg.setColorAt(0, selected ? accentColor.darker(130) : accentColor.darker(320));
bg.setColorAt(1, selected ? QColor(40, 48, 60) : QColor(18, 22, 30));
painter->setPen(Qt::NoPen);
painter->setBrush(bg);
painter->drawRoundedRect(cardRect, 6, 6);
painter->setBrush(accentColor);
painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
}
static QString makeKey(const QString &user, const QString &card)
{
return user + u'|' + card;
}
void UserListPainter::drawCardArt(QPainter *painter,
const QRect &rect,
int cardRight,
const QString &userName,
const QMap<QString, QPixmap> *cardArtCache,
const CardArtParams &params,
const QPixmap *overridePixmap = nullptr)
{
QPixmap art;
if (overridePixmap && !overridePixmap->isNull()) {
art = *overridePixmap;
} else {
if (!cardArtCache) {
return;
}
const QString key = makeKey(userName, params.cardName);
if (!cardArtCache->contains(key)) {
return;
}
art = cardArtCache->value(key);
}
if (art.isNull()) {
return;
}
const int cardH = rect.height() - 4;
const int totalW = cardRight - rect.left();
const int marginL = qRound(totalW * params.marginPctL);
const int marginR = qRound(totalW * params.marginPctR);
const int drawW = totalW - marginL - marginR;
const double basescale = qMax(double(drawW) / art.width(), double(cardH) / art.height());
const double scale = basescale * params.zoom;
const int scaledW = qRound(art.width() * scale);
const int scaledH = qRound(art.height() * scale);
const QPixmap scaled = art.scaled(scaledW, scaledH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
const int srcX = (scaledW - drawW) / 2;
const int srcY = qRound((scaledH - cardH) * params.verticalOffset);
// Clamp srcY so we never copy outside the pixmap bounds
const int safeSrcY = qBound(0, srcY, qMax(0, scaledH - cardH));
QImage img =
scaled.copy(srcX, safeSrcY, drawW, cardH).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
{
QPainter mask(&img);
mask.setCompositionMode(QPainter::CompositionMode_DestinationIn);
QLinearGradient grad(0, 0, img.width(), 0);
grad.setColorAt(0.00, Qt::transparent);
grad.setColorAt(0.22, Qt::white);
grad.setColorAt(0.78, Qt::white);
grad.setColorAt(1.00, Qt::transparent);
mask.fillRect(img.rect(), grad);
}
painter->setOpacity(0.55);
painter->drawImage(rect.left() + marginL, rect.top() + 2, img);
painter->setOpacity(1.0);
}
QRect UserListPainter::getAvatarRect(const QRect &rect)
{
const int avatarX = rect.left() + LeftPadding;
const int avatarY = rect.top() + (rect.height() - AvatarSize) / 2;
return QRect(avatarX, avatarY, AvatarSize, AvatarSize);
}
void UserListPainter::drawAvatar(QPainter *painter,
const QRect &avatarRect,
const QString &userName,
const QColor &accentColor,
const UserLevelFlags &userLevel,
const ServerInfo_User &userInfo,
const QString &privLevel,
const QMap<QString, QPixmap> *avatarCache)
{
QPainterPath clipPath;
clipPath.addEllipse(avatarRect);
painter->save();
painter->setClipPath(clipPath);
bool drewAvatar = false;
if (avatarCache && avatarCache->contains(userName)) {
const QPixmap &avatar = avatarCache->value(userName);
if (!avatar.isNull()) {
painter->drawPixmap(
avatarRect, avatar.scaled(avatarRect.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
drewAvatar = true;
}
}
if (!drewAvatar) {
painter->setBrush(accentColor.darker(200));
painter->setPen(Qt::NoPen);
painter->drawEllipse(avatarRect);
const QPixmap pawn =
UserLevelPixmapGenerator::generatePixmap(24, userLevel, userInfo.pawn_colors(), false, privLevel);
painter->drawPixmap(avatarRect.center().x() - 12, avatarRect.center().y() - 12, pawn);
}
painter->restore();
}
void UserListPainter::drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online)
{
const QColor statusColor = online ? QColor(34, 197, 94) : QColor(70, 80, 95);
painter->setPen(QPen(statusColor, 2));
painter->setBrush(Qt::NoBrush);
painter->drawEllipse(avatarRect.adjusted(-1, -1, 1, 1));
}
void UserListPainter::drawUserName(QPainter *painter,
const QStyleOptionViewItem &option,
const QRect &rect,
int cardRight,
int textX,
const QString &userName,
bool online,
bool selected)
{
QFont nameFont = option.font;
nameFont.setBold(true);
painter->setFont(nameFont);
const QRect nameRect(textX, rect.top() + 8, cardRight - textX - 10, 20);
const QString elidedName = QFontMetrics(nameFont).elidedText(userName, Qt::ElideRight, cardRight - textX - 10);
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->drawText(nameRect, Qt::AlignVCenter | Qt::AlignLeft, elidedName);
}
void UserListPainter::drawCountryFlag(QPainter *painter, const QRect &rect, int textX, const ServerInfo_User &userInfo)
{
const QPixmap flag = CountryPixmapGenerator::generatePixmap(13, QString::fromStdString(userInfo.country()));
if (!flag.isNull()) {
painter->drawPixmap(textX, rect.top() + 46, flag);
}
}
QList<UserListPainter::Badge> UserListPainter::buildBadges(const UserLevelFlags &userLevel, const QString &privLevel)
{
QList<Badge> badges;
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
badges << Badge{"ADMIN", QColor(245, 158, 11)};
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
badges << Badge{"MOD", QColor(59, 130, 246)};
} else if (userLevel.testFlag(ServerInfo_User::IsJudge)) {
badges << Badge{"JUDGE", QColor(168, 85, 247)};
}
if (privLevel == "VIP") {
badges << Badge{"VIP", QColor(20, 184, 166)};
} else if (privLevel == "DONATOR") {
badges << Badge{"DONATOR", QColor(249, 115, 22)};
}
return badges;
}
void UserListPainter::drawBadges(QPainter *painter,
const QStyleOptionViewItem &option,
const QRect &rect,
int cardRight,
const QList<Badge> &badges,
bool online)
{
if (badges.isEmpty()) {
return;
}
QFont badgeFont = option.font;
badgeFont.setPointSizeF(badgeFont.pointSizeF() * 0.68);
badgeFont.setBold(true);
painter->setFont(badgeFont);
QFontMetrics fm(badgeFont);
int totalBadgeW = 0;
for (const Badge &b : badges) {
totalBadgeW += fm.horizontalAdvance(b.text) + 8 + 4;
}
totalBadgeW -= 4;
int bx = cardRight - 6 - totalBadgeW;
for (const Badge &b : badges) {
const QColor col = online ? b.color : b.color.darker(180);
const int bw = fm.horizontalAdvance(b.text) + 8;
const QRect br(bx, rect.top() + 44, bw, 13);
painter->setPen(Qt::NoPen);
painter->setBrush(col.darker(online ? 160 : 220));
painter->drawRoundedRect(br, 3, 3);
painter->setPen(col.lighter(online ? 160 : 100));
painter->drawText(br, Qt::AlignCenter, b.text);
bx += bw + 4;
}
}
void UserListPainter::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index,
const ServerInfo_User &userInfo,
const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap)
{
painter->save();
painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
const QRect rect = option.rect;
const bool online = index.data(Qt::UserRole + 1).toBool();
const bool selected = option.state & QStyle::State_Selected;
const UserLevelFlags userLevel(userInfo.user_level());
const QString userName = QString::fromStdString(userInfo.name());
const QString privLevel = QString::fromStdString(userInfo.privlevel());
const QColor accentColor = getAccentColor(userLevel, online);
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
const int cardRight = getCardRight(option, rect);
const CardArtParams params = (cardArtParamsMap && cardArtParamsMap->contains(userName))
? cardArtParamsMap->value(userName)
: CardArtParams{};
drawBackground(painter, cardRect, accentColor, selected);
drawCardArt(painter, rect, cardRight, userName, cardArtCache, params);
const QRect avatarRect = getAvatarRect(rect);
drawAvatar(painter, avatarRect, userName, accentColor, userLevel, userInfo, privLevel, avatarCache);
drawStatusRing(painter, avatarRect, online);
const int textX = avatarRect.right() + TextSpacing;
drawUserName(painter, option, rect, cardRight, textX, userName, online, selected);
drawCountryFlag(painter, rect, textX, userInfo);
const QList<Badge> badges = buildBadges(userLevel, privLevel);
drawBadges(painter, option, rect, cardRight, badges, online);
painter->restore();
}

View file

@ -0,0 +1,86 @@
#ifndef COCKATRICE_USER_LIST_PAINTER_H
#define COCKATRICE_USER_LIST_PAINTER_H
#include "user_level.h"
#include <QColor>
#include <QList>
#include <QMap>
#include <QPixmap>
#include <QRect>
#include <QSize>
class QPainter;
class QModelIndex;
class QStyleOptionViewItem;
class ServerInfo_User;
struct CardArtParams
{
QString cardName = "";
double marginPctL = 0.33;
double marginPctR = 0.02;
double verticalOffset = 0.35;
double zoom = 1.0;
};
class UserListPainter
{
public:
static void paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index,
const ServerInfo_User &userInfo,
const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap);
static QSize sizeHint();
static void drawCardArt(QPainter *painter,
const QRect &rect,
int cardRight,
const QString &userName,
const QMap<QString, QPixmap> *cardArtCache,
const CardArtParams &params,
const QPixmap *overridePixmap);
private:
struct Badge
{
QString text;
QColor color;
};
static QColor getAccentColor(const UserLevelFlags &userLevel, bool online);
static int getCardRight(const QStyleOptionViewItem &option, const QRect &rect);
static void drawBackground(QPainter *painter, const QRectF &cardRect, const QColor &accentColor, bool selected);
static QRect getAvatarRect(const QRect &rect);
static void drawAvatar(QPainter *painter,
const QRect &avatarRect,
const QString &userName,
const QColor &accentColor,
const UserLevelFlags &userLevel,
const ServerInfo_User &userInfo,
const QString &privLevel,
const QMap<QString, QPixmap> *avatarCache);
static void drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online);
static void drawUserName(QPainter *painter,
const QStyleOptionViewItem &option,
const QRect &rect,
int cardRight,
int textX,
const QString &userName,
bool online,
bool selected);
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 void drawBadges(QPainter *painter,
const QStyleOptionViewItem &option,
const QRect &rect,
int cardRight,
const QList<Badge> &badges,
bool online);
};
#endif // COCKATRICE_USER_LIST_PAINTER_H

View file

@ -1,10 +1,13 @@
#include "user_list_widget.h"
#include "../../../../client/settings/cache_settings.h"
#include "../../../card_picture_loader/card_picture_loader.h"
#include "../../interface/pixel_map_generator.h"
#include "../../interface/widgets/tabs/tab_account.h"
#include "../../interface/widgets/tabs/tab_supervisor.h"
#include "../game_selector.h"
#include "user_context_menu.h"
#include "user_list_painter.h"
#include <QApplication>
#include <QCheckBox>
@ -15,13 +18,18 @@
#include <QLineEdit>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QRadioButton>
#include <QSpinBox>
#include <QWidget>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
#include <libcockatrice/protocol/pb/response_get_user_info.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h>
BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent)
@ -308,7 +316,18 @@ QString AdminNotesDialog::getNotes() const
return notes->toPlainText();
}
UserListItemDelegate::UserListItemDelegate(QObject *const parent) : QStyledItemDelegate(parent)
namespace UserListRoles
{
constexpr int Online = Qt::UserRole + 1;
constexpr int UserInfo = Qt::UserRole + 2;
} // namespace UserListRoles
UserListItemDelegate::UserListItemDelegate(QObject *const parent,
const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap)
: QStyledItemDelegate(parent), avatarCache(avatarCache), cardArtCache(cardArtCache),
cardArtParamsMap(cardArtParamsMap)
{
}
@ -331,6 +350,32 @@ bool UserListItemDelegate::editorEvent(QEvent *event,
return QStyledItemDelegate::editorEvent(event, model, option, index);
}
QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (!SettingsCache::instance().getStyleUserList()) {
return QStyledItemDelegate::sizeHint(option, index);
}
return UserListPainter::sizeHint();
}
void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (!SettingsCache::instance().getStyleUserList()) {
QStyledItemDelegate::paint(painter, option, index);
return;
}
const QVariant var = index.data(UserListRoles::UserInfo);
if (!var.isValid()) {
QStyledItemDelegate::paint(painter, option, index);
return;
}
UserListPainter::paint(painter, option, index, var.value<ServerInfo_User>(), avatarCache, cardArtCache,
cardArtParamsMap);
}
UserListTWI::UserListTWI(const ServerInfo_User &_userInfo) : QTreeWidgetItem(Type)
{
setUserInfo(_userInfo);
@ -347,11 +392,12 @@ void UserListTWI::setUserInfo(const ServerInfo_User &_userInfo)
setData(2, Qt::UserRole, QString::fromStdString(userInfo.name()));
setData(2, Qt::DisplayRole, QString::fromStdString(userInfo.name()));
setData(3, Qt::InitialSortOrderRole, QString::fromStdString(userInfo.privlevel()));
setData(0, UserListRoles::UserInfo, QVariant::fromValue(userInfo));
}
void UserListTWI::setOnline(bool online)
{
setData(0, Qt::UserRole + 1, online);
setData(0, UserListRoles::Online, online);
setData(2, Qt::ForegroundRole, online ? qApp->palette().brush(QPalette::WindowText) : QBrush(Qt::gray));
}
@ -370,8 +416,8 @@ void UserListTWI::setOnline(bool online)
bool UserListTWI::operator<(const QTreeWidgetItem &other) const
{
// Sort by online/offline
if (data(0, Qt::UserRole + 1) != other.data(0, Qt::UserRole + 1)) {
return data(0, Qt::UserRole + 1).toBool();
if (data(0, UserListRoles::Online) != other.data(0, UserListRoles::Online)) {
return data(0, UserListRoles::Online).toBool();
}
const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt());
@ -418,20 +464,38 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
QWidget *parent)
: QGroupBox(parent), tabSupervisor(_tabSupervisor), client(_client), type(_type), onlineCount(0)
{
itemDelegate = new UserListItemDelegate(this);
avatarProvider = new UserAvatarProvider(client, this);
cardArtProvider = new UserCardArtProvider(this);
itemDelegate =
new UserListItemDelegate(this, &avatarProvider->cache(), &cardArtProvider->cache(), &cardArtParamsMap);
userContextMenu = new UserContextMenu(tabSupervisor, this);
connect(userContextMenu, &UserContextMenu::openMessageDialog, this, &UserListWidget::openMessageDialog);
userTree = new QTreeWidget;
userTree->setColumnCount(3);
userTree->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
userTree->setColumnCount(4); // 0=display, 1=flag(hidden), 2=name(hidden), 3=privlevel(hidden)
userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
userTree->header()->setMinimumSectionSize(0);
userTree->setHeaderHidden(true);
userTree->setRootIsDecorated(false);
userTree->setIconSize(QSize(20, 18));
userTree->setItemDelegate(itemDelegate);
userTree->setAlternatingRowColors(true);
userTree->hideColumn(1);
userTree->hideColumn(2);
userTree->hideColumn(3);
connect(userTree, &QTreeWidget::itemActivated, this, &UserListWidget::userClicked);
userTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
userTree->header()->setStretchLastSection(true);
connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this,
[this](const QString &) { userTree->viewport()->update(); });
connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this,
[this](const QString &) { userTree->viewport()->update(); });
connect(&SettingsCache::instance(), &SettingsCache::styleUserListChanged, this, &UserListWidget::applyDisplayMode);
applyDisplayMode();
QVBoxLayout *vbox = new QVBoxLayout;
vbox->addWidget(userTree);
@ -441,6 +505,34 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
retranslateUi();
}
void UserListWidget::bind(UserListManager *mgr)
{
manager = mgr;
connect(manager, &UserListManager::listsChanged, this, &UserListWidget::rebuild);
rebuild();
}
void UserListWidget::applyDisplayMode()
{
const bool styled = SettingsCache::instance().getStyleUserList();
if (styled) {
userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
userTree->hideColumn(1);
userTree->hideColumn(2);
userTree->hideColumn(3);
} else {
userTree->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
userTree->showColumn(1);
userTree->showColumn(2);
userTree->hideColumn(3);
}
userTree->viewport()->update();
}
void UserListWidget::retranslateUi()
{
userContextMenu->retranslateUi();
@ -461,9 +553,59 @@ void UserListWidget::retranslateUi()
updateCount();
}
void UserListWidget::rebuild()
{
userTree->clear();
users.clear();
cardArtParamsMap.clear();
onlineCount = 0;
if (!manager) {
return;
}
const QMap<QString, ServerInfo_User> *source = nullptr;
switch (type) {
case AllUsersList:
case RoomList:
source = &manager->getAllUsersList();
break;
case BuddyList:
source = &manager->getBuddyList();
break;
case IgnoreList:
source = &manager->getIgnoreList();
break;
}
for (auto it = source->cbegin(); it != source->cend(); ++it) {
processUserInfo(it.value(), manager->getOnlineUser(it.key()) != nullptr);
}
sortItems();
}
void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)
{
const QString userName = QString::fromStdString(user.name());
// Always update params from the latest ServerInfo_User, whether the
// item is new or existing, so a live server-push refreshes the rendering.
if (user.has_card_art_params()) {
const auto &cap = user.card_art_params();
CardArtParams params;
params.cardName = QString::fromStdString(cap.card_name());
params.marginPctL = cap.margin_pct_l();
params.marginPctR = cap.margin_pct_r();
params.verticalOffset = cap.vertical_offset();
params.zoom = cap.zoom();
cardArtParamsMap.insert(userName, params);
cardArtProvider->requestCardArt(userName, params.cardName);
} else {
cardArtParamsMap.remove(userName); // clear stale params on removal
}
UserListTWI *item = users.value(userName);
if (item) {
item->setUserInfo(user);
@ -475,25 +617,27 @@ void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)
++onlineCount;
}
updateCount();
avatarProvider->requestAvatar(userName);
}
item->setOnline(online);
userTree->viewport()->update();
}
bool UserListWidget::deleteUser(const QString &userName)
{
UserListTWI *twi = users.value(userName);
if (twi) {
users.remove(userName);
userTree->takeTopLevelItem(userTree->indexOfTopLevelItem(twi));
if (twi->data(0, Qt::UserRole + 1).toBool()) {
--onlineCount;
}
delete twi;
updateCount();
return true;
if (!twi) {
return false;
}
return false;
users.remove(userName);
userTree->takeTopLevelItem(userTree->indexOfTopLevelItem(twi));
if (twi->data(0, Qt::UserRole + 1).toBool()) {
--onlineCount;
}
delete twi;
updateCount();
return true;
}
void UserListWidget::setUserOnline(const QString &userName, bool online)
@ -537,5 +681,5 @@ void UserListWidget::showContextMenu(const QPoint &pos, const QModelIndex &index
void UserListWidget::sortItems()
{
userTree->sortItems(1, Qt::AscendingOrder);
userTree->sortItems(0, Qt::AscendingOrder);
}

View file

@ -7,9 +7,16 @@
#ifndef USERLIST_H
#define USERLIST_H
#include "../../cards/card_info_picture_art_crop_widget.h"
#include "user_avatar_provider.h"
#include "user_card_art_provider.h"
#include "user_list_manager.h"
#include "user_list_painter.h"
#include <QComboBox>
#include <QDialog>
#include <QGroupBox>
#include <QQueue>
#include <QStyledItemDelegate>
#include <QTextEdit>
#include <QTreeWidgetItem>
@ -94,12 +101,21 @@ public:
class UserListItemDelegate : public QStyledItemDelegate
{
const QMap<QString, QPixmap> *avatarCache;
const QMap<QString, QPixmap> *cardArtCache;
const QMap<QString, CardArtParams> *cardArtParamsMap;
public:
explicit UserListItemDelegate(QObject *const parent);
explicit UserListItemDelegate(QObject *const parent,
const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap);
bool editorEvent(QEvent *event,
QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index) override;
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};
class UserListTWI : public QTreeWidgetItem
@ -131,6 +147,11 @@ public:
};
private:
UserListManager *manager = nullptr;
UserAvatarProvider *avatarProvider = nullptr;
UserCardArtProvider *cardArtProvider = nullptr;
QMap<QString, CardArtParams> cardArtParamsMap;
QMap<QString, UserListTWI *> users;
TabSupervisor *tabSupervisor;
AbstractClient *client;
@ -155,7 +176,10 @@ public:
AbstractClient *_client,
UserListType _type,
QWidget *parent = nullptr);
void bind(UserListManager *mgr);
void applyDisplayMode();
void retranslateUi();
void rebuild();
void processUserInfo(const ServerInfo_User &user, bool online);
bool deleteUser(const QString &userName);
void setUserOnline(const QString &userName, bool online);